Skip to content

Commit

Permalink
[Autocomplete] Add more details in the onChange event (mui#19959)
Browse files Browse the repository at this point in the history
  • Loading branch information
akharkhonov authored and eps1lon committed Mar 4, 2020
1 parent 8d0ccb5 commit 68da08a
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 21 deletions.
5 changes: 3 additions & 2 deletions packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import * as React from 'react';
import { StandardProps } from '@material-ui/core';
import { PopperProps } from '@material-ui/core/Popper';
import {
ChangeReason,
ChangeDetails,
UseAutocompleteCommonProps,
createFilterOptions,
UseAutocompleteProps,
} from '../useAutocomplete';

export { createFilterOptions };
export { ChangeReason, ChangeDetails, createFilterOptions };

export interface RenderOptionState {
inputValue: string;
Expand Down
100 changes: 100 additions & 0 deletions packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,106 @@ describe('<Autocomplete />', () => {
});
});

describe('prop: onChange', () => {
it('provides a reason and details on option creation', () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
render(
<Autocomplete
freeSolo
onChange={handleChange}
options={options}
renderInput={params => <TextField {...params} autoFocus />}
/>,
);
fireEvent.change(document.activeElement, { target: { value: options[2] } });
fireEvent.keyDown(document.activeElement, { key: 'Enter' });
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal(options[2]);
expect(handleChange.args[0][2]).to.equal('create-option');
expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] });
});

it('provides a reason and details on option selection', () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
render(
<Autocomplete
onChange={handleChange}
options={options}
renderInput={params => <TextField {...params} autoFocus />}
/>,
);
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
fireEvent.keyDown(document.activeElement, { key: 'Enter' });
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal(options[0]);
expect(handleChange.args[0][2]).to.equal('select-option');
expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] });
});

it('provides a reason and details on option removing', () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
render(
<Autocomplete
multiple
onChange={handleChange}
value={options}
options={options}
renderInput={params => <TextField {...params} autoFocus />}
/>,
);
fireEvent.keyDown(document.activeElement, { key: 'Backspace' });
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.deep.equal(options.slice(0, 2));
expect(handleChange.args[0][2]).to.equal('remove-option');
expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] });
});

it('provides a reason and details on blur', () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
render(
<Autocomplete
autoSelect
onChange={handleChange}
options={options}
renderInput={params => <TextField {...params} autoFocus />}
/>,
);
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
document.activeElement.blur();
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.equal(options[0]);
expect(handleChange.args[0][2]).to.equal('blur');
expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] });
});

it('provides a reason and details on clear', () => {
const handleChange = spy();
const options = ['one', 'two', 'three'];
const { container } = render(
<Autocomplete
multiple
value={options}
onChange={handleChange}
options={options}
renderInput={params => <TextField {...params} autoFocus />}
/>,
);

const button = container.querySelector('button');
fireEvent.click(button);
expect(handleChange.callCount).to.equal(1);
expect(handleChange.args[0][1]).to.deep.equal([]);
expect(handleChange.args[0][2]).to.equal('clear');
expect(handleChange.args[0][3]).to.equal(undefined);
});
});

describe('prop: onInputChange', () => {
it('provides a reason on input change', () => {
const handleInputChange = spy();
Expand Down
18 changes: 16 additions & 2 deletions packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ export interface UseAutocompleteCommonProps<T> {
selectOnFocus?: boolean;
}

export type ChangeReason = 'create-option' | 'select-option' | 'remove-option' | 'clear' | 'blur';
export interface ChangeDetails<T = string> {
option: T;
}
export interface UseAutocompleteMultipleProps<T> extends UseAutocompleteCommonProps<T> {
/**
* If `true`, `value` must be an array and the menu will support multiple selections.
Expand All @@ -187,7 +191,12 @@ export interface UseAutocompleteMultipleProps<T> extends UseAutocompleteCommonPr
* @param {object} event The event source of the callback.
* @param {T[]} value
*/
onChange?: (event: React.ChangeEvent<{}>, value: T[]) => void;
onChange?: (
event: React.ChangeEvent<{}>,
value: T[],
reason: ChangeReason,
details?: ChangeDetails<T>,
) => void;
}

export interface UseAutocompleteSingleProps<T> extends UseAutocompleteCommonProps<T> {
Expand All @@ -212,7 +221,12 @@ export interface UseAutocompleteSingleProps<T> extends UseAutocompleteCommonProp
* @param {object} event The event source of the callback.
* @param {T} value
*/
onChange?: (event: React.ChangeEvent<{}>, value: T | null) => void;
onChange?: (
event: React.ChangeEvent<{}>,
value: T | null,
reason: ChangeReason,
details?: ChangeDetails<T>,
) => void;
}

export type UseAutocompleteProps<T> =
Expand Down
42 changes: 25 additions & 17 deletions packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,25 +458,27 @@ export default function useAutocomplete(props) {
}
};

const handleValue = (event, newValue) => {
const handleValue = (event, newValue, reason, details) => {
if (value === newValue) {
return;
}

if (onChange) {
onChange(event, newValue);
onChange(event, newValue, reason, details);
}

setValue(newValue);
};

const selectNewValue = (event, newValue, origin = 'option') => {
const selectNewValue = (event, option, reasonProp = 'select-option', origin = 'options') => {
let reason = reasonProp;
let newValue = option;

if (multiple) {
const item = newValue;
newValue = Array.isArray(value) ? [...value] : [];

if (process.env.NODE_ENV !== 'production') {
const matches = newValue.filter(val => getOptionSelected(item, val));
const matches = newValue.filter(val => getOptionSelected(option, val));

if (matches.length > 1) {
console.error(
Expand All @@ -490,18 +492,19 @@ export default function useAutocomplete(props) {
}
}

const itemIndex = findIndex(newValue, valueItem => getOptionSelected(item, valueItem));
const itemIndex = findIndex(newValue, valueItem => getOptionSelected(option, valueItem));

if (itemIndex === -1) {
newValue.push(item);
newValue.push(option);
} else if (origin !== 'freeSolo') {
newValue.splice(itemIndex, 1);
reason = 'remove-option';
}
}

resetInputValue(event, newValue);

handleValue(event, newValue);
handleValue(event, newValue, reason, { option });
if (!disableCloseOnSelect) {
handleClose(event);
}
Expand Down Expand Up @@ -578,7 +581,7 @@ export default function useAutocomplete(props) {
onInputChange(event, '', 'clear');
}

handleValue(event, multiple ? [] : null);
handleValue(event, multiple ? [] : null, 'clear');
};

const handleKeyDown = other => event => {
Expand Down Expand Up @@ -640,7 +643,7 @@ export default function useAutocomplete(props) {
if (highlightedIndexRef.current !== -1 && popupOpen) {
// We don't want to validate the form.
event.preventDefault();
selectNewValue(event, filteredOptions[highlightedIndexRef.current]);
selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'select-option');

// Move the selection to the end.
if (autoComplete) {
Expand All @@ -654,7 +657,7 @@ export default function useAutocomplete(props) {
// Allow people to add new values before they submit the form.
event.preventDefault();
}
selectNewValue(event, inputValue, 'freeSolo');
selectNewValue(event, inputValue, 'create-option', 'freeSolo');
}
break;
case 'Escape':
Expand All @@ -677,7 +680,9 @@ export default function useAutocomplete(props) {
const index = focusedTag === -1 ? value.length - 1 : focusedTag;
const newValue = [...value];
newValue.splice(index, 1);
handleValue(event, newValue);
handleValue(event, newValue, 'remove-option', {
option: value[index],
});
}
break;
default:
Expand Down Expand Up @@ -706,9 +711,9 @@ export default function useAutocomplete(props) {
}

if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
selectNewValue(event, filteredOptions[highlightedIndexRef.current]);
selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
} else if (autoSelect && freeSolo && inputValue !== '') {
selectNewValue(event, inputValue, 'freeSolo');
selectNewValue(event, inputValue, 'blur', 'freeSolo');
} else if (!freeSolo) {
resetInputValue(event, value);
}
Expand All @@ -729,7 +734,7 @@ export default function useAutocomplete(props) {

if (newValue === '') {
if (!disableClearable && !multiple) {
handleValue(event, null);
handleValue(event, null, 'clear');
}
} else {
handleOpen(event);
Expand All @@ -749,7 +754,7 @@ export default function useAutocomplete(props) {

const handleOptionClick = event => {
const index = Number(event.currentTarget.getAttribute('data-option-index'));
selectNewValue(event, filteredOptions[index]);
selectNewValue(event, filteredOptions[index], 'select-option');

if (
blurOnSelect === true ||
Expand All @@ -765,7 +770,9 @@ export default function useAutocomplete(props) {
const handleTagDelete = index => event => {
const newValue = [...value];
newValue.splice(index, 1);
handleValue(event, newValue);
handleValue(event, newValue, 'remove-option', {
option: value[index],
});
};

const handleListboxRef = useEventCallback(node => {
Expand Down Expand Up @@ -1050,6 +1057,7 @@ useAutocomplete.propTypes = {
*
* @param {object} event The event source of the callback
* @param {any} value
* @param {string} reason One of "create-option", "select-option", "remove-option", "blur" or "clear"
*/
onChange: PropTypes.func,
/**
Expand Down

0 comments on commit 68da08a

Please sign in to comment.