From 6ad09a31683fc282ca792a3257736f723667cefe Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Mon, 6 Oct 2025 11:21:29 -0300 Subject: [PATCH 01/11] :construction: Get intial iteration working with Cursor --- .../searchQueryBuilder/tokens/combobox.tsx | 18 ++++-- .../tokens/filter/valueCombobox.tsx | 57 ++++++++++++++++--- .../tokens/filter/valueListBox.tsx | 45 ++++++++++++++- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/combobox.tsx b/static/app/components/searchQueryBuilder/tokens/combobox.tsx index 82a1516af3300e..6949ae741ef2ac 100644 --- a/static/app/components/searchQueryBuilder/tokens/combobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/combobox.tsx @@ -95,10 +95,11 @@ type SearchQueryBuilderComboboxProps} ) => void; onKeyUp?: (e: KeyboardEvent) => void; - onOpenChange?: (newOpenState: boolean) => void; + onOpenChange?: (newOpenState: boolean, state?: any) => void; onPaste?: (e: React.ClipboardEvent) => void; openOnFocus?: boolean; placeholder?: string; + preserveFocusOnInputChange?: boolean; ref?: React.Ref; /** * Function to determine whether the menu should close when interacting with @@ -365,6 +366,7 @@ export function SearchQueryBuilderCombobox< onClick, customMenu, isOpen: incomingIsOpen, + preserveFocusOnInputChange = false, ['data-test-id']: dataTestId, ref, }: SearchQueryBuilderComboboxProps) { @@ -462,10 +464,15 @@ export function SearchQueryBuilderCombobox< const previousInputValue = usePrevious(inputValue); useEffect(() => { - if (inputValue !== previousInputValue) { + if (!preserveFocusOnInputChange && inputValue !== previousInputValue) { state.selectionManager.setFocusedKey(null); } - }, [inputValue, previousInputValue, state.selectionManager]); + }, [ + inputValue, + previousInputValue, + state.selectionManager, + preserveFocusOnInputChange, + ]); const totalOptions = items.reduce( (acc, item) => acc + (itemIsSection(item) ? item.options.length : 1), @@ -482,9 +489,10 @@ export function SearchQueryBuilderCombobox< isOpen: incomingIsOpen, }); + // Always notify parent of state so they can maintain up-to-date references useEffect(() => { - onOpenChange?.(isOpen); - }, [onOpenChange, isOpen]); + onOpenChange?.(isOpen, state); + }, [onOpenChange, isOpen, state]); const { overlayProps, diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index ca21492a1a957d..fa3b219a0d9a25 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -323,10 +323,12 @@ function useFilterSuggestions({ token, filterValue, selectedValues, + initialSelectedValues, ctrlKeyPressed, }: { ctrlKeyPressed: boolean; filterValue: string; + initialSelectedValues: Array<{selected: boolean; value: string}>; selectedValues: Array<{selected: boolean; value: string}>; token: TokenResult; }) { @@ -381,8 +383,13 @@ function useFilterSuggestions({ enabled: shouldFetchValues, }); + // Create a ref to hold current selected values for the checkbox to access + // without causing item recreation + const selectedValuesRef = useRef(selectedValues); + selectedValuesRef.current = selectedValues; + const createItem = useCallback( - (suggestion: SuggestionItem, selected = false) => { + (suggestion: SuggestionItem) => { return { label: suggestion.label ?? suggestion.value, value: suggestion.value, @@ -395,6 +402,11 @@ function useFilterSuggestions({ return null; } + // Look up selected state dynamically from ref to avoid recreating items + const selected = selectedValuesRef.current.some( + v => v.value === suggestion.value && v.selected + ); + return ( (() => { const itemsWithoutSection = suggestionGroups .filter(group => group.sectionText === '') .flatMap(group => group.suggestions) - .filter(suggestion => !selectedValues.some(v => v.value === suggestion.value)); + .filter( + suggestion => !initialSelectedValues.some(v => v.value === suggestion.value) + ); const sections = suggestionGroups.filter(group => group.sectionText !== ''); return [ { sectionText: '', items: getItemsWithKeys([ - ...selectedValues.map(value => { + ...initialSelectedValues.map(value => { const matchingSuggestion = suggestionGroups .flatMap(group => group.suggestions) .find(suggestion => suggestion.value === value.value); if (matchingSuggestion) { - return createItem(matchingSuggestion, value.selected); + return createItem(matchingSuggestion); } - return createItem({value: value.value}, value.selected); + return createItem({value: value.value}); }), ...itemsWithoutSection.map(suggestion => createItem(suggestion)), ]), @@ -463,12 +479,14 @@ function useFilterSuggestions({ sectionText: group.sectionText, items: getItemsWithKeys( group.suggestions - .filter(suggestion => !selectedValues.some(v => v.value === suggestion.value)) + .filter( + suggestion => !initialSelectedValues.some(v => v.value === suggestion.value) + ) .map(suggestion => createItem(suggestion)) ), })), ]; - }, [createItem, selectedValues, suggestionGroups]); + }, [createItem, initialSelectedValues, suggestionGroups]); // Flat list used for state management const items = useMemo(() => { @@ -546,6 +564,7 @@ export function SearchQueryBuilderValueCombobox({ }: SearchQueryValueBuilderProps) { const ref = useRef(null); const inputRef = useRef(null); + const comboboxStateRef = useRef(null); const organization = useOrganization(); const { getFieldDefinition, @@ -613,10 +632,14 @@ export function SearchQueryBuilderValueCombobox({ } }, []); + // Track the initial selected values when the combobox is mounted to preserve list order during selection + const initialSelectedValues = useRef(selectedValuesUnescaped); + const {items, suggestionSectionItems, isFetching} = useFilterSuggestions({ token, filterValue, selectedValues: selectedValuesUnescaped, + initialSelectedValues: initialSelectedValues.current, ctrlKeyPressed, }); @@ -933,14 +956,30 @@ export function SearchQueryBuilderValueCombobox({ token={token} inputLabel={t('Edit filter value')} onInputChange={e => setInputValue(e.target.value)} - onKeyDown={onKeyDown} + onKeyDown={(e, {state}) => { + comboboxStateRef.current = state; + onKeyDown(e); + }} onKeyUp={updateSelectionIndex} - onClick={updateSelectionIndex} + onClick={() => { + updateSelectionIndex(); + // Also capture state on click to ensure it's available for mouse selections + if (inputRef.current) { + inputRef.current.focus(); + } + }} + onOpenChange={(_isOpen, state) => { + // Capture the combobox state so we can restore focus after selections + if (state) { + comboboxStateRef.current = state; + } + }} autoFocus maxOptions={50} openOnFocus customMenu={customMenu} shouldCloseOnInteractOutside={shouldCloseOnInteractOutside} + preserveFocusOnInputChange={canSelectMultipleValues} > {suggestionSectionItems.map(section => (
diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx index 930bc2a2a6e6df..0ac1308d7e67ad 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx @@ -1,7 +1,9 @@ -import {Fragment, useCallback} from 'react'; +import {Fragment, useCallback, useEffect, useLayoutEffect, useRef} from 'react'; import {createPortal} from 'react-dom'; import styled from '@emotion/styled'; +import {getItemId} from '@react-aria/listbox'; import {isMac} from '@react-aria/utils'; +import type {Key} from '@react-types/shared'; import {ListBox} from 'sentry/components/core/compactSelect/listBox'; import type {SelectOptionOrSectionWithKey} from 'sentry/components/core/compactSelect/types'; @@ -118,6 +120,47 @@ export function ValueListBox>({ token, wrapperRef, }: ValueListBoxProps) { + // Track and restore focused option if react-aria clears it during multi-select updates + const lastFocusedKeyRef = useRef(null); + + const centerKeyInView = useCallback( + (key: Key | null) => { + if (key === null) return; + const container = listBoxRef.current; + if (!container) return; + const elId = getItemId(state as any, key as any); + if (!elId) return; + const el = document.getElementById(elId); + if (!el) return; + + const containerRect = container.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const offsetTopWithinContainer = + container.scrollTop + (elRect.top - containerRect.top); + const targetScrollTop = + offsetTopWithinContainer - container.clientHeight / 2 + elRect.height / 2; + + container.scrollTop = Math.max(0, targetScrollTop); + }, + [listBoxRef, state] + ); + + useEffect(() => { + lastFocusedKeyRef.current = state.selectionManager.focusedKey; + }, [state.selectionManager.focusedKey]); + + useLayoutEffect(() => { + if (!isOpen) return; + if ( + lastFocusedKeyRef.current !== null && + state.selectionManager.focusedKey === null + ) { + const keyToRestore = lastFocusedKeyRef.current; + state.selectionManager.setFocusedKey(keyToRestore); + // Center the restored option on next tick + setTimeout(() => centerKeyInView(keyToRestore), 0); + } + }, [isOpen, state, centerKeyInView]); const totalOptions = items.reduce( (acc, item) => acc + (itemIsSection(item) ? item.options.length : 1), 0 From d9c1997cbddbe34922c45a2f013f6e6e6509d104 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Oct 2025 13:18:12 -0300 Subject: [PATCH 02/11] :construction: Cursor finally got everything playing nicely --- .../tokens/filter/valueCombobox.tsx | 88 ++++++++++++++++--- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index fa3b219a0d9a25..e46c323cab0763 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -449,18 +449,32 @@ function useFilterSuggestions({ // Use initialSelectedValues for ordering (to avoid re-ordering during selection) // Selected state is looked up dynamically in the checkbox render to avoid recreating items const suggestionSectionItems = useMemo(() => { + const allSuggestionValues = suggestionGroups + .flatMap(group => group.suggestions) + .map(s => s.value); + + // Build list with initialSelectedValues at top, followed by all other suggestions + const initialValueSet = new Set(initialSelectedValues.map(v => v.value)); + const itemsWithoutSection = suggestionGroups .filter(group => group.sectionText === '') .flatMap(group => group.suggestions) - .filter( - suggestion => !initialSelectedValues.some(v => v.value === suggestion.value) - ); + .filter(suggestion => !initialValueSet.has(suggestion.value)); const sections = suggestionGroups.filter(group => group.sectionText !== ''); + // Add the current filterValue as an option if it's not empty and not already in the list + const trimmedFilterValue = filterValue.trim(); + const shouldShowFilterValue = + canSelectMultipleValues && + trimmedFilterValue.length > 0 && + !allSuggestionValues.includes(trimmedFilterValue) && + !initialValueSet.has(trimmedFilterValue); + return [ { sectionText: '', items: getItemsWithKeys([ + // Show initial selected values at top (these were added via typing+comma) ...initialSelectedValues.map(value => { const matchingSuggestion = suggestionGroups .flatMap(group => group.suggestions) @@ -472,6 +486,9 @@ function useFilterSuggestions({ return createItem({value: value.value}); }), + // Show currently typed value if it's new + ...(shouldShowFilterValue ? [createItem({value: filterValue.trim()})] : []), + // Then all other suggestions in their original order ...itemsWithoutSection.map(suggestion => createItem(suggestion)), ]), }, @@ -479,14 +496,22 @@ function useFilterSuggestions({ sectionText: group.sectionText, items: getItemsWithKeys( group.suggestions - .filter( - suggestion => !initialSelectedValues.some(v => v.value === suggestion.value) - ) + .filter(suggestion => !initialValueSet.has(suggestion.value)) .map(suggestion => createItem(suggestion)) ), })), ]; - }, [createItem, initialSelectedValues, suggestionGroups]); + // selectedValues is needed to trigger re-renders when checkboxes are toggled + // initialSelectedValues is a ref so it won't trigger re-renders, but we list it for correctness + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + createItem, + suggestionGroups, + canSelectMultipleValues, + filterValue, + selectedValues, + initialSelectedValues, + ]); // Flat list used for state management const items = useMemo(() => { @@ -497,6 +522,7 @@ function useFilterSuggestions({ items, suggestionSectionItems, isFetching: isFetching || isDebouncing, + suggestionGroups, }; } @@ -634,14 +660,48 @@ export function SearchQueryBuilderValueCombobox({ // Track the initial selected values when the combobox is mounted to preserve list order during selection const initialSelectedValues = useRef(selectedValuesUnescaped); + const prevInputValue = useRef(inputValue); - const {items, suggestionSectionItems, isFetching} = useFilterSuggestions({ - token, - filterValue, - selectedValues: selectedValuesUnescaped, - initialSelectedValues: initialSelectedValues.current, - ctrlKeyPressed, - }); + const {items, suggestionSectionItems, isFetching, suggestionGroups} = + useFilterSuggestions({ + token, + filterValue, + selectedValues: selectedValuesUnescaped, + initialSelectedValues: initialSelectedValues.current, + ctrlKeyPressed, + }); + + // Update initialSelectedValues only for custom typed values (not existing suggestions) + useEffect(() => { + if (!canSelectMultipleValues) { + return; + } + + // Only process if inputValue actually changed + if (prevInputValue.current === inputValue) { + return; + } + prevInputValue.current = inputValue; + + // Get all values that exist in suggestions + const allSuggestionValues = new Set( + suggestionGroups.flatMap(group => group.suggestions).map(s => s.value) + ); + + // Find new selected values that are NOT in suggestions (i.e., custom typed values) + const currentInitialSet = new Set(initialSelectedValues.current.map(v => v.value)); + const newCustomValues = selectedValuesUnescaped.filter( + v => + v.selected && !currentInitialSet.has(v.value) && !allSuggestionValues.has(v.value) + ); + + if (newCustomValues.length > 0) { + initialSelectedValues.current = [ + ...initialSelectedValues.current, + ...newCustomValues, + ]; + } + }, [inputValue, canSelectMultipleValues, selectedValuesUnescaped, suggestionGroups]); const analyticsData = useMemo( () => ({ From f88dc2640c0baded3fa37fd2a0c626ca5c809c39 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Oct 2025 13:18:37 -0300 Subject: [PATCH 03/11] :white_check_mark: Add in test to check re-ordering happens after menu is closed and re-opened --- .../searchQueryBuilder/index.spec.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 4160fad689d866..17efbfa40357de 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -2320,6 +2320,38 @@ describe('SearchQueryBuilder', () => { }); }); + it('sorts multi-selected values after re-opening the combobox', async () => { + const user = userEvent.setup(); + render(); + + await userEvent.click( + screen.getByRole('button', {name: 'Edit value for filter: browser.name'}) + ); + + const checkboxOptions = await screen.findAllByRole('checkbox'); + expect(checkboxOptions).toHaveLength(4); + expect(checkboxOptions[0]).toHaveAccessibleName('Toggle Chrome'); + expect(checkboxOptions[1]).toHaveAccessibleName('Toggle Firefox'); + + await user.keyboard('{Control>}'); + await user.click(checkboxOptions[1]!); + + expect(checkboxOptions[1]).toHaveAccessibleName('Toggle Firefox'); + expect(checkboxOptions[1]).toBeChecked(); + + await user.keyboard('{Escape}'); + await userEvent.click( + screen.getByRole('button', {name: 'Edit value for filter: browser.name'}) + ); + + const updatedCheckboxOptions = await screen.findAllByRole('checkbox'); + expect(updatedCheckboxOptions).toHaveLength(4); + expect(updatedCheckboxOptions[0]).toHaveAccessibleName('Toggle Firefox'); + expect(updatedCheckboxOptions[0]).toBeChecked(); + expect(updatedCheckboxOptions[1]).toHaveAccessibleName('Toggle Chrome'); + expect(updatedCheckboxOptions[1]).not.toBeChecked(); + }); + it('collapses many selected options', async () => { render( Date: Tue, 7 Oct 2025 13:46:49 -0300 Subject: [PATCH 04/11] :fire: Remove redundent comment --- .../searchQueryBuilder/tokens/filter/valueCombobox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index e46c323cab0763..cffd3c1768ef21 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -502,7 +502,6 @@ function useFilterSuggestions({ })), ]; // selectedValues is needed to trigger re-renders when checkboxes are toggled - // initialSelectedValues is a ref so it won't trigger re-renders, but we list it for correctness // eslint-disable-next-line react-hooks/exhaustive-deps }, [ createItem, From 807d98de5e108fc4629d2a7ee2307441ce1c6658 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Oct 2025 15:48:36 -0300 Subject: [PATCH 05/11] :bug: Fix bug where it was explicit enough in checking for selected item --- .../searchQueryBuilder/tokens/filter/valueCombobox.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index cffd3c1768ef21..3af7636756e7de 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -144,10 +144,9 @@ function getSelectedValuesFromText( // Check if this value is selected by looking at the character after the value in // the text. If there's a comma after the value, it means this value is selected. - // We need to check the text content to ensure that we account for any quotes the - // user may have added. - const valueText = item.value?.text ?? ''; - const selected = text.charAt(text.indexOf(valueText) + valueText.length) === ','; + // Use the actual token location from the parser instead of indexOf to avoid + // matching substrings (e.g., "artifact_bundle.assemble" inside "artifact_bundle.assemble.find_projects") + const selected = text.charAt(item.value?.location.end.offset ?? 0) === ','; return {value, selected}; }); From 8b1af2e2d93fd6d21bed903837f2eb8259bfaae1 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Oct 2025 15:49:04 -0300 Subject: [PATCH 06/11] :fire: Remove setTimeout --- .../searchQueryBuilder/tokens/filter/valueListBox.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx index 0ac1308d7e67ad..c8fb28a0d0bf94 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx @@ -157,8 +157,7 @@ export function ValueListBox>({ ) { const keyToRestore = lastFocusedKeyRef.current; state.selectionManager.setFocusedKey(keyToRestore); - // Center the restored option on next tick - setTimeout(() => centerKeyInView(keyToRestore), 0); + centerKeyInView(keyToRestore); } }, [isOpen, state, centerKeyInView]); const totalOptions = items.reduce( From a89ffa015ebe1967f4ffc9cc68b3febd0897308d Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Oct 2025 07:38:32 -0300 Subject: [PATCH 07/11] :bulb: Shorten comment --- .../searchQueryBuilder/tokens/filter/valueCombobox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index 3af7636756e7de..ff3abef71377d7 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -145,7 +145,7 @@ function getSelectedValuesFromText( // Check if this value is selected by looking at the character after the value in // the text. If there's a comma after the value, it means this value is selected. // Use the actual token location from the parser instead of indexOf to avoid - // matching substrings (e.g., "artifact_bundle.assemble" inside "artifact_bundle.assemble.find_projects") + // matching substrings (e.g., "cool.property" inside "cool.property.thing") const selected = text.charAt(item.value?.location.end.offset ?? 0) === ','; return {value, selected}; From 8fa391a5dee12127a1f2889faec758ab8d9ba887 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Oct 2025 08:03:47 -0300 Subject: [PATCH 08/11] :recycle: Tidy up implementation a bit --- .../tokens/filter/valueListBox.tsx | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx index c8fb28a0d0bf94..0bf300de11d4c2 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx @@ -125,22 +125,23 @@ export function ValueListBox>({ const centerKeyInView = useCallback( (key: Key | null) => { - if (key === null) return; - const container = listBoxRef.current; - if (!container) return; - const elId = getItemId(state as any, key as any); - if (!elId) return; - const el = document.getElementById(elId); + if (key === null || !listBoxRef.current) return; + + const el = document.getElementById(getItemId(state as any, key as any)); if (!el) return; - const containerRect = container.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); + const containerRect = listBoxRef.current.getBoundingClientRect(); + const offsetTopWithinContainer = - container.scrollTop + (elRect.top - containerRect.top); + listBoxRef.current.scrollTop + (elRect.top - containerRect.top); + const targetScrollTop = - offsetTopWithinContainer - container.clientHeight / 2 + elRect.height / 2; + offsetTopWithinContainer - + listBoxRef.current.clientHeight / 2 + + elRect.height / 2; - container.scrollTop = Math.max(0, targetScrollTop); + listBoxRef.current.scrollTop = Math.max(0, targetScrollTop); }, [listBoxRef, state] ); @@ -151,15 +152,13 @@ export function ValueListBox>({ useLayoutEffect(() => { if (!isOpen) return; - if ( - lastFocusedKeyRef.current !== null && - state.selectionManager.focusedKey === null - ) { - const keyToRestore = lastFocusedKeyRef.current; - state.selectionManager.setFocusedKey(keyToRestore); - centerKeyInView(keyToRestore); - } + if (!lastFocusedKeyRef.current) return; + if (state.selectionManager.focusedKey !== null) return; + + state.selectionManager.setFocusedKey(lastFocusedKeyRef.current); + centerKeyInView(lastFocusedKeyRef.current); }, [isOpen, state, centerKeyInView]); + const totalOptions = items.reduce( (acc, item) => acc + (itemIsSection(item) ? item.options.length : 1), 0 @@ -222,11 +221,11 @@ export function ValueListBox>({ size="sm" style={{maxWidth: overlayProps.style!.maxWidth}} /> - {isLoading && anyItemsShowing ? ( + {/* {isLoading && anyItemsShowing ? ( - ) : null} + ) : null} */}