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

View File

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