Skip to content

Commit

Permalink
chore: < GridKeyboardNavigationContext /> - migrate to TS (#1656)
Browse files Browse the repository at this point in the history
  • Loading branch information
SergeyRoyt authored Oct 18, 2023
1 parent 4a87218 commit df1f57f
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,6 +88,8 @@ const ColorPickerContent: VibeComponent<ColorPickerContentProps, HTMLDivElement>

const colorsRef = useRef(null);
const buttonRef = useRef(null);
const gridRef = useRef(null);
const mergedRef = useMergeRefs({ refs: [ref, gridRef] });

const colorsToRender = useMemo(() => {
if (forceUseRawColorList) {
Expand Down Expand Up @@ -116,11 +119,11 @@ const ColorPickerContent: VibeComponent<ColorPickerContentProps, HTMLDivElement>
);

const positions = useMemo(() => [{ topElement: colorsRef, bottomElement: buttonRef }], []);
const keyboardContext = useGridKeyboardNavigationContext(positions, ref);
const keyboardContext = useGridKeyboardNavigationContext(positions, gridRef);
const width = calculateColorPickerWidth(colorSize, numberOfColorsInLine);

return (
<div className={className} style={{ width }} ref={ref} tabIndex={-1}>
<div className={className} style={{ width }} ref={mergedRef} tabIndex={-1}>
<GridKeyboardNavigationContext.Provider value={keyboardContext}>
<ColorPickerColorsGrid
ref={colorsRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,40 @@ import {
getOppositeDirection,
getOutmostElementInDirection
} from "./helper";
import { NavDirections } from "../../hooks/useFullKeyboardListeners";
import { GridElementRef, GridKeyboardNavigationContextType, Position } from "./GridKeyboardNavigationContextConstants";

export const GridKeyboardNavigationContext = React.createContext();
export const GridKeyboardNavigationContext = React.createContext<GridKeyboardNavigationContextType>(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();
Expand All @@ -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 };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MutableRefObject } from "react";
import { NavDirections } from "../../hooks/useFullKeyboardListeners";

export type GridElementRef = MutableRefObject<HTMLElement> & { current?: HTMLElement & { disabled?: boolean } };
export type DirectionMap = Map<GridElementRef, GridElementRef>;
export type DirectionMaps = Record<NavDirections, DirectionMap>;

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;
}
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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(),
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/components/Menu/MenuGridItem/MenuGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const MenuGridItem: VibeComponent<MenuGridItemProps> & {
});

const keyboardContext = useMenuGridItemNavContext({
wrapperRef: mergedRef,
wrapperRef: componentRef,
setActiveItemIndex,
getPreviousSelectableIndex,
getNextSelectableIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down

0 comments on commit df1f57f

Please sign in to comment.