diff --git a/src/components/ColorPicker/components/ColorPickerContent/ColorPickerContent.tsx b/src/components/ColorPicker/components/ColorPickerContent/ColorPickerContent.tsx index af35b12e67..36bec32ff2 100644 --- a/src/components/ColorPicker/components/ColorPickerContent/ColorPickerContent.tsx +++ b/src/components/ColorPicker/components/ColorPickerContent/ColorPickerContent.tsx @@ -17,6 +17,7 @@ import { import { ColorPickerClearButton } from "./ColorPickerClearButton"; import { ColorPickerColorsGrid } from "./ColorPickerColorsGrid"; import { VibeComponentProps, VibeComponent, SubIcon, withStaticProps } from "../../../../types"; +import { useMergeRefs } from "../../../../hooks"; export interface ColorPickerContentProps extends VibeComponentProps { value: ColorPickerValue; @@ -87,6 +88,8 @@ const ColorPickerContent: VibeComponent const colorsRef = useRef(null); const buttonRef = useRef(null); + const gridRef = useRef(null); + const mergedRef = useMergeRefs({ refs: [ref, gridRef] }); const colorsToRender = useMemo(() => { if (forceUseRawColorList) { @@ -116,11 +119,11 @@ const ColorPickerContent: VibeComponent ); const positions = useMemo(() => [{ topElement: colorsRef, bottomElement: buttonRef }], []); - const keyboardContext = useGridKeyboardNavigationContext(positions, ref); + const keyboardContext = useGridKeyboardNavigationContext(positions, gridRef); const width = calculateColorPickerWidth(colorSize, numberOfColorsInLine); return ( -
+
(null); /** - * @param {({topElement: React.MutableRefObject, bottomElement: React.MutableRefObject}| - * {leftElement: React.MutableRefObject, rightElement: React.MutableRefObject})[]} positions - the positions of the navigable items + * @param {({topElement: MutableRefObject, bottomElement: MutableRefObject}| + * {leftElement: MutableRefObject, rightElement: MutableRefObject})[]} positions - the positions of the navigable items * @param {*} wrapperRef - a reference for a wrapper element which contains all the referenced elements + * @param options - { disabled: boolean } */ -export const useGridKeyboardNavigationContext = (positions, wrapperRef, { disabled } = { disabled: false }) => { +export const useGridKeyboardNavigationContext = ( + positions: Position[], + wrapperRef: GridElementRef, + options: { disabled: boolean } = { disabled: false } +) => { const directionMaps = useMemo(() => getDirectionMaps(positions), [positions]); const upperContext = useContext(GridKeyboardNavigationContext); const { lastNavigationDirectionRef } = useLastNavigationDirection(); const onWrapperFocus = useCallback(() => { const keyboardDirection = lastNavigationDirectionRef.current; - if (!keyboardDirection || disabled) { + if (!keyboardDirection || options.disabled) { return; } const oppositeDirection = getOppositeDirection(keyboardDirection); const refToFocus = getOutmostElementInDirection(directionMaps, oppositeDirection); refToFocus?.current?.focus(); - }, [directionMaps, disabled, lastNavigationDirectionRef]); + }, [directionMaps, options.disabled, lastNavigationDirectionRef]); useEventListener({ eventName: "focus", callback: onWrapperFocus, ref: wrapperRef }); const onOutboundNavigation = useCallback( - (elementRef, direction) => { - if (disabled) return; + (elementRef: GridElementRef, direction: NavDirections) => { + if (options.disabled) return; const maybeNextElement = getNextElementToFocusInDirection(directionMaps[direction], elementRef); if (maybeNextElement) { elementRef.current?.blur(); @@ -43,7 +50,7 @@ export const useGridKeyboardNavigationContext = (positions, wrapperRef, { disabl // nothing on that direction - try updating the upper context upperContext?.onOutboundNavigation(wrapperRef, direction); }, - [directionMaps, upperContext, wrapperRef, disabled] + [directionMaps, upperContext, wrapperRef, options.disabled] ); return { onOutboundNavigation }; }; diff --git a/src/components/GridKeyboardNavigationContext/GridKeyboardNavigationContextConstants.ts b/src/components/GridKeyboardNavigationContext/GridKeyboardNavigationContextConstants.ts new file mode 100644 index 0000000000..c25a873f80 --- /dev/null +++ b/src/components/GridKeyboardNavigationContext/GridKeyboardNavigationContextConstants.ts @@ -0,0 +1,22 @@ +import { MutableRefObject } from "react"; +import { NavDirections } from "../../hooks/useFullKeyboardListeners"; + +export type GridElementRef = MutableRefObject & { current?: HTMLElement & { disabled?: boolean } }; +export type DirectionMap = Map; +export type DirectionMaps = Record; + +export type Position = VerticalPosition & HorizontalPosition; + +type VerticalPosition = { + topElement?: GridElementRef; + bottomElement?: GridElementRef; +}; + +type HorizontalPosition = { + leftElement?: GridElementRef; + rightElement?: GridElementRef; +}; + +export interface GridKeyboardNavigationContextType { + onOutboundNavigation?: (ref: GridElementRef, direction: NavDirections) => void; +} diff --git a/src/components/GridKeyboardNavigationContext/helper.js b/src/components/GridKeyboardNavigationContext/helper.ts similarity index 82% rename from src/components/GridKeyboardNavigationContext/helper.js rename to src/components/GridKeyboardNavigationContext/helper.ts index eeee615d07..a58e13e09c 100644 --- a/src/components/GridKeyboardNavigationContext/helper.js +++ b/src/components/GridKeyboardNavigationContext/helper.ts @@ -1,6 +1,7 @@ import { NavDirections } from "../../hooks/useFullKeyboardListeners"; +import { DirectionMap, DirectionMaps, GridElementRef, Position } from "./GridKeyboardNavigationContextConstants"; -function throwIfCausingCircularDependency(directionMaps, newPosition) { +function throwIfCausingCircularDependency(directionMaps: DirectionMaps, newPosition: Position) { const { topElement, bottomElement, leftElement, rightElement } = newPosition; if (topElement && bottomElement) { if (directionMaps[NavDirections.UP].get(topElement) === bottomElement) { @@ -19,15 +20,15 @@ function throwIfCausingCircularDependency(directionMaps, newPosition) { } } - function throwMessage(directionFrom, directionTo) { + function throwMessage(directionFrom: string, directionTo: string) { throw new Error( `Circular positioning detected: the ${directionFrom} element is already positioned to the ${directionTo} of the ${directionTo} element. This probably means the layout isn't ordered correctly.` ); } } -export const getDirectionMaps = positions => { - const directionMaps = { +export const getDirectionMaps = (positions: Position[]) => { + const directionMaps: DirectionMaps = { [NavDirections.RIGHT]: new Map(), [NavDirections.LEFT]: new Map(), [NavDirections.UP]: new Map(), @@ -49,7 +50,7 @@ export const getDirectionMaps = positions => { return directionMaps; }; -export const getOppositeDirection = direction => { +export const getOppositeDirection = (direction: NavDirections) => { switch (direction) { case NavDirections.LEFT: return NavDirections.RIGHT; @@ -64,7 +65,10 @@ export const getOppositeDirection = direction => { } }; -export const getOutmostElementInDirection = (directionMaps, direction) => { +export const getOutmostElementInDirection = ( + directionMaps: DirectionMaps, + direction: NavDirections +): GridElementRef => { const directionMap = directionMaps[direction]; const firstEntry = [...directionMap][0]; // start with any element if (!firstEntry) { @@ -80,7 +84,10 @@ export const getOutmostElementInDirection = (directionMaps, direction) => { return getLastFocusableElementFromElementInDirection(directionMap, firstRef); }; -export const getNextElementToFocusInDirection = (directionMap, elementRef) => { +export const getNextElementToFocusInDirection = ( + directionMap: DirectionMap, + elementRef: GridElementRef +): null | GridElementRef => { const next = directionMap.get(elementRef); if (!next) { // this is the last element on the direction map - there' nothing next @@ -93,7 +100,7 @@ export const getNextElementToFocusInDirection = (directionMap, elementRef) => { return next; }; -function getLastFocusableElementFromElementInDirection(directionMap, initialRef) { +function getLastFocusableElementFromElementInDirection(directionMap: DirectionMap, initialRef: GridElementRef) { let done = false; let currentRef = initialRef; diff --git a/src/components/Menu/MenuGridItem/MenuGridItem.tsx b/src/components/Menu/MenuGridItem/MenuGridItem.tsx index 6564429af6..e18758e579 100644 --- a/src/components/Menu/MenuGridItem/MenuGridItem.tsx +++ b/src/components/Menu/MenuGridItem/MenuGridItem.tsx @@ -82,7 +82,7 @@ const MenuGridItem: VibeComponent & { }); const keyboardContext = useMenuGridItemNavContext({ - wrapperRef: mergedRef, + wrapperRef: componentRef, setActiveItemIndex, getPreviousSelectableIndex, getNextSelectableIndex, diff --git a/src/components/Menu/MenuGridItem/useMenuGridItemNavContext.tsx b/src/components/Menu/MenuGridItem/useMenuGridItemNavContext.tsx index f14d94a43a..3a38277856 100644 --- a/src/components/Menu/MenuGridItem/useMenuGridItemNavContext.tsx +++ b/src/components/Menu/MenuGridItem/useMenuGridItemNavContext.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from "react"; import { NavDirections } from "../../../hooks/useFullKeyboardListeners"; import { useGridKeyboardNavigationContext } from "../../GridKeyboardNavigationContext/GridKeyboardNavigationContext"; import { CloseMenuOption } from "../Menu/MenuConstants"; +import { GridElementRef } from "../../GridKeyboardNavigationContext/GridKeyboardNavigationContextConstants"; export const useMenuGridItemNavContext = ({ wrapperRef, @@ -12,7 +13,7 @@ export const useMenuGridItemNavContext = ({ isUnderSubMenu, closeMenu }: { - wrapperRef?: (node: HTMLElement) => void; + wrapperRef?: GridElementRef; setActiveItemIndex?: (index: number) => void; getNextSelectableIndex?: (activeItemIndex: number) => number; getPreviousSelectableIndex?: (activeItemIndex: number) => number;