import { FocusDetails, getCurrentFocusKey, init, SpatialNavigation, useFocusable, UseFocusableConfig, UseFocusableResult, } from "@noriginmedia/norigin-spatial-navigation"; import { RefObject, useEffect, useState } from "react"; import { focusQueue, Router } from ".."; init({ shouldFocusDOMNode: false, throttle: 200 }); let addFocusable = SpatialNavigation.addFocusable.bind(SpatialNavigation); let updateFocusable = SpatialNavigation.updateFocusable.bind(SpatialNavigation); let sortSiblingsByPriority = SpatialNavigation.sortSiblingsByPriority.bind(SpatialNavigation); let removeFocusable = SpatialNavigation.removeFocusable.bind(SpatialNavigation); let setFocus = SpatialNavigation.setFocus.bind(SpatialNavigation); let setCurrentFocusedKey = SpatialNavigation.setCurrentFocusedKey.bind(SpatialNavigation); let updateLayout = SpatialNavigation.updateLayout.bind(SpatialNavigation); type SaveFocusType = "session" | "local"; export function GetFocusedElement (focusKey: string) { return (SpatialNavigation as any).focusableComponents[focusKey]?.node as HTMLElement | undefined; } export function GetFocusedTree (leaf: string): string[] { const tree: string[] = []; let component = (SpatialNavigation as any).focusableComponents[leaf]; while (component) { tree.push(component.focusKey); if (component.parentFocusKey && !tree.includes(component.parentFocusKey)) { component = (SpatialNavigation as any).focusableComponents[component.parentFocusKey]; } else { break; } } return tree; } export function dispatchFocusedEvent (event: Event, override?: Element | Window) { const focusedElement = GetFocusedElement(getCurrentFocusKey()); const finalTarget = override ?? focusedElement ?? window; return finalTarget.dispatchEvent(event); } export interface FocusEventMap { 'focuschanged': Event; } export function useFocusEventListener (eventName: K, handler: (event: FocusEventMap[K]) => void, element?: RefObject): void { useEffect(() => { const finalElement = element ? element.current : window; finalElement?.addEventListener(eventName, handler); return () => finalElement?.removeEventListener(eventName, handler); }, [eventName, handler, element?.current]); } export function useGlobalFocus () { const [focused, setFocused] = useState(undefined); useEffect(() => { const handler = () => setFocused(getCurrentFocusKey()); window.addEventListener('focuschanged', handler); return () => window.removeEventListener('focuschanged', handler); }, []); return focused; } SpatialNavigation.updateLayout = (focusKey) => { updateLayout(focusKey); }; SpatialNavigation.setFocus = (newFocusKey, focusDetails) => { setFocus(newFocusKey, focusDetails); dispatchFocusedEvent(new CustomEvent('focuschanged', { bubbles: true, detail: focusDetails })); }; SpatialNavigation.setCurrentFocusedKey = (newFocusKey, focusDetails) => { setCurrentFocusedKey(newFocusKey, focusDetails); window.dispatchEvent(new CustomEvent('focuschanged', { bubbles: true, detail: focusDetails })); }; SpatialNavigation.updateFocusable = (key, data) => { updateFocusable(key, data); }; SpatialNavigation.sortSiblingsByPriority = (siblings, currentLayout, direction, focusKey) => { const sorted = sortSiblingsByPriority(siblings, currentLayout, direction, focusKey); return sorted.filter(e => e.node.checkVisibility({ visibilityProperty: true })); }; SpatialNavigation.addFocusable = (toAdd) => { addFocusable(toAdd); const queuedFocus = focusQueue[0]; if (queuedFocus === toAdd.focusKey) { // Use double request to account for dynamic layouts requestAnimationFrame(() => requestAnimationFrame(() => { setFocus(queuedFocus, { instant: true }); })); } const component: { lastFocusedChildKey?: string; preferredChildFocusKey?: string; node: HTMLElement; focusKey: string; focusableDefault?: boolean; } = (SpatialNavigation as any).focusableComponents[toAdd.focusKey]; if (component.node?.hasAttribute("save-child-focus")) { const storageKey = `${component.focusKey}-last-child-focus`; const saveChildFocus = component.node.getAttribute( "save-child-focus", ) as SaveFocusType; if (saveChildFocus === "session" && sessionStorage.getItem(storageKey)) { SpatialNavigation.saveLastFocusedChildKey( component as any, sessionStorage.getItem(storageKey)!, ); } else if (saveChildFocus === "local" && localStorage.getItem(storageKey)) { SpatialNavigation.saveLastFocusedChildKey( component as any, localStorage.getItem(storageKey)!, ); } } }; // Override remove callback to insert custom functionality like saving to storage SpatialNavigation.removeFocusable = ({ focusKey }) => { const component: { lastFocusedChildKey?: string; node: HTMLElement; focusKey: string; } = (SpatialNavigation as any).focusableComponents[focusKey]; if (component) { if (component.node?.hasAttribute("save-child-focus")) { const saveChildFocus = component.node.getAttribute( "save-child-focus", ) as SaveFocusType; const storageKey = `${component.focusKey}-last-child-focus`; if (saveChildFocus === "session") { if (component.lastFocusedChildKey) { sessionStorage.setItem(storageKey, component.lastFocusedChildKey); } else { //sessionStorage.removeItem(storageKey); } } else if (saveChildFocus === "local") { if (component.lastFocusedChildKey) { localStorage.setItem(storageKey, component.lastFocusedChildKey); } else { //localStorage.removeItem(storageKey); } } } removeFocusable(component); } }; SpatialNavigation.saveLastFocusedChildKey = (component, focusKey) => { component.lastFocusedChildKey = focusKey; }; export function useFocusableDynamic

(conf?: UseFocusableConfig

): UseFocusableResult { const [focusable, setFocusable] = useState(conf?.focusable); const result = useFocusable({ ...conf, focusable: focusable && conf?.focusable }); useEffect(() => { const observer = new MutationObserver(() => { setFocusable(result.ref.current.checkVisibility({ visibilityProperty: true })); }); observer.observe(result.ref.current, { subtree: true, attributes: true }); return () => observer.disconnect(); }, [result.ref.current]); return result; }