This repository has been archived by the owner on Nov 16, 2023. It is now read-only.
/
usePicker.ts
123 lines (110 loc) · 4.05 KB
/
usePicker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as React from 'react'
import * as Fuse from 'fuse.js'
import { IOption } from './models'
import { FuseResult, MatchedOption, convertMatchedTextIntoMatchedOption } from '../FuseMatch'
/**
* See http://fusejs.io/ for information about options meaning and configuration
*/
const fuseOptions: Fuse.FuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.4,
location: 0,
distance: 10,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"name"
]
}
type IndexFunction = (x: number, limit?: number) => number
// TODO: Id function doesn't need limit but TS requires consistent arguments
const id = (x: number) => x
const increment = (x: number, limit: number) => (x + 1) > limit ? 0 : x + 1
const decrement = (x: number, limit: number) => (x - 1) < 0 ? limit : x - 1
const convertOptionToMatchedOption = (option: IOption): MatchedOption<IOption> => {
return {
highlighted: false,
matchedStrings: [{ text: option.name, matched: false }],
original: option
}
}
const getMatchedOptions = (searchText: string, options: IOption[], fuse: Fuse, maxDisplayedOptions: number): MatchedOption<IOption>[] => {
return searchText.trim().length === 0
? options
.filter((_, i) => i < maxDisplayedOptions)
.map(convertOptionToMatchedOption)
: fuse.search<FuseResult<IOption>>(searchText)
.filter((_, i) => i < maxDisplayedOptions)
.map(result => convertMatchedTextIntoMatchedOption(result.item.name, result.matches[0].indices, result.item))
}
export const usePicker = (
options: IOption[],
maxDisplayedOptions: number,
onSelectOption: (option: IOption) => void,
) => {
const fuseRef = React.useRef(new Fuse(options, fuseOptions))
const [searchText, setSearchText] = React.useState('')
const [highlightIndex, setHighlighIndex] = React.useState(0)
const [matchedOptions, setMatchedOptions] = React.useState<MatchedOption<IOption>[]>([])
const resetHighlighIndex = () => setHighlighIndex(0)
const onClickOption = (option: IOption) => onSelectOption(option)
const onSelectHighlightedOption = () => {
const option = matchedOptions[highlightIndex]
if (option) {
onSelectOption(option.original)
}
}
React.useEffect(() => {
fuseRef.current = new Fuse(options, fuseOptions)
const computed = getMatchedOptions(searchText, options, fuseRef.current, maxDisplayedOptions)
setMatchedOptions(computed)
}, [options.length, searchText])
// Ensure highlight index is within bounds
React.useEffect(() => {
// Decrease highlight index to last item when options list shrinks due to search filter
let min = highlightIndex > (matchedOptions.length - 1)
? (matchedOptions.length - 1)
: highlightIndex
// Don't allow an index less than 0 (if options length is 0)
min = Math.max(0, min)
setHighlighIndex(min)
}, [matchedOptions.length])
const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
let modifyFunction: IndexFunction = id
switch (event.key) {
case 'ArrowUp': {
modifyFunction = decrement
break;
}
case 'ArrowDown':
modifyFunction = increment
break;
case 'Enter':
case 'Tab':
// Only simulate completion on 'forward' tab
if (event.shiftKey) {
return
}
onSelectHighlightedOption()
event.stopPropagation()
event.preventDefault()
break;
default:
}
setHighlighIndex(modifyFunction(highlightIndex, matchedOptions.length - 1))
}
return {
searchText,
setSearchText,
onKeyDown,
matchedOptions,
onClickOption,
highlightIndex,
resetHighlighIndex,
}
}