{"version":3,"file":"useListNavigation.cjs","sources":["../../../../src/hooks/useListNavigation/useListNavigation.ts"],"sourcesContent":["import { type RefObject, useEffect } from \"react\";\n\ntype Timer = number | undefined;\ntype KeyBuffer = { keys: string } | undefined;\ntype Direction = \"prev\" | \"next\" | \"first\" | \"last\";\ninterface MoveDetails {\n    event: KeyboardEvent;\n    list: HTMLElement;\n    currentFocus: HTMLButtonElement;\n}\ninterface ListDetails {\n    list: HTMLElement;\n    search: KeyBuffer;\n    searchResetTimer: Timer;\n}\ninterface SearchDetails extends ListDetails {\n    key: string;\n}\ninterface EventDetails extends ListDetails {\n    event: KeyboardEvent;\n}\ntype UseListNavigationProps<T> = {\n    /** Ref til et element med rollen `listbox` */\n    ref: RefObject<T | null>;\n};\n\nexport function useListNavigation<T extends HTMLElement>({\n    ref,\n}: UseListNavigationProps<T>): void {\n    useEffect(() => {\n        let searchResetTimer: Timer;\n        const search: KeyBuffer = { keys: \"\" }; // keypress buffer is an object to preserve state\n        const list = ref.current;\n        const handler = (event: KeyboardEvent) => {\n            if (list) {\n                handleListKeyNav({ list, event, search, searchResetTimer });\n            }\n        };\n\n        if (list) {\n            list.addEventListener(\"keydown\", handler);\n        }\n\n        return () => {\n            if (list) {\n                list.removeEventListener(\"keydown\", handler);\n            }\n        };\n    }, [ref]);\n}\n\nfunction handleMoveTo(\n    direction: Direction,\n    { event, list, currentFocus }: MoveDetails,\n) {\n    event.preventDefault();\n    moveFocusTo(direction, list, currentFocus);\n}\n\nfunction handleListKeyNav({\n    list,\n    event,\n    search,\n    searchResetTimer,\n}: EventDetails) {\n    const { key, target } = event;\n    const currentFocus = target as HTMLButtonElement;\n\n    const moveDetails = {\n        event,\n        list,\n        currentFocus,\n    };\n\n    switch (key) {\n        case \"ArrowUp\":\n        case \"PageUp\":\n            handleMoveTo(\"prev\", moveDetails);\n            break;\n        case \"ArrowDown\":\n        case \"PageDown\":\n            handleMoveTo(\"next\", moveDetails);\n            break;\n        case \"Home\":\n            handleMoveTo(\"first\", moveDetails);\n            break;\n        case \"End\":\n            handleMoveTo(\"last\", moveDetails);\n            break;\n        case \"Tab\":\n            // in a standard select, tab does nothing in-menu\n            event.preventDefault();\n            break;\n        case \"Enter\":\n        case \" \":\n            break;\n\n        default:\n            if (search !== undefined) {\n                const searchResult = findItem({\n                    list,\n                    key,\n                    search,\n                    searchResetTimer,\n                });\n                if (searchResult) {\n                    searchResult.focus();\n                }\n            }\n            break;\n    }\n}\n\nfunction moveFocusTo(\n    direction: Direction,\n    list: HTMLElement,\n    current: HTMLButtonElement,\n) {\n    const thisOption = current;\n    switch (direction) {\n        case \"prev\": {\n            const prevOption: HTMLButtonElement | null =\n                thisOption &&\n                (thisOption.previousElementSibling as HTMLButtonElement);\n            if (prevOption) {\n                prevOption.focus();\n            }\n            break;\n        }\n        case \"next\": {\n            const nextOption: HTMLButtonElement | null =\n                thisOption &&\n                (thisOption.nextElementSibling as HTMLButtonElement);\n            if (nextOption) {\n                nextOption.focus();\n            }\n            break;\n        }\n        case \"first\": {\n            const firstItem =\n                list.querySelector<HTMLButtonElement>(`[role=\"option\"]`);\n            if (firstItem) {\n                firstItem.focus();\n            }\n            break;\n        }\n        case \"last\": {\n            const listItems =\n                list.querySelectorAll<HTMLButtonElement>(`[role=\"option\"]`);\n            if (listItems.length) {\n                listItems[listItems.length - 1].focus();\n            }\n            break;\n        }\n    }\n}\n\nfunction findItem({\n    list,\n    key,\n    search,\n    searchResetTimer,\n}: SearchDetails): HTMLButtonElement | null {\n    const listItems = list.querySelectorAll(`[role=\"option\"]`);\n    if (!listItems.length) return null;\n\n    if (search) {\n        search.keys = search.keys.concat(key);\n        resetWhenIdle(search, searchResetTimer);\n\n        for (let n = 0; n < listItems.length; n++) {\n            const label = (listItems[n] as HTMLButtonElement).innerText;\n            if (label && label.toLowerCase().indexOf(search.keys) === 0) {\n                return listItems[n] as HTMLButtonElement;\n            }\n        }\n    }\n\n    return null;\n}\n\nfunction resetWhenIdle(search: KeyBuffer, timer: Timer) {\n    if (timer) {\n        clearTimeout(timer);\n        timer = undefined;\n    }\n    timer = setTimeout(\n        () => {\n            // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>\n            search ? (search.keys = \"\") : (search = { keys: \"\" });\n            timer = undefined;\n        },\n        500,\n        search,\n        timer,\n    );\n}\n"],"names":["handleMoveTo","direction","event","list","currentFocus","preventDefault","current","thisOption","prevOption","previousElementSibling","focus","nextOption","nextElementSibling","firstItem","querySelector","listItems","querySelectorAll","length","moveFocusTo","ref","useEffect","search","keys","handler","searchResetTimer","key","target","moveDetails","searchResult","concat","timer","clearTimeout","setTimeout","resetWhenIdle","n","label","innerText","toLowerCase","indexOf","findItem","handleListKeyNav","addEventListener","removeEventListener"],"mappings":"yGAmDA,SAASA,EACLC,GACEC,MAAAA,EAAOC,KAAAA,EAAMC,aAAAA,IAEfF,EAAMG,iBA0DV,SACIJ,EACAE,EACAG,GAEA,MAAMC,EAAaD,EACnB,OAAQL,GACJ,IAAK,OAAQ,CACT,MAAMO,EACFD,GACCA,EAAWE,uBACZD,GACAA,EAAWE,QAEf,KACJ,CACA,IAAK,OAAQ,CACT,MAAMC,EACFJ,GACCA,EAAWK,mBACZD,GACAA,EAAWD,QAEf,KACJ,CACA,IAAK,QAAS,CACV,MAAMG,EACFV,EAAKW,cAAiC,mBACtCD,GACAA,EAAUH,QAEd,KACJ,CACA,IAAK,OAAQ,CACT,MAAMK,EACFZ,EAAKa,iBAAoC,mBACzCD,EAAUE,QACVF,EAAUA,EAAUE,OAAS,GAAGP,QAEpC,KACJ,EAER,CAnGIQ,CAAYjB,EAAWE,EAAMC,EACjC,2BA/BO,UACHe,IAAAA,IAEAC,EAAAA,UAAU,KAEN,MAAMC,EAAoB,CAAEC,KAAM,IAC5BnB,EAAOgB,EAAIb,QACXiB,EAAWrB,IACTC,GAyBhB,UACIA,KAAAA,EACAD,MAAAA,EACAmB,OAAAA,EACAG,iBAAAA,IAEA,MAAQC,IAAAA,EAAKC,OAAAA,GAAWxB,EAGlByB,EAAc,CAChBzB,MAAAA,EACAC,KAAAA,EACAC,aALiBsB,GAQrB,OAAQD,GACJ,IAAK,UACL,IAAK,SACDzB,EAAa,OAAQ2B,GACrB,MACJ,IAAK,YACL,IAAK,WACD3B,EAAa,OAAQ2B,GACrB,MACJ,IAAK,OACD3B,EAAa,QAAS2B,GACtB,MACJ,IAAK,MACD3B,EAAa,OAAQ2B,GACrB,MACJ,IAAK,MAEDzB,EAAMG,iBACN,MACJ,IAAK,QACL,IAAK,IACD,MAEJ,QACI,QAAe,IAAXgB,EAAsB,CACtB,MAAMO,EA0DtB,UACIzB,KAAAA,EACAsB,IAAAA,EACAJ,OAAAA,EACAG,iBAAAA,IAEA,MAAMT,EAAYZ,EAAKa,iBAAiB,mBACxC,IAAKD,EAAUE,OAAQ,OAAO,KAE9B,GAAII,EAAQ,CACRA,EAAOC,KAAOD,EAAOC,KAAKO,OAAOJ,GAczC,SAAuBJ,EAAmBS,GAClCA,IACAC,aAAaD,GACbA,OAAQ,GAEZA,EAAQE,WACJ,KAEIX,EAAUA,EAAOC,KAAO,GAAOD,EAAS,CAAEC,KAAM,IAChDQ,OAAQ,GAEZ,IACAT,EACAS,EAER,CA5BQG,CAAcZ,EAAQG,GAEtB,IAAA,IAASU,EAAI,EAAGA,EAAInB,EAAUE,OAAQiB,IAAK,CACvC,MAAMC,EAASpB,EAAUmB,GAAyBE,UAClD,GAAID,GAAsD,IAA7CA,EAAME,cAAcC,QAAQjB,EAAOC,MAC5C,OAAOP,EAAUmB,EAEzB,CACJ,CAEA,OAAO,IACX,CAhFqCK,CAAS,CAC1BpC,KAAAA,EACAsB,IAAAA,EACAJ,OAAAA,EACAG,iBAAAA,IAEAI,GACAA,EAAalB,OAErB,EAGZ,CA5EgB8B,CAAiB,CAAErC,KAAAA,EAAMD,MAAAA,EAAOmB,OAAAA,EAAQG,iBAL5CA,aASJ,OAAIrB,GACAA,EAAKsC,iBAAiB,UAAWlB,GAG9B,KACCpB,GACAA,EAAKuC,oBAAoB,UAAWnB,KAG7C,CAACJ,GACR"}