Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(text-field): Split out helper text as a subelement #1611

Merged
merged 17 commits into from
Nov 27, 2017
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 7 additions & 87 deletions packages/mdc-textfield/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,54 +100,6 @@ since it won't be added until that JS runs, adding it manually will prevent an i
</div>
```

### Using helper text

MDC Text Fields can include helper text that is useful for providing supplemental
information to users, as well for validation messages (covered below).

```html
<div class="mdc-text-field">
<input type="text" id="username" class="mdc-text-field__input" aria-controls="username-helper-text">
<label for="username" class="mdc-text-field__label">Username</label>
<div class="mdc-text-field__bottom-line"></div>
</div>
<p id="username-helper-text" class="mdc-text-field-helper-text" aria-hidden="true">
This will be displayed on your public profile
</p>
```

Helper text appears on input field focus and disappears on input field blur by default when using
the text-field JS component.

#### Persistent helper text

If you'd like the helper text to always be visible, add the
`mdc-text-field-helper-text--persistent` modifier class to the element.

```html
<div class="mdc-text-field">
<input type="email" id="email" class="mdc-text-field__input">
<label for="email" class="mdc-text-field__label">Email address</label>
<div class="mdc-text-field__bottom-line"></div>
</div>
<p class="mdc-text-field-helper-text mdc-text-field-helper-text--persistent">
We will <em>never</em> share your email address with third parties
</p>
```

#### Helper text and accessibility

Note that in every example where the helper text is dependent on the state of the input element, we
assign an id to the `mdc-text-field-helper-text` element and set that id to the value of the
`aria-controls` attribute on the input element. We recommend doing this as well as it will help
indicate to assistive devices that the display of the helper text is dependent on the interaction with
the input element.

When using our vanilla JS component, if it sees that the input element has an `aria-controls`
attribute, it will look for an element with the id specified and treat it as the text field's help
text element, taking care of adding/removing `aria-hidden` and other a11y attributes. This can also
be done programmatically, which is described below.

### Validation

MDC TextField provides validity styling by using the `:invalid` and `:required` attributes provided
Expand All @@ -162,29 +114,9 @@ by HTML5's form validation API.
```

By default an input's validity is checked via `checkValidity()` on blur, and the styles are updated
accordingly. Set the MDCTextField.valid variable to set the input's validity explicitly. MDC TextField
accordingly. Set the MDCTextField.valid field to set the input's validity explicitly. MDC TextField
automatically appends an asterisk to the label text if the required attribute is set.

Helper text can be used to provide additional validation messages. Use
`mdc-text-field-helper-text--validation-msg` to provide styles for using the helper text as a validation
message. This can be easily combined with `mdc-text-field-helper-text--persistent` to provide a robust
UX for client-side form field validation.

```html
<div class="mdc-text-field">
<input required minlength=8 type="password" class="mdc-text-field__input" id="pw"
aria-controls="pw-validation-msg">
<label for="pw" class="mdc-text-field__label">Choose password</label>
<div class="mdc-text-field__bottom-line"></div>
</div>
<p class="mdc-text-field-helper-text
mdc-text-field-helper-text--persistent
mdc-text-field-helper-text--validation-msg"
id="pw-validation-msg">
Must be at least 8 characters long
</p>
```

### Leading and Trailing Icons
Leading and trailing icons can be added to MDC Text Fields as visual indicators
as well as interaction targets. To do so, add the relevant classes
Expand Down Expand Up @@ -267,7 +199,7 @@ behave normally.
</div>
```

Note that Text field boxes support all of the same features as normal text-fields, including help
Note that Text field boxes support all of the same features as normal text-fields, including helper
text, validation, and dense UI.

#### CSS-only text field boxes
Expand Down Expand Up @@ -366,17 +298,6 @@ By default the ripple factory simply calls `new MDCRipple(el)` and returns the r
Similar to regular DOM elements, the `MDCTextField` functionality is exposed through accessor
methods.

##### MDCTextField.helperTextElement

HTMLLabelElement. This is a normal property (non-accessor) that holds a reference to the element
being used as the text field's "helper text". It defaults to `null`. If the text field's input element
contains an `aria-controls` attribute on instantiation of the component, it will look for an element
with the corresponding id within the document and automatically assign it to this property.

##### MDCTextField.helperTextContent

String setter. Proxies to the foundation's `setHelperTextContent` method when set.

##### MDCTextField.disabled

Boolean. Proxies to the foundation's `isDisabled/setDisabled` methods when retrieved/set
Expand Down Expand Up @@ -408,18 +329,13 @@ complicated.
| notifyIconAction() => void | Emits a custom event "MDCTextField:icon" denoting a user has clicked the icon |
| addClassToBottomLine(className: string) => void | Adds a class to the bottom line element |
| removeClassFromBottomLine(className: string) => void | Removes a class from the bottom line element |
| addClassToHelperText(className: string) => void | Adds a class to the helper text element. Note that in our code we check for whether or not we have a helper text element and if we don't, we simply return. |
| removeClassFromHelperText(className: string) => void | Removes a class from the helper text element. |
| helperTextHasClass(className: string) => boolean | Returns whether or not the helper text element contains the current class |
| registerInputInteractionHandler(evtType: string, handler: EventListener) => void | Registers an event listener on the native input element for a given event |
| deregisterInputInteractionHandler(evtType: string, handler: EventListener) => void | Deregisters an event listener on the native input element for a given event |
| registerBottomLineEventHandler(evtType: string, handler: EventListener) => void | Registers an event listener on the bottom line element for a given event |
| deregisterBottomLineEventHandler(evtType: string, handler: EventListener) => void | Deregisters an event listener on the bottom line element for a given event |
| setHelperTextAttr(name: string, value: string) => void | Sets an attribute with a given value on the helper text element |
| removeHelperTextAttr(name: string) => void | Removes an attribute from the helper text element |
| setHelperTextContent(content: string) => void | Sets the content of the helper text element |
| getNativeInput() => {value: string, disabled: boolean, badInput: boolean, checkValidity: () => boolean}? | Returns an object representing the native text input element, with a similar API shape. The object returned should include the `value`, `disabled` and `badInput` properties, as well as the `checkValidity()` function. We _never_ alter the value within our code, however we _do_ update the disabled property, so if you choose to duck-type the return value for this method in your implementation it's important to keep this in mind. Also note that this method can return null, which the foundation will handle gracefully. |
| getBottomLineFoundation() => MDCTextFieldBottomLineFoundation | Returns the instance of the bottom line element's foundation |
| getHelperTextFoundation() => MDCTextFieldHelperTextFoundation | Returns the instance of the helper text element's foundation |

#### The full foundation API

Expand Down Expand Up @@ -457,6 +373,10 @@ event with clientX/clientY properties.
Handles the end of the bottom line animation, performing actions that must wait for animations to
finish. Expects a transition-end event.

##### MDCTextField.helperTextContent

Sets the content of the helper text, if it exists.

### Theming

MDC TextField components use the configured theme's primary color for its underline and label text
Expand Down
8 changes: 8 additions & 0 deletions packages/mdc-textfield/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

/* eslint-disable no-unused-vars */
import MDCTextFieldBottomLineFoundation from './bottom-line/foundation';
import MDCTextFieldHelperTextFoundation from './helper-text/foundation';

/* eslint no-unused-vars: [2, {"args": "none"}] */

Expand Down Expand Up @@ -188,6 +189,13 @@ class MDCTextFieldAdapter {
* @return {?MDCTextFieldBottomLineFoundation}
*/
getBottomLineFoundation() {}

/**
* Returns the foundation for the helper text element. Returns undefined if
* there is no helper text element.
* @return {?MDCTextFieldHelperTextFoundation}
*/
getHelperTextFoundation() {}
}

export {MDCTextFieldAdapter, NativeInputType};
5 changes: 1 addition & 4 deletions packages/mdc-textfield/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

/** @enum {string} */
const strings = {
ARIA_HIDDEN: 'aria-hidden',
ROLE: 'role',
ARIA_CONTROLS: 'aria-controls',
INPUT_SELECTOR: '.mdc-text-field__input',
LABEL_SELECTOR: '.mdc-text-field__label',
ICON_SELECTOR: '.mdc-text-field__icon',
Expand All @@ -33,8 +32,6 @@ const cssClasses = {
DISABLED: 'mdc-text-field--disabled',
FOCUSED: 'mdc-text-field--focused',
INVALID: 'mdc-text-field--invalid',
HELPER_TEXT_PERSISTENT: 'mdc-text-field-helper-text--persistent',
HELPER_TEXT_VALIDATION_MSG: 'mdc-text-field-helper-text--validation-msg',
LABEL_FLOAT_ABOVE: 'mdc-text-field__label--float-above',
LABEL_SHAKE: 'mdc-text-field__label--shake',
BOX: 'mdc-text-field--box',
Expand Down
64 changes: 13 additions & 51 deletions packages/mdc-textfield/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,13 @@ class MDCTextFieldFoundation extends MDCFoundation {
registerTextFieldInteractionHandler: () => {},
deregisterTextFieldInteractionHandler: () => {},
notifyIconAction: () => {},
addClassToHelperText: () => {},
removeClassFromHelperText: () => {},
helperTextHasClass: () => false,
registerInputInteractionHandler: () => {},
deregisterInputInteractionHandler: () => {},
registerBottomLineEventHandler: () => {},
deregisterBottomLineEventHandler: () => {},
setHelperTextAttr: () => {},
removeHelperTextAttr: () => {},
setHelperTextContent: () => {},
getNativeInput: () => {},
getBottomLineFoundation: () => {},
getHelperTextFoundation: () => {},
});
}

Expand Down Expand Up @@ -161,7 +156,10 @@ class MDCTextFieldFoundation extends MDCFoundation {
}
this.adapter_.addClassToLabel(LABEL_FLOAT_ABOVE);
this.adapter_.removeClassFromLabel(LABEL_SHAKE);
this.showHelperText_();
const helperText = this.adapter_.getHelperTextFoundation();
if (helperText) {
helperText.showToScreenReader();
}
this.isFocused_ = true;
}

Expand All @@ -187,15 +185,6 @@ class MDCTextFieldFoundation extends MDCFoundation {
}
}

/**
* Makes the helper text visible to screen readers.
* @private
*/
showHelperText_() {
const {ARIA_HIDDEN} = MDCTextFieldFoundation.strings;
this.adapter_.removeHelperTextAttr(ARIA_HIDDEN);
}

/**
* Handles when bottom line animation ends, performing actions that must wait
* for animations to finish.
Expand Down Expand Up @@ -244,40 +233,10 @@ class MDCTextFieldFoundation extends MDCFoundation {
this.adapter_.addClassToLabel(LABEL_SHAKE);
this.adapter_.addClass(INVALID);
}
this.updateHelperText_(isValid);
}

/**
* Updates the state of the Text Field's helper text based on validity and
* the Text Field's options.
* @param {boolean} isValid
*/
updateHelperText_(isValid) {
const {HELPER_TEXT_PERSISTENT, HELPER_TEXT_VALIDATION_MSG} = MDCTextFieldFoundation.cssClasses;
const {ROLE} = MDCTextFieldFoundation.strings;
const helperTextIsPersistent = this.adapter_.helperTextHasClass(HELPER_TEXT_PERSISTENT);
const helperTextIsValidationMsg = this.adapter_.helperTextHasClass(HELPER_TEXT_VALIDATION_MSG);
const validationMsgNeedsDisplay = helperTextIsValidationMsg && !isValid;

if (validationMsgNeedsDisplay) {
this.adapter_.setHelperTextAttr(ROLE, 'alert');
} else {
this.adapter_.removeHelperTextAttr(ROLE);
}

if (helperTextIsPersistent || validationMsgNeedsDisplay) {
return;
const helperText = this.adapter_.getHelperTextFoundation();
if (helperText) {
helperText.setValidity(isValid);
}
this.hideHelperText_();
}

/**
* Hides the helper text from screen readers.
* @private
*/
hideHelperText_() {
const {ARIA_HIDDEN} = MDCTextFieldFoundation.strings;
this.adapter_.setHelperTextAttr(ARIA_HIDDEN, 'true');
}

/**
Expand Down Expand Up @@ -313,10 +272,13 @@ class MDCTextFieldFoundation extends MDCFoundation {
}

/**
* @param {string} content Sets the content of the helper text field
* @param {string} content Sets the content of the helper text.
*/
setHelperTextContent(content) {
this.adapter_.setHelperTextContent(content);
const helperText = this.adapter_.getHelperTextFoundation();
if (helperText) {
helperText.setContent(content);
}
}

/**
Expand Down
Loading