react-use-focus-trap
Version:
A react hook to trap the focus within a reference
1 lines • 7.43 kB
Source Map (JSON)
{"mappings":";;AAAA;ACAO,SAAS,yCAAsB,CAAC,eAAe,EAAE;IACtD,MAAM,MAAM,GAAG,QAAQ,CAAC,eAAe,CAAC,AAAC;IACzC,OAAO,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC;CAC5B;AAEM,SAAS,yCAAc,CAAC,SAAS,EAAE,UAAU,EAAE;IACpD,MAAM,UAAU,GAAG;QAAC,SAAS;QAAE,UAAU;KAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAClD,yCAAiB,CAAC,IAAI,CAAC,CACxB,AAAC;IACF,OAAO,UAAU,CACd,GAAG,CAAC,CAAC,aAAa,GACjB,2CAAqB,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC,CAC9D,CACA,MAAM,CAAC,CAAC,aAAa,EAAE,YAAY,GAAK,aAAa,GAAG,YAAY,CAAC,CAAC;CAC1E;AAED;;;;;;;;;;GAUG,CACH,SAAS,2CAAqB,CAAC,QAAQ,EAAE,uBAAuB,EAAE;IAChE,IAAI,QAAQ,GAAG,CAAC,EACd,MAAM,IAAI,KAAK,CACb,CAAC,2EAA2E,EAAE,QAAQ,CAAC,CAAC,CACzF,CAAC;IAEJ,wGAAwG;IACxG,OAAO,QAAQ,KAAK,CAAC,GAAG,uBAAuB,GAAG,CAAC,GAAG,QAAQ,CAAC;CAChE;AAEM,SAAS,yCAAiB,CAAC,UAAU,EAAE;IAC5C,OAAO,yCAAsB,CAAC,UAAU,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC;CACpE;;;ADpCD,MAAM,+CAAyB,GAC7B,iMAAiM,AAAC;AACpM,MAAM,6BAAO,GAAG,CAAC,AAAC;AAEX,SAAS,yCAAY,GAAG;IAC7B,MAAM,OAAO,GAAG,CAAA,GAAA,aAAM,CAAA,CAAC,IAAI,CAAC,AAAC;IAE7B,MAAM,uBAAuB,GAAG,CAAA,GAAA,kBAAW,CAAA,CACzC,CACE,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAAG,KAAK,EACvB,SAAS,GAAG,CAAC,GACV;QACH,IAAI,SAAS,GAAG,oBAAoB,CAAC,MAAM,EACzC,gFAAgF;QAChF,wDAAwD;QACxD,OAAO,KAAK,CAAC;QAGf,MAAM,SAAS,GAAG,CAAC,CAAC,eAAe,AAAC;QACpC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,GAAG,CAAC,AAAC;QAEjD,IAAI,CAAC,YAAY,EACf,YAAY,GACV,oBAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAG9D,IAAI,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,CAAC,GAAG,YAAY,GAAG,CAAC,AAAC;QAChE,IAAI,SAAS,GAAG,QAAQ,EACtB,SAAS,GAAG,CAAC,CAAC;QAGhB,IAAI,SAAS,GAAG,CAAC,EACf,SAAS,GAAG,QAAQ,CAAC;QAGvB,MAAM,YAAY,GAAG,oBAAoB,CAAC,SAAS,CAAC,AAAC;QAErD,YAAY,CAAC,KAAK,EAAE,CAAC;QAErB,IAAI,QAAQ,CAAC,aAAa,KAAK,YAAY,EACzC,oBAAoB;QACpB,uBAAuB,CACrB,oBAAoB,EACpB,SAAS,EACT,eAAe,EACf,SAAS,GAAG,CAAC,CACd,CAAC;KAEL,CACF,AAAC;IAEF,mCAAmC;IACnC,MAAM,OAAO,GAAG,CAAA,GAAA,kBAAW,CAAA,CAAC,CAAC,GAAG,GAAK;QACnC,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,AAAC;QACpC,IAAI,WAAW,KAAK,IAAI,EACtB;YAAA,IAAI,GAAG,CAAC,KAAK,KAAK,6BAAO,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE;gBAC9C,GAAG,CAAC,cAAc,EAAE,CAAC;gBACrB,MAAM,eAAe,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,AAAC;gBACvC,IAAI,cAAc,GAAG,KAAK,CAAC,IAAI,CAC7B,WAAW,CAAC,gBAAgB,CAAC,+CAAyB,CAAC,CACxD,CAAC,MAAM,CACN,CAAC,gBAAgB,GAAK,CAAA,GAAA,yCAAiB,CAAA,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAC/D,AAAC,EAAC,sEAAsE;gBACzE,oDAAoD;gBACpD,cAAc,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA,GAAA,yCAAc,CAAA,CAAC,CAAC;gBAErD,uBAAuB,CAAC,cAAc,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;aACrE;SAAA,AACF;IACD,uDAAuD;KACxD,EAAE,EAAE,CAAC,AAAC;IAEP,CAAA,GAAA,gBAAS,CAAA,CAAC,IAAM;QACd,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAE5C,OAAO,IAAM;YACX,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;SAChD,CAAC;IACF,uDAAuD;KACxD,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QAAC,OAAO;KAAC,CAAC;CAClB","sources":["src/useFocusTrap.js","src/util.js"],"sourcesContent":["import { useCallback, useEffect, useRef } from \"react\";\nimport { getTabIndexOfNode, sortByTabIndex } from \"./util.js\";\n\nconst focusableElementsSelector =\n \"a[href], area[href], input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]\";\nconst TAB_KEY = 9;\n\nexport function useFocusTrap() {\n const trapRef = useRef(null);\n\n const selectNextFocusableElem = useCallback(\n (\n sortedFocusableElems,\n currentIndex,\n shiftKeyPressed = false,\n skipCount = 0\n ) => {\n if (skipCount > sortedFocusableElems.length) {\n // this means that it ran through all of elements but non was properly focusable\n // hence we stop it to avoid running in an infinite loop\n return false;\n }\n\n const backwards = !!shiftKeyPressed;\n const maxIndex = sortedFocusableElems.length - 1;\n\n if (!currentIndex) {\n currentIndex =\n sortedFocusableElems.indexOf(document.activeElement) ?? 0;\n }\n\n let nextIndex = backwards ? currentIndex - 1 : currentIndex + 1;\n if (nextIndex > maxIndex) {\n nextIndex = 0;\n }\n\n if (nextIndex < 0) {\n nextIndex = maxIndex;\n }\n\n const newFocusElem = sortedFocusableElems[nextIndex];\n\n newFocusElem.focus();\n\n if (document.activeElement !== newFocusElem) {\n // run another round\n selectNextFocusableElem(\n sortedFocusableElems,\n nextIndex,\n shiftKeyPressed,\n skipCount + 1\n );\n }\n }\n );\n\n // defining the trap function first\n const trapper = useCallback((evt) => {\n const trapRefElem = trapRef.current;\n if (trapRefElem !== null) {\n if (evt.which === TAB_KEY || evt.key === \"Tab\") {\n evt.preventDefault();\n const shiftKeyPressed = !!evt.shiftKey;\n let focusableElems = Array.from(\n trapRefElem.querySelectorAll(focusableElementsSelector)\n ).filter(\n (focusableElement) => getTabIndexOfNode(focusableElement) >= 0\n ); // caching this is NOT a good idea in dynamic applications - so don't!\n // now we need to sort it by tabIndex, highest first\n focusableElems = focusableElems.sort(sortByTabIndex);\n\n selectNextFocusableElem(focusableElems, undefined, shiftKeyPressed);\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n window.addEventListener(\"keydown\", trapper);\n\n return () => {\n window.removeEventListener(\"keydown\", trapper);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return [trapRef];\n}\n","export function convertToIntOrFallback(stringToConvert) {\n const parsed = parseInt(stringToConvert);\n return parsed ? parsed : 0;\n}\n\nexport function sortByTabIndex(firstNode, secondNode) {\n const tabIndexes = [firstNode, secondNode].map((node) =>\n getTabIndexOfNode(node)\n );\n return tabIndexes\n .map((tabIndexValue) =>\n sanitizeTabIndexInput(tabIndexValue, Math.max(...tabIndexes))\n )\n .reduce((previousValue, currentValue) => previousValue - currentValue);\n}\n\n/**\n * Prepares a tab-index to be further processed for the tab order of the focus trap.\n * It can't be less than 0, because negative values can not be part of the tab order at all.\n * In case it's exactly 0 it actually needs to be higher than any positive (> 0) value, since tab-index=0 means \"follow the system default order\".\n * The default tab order comes _after_ special tab indexes (>0).\n * @param {number} tabIndex The index to sanitize\n * @param {number} highestPositiveTabIndex The largest number among the tab indexes from the same context\n * @throws An error if the tabIndex is less than 0\n * @returns Tha sanitized tab index\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex} for further information on the tabindex and its order\n */\nfunction sanitizeTabIndexInput(tabIndex, highestPositiveTabIndex) {\n if (tabIndex < 0) {\n throw new Error(\n `Unable to sort given input. A negative value is not part of the tab order: ${tabIndex}`\n );\n }\n // 0 based tab indexes have a higher order than positive valued indicies, thus we add 1 to the max value\n return tabIndex === 0 ? highestPositiveTabIndex + 1 : tabIndex;\n}\n\nexport function getTabIndexOfNode(targetNode) {\n return convertToIntOrFallback(targetNode.getAttribute(\"tabindex\"));\n}\n"],"names":[],"version":3,"file":"useFocusTrap.js.map"}