@vielzeug/focus 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/focus.cjs +2 -0
- package/dist/focus.cjs.map +1 -0
- package/dist/focus.iife.js +2 -0
- package/dist/focus.iife.js.map +1 -0
- package/dist/focus.js +2 -0
- package/dist/focus.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/list-navigation.cjs +2 -0
- package/dist/list-navigation.cjs.map +1 -0
- package/dist/list-navigation.d.ts +39 -0
- package/dist/list-navigation.d.ts.map +1 -0
- package/dist/list-navigation.js +123 -0
- package/dist/list-navigation.js.map +1 -0
- package/dist/restore-focus.cjs +2 -0
- package/dist/restore-focus.cjs.map +1 -0
- package/dist/restore-focus.d.ts +13 -0
- package/dist/restore-focus.d.ts.map +1 -0
- package/dist/restore-focus.js +31 -0
- package/dist/restore-focus.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @vielzeug/focus
|
|
2
|
+
|
|
3
|
+
Framework-neutral focus navigation and restoration primitives.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @vielzeug/focus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { captureFocus, createListNavigation } from '@vielzeug/focus';
|
|
15
|
+
|
|
16
|
+
const restore = captureFocus();
|
|
17
|
+
|
|
18
|
+
const listNavigation = createListNavigation({
|
|
19
|
+
getItems: () => items,
|
|
20
|
+
loop: true,
|
|
21
|
+
onNavigate: ({ item }) => item.focus(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
listElement.addEventListener('keydown', listNavigation.handleKeydown);
|
|
25
|
+
|
|
26
|
+
restore();
|
|
27
|
+
listNavigation.dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
[Full documentation](https://vielzeug.dev/focus/)
|
package/dist/focus.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},t={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowLeft`],prev:[`ArrowUp`,`ArrowRight`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowLeft`],prev:[`ArrowRight`]},vertical:e.vertical},n=500,r=(e,t)=>typeof e==`function`?e():e??t,i=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},a=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},o=e=>typeof e==`number`&&Number.isFinite(e)&&e>0?e:n,s=n=>{let s=new AbortController,c=-1,l=!1,u=``,d=0,f,p=(e,t)=>n.isItemDisabled?.(e,t)??!1,m=(e,t)=>t>=0&&t<e.length&&!p(e[t],t),h=e=>(m(e,c)||(c=-1),c),g=()=>{u=``,d=0},_=(e,t,r,i)=>m(e,t)?(c=t,n.onNavigate?.({action:r,event:i,index:t,item:e[t]}),c):(c=-1,c),v=(e,t,n)=>n===`forward`?i(e,t,(e,t)=>!p(e,t)):a(e,t,(e,t)=>!p(e,t)),y=(e,t,r)=>{let i=h(e);if(!e.length)return c=-1,c;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=v(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return _(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=v(e,n,t);if(i>=0)return _(e,i,s,r)}return c},b=(e,t,n)=>(g(),h(e),e.length?t===`first`?_(e,v(e,0,`forward`),t,n):t===`last`?_(e,v(e,e.length-1,`backward`),t,n):y(e,t===`next`?`forward`:`backward`,n):(c=-1,c)),x=(e,t,r)=>{let i=h(e),a=i<0?0:r?i:i+1;for(let r=0;r<e.length;r++){let i=(a+r)%e.length,o=e[i];if(!p(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},S=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return!1;let r=Date.now(),i=e.key.toLocaleLowerCase(),a=o(n.typeahead.delayMs);r-d>=a&&(u=``),d=r;let s=u+i;u=s.length>1&&[...s].every(e=>e===i)?i:s;let c=x(t,u,u.length>1);return c<0&&u.length>1&&(u=i,c=x(t,u,!1)),c<0?!1:(_(t,c,`typeahead`,e),!0)},C=e=>{if(l)return-1;let t=n.getItems();if(h(t),g(),e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return m(t,r)?(c=r,c):(c=-1,c)},w=e=>l?-1:b(n.getItems(),e),T=()=>{if(l)return;let e=n.getItems(),t=h(e);return t>=0?e[t]:void 0},E=()=>{l||(c=-1,g())},D=()=>l?-1:h(n.getItems()),O=i=>{let a=n.keys,o=(r(n.direction,`ltr`)===`rtl`?t:e)[r(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((a?.[e]??o[e]).includes(i))return e},k=e=>{if(l||r(n.disabled,!1))return!1;let t=n.getItems();h(t);let i=O(e.key);return i?(e.preventDefault(),b(t,i,e),!0):S(e,t)},A=()=>{l||(l=!0,c=-1,g(),f?.(),f=void 0,s.abort())};return n.signal?.aborted?A():n.signal&&(n.signal.addEventListener(`abort`,A,{once:!0}),f=()=>n.signal?.removeEventListener(`abort`,A)),{get disposalSignal(){return s.signal},dispose:A,get disposed(){return l},getActiveItem:T,getIndex:D,handleKeydown:k,navigate:w,reset:E,set:C,[Symbol.dispose]:A}},c=e=>typeof e==`function`?e():e,l=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},u=e=>e.isConnected,d=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,f=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,p=e=>u(e)&&!d(e)&&!f(e),m=(e,t)=>(e.focus({preventScroll:t}),l(e.ownerDocument)===e),h=(e,t={})=>{let n=c(e);if(n&&p(n)&&m(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r=c(t.fallback);return!r||!p(r)?!1:m(r,t.preventScroll)},g=(e={})=>{let t=l(document),n=!e.signal?.aborted;n||(t=null);let r=()=>{n=!1,t=null};return e.signal?.aborted||e.signal?.addEventListener(`abort`,r,{once:!0}),()=>{if(!n)return!1;n=!1,e.signal?.removeEventListener(`abort`,r);let i=t;return t=null,i?h(i,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?h(e.fallback,{preventScroll:e.preventScroll}):!1}};exports.captureFocus=g,exports.createListNavigation=s,exports.restoreFocus=h;
|
|
2
|
+
//# sourceMappingURL=focus.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.cjs","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, readonly string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nconst DEFAULT_KEYS_RTL: typeof DEFAULT_KEYS = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nconst DEFAULT_TYPEAHEAD_DELAY_MS = 500;\n\nconst read = <T>(value: MaybeGetter<T> | undefined, fallback: T): T =>\n typeof value === 'function' ? (value as () => T)() : (value ?? fallback);\n\nconst findForward = <T>(items: readonly T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let index = start; index < items.length; index++) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(\n items: readonly T[],\n start: number,\n predicate: (item: T, index: number) => boolean,\n): number => {\n for (let index = start; index >= 0; index--) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst resolveTypeaheadDelay = (delay: number | undefined): number =>\n typeof delay === 'number' && Number.isFinite(delay) && delay > 0 ? delay : DEFAULT_TYPEAHEAD_DELAY_MS;\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const disposalController = new AbortController();\n let index = -1;\n let disposed = false;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\n let removeExternalAbortListener: (() => void) | undefined;\n\n const isItemDisabled = (item: T, itemIndex: number): boolean => options.isItemDisabled?.(item, itemIndex) ?? false;\n\n const isUsableIndex = (items: readonly T[], itemIndex: number): boolean =>\n itemIndex >= 0 && itemIndex < items.length && !isItemDisabled(items[itemIndex], itemIndex);\n\n const normalizeIndex = (items: readonly T[]): number => {\n if (!isUsableIndex(items, index)) index = -1;\n\n return index;\n };\n\n const resetTypeahead = (): void => {\n typeaheadBuffer = '';\n lastTypeaheadAt = 0;\n };\n\n const commitIndex = (\n items: readonly T[],\n nextIndex: number,\n action: ListKeyAction,\n event?: KeyboardEvent,\n ): number => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return index;\n }\n\n index = nextIndex;\n options.onNavigate?.({ action, event, index: nextIndex, item: items[nextIndex] });\n\n return index;\n };\n\n const findEnabledIndex = (items: readonly T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward')\n return findForward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n\n return findBackward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n };\n\n const move = (items: readonly T[], direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n\n const nextIndex = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (nextIndex >= 0) return commitIndex(items, nextIndex, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(items, wrapped, action, event);\n }\n\n return index;\n };\n\n const navigateWithItems = (items: readonly T[], action: ListNavigationAction, event?: KeyboardEvent): number => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n if (action === 'first') {\n return commitIndex(items, findEnabledIndex(items, 0, 'forward'), action, event);\n }\n\n if (action === 'last') {\n return commitIndex(items, findEnabledIndex(items, items.length - 1, 'backward'), action, event);\n }\n\n return move(items, action === 'next' ? 'forward' : 'backward', event);\n };\n\n const findTypeaheadMatch = (items: readonly T[], search: string, includeCurrent: boolean): number => {\n const current = normalizeIndex(items);\n const start = current < 0 ? 0 : includeCurrent ? current : current + 1;\n\n for (let offset = 0; offset < items.length; offset++) {\n const itemIndex = (start + offset) % items.length;\n const item = items[itemIndex];\n\n if (isItemDisabled(item, itemIndex)) continue;\n\n if (options.typeahead?.getLabel(item, itemIndex).toLocaleLowerCase().startsWith(search)) {\n return itemIndex;\n }\n }\n\n return -1;\n };\n\n const handleTypeahead = (event: KeyboardEvent, items: readonly T[]): boolean => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return false;\n\n const now = Date.now();\n const key = event.key.toLocaleLowerCase();\n const delay = resolveTypeaheadDelay(options.typeahead.delayMs);\n\n if (now - lastTypeaheadAt >= delay) typeaheadBuffer = '';\n\n lastTypeaheadAt = now;\n\n const candidate = typeaheadBuffer + key;\n const repeatedCharacter = candidate.length > 1 && [...candidate].every((character) => character === key);\n typeaheadBuffer = repeatedCharacter ? key : candidate;\n\n let nextIndex = findTypeaheadMatch(items, typeaheadBuffer, typeaheadBuffer.length > 1);\n\n if (nextIndex < 0 && typeaheadBuffer.length > 1) {\n typeaheadBuffer = key;\n nextIndex = findTypeaheadMatch(items, typeaheadBuffer, false);\n }\n\n if (nextIndex < 0) return false;\n\n commitIndex(items, nextIndex, 'typeahead', event);\n\n return true;\n };\n\n const set = (nextIndex: number): number => {\n if (disposed) return -1;\n\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (nextIndex < 0 || !items.length) {\n index = -1;\n\n return index;\n }\n\n const clamped = Math.min(nextIndex, items.length - 1);\n\n if (!isUsableIndex(items, clamped)) {\n index = -1;\n\n return index;\n }\n\n index = clamped;\n\n return index;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\n if (disposed) return undefined;\n\n const items = options.getItems();\n const current = normalizeIndex(items);\n\n return current >= 0 ? items[current] : undefined;\n };\n\n const reset = (): void => {\n if (disposed) return;\n\n index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n if (disposed) return -1;\n\n return normalizeIndex(options.getItems());\n };\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = read(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[read(options.orientation, 'vertical')];\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n if ((keys?.[action] ?? defaults[action]).includes(eventKey)) return action;\n }\n\n return undefined;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || read(options.disabled, false)) return false;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n event.preventDefault();\n navigateWithItems(items, action, event);\n\n return true;\n }\n\n return handleTypeahead(event, items);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n index = -1;\n resetTypeahead();\n removeExternalAbortListener?.();\n removeExternalAbortListener = undefined;\n disposalController.abort();\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n removeExternalAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n","export type FocusableElement = HTMLElement | SVGElement;\nexport type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);\n\nexport type RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\nexport type CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\nexport type FocusRestorer = () => boolean;\n\nconst resolveTarget = (target: FocusTarget): FocusableElement | null | undefined => {\n return typeof target === 'function' ? target() : target;\n};\n\nconst getDeepActiveElement = (rootDocument: Document): Element | null => {\n let active: Element | null = rootDocument.activeElement;\n\n while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n\n return active;\n};\n\nconst isConnected = (target: FocusableElement): boolean => {\n // `isConnected` pierces shadow boundaries (shadow-including root is the document);\n // `ownerDocument.contains` does NOT — it would reject elements inside shadow roots.\n return target.isConnected;\n};\n\nconst isDisabled = (target: FocusableElement): boolean => {\n return target instanceof HTMLElement && 'disabled' in target && Boolean((target as HTMLInputElement).disabled);\n};\n\nconst isInert = (target: FocusableElement): boolean => {\n if (target.closest('[inert]')) return true;\n\n return target instanceof HTMLElement && target.inert;\n};\n\nconst canRestoreTo = (target: FocusableElement): boolean => {\n return isConnected(target) && !isDisabled(target) && !isInert(target);\n};\n\nconst focusAndVerify = (target: FocusableElement, preventScroll: boolean | undefined): boolean => {\n target.focus({ preventScroll });\n\n return getDeepActiveElement(target.ownerDocument) === target;\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const next = resolveTarget(target);\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n const fallback = resolveTarget(options.fallback);\n\n if (!fallback || !canRestoreTo(fallback)) return false;\n\n return focusAndVerify(fallback, options.preventScroll);\n};\n\nexport const captureFocus = (options: CaptureFocusOptions = {}): FocusRestorer => {\n let captured = getDeepActiveElement(document) as FocusableElement | null;\n let available = !options.signal?.aborted;\n\n if (!available) captured = null;\n\n const cancel = (): void => {\n available = false;\n captured = null;\n };\n\n if (!options.signal?.aborted) {\n options.signal?.addEventListener('abort', cancel, { once: true });\n }\n\n return () => {\n if (!available) return false;\n\n available = false;\n options.signal?.removeEventListener('abort', cancel);\n\n const target = captured;\n captured = null;\n\n if (target) {\n return restoreFocus(target, {\n fallback: options.fallback,\n preventScroll: options.preventScroll,\n });\n }\n\n if (options.fallback) {\n return restoreFocus(options.fallback, {\n preventScroll: options.preventScroll,\n });\n }\n\n return false;\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Focus=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},n={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowLeft`],prev:[`ArrowUp`,`ArrowRight`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowLeft`],prev:[`ArrowRight`]},vertical:t.vertical},r=500,i=(e,t)=>typeof e==`function`?e():e??t,a=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},o=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},s=e=>typeof e==`number`&&Number.isFinite(e)&&e>0?e:r,c=e=>{let r=new AbortController,c=-1,l=!1,u=``,d=0,f,p=(t,n)=>e.isItemDisabled?.(t,n)??!1,m=(e,t)=>t>=0&&t<e.length&&!p(e[t],t),h=e=>(m(e,c)||(c=-1),c),g=()=>{u=``,d=0},_=(t,n,r,i)=>m(t,n)?(c=n,e.onNavigate?.({action:r,event:i,index:n,item:t[n]}),c):(c=-1,c),v=(e,t,n)=>n===`forward`?a(e,t,(e,t)=>!p(e,t)):o(e,t,(e,t)=>!p(e,t)),y=(t,n,r)=>{let i=h(t);if(!t.length)return c=-1,c;let a=i<0?n===`forward`?0:t.length-1:n===`forward`?i+1:i-1,o=v(t,a,n),s=n===`forward`?`next`:`prev`;if(o>=0)return _(t,o,s,r);if(e.loop){let e=n===`forward`?0:t.length-1,i=v(t,e,n);if(i>=0)return _(t,i,s,r)}return c},b=(e,t,n)=>(g(),h(e),e.length?t===`first`?_(e,v(e,0,`forward`),t,n):t===`last`?_(e,v(e,e.length-1,`backward`),t,n):y(e,t===`next`?`forward`:`backward`,n):(c=-1,c)),x=(t,n,r)=>{let i=h(t),a=i<0?0:r?i:i+1;for(let r=0;r<t.length;r++){let i=(a+r)%t.length,o=t[i];if(!p(o,i)&&e.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(n))return i}return-1},S=(t,n)=>{if(!e.typeahead||t.key.length!==1||t.ctrlKey||t.altKey||t.metaKey)return!1;let r=Date.now(),i=t.key.toLocaleLowerCase(),a=s(e.typeahead.delayMs);r-d>=a&&(u=``),d=r;let o=u+i;u=o.length>1&&[...o].every(e=>e===i)?i:o;let c=x(n,u,u.length>1);return c<0&&u.length>1&&(u=i,c=x(n,u,!1)),c<0?!1:(_(n,c,`typeahead`,t),!0)},C=t=>{if(l)return-1;let n=e.getItems();if(h(n),g(),t<0||!n.length)return c=-1,c;let r=Math.min(t,n.length-1);return m(n,r)?(c=r,c):(c=-1,c)},w=t=>l?-1:b(e.getItems(),t),T=()=>{if(l)return;let t=e.getItems(),n=h(t);return n>=0?t[n]:void 0},E=()=>{l||(c=-1,g())},D=()=>l?-1:h(e.getItems()),O=r=>{let a=e.keys,o=(i(e.direction,`ltr`)===`rtl`?n:t)[i(e.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((a?.[e]??o[e]).includes(r))return e},k=t=>{if(l||i(e.disabled,!1))return!1;let n=e.getItems();h(n);let r=O(t.key);return r?(t.preventDefault(),b(n,r,t),!0):S(t,n)},A=()=>{l||(l=!0,c=-1,g(),f?.(),f=void 0,r.abort())};return e.signal?.aborted?A():e.signal&&(e.signal.addEventListener(`abort`,A,{once:!0}),f=()=>e.signal?.removeEventListener(`abort`,A)),{get disposalSignal(){return r.signal},dispose:A,get disposed(){return l},getActiveItem:T,getIndex:D,handleKeydown:k,navigate:w,reset:E,set:C,[Symbol.dispose]:A}},l=e=>typeof e==`function`?e():e,u=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},d=e=>e.isConnected,f=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,p=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,m=e=>d(e)&&!f(e)&&!p(e),h=(e,t)=>(e.focus({preventScroll:t}),u(e.ownerDocument)===e),g=(e,t={})=>{let n=l(e);if(n&&m(n)&&h(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r=l(t.fallback);return!r||!m(r)?!1:h(r,t.preventScroll)};return e.captureFocus=(e={})=>{let t=u(document),n=!e.signal?.aborted;n||(t=null);let r=()=>{n=!1,t=null};return e.signal?.aborted||e.signal?.addEventListener(`abort`,r,{once:!0}),()=>{if(!n)return!1;n=!1,e.signal?.removeEventListener(`abort`,r);let i=t;return t=null,i?g(i,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?g(e.fallback,{preventScroll:e.preventScroll}):!1}},e.createListNavigation=c,e.restoreFocus=g,e})({});
|
|
2
|
+
//# sourceMappingURL=focus.iife.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.iife.js","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, readonly string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nconst DEFAULT_KEYS_RTL: typeof DEFAULT_KEYS = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nconst DEFAULT_TYPEAHEAD_DELAY_MS = 500;\n\nconst read = <T>(value: MaybeGetter<T> | undefined, fallback: T): T =>\n typeof value === 'function' ? (value as () => T)() : (value ?? fallback);\n\nconst findForward = <T>(items: readonly T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let index = start; index < items.length; index++) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(\n items: readonly T[],\n start: number,\n predicate: (item: T, index: number) => boolean,\n): number => {\n for (let index = start; index >= 0; index--) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst resolveTypeaheadDelay = (delay: number | undefined): number =>\n typeof delay === 'number' && Number.isFinite(delay) && delay > 0 ? delay : DEFAULT_TYPEAHEAD_DELAY_MS;\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const disposalController = new AbortController();\n let index = -1;\n let disposed = false;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\n let removeExternalAbortListener: (() => void) | undefined;\n\n const isItemDisabled = (item: T, itemIndex: number): boolean => options.isItemDisabled?.(item, itemIndex) ?? false;\n\n const isUsableIndex = (items: readonly T[], itemIndex: number): boolean =>\n itemIndex >= 0 && itemIndex < items.length && !isItemDisabled(items[itemIndex], itemIndex);\n\n const normalizeIndex = (items: readonly T[]): number => {\n if (!isUsableIndex(items, index)) index = -1;\n\n return index;\n };\n\n const resetTypeahead = (): void => {\n typeaheadBuffer = '';\n lastTypeaheadAt = 0;\n };\n\n const commitIndex = (\n items: readonly T[],\n nextIndex: number,\n action: ListKeyAction,\n event?: KeyboardEvent,\n ): number => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return index;\n }\n\n index = nextIndex;\n options.onNavigate?.({ action, event, index: nextIndex, item: items[nextIndex] });\n\n return index;\n };\n\n const findEnabledIndex = (items: readonly T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward')\n return findForward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n\n return findBackward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n };\n\n const move = (items: readonly T[], direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n\n const nextIndex = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (nextIndex >= 0) return commitIndex(items, nextIndex, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(items, wrapped, action, event);\n }\n\n return index;\n };\n\n const navigateWithItems = (items: readonly T[], action: ListNavigationAction, event?: KeyboardEvent): number => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n if (action === 'first') {\n return commitIndex(items, findEnabledIndex(items, 0, 'forward'), action, event);\n }\n\n if (action === 'last') {\n return commitIndex(items, findEnabledIndex(items, items.length - 1, 'backward'), action, event);\n }\n\n return move(items, action === 'next' ? 'forward' : 'backward', event);\n };\n\n const findTypeaheadMatch = (items: readonly T[], search: string, includeCurrent: boolean): number => {\n const current = normalizeIndex(items);\n const start = current < 0 ? 0 : includeCurrent ? current : current + 1;\n\n for (let offset = 0; offset < items.length; offset++) {\n const itemIndex = (start + offset) % items.length;\n const item = items[itemIndex];\n\n if (isItemDisabled(item, itemIndex)) continue;\n\n if (options.typeahead?.getLabel(item, itemIndex).toLocaleLowerCase().startsWith(search)) {\n return itemIndex;\n }\n }\n\n return -1;\n };\n\n const handleTypeahead = (event: KeyboardEvent, items: readonly T[]): boolean => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return false;\n\n const now = Date.now();\n const key = event.key.toLocaleLowerCase();\n const delay = resolveTypeaheadDelay(options.typeahead.delayMs);\n\n if (now - lastTypeaheadAt >= delay) typeaheadBuffer = '';\n\n lastTypeaheadAt = now;\n\n const candidate = typeaheadBuffer + key;\n const repeatedCharacter = candidate.length > 1 && [...candidate].every((character) => character === key);\n typeaheadBuffer = repeatedCharacter ? key : candidate;\n\n let nextIndex = findTypeaheadMatch(items, typeaheadBuffer, typeaheadBuffer.length > 1);\n\n if (nextIndex < 0 && typeaheadBuffer.length > 1) {\n typeaheadBuffer = key;\n nextIndex = findTypeaheadMatch(items, typeaheadBuffer, false);\n }\n\n if (nextIndex < 0) return false;\n\n commitIndex(items, nextIndex, 'typeahead', event);\n\n return true;\n };\n\n const set = (nextIndex: number): number => {\n if (disposed) return -1;\n\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (nextIndex < 0 || !items.length) {\n index = -1;\n\n return index;\n }\n\n const clamped = Math.min(nextIndex, items.length - 1);\n\n if (!isUsableIndex(items, clamped)) {\n index = -1;\n\n return index;\n }\n\n index = clamped;\n\n return index;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\n if (disposed) return undefined;\n\n const items = options.getItems();\n const current = normalizeIndex(items);\n\n return current >= 0 ? items[current] : undefined;\n };\n\n const reset = (): void => {\n if (disposed) return;\n\n index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n if (disposed) return -1;\n\n return normalizeIndex(options.getItems());\n };\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = read(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[read(options.orientation, 'vertical')];\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n if ((keys?.[action] ?? defaults[action]).includes(eventKey)) return action;\n }\n\n return undefined;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || read(options.disabled, false)) return false;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n event.preventDefault();\n navigateWithItems(items, action, event);\n\n return true;\n }\n\n return handleTypeahead(event, items);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n index = -1;\n resetTypeahead();\n removeExternalAbortListener?.();\n removeExternalAbortListener = undefined;\n disposalController.abort();\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n removeExternalAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n","export type FocusableElement = HTMLElement | SVGElement;\nexport type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);\n\nexport type RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\nexport type CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\nexport type FocusRestorer = () => boolean;\n\nconst resolveTarget = (target: FocusTarget): FocusableElement | null | undefined => {\n return typeof target === 'function' ? target() : target;\n};\n\nconst getDeepActiveElement = (rootDocument: Document): Element | null => {\n let active: Element | null = rootDocument.activeElement;\n\n while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n\n return active;\n};\n\nconst isConnected = (target: FocusableElement): boolean => {\n // `isConnected` pierces shadow boundaries (shadow-including root is the document);\n // `ownerDocument.contains` does NOT — it would reject elements inside shadow roots.\n return target.isConnected;\n};\n\nconst isDisabled = (target: FocusableElement): boolean => {\n return target instanceof HTMLElement && 'disabled' in target && Boolean((target as HTMLInputElement).disabled);\n};\n\nconst isInert = (target: FocusableElement): boolean => {\n if (target.closest('[inert]')) return true;\n\n return target instanceof HTMLElement && target.inert;\n};\n\nconst canRestoreTo = (target: FocusableElement): boolean => {\n return isConnected(target) && !isDisabled(target) && !isInert(target);\n};\n\nconst focusAndVerify = (target: FocusableElement, preventScroll: boolean | undefined): boolean => {\n target.focus({ preventScroll });\n\n return getDeepActiveElement(target.ownerDocument) === target;\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const next = resolveTarget(target);\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n const fallback = resolveTarget(options.fallback);\n\n if (!fallback || !canRestoreTo(fallback)) return false;\n\n return focusAndVerify(fallback, options.preventScroll);\n};\n\nexport const captureFocus = (options: CaptureFocusOptions = {}): FocusRestorer => {\n let captured = getDeepActiveElement(document) as FocusableElement | null;\n let available = !options.signal?.aborted;\n\n if (!available) captured = null;\n\n const cancel = (): void => {\n available = false;\n captured = null;\n };\n\n if (!options.signal?.aborted) {\n options.signal?.addEventListener('abort', cancel, { once: true });\n }\n\n return () => {\n if (!available) return false;\n\n available = false;\n options.signal?.removeEventListener('abort', cancel);\n\n const target = captured;\n captured = null;\n\n if (target) {\n return restoreFocus(target, {\n fallback: options.fallback,\n preventScroll: options.preventScroll,\n });\n }\n\n if (options.fallback) {\n return restoreFocus(options.fallback, {\n preventScroll: options.preventScroll,\n });\n }\n\n return false;\n };\n};\n"],"mappings":"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"}
|
package/dist/focus.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},t={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowLeft`],prev:[`ArrowUp`,`ArrowRight`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowLeft`],prev:[`ArrowRight`]},vertical:e.vertical},n=500,r=(e,t)=>typeof e==`function`?e():e??t,i=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},a=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},o=e=>typeof e==`number`&&Number.isFinite(e)&&e>0?e:n,s=n=>{let s=new AbortController,c=-1,l=!1,u=``,d=0,f,p=(e,t)=>n.isItemDisabled?.(e,t)??!1,m=(e,t)=>t>=0&&t<e.length&&!p(e[t],t),h=e=>(m(e,c)||(c=-1),c),g=()=>{u=``,d=0},_=(e,t,r,i)=>m(e,t)?(c=t,n.onNavigate?.({action:r,event:i,index:t,item:e[t]}),c):(c=-1,c),v=(e,t,n)=>n===`forward`?i(e,t,(e,t)=>!p(e,t)):a(e,t,(e,t)=>!p(e,t)),y=(e,t,r)=>{let i=h(e);if(!e.length)return c=-1,c;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=v(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return _(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=v(e,n,t);if(i>=0)return _(e,i,s,r)}return c},b=(e,t,n)=>(g(),h(e),e.length?t===`first`?_(e,v(e,0,`forward`),t,n):t===`last`?_(e,v(e,e.length-1,`backward`),t,n):y(e,t===`next`?`forward`:`backward`,n):(c=-1,c)),x=(e,t,r)=>{let i=h(e),a=i<0?0:r?i:i+1;for(let r=0;r<e.length;r++){let i=(a+r)%e.length,o=e[i];if(!p(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},S=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return!1;let r=Date.now(),i=e.key.toLocaleLowerCase(),a=o(n.typeahead.delayMs);r-d>=a&&(u=``),d=r;let s=u+i;u=s.length>1&&[...s].every(e=>e===i)?i:s;let c=x(t,u,u.length>1);return c<0&&u.length>1&&(u=i,c=x(t,u,!1)),c<0?!1:(_(t,c,`typeahead`,e),!0)},C=e=>{if(l)return-1;let t=n.getItems();if(h(t),g(),e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return m(t,r)?(c=r,c):(c=-1,c)},w=e=>l?-1:b(n.getItems(),e),T=()=>{if(l)return;let e=n.getItems(),t=h(e);return t>=0?e[t]:void 0},E=()=>{l||(c=-1,g())},D=()=>l?-1:h(n.getItems()),O=i=>{let a=n.keys,o=(r(n.direction,`ltr`)===`rtl`?t:e)[r(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((a?.[e]??o[e]).includes(i))return e},k=e=>{if(l||r(n.disabled,!1))return!1;let t=n.getItems();h(t);let i=O(e.key);return i?(e.preventDefault(),b(t,i,e),!0):S(e,t)},A=()=>{l||(l=!0,c=-1,g(),f?.(),f=void 0,s.abort())};return n.signal?.aborted?A():n.signal&&(n.signal.addEventListener(`abort`,A,{once:!0}),f=()=>n.signal?.removeEventListener(`abort`,A)),{get disposalSignal(){return s.signal},dispose:A,get disposed(){return l},getActiveItem:T,getIndex:D,handleKeydown:k,navigate:w,reset:E,set:C,[Symbol.dispose]:A}},c=e=>typeof e==`function`?e():e,l=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},u=e=>e.isConnected,d=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,f=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,p=e=>u(e)&&!d(e)&&!f(e),m=(e,t)=>(e.focus({preventScroll:t}),l(e.ownerDocument)===e),h=(e,t={})=>{let n=c(e);if(n&&p(n)&&m(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r=c(t.fallback);return!r||!p(r)?!1:m(r,t.preventScroll)},g=(e={})=>{let t=l(document),n=!e.signal?.aborted;n||(t=null);let r=()=>{n=!1,t=null};return e.signal?.aborted||e.signal?.addEventListener(`abort`,r,{once:!0}),()=>{if(!n)return!1;n=!1,e.signal?.removeEventListener(`abort`,r);let i=t;return t=null,i?h(i,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?h(e.fallback,{preventScroll:e.preventScroll}):!1}};export{g as captureFocus,s as createListNavigation,h as restoreFocus};
|
|
2
|
+
//# sourceMappingURL=focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.js","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, readonly string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nconst DEFAULT_KEYS_RTL: typeof DEFAULT_KEYS = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nconst DEFAULT_TYPEAHEAD_DELAY_MS = 500;\n\nconst read = <T>(value: MaybeGetter<T> | undefined, fallback: T): T =>\n typeof value === 'function' ? (value as () => T)() : (value ?? fallback);\n\nconst findForward = <T>(items: readonly T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let index = start; index < items.length; index++) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(\n items: readonly T[],\n start: number,\n predicate: (item: T, index: number) => boolean,\n): number => {\n for (let index = start; index >= 0; index--) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst resolveTypeaheadDelay = (delay: number | undefined): number =>\n typeof delay === 'number' && Number.isFinite(delay) && delay > 0 ? delay : DEFAULT_TYPEAHEAD_DELAY_MS;\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const disposalController = new AbortController();\n let index = -1;\n let disposed = false;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\n let removeExternalAbortListener: (() => void) | undefined;\n\n const isItemDisabled = (item: T, itemIndex: number): boolean => options.isItemDisabled?.(item, itemIndex) ?? false;\n\n const isUsableIndex = (items: readonly T[], itemIndex: number): boolean =>\n itemIndex >= 0 && itemIndex < items.length && !isItemDisabled(items[itemIndex], itemIndex);\n\n const normalizeIndex = (items: readonly T[]): number => {\n if (!isUsableIndex(items, index)) index = -1;\n\n return index;\n };\n\n const resetTypeahead = (): void => {\n typeaheadBuffer = '';\n lastTypeaheadAt = 0;\n };\n\n const commitIndex = (\n items: readonly T[],\n nextIndex: number,\n action: ListKeyAction,\n event?: KeyboardEvent,\n ): number => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return index;\n }\n\n index = nextIndex;\n options.onNavigate?.({ action, event, index: nextIndex, item: items[nextIndex] });\n\n return index;\n };\n\n const findEnabledIndex = (items: readonly T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward')\n return findForward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n\n return findBackward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n };\n\n const move = (items: readonly T[], direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n\n const nextIndex = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (nextIndex >= 0) return commitIndex(items, nextIndex, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(items, wrapped, action, event);\n }\n\n return index;\n };\n\n const navigateWithItems = (items: readonly T[], action: ListNavigationAction, event?: KeyboardEvent): number => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n if (action === 'first') {\n return commitIndex(items, findEnabledIndex(items, 0, 'forward'), action, event);\n }\n\n if (action === 'last') {\n return commitIndex(items, findEnabledIndex(items, items.length - 1, 'backward'), action, event);\n }\n\n return move(items, action === 'next' ? 'forward' : 'backward', event);\n };\n\n const findTypeaheadMatch = (items: readonly T[], search: string, includeCurrent: boolean): number => {\n const current = normalizeIndex(items);\n const start = current < 0 ? 0 : includeCurrent ? current : current + 1;\n\n for (let offset = 0; offset < items.length; offset++) {\n const itemIndex = (start + offset) % items.length;\n const item = items[itemIndex];\n\n if (isItemDisabled(item, itemIndex)) continue;\n\n if (options.typeahead?.getLabel(item, itemIndex).toLocaleLowerCase().startsWith(search)) {\n return itemIndex;\n }\n }\n\n return -1;\n };\n\n const handleTypeahead = (event: KeyboardEvent, items: readonly T[]): boolean => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return false;\n\n const now = Date.now();\n const key = event.key.toLocaleLowerCase();\n const delay = resolveTypeaheadDelay(options.typeahead.delayMs);\n\n if (now - lastTypeaheadAt >= delay) typeaheadBuffer = '';\n\n lastTypeaheadAt = now;\n\n const candidate = typeaheadBuffer + key;\n const repeatedCharacter = candidate.length > 1 && [...candidate].every((character) => character === key);\n typeaheadBuffer = repeatedCharacter ? key : candidate;\n\n let nextIndex = findTypeaheadMatch(items, typeaheadBuffer, typeaheadBuffer.length > 1);\n\n if (nextIndex < 0 && typeaheadBuffer.length > 1) {\n typeaheadBuffer = key;\n nextIndex = findTypeaheadMatch(items, typeaheadBuffer, false);\n }\n\n if (nextIndex < 0) return false;\n\n commitIndex(items, nextIndex, 'typeahead', event);\n\n return true;\n };\n\n const set = (nextIndex: number): number => {\n if (disposed) return -1;\n\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (nextIndex < 0 || !items.length) {\n index = -1;\n\n return index;\n }\n\n const clamped = Math.min(nextIndex, items.length - 1);\n\n if (!isUsableIndex(items, clamped)) {\n index = -1;\n\n return index;\n }\n\n index = clamped;\n\n return index;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\n if (disposed) return undefined;\n\n const items = options.getItems();\n const current = normalizeIndex(items);\n\n return current >= 0 ? items[current] : undefined;\n };\n\n const reset = (): void => {\n if (disposed) return;\n\n index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n if (disposed) return -1;\n\n return normalizeIndex(options.getItems());\n };\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = read(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[read(options.orientation, 'vertical')];\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n if ((keys?.[action] ?? defaults[action]).includes(eventKey)) return action;\n }\n\n return undefined;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || read(options.disabled, false)) return false;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n event.preventDefault();\n navigateWithItems(items, action, event);\n\n return true;\n }\n\n return handleTypeahead(event, items);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n index = -1;\n resetTypeahead();\n removeExternalAbortListener?.();\n removeExternalAbortListener = undefined;\n disposalController.abort();\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n removeExternalAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n","export type FocusableElement = HTMLElement | SVGElement;\nexport type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);\n\nexport type RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\nexport type CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\nexport type FocusRestorer = () => boolean;\n\nconst resolveTarget = (target: FocusTarget): FocusableElement | null | undefined => {\n return typeof target === 'function' ? target() : target;\n};\n\nconst getDeepActiveElement = (rootDocument: Document): Element | null => {\n let active: Element | null = rootDocument.activeElement;\n\n while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n\n return active;\n};\n\nconst isConnected = (target: FocusableElement): boolean => {\n // `isConnected` pierces shadow boundaries (shadow-including root is the document);\n // `ownerDocument.contains` does NOT — it would reject elements inside shadow roots.\n return target.isConnected;\n};\n\nconst isDisabled = (target: FocusableElement): boolean => {\n return target instanceof HTMLElement && 'disabled' in target && Boolean((target as HTMLInputElement).disabled);\n};\n\nconst isInert = (target: FocusableElement): boolean => {\n if (target.closest('[inert]')) return true;\n\n return target instanceof HTMLElement && target.inert;\n};\n\nconst canRestoreTo = (target: FocusableElement): boolean => {\n return isConnected(target) && !isDisabled(target) && !isInert(target);\n};\n\nconst focusAndVerify = (target: FocusableElement, preventScroll: boolean | undefined): boolean => {\n target.focus({ preventScroll });\n\n return getDeepActiveElement(target.ownerDocument) === target;\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const next = resolveTarget(target);\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n const fallback = resolveTarget(options.fallback);\n\n if (!fallback || !canRestoreTo(fallback)) return false;\n\n return focusAndVerify(fallback, options.preventScroll);\n};\n\nexport const captureFocus = (options: CaptureFocusOptions = {}): FocusRestorer => {\n let captured = getDeepActiveElement(document) as FocusableElement | null;\n let available = !options.signal?.aborted;\n\n if (!available) captured = null;\n\n const cancel = (): void => {\n available = false;\n captured = null;\n };\n\n if (!options.signal?.aborted) {\n options.signal?.addEventListener('abort', cancel, { once: true });\n }\n\n return () => {\n if (!available) return false;\n\n available = false;\n options.signal?.removeEventListener('abort', cancel);\n\n const target = captured;\n captured = null;\n\n if (target) {\n return restoreFocus(target, {\n fallback: options.fallback,\n preventScroll: options.preventScroll,\n });\n }\n\n if (options.fallback) {\n return restoreFocus(options.fallback, {\n preventScroll: options.preventScroll,\n });\n }\n\n return false;\n };\n};\n"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./list-navigation.cjs"),t=require("./restore-focus.cjs");exports.captureFocus=t.captureFocus,exports.createListNavigation=e.createListNavigation,exports.restoreFocus=t.restoreFocus;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ListKeyAction, ListNavigation, ListNavigationAction, ListNavigationChange, ListNavigationOptions, ListNavigationTypeaheadOptions, MaybeGetter, } from './list-navigation';
|
|
2
|
+
export { createListNavigation } from './list-navigation';
|
|
3
|
+
export type { CaptureFocusOptions, FocusRestorer, FocusTarget, RestoreFocusOptions, } from './restore-focus';
|
|
4
|
+
export { captureFocus, restoreFocus } from './restore-focus';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,8BAA8B,EAC9B,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowRight`],prev:[`ArrowLeft`]},vertical:{first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]}},t={both:{first:[`Home`],last:[`End`],next:[`ArrowDown`,`ArrowLeft`],prev:[`ArrowUp`,`ArrowRight`]},horizontal:{first:[`Home`],last:[`End`],next:[`ArrowLeft`],prev:[`ArrowRight`]},vertical:e.vertical},n=500,r=(e,t)=>typeof e==`function`?e():e??t,i=(e,t,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},a=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},o=e=>typeof e==`number`&&Number.isFinite(e)&&e>0?e:n,s=n=>{let s=new AbortController,c=-1,l=!1,u=``,d=0,f,p=(e,t)=>n.isItemDisabled?.(e,t)??!1,m=(e,t)=>t>=0&&t<e.length&&!p(e[t],t),h=e=>(m(e,c)||(c=-1),c),g=()=>{u=``,d=0},_=(e,t,r,i)=>m(e,t)?(c=t,n.onNavigate?.({action:r,event:i,index:t,item:e[t]}),c):(c=-1,c),v=(e,t,n)=>n===`forward`?i(e,t,(e,t)=>!p(e,t)):a(e,t,(e,t)=>!p(e,t)),y=(e,t,r)=>{let i=h(e);if(!e.length)return c=-1,c;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=v(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return _(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=v(e,n,t);if(i>=0)return _(e,i,s,r)}return c},b=(e,t,n)=>(g(),h(e),e.length?t===`first`?_(e,v(e,0,`forward`),t,n):t===`last`?_(e,v(e,e.length-1,`backward`),t,n):y(e,t===`next`?`forward`:`backward`,n):(c=-1,c)),x=(e,t,r)=>{let i=h(e),a=i<0?0:r?i:i+1;for(let r=0;r<e.length;r++){let i=(a+r)%e.length,o=e[i];if(!p(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},S=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return!1;let r=Date.now(),i=e.key.toLocaleLowerCase(),a=o(n.typeahead.delayMs);r-d>=a&&(u=``),d=r;let s=u+i;u=s.length>1&&[...s].every(e=>e===i)?i:s;let c=x(t,u,u.length>1);return c<0&&u.length>1&&(u=i,c=x(t,u,!1)),c<0?!1:(_(t,c,`typeahead`,e),!0)},C=e=>{if(l)return-1;let t=n.getItems();if(h(t),g(),e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return m(t,r)?(c=r,c):(c=-1,c)},w=e=>l?-1:b(n.getItems(),e),T=()=>{if(l)return;let e=n.getItems(),t=h(e);return t>=0?e[t]:void 0},E=()=>{l||(c=-1,g())},D=()=>l?-1:h(n.getItems()),O=i=>{let a=n.keys,o=(r(n.direction,`ltr`)===`rtl`?t:e)[r(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((a?.[e]??o[e]).includes(i))return e},k=e=>{if(l||r(n.disabled,!1))return!1;let t=n.getItems();h(t);let i=O(e.key);return i?(e.preventDefault(),b(t,i,e),!0):S(e,t)},A=()=>{l||(l=!0,c=-1,g(),f?.(),f=void 0,s.abort())};return n.signal?.aborted?A():n.signal&&(n.signal.addEventListener(`abort`,A,{once:!0}),f=()=>n.signal?.removeEventListener(`abort`,A)),{get disposalSignal(){return s.signal},dispose:A,get disposed(){return l},getActiveItem:T,getIndex:D,handleKeydown:k,navigate:w,reset:E,set:C,[Symbol.dispose]:A}};exports.createListNavigation=s;
|
|
2
|
+
//# sourceMappingURL=list-navigation.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-navigation.cjs","names":[],"sources":["../src/list-navigation.ts"],"sourcesContent":["export type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, readonly string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nconst DEFAULT_KEYS_RTL: typeof DEFAULT_KEYS = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nconst DEFAULT_TYPEAHEAD_DELAY_MS = 500;\n\nconst read = <T>(value: MaybeGetter<T> | undefined, fallback: T): T =>\n typeof value === 'function' ? (value as () => T)() : (value ?? fallback);\n\nconst findForward = <T>(items: readonly T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let index = start; index < items.length; index++) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(\n items: readonly T[],\n start: number,\n predicate: (item: T, index: number) => boolean,\n): number => {\n for (let index = start; index >= 0; index--) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst resolveTypeaheadDelay = (delay: number | undefined): number =>\n typeof delay === 'number' && Number.isFinite(delay) && delay > 0 ? delay : DEFAULT_TYPEAHEAD_DELAY_MS;\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const disposalController = new AbortController();\n let index = -1;\n let disposed = false;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\n let removeExternalAbortListener: (() => void) | undefined;\n\n const isItemDisabled = (item: T, itemIndex: number): boolean => options.isItemDisabled?.(item, itemIndex) ?? false;\n\n const isUsableIndex = (items: readonly T[], itemIndex: number): boolean =>\n itemIndex >= 0 && itemIndex < items.length && !isItemDisabled(items[itemIndex], itemIndex);\n\n const normalizeIndex = (items: readonly T[]): number => {\n if (!isUsableIndex(items, index)) index = -1;\n\n return index;\n };\n\n const resetTypeahead = (): void => {\n typeaheadBuffer = '';\n lastTypeaheadAt = 0;\n };\n\n const commitIndex = (\n items: readonly T[],\n nextIndex: number,\n action: ListKeyAction,\n event?: KeyboardEvent,\n ): number => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return index;\n }\n\n index = nextIndex;\n options.onNavigate?.({ action, event, index: nextIndex, item: items[nextIndex] });\n\n return index;\n };\n\n const findEnabledIndex = (items: readonly T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward')\n return findForward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n\n return findBackward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n };\n\n const move = (items: readonly T[], direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n\n const nextIndex = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (nextIndex >= 0) return commitIndex(items, nextIndex, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(items, wrapped, action, event);\n }\n\n return index;\n };\n\n const navigateWithItems = (items: readonly T[], action: ListNavigationAction, event?: KeyboardEvent): number => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n if (action === 'first') {\n return commitIndex(items, findEnabledIndex(items, 0, 'forward'), action, event);\n }\n\n if (action === 'last') {\n return commitIndex(items, findEnabledIndex(items, items.length - 1, 'backward'), action, event);\n }\n\n return move(items, action === 'next' ? 'forward' : 'backward', event);\n };\n\n const findTypeaheadMatch = (items: readonly T[], search: string, includeCurrent: boolean): number => {\n const current = normalizeIndex(items);\n const start = current < 0 ? 0 : includeCurrent ? current : current + 1;\n\n for (let offset = 0; offset < items.length; offset++) {\n const itemIndex = (start + offset) % items.length;\n const item = items[itemIndex];\n\n if (isItemDisabled(item, itemIndex)) continue;\n\n if (options.typeahead?.getLabel(item, itemIndex).toLocaleLowerCase().startsWith(search)) {\n return itemIndex;\n }\n }\n\n return -1;\n };\n\n const handleTypeahead = (event: KeyboardEvent, items: readonly T[]): boolean => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return false;\n\n const now = Date.now();\n const key = event.key.toLocaleLowerCase();\n const delay = resolveTypeaheadDelay(options.typeahead.delayMs);\n\n if (now - lastTypeaheadAt >= delay) typeaheadBuffer = '';\n\n lastTypeaheadAt = now;\n\n const candidate = typeaheadBuffer + key;\n const repeatedCharacter = candidate.length > 1 && [...candidate].every((character) => character === key);\n typeaheadBuffer = repeatedCharacter ? key : candidate;\n\n let nextIndex = findTypeaheadMatch(items, typeaheadBuffer, typeaheadBuffer.length > 1);\n\n if (nextIndex < 0 && typeaheadBuffer.length > 1) {\n typeaheadBuffer = key;\n nextIndex = findTypeaheadMatch(items, typeaheadBuffer, false);\n }\n\n if (nextIndex < 0) return false;\n\n commitIndex(items, nextIndex, 'typeahead', event);\n\n return true;\n };\n\n const set = (nextIndex: number): number => {\n if (disposed) return -1;\n\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (nextIndex < 0 || !items.length) {\n index = -1;\n\n return index;\n }\n\n const clamped = Math.min(nextIndex, items.length - 1);\n\n if (!isUsableIndex(items, clamped)) {\n index = -1;\n\n return index;\n }\n\n index = clamped;\n\n return index;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\n if (disposed) return undefined;\n\n const items = options.getItems();\n const current = normalizeIndex(items);\n\n return current >= 0 ? items[current] : undefined;\n };\n\n const reset = (): void => {\n if (disposed) return;\n\n index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n if (disposed) return -1;\n\n return normalizeIndex(options.getItems());\n };\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = read(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[read(options.orientation, 'vertical')];\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n if ((keys?.[action] ?? defaults[action]).includes(eventKey)) return action;\n }\n\n return undefined;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || read(options.disabled, false)) return false;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n event.preventDefault();\n navigateWithItems(items, action, event);\n\n return true;\n }\n\n return handleTypeahead(event, items);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n index = -1;\n resetTypeahead();\n removeExternalAbortListener?.();\n removeExternalAbortListener = undefined;\n disposalController.abort();\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n removeExternalAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type MaybeGetter<T> = T | (() => T);
|
|
2
|
+
export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';
|
|
3
|
+
export type ListKeyAction = ListNavigationAction | 'typeahead';
|
|
4
|
+
export type ListNavigationChange<T> = {
|
|
5
|
+
action: ListKeyAction;
|
|
6
|
+
event?: KeyboardEvent;
|
|
7
|
+
index: number;
|
|
8
|
+
item: T;
|
|
9
|
+
};
|
|
10
|
+
export type ListNavigationTypeaheadOptions<T> = {
|
|
11
|
+
delayMs?: number;
|
|
12
|
+
getLabel: (item: T, index: number) => string;
|
|
13
|
+
};
|
|
14
|
+
export type ListNavigationOptions<T> = {
|
|
15
|
+
direction?: MaybeGetter<'ltr' | 'rtl'>;
|
|
16
|
+
disabled?: MaybeGetter<boolean | undefined>;
|
|
17
|
+
getItems: () => readonly T[];
|
|
18
|
+
isItemDisabled?: (item: T, index: number) => boolean;
|
|
19
|
+
keys?: Partial<Record<ListNavigationAction, readonly string[]>>;
|
|
20
|
+
loop?: boolean;
|
|
21
|
+
onNavigate?: (change: ListNavigationChange<T>) => void;
|
|
22
|
+
orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
typeahead?: ListNavigationTypeaheadOptions<T>;
|
|
25
|
+
};
|
|
26
|
+
export type ListNavigation<T> = {
|
|
27
|
+
[Symbol.dispose](): void;
|
|
28
|
+
readonly disposalSignal: AbortSignal;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
readonly disposed: boolean;
|
|
31
|
+
getActiveItem(): T | undefined;
|
|
32
|
+
getIndex(): number;
|
|
33
|
+
handleKeydown(event: KeyboardEvent): boolean;
|
|
34
|
+
navigate(action: ListNavigationAction): number;
|
|
35
|
+
reset(): void;
|
|
36
|
+
set(index: number): number;
|
|
37
|
+
};
|
|
38
|
+
export declare const createListNavigation: <T>(options: ListNavigationOptions<T>) => ListNavigation<T>;
|
|
39
|
+
//# sourceMappingURL=list-navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-navigation.d.ts","sourceRoot":"","sources":["../src/list-navigation.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAE3C,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,WAAW,CAAC;AAE/D,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI;IACpC,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,CAAC;CACT,CAAC;AAEF,MAAM,MAAM,8BAA8B,CAAC,CAAC,IAAI;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,WAAW,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,SAAS,CAAC,EAAE,CAAC;IAC7B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACvD,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;IAC9D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,QAAQ,IAAI,MAAM,CAAC;IACnB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,KAAK,IAAI,IAAI,CAAC;IACd,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AA0CF,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,cAAc,CAAC,CAAC,CA4Q3F,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/list-navigation.ts
|
|
2
|
+
var e = {
|
|
3
|
+
both: {
|
|
4
|
+
first: ["Home"],
|
|
5
|
+
last: ["End"],
|
|
6
|
+
next: ["ArrowDown", "ArrowRight"],
|
|
7
|
+
prev: ["ArrowUp", "ArrowLeft"]
|
|
8
|
+
},
|
|
9
|
+
horizontal: {
|
|
10
|
+
first: ["Home"],
|
|
11
|
+
last: ["End"],
|
|
12
|
+
next: ["ArrowRight"],
|
|
13
|
+
prev: ["ArrowLeft"]
|
|
14
|
+
},
|
|
15
|
+
vertical: {
|
|
16
|
+
first: ["Home"],
|
|
17
|
+
last: ["End"],
|
|
18
|
+
next: ["ArrowDown"],
|
|
19
|
+
prev: ["ArrowUp"]
|
|
20
|
+
}
|
|
21
|
+
}, t = {
|
|
22
|
+
both: {
|
|
23
|
+
first: ["Home"],
|
|
24
|
+
last: ["End"],
|
|
25
|
+
next: ["ArrowDown", "ArrowLeft"],
|
|
26
|
+
prev: ["ArrowUp", "ArrowRight"]
|
|
27
|
+
},
|
|
28
|
+
horizontal: {
|
|
29
|
+
first: ["Home"],
|
|
30
|
+
last: ["End"],
|
|
31
|
+
next: ["ArrowLeft"],
|
|
32
|
+
prev: ["ArrowRight"]
|
|
33
|
+
},
|
|
34
|
+
vertical: e.vertical
|
|
35
|
+
}, n = 500, r = (e, t) => typeof e == "function" ? e() : e ?? t, i = (e, t, n) => {
|
|
36
|
+
for (let r = t; r < e.length; r++) if (n(e[r], r)) return r;
|
|
37
|
+
return -1;
|
|
38
|
+
}, a = (e, t, n) => {
|
|
39
|
+
for (let r = t; r >= 0; r--) if (n(e[r], r)) return r;
|
|
40
|
+
return -1;
|
|
41
|
+
}, o = (e) => typeof e == "number" && Number.isFinite(e) && e > 0 ? e : n, s = (n) => {
|
|
42
|
+
let s = new AbortController(), c = -1, l = !1, u = "", d = 0, f, p = (e, t) => n.isItemDisabled?.(e, t) ?? !1, m = (e, t) => t >= 0 && t < e.length && !p(e[t], t), h = (e) => (m(e, c) || (c = -1), c), g = () => {
|
|
43
|
+
u = "", d = 0;
|
|
44
|
+
}, _ = (e, t, r, i) => m(e, t) ? (c = t, n.onNavigate?.({
|
|
45
|
+
action: r,
|
|
46
|
+
event: i,
|
|
47
|
+
index: t,
|
|
48
|
+
item: e[t]
|
|
49
|
+
}), c) : (c = -1, c), v = (e, t, n) => n === "forward" ? i(e, t, (e, t) => !p(e, t)) : a(e, t, (e, t) => !p(e, t)), y = (e, t, r) => {
|
|
50
|
+
let i = h(e);
|
|
51
|
+
if (!e.length) return c = -1, c;
|
|
52
|
+
let a = i < 0 ? t === "forward" ? 0 : e.length - 1 : t === "forward" ? i + 1 : i - 1, o = v(e, a, t), s = t === "forward" ? "next" : "prev";
|
|
53
|
+
if (o >= 0) return _(e, o, s, r);
|
|
54
|
+
if (n.loop) {
|
|
55
|
+
let n = t === "forward" ? 0 : e.length - 1, i = v(e, n, t);
|
|
56
|
+
if (i >= 0) return _(e, i, s, r);
|
|
57
|
+
}
|
|
58
|
+
return c;
|
|
59
|
+
}, b = (e, t, n) => (g(), h(e), e.length ? t === "first" ? _(e, v(e, 0, "forward"), t, n) : t === "last" ? _(e, v(e, e.length - 1, "backward"), t, n) : y(e, t === "next" ? "forward" : "backward", n) : (c = -1, c)), x = (e, t, r) => {
|
|
60
|
+
let i = h(e), a = i < 0 ? 0 : r ? i : i + 1;
|
|
61
|
+
for (let r = 0; r < e.length; r++) {
|
|
62
|
+
let i = (a + r) % e.length, o = e[i];
|
|
63
|
+
if (!p(o, i) && n.typeahead?.getLabel(o, i).toLocaleLowerCase().startsWith(t)) return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}, S = (e, t) => {
|
|
67
|
+
if (!n.typeahead || e.key.length !== 1 || e.ctrlKey || e.altKey || e.metaKey) return !1;
|
|
68
|
+
let r = Date.now(), i = e.key.toLocaleLowerCase(), a = o(n.typeahead.delayMs);
|
|
69
|
+
r - d >= a && (u = ""), d = r;
|
|
70
|
+
let s = u + i;
|
|
71
|
+
u = s.length > 1 && [...s].every((e) => e === i) ? i : s;
|
|
72
|
+
let c = x(t, u, u.length > 1);
|
|
73
|
+
return c < 0 && u.length > 1 && (u = i, c = x(t, u, !1)), c < 0 ? !1 : (_(t, c, "typeahead", e), !0);
|
|
74
|
+
}, C = (e) => {
|
|
75
|
+
if (l) return -1;
|
|
76
|
+
let t = n.getItems();
|
|
77
|
+
if (h(t), g(), e < 0 || !t.length) return c = -1, c;
|
|
78
|
+
let r = Math.min(e, t.length - 1);
|
|
79
|
+
return m(t, r) ? (c = r, c) : (c = -1, c);
|
|
80
|
+
}, w = (e) => l ? -1 : b(n.getItems(), e), T = () => {
|
|
81
|
+
if (l) return;
|
|
82
|
+
let e = n.getItems(), t = h(e);
|
|
83
|
+
return t >= 0 ? e[t] : void 0;
|
|
84
|
+
}, E = () => {
|
|
85
|
+
l || (c = -1, g());
|
|
86
|
+
}, D = () => l ? -1 : h(n.getItems()), O = (i) => {
|
|
87
|
+
let a = n.keys, o = (r(n.direction, "ltr") === "rtl" ? t : e)[r(n.orientation, "vertical")];
|
|
88
|
+
for (let e of [
|
|
89
|
+
"next",
|
|
90
|
+
"prev",
|
|
91
|
+
"first",
|
|
92
|
+
"last"
|
|
93
|
+
]) if ((a?.[e] ?? o[e]).includes(i)) return e;
|
|
94
|
+
}, k = (e) => {
|
|
95
|
+
if (l || r(n.disabled, !1)) return !1;
|
|
96
|
+
let t = n.getItems();
|
|
97
|
+
h(t);
|
|
98
|
+
let i = O(e.key);
|
|
99
|
+
return i ? (e.preventDefault(), b(t, i, e), !0) : S(e, t);
|
|
100
|
+
}, A = () => {
|
|
101
|
+
l || (l = !0, c = -1, g(), f?.(), f = void 0, s.abort());
|
|
102
|
+
};
|
|
103
|
+
return n.signal?.aborted ? A() : n.signal && (n.signal.addEventListener("abort", A, { once: !0 }), f = () => n.signal?.removeEventListener("abort", A)), {
|
|
104
|
+
get disposalSignal() {
|
|
105
|
+
return s.signal;
|
|
106
|
+
},
|
|
107
|
+
dispose: A,
|
|
108
|
+
get disposed() {
|
|
109
|
+
return l;
|
|
110
|
+
},
|
|
111
|
+
getActiveItem: T,
|
|
112
|
+
getIndex: D,
|
|
113
|
+
handleKeydown: k,
|
|
114
|
+
navigate: w,
|
|
115
|
+
reset: E,
|
|
116
|
+
set: C,
|
|
117
|
+
[Symbol.dispose]: A
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
export { s as createListNavigation };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=list-navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-navigation.js","names":[],"sources":["../src/list-navigation.ts"],"sourcesContent":["export type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n};\n\nconst DEFAULT_KEYS: Record<'both' | 'horizontal' | 'vertical', Record<ListNavigationAction, readonly string[]>> = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowRight'], prev: ['ArrowLeft'] },\n vertical: { first: ['Home'], last: ['End'], next: ['ArrowDown'], prev: ['ArrowUp'] },\n};\n\nconst DEFAULT_KEYS_RTL: typeof DEFAULT_KEYS = {\n both: { first: ['Home'], last: ['End'], next: ['ArrowDown', 'ArrowLeft'], prev: ['ArrowUp', 'ArrowRight'] },\n horizontal: { first: ['Home'], last: ['End'], next: ['ArrowLeft'], prev: ['ArrowRight'] },\n vertical: DEFAULT_KEYS.vertical,\n};\n\nconst DEFAULT_TYPEAHEAD_DELAY_MS = 500;\n\nconst read = <T>(value: MaybeGetter<T> | undefined, fallback: T): T =>\n typeof value === 'function' ? (value as () => T)() : (value ?? fallback);\n\nconst findForward = <T>(items: readonly T[], start: number, predicate: (item: T, index: number) => boolean): number => {\n for (let index = start; index < items.length; index++) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst findBackward = <T>(\n items: readonly T[],\n start: number,\n predicate: (item: T, index: number) => boolean,\n): number => {\n for (let index = start; index >= 0; index--) {\n if (predicate(items[index], index)) return index;\n }\n\n return -1;\n};\n\nconst resolveTypeaheadDelay = (delay: number | undefined): number =>\n typeof delay === 'number' && Number.isFinite(delay) && delay > 0 ? delay : DEFAULT_TYPEAHEAD_DELAY_MS;\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const disposalController = new AbortController();\n let index = -1;\n let disposed = false;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\n let removeExternalAbortListener: (() => void) | undefined;\n\n const isItemDisabled = (item: T, itemIndex: number): boolean => options.isItemDisabled?.(item, itemIndex) ?? false;\n\n const isUsableIndex = (items: readonly T[], itemIndex: number): boolean =>\n itemIndex >= 0 && itemIndex < items.length && !isItemDisabled(items[itemIndex], itemIndex);\n\n const normalizeIndex = (items: readonly T[]): number => {\n if (!isUsableIndex(items, index)) index = -1;\n\n return index;\n };\n\n const resetTypeahead = (): void => {\n typeaheadBuffer = '';\n lastTypeaheadAt = 0;\n };\n\n const commitIndex = (\n items: readonly T[],\n nextIndex: number,\n action: ListKeyAction,\n event?: KeyboardEvent,\n ): number => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return index;\n }\n\n index = nextIndex;\n options.onNavigate?.({ action, event, index: nextIndex, item: items[nextIndex] });\n\n return index;\n };\n\n const findEnabledIndex = (items: readonly T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward')\n return findForward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n\n return findBackward(items, start, (item, itemIndex) => !isItemDisabled(item, itemIndex));\n };\n\n const move = (items: readonly T[], direction: 'forward' | 'backward', event?: KeyboardEvent): number => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n\n const nextIndex = findEnabledIndex(items, start, direction);\n const action: ListNavigationAction = direction === 'forward' ? 'next' : 'prev';\n\n if (nextIndex >= 0) return commitIndex(items, nextIndex, action, event);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(items, wrapped, action, event);\n }\n\n return index;\n };\n\n const navigateWithItems = (items: readonly T[], action: ListNavigationAction, event?: KeyboardEvent): number => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return index;\n }\n\n if (action === 'first') {\n return commitIndex(items, findEnabledIndex(items, 0, 'forward'), action, event);\n }\n\n if (action === 'last') {\n return commitIndex(items, findEnabledIndex(items, items.length - 1, 'backward'), action, event);\n }\n\n return move(items, action === 'next' ? 'forward' : 'backward', event);\n };\n\n const findTypeaheadMatch = (items: readonly T[], search: string, includeCurrent: boolean): number => {\n const current = normalizeIndex(items);\n const start = current < 0 ? 0 : includeCurrent ? current : current + 1;\n\n for (let offset = 0; offset < items.length; offset++) {\n const itemIndex = (start + offset) % items.length;\n const item = items[itemIndex];\n\n if (isItemDisabled(item, itemIndex)) continue;\n\n if (options.typeahead?.getLabel(item, itemIndex).toLocaleLowerCase().startsWith(search)) {\n return itemIndex;\n }\n }\n\n return -1;\n };\n\n const handleTypeahead = (event: KeyboardEvent, items: readonly T[]): boolean => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return false;\n\n const now = Date.now();\n const key = event.key.toLocaleLowerCase();\n const delay = resolveTypeaheadDelay(options.typeahead.delayMs);\n\n if (now - lastTypeaheadAt >= delay) typeaheadBuffer = '';\n\n lastTypeaheadAt = now;\n\n const candidate = typeaheadBuffer + key;\n const repeatedCharacter = candidate.length > 1 && [...candidate].every((character) => character === key);\n typeaheadBuffer = repeatedCharacter ? key : candidate;\n\n let nextIndex = findTypeaheadMatch(items, typeaheadBuffer, typeaheadBuffer.length > 1);\n\n if (nextIndex < 0 && typeaheadBuffer.length > 1) {\n typeaheadBuffer = key;\n nextIndex = findTypeaheadMatch(items, typeaheadBuffer, false);\n }\n\n if (nextIndex < 0) return false;\n\n commitIndex(items, nextIndex, 'typeahead', event);\n\n return true;\n };\n\n const set = (nextIndex: number): number => {\n if (disposed) return -1;\n\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (nextIndex < 0 || !items.length) {\n index = -1;\n\n return index;\n }\n\n const clamped = Math.min(nextIndex, items.length - 1);\n\n if (!isUsableIndex(items, clamped)) {\n index = -1;\n\n return index;\n }\n\n index = clamped;\n\n return index;\n };\n\n const navigate = (action: ListNavigationAction): number => {\n if (disposed) return -1;\n\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\n if (disposed) return undefined;\n\n const items = options.getItems();\n const current = normalizeIndex(items);\n\n return current >= 0 ? items[current] : undefined;\n };\n\n const reset = (): void => {\n if (disposed) return;\n\n index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n if (disposed) return -1;\n\n return normalizeIndex(options.getItems());\n };\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = read(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[read(options.orientation, 'vertical')];\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n if ((keys?.[action] ?? defaults[action]).includes(eventKey)) return action;\n }\n\n return undefined;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n if (disposed || read(options.disabled, false)) return false;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n event.preventDefault();\n navigateWithItems(items, action, event);\n\n return true;\n }\n\n return handleTypeahead(event, items);\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n index = -1;\n resetTypeahead();\n removeExternalAbortListener?.();\n removeExternalAbortListener = undefined;\n disposalController.abort();\n };\n\n if (options.signal?.aborted) {\n dispose();\n } else if (options.signal) {\n options.signal.addEventListener('abort', dispose, { once: true });\n removeExternalAbortListener = () => options.signal?.removeEventListener('abort', dispose);\n }\n\n return {\n get disposalSignal() {\n return disposalController.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n [Symbol.dispose]: dispose,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=e=>typeof e==`function`?e():e,t=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},n=e=>e.isConnected,r=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,i=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,a=e=>n(e)&&!r(e)&&!i(e),o=(e,n)=>(e.focus({preventScroll:n}),t(e.ownerDocument)===e),s=(t,n={})=>{let r=e(t);if(r&&a(r)&&o(r,n.preventScroll))return!0;if(!n.fallback)return!1;let i=e(n.fallback);return!i||!a(i)?!1:o(i,n.preventScroll)},c=(e={})=>{let n=t(document),r=!e.signal?.aborted;r||(n=null);let i=()=>{r=!1,n=null};return e.signal?.aborted||e.signal?.addEventListener(`abort`,i,{once:!0}),()=>{if(!r)return!1;r=!1,e.signal?.removeEventListener(`abort`,i);let t=n;return n=null,t?s(t,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?s(e.fallback,{preventScroll:e.preventScroll}):!1}};exports.captureFocus=c,exports.restoreFocus=s;
|
|
2
|
+
//# sourceMappingURL=restore-focus.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-focus.cjs","names":[],"sources":["../src/restore-focus.ts"],"sourcesContent":["export type FocusableElement = HTMLElement | SVGElement;\nexport type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);\n\nexport type RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\nexport type CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\nexport type FocusRestorer = () => boolean;\n\nconst resolveTarget = (target: FocusTarget): FocusableElement | null | undefined => {\n return typeof target === 'function' ? target() : target;\n};\n\nconst getDeepActiveElement = (rootDocument: Document): Element | null => {\n let active: Element | null = rootDocument.activeElement;\n\n while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n\n return active;\n};\n\nconst isConnected = (target: FocusableElement): boolean => {\n // `isConnected` pierces shadow boundaries (shadow-including root is the document);\n // `ownerDocument.contains` does NOT — it would reject elements inside shadow roots.\n return target.isConnected;\n};\n\nconst isDisabled = (target: FocusableElement): boolean => {\n return target instanceof HTMLElement && 'disabled' in target && Boolean((target as HTMLInputElement).disabled);\n};\n\nconst isInert = (target: FocusableElement): boolean => {\n if (target.closest('[inert]')) return true;\n\n return target instanceof HTMLElement && target.inert;\n};\n\nconst canRestoreTo = (target: FocusableElement): boolean => {\n return isConnected(target) && !isDisabled(target) && !isInert(target);\n};\n\nconst focusAndVerify = (target: FocusableElement, preventScroll: boolean | undefined): boolean => {\n target.focus({ preventScroll });\n\n return getDeepActiveElement(target.ownerDocument) === target;\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const next = resolveTarget(target);\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n const fallback = resolveTarget(options.fallback);\n\n if (!fallback || !canRestoreTo(fallback)) return false;\n\n return focusAndVerify(fallback, options.preventScroll);\n};\n\nexport const captureFocus = (options: CaptureFocusOptions = {}): FocusRestorer => {\n let captured = getDeepActiveElement(document) as FocusableElement | null;\n let available = !options.signal?.aborted;\n\n if (!available) captured = null;\n\n const cancel = (): void => {\n available = false;\n captured = null;\n };\n\n if (!options.signal?.aborted) {\n options.signal?.addEventListener('abort', cancel, { once: true });\n }\n\n return () => {\n if (!available) return false;\n\n available = false;\n options.signal?.removeEventListener('abort', cancel);\n\n const target = captured;\n captured = null;\n\n if (target) {\n return restoreFocus(target, {\n fallback: options.fallback,\n preventScroll: options.preventScroll,\n });\n }\n\n if (options.fallback) {\n return restoreFocus(options.fallback, {\n preventScroll: options.preventScroll,\n });\n }\n\n return false;\n };\n};\n"],"mappings":"AAcA,IAAM,EAAiB,GACd,OAAO,GAAW,WAAa,EAAO,EAAI,EAG7C,EAAwB,GAA2C,CACvE,IAAI,EAAyB,EAAa,cAE1C,KAAO,aAAkB,aAAe,EAAO,YAAY,eACzD,EAAS,EAAO,WAAW,cAG7B,OAAO,CACT,EAEM,EAAe,GAGZ,EAAO,YAGV,EAAc,GACX,aAAkB,aAAe,aAAc,GAAU,EAAS,EAA4B,SAGjG,EAAW,GACX,EAAO,QAAQ,SAAS,EAAU,GAE/B,aAAkB,aAAe,EAAO,MAG3C,EAAgB,GACb,EAAY,CAAM,GAAK,CAAC,EAAW,CAAM,GAAK,CAAC,EAAQ,CAAM,EAGhE,GAAkB,EAA0B,KAChD,EAAO,MAAM,CAAE,eAAc,CAAC,EAEvB,EAAqB,EAAO,aAAa,IAAM,GAG3C,GAAgB,EAAqB,EAA+B,CAAC,IAAe,CAC/F,IAAM,EAAO,EAAc,CAAM,EAEjC,GAAI,GAAQ,EAAa,CAAI,GAAK,EAAe,EAAM,EAAQ,aAAa,EAC1E,MAAO,GAGT,GAAI,CAAC,EAAQ,SAAU,MAAO,GAE9B,IAAM,EAAW,EAAc,EAAQ,QAAQ,EAI/C,MAFI,CAAC,GAAY,CAAC,EAAa,CAAQ,EAAU,GAE1C,EAAe,EAAU,EAAQ,aAAa,CACvD,EAEa,GAAgB,EAA+B,CAAC,IAAqB,CAChF,IAAI,EAAW,EAAqB,QAAQ,EACxC,EAAY,CAAC,EAAQ,QAAQ,QAE5B,IAAW,EAAW,MAE3B,IAAM,MAAqB,CACzB,EAAY,GACZ,EAAW,IACb,EAMA,OAJK,EAAQ,QAAQ,SACnB,EAAQ,QAAQ,iBAAiB,QAAS,EAAQ,CAAE,KAAM,EAAK,CAAC,MAGrD,CACX,GAAI,CAAC,EAAW,MAAO,GAEvB,EAAY,GACZ,EAAQ,QAAQ,oBAAoB,QAAS,CAAM,EAEnD,IAAM,EAAS,EAgBf,MAfA,GAAW,KAEP,EACK,EAAa,EAAQ,CAC1B,SAAU,EAAQ,SAClB,cAAe,EAAQ,aACzB,CAAC,EAGC,EAAQ,SACH,EAAa,EAAQ,SAAU,CACpC,cAAe,EAAQ,aACzB,CAAC,EAGI,EACT,CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type FocusableElement = HTMLElement | SVGElement;
|
|
2
|
+
export type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);
|
|
3
|
+
export type RestoreFocusOptions = {
|
|
4
|
+
fallback?: FocusTarget;
|
|
5
|
+
preventScroll?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CaptureFocusOptions = RestoreFocusOptions & {
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
};
|
|
10
|
+
export type FocusRestorer = () => boolean;
|
|
11
|
+
export declare const restoreFocus: (target: FocusTarget, options?: RestoreFocusOptions) => boolean;
|
|
12
|
+
export declare const captureFocus: (options?: CaptureFocusOptions) => FocusRestorer;
|
|
13
|
+
//# sourceMappingURL=restore-focus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-focus.d.ts","sourceRoot":"","sources":["../src/restore-focus.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,UAAU,CAAC;AACxD,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACtD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC;AA0C1C,eAAO,MAAM,YAAY,GAAI,QAAQ,WAAW,EAAE,UAAS,mBAAwB,KAAG,OAcrF,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,UAAS,mBAAwB,KAAG,aAuChE,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/restore-focus.ts
|
|
2
|
+
var e = (e) => typeof e == "function" ? e() : e, t = (e) => {
|
|
3
|
+
let t = e.activeElement;
|
|
4
|
+
for (; t instanceof HTMLElement && t.shadowRoot?.activeElement;) t = t.shadowRoot.activeElement;
|
|
5
|
+
return t;
|
|
6
|
+
}, n = (e) => e.isConnected, r = (e) => e instanceof HTMLElement && "disabled" in e && !!e.disabled, i = (e) => e.closest("[inert]") ? !0 : e instanceof HTMLElement && e.inert, a = (e) => n(e) && !r(e) && !i(e), o = (e, n) => (e.focus({ preventScroll: n }), t(e.ownerDocument) === e), s = (t, n = {}) => {
|
|
7
|
+
let r = e(t);
|
|
8
|
+
if (r && a(r) && o(r, n.preventScroll)) return !0;
|
|
9
|
+
if (!n.fallback) return !1;
|
|
10
|
+
let i = e(n.fallback);
|
|
11
|
+
return !i || !a(i) ? !1 : o(i, n.preventScroll);
|
|
12
|
+
}, c = (e = {}) => {
|
|
13
|
+
let n = t(document), r = !e.signal?.aborted;
|
|
14
|
+
r || (n = null);
|
|
15
|
+
let i = () => {
|
|
16
|
+
r = !1, n = null;
|
|
17
|
+
};
|
|
18
|
+
return e.signal?.aborted || e.signal?.addEventListener("abort", i, { once: !0 }), () => {
|
|
19
|
+
if (!r) return !1;
|
|
20
|
+
r = !1, e.signal?.removeEventListener("abort", i);
|
|
21
|
+
let t = n;
|
|
22
|
+
return n = null, t ? s(t, {
|
|
23
|
+
fallback: e.fallback,
|
|
24
|
+
preventScroll: e.preventScroll
|
|
25
|
+
}) : e.fallback ? s(e.fallback, { preventScroll: e.preventScroll }) : !1;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { c as captureFocus, s as restoreFocus };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=restore-focus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-focus.js","names":[],"sources":["../src/restore-focus.ts"],"sourcesContent":["export type FocusableElement = HTMLElement | SVGElement;\nexport type FocusTarget = FocusableElement | null | undefined | (() => FocusableElement | null | undefined);\n\nexport type RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\nexport type CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\nexport type FocusRestorer = () => boolean;\n\nconst resolveTarget = (target: FocusTarget): FocusableElement | null | undefined => {\n return typeof target === 'function' ? target() : target;\n};\n\nconst getDeepActiveElement = (rootDocument: Document): Element | null => {\n let active: Element | null = rootDocument.activeElement;\n\n while (active instanceof HTMLElement && active.shadowRoot?.activeElement) {\n active = active.shadowRoot.activeElement;\n }\n\n return active;\n};\n\nconst isConnected = (target: FocusableElement): boolean => {\n // `isConnected` pierces shadow boundaries (shadow-including root is the document);\n // `ownerDocument.contains` does NOT — it would reject elements inside shadow roots.\n return target.isConnected;\n};\n\nconst isDisabled = (target: FocusableElement): boolean => {\n return target instanceof HTMLElement && 'disabled' in target && Boolean((target as HTMLInputElement).disabled);\n};\n\nconst isInert = (target: FocusableElement): boolean => {\n if (target.closest('[inert]')) return true;\n\n return target instanceof HTMLElement && target.inert;\n};\n\nconst canRestoreTo = (target: FocusableElement): boolean => {\n return isConnected(target) && !isDisabled(target) && !isInert(target);\n};\n\nconst focusAndVerify = (target: FocusableElement, preventScroll: boolean | undefined): boolean => {\n target.focus({ preventScroll });\n\n return getDeepActiveElement(target.ownerDocument) === target;\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const next = resolveTarget(target);\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n const fallback = resolveTarget(options.fallback);\n\n if (!fallback || !canRestoreTo(fallback)) return false;\n\n return focusAndVerify(fallback, options.preventScroll);\n};\n\nexport const captureFocus = (options: CaptureFocusOptions = {}): FocusRestorer => {\n let captured = getDeepActiveElement(document) as FocusableElement | null;\n let available = !options.signal?.aborted;\n\n if (!available) captured = null;\n\n const cancel = (): void => {\n available = false;\n captured = null;\n };\n\n if (!options.signal?.aborted) {\n options.signal?.addEventListener('abort', cancel, { once: true });\n }\n\n return () => {\n if (!available) return false;\n\n available = false;\n options.signal?.removeEventListener('abort', cancel);\n\n const target = captured;\n captured = null;\n\n if (target) {\n return restoreFocus(target, {\n fallback: options.fallback,\n preventScroll: options.preventScroll,\n });\n }\n\n if (options.fallback) {\n return restoreFocus(options.fallback, {\n preventScroll: options.preventScroll,\n });\n }\n\n return false;\n };\n};\n"],"mappings":";AAcA,IAAM,KAAiB,MACd,OAAO,KAAW,aAAa,EAAO,IAAI,GAG7C,KAAwB,MAA2C;CACvE,IAAI,IAAyB,EAAa;CAE1C,OAAO,aAAkB,eAAe,EAAO,YAAY,gBACzD,IAAS,EAAO,WAAW;CAG7B,OAAO;AACT,GAEM,KAAe,MAGZ,EAAO,aAGV,KAAc,MACX,aAAkB,eAAe,cAAc,KAAU,EAAS,EAA4B,UAGjG,KAAW,MACX,EAAO,QAAQ,SAAS,IAAU,KAE/B,aAAkB,eAAe,EAAO,OAG3C,KAAgB,MACb,EAAY,CAAM,KAAK,CAAC,EAAW,CAAM,KAAK,CAAC,EAAQ,CAAM,GAGhE,KAAkB,GAA0B,OAChD,EAAO,MAAM,EAAE,iBAAc,CAAC,GAEvB,EAAqB,EAAO,aAAa,MAAM,IAG3C,KAAgB,GAAqB,IAA+B,CAAC,MAAe;CAC/F,IAAM,IAAO,EAAc,CAAM;CAEjC,IAAI,KAAQ,EAAa,CAAI,KAAK,EAAe,GAAM,EAAQ,aAAa,GAC1E,OAAO;CAGT,IAAI,CAAC,EAAQ,UAAU,OAAO;CAE9B,IAAM,IAAW,EAAc,EAAQ,QAAQ;CAI/C,OAFI,CAAC,KAAY,CAAC,EAAa,CAAQ,IAAU,KAE1C,EAAe,GAAU,EAAQ,aAAa;AACvD,GAEa,KAAgB,IAA+B,CAAC,MAAqB;CAChF,IAAI,IAAW,EAAqB,QAAQ,GACxC,IAAY,CAAC,EAAQ,QAAQ;CAEjC,AAAK,MAAW,IAAW;CAE3B,IAAM,UAAqB;EAEzB,AADA,IAAY,IACZ,IAAW;CACb;CAMA,OAJK,EAAQ,QAAQ,WACnB,EAAQ,QAAQ,iBAAiB,SAAS,GAAQ,EAAE,MAAM,GAAK,CAAC,SAGrD;EACX,IAAI,CAAC,GAAW,OAAO;EAGvB,AADA,IAAY,IACZ,EAAQ,QAAQ,oBAAoB,SAAS,CAAM;EAEnD,IAAM,IAAS;EAgBf,OAfA,IAAW,MAEP,IACK,EAAa,GAAQ;GAC1B,UAAU,EAAQ;GAClB,eAAe,EAAQ;EACzB,CAAC,IAGC,EAAQ,WACH,EAAa,EAAQ,UAAU,EACpC,eAAe,EAAQ,cACzB,CAAC,IAGI;CACT;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielzeug/focus",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Framework-neutral list navigation and focus restoration primitives",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/helmuthdu/vielzeug",
|
|
8
|
+
"directory": "packages/focus"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs",
|
|
24
|
+
"types": "./dist/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "vite build && pnpm run build:bundle && pnpm run build:types",
|
|
33
|
+
"build:bundle": "vite build --config vite.bundle.config.ts",
|
|
34
|
+
"build:types": "tsc -p tsconfig.declarations.json",
|
|
35
|
+
"fix": "biome check --write src",
|
|
36
|
+
"lint": "biome ci src",
|
|
37
|
+
"prepublishOnly": "pnpm run build",
|
|
38
|
+
"test": "vitest"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^26.2.0",
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"vite": "^8.2.1",
|
|
44
|
+
"vitest": "^4.1.10"
|
|
45
|
+
}
|
|
46
|
+
}
|