-
Notifications
You must be signed in to change notification settings - Fork 3.4k
feat(component-testing): Fuzzy search #15546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ac8a3b
088e5c5
0ff484b
cbb12a7
c5a8f58
d8a4c16
ed80b68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from './useCurrent' | ||
|
|
||
| export * from './useFuzzySearch' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { useLayoutEffect, useRef } from 'react' | ||
|
|
||
| /** | ||
| * Wraps the provided `value` in a Concurrent safe ref for consumption in `useEffect` and similar without forcing reruns on value changes | ||
| * | ||
| * **CAUTION:** `useCurrent` makes it easy to shoot yourself in the foot, particularly in scenarios involving callbacks. Use sparingly | ||
| */ | ||
| export const useCurrent = <T>(value: T) => { | ||
| const ref = useRef(value) | ||
|
|
||
| useLayoutEffect(() => { | ||
| ref.current = value | ||
| }, [value]) | ||
|
|
||
| return ref | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||
| import Fuse from 'fuse.js' | ||||||
| import { useEffect, useLayoutEffect, useMemo, useState } from 'react' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
What do you think about this? Actually, this is the only right way to import React as ESM module. Moreover it is much easier to read code when default react hooks are started with facebook/react#18102
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is this actually relevant to us? Why would it matter?
I disagree with this and see no significant difference other than styling. I will not be changing it for your (CT) codebase, but for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did post 2 links. You are doing actually an invalid thing, you are trying to import UMD using esm default export. Once react will finally have esm export this will not be possible. So it's not the styling.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know very little about the crazy mess that is different types of JS modules, but the most recent React team interaction on modules/importing was the following: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports In particular, I will point out this:
As a question of my own, don't
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, just because for now it's still an all-piece of react in one object that just exports different parts. But yeah export default is the biggest mess. |
||||||
|
|
||||||
| /** | ||||||
| * Supports all Fuse options, with modified key definitions | ||||||
| */ | ||||||
| export interface FuzzySearchConfig<T> extends Omit<Fuse.IFuseOptions<T>, 'keys'> { | ||||||
| keys?: { | ||||||
| [Key in keyof T]?: true | { | ||||||
| weight: number | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Provides fuzzy matching search over the supplied items, using weighted keys | ||||||
| * @param items The items to search | ||||||
| * @param searchInput A search string, provided if the hook is driven by external state | ||||||
| * @param config The configuration of how to perform searches, based on Fuse config options | ||||||
| * | ||||||
| * @see https://fusejs.io/api/options.html | ||||||
| */ | ||||||
| export const useFuzzySearch = <T>(items: T[], searchInput?: string, config?: FuzzySearchConfig<T>): { | ||||||
| searchInput: string | ||||||
| results: Fuse.FuseResult<T>[] | undefined | ||||||
| orderedResults: T[] | ||||||
| onSearch: (searchInput: string) => void | ||||||
| } => { | ||||||
| const finalConfig = useMemo((): Fuse.IFuseOptions<T> | undefined => { | ||||||
| if (!config?.keys) { | ||||||
| return config as Fuse.IFuseOptions<T> | ||||||
| } | ||||||
|
|
||||||
| return { | ||||||
| ...config, | ||||||
| keys: Object.keys(config.keys).map((key) => { | ||||||
| const keyEntry = config.keys?.[key as keyof T] as true | { | ||||||
| weight: number | ||||||
| } | undefined | ||||||
|
|
||||||
| if (keyEntry === undefined) { | ||||||
agg23 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| throw new Error('Unreachable branch. keyEntry does not exist') | ||||||
| } | ||||||
|
|
||||||
| if (typeof keyEntry === 'boolean') { | ||||||
| return key | ||||||
| } | ||||||
|
|
||||||
| return { | ||||||
| ...keyEntry, | ||||||
| name: key, | ||||||
| } | ||||||
| }), | ||||||
| } | ||||||
| }, [config]) | ||||||
|
|
||||||
| const [cachedFuse, setCachedFuse] = useState<Fuse<T> | undefined>(() => new Fuse(items, finalConfig)) | ||||||
| const [lastSearchInput, setLastSearchInput] = useState<string>('') | ||||||
|
|
||||||
| const results = useMemo(() => lastSearchInput !== '' ? cachedFuse?.search(lastSearchInput) : undefined, [lastSearchInput, cachedFuse]) | ||||||
| const orderedResults = useMemo(() => results?.map((r) => r.item), [results]) | ||||||
|
|
||||||
| useLayoutEffect(() => setLastSearchInput(searchInput ?? ''), [searchInput]) | ||||||
|
|
||||||
| useEffect(() => setCachedFuse(new Fuse(items, finalConfig)), [items, finalConfig]) | ||||||
|
|
||||||
| return { | ||||||
| searchInput: lastSearchInput, | ||||||
| results, | ||||||
| // If orderedResults is undefined, that means we didn't search (empty searchInput) | ||||||
| orderedResults: orderedResults !== undefined ? orderedResults : items, | ||||||
| onSearch: setLastSearchInput, | ||||||
| } | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.