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

[Autocomplete] Improve focus logic #18286

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/pages/components/autocomplete/CustomizedHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const Listbox = styled('ul')`

export default function CustomizedHook() {
const {
getComboboxProps,
getRootProps,
getInputLabelProps,
getInputProps,
getTagProps,
Expand All @@ -143,7 +143,7 @@ export default function CustomizedHook() {

return (
<div>
<div {...getComboboxProps()}>
<div {...getRootProps()}>
<Label {...getInputLabelProps()}>Customized hook</Label>
<InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
{value.map((option, index) => (
Expand Down
4 changes: 2 additions & 2 deletions docs/src/pages/components/autocomplete/CustomizedHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const Listbox = styled('ul')`

export default function CustomizedHook() {
const {
getComboboxProps,
getRootProps,
getInputLabelProps,
getInputProps,
getTagProps,
Expand All @@ -143,7 +143,7 @@ export default function CustomizedHook() {

return (
<div>
<div {...getComboboxProps()}>
<div {...getRootProps()}>
<Label {...getInputLabelProps()}>Customized hook</Label>
<InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
{value.map((option: FilmOptionType, index: number) => (
Expand Down
4 changes: 2 additions & 2 deletions docs/src/pages/components/autocomplete/UseAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const useStyles = makeStyles(theme => ({
export default function UseAutocomplete() {
const classes = useStyles();
const {
getComboboxProps,
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
Expand All @@ -49,7 +49,7 @@ export default function UseAutocomplete() {

return (
<div>
<div {...getComboboxProps()}>
<div {...getRootProps()}>
<label className={classes.label} {...getInputLabelProps()}>
useAutocomplete
</label>
Expand Down
4 changes: 2 additions & 2 deletions docs/src/pages/components/autocomplete/UseAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const useStyles = makeStyles((theme: Theme) =>
export default function UseAutocomplete() {
const classes = useStyles();
const {
getComboboxProps,
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
Expand All @@ -51,7 +51,7 @@ export default function UseAutocomplete() {

return (
<div>
<div {...getComboboxProps()}>
<div {...getRootProps()}>
<label className={classes.label} {...getInputLabelProps()}>
useAutocomplete
</label>
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/components/autocomplete/autocomplete.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The Autocomplete component uses this hook internally.
import useAutocomplete from '@material-ui/lab/useAutocomplete';
```

- 📦 [4 kB gzipped](/size-snapshot).
- 📦 [4.5 kB gzipped](/size-snapshot).

{{"demo": "pages/components/autocomplete/UseAutocomplete.js", "defaultCodeOpen": false}}

Expand Down
4 changes: 2 additions & 2 deletions packages/material-ui-lab/src/Autocomplete/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
const PopperComponent = disablePortal ? DisablePortal : PopperComponentProp;

const {
getComboboxProps,
getRootProps,
getInputProps,
getInputLabelProps,
getPopupIndicatorProps,
Expand Down Expand Up @@ -282,7 +282,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
},
className,
)}
{...getComboboxProps()}
{...getRootProps()}
{...other}
>
{renderInput({
Expand Down
52 changes: 52 additions & 0 deletions packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,56 @@ describe('<Autocomplete />', () => {
expect(handleChange.callCount).to.equal(1);
});
});

describe('prop: autoComplete', () => {
it('add a completion string', () => {
const { getByRole } = render(
<Autocomplete
autoComplete
options={['one', 'two']}
renderInput={params => <TextField autoFocus {...params} />}
/>,
);
const textbox = getByRole('textbox');
fireEvent.change(textbox, { target: { value: 'O' } });
expect(textbox.value).to.equal('O');
fireEvent.keyDown(textbox, { key: 'ArrowDown' });
expect(textbox.value).to.equal('one');
expect(textbox.selectionStart).to.equal(1);
expect(textbox.selectionEnd).to.equal(3);
fireEvent.keyDown(textbox, { key: 'Enter' });
expect(textbox.value).to.equal('one');
expect(textbox.selectionStart).to.equal(3);
expect(textbox.selectionEnd).to.equal(3);
});
});

describe('click input', () => {
it('toggles if empty', () => {
const { getByRole } = render(
<Autocomplete options={['one', 'two']} renderInput={params => <TextField {...params} />} />,
);
const textbox = getByRole('textbox');
const combobox = getByRole('combobox');
expect(combobox).to.have.attribute('aria-expanded', 'false');
fireEvent.mouseDown(textbox);
expect(combobox).to.have.attribute('aria-expanded', 'true');
fireEvent.mouseDown(textbox);
expect(combobox).to.have.attribute('aria-expanded', 'false');
});

it('selects all the first time', () => {
const { getByRole } = render(
<Autocomplete
value="one"
options={['one', 'two']}
renderInput={params => <TextField {...params} />}
/>,
);
const textbox = getByRole('textbox');
fireEvent.click(textbox);
expect(textbox.selectionStart).to.equal(0);
expect(textbox.selectionEnd).to.equal(3);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export interface UseAutocompleteProps {
export default function useAutocomplete(
props: UseAutocompleteProps,
): {
getComboboxProps: () => {};
getRootProps: () => {};
getInputProps: () => {};
getInputLabelProps: () => {};
getClearProps: () => {};
Expand Down
46 changes: 39 additions & 7 deletions packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default function useAutocomplete(props) {
}
});

const firstFocus = React.useRef(true);
const inputRef = React.useRef(null);
const listboxRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
Expand Down Expand Up @@ -531,6 +532,14 @@ export default function useAutocomplete(props) {
// We don't want to validate the form.
event.preventDefault();
selectNewValue(event, filteredOptions[highlightedIndexRef.current]);

// Move the selection to the end.
if (autoComplete) {
inputRef.current.setSelectionRange(
inputRef.current.value.length,
inputRef.current.value.length,
);
}
} else if (freeSolo && inputValue !== '') {
selectNewValue(event, inputValue);
}
Expand Down Expand Up @@ -572,6 +581,7 @@ export default function useAutocomplete(props) {

const handleBlur = event => {
setFocused(false);
firstFocus.current = true;

if (debug && inputValue !== '') {
return;
Expand Down Expand Up @@ -640,6 +650,30 @@ export default function useAutocomplete(props) {
}
};

const handleMouseDown = event => {
if (event.target.nodeName !== 'INPUT') {
// Prevent blur
event.preventDefault();
}
};

const handleClick = () => {
if (
firstFocus.current &&
inputRef.current.selectionEnd - inputRef.current.selectionStart === 0
) {
inputRef.current.select();
}

firstFocus.current = false;
};

const handleInputMouseDown = () => {
if (inputValue === '') {
handlePopupIndicator();
}
};

let dirty = freeSolo && inputValue.length > 0;
dirty = dirty || (multiple ? value.length > 0 : value !== null);

Expand All @@ -663,11 +697,13 @@ export default function useAutocomplete(props) {
}

return {
getComboboxProps: () => ({
getRootProps: () => ({
'aria-owns': popupOpen ? `${id}-popup` : null,
role: 'combobox',
'aria-expanded': popupOpen,
onKeyDown: handleKeyDown,
onMouseDown: handleMouseDown,
onClick: handleClick,
}),
getInputLabelProps: () => ({
id: `${id}-label`,
Expand All @@ -678,6 +714,7 @@ export default function useAutocomplete(props) {
onBlur: handleBlur,
onFocus: handleFocus,
onChange: handleInputChange,
onMouseDown: handleInputMouseDown,
// if open then this is handled imperativeley so don't let react override
// only have an opinion about this when closed
'aria-activedescendant': popupOpen ? undefined : null,
Expand All @@ -693,16 +730,10 @@ export default function useAutocomplete(props) {
getClearProps: () => ({
tabIndex: -1,
onClick: handleClear,
onMouseDown: event => {
event.preventDefault();
},
}),
getPopupIndicatorProps: () => ({
tabIndex: -1,
onClick: handlePopupIndicator,
onMouseDown: event => {
event.preventDefault();
},
}),
getTagProps: ({ index }) => ({
key: index,
Expand All @@ -716,6 +747,7 @@ export default function useAutocomplete(props) {
'aria-labelledby': `${id}-label`,
ref: handleListboxRef,
onMouseDown: event => {
// Prevent blur
event.preventDefault();
},
}),
Expand Down