Skip to content

Commit

Permalink
fix(text-field): autofocus floats label (material-components#875)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Goo committed May 30, 2019
1 parent d733c29 commit 9931969
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 27 deletions.
44 changes: 33 additions & 11 deletions packages/text-field/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Props<T extends HTMLElement = HTMLInputElement> = InputProps<T> &

interface InputState {
wasUserTriggeredChange: boolean;
isMounted: boolean;
}

declare type ValidationAttrWhiteList =
Expand Down Expand Up @@ -97,7 +98,10 @@ export default class Input<
value: '',
};

state = {wasUserTriggeredChange: false};
state = {
wasUserTriggeredChange: false,
isMounted: false,
};

componentDidMount() {
const {
Expand All @@ -109,7 +113,6 @@ export default class Input<
setDisabled,
handleValueChange,
foundation,
isValid,
} = this.props;
if (syncInput) {
syncInput(this);
Expand All @@ -126,13 +129,10 @@ export default class Input<
() => foundation && foundation.setValue(this.valueToString(value))
);
}
if (foundation && isValid !== undefined) {
foundation.setUseNativeValidation(false);
foundation.setValid(!!isValid);
}
this.setState({isMounted: true});
}

componentDidUpdate(prevProps: Props<T>) {
componentDidUpdate(prevProps: Props<T>, prevState: InputState) {
const {
id,
foundation,
Expand All @@ -144,6 +144,13 @@ export default class Input<
setDisabled,
} = this.props;

if (
(!prevState.isMounted && this.state.isMounted && this.props.foundation) ||
(this.state.isMounted && !prevProps.foundation && this.props.foundation)
) {
this.initializeComponentWithFoundation();
}

this.handleValidationAttributeUpdate(prevProps);

if (disabled !== prevProps.disabled) {
Expand Down Expand Up @@ -178,6 +185,23 @@ export default class Input<
}
}

/**
* This method is for any initialization logic the depends on the foundation.
* Any other initialization logic should belong in the componentDidMount.
*/
private initializeComponentWithFoundation = () => {
const {handleFocusChange, foundation, autoFocus, isValid} = this.props;
if (autoFocus) {
handleFocusChange && handleFocusChange(true);
}
// there is no reason for this to be in Input.tsx

if (foundation && isValid !== undefined) {
foundation.setUseNativeValidation(false);
foundation.setValid(isValid as boolean);
}
};

valueToString(value?: string | string[] | number) {
let str;
if (typeof value === 'object') {
Expand All @@ -204,8 +228,7 @@ export default class Input<
T extends HTMLInputElement ? HTMLInputElement : HTMLTextAreaElement
>
) => {
const {foundation, handleFocusChange, onFocus = () => {}} = this.props;
foundation && foundation.activateFocus();
const {handleFocusChange, onFocus = () => {}} = this.props;
handleFocusChange && handleFocusChange(true);
onFocus(evt);
};
Expand All @@ -215,8 +238,7 @@ export default class Input<
T extends HTMLInputElement ? HTMLInputElement : HTMLTextAreaElement
>
) => {
const {foundation, handleFocusChange, onBlur = () => {}} = this.props;
foundation && foundation.deactivateFocus();
const {handleFocusChange, onBlur = () => {}} = this.props;
handleFocusChange && handleFocusChange(false);
onBlur(evt);
};
Expand Down
10 changes: 9 additions & 1 deletion packages/text-field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,15 @@ class TextField<
const {props} = child;
return Object.assign({}, props, {
foundation: this.state.foundation,
handleFocusChange: (isFocused: boolean) => this.setState({isFocused}),
handleFocusChange: (isFocused: boolean) => {
this.setState({isFocused});
if (!this.state.foundation) return;
if (isFocused) {
this.state.foundation.activateFocus();
} else {
this.state.foundation.deactivateFocus();
}
},
setDisabled: (disabled: boolean) => this.setState({disabled}),
setInputId: (id: string) => this.setState({inputId: id}),
syncInput: (input: Input<T>) => (this.inputComponent_ = input),
Expand Down
1 change: 1 addition & 0 deletions test/screenshot/golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"text-field/fullWidth": "6555b4398509aa79f2e6ae1ef3be678a9a877984c4aaf77804afc01fe4fc50a6",
"text-field/outlined": "a6f22c45fe20e8dab39c39711cbec6c249933b7c655d4dff39d4a635c846acc6",
"text-field/refTest": "0310e81ea870fb0e6e5968d0e1567e22b0e952aeeff9653d3dfe8d9b5b1f5588",
"text-field/autoFocus": "3b5e7d823fb7c8caf0f7568add1f99bc7c0171afb19f5d1f9978a68091dd07bd",
"top-app-bar/fixed": "7a2dd6318d62ac2eabd66f1b28100db7c15840ccb753660065fa9524db6435d6",
"top-app-bar/prominent": "2506ed2dd5f370c7bab69315d2daebd58b443d2b9e32bbaec762e40a8736309b",
"top-app-bar/short": "90dba9623f16d58cfc4a24b2a3ab652c7e0cc6d5ccfd030566a170a55d6bce0c",
Expand Down
21 changes: 21 additions & 0 deletions test/screenshot/text-field/autoFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import TextField, {Input} from '../../../packages/text-field';

class OutlinedTextField extends React.Component<{}, {value: string}> {
inputEl: Input<HTMLInputElement> | null = null;
state = {value: ''};

onChange: (e: React.FormEvent) => void = (e) =>
this.setState({value: (e.target as HTMLInputElement).value});

render() {
return (
<div>
<TextField label='Dog' outlined>
<Input autoFocus value={this.state.value} onChange={this.onChange} />
</TextField>
</div>
);
}
}
export default OutlinedTextField;
9 changes: 8 additions & 1 deletion test/screenshot/text-field/variants.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
export default ['standard', 'fullWidth', 'outlined', 'textArea', 'refTest'];
export default [
'standard',
'fullWidth',
'outlined',
'textArea',
'refTest',
'autoFocus',
];
47 changes: 33 additions & 14 deletions test/unit/text-field/Input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,39 @@ test('#componentDidMount does not call props.handleValueChange when there is no
);
});

test(
'#props.handleFocusChange is called when props.autoFocus is true' +
', there is a props.foundation, and component has mounted',
() => {
const handleFocusChange = td.func();
const props: any = {handleFocusChange, autoFocus: true, foundation: {}};
mount(<Input {...props} />);
td.verify(handleFocusChange(true), {times: 1});
}
);

test(
'#props.handleFocusChange is not called when props.autoFocus is undefined' +
', there is a props.foundation, and component has mounted',
() => {
const handleFocusChange = td.func();
const props: any = {handleFocusChange, foundation: {}};
mount(<Input {...props} />);
td.verify(handleFocusChange(td.matchers.isA(Boolean)), {times: 0});
}
);

test(
'#props.handleFocusChange is not called when props.autoFocus is true' +
', there is no props.foundation, and component has mounted',
() => {
const handleFocusChange = td.func();
const props: any = {handleFocusChange, autoFocus: true};
mount(<Input {...props} />);
td.verify(handleFocusChange(td.matchers.isA(Boolean)), {times: 0});
}
);

test('change to minLength calls handleValidationAttributeChange', () => {
const foundation: any = buildFoundation({
handleValidationAttributeChange: td.func(),
Expand Down Expand Up @@ -354,13 +387,6 @@ test('#event.onFocus calls props.handleFocusChange(true)', () => {
td.verify(handleFocusChange(true), {times: 1});
});

test('#event.onFocus calls foundation.activateFocus()', () => {
const foundation = buildFoundation({activateFocus: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.simulate('focus');
td.verify(foundation.activateFocus(), {times: 1});
});

test('#event.onFocus calls props.onFocus()', () => {
const onFocus = td.func();
const props: any = {onFocus, foundation: buildFoundation()};
Expand All @@ -378,13 +404,6 @@ test('#event.onBlur calls props.handleFocusChange(false)', () => {
td.verify(handleFocusChange(false), {times: 1});
});

test('#event.onBlur calls foundation.deactivateFocus()', () => {
const foundation = buildFoundation({deactivateFocus: td.func()});
const wrapper = shallow(<Input foundation={foundation} />);
wrapper.simulate('blur');
td.verify(foundation.deactivateFocus(), {times: 1});
});

test('#event.onBlur calls props.onBlur()', () => {
const onBlur = td.func();
const props: any = {onBlur, foundation: buildFoundation()};
Expand Down
36 changes: 36 additions & 0 deletions test/unit/text-field/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,42 @@ test('#inputProps.handleFocusChange updates state.isFocused', () => {
assert.isTrue(wrapper.state().isFocused);
});

test('#inputProps.handleFocusChange calls foundation.activateFocus if isFocused is true', () => {
const wrapper = mount<TextField<HTMLInputElement>>(
<TextField label='my label'>
<Input />
</TextField>
);
const activateFocus = td.func();
const foundation = {activateFocus} as any;
wrapper.setState({foundation});
wrapper
.instance()
.inputProps(
coerceForTesting<React.ReactElement<InputProps<HTMLInputElement>>>({})
)
.handleFocusChange(true);
td.verify(activateFocus(), {times: 1});
});

test('#inputProps.handleFocusChange calls foundation.deactivateFocus if isFocused is false', () => {
const wrapper = mount<TextField<HTMLInputElement>>(
<TextField label='my label'>
<Input />
</TextField>
);
const deactivateFocus = td.func();
const foundation = {deactivateFocus} as any;
wrapper.setState({foundation});
wrapper
.instance()
.inputProps(
coerceForTesting<React.ReactElement<InputProps<HTMLInputElement>>>({})
)
.handleFocusChange(false);
td.verify(deactivateFocus(), {times: 1});
});

test('#inputProps.setDisabled updates state.disabled', () => {
const wrapper = mount<TextField<HTMLInputElement>>(
<TextField label='my label'>
Expand Down

0 comments on commit 9931969

Please sign in to comment.