{"version":3,"file":"useListNavigation.cjs","sources":["../../../../src/hooks/useListNavigation/useListNavigation.ts"],"sourcesContent":["import { type RefObject, useLayoutEffect } from \"react\";\n\ntype TimerHandle = { id: 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: TimerHandle;\n}\ninterface SearchDetails extends ListDetails {\n    key: string;\n}\ninterface EventDetails extends ListDetails {\n    event: KeyboardEvent;\n}\n/**\n * Enten `ref` eller `element` må gis. `ref` brukes når listen alltid\n * er i DOM-en; `element` foretrekkes når elementet mountes/unmountes\n * dynamisk (f.eks. portalert via floating-ui), slik at listeneren\n * re-festes når elementet faktisk er tilgjengelig.\n */\ntype UseListNavigationProps<T> =\n    | {\n          ref: RefObject<T | null>;\n          element?: never;\n      }\n    | {\n          ref?: never;\n          element: T | null;\n      };\n\nexport function useListNavigation<T extends HTMLElement>({\n    ref,\n    element,\n}: UseListNavigationProps<T>): void {\n    // Bruker `useLayoutEffect` slik at keydown-listeneren er på plass før\n    // browseren maler neste frame. Det er viktig for konsumenter som\n    // flytter fokus inn i lista i en `requestAnimationFrame`-callback\n    // (som Select), ellers kan første tastetrykk gå tapt.\n    useLayoutEffect(() => {\n        // `ref?.current` leses inne i effekten — refen er fylt i commit-\n        // fasen, og var `null` i render-tid for callere som passerer\n        // `ref` (som Combobox).\n        const list = element ?? ref?.current ?? null;\n        if (!list) return;\n\n        // `TimerHandle` er et muterbart objekt slik at `resetWhenIdle`\n        // kan oppdatere `id` på samme referanse — `setTimeout`-handlen må\n        // være delt på tvers av tastetrykk for at `clearTimeout` skal\n        // kunne avbryte forrige timeout.\n        const searchResetTimer: TimerHandle = { id: undefined };\n        const search: KeyBuffer = { keys: \"\" }; // keypress buffer is an object to preserve state\n        const handler = (event: KeyboardEvent) => {\n            handleListKeyNav({ list, event, search, searchResetTimer });\n        };\n\n        list.addEventListener(\"keydown\", handler);\n        return () => {\n            list.removeEventListener(\"keydown\", handler);\n            if (searchResetTimer.id !== undefined) {\n                clearTimeout(searchResetTimer.id);\n            }\n        };\n    }, [element, 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: TimerHandle) {\n    if (timer.id !== undefined) {\n        clearTimeout(timer.id);\n        timer.id = undefined;\n    }\n    timer.id = window.setTimeout(() => {\n        if (search) {\n            search.keys = \"\";\n        }\n        timer.id = undefined;\n    }, 500);\n}\n"],"names":["handleMoveTo","direction","event","list","currentFocus","preventDefault","current","thisOption","prevOption","previousElementSibling","focus","nextOption","nextElementSibling","firstItem","querySelector","listItems","querySelectorAll","length","moveFocusTo","ref","element","useLayoutEffect","searchResetTimer","id","search","keys","handler","key","target","moveDetails","searchResult","concat","timer","clearTimeout","window","setTimeout","resetWhenIdle","n","label","innerText","toLowerCase","indexOf","findItem","handleListKeyNav","addEventListener","removeEventListener"],"mappings":"yGAwEA,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,2BAzCO,UACHe,IAAAA,EACAC,QAAAA,IAMAC,EAAAA,gBAAgB,KAIZ,MAAMlB,EAAOiB,GAAWD,GAAKb,SAAW,KACxC,IAAKH,EAAM,OAMX,MAAMmB,EAAgC,CAAEC,QAAI,GACtCC,EAAoB,CAAEC,KAAM,IAC5BC,EAAWxB,KAsBzB,UACIC,KAAAA,EACAD,MAAAA,EACAsB,OAAAA,EACAF,iBAAAA,IAEA,MAAQK,IAAAA,EAAKC,OAAAA,GAAW1B,EAGlB2B,EAAc,CAChB3B,MAAAA,EACAC,KAAAA,EACAC,aALiBwB,GAQrB,OAAQD,GACJ,IAAK,UACL,IAAK,SACD3B,EAAa,OAAQ6B,GACrB,MACJ,IAAK,YACL,IAAK,WACD7B,EAAa,OAAQ6B,GACrB,MACJ,IAAK,OACD7B,EAAa,QAAS6B,GACtB,MACJ,IAAK,MACD7B,EAAa,OAAQ6B,GACrB,MACJ,IAAK,MAED3B,EAAMG,iBACN,MACJ,IAAK,QACL,IAAK,IACD,MAEJ,QACI,QAAe,IAAXmB,EAAsB,CACtB,MAAMM,EA0DtB,UACI3B,KAAAA,EACAwB,IAAAA,EACAH,OAAAA,EACAF,iBAAAA,IAEA,MAAMP,EAAYZ,EAAKa,iBAAiB,mBACxC,IAAKD,EAAUE,OAAQ,OAAO,KAE9B,GAAIO,EAAQ,CACRA,EAAOC,KAAOD,EAAOC,KAAKM,OAAOJ,GAczC,SAAuBH,EAAmBQ,QACrB,IAAbA,EAAMT,KACNU,aAAaD,EAAMT,IACnBS,EAAMT,QAAK,GAEfS,EAAMT,GAAKW,OAAOC,WAAW,KACrBX,IACAA,EAAOC,KAAO,IAElBO,EAAMT,QAAK,GACZ,IACP,CAxBQa,CAAcZ,EAAQF,GAEtB,IAAA,IAASe,EAAI,EAAGA,EAAItB,EAAUE,OAAQoB,IAAK,CACvC,MAAMC,EAASvB,EAAUsB,GAAyBE,UAClD,GAAID,GAAsD,IAA7CA,EAAME,cAAcC,QAAQjB,EAAOC,MAC5C,OAAOV,EAAUsB,EAEzB,CACJ,CAEA,OAAO,IACX,CAhFqCK,CAAS,CAC1BvC,KAAAA,EACAwB,IAAAA,EACAH,OAAAA,EACAF,iBAAAA,IAEAQ,GACAA,EAAapB,OAErB,EAGZ,CAzEYiC,CAAiB,CAAExC,KAAAA,EAAMD,MAAAA,EAAOsB,OAAAA,EAAQF,iBAAAA,KAG5C,OAAAnB,EAAKyC,iBAAiB,UAAWlB,GAC1B,KACHvB,EAAK0C,oBAAoB,UAAWnB,QACR,IAAxBJ,EAAiBC,IACjBU,aAAaX,EAAiBC,MAGvC,CAACH,EAASD,GACjB"}