/* eslint-disable @typescript-eslint/no-explicit-any */ import { Component, ComponentType, KeyboardEvent } from 'react'; import { focusOn } from './focus'; function getAllItems(ref: HTMLElement) { const nodes = ref.closest('ul[role="tree"]'); if (nodes) { return nodes.querySelectorAll('li[role="treeitem"]'); } return null; } function getFirstItem(ref: HTMLElement): HTMLElement | null { const nodes = ref.closest('ul[role="tree"]'); if (nodes) { return nodes.querySelector('li[role="treeitem"]'); } return null; } function getLastItem(ref: HTMLElement): HTMLElement | null { const nodes = getAllItems(ref); if (nodes && nodes.length > 0) { return nodes.item(nodes.length - 1) as HTMLElement; } return null; } function getParentItem(ref: HTMLElement): HTMLElement | null { const parentElement = ref.parentElement; if (parentElement) { return parentElement.closest('li[role="treeitem"]'); } return null; } function getFirstChildItem(ref: HTMLElement): HTMLElement | null { return ref.querySelector('li[role="treeitem"]'); } function getNextItem(ref: HTMLElement): HTMLElement | null { let nextElement = null; let currentFound; let hasNext; const nodes = getAllItems(ref)?.values(); if (!nodes) { return nextElement; } do { const { value, done } = nodes.next(); if (currentFound) { nextElement = value; hasNext = false; } else { currentFound = value === ref; hasNext = !done; } } while (hasNext); return nextElement; } function getPreviousItem(ref: HTMLElement) { let previousElement = null; let hasNext; const nodes = getAllItems(ref)?.values(); if (!nodes) { return previousElement; } do { const { value, done } = nodes.next(); const currentFound = value === ref; if (currentFound) { hasNext = false; } else { previousElement = value; hasNext = !done; } } while (hasNext); return previousElement; } export type TreeGestureProps = { onSelect: (e: KeyboardEvent, item: any) => void; onToggle: (e: KeyboardEvent, item: any) => void; onToggleAllSiblings: (e: KeyboardEvent, siblings: any) => void; }; export type WithTreeInjectedProps = TreeGestureProps & { onKeyDown: (e: KeyboardEvent, ref: any, item: any) => void; }; export function withTreeGesture( WrappedComponent: ComponentType, ) { type Props = Omit; return class TreeGesture extends Component { static displayName = `TreeGesture(${WrappedComponent.displayName})`; constructor(props: Props) { super(props); this.onKeyDown = this.onKeyDown.bind(this); } onKeyDown(event: KeyboardEvent, ref: any, item: any) { const { hasChildren, isOpened, siblings } = item; switch (event.key) { case 'Enter': case ' ': case 'Spacebar': event.stopPropagation(); this.props.onSelect(event, item); break; case 'Left': case 'ArrowLeft': if (hasChildren && isOpened) { event.stopPropagation(); this.props.onToggle(event, item); } else if (!hasChildren || !isOpened) { event.stopPropagation(); focusOn(getParentItem(ref)); } break; case 'Right': case 'ArrowRight': if (hasChildren && !isOpened) { event.stopPropagation(); this.props.onToggle(event, item); } else if (hasChildren && isOpened) { event.stopPropagation(); focusOn(getFirstChildItem(ref)); } break; case 'Down': case 'ArrowDown': event.stopPropagation(); focusOn(getNextItem(ref)); break; case 'Up': case 'ArrowUp': event.stopPropagation(); focusOn(getPreviousItem(ref)); break; case 'Home': event.stopPropagation(); focusOn(getFirstItem(ref)); break; case 'End': event.stopPropagation(); focusOn(getLastItem(ref)); break; default: break; } if (event.nativeEvent.key === '*' || event.nativeEvent.key === 'Multiply') { event.stopPropagation(); this.props.onToggleAllSiblings(event, siblings); } } render() { const props = { ...(this.props as any) }; props.onKeyDown = this.onKeyDown; return ; } }; }