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

[TextField / Labelled] Add character count #709

Merged
merged 1 commit into from Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions UNRELEASED.md
Expand Up @@ -10,6 +10,8 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f

### Enhancements

- `TextField` accepts a `showCharacterCount` prop to enable the display of character count ([#709](https://github.com/Shopify/polaris-react/pull/709))

### Bug fixes

- Fixed vertical misalignment in `Banner.Header`([#870](https://github.com/Shopify/polaris-react/pull/870))
Expand Down
32 changes: 31 additions & 1 deletion src/components/TextField/README.md
Expand Up @@ -497,7 +497,7 @@ class HelpTextExample extends React.Component {

Use as a special form of help text that works best inline.

- Use a prefix for things like currency symbols (e.g. “$”, “¥”, “£”).
- Use a prefix for things like currency symbols (e.g. “\$”, “¥”, “£”).
- Use suffix for things like units of measure (e.g. “in”, “cm”).

```jsx
Expand Down Expand Up @@ -737,6 +737,36 @@ Use to show that a textfield is not available for interaction. Most often used i
<TextField label="Store name" disabled />
```

### Text field with character count

<!-- example-for: web -->

Use to display the current number of characters in a text field. Use in conjunction with max length to display the current remaining number of characters in the text field.

```jsx
class TextFieldExample extends React.Component {
state = {
value: 'Jaded Pixel',
};

handleChange = (value) => {
this.setState({value});
};

render() {
return (
<TextField
label="Store name"
value={this.state.value}
onChange={this.handleChange}
maxLength={20}
showCharacterCount
/>
);
}
}
```

---

## Related components
Expand Down
12 changes: 12 additions & 0 deletions src/components/TextField/TextField.scss
Expand Up @@ -170,6 +170,18 @@ $stacking-order: (
pointer-events: none;
}

.CharacterCount {
@include text-emphasis-subdued;
z-index: z-index(contents, $stacking-order);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the z-index necessary to specify here?

Choose a reason for hiding this comment

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

margin-right: $backdrop-horizontal-spacing;
line-height: control-height();
pointer-events: none;
}

.AlignFieldBottom {
align-self: flex-end;
}

.Spinner {
z-index: z-index(contents, $stacking-order);
display: flex;
Expand Down
50 changes: 46 additions & 4 deletions src/components/TextField/TextField.tsx
Expand Up @@ -7,6 +7,7 @@ import Labelled, {Action, helpTextID, labelID} from '../Labelled';
import Connected from '../Connected';

import {Error, Key} from '../../types';
import {withAppProvider, WithAppProviderProps} from '../AppProvider';
import {Resizer, Spinner} from './components';
import * as styles from './TextField.scss';

Expand Down Expand Up @@ -96,6 +97,8 @@ export interface BaseProps {
ariaActiveDescendant?: string;
/** Indicates what kind of user input completion suggestions are provided */
ariaAutocomplete?: string;
/** Indicates whether or not the character count should be displayed */
showCharacterCount?: boolean;
/** Callback when value is changed */
onChange?(value: string, id: string): void;
/** Callback when input is focused */
Expand All @@ -112,17 +115,19 @@ export type Props = NonMutuallyExclusiveProps &
| {disabled: true}
| {onChange(value: string, id: string): void});

export type CombinedProps = Props & WithAppProviderProps;

const getUniqueID = createUniqueIDFactory('TextField');

export default class TextField extends React.PureComponent<Props, State> {
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
class TextField extends React.PureComponent<CombinedProps, State> {
static getDerivedStateFromProps(nextProps: CombinedProps, prevState: State) {
return {id: nextProps.id || prevState.id};
}

private input: HTMLElement;
private buttonPressTimer: number;

constructor(props: Props) {
constructor(props: CombinedProps) {
super(props);

this.state = {
Expand All @@ -132,7 +137,7 @@ export default class TextField extends React.PureComponent<Props, State> {
};
}

componentDidUpdate({focused}: Props) {
componentDidUpdate({focused}: CombinedProps) {
if (
this.input &&
focused !== this.props.focused &&
Expand Down Expand Up @@ -177,6 +182,8 @@ export default class TextField extends React.PureComponent<Props, State> {
ariaActiveDescendant,
ariaAutocomplete,
ariaControls,
showCharacterCount,
polaris: {intl},
} = this.props;

const {height} = this.state;
Expand Down Expand Up @@ -205,6 +212,35 @@ export default class TextField extends React.PureComponent<Props, State> {
</div>
) : null;

const characterCount = value.length;
const characterCountLabel = intl.translate(
maxLength
? 'Polaris.TextField.characterCountWithMaxLength'
: 'Polaris.TextField.characterCount',
{count: characterCount, limit: maxLength},
);

const characterCountClassName = classNames(
styles.CharacterCount,
multiline && styles.AlignFieldBottom,
);

const characterCountText = !maxLength
? characterCount
: `${characterCount}/${maxLength}`;

const characterCountMarkup = showCharacterCount ? (
<div
id={`${id}CharacterCounter`}
className={characterCountClassName}
aria-label={characterCountLabel}
aria-live="polite"
aria-atomic="true"
>
{characterCountText}
</div>
) : null;

const spinnerMarkup =
type === 'number' && !disabled ? (
<Spinner
Expand Down Expand Up @@ -232,6 +268,9 @@ export default class TextField extends React.PureComponent<Props, State> {
if (helpText) {
describedBy.push(helpTextID(id));
}
if (showCharacterCount) {
describedBy.push(`${id}CharacterCounter`);
}

const labelledBy = [labelID(id)];
if (prefix) {
Expand Down Expand Up @@ -303,6 +342,7 @@ export default class TextField extends React.PureComponent<Props, State> {
{prefixMarkup}
{input}
{suffixMarkup}
{characterCountMarkup}
{spinnerMarkup}
<div className={styles.Backdrop} />
{resizer}
Expand Down Expand Up @@ -414,3 +454,5 @@ function normalizeAutoComplete(autoComplete?: boolean) {
}
return autoComplete ? 'on' : 'off';
}

export default withAppProvider<Props>()(TextField);
35 changes: 35 additions & 0 deletions src/components/TextField/tests/TextField.test.tsx
Expand Up @@ -305,6 +305,41 @@ describe('<TextField />', () => {
});
});

describe('characterCount', () => {
it('displays number of characters entered in input field', () => {
const textField = mountWithAppProvider(
<TextField
value="test"
showCharacterCount
label="TextField"
id="MyField"
onChange={noop}
/>,
);

const characterCount = textField.find('#MyFieldCharacterCounter');

expect(characterCount.text()).toBe('4');
});

it('displays remaining characters as fraction in input field with maxLength', () => {
const textField = mountWithAppProvider(
<TextField
value="test"
maxLength={10}
showCharacterCount
label="TextField"
id="MyField"
onChange={noop}
/>,
);

const characterCount = textField.find('#MyFieldCharacterCounter');

expect(characterCount.text()).toBe('4/10');
});
});

describe('type', () => {
it('sets the type on the input', () => {
const type = shallowWithAppProvider(
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en.json
Expand Up @@ -192,6 +192,11 @@

"Tag": {
"ariaLabel": "Remove {children}"
},

"TextField": {
"characterCount": "{count} characters",
"characterCountWithMaxLength": "{count} of {limit} characters used"
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

}
}
}