update
This commit is contained in:
1 parent
67c757f481
commit
b8dda3c8d0
77 files changed
+1577
-2808
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
interface RovingTabIndexResult {
|
||||
containerRef: { current: HTMLDivElement | null };
|
||||
focusedIndex: number;
|
||||
@@ -6,11 +8,11 @@ interface RovingTabIndexResult {
|
||||
getTabIndex: (index: number) => 0 | -1;
|
||||
}
|
||||
|
||||
/** Roving tabindex for keyboard nav in lists. Supports Arrow/Home/End/j/k with wrap-around. */
|
||||
export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
|
||||
// Use internal mutable state for testability outside React
|
||||
// In a React component context, this still works correctly because
|
||||
// the function is called once per render and returns fresh closures
|
||||
/**
|
||||
* Core roving-tabindex logic, independent of React.
|
||||
* Used directly by tests and wrapped by `useRovingTabIndex` for React components.
|
||||
*/
|
||||
export function createRovingTabIndex(itemCount: number): RovingTabIndexResult {
|
||||
let _focusedIndex = 0;
|
||||
const containerRef = { current: null as HTMLDivElement | null };
|
||||
|
||||
@@ -41,7 +43,6 @@ export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
|
||||
|
||||
_focusedIndex = nextIndex;
|
||||
|
||||
// Attempt to focus the target element in the DOM (no-op in tests)
|
||||
const target = containerRef.current?.querySelector(
|
||||
`[data-index="${nextIndex}"]`,
|
||||
) as HTMLElement | null;
|
||||
@@ -62,3 +63,57 @@ export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
|
||||
getTabIndex,
|
||||
};
|
||||
}
|
||||
|
||||
/** React hook wrapper around roving tabindex. Uses real React state for reactivity in components. */
|
||||
export function useRovingTabIndex(itemCount: number): RovingTabIndexResult {
|
||||
const [focusedIndex, setFocusedIndexState] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const focusedRef = useRef(0);
|
||||
|
||||
const setFocusedIndex = useCallback((index: number) => {
|
||||
focusedRef.current = index;
|
||||
setFocusedIndexState(index);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((event: React.KeyboardEvent | { key: string; preventDefault: () => void }) => {
|
||||
if (itemCount === 0) return;
|
||||
|
||||
let nextIndex = focusedRef.current;
|
||||
|
||||
if (event.key === "ArrowDown" || event.key === "j") {
|
||||
event.preventDefault();
|
||||
nextIndex = (focusedRef.current + 1) % itemCount;
|
||||
} else if (event.key === "ArrowUp" || event.key === "k") {
|
||||
event.preventDefault();
|
||||
nextIndex = (focusedRef.current - 1 + itemCount) % itemCount;
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
nextIndex = 0;
|
||||
} else if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
nextIndex = itemCount - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
focusedRef.current = nextIndex;
|
||||
setFocusedIndexState(nextIndex);
|
||||
|
||||
const target = containerRef.current?.querySelector(
|
||||
`[data-index="${nextIndex}"]`,
|
||||
) as HTMLElement | null;
|
||||
target?.focus();
|
||||
}, [itemCount]);
|
||||
|
||||
const getTabIndex = useCallback((index: number): 0 | -1 => {
|
||||
return index === focusedRef.current ? 0 : -1;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
focusedIndex,
|
||||
setFocusedIndex,
|
||||
handleKeyDown,
|
||||
getTabIndex,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user