diff --git a/src/components/Select.js b/src/components/Select.js index 9c9a39be0..141a2d190 100644 --- a/src/components/Select.js +++ b/src/components/Select.js @@ -1,11 +1,9 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import ReactSelect from 'react-select'; import cn from 'classnames'; import * as css from './Select.module.css'; -const toOption = (value) => ({ value, label: value }); - export const Select = ({ title, options, @@ -17,8 +15,12 @@ export const Select = ({ variant, instanceId }) => { + const [input, setInput] = useState(''); const opts = useMemo(() => options.map(toOption), [options]); - const handleOnChange = (o, action) => onChange(o ? o.value : o); + const filteredOpts = useMemo( + () => (input === '' ? opts : filterAndSortRank(opts, input)), + [opts, input] + ); return (
@@ -36,10 +38,12 @@ export const Select = ({ placeholder={placeholder} className={css.select} classNamePrefix="rs" - options={opts} - defaultValue={selected ? toOption(selected) : selected} - onChange={handleOnChange} + options={filteredOpts} + defaultValue={selected ? toOption(selected) : ''} + onChange={(o) => onChange(o ? o.value : null)} instanceId={instanceId} + onInputChange={setInput} + filterOption={() => true} />
@@ -48,4 +52,49 @@ export const Select = ({
); }; + export default Select; + +const toOption = (value) => ({ value, label: value }); + +// trim, lowercase and strip accents +const normalize = (value) => + value + .trim() + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + +const rank = (value, input) => { + // exact match: highest priority + if (value === input) return 0; + + // complete word match: higher priority based on word position + const words = value.split(' '); + for (let i = 0; i < words.length; i++) { + if (words[i] === input) return i + 1; + } + + // partial match: lower priority based on character position + const index = value.indexOf(input); + return index === -1 ? Number.MAX_SAFE_INTEGER : 1000 + index; +}; + +const filterAndSortRank = (options, input) => { + // It doesn't seem possible to only sort the filtered options in react-select, but we can re-implement the filtering to do so. + // https://github.com/JedWatson/react-select/discussions/4426 + + const normalizedInput = normalize(input); + + return options + .filter((o) => normalize(o.value).includes(normalizedInput)) + .sort((optA, optB) => { + const rankDelta = + rank(normalize(optA.value), normalizedInput) - + rank(normalize(optB.value), normalizedInput); + + if (rankDelta !== 0) return rankDelta; + + return optA.value.localeCompare(optB.value); + }); +};