@noriginmedia/norigin-spatial-navigation
Version:
React hooks based Spatial Navigation solution
282 lines (281 loc) • 12.4 kB
TypeScript
import WritingDirection from './WritingDirection';
export declare type Direction = 'up' | 'down' | 'left' | 'right';
declare type DistanceCalculationMethod = 'center' | 'edges' | 'corners';
declare type DistanceCalculationFunction = (refCorners: Corners, siblingCorners: Corners, isVerticalDirection: boolean, distanceCalculationMethod: DistanceCalculationMethod) => number;
export declare const ROOT_FOCUS_KEY = "SN:ROOT";
export interface FocusableComponentLayout {
left: number;
top: number;
readonly right: number;
readonly bottom: number;
width: number;
height: number;
x: number;
y: number;
node: HTMLElement;
}
interface FocusableComponent {
focusKey: string;
node: HTMLElement;
parentFocusKey: string;
onEnterPress: (details?: KeyPressDetails) => void;
onEnterRelease: () => void;
onArrowPress: (direction: string, details: KeyPressDetails) => boolean;
onArrowRelease: (direction: string) => void;
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
onUpdateFocus: (focused: boolean) => void;
onUpdateHasFocusedChild: (hasFocusedChild: boolean) => void;
saveLastFocusedChild: boolean;
trackChildren: boolean;
preferredChildFocusKey?: string;
focusable: boolean;
isFocusBoundary: boolean;
focusBoundaryDirections?: Direction[];
autoRestoreFocus: boolean;
forceFocus: boolean;
lastFocusedChildKey?: string;
layout?: FocusableComponentLayout;
layoutUpdated?: boolean;
}
interface FocusableComponentUpdatePayload {
node: HTMLElement;
preferredChildFocusKey?: string;
focusable: boolean;
isFocusBoundary: boolean;
focusBoundaryDirections?: Direction[];
onEnterPress: (details?: KeyPressDetails) => void;
onEnterRelease: () => void;
onArrowPress: (direction: string, details: KeyPressDetails) => boolean;
onArrowRelease: (direction: string) => void;
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
}
interface FocusableComponentRemovePayload {
focusKey: string;
}
interface CornerCoordinates {
x: number;
y: number;
}
interface Corners {
a: CornerCoordinates;
b: CornerCoordinates;
}
export declare type PressedKeys = {
[index: string]: number;
};
/**
* Extra details about pressed keys passed on the key events
*/
export interface KeyPressDetails {
pressedKeys: PressedKeys;
}
/**
* Extra details passed from outside to be bounced back on other callbacks
*/
export interface FocusDetails {
event?: Event;
nativeEvent?: Event;
[key: string]: any;
}
export declare type BackwardsCompatibleKeyMap = {
[index: string]: string | number | (number | string)[];
};
export declare type KeyMap = {
[index: string]: (string | number)[];
};
declare class SpatialNavigationService {
private focusableComponents;
private visualDebugger;
/**
* Focus key of the currently focused element
*/
private focusKey;
private shouldFocusDOMNode;
private shouldUseNativeEvents;
/**
* This collection contains focus keys of the elements that are having a child focused
* Might be handy for styling of certain parent components if their child is focused.
*/
private parentsHavingFocusedChild;
/**
* When shouldFocusDOMNode is true, this prop specifies the focus options that should be passed to the element being focused.
*/
private domNodeFocusOptions;
private enabled;
/**
* Used in the React Native environment
* In this mode, the library works as a "read-only" helper to sync focused
* states for the components when they are focused by the native focus engine
*/
private nativeMode;
/**
* Throttling delay for key presses in milliseconds
*/
private throttle;
/**
* Enables/disables throttling feature
*/
private throttleKeypresses;
/**
* Storing pressed keys counter by the eventType
*/
private pressedKeys;
/**
* Flag used to block key events from this service
*/
private paused;
/**
* Enables/disables getBoundingClientRect
*/
private useGetBoundingClientRect;
private keyDownEventListener;
private keyDownEventListenerThrottled;
private keyUpEventListener;
private keyMap;
private debug;
private logIndex;
private setFocusDebounced;
private writingDirection;
private distanceCalculationMethod;
private customDistanceCalculationFunction?;
/**
* Used to determine the coordinate that will be used to filter items that are over the "edge"
*/
static getCutoffCoordinate(isVertical: boolean, isIncremental: boolean, isSibling: boolean, layout: FocusableComponentLayout, writingDirection: WritingDirection): number;
/**
* Returns two corners (a and b) coordinates that are used as a reference points
* Where "a" is always leftmost and topmost corner, and "b" is rightmost bottommost corner
*/
static getRefCorners(direction: string, isSibling: boolean, layout: FocusableComponentLayout): {
a: {
x: number;
y: number;
};
b: {
x: number;
y: number;
};
};
/**
* Calculates if the sibling node is intersecting enough with the ref node by the secondary coordinate
*/
static isAdjacentSlice(refCorners: Corners, siblingCorners: Corners, isVerticalDirection: boolean): boolean;
static getPrimaryAxisDistance(refCorners: Corners, siblingCorners: Corners, isVerticalDirection: boolean): number;
static getSecondaryAxisDistance(refCorners: Corners, siblingCorners: Corners, isVerticalDirection: boolean, distanceCalculationMethod: DistanceCalculationMethod, customDistanceCalculationFunction?: DistanceCalculationFunction): number;
/**
* Inspired by: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS_for_TV/TV_remote_control_navigation#Algorithm_design
* Ref Corners are the 2 corners of the current component in the direction of navigation
* They are used as a base to measure adjacent slices
*/
sortSiblingsByPriority(siblings: FocusableComponent[], currentLayout: FocusableComponentLayout, direction: string, focusKey: string): FocusableComponent[];
constructor();
init({ debug, visualDebug, nativeMode, throttle: throttleParam, throttleKeypresses, useGetBoundingClientRect, shouldFocusDOMNode, domNodeFocusOptions, shouldUseNativeEvents, rtl, distanceCalculationMethod, customDistanceCalculationFunction }?: {
debug?: boolean;
visualDebug?: boolean;
nativeMode?: boolean;
throttle?: number;
throttleKeypresses?: boolean;
useGetBoundingClientRect?: boolean;
shouldFocusDOMNode?: boolean;
domNodeFocusOptions?: {};
shouldUseNativeEvents?: boolean;
rtl?: boolean;
distanceCalculationMethod?: DistanceCalculationMethod;
customDistanceCalculationFunction?: DistanceCalculationFunction;
}): void;
setThrottle({ throttle: throttleParam, throttleKeypresses }?: {
throttle?: number;
throttleKeypresses?: boolean;
}): void;
destroy(): void;
getEventType(keyCode: number | string): string;
static getKeyCode(event: KeyboardEvent): string | number;
bindEventHandlers(): void;
unbindEventHandlers(): void;
onEnterPress(keysDetails: KeyPressDetails): void;
onEnterRelease(): void;
onArrowPress(direction: string, keysDetails: KeyPressDetails): boolean;
onArrowRelease(direction: string): void;
/**
* Move focus by direction, if you can't use buttons or focusing by key.
*
* @example
* navigateByDirection('right') // The focus is moved to right
*/
navigateByDirection(direction: string, focusDetails: FocusDetails): void;
/**
* This function navigates between siblings OR goes up by the Tree
* Based on the Direction
*/
smartNavigate(direction: string, fromParentFocusKey: string, focusDetails: FocusDetails): void;
saveLastFocusedChildKey(component: FocusableComponent, focusKey: string): void;
log(functionName: string, debugString: string, ...rest: any[]): void;
/**
* Returns the current focus key
*/
getCurrentFocusKey(): string;
/**
* Returns the focus key to which focus can be forced if there are force-focusable components.
* A component closest to the top left viewport corner (0,0) is returned.
*/
getForcedFocusKey(): string | undefined;
/**
* This function tries to determine the next component to Focus
* It's either the target node OR the one down by the Tree if node has children components
* Based on "targetFocusKey" which means the "intended component to focus"
*/
getNextFocusKey(targetFocusKey: string): string;
addFocusable({ focusKey, node, parentFocusKey, onEnterPress, onEnterRelease, onArrowPress, onArrowRelease, onFocus, onBlur, saveLastFocusedChild, trackChildren, onUpdateFocus, onUpdateHasFocusedChild, preferredChildFocusKey, autoRestoreFocus, forceFocus, focusable, isFocusBoundary, focusBoundaryDirections }: FocusableComponent): void;
removeFocusable({ focusKey }: FocusableComponentRemovePayload): void;
getNodeLayoutByFocusKey(focusKey: string): FocusableComponentLayout;
setCurrentFocusedKey(newFocusKey: string, focusDetails: FocusDetails): void;
updateParentsHasFocusedChild(focusKey: string, focusDetails: FocusDetails): void;
updateParentsLastFocusedChild(focusKey: string): void;
getKeyMap(): KeyMap;
setKeyMap(keyMap: BackwardsCompatibleKeyMap): void;
isFocusableComponent(focusKey: string): boolean;
/**
* Checks whether the focusableComponent is actually participating in spatial navigation (in other words, is a
* 'focusable' focusableComponent). Seems less confusing than calling it isFocusableFocusableComponent()
*/
isParticipatingFocusableComponent(focusKey: string): boolean;
onIntermediateNodeBecameFocused(focusKey: string, focusDetails: FocusDetails): void;
onIntermediateNodeBecameBlurred(focusKey: string, focusDetails: FocusDetails): void;
pause(): void;
resume(): void;
setFocus(focusKey: string, focusDetails?: FocusDetails): void;
updateAllLayouts(): void;
updateLayout(focusKey: string): void;
updateFocusable(focusKey: string, { node, preferredChildFocusKey, focusable, isFocusBoundary, focusBoundaryDirections, onEnterPress, onEnterRelease, onArrowPress, onFocus, onBlur }: FocusableComponentUpdatePayload): void;
isNativeMode(): boolean;
doesFocusableExist(focusKey: string): boolean;
/**
* This function updates the writing direction
* @param rtl whether the writing direction is right-to-left
*/
updateRtl(rtl: boolean): void;
}
/**
* Export singleton
*/
export declare const SpatialNavigation: SpatialNavigationService;
export declare const init: ({ debug, visualDebug, nativeMode, throttle: throttleParam, throttleKeypresses, useGetBoundingClientRect, shouldFocusDOMNode, domNodeFocusOptions, shouldUseNativeEvents, rtl, distanceCalculationMethod, customDistanceCalculationFunction }?: {
debug?: boolean;
visualDebug?: boolean;
nativeMode?: boolean;
throttle?: number;
throttleKeypresses?: boolean;
useGetBoundingClientRect?: boolean;
shouldFocusDOMNode?: boolean;
domNodeFocusOptions?: {};
shouldUseNativeEvents?: boolean;
rtl?: boolean;
distanceCalculationMethod?: DistanceCalculationMethod;
customDistanceCalculationFunction?: DistanceCalculationFunction;
}) => void, setThrottle: ({ throttle: throttleParam, throttleKeypresses }?: {
throttle?: number;
throttleKeypresses?: boolean;
}) => void, destroy: () => void, setKeyMap: (keyMap: BackwardsCompatibleKeyMap) => void, setFocus: (focusKey: string, focusDetails?: FocusDetails) => void, navigateByDirection: (direction: string, focusDetails: FocusDetails) => void, pause: () => void, resume: () => void, updateAllLayouts: () => void, getCurrentFocusKey: () => string, doesFocusableExist: (focusKey: string) => boolean, updateRtl: (rtl: boolean) => void;
export {};