Skip to content
This repository was archived by the owner on Jan 14, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c3f9439

Browse files
태재영Matt Goo
authored andcommittedJun 11, 2019
feat(text-field): character counter (#861)
1 parent 407de75 commit c3f9439

File tree

16 files changed

+419
-54
lines changed

16 files changed

+419
-54
lines changed
 

‎packages/list/ListItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import {MDCListFoundation} from '@material/list/foundation';
2929
import {ListItemContext, ListItemContextShape} from './index';
3030

3131
export interface ListItemProps<T extends HTMLElement = HTMLElement>
32-
extends React.HTMLProps<T>, ListItemContextShape, InjectedProps<T> {
32+
extends React.HTMLProps<T>,
33+
ListItemContextShape,
34+
InjectedProps<T> {
3335
checkboxList?: boolean;
3436
radioList?: boolean;
3537
tag?: string;

‎packages/menu/MenuListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class MenuListItem<T extends HTMLElement = HTMLElement> extends React.Component<
3636
const {
3737
role = 'menuitem',
3838
children,
39-
/* eslint-disable no-unused-vars */
39+
/* eslint-disable @typescript-eslint/no-unused-vars */
4040
computeBoundingRect,
41-
/* eslint-disable no-unused-vars */
41+
/* eslint-disable @typescript-eslint/no-unused-vars */
4242
...otherProps
4343
} = this.props;
4444

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# React Text Field Character Counter
2+
3+
MDC React Text Field Character Counter is a React Component which uses [MDC Text Field Character Counter](https://github.com/material-components/material-components-web/tree/master/packages/mdc-textfield/character-counter)'s Sass and Foundational JavaScript logic.
4+
5+
## Usage
6+
7+
```js
8+
import CharacterCounter from '@material/react-text-field/character-counter/index.js';
9+
10+
const MyComponent = () => {
11+
return (
12+
<CharacterCounter />
13+
);
14+
}
15+
```
16+
17+
## Props
18+
19+
Prop Name | Type | Description
20+
--- | --- | ---
21+
className | String | CSS classes for element.
22+
template | String | You can set custom template. [See below](#custom-template)
23+
24+
## Custom Template
25+
26+
CharacterCounter provides customization with the `template` prop in CharacterCounter.
27+
The `template` prop accepts the `${count}` and `${maxLength}` arguments.
28+
The default template is `${count} / ${maxLength}`, so it appears `0 / 140`.
29+
If you set template as `${count} : ${maxLength}`, it appears as `0 : 140`.
30+
31+
### Sample
32+
33+
``` js
34+
import React from 'react';
35+
import TextField, {CharacterCounter, Input} from '@material/react-text-field';
36+
37+
class MyApp extends React.Component {
38+
state = {value: 'Happy Coding!'};
39+
40+
render() {
41+
return (
42+
<TextField characterCounter={<CharacterCounter template='${count} : ${maxLength}' />}>
43+
<Input
44+
maxLength={140}
45+
value={this.state.value}
46+
onChange={(e) => this.setState({value: e.target.value})}
47+
/>
48+
</TextField>
49+
);
50+
}
51+
}
52+
```
53+
54+
## Sass Mixins
55+
56+
Sass mixins may be available to customize various aspects of the Components. Please refer to the
57+
MDC Web repository for more information on what mixins are available, and how to use them.
58+
59+
[Advanced Sass Mixins](https://github.com/material-components/material-components-web/tree/master/packages/mdc-textfield/character-counter#sass-mixins)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2019 Google, Inc.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
23+
@import "@material/textfield/character-counter/mdc-text-field-character-counter";
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2019 Google, Inc.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
import React from 'react';
23+
import classnames from 'classnames';
24+
import {MDCTextFieldCharacterCounterAdapter} from '@material/textfield/character-counter/adapter';
25+
import {MDCTextFieldCharacterCounterFoundation} from '@material/textfield/character-counter/foundation';
26+
27+
const cssClasses = MDCTextFieldCharacterCounterFoundation.cssClasses;
28+
29+
const TEMPLATE = {
30+
COUNT: '${count}',
31+
MAX_LENGTH: '${maxLength}',
32+
};
33+
34+
export interface CharacterCounterProps extends React.HTMLProps<HTMLDivElement> {
35+
count?: number;
36+
maxLength?: number;
37+
template?: string;
38+
}
39+
40+
export default class CharacterCounter extends React.Component<
41+
CharacterCounterProps
42+
> {
43+
foundation = new MDCTextFieldCharacterCounterFoundation(this.adapter);
44+
45+
componentWillUnmount() {
46+
this.foundation.destroy();
47+
}
48+
49+
get adapter(): MDCTextFieldCharacterCounterAdapter {
50+
return {
51+
// Please manage content through JSX
52+
setContent: () => undefined,
53+
};
54+
}
55+
56+
renderTemplate(template: string) {
57+
const {count = 0, maxLength = 0} = this.props;
58+
59+
return template
60+
.replace(TEMPLATE.COUNT, count.toString())
61+
.replace(TEMPLATE.MAX_LENGTH, maxLength.toString());
62+
}
63+
64+
get classes() {
65+
return classnames(cssClasses.ROOT, this.props.className);
66+
}
67+
68+
get otherProps() {
69+
const {
70+
/* eslint-disable @typescript-eslint/no-unused-vars */
71+
className,
72+
count,
73+
maxLength,
74+
template,
75+
/* eslint-disable @typescript-eslint/no-unused-vars */
76+
...otherProps
77+
} = this.props;
78+
79+
return otherProps;
80+
}
81+
82+
render() {
83+
const {template} = this.props;
84+
85+
return (
86+
<div className={this.classes} {...this.otherProps}>
87+
{this.renderTemplate(
88+
template ? template : `${TEMPLATE.COUNT} / ${TEMPLATE.MAX_LENGTH}`
89+
)}
90+
</div>
91+
);
92+
}
93+
}

‎packages/text-field/index.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ import {
3131
} from '@material/textfield/adapter';
3232
import {MDCTextFieldFoundation} from '@material/textfield/foundation';
3333
import Input, {InputProps} from './Input';
34-
import Icon, {IconProps} from './icon/index';
35-
import HelperText, {HelperTextProps} from './helper-text/index';
34+
import Icon, {IconProps} from './icon';
35+
import HelperText, {HelperTextProps} from './helper-text';
36+
import CharacterCounter, {CharacterCounterProps} from './character-counter';
3637
import FloatingLabel from '@material/react-floating-label';
3738
import LineRipple from '@material/react-line-ripple';
3839
import NotchedOutline from '@material/react-notched-outline';
@@ -48,7 +49,7 @@ export interface Props<T extends HTMLElement = HTMLInputElement> {
4849
floatingLabelClassName?: string;
4950
fullWidth?: boolean;
5051
helperText?: React.ReactElement<HelperTextProps>;
51-
characterCounter?: React.ReactElement<any>;
52+
characterCounter?: React.ReactElement<CharacterCounterProps>;
5253
label?: React.ReactNode;
5354
leadingIcon?: React.ReactElement<React.HTMLProps<HTMLOrSVGElement>>;
5455
lineRippleClassName?: string;
@@ -82,6 +83,7 @@ interface TextFieldState {
8283
class TextField<
8384
T extends HTMLElement = HTMLInputElement
8485
> extends React.Component<TextFieldProps<T>, TextFieldState> {
86+
textFieldElement: React.RefObject<HTMLDivElement> = React.createRef();
8587
floatingLabelElement: React.RefObject<FloatingLabel> = React.createRef();
8688
inputComponent_: null | Input<T> = null;
8789

@@ -175,6 +177,7 @@ class TextField<
175177
floatingLabelClassName,
176178
fullWidth,
177179
helperText,
180+
characterCounter,
178181
label,
179182
leadingIcon,
180183
lineRippleClassName,
@@ -278,10 +281,11 @@ class TextField<
278281
};
279282
}
280283

281-
inputProps(child: React.ReactElement<InputProps<T>>) {
284+
get inputProps() {
282285
// ref does exist on React.ReactElement<InputProps<T>>
283286
// @ts-ignore
284-
const {props} = child;
287+
const {props} = React.Children.only(this.props.children);
288+
285289
return Object.assign({}, props, {
286290
foundation: this.state.foundation,
287291
handleFocusChange: (isFocused: boolean) => {
@@ -300,6 +304,14 @@ class TextField<
300304
});
301305
}
302306

307+
get characterCounterProps() {
308+
const {value, maxLength} = this.inputProps;
309+
return {
310+
count: value ? value.length : 0,
311+
maxLength: maxLength ? parseInt(maxLength) : 0,
312+
};
313+
}
314+
303315
/**
304316
* render methods
305317
*/
@@ -323,11 +335,15 @@ class TextField<
323335
className={this.classes}
324336
onClick={() => foundation!.handleTextFieldInteraction()}
325337
onKeyDown={() => foundation!.handleTextFieldInteraction()}
338+
ref={this.textFieldElement}
326339
key='text-field-container'
327340
>
328341
{leadingIcon
329342
? this.renderIcon(leadingIcon, onLeadingIconSelect)
330343
: null}
344+
{textarea &&
345+
characterCounter &&
346+
this.renderCharacterCounter(characterCounter)}
331347
{this.renderInput()}
332348
{this.notchedOutlineAdapter.hasOutline() ? (
333349
this.renderNotchedOutline()
@@ -352,8 +368,7 @@ class TextField<
352368
const child: React.ReactElement<InputProps<T>> = React.Children.only(
353369
this.props.children
354370
);
355-
const props = this.inputProps(child);
356-
return React.cloneElement(child, props);
371+
return React.cloneElement(child, this.inputProps);
357372
}
358373

359374
renderLabel() {
@@ -402,12 +417,14 @@ class TextField<
402417

403418
renderHelperLine(
404419
helperText?: React.ReactElement<HelperTextProps>,
405-
characterCounter?: React.ReactElement<any>
420+
characterCounter?: React.ReactElement<CharacterCounterProps>
406421
) {
407422
return (
408423
<div className={cssClasses.HELPER_LINE}>
409424
{helperText && this.renderHelperText(helperText)}
410-
{characterCounter}
425+
{characterCounter &&
426+
!this.props.textarea &&
427+
this.renderCharacterCounter(characterCounter)}
411428
</div>
412429
);
413430
}
@@ -436,7 +453,25 @@ class TextField<
436453
</Icon>
437454
);
438455
}
456+
457+
renderCharacterCounter(
458+
characterCounter: React.ReactElement<CharacterCounterProps>
459+
) {
460+
return React.cloneElement(
461+
characterCounter,
462+
Object.assign(this.characterCounterProps, characterCounter.props)
463+
);
464+
}
439465
}
440466

441-
export {Icon, HelperText, Input, IconProps, HelperTextProps, InputProps};
467+
export {
468+
Icon,
469+
HelperText,
470+
CharacterCounter,
471+
Input,
472+
IconProps,
473+
HelperTextProps,
474+
CharacterCounterProps,
475+
InputProps,
476+
};
442477
export default TextField;

‎test/screenshot/golden.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"tab-bar": "6c28ec268b2baf308459e7df9d7471fb7907b6473240b9a28a81be54a335f932",
2323
"tab-indicator": "7ce7ce8fd50301c67d7ebfb0ba953208260ce2881bee0c7e640c46bf60dc90b6",
2424
"tab-scroller": "468866dd0c222b36b55485ab44a5760133a4ddfb2a6cf81e6ae4672d7e02a447",
25+
"text-field/character-counter": "b6c744bd58b76dd7d3794fa84dae98e44a612b11f7e6dab895e91aceae8aba73",
2526
"text-field/helper-text": "59604d0f39e0846fc97aae7573d317dded215282a677e4641c5e33426e3a2a1e",
2627
"text-field/icon": "0bbc8c762d27071e55983e5742548d166864b6fcebc0b9f1e413523fb93b7075",
2728
"text-field/textArea": "dde78e3f154a8b910a989f8ce96e320e7ad2b3e199e6e7a81034174c598cbd9d",

‎test/screenshot/screenshot-test-urls.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const urls = [
2525
'tab-bar',
2626
'tab-indicator',
2727
'tab-scroller',
28+
'text-field/character-counter',
2829
'text-field/helper-text',
2930
'text-field/icon',
3031
'typography',

‎test/screenshot/text-field/TestTextField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class TestField extends React.Component<TestFieldProps, TestFieldState> {
5454
required={required}
5555
disabled={disabled}
5656
onChange={this.onChange}
57+
maxLength={140}
5758
/>
5859
</TextField>
5960
</div>

0 commit comments

Comments
 (0)
This repository has been archived.