Enhance Select (#15550)

This commit is contained in:
Geido 2021-07-06 18:07:33 +02:00 committed by GitHub
parent 0af5a3d600
commit 314d49c13b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 50 additions and 98 deletions

View File

@ -18,7 +18,7 @@
*/
import React, { ReactNode, useState, useCallback } from 'react';
import ControlHeader from 'src/explore/components/ControlHeader';
import Select, { SelectProps, OptionsType, OptionsTypePage } from './Select';
import Select, { SelectProps, OptionsTypePage } from './Select';
export default {
title: 'Select',
@ -144,11 +144,6 @@ InteractiveSelect.argTypes = {
disable: true,
},
},
paginatedFetch: {
table: {
disable: true,
},
},
};
InteractiveSelect.story = {
@ -302,7 +297,6 @@ const USERS = [
export const AsyncSelect = ({
withError,
responseTime,
paginatedFetch,
...rest
}: SelectProps & {
withError: boolean;
@ -310,7 +304,7 @@ export const AsyncSelect = ({
}) => {
const [requests, setRequests] = useState<ReactNode[]>([]);
const getResults = (username: string) => {
const getResults = (username?: string) => {
let results: { label: string; value: string }[] = [];
if (!username) {
@ -329,7 +323,7 @@ export const AsyncSelect = ({
return results;
};
const setRequestLog = (username: string, results: number, total: number) => {
const setRequestLog = (results: number, total: number, username?: string) => {
const request = (
<>
Emulating network request with search <b>{username || 'empty'}</b> ...{' '}
@ -343,20 +337,6 @@ export const AsyncSelect = ({
setRequests(requests => [request, ...requests]);
};
const fetchUserList = useCallback(
(search: string): Promise<OptionsType> => {
const username = search.trim().toLowerCase();
return new Promise(resolve => {
const results = getResults(username);
setRequestLog(username, results.length, results.length);
setTimeout(() => {
resolve(results);
}, responseTime * 1000);
});
},
[responseTime],
);
const fetchUserListPage = useCallback(
(
search: string,
@ -367,16 +347,14 @@ export const AsyncSelect = ({
return new Promise(resolve => {
let results = getResults(username);
const totalCount = results.length;
if (paginatedFetch) {
results = results.splice(offset, limit);
}
setRequestLog(username, offset + results.length, totalCount);
results = results.splice(offset, limit);
setRequestLog(offset + results.length, totalCount, username);
setTimeout(() => {
resolve({ data: results, totalCount });
}, responseTime * 1000);
});
},
[paginatedFetch, responseTime],
[responseTime],
);
const fetchUserListError = async (): Promise<OptionsTypePage> =>
@ -393,14 +371,7 @@ export const AsyncSelect = ({
>
<Select
{...rest}
paginatedFetch={paginatedFetch}
options={
withError
? fetchUserListError
: paginatedFetch
? fetchUserListPage
: fetchUserList
}
options={withError ? fetchUserListError : fetchUserListPage}
/>
</div>
<div
@ -425,7 +396,6 @@ export const AsyncSelect = ({
AsyncSelect.args = {
withError: false,
paginatedFetch: false,
pageSize: 10,
allowNewOptions: false,
};

View File

@ -50,6 +50,7 @@ type PickedSelectProps = Pick<
| 'defaultValue'
| 'disabled'
| 'filterOption'
| 'notFoundContent'
| 'onChange'
| 'placeholder'
| 'showSearch'
@ -63,8 +64,6 @@ export type OptionsTypePage = {
totalCount: number;
};
export type OptionsPromise = (search: string) => Promise<OptionsType>;
export type OptionsPagePromise = (
search: string,
offset: number,
@ -77,8 +76,7 @@ export interface SelectProps extends PickedSelectProps {
header?: ReactNode;
mode?: 'single' | 'multiple';
name?: string; // discourage usage
options: OptionsType | OptionsPromise | OptionsPagePromise;
paginatedFetch?: boolean;
options: OptionsType | OptionsPagePromise;
pageSize?: number;
invertSelection?: boolean;
}
@ -94,6 +92,10 @@ const StyledSelect = styled(AntdSelect, {
${({ theme, hasHeader }) => `
width: 100%;
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
&& .ant-select-selector {
border-radius: ${theme.gridUnit}px;
}
`}
`;
@ -120,11 +122,11 @@ const StyledError = styled.div`
`}
`;
// default behaviors
const MAX_TAG_COUNT = 4;
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
const DEBOUNCE_TIMEOUT = 500;
const DEFAULT_PAGE_SIZE = 50;
const EMPTY_OPTIONS: OptionsType = [];
const Error = ({ error }: { error: string }) => (
<StyledError>
@ -135,11 +137,10 @@ const Error = ({ error }: { error: string }) => (
const Select = ({
allowNewOptions = false,
ariaLabel,
filterOption,
filterOption = true,
header = null,
mode = 'single',
name,
paginatedFetch,
pageSize = DEFAULT_PAGE_SIZE,
placeholder = t('Select ...'),
options,
@ -151,8 +152,11 @@ const Select = ({
const isAsync = typeof options === 'function';
const isSingleMode = mode === 'single';
const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch;
const initialOptions = options && Array.isArray(options) ? options : [];
const [selectOptions, setOptions] = useState<OptionsType>(initialOptions);
const initialOptions =
options && Array.isArray(options) ? options : EMPTY_OPTIONS;
const [selectOptions, setSelectOptions] = useState<OptionsType>(
initialOptions,
);
const [selectValue, setSelectValue] = useState(value);
const [searchedValue, setSearchedValue] = useState('');
const [isLoading, setLoading] = useState(false);
@ -167,6 +171,16 @@ const Select = ({
? 'tags'
: 'multiple';
useEffect(() => {
setSelectOptions(
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
);
}, [options]);
useEffect(() => {
setSelectValue(value);
}, [value]);
const handleTopOptions = useCallback(
(selectedValue: AntdSelectValue | undefined) => {
// bringing selected options to the top of the list
@ -193,7 +207,7 @@ const Select = ({
const sortedOptions = [...topOptions, ...otherOptions];
if (!isEqual(sortedOptions, selectOptions)) {
setOptions(sortedOptions);
setSelectOptions(sortedOptions);
}
}
},
@ -244,7 +258,7 @@ const Select = ({
const handleData = (data: OptionsType) => {
if (data && Array.isArray(data) && data.length) {
// merges with existing and creates unique options
setOptions(prevOptions => [
setSelectOptions(prevOptions => [
...prevOptions,
...data.filter(
newOpt =>
@ -254,24 +268,6 @@ const Select = ({
}
};
const handleFetch = useMemo(
() => (value: string) => {
if (fetchedQueries.current.has(value)) {
return;
}
setLoading(true);
const fetchOptions = options as OptionsPromise;
fetchOptions(value)
.then((data: OptionsType) => {
handleData(data);
fetchedQueries.current.add(value);
})
.catch(onError)
.finally(() => setLoading(false));
},
[options],
);
const handlePaginatedFetch = useMemo(
() => (value: string, offset: number, limit: number) => {
const key = `${value};${offset};${limit}`;
@ -305,7 +301,7 @@ const Select = ({
!initialOptions.find(o => o.value === searchedValue)
) {
selectOptions.shift();
setOptions(selectOptions);
setSelectOptions(selectOptions);
}
if (searchValue && !hasOption(searchValue, selectOptions)) {
const newOption = {
@ -314,7 +310,7 @@ const Select = ({
};
// adds a custom option
const newOptions = [...selectOptions, newOption];
setOptions(newOptions);
setSelectOptions(newOptions);
setSelectValue(searchValue);
}
}
@ -337,24 +333,22 @@ const Select = ({
};
const handleFilterOption = (search: string, option: AntdLabeledValue) => {
const searchValue = search.trim().toLowerCase();
if (filterOption && typeof filterOption === 'boolean') return filterOption;
if (filterOption && typeof filterOption === 'function') {
if (typeof filterOption === 'function') {
return filterOption(search, option);
}
const { value, label } = option;
if (
value &&
label &&
typeof value === 'string' &&
typeof label === 'string'
) {
if (filterOption) {
const searchValue = search.trim().toLowerCase();
const { value, label } = option;
const valueText = String(value);
const labelText = String(label);
return (
value.toLowerCase().includes(searchValue) ||
label.toLowerCase().includes(searchValue)
valueText.toLowerCase().includes(searchValue) ||
labelText.toLowerCase().includes(searchValue)
);
}
return true;
return false;
};
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
@ -369,23 +363,11 @@ const Select = ({
useEffect(() => {
const foundOption = hasOption(searchedValue, selectOptions);
if (isAsync && !foundOption) {
if (paginatedFetch) {
const offset = 0;
handlePaginatedFetch(searchedValue, offset, pageSize);
setOffset(offset);
} else {
handleFetch(searchedValue);
}
const offset = 0;
handlePaginatedFetch(searchedValue, offset, pageSize);
setOffset(offset);
}
}, [
isAsync,
handleFetch,
searchedValue,
selectOptions,
pageSize,
paginatedFetch,
handlePaginatedFetch,
]);
}, [isAsync, searchedValue, selectOptions, pageSize, handlePaginatedFetch]);
useEffect(() => {
if (isSingleMode) {
@ -416,7 +398,7 @@ const Select = ({
mode={mappedMode}
onDeselect={handleOnDeselect}
onDropdownVisibleChange={handleOnDropdownVisibleChange}
onPopupScroll={paginatedFetch ? handlePagination : undefined}
onPopupScroll={isAsync ? handlePagination : undefined}
onSearch={handleOnSearch}
onSelect={handleOnSelect}
onClear={() => setSelectValue(undefined)}