Skip to content

Commit

Permalink
fix(useListNavigation): sync indexRef to selectedIndex on open (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks committed Jan 12, 2024
1 parent 9349851 commit c9c5058
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-maps-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@floating-ui/react": patch
---

fix(useListNavigation): sync internal `indexRef` to `selectedIndex` on open. Fixes an issue where if `selectedIndex` changed after initial render before opening, `activeIndex` would not be correctly synced.
1 change: 1 addition & 0 deletions packages/react/src/hooks/useListNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ export function useListNavigation<RT extends ReferenceType = ReferenceType>(
// Regardless of the pointer modality, we want to ensure the selected
// item comes into view when the floating element is opened.
forceScrollIntoViewRef.current = true;
indexRef.current = selectedIndex;
onNavigate(selectedIndex);
}
} else if (previousMountedRef.current) {
Expand Down
96 changes: 96 additions & 0 deletions packages/react/test/unit/useListNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {useLayoutEffect, useRef, useState} from 'react';
import {vi} from 'vitest';

import {
FloatingFocusManager,
FloatingList,
useClick,
useDismiss,
useFloating,
useInteractions,
useListItem,
useListNavigation,
} from '../../src';
import type {UseListNavigationProps} from '../../src/hooks/useListNavigation';
Expand Down Expand Up @@ -944,3 +947,96 @@ test('scheduled list population', async () => {

expect(screen.getAllByRole('option')[0]).toHaveFocus();
});

test('async selectedIndex', async () => {
const options = ['core', 'dom', 'react', 'react-dom', 'vue', 'react-native'];

function Option({
option,
activeIndex,
selectedIndex,
}: {
option: string;
activeIndex: number | null;
selectedIndex: number | null;
}) {
const {ref, index} = useListItem();
return (
<button
ref={ref}
role="option"
tabIndex={index === activeIndex ? 0 : -1}
aria-selected={index === selectedIndex}
>
<span>{option}</span>
</button>
);
}

function Select() {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(false);

if (selectedIndex !== 2) {
setSelectedIndex(2);
}

const {refs, floatingStyles, context} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
});

const elementsRef = useRef([]);

const click = useClick(context);
const listNav = useListNavigation(context, {
listRef: elementsRef,
activeIndex,
selectedIndex,
onNavigate: setActiveIndex,
});

const {getReferenceProps, getFloatingProps} = useInteractions([
listNav,
click,
]);

return (
<>
<button ref={refs.setReference} {...getReferenceProps()}>
Open
</button>
{isOpen && (
<FloatingFocusManager context={context} modal={false}>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
>
<FloatingList elementsRef={elementsRef}>
{options.map((option) => (
<Option
key={option}
option={option}
activeIndex={activeIndex}
selectedIndex={selectedIndex}
/>
))}
</FloatingList>
</div>
</FloatingFocusManager>
)}
</>
);
}

render(<Select />);

fireEvent.click(screen.getByRole('button'));
await act(async () => {});

expect(screen.getAllByRole('option')[2]).toHaveFocus();
await userEvent.keyboard('{ArrowDown}');
expect(screen.getAllByRole('option')[3]).toHaveFocus();
});

0 comments on commit c9c5058

Please sign in to comment.