mirror of
https://github.com/apache/superset.git
synced 2024-09-18 19:49:37 -04:00
fix: Handles disabled options on Select All (#22830)
This commit is contained in:
parent
ddd8d17aa4
commit
5e64211bdb
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
}));
|
||||||
|
Loading…
Reference in New Issue
Block a user