Skip to content
This repository was archived by the owner on Jul 29, 2025. It is now read-only.

Commit 632896a

Browse files
mgr34Matt Goo
authored andcommitted
fix(text field): adds missing adapter method notifyIconAction on Icon (#600)
1 parent bdf2e9b commit 632896a

File tree

10 files changed

+195
-18
lines changed

10 files changed

+195
-18
lines changed

packages/text-field/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ React Text Field accepts one child element which is the input element. For ease
2929
```js
3030
import React from 'react';
3131
import TextField, {HelperText, Input} from '@material/react-text-field';
32+
import MaterialIcon from from '@material/react-material-icon';
3233

3334
class MyApp extends React.Component {
3435
state = {value: 'Woof'};
@@ -39,10 +40,11 @@ class MyApp extends React.Component {
3940
<TextField
4041
label='Dog'
4142
helperText={<HelperText>Help Me!</HelperText>}
42-
>
43-
<Input
44-
value={this.state.value}
45-
onChange={(e) => this.setState({value: e.target.value})}/>
43+
onTrailingIconSelect={() => this.setState({value: ''})}
44+
trailingIcon={<MaterialIcon role="button" icon="delete"/>}
45+
><Input
46+
value={this.state.value}
47+
onChange={(e) => this.setState({value: e.target.value})} />
4648
</TextField>
4749
</div>
4850
);
@@ -66,6 +68,8 @@ label | String | Mandatory. Label text that appears as the floating label or pla
6668
leadingIcon | Element | An icon element that appears as the leading icon.
6769
lineRippleClassName | String | An optional class added to the line ripple element.
6870
notchedOutlineClassName | String | An optional class added to the notched outline element.
71+
onLeadingIconSelect | Function | Optional callback fired on interaction with `leadingIcon`.
72+
onTrailingIconSelect | Function | Optional callback fired on interaction with `trailingIcon`.
6973
outlined | Boolean | Enables outlined variant.
7074
textarea | Boolean | Enables textarea variant.
7175
trailingIcon | Element | An icon element that appears as the trailing icon.

packages/text-field/icon/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Prop Name | Type | Description
2222
--- | --- | ---
2323
disabled | Boolean | Toggles the disabled state of the icon.
2424
children | Element | Required. Expects a single child icon element.
25+
onSelect | Function() => void | Optional callback for user interaction with icon
26+
> Notes: `onSelect` fired on click event and "Enter key" keydown event.
27+
> `onSelect` will add tabindex of 0 if tabindex is not previously added to icon
2528
2629
## Icon
2730

packages/text-field/icon/index.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ import * as classnames from 'classnames';
2525
import {MDCTextFieldIconFoundation} from '@material/textfield/dist/mdc.textfield';
2626

2727
export interface IconProps extends React.HTMLProps<HTMLOrSVGElement> {
28-
disabled: boolean;
28+
disabled?: boolean;
2929
children: React.ReactElement<React.HTMLProps<HTMLOrSVGElement>>;
30+
onSelect?: () => void;
3031
};
3132

3233
interface IconState {
@@ -47,12 +48,11 @@ export default class Icon extends React.Component<
4748
constructor(props: IconProps) {
4849
super(props);
4950
const {
50-
tabIndex: tabindex, // note that foundation.js alters tabindex not tabIndex
5151
role,
5252
} = props.children.props;
5353

5454
this.state = {
55-
tabindex,
55+
tabindex: this.tabindex,
5656
role,
5757
};
5858
}
@@ -69,6 +69,10 @@ export default class Icon extends React.Component<
6969
if (this.props.disabled !== prevProps.disabled) {
7070
this.foundation_.setDisabled(this.props.disabled);
7171
}
72+
73+
if (this.props.onSelect !== prevProps.onSelect) {
74+
this.setState({tabindex: this.tabindex});
75+
}
7276
}
7377

7478
componentWillUnmount() {
@@ -77,6 +81,16 @@ export default class Icon extends React.Component<
7781
}
7882
}
7983

84+
get tabindex() {
85+
// if tabIndex is not set onSelect will never fire.
86+
// note that foundation.js alters tabindex not tabIndex
87+
if (typeof this.props.children.props.tabIndex === 'number') {
88+
return this.props.children.props.tabIndex;
89+
}
90+
91+
return this.props.onSelect ? 0 : -1;
92+
}
93+
8094
get adapter() {
8195
return {
8296
// need toString() call when tabindex === 0.
@@ -94,15 +108,27 @@ export default class Icon extends React.Component<
94108
removeAttr: (attr: keyof IconState) => (
95109
this.setState((prevState) => ({...prevState, [attr]: null}))
96110
),
111+
notifyIconAction: () => ( this.props.onSelect
112+
? this.props.onSelect()
113+
: null
114+
),
97115
};
98116
}
99117

118+
handleClick = (e: React.MouseEvent<HTMLElement>) =>
119+
this.foundation_.handleInteraction(e);
120+
121+
handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) =>
122+
this.foundation_.handleInteraction(e)
123+
100124
addIconAttrsToChildren = () => {
101125
const {tabindex: tabIndex, role} = this.state;
102126
const child = React.Children.only(this.props.children);
103127
const className = classnames('mdc-text-field__icon', child.props.className);
104128
const props = Object.assign({}, child.props, {
105129
className,
130+
onClick: this.handleClick,
131+
onKeyDown: this.handleKeyDown,
106132
tabIndex,
107133
role,
108134
});

packages/text-field/index.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export interface Props<T> {
4545
lineRippleClassName: string;
4646
notchedOutlineClassName: string;
4747
outlined: boolean;
48+
onLeadingIconSelect?: () => void;
49+
onTrailingIconSelect?: () => void;
4850
textarea: boolean;
4951
trailingIcon?: React.ReactElement<React.HTMLProps<HTMLOrSVGElement>>;
5052
};
@@ -172,6 +174,8 @@ class TextField<T extends {}> extends React.Component<TextFieldProps<T>, TextFie
172174
leadingIcon,
173175
lineRippleClassName,
174176
notchedOutlineClassName,
177+
onLeadingIconSelect,
178+
onTrailingIconSelect,
175179
outlined,
176180
textarea,
177181
trailingIcon,
@@ -309,6 +313,8 @@ class TextField<T extends {}> extends React.Component<TextFieldProps<T>, TextFie
309313
fullWidth,
310314
helperText,
311315
outlined,
316+
onLeadingIconSelect,
317+
onTrailingIconSelect,
312318
leadingIcon,
313319
trailingIcon,
314320
textarea,
@@ -322,12 +328,12 @@ class TextField<T extends {}> extends React.Component<TextFieldProps<T>, TextFie
322328
onKeyDown={() => foundation && foundation.handleTextFieldInteraction()}
323329
key='text-field-container'
324330
>
325-
{leadingIcon ? this.renderIcon(leadingIcon) : null}
331+
{leadingIcon ? this.renderIcon(leadingIcon, onLeadingIconSelect) : null}
326332
{foundation ? this.renderInput() : null}
327333
{label && !fullWidth ? this.renderLabel() : null}
328334
{outlined ? this.renderNotchedOutline() : null}
329335
{!fullWidth && !textarea && !outlined ? this.renderLineRipple() : null}
330-
{trailingIcon ? this.renderIcon(trailingIcon) : null}
336+
{trailingIcon ? this.renderIcon(trailingIcon, onTrailingIconSelect) : null}
331337
</div>
332338
);
333339

@@ -409,10 +415,11 @@ class TextField<T extends {}> extends React.Component<TextFieldProps<T>, TextFie
409415
return React.cloneElement(helperText, props);
410416
}
411417

412-
renderIcon(icon: React.ReactElement<React.HTMLProps<HTMLOrSVGElement>>) {
418+
renderIcon(icon: React.ReactElement<React.HTMLProps<HTMLOrSVGElement>>,
419+
onSelect?: () => void) {
413420
const {disabled} = this.state;
414421
// Toggling disabled will trigger icon.foundation.setDisabled()
415-
return <Icon disabled={disabled}>{icon}</Icon>;
422+
return <Icon disabled={disabled} onSelect={onSelect}>{icon}</Icon>;
416423
}
417424
}
418425

test/screenshot/golden.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"tab-indicator": "7ce7ce8fd50301c67d7ebfb0ba953208260ce2881bee0c7e640c46bf60dc90b6",
2222
"tab-scroller": "468866dd0c222b36b55485ab44a5760133a4ddfb2a6cf81e6ae4672d7e02a447",
2323
"text-field/helper-text": "59604d0f39e0846fc97aae7573d317dded215282a677e4641c5e33426e3a2a1e",
24-
"text-field/icon": "c34dae5444deec222533b3f43448c0393b0c8543a5af4e50cc12d71611f366a7",
24+
"text-field/icon": "0bbc8c762d27071e55983e5742548d166864b6fcebc0b9f1e413523fb93b7075",
2525
"text-field/textArea": "871b32d2fd1982e191e9d5f6ac32e8eb4d82f9fbb1a83359bce8e8f2e9edd027",
2626
"text-field/standard": "be2ea75813583dac8d3ad988282cfa19fa7266a29b095cbd60a34912a1900251",
2727
"text-field/fullWidth": "7c854723b1b4ce7e6df614f44f7b7845bef6098ac30714da5c5128bbd57eb51f",
@@ -44,4 +44,4 @@
4444
"drawer/modal": "da83487c9349b253f7d4de01f92d442de55aab92a8028b77ff1a48cfaa265b72",
4545
"drawer/permanentToModal": "6355905c2241b5e6fdddc2e25119a1cc3b062577375a88b59e6750c4b76e4561",
4646
"typography": "c5e87d672d8c05ca3b61c0df4971eabe3c6a6a1f24a9b98f71f55a23360c498a"
47-
}
47+
}

test/screenshot/text-field/TextFieldPermutations.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {
1313
import TestField from './TestTextField';
1414

1515
const textFields = (variantProps: {variant?: string}) => {
16-
return iconsMap.map((icon: {leadingIcon?: React.ReactNode, trailingIcon?: React.ReactNode}) => {
16+
return iconsMap.map((icon: {
17+
leadingIcon?: React.ReactNode,
18+
onLeadingIconSelect?: () => void,
19+
trailingIcon?: React.ReactNode,
20+
onTrailingIconSelect?: () => void, }) => {
1721
return denseMap.map((dense: {dense?: boolean}) => {
1822
return rtlMap.map((isRtl: {isRtl?: boolean}) => {
1923
return requiredMap.map((isRequired: {required?: boolean}) => {

test/screenshot/text-field/attributesMap.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const iconAlt = <MaterialIcon icon='work' />;
1010

1111
const iconsMap = [
1212
{},
13-
{leadingIcon: icon},
14-
{trailingIcon: icon},
13+
{leadingIcon: icon, onLeadingIconSelect: () => console.log('bark')},
14+
{trailingIcon: icon, onTrailingIconSelect: () => console.log('shhh')},
1515
{leadingIcon: icon, trailingIcon: iconAlt},
1616
];
1717
const denseMap = [{}, {dense: true}];

test/screenshot/text-field/icon/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const TextFieldIconScreenshotTest = () => {
1515
<Icon>
1616
<MaterialIcon tabIndex={0} role='button' icon='favorite' />
1717
</Icon>
18+
19+
<Icon onSelect={() => console.log('❤️')}>
20+
<MaterialIcon role='button' icon='favorite' />
21+
</Icon>
1822
</div>
1923
);
2024
};

test/unit/text-field/icon/index.test.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {assert} from 'chai';
44
import {shallow} from 'enzyme';
55
import Icon from '../../../../packages/text-field/icon/index';
66
import MaterialIcon from '../../../../packages/material-icon/index';
7+
import {coerceForTesting} from '../../helpers/types';
78

89
suite('Text Field Icon');
910

@@ -59,7 +60,7 @@ test('#componentDidMount calls #foundation.setDisabled if disabled prop is true'
5960
<i />
6061
</Icon>
6162
);
62-
assert.equal(wrapper.state().tabindex, undefined);
63+
assert.equal(wrapper.state().tabindex, -1); // foundation setDisabled sets tabIndex -1
6364
assert.equal(wrapper.state().role, undefined);
6465
});
6566

@@ -73,6 +74,26 @@ test('#componentDidMount calls #foundation.setDisabled if disabled prop is true
7374
assert.equal(wrapper.state().role, undefined);
7475
});
7576

77+
test('#componentDidMount sets tabindex if prop not present but onSelect exists', () => {
78+
// w/out tabindex onSelect will never fire && cursor: pointer is not applied
79+
const wrapper = shallow<Icon>(
80+
<Icon onSelect={() => {}}><MaterialIcon icon='favorite' /></Icon>);
81+
82+
assert.equal(wrapper.state().tabindex, 0);
83+
});
84+
85+
test(
86+
'#componentDidUpdate will set tabindex if prop not present but updates ' +
87+
'with onSelect',
88+
() => {
89+
const wrapper = shallow<Icon>(<Icon><MaterialIcon icon='favorite' /></Icon>);
90+
91+
assert.equal(wrapper.state().tabindex, -1);
92+
wrapper.setProps({onSelect: () => {}});
93+
assert.equal(wrapper.state().tabindex, 0);
94+
}
95+
);
96+
7697
test(
7798
'#componentDidUpdate calls #foundation.setDisabled if ' +
7899
'props.disabled updates',
@@ -201,6 +222,47 @@ test('#adapter.removeAttr for role works with Custom Component', () => {
201222
assert.equal(wrapper.state().role, undefined);
202223
});
203224

225+
test('#adapter.notifyIconAction calls props.onSelect', () => {
226+
const onSelect = coerceForTesting<() => void>(td.func());
227+
const wrapper = shallow<Icon>(
228+
<Icon onSelect={onSelect}>
229+
<MaterialIcon icon='favorite' role='button' />
230+
</Icon>
231+
);
232+
wrapper.instance().foundation_.adapter_.notifyIconAction();
233+
td.verify(onSelect(), {times: 1});
234+
});
235+
236+
test('onClick calls foundation.handleInteraction', () => {
237+
const onSelect = coerceForTesting<() => void>(td.func());
238+
const wrapper = shallow<Icon>(
239+
<Icon onSelect={onSelect}>
240+
<MaterialIcon icon='favorite' role='button' />
241+
</Icon>
242+
);
243+
const evt = coerceForTesting<React.MouseEvent>({});
244+
wrapper.instance().foundation_.handleInteraction = td.func();
245+
wrapper.simulate('click', evt);
246+
td.verify(wrapper.instance().foundation_.handleInteraction(evt), {
247+
times: 1,
248+
});
249+
});
250+
251+
test('onKeyDown call foundation.handleInteraction', () => {
252+
const onSelect = coerceForTesting<() => void>(td.func());
253+
const wrapper = shallow<Icon>(
254+
<Icon onSelect={onSelect}>
255+
<MaterialIcon icon='favorite' role='button' />
256+
</Icon>
257+
);
258+
const evt = coerceForTesting<React.KeyboardEvent>({});
259+
wrapper.instance().foundation_.handleInteraction = td.func();
260+
wrapper.simulate('keydown', evt);
261+
td.verify(wrapper.instance().foundation_.handleInteraction(evt), {
262+
times: 1,
263+
});
264+
});
265+
204266
test('updating the role reflects on DOM node', () => {
205267
const wrapper = shallow(
206268
<Icon>

0 commit comments

Comments
 (0)