Skip to content

Commit fe4c44c

Browse files
authored
Merge pull request #1652 from fturmel/PR/select-filters-sort-order
Improve sort order of `Select` component filtered results
2 parents e6650f7 + 8cd9766 commit fe4c44c

File tree

1 file changed

+56
-7
lines changed

1 file changed

+56
-7
lines changed

src/components/Select.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import ReactSelect from 'react-select';
33
import cn from 'classnames';
44

55
import * as css from './Select.module.css';
66

7-
const toOption = (value) => ({ value, label: value });
8-
97
export const Select = ({
108
title,
119
options,
@@ -17,8 +15,12 @@ export const Select = ({
1715
variant,
1816
instanceId
1917
}) => {
18+
const [input, setInput] = useState('');
2019
const opts = useMemo(() => options.map(toOption), [options]);
21-
const handleOnChange = (o, action) => onChange(o ? o.value : o);
20+
const filteredOpts = useMemo(
21+
() => (input === '' ? opts : filterAndSortRank(opts, input)),
22+
[opts, input]
23+
);
2224

2325
return (
2426
<section className={cn(css.root, className, { [css[variant]]: variant })}>
@@ -36,10 +38,12 @@ export const Select = ({
3638
placeholder={placeholder}
3739
className={css.select}
3840
classNamePrefix="rs"
39-
options={opts}
40-
defaultValue={selected ? toOption(selected) : selected}
41-
onChange={handleOnChange}
41+
options={filteredOpts}
42+
defaultValue={selected ? toOption(selected) : ''}
43+
onChange={(o) => onChange(o ? o.value : null)}
4244
instanceId={instanceId}
45+
onInputChange={setInput}
46+
filterOption={() => true}
4347
/>
4448

4549
<div className={css.itemSpacer}></div>
@@ -48,4 +52,49 @@ export const Select = ({
4852
</section>
4953
);
5054
};
55+
5156
export default Select;
57+
58+
const toOption = (value) => ({ value, label: value });
59+
60+
// trim, lowercase and strip accents
61+
const normalize = (value) =>
62+
value
63+
.trim()
64+
.toLowerCase()
65+
.normalize('NFD')
66+
.replace(/[\u0300-\u036f]/g, '');
67+
68+
const rank = (value, input) => {
69+
// exact match: highest priority
70+
if (value === input) return 0;
71+
72+
// complete word match: higher priority based on word position
73+
const words = value.split(' ');
74+
for (let i = 0; i < words.length; i++) {
75+
if (words[i] === input) return i + 1;
76+
}
77+
78+
// partial match: lower priority based on character position
79+
const index = value.indexOf(input);
80+
return index === -1 ? Number.MAX_SAFE_INTEGER : 1000 + index;
81+
};
82+
83+
const filterAndSortRank = (options, input) => {
84+
// It doesn't seem possible to only sort the filtered options in react-select, but we can re-implement the filtering to do so.
85+
// https://github.com/JedWatson/react-select/discussions/4426
86+
87+
const normalizedInput = normalize(input);
88+
89+
return options
90+
.filter((o) => normalize(o.value).includes(normalizedInput))
91+
.sort((optA, optB) => {
92+
const rankDelta =
93+
rank(normalize(optA.value), normalizedInput) -
94+
rank(normalize(optB.value), normalizedInput);
95+
96+
if (rankDelta !== 0) return rankDelta;
97+
98+
return optA.value.localeCompare(optB.value);
99+
});
100+
};

0 commit comments

Comments
 (0)