fix: Handles disabled options on Select All (#22830)

This commit is contained in:
Michael S. Molina 2023-02-09 08:31:58 -05:00 committed by GitHub
parent ddd8d17aa4
commit 5e64211bdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 128 additions and 37 deletions

View File

@ -22,11 +22,18 @@ import userEvent from '@testing-library/user-event';
import Select from 'src/components/Select/Select'; import Select from 'src/components/Select/Select';
import { SELECT_ALL_VALUE } from './utils'; import { SELECT_ALL_VALUE } from './utils';
type Option = {
label: string;
value: number;
gender: string;
disabled?: boolean;
};
const ARIA_LABEL = 'Test'; const ARIA_LABEL = 'Test';
const NEW_OPTION = 'Kyle'; const NEW_OPTION = 'Kyle';
const NO_DATA = 'No Data'; const NO_DATA = 'No Data';
const LOADING = 'Loading...'; const LOADING = 'Loading...';
const OPTIONS = [ const OPTIONS: Option[] = [
{ label: 'John', value: 1, gender: 'Male' }, { label: 'John', value: 1, gender: 'Male' },
{ label: 'Liam', value: 2, gender: 'Male' }, { label: 'Liam', value: 2, gender: 'Male' },
{ label: 'Olivia', value: 3, gender: 'Female' }, { label: 'Olivia', value: 3, gender: 'Female' },
@ -826,6 +833,56 @@ test('does not render "Select All" when there are 0 or 1 options', async () => {
expect(screen.queryByText(selectAllOptionLabel(2))).toBeInTheDocument(); expect(screen.queryByText(selectAllOptionLabel(2))).toBeInTheDocument();
}); });
test('do not count unselected disabled options in "Select All"', async () => {
const options = [...OPTIONS];
options[0].disabled = true;
options[1].disabled = true;
render(
<Select
{...defaultProps}
options={options}
mode="multiple"
value={options[0]}
/>,
);
await open();
// We have 2 options disabled but one is selected initially
// Select All should count one and ignore the other
expect(
screen.getByText(selectAllOptionLabel(OPTIONS.length - 1)),
).toBeInTheDocument();
});
test('"Select All" does not affect disabled options', async () => {
const options = [...OPTIONS];
options[0].disabled = true;
options[1].disabled = true;
render(
<Select
{...defaultProps}
options={options}
mode="multiple"
value={options[0]}
/>,
);
await open();
// We have 2 options disabled but one is selected initially
expect(await findSelectValue()).toHaveTextContent(options[0].label);
expect(await findSelectValue()).not.toHaveTextContent(options[1].label);
// Checking Select All shouldn't affect the disabled options
const selectAll = selectAllOptionLabel(OPTIONS.length - 1);
userEvent.click(await findSelectOption(selectAll));
expect(await findSelectValue()).toHaveTextContent(options[0].label);
expect(await findSelectValue()).not.toHaveTextContent(options[1].label);
// Unchecking Select All shouldn't affect the disabled options
userEvent.click(await findSelectOption(selectAll));
expect(await findSelectValue()).toHaveTextContent(options[0].label);
expect(await findSelectValue()).not.toHaveTextContent(options[1].label);
});
/* /*
TODO: Add tests that require scroll interaction. Needs further investigation. TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available - Fetches more data when scrolling and more data is available

View File

@ -45,6 +45,8 @@ import {
getSuffixIcon, getSuffixIcon,
SELECT_ALL_VALUE, SELECT_ALL_VALUE,
selectAllOption, selectAllOption,
mapValues,
mapOptions,
} from './utils'; } from './utils';
import { SelectOptionsType, SelectProps } from './types'; import { SelectOptionsType, SelectProps } from './types';
import { import {
@ -177,23 +179,31 @@ const Select = forwardRef(
return result.filter(opt => opt.value !== SELECT_ALL_VALUE); return result.filter(opt => opt.value !== SELECT_ALL_VALUE);
}, [selectOptions, selectValue]); }, [selectOptions, selectValue]);
const enabledOptions = useMemo(
() => fullSelectOptions.filter(option => !option.disabled),
[fullSelectOptions],
);
const selectAllEligible = useMemo(
() =>
fullSelectOptions.filter(
option => hasOption(option.value, selectValue) || !option.disabled,
),
[fullSelectOptions, selectValue],
);
const selectAllEnabled = useMemo( const selectAllEnabled = useMemo(
() => () =>
!isSingleMode && !isSingleMode &&
selectOptions.length > 0 && selectOptions.length > 0 &&
fullSelectOptions.length > 1 && enabledOptions.length > 1 &&
!inputValue, !inputValue,
[ [isSingleMode, selectOptions.length, enabledOptions.length, inputValue],
isSingleMode,
selectOptions.length,
fullSelectOptions.length,
inputValue,
],
); );
const selectAllMode = useMemo( const selectAllMode = useMemo(
() => ensureIsArray(selectValue).length === fullSelectOptions.length + 1, () => ensureIsArray(selectValue).length === selectAllEligible.length + 1,
[selectValue, fullSelectOptions], [selectValue, selectAllEligible],
); );
const handleOnSelect = ( const handleOnSelect = (
@ -209,19 +219,19 @@ const Select = forwardRef(
if (value === getValue(SELECT_ALL_VALUE)) { if (value === getValue(SELECT_ALL_VALUE)) {
if (isLabeledValue(selectedItem)) { if (isLabeledValue(selectedItem)) {
return [ return [
...fullSelectOptions, ...selectAllEligible,
selectAllOption, selectAllOption,
] as AntdLabeledValue[]; ] as AntdLabeledValue[];
} }
return [ return [
SELECT_ALL_VALUE, SELECT_ALL_VALUE,
...fullSelectOptions.map(opt => opt.value), ...selectAllEligible.map(opt => opt.value),
] as AntdLabeledValue[]; ] as AntdLabeledValue[];
} }
if (!hasOption(value, array)) { if (!hasOption(value, array)) {
const result = [...array, selectedItem]; const result = [...array, selectedItem];
if ( if (
result.length === fullSelectOptions.length && result.length === selectAllEligible.length &&
selectAllEnabled selectAllEnabled
) { ) {
return isLabeledValue(selectedItem) return isLabeledValue(selectedItem)
@ -236,12 +246,26 @@ const Select = forwardRef(
setInputValue(''); setInputValue('');
}; };
const clear = () => {
setSelectValue(
fullSelectOptions
.filter(
option => option.disabled && hasOption(option.value, selectValue),
)
.map(option =>
labelInValue
? { label: option.label, value: option.value }
: option.value,
),
);
};
const handleOnDeselect = ( const handleOnDeselect = (
value: string | number | AntdLabeledValue | undefined, value: string | number | AntdLabeledValue | undefined,
) => { ) => {
if (Array.isArray(selectValue)) { if (Array.isArray(selectValue)) {
if (getValue(value) === getValue(SELECT_ALL_VALUE)) { if (getValue(value) === getValue(SELECT_ALL_VALUE)) {
setSelectValue(undefined); clear();
} else { } else {
let array = selectValue as AntdLabeledValue[]; let array = selectValue as AntdLabeledValue[];
array = array.filter( array = array.filter(
@ -312,7 +336,7 @@ const Select = forwardRef(
); );
const handleClear = () => { const handleClear = () => {
setSelectValue(undefined); clear();
if (onClear) { if (onClear) {
onClear(); onClear();
} }
@ -337,7 +361,7 @@ const Select = forwardRef(
// if all values are selected, add select all to value // if all values are selected, add select all to value
if ( if (
!isSingleMode && !isSingleMode &&
ensureIsArray(value).length === fullSelectOptions.length && ensureIsArray(value).length === selectAllEligible.length &&
selectOptions.length > 0 selectOptions.length > 0
) { ) {
setSelectValue( setSelectValue(
@ -353,7 +377,7 @@ const Select = forwardRef(
value, value,
isSingleMode, isSingleMode,
labelInValue, labelInValue,
fullSelectOptions.length, selectAllEligible.length,
selectOptions.length, selectOptions.length,
]); ]);
@ -362,22 +386,22 @@ const Select = forwardRef(
v => getValue(v) === SELECT_ALL_VALUE, v => getValue(v) === SELECT_ALL_VALUE,
); );
if (checkSelectAll && !selectAllMode) { if (checkSelectAll && !selectAllMode) {
const optionsToSelect = fullSelectOptions.map(option => const optionsToSelect = selectAllEligible.map(option =>
labelInValue ? option : option.value, labelInValue ? option : option.value,
); );
optionsToSelect.push(labelInValue ? selectAllOption : SELECT_ALL_VALUE); optionsToSelect.push(labelInValue ? selectAllOption : SELECT_ALL_VALUE);
setSelectValue(optionsToSelect); setSelectValue(optionsToSelect);
} }
}, [selectValue, selectAllMode, labelInValue, fullSelectOptions]); }, [selectValue, selectAllMode, labelInValue, selectAllEligible]);
const selectAllLabel = useMemo( const selectAllLabel = useMemo(
() => () => () => () =>
// TODO: localize // TODO: localize
`${SELECT_ALL_VALUE} (${formatNumber( `${SELECT_ALL_VALUE} (${formatNumber(
NumberFormats.INTEGER, NumberFormats.INTEGER,
fullSelectOptions.length, selectAllEligible.length,
)})`, )})`,
[fullSelectOptions.length], [selectAllEligible],
); );
const handleOnChange = (values: any, options: any) => { const handleOnChange = (values: any, options: any) => {
@ -394,30 +418,22 @@ const Select = forwardRef(
) { ) {
// send all options to onchange if all are not currently there // send all options to onchange if all are not currently there
if (!selectAllMode) { if (!selectAllMode) {
newValues = labelInValue newValues = mapValues(selectAllEligible, labelInValue);
? fullSelectOptions.map(opt => ({ newOptions = mapOptions(selectAllEligible);
key: opt.value,
value: opt.value,
label: opt.label,
}))
: fullSelectOptions.map(opt => opt.value);
newOptions = fullSelectOptions.map(opt => ({
children: opt.label,
key: opt.value,
value: opt.value,
label: opt.label,
}));
} else { } else {
newValues = ensureIsArray(values).filter( newValues = ensureIsArray(values).filter(
(val: any) => getValue(val) !== SELECT_ALL_VALUE, (val: any) => getValue(val) !== SELECT_ALL_VALUE,
); );
} }
} else if ( } else if (
ensureIsArray(values).length === fullSelectOptions.length && ensureIsArray(values).length === selectAllEligible.length &&
selectAllMode selectAllMode
) { ) {
newValues = []; const array = selectAllEligible.filter(
newValues = []; option => hasOption(option.value, selectValue) && option.disabled,
);
newValues = mapValues(array, labelInValue);
newOptions = mapOptions(array);
} }
} }
onChange?.(newValues, newOptions); onChange?.(newValues, newOptions);

View File

@ -204,3 +204,21 @@ export const renderSelectOptions = (options: SelectOptionsType) =>
</Option> </Option>
); );
}); });
export const mapValues = (values: SelectOptionsType, labelInValue: boolean) =>
labelInValue
? values.map(opt => ({
key: opt.value,
value: opt.value,
label: opt.label,
}))
: values.map(opt => opt.value);
export const mapOptions = (values: SelectOptionsType) =>
values.map(opt => ({
children: opt.label,
key: opt.value,
value: opt.value,
label: opt.label,
disabled: opt.disabled,
}));