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 8 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
100 changes: 2 additions & 98 deletions packages/mdc-textfield/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,91 +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
by HTML5's form validation API.

```html
<div class="mdc-text-field">
<input type="password" id="pw" class="mdc-text-field__input" required minlength=8>
<label for="pw" class="mdc-text-field__label">Password</label>
<div class="mdc-text-field__bottom-line"></div>
</div>
```

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
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 +182,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,13 +281,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.disabled

Boolean. Proxies to the foundation's `isDisabled/setDisabled` methods when retrieved/set
Expand Down Expand Up @@ -404,17 +312,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 |
| 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
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 @@ -182,6 +183,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
56 changes: 8 additions & 48 deletions packages/mdc-textfield/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,13 @@ class MDCTextFieldFoundation extends MDCFoundation {
registerTextFieldInteractionHandler: () => {},
deregisterTextFieldInteractionHandler: () => {},
notifyIconAction: () => {},
addClassToHelperText: () => {},
removeClassFromHelperText: () => {},
helperTextHasClass: () => false,
registerInputInteractionHandler: () => {},
deregisterInputInteractionHandler: () => {},
registerBottomLineEventHandler: () => {},
deregisterBottomLineEventHandler: () => {},
setHelperTextAttr: () => {},
removeHelperTextAttr: () => {},
getNativeInput: () => {},
getBottomLineFoundation: () => {},
getHelperTextFoundation: () => {},
});
}

Expand Down Expand Up @@ -160,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.show();
}
this.isFocused_ = true;
}

Expand All @@ -186,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 @@ -243,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.update(isValid);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update method on helperText is not a clear description. Update what? Is this the ONLY way to update a helper text? You can also update it with new content right?

I think a better method name is setValidity

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done

}
this.hideHelperText_();
}

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

/**
Expand Down
133 changes: 133 additions & 0 deletions packages/mdc-textfield/helper-text/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<!--docs:
title: "Text Field Helper Text"
layout: detail
section: components
excerpt: "The helper text provides supplemental information and/or validation messages to users"
iconId: text_field
path: /catalog/input-controls/text-fields/helper-text/
-->

# Text Field Helper Text

The helper text provides supplemental information and/or validation messages to users. It appears on input field focus and disappears on input field blur by default, or it can be persistent.

## Design & API Documentation

<ul class="icon-list">
<li class="icon-list-item icon-list-item--spec">
<a href="https://material.io/guidelines/components/text-fields.html#text-fields-layout">Material Design guidelines: Text Fields Layout</a>
</li>
</ul>


## Usage

### 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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the part of this sentence that talks about validation messages...just move it within the README to be closer to where you actually talk about validation messages. Just adding (covered below) doesn't help users figure out what you are talking about.


```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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a code snippet example demonstrating the id concept?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code snippet would be exactly the same as the the first example snippet on the page. Should I remove the id/aria-controls attributes from the first snippet since they are technically optional, or let it be duplicate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should remove it from the first snippet...but open to suggestions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me

`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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats "it"? Is it MDCTextField?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The browser 👍

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't shorten to a11y within the context of a README. Write out accessibility

be done programmatically, which is described below.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm its really unclear to me as a reader exactly what can be don programmatically...and it is not obvious the programmatic part is also the "MDCTextFieldHelperText API" section. Can you take another pass on this paragraph?


### Validation

MDC TextField provides validity styling by using the `:invalid` and `:required` attributes provided
by HTML5's form validation API.

```html
<div class="mdc-text-field">
<input type="password" id="pw" class="mdc-text-field__input" required minlength=8>
<label for="pw" class="mdc-text-field__label">Password</label>
<div class="mdc-text-field__bottom-line"></div>
</div>
```

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the MDCTextField.valid field....not variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops some of this section isn't actually related to helper text....moved it back to the parent README and kept only the parts relevant to helper text.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: drop the word easily. Who are we to claim this is easy?? I mean we try and make it easy, but user's get to decide if something was easy or not. We should only document how to do something, not the adjective we apply to that process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Thanks for catching, I think this is so important.

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>
```

#### MDCTextFieldHelperText API

##### MDCTextFieldHelperText.foundation

MDCTextFieldHelperTextFoundation. This allows the parent MDCTextField component to access the public methods on the MDCTextFieldHelperTextFoundation class.

### Using the foundation class

Method Signature | Description
--- | ---
addClass(className: string) => void | Adds a class to the helper text element
removeClass(className: string) => void | Removes a class from the helper text element
hasClass(className: string) => boolean | Returns true if classname exists for the helper text element
setAttr(attr: string, value: string) => void | Sets an attribute with a given value on the helper text element
removeAttr(attr: string) => void | Removes an attribute on the helper text element |

#### The full foundation API

##### MDCTextFieldHelperTextFoundation.show()

Makes the helper text visible to screen readers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm its really not obvious that show only makes it visible to screen readers, and that making helper text visible to users in general is handle in some other code. Whats a better method name to make that more clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about showToScreenReader? some other options: makeVisible, makeVisibleToScreenReader, announce, announceToScreenReader


##### MDCTextFieldHelperTextFoundation.update(inputIsValid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above about using setValidity instead of update


Updates the state of the helper text based on input validity.
Loading