UNPKG

navkit-vue

Version:

Vue navigation and utility composables

224 lines (183 loc) 5.33 kB
import { computed, isRef, onMounted, onUnmounted, ref, unref, watch, } from "vue"; import type { NavigationXType } from "../types/navigation"; import { KeyboardEnum } from "../enums/keyboard.enum"; export function useNavigationX({ columns, initialPosition = 0, disabled = ref(false), focusableSelector = "[data-focusable]", autofocus = true, focusClass = "focused", cyclic = false, onEnter, onReturn, onColumnEnd, onColumnStart, onDown, onUp, }: NavigationXType) { const computedCols = computed(() => unref(columns)); const currentPosition = ref(initialPosition); const lastFocusedElement = ref<HTMLElement | null>(null); let isProcessing = false; // Disabling the hook const isDisabled = computed(() => unref(disabled)); const isDisabledWatcher = watch(isDisabled, (newValue) => { if (newValue) { lastFocusedElement.value?.blur(); lastFocusedElement.value?.classList.remove(focusClass); window.removeEventListener("keydown", handleKeyDown); } else { if (autofocus && !isDisabled.value) { const element = getFocusableElement(currentPosition.value); focusElement(element as HTMLElement); } window.addEventListener("keydown", handleKeyDown); } }); const toggleDisabled = (value?: boolean) => { if (value !== undefined) { if (isRef(disabled)) { disabled.value = value; } else { disabled = value; } } else { if (isRef(disabled)) { disabled.value = !disabled.value; } else { disabled = !disabled; } } }; function setPosition(pos: number) { return (currentPosition.value = pos); } const findFocusableElements = () => { return Array.from(document.querySelectorAll(focusableSelector)); }; const isValidPosition = (col: number): boolean => { return col >= 0 && col < computedCols.value; }; const getFocusableElement = (pos: number) => { const elements = findFocusableElements(); let currentCol = 0; for (const element of elements) { if (currentCol === pos) { return element as HTMLElement; } currentCol++; } return null; }; const focusElement = (element: HTMLElement | null) => { if (isProcessing || !element) return; isProcessing = true; try { if (lastFocusedElement.value) { lastFocusedElement.value.classList.remove(focusClass); lastFocusedElement.value.blur(); } element.classList.add(focusClass); if (autofocus) { element.focus(); } lastFocusedElement.value = element; } catch (error) { console.error("Focus error:", error); } finally { isProcessing = false; } }; const handleKeyDown = (event: KeyboardEvent) => { if (isDisabled.value || isProcessing) return; let newPosition = currentPosition.value; switch (event.key) { case KeyboardEnum.Left: newPosition--; if (newPosition < 0) { if (cyclic) { newPosition = computedCols.value - 1; } else { onColumnStart?.(); return; } } break; case KeyboardEnum.Right: newPosition++; if (newPosition >= computedCols.value) { if (cyclic) { newPosition = 0; } else { onColumnEnd?.(); return; } } break; case KeyboardEnum.Up: onUp?.(); break; case KeyboardEnum.Down: onDown?.(); break; case KeyboardEnum.Enter: onEnter?.(currentPosition.value); return; case KeyboardEnum.Back: onReturn?.(currentPosition.value); return; default: console.warn("Unhandled Key:", event.code); return; } if (isValidPosition(newPosition)) { currentPosition.value = newPosition; } else { console.error("Position Not Valid:", newPosition); } }; const currentPositionWatcher = watch(currentPosition, (newPosition) => { if (!isDisabled.value && !isProcessing) { const element = getFocusableElement(newPosition); focusElement(element as HTMLElement); } }); // Lifecycle hooks onMounted(() => { if (isDisabled.value) { window.removeEventListener("keydown", handleKeyDown); } else { window.addEventListener("keydown", handleKeyDown); } if (autofocus && !isDisabled.value) { const element = getFocusableElement(currentPosition.value); focusElement(element as HTMLElement); } }); onUnmounted(() => { window.removeEventListener("keydown", handleKeyDown); if (lastFocusedElement.value) { lastFocusedElement.value.classList.remove(focusClass); } isDisabledWatcher(); currentPositionWatcher(); }); return { position: currentPosition, currentElement: lastFocusedElement.value, setPosition, toggleDisabled, isValidPosition, focusElement, getCurrentFocusedElement: () => lastFocusedElement.value, }; }