Skip to content

Commit

Permalink
Add character count
Browse files Browse the repository at this point in the history
  • Loading branch information
Justin Fangrad authored and emarchak committed Jan 29, 2019
1 parent a66c0db commit 447bd0b
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 5 deletions.
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);
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"
}
}
}

0 comments on commit 447bd0b

Please sign in to comment.