|
1 | 1 | 'use client'; |
2 | | -import {Fragment, Ref, useEffect, useMemo, useRef, useState} from 'react'; |
| 2 | +import {Fragment, Ref, useCallback, useEffect, useMemo, useRef, useState} from 'react'; |
3 | 3 | import {Combobox, ComboboxItem, ComboboxList, ComboboxProvider} from '@ariakit/react'; |
4 | 4 | import {CaretRightIcon, CaretSortIcon, MagnifyingGlassIcon} from '@radix-ui/react-icons'; |
5 | 5 | import * as RadixSelect from '@radix-ui/react-select'; |
@@ -63,26 +63,40 @@ export function PlatformSelector({ |
63 | 63 | const currentPlatformKey = currentPlatform?.key; |
64 | 64 | const [open, setOpen] = useState(false); |
65 | 65 | const [searchValue, setSearchValue] = useState(''); |
| 66 | + const debounceRef = useRef<NodeJS.Timeout>(); |
| 67 | + |
| 68 | + // Debounced search handler to prevent rapid re-renders |
| 69 | + const debouncedSetSearchValue = useCallback((value: string) => { |
| 70 | + if (debounceRef.current) { |
| 71 | + clearTimeout(debounceRef.current); |
| 72 | + } |
| 73 | + debounceRef.current = setTimeout(() => setSearchValue(value), 100); |
| 74 | + }, []); |
66 | 75 |
|
67 | 76 | const matches = useMemo(() => { |
68 | 77 | if (!searchValue) { |
69 | 78 | return platformsAndGuides; |
70 | 79 | } |
| 80 | + |
| 81 | + // Find currently selected platform first to ensure it's never filtered out |
| 82 | + const selectedPlatform = platformsAndGuides.find( |
| 83 | + lang => lang.key === currentPlatformKey |
| 84 | + ); |
| 85 | + |
| 86 | + // Filter out the selected platform from search, then add it back at the end |
| 87 | + const otherPlatforms = platformsAndGuides.filter( |
| 88 | + lang => lang.key !== currentPlatformKey |
| 89 | + ); |
| 90 | + |
71 | 91 | // any of these fields can be used to match the search value |
72 | 92 | const keys = ['title', 'name', 'aliases', 'sdk', 'keywords']; |
73 | | - const matches_ = matchSorter(platformsAndGuides, searchValue, { |
| 93 | + const matches_ = matchSorter(otherPlatforms, searchValue, { |
74 | 94 | keys, |
75 | 95 | threshold: matchSorter.rankings.ACRONYM, |
76 | 96 | }); |
77 | | - // Radix Select does not work if we don't render the selected item, so we |
78 | | - // make sure to include it in the list of matches. |
79 | | - const selectedPlatform = platformsAndGuides.find( |
80 | | - lang => lang.key === currentPlatformKey |
81 | | - ); |
82 | | - if (selectedPlatform && !matches_.includes(selectedPlatform)) { |
83 | | - matches_.push(selectedPlatform); |
84 | | - } |
85 | | - return matches_; |
| 97 | + |
| 98 | + // Always include the selected platform at the beginning |
| 99 | + return selectedPlatform ? [selectedPlatform, ...matches_] : matches_; |
86 | 100 | }, [searchValue, currentPlatformKey, platformsAndGuides]); |
87 | 101 |
|
88 | 102 | const router = useRouter(); |
@@ -117,6 +131,15 @@ export function PlatformSelector({ |
117 | 131 | setStoredPlatformKey(localStorage.getItem('active-platform')); |
118 | 132 | } |
119 | 133 | }, [currentPlatformKey]); |
| 134 | + |
| 135 | + // Cleanup debounce timer on unmount |
| 136 | + useEffect(() => { |
| 137 | + return () => { |
| 138 | + if (debounceRef.current) { |
| 139 | + clearTimeout(debounceRef.current); |
| 140 | + } |
| 141 | + }; |
| 142 | + }, []); |
120 | 143 |
|
121 | 144 | const path = usePathname(); |
122 | 145 | const isPlatformPage = Boolean( |
@@ -144,7 +167,7 @@ export function PlatformSelector({ |
144 | 167 | open={open} |
145 | 168 | setOpen={setOpen} |
146 | 169 | includesBaseElement={false} |
147 | | - setValue={setSearchValue} |
| 170 | + setValue={debouncedSetSearchValue} |
148 | 171 | > |
149 | 172 | <RadixSelect.Trigger aria-label="Platform" className={styles.select}> |
150 | 173 | <RadixSelect.Value placeholder="Choose your SDK" /> |
|
0 commit comments