@vielzeug/focus 2.1.0 → 3.0.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 +21 -8
- package/dist/focus.cjs +1 -1
- package/dist/focus.cjs.map +1 -1
- package/dist/focus.iife.js +1 -1
- package/dist/focus.iife.js.map +1 -1
- package/dist/focus.js +1 -1
- package/dist/focus.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/list-navigation.cjs +1 -1
- package/dist/list-navigation.cjs.map +1 -1
- package/dist/list-navigation.d.ts +13 -14
- package/dist/list-navigation.d.ts.map +1 -1
- package/dist/list-navigation.js +76 -62
- package/dist/list-navigation.js.map +1 -1
- package/dist/restore-focus.cjs +1 -1
- package/dist/restore-focus.cjs.map +1 -1
- package/dist/restore-focus.d.ts.map +1 -1
- package/dist/restore-focus.js +19 -3
- package/dist/restore-focus.js.map +1 -1
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @vielzeug/focus
|
|
2
2
|
|
|
3
|
-
Framework-neutral
|
|
3
|
+
> Framework-neutral list navigation and focus restoration primitives
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
pnpm add @vielzeug/focus
|
|
9
|
+
npm install @vielzeug/focus
|
|
10
|
+
yarn add @vielzeug/focus
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## Quick Start
|
|
@@ -14,17 +16,28 @@ pnpm add @vielzeug/focus
|
|
|
14
16
|
import { captureFocus, createListNavigation } from '@vielzeug/focus';
|
|
15
17
|
|
|
16
18
|
const restore = captureFocus();
|
|
17
|
-
|
|
18
|
-
const listNavigation = createListNavigation({
|
|
19
|
+
const navigation = createListNavigation({
|
|
19
20
|
getItems: () => items,
|
|
20
21
|
loop: true,
|
|
21
|
-
onNavigate: ({ item }) => item.focus(),
|
|
22
22
|
});
|
|
23
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
24
|
+
navigation.handleKeydown(event)?.change?.item.focus();
|
|
25
|
+
};
|
|
23
26
|
|
|
24
|
-
listElement.addEventListener('keydown',
|
|
27
|
+
listElement.addEventListener('keydown', onKeydown);
|
|
25
28
|
|
|
26
29
|
restore();
|
|
27
|
-
|
|
30
|
+
listElement.removeEventListener('keydown', onKeydown);
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
- [Overview](https://vielzeug.dev/focus/)
|
|
36
|
+
- [Usage Guide](https://vielzeug.dev/focus/usage)
|
|
37
|
+
- [API Reference](https://vielzeug.dev/focus/api)
|
|
38
|
+
- [Examples](https://vielzeug.dev/focus/examples)
|
|
39
|
+
- [Migration Guide](https://vielzeug.dev/focus/migration)
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
package/dist/focus.cjs
CHANGED
|
@@ -1,2 +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
|
|
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,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},i=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},a=e=>{if(e===void 0)return n;if(!Number.isFinite(e)||e<=0)throw RangeError(`Typeahead delay must be a positive finite number`);return e},o=n=>{let o=a(n.typeahead?.delayMs),s=new Map;for(let e of[`next`,`prev`,`first`,`last`])for(let t of n.keys?.[e]??[]){let n=s.get(t);if(n&&n!==e)throw RangeError(`Key "${t}" is assigned to both ${n} and ${e}`);s.set(t,e)}let c=-1,l=``,u=0,d=(e,t)=>n.isItemDisabled?.(e,t)??!1,f=(e,t)=>t>=0&&t<e.length&&!d(e[t],t),p=e=>(f(e,c)||(c=-1),c),m=()=>{l=``,u=0},h=(e,t,n,r)=>f(e,t)?(c=t,Object.freeze({action:n,event:r,index:t,item:e[t]})):(c=-1,null),g=(e,t,n)=>n===`forward`?r(e,t,(e,t)=>!d(e,t)):i(e,t,(e,t)=>!d(e,t)),_=(e,t,r)=>{let i=p(e);if(!e.length)return c=-1,null;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=g(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return h(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=g(e,n,t);if(i>=0)return h(e,i,s,r)}return null},v=(e,t,n)=>(m(),p(e),e.length?t===`first`?h(e,g(e,0,`forward`),t,n):t===`last`?h(e,g(e,e.length-1,`backward`),t,n):_(e,t===`next`?`forward`:`backward`,n):(c=-1,null)),y=(e,t,r)=>{let i=p(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(!d(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},b=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return null;let r=performance.now(),i=e.key.toLocaleLowerCase();r-u>=o&&(l=``),u=r;let a=l+i;l=a.length>1&&[...a].every(e=>e===i)?i:a;let s=y(t,l,l.length>1);return s<0&&l.length>1&&(l=i,s=y(t,l,!1)),s<0?null:h(t,s,`typeahead`,e)},x=e=>{let t=n.getItems();if(p(t),m(),!Number.isInteger(e)||e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return f(t,r)?(c=r,c):(c=-1,c)},S=e=>v(n.getItems(),e),C=()=>{let e=n.getItems(),t=p(e);return t>=0?e[t]:void 0},w=()=>{c=-1,m()},T=()=>p(n.getItems()),E=(e,t)=>typeof e==`function`?e():e??t,D=r=>{let i=n.keys,a=(E(n.direction,`ltr`)===`rtl`?t:e)[E(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((i?.[e]??a[e]).includes(r))return e};return{getActiveItem:C,getIndex:T,handleKeydown:e=>{if(e.defaultPrevented||e.isComposing||E(n.disabled,!1))return null;let t=n.getItems();p(t);let r=D(e.key);if(r){let n=v(t,r,e);return e.preventDefault(),Object.freeze({change:n,handled:!0})}let i=b(e,t);return i?(n.typeahead?.preventDefault&&e.preventDefault(),Object.freeze({change:i,handled:!0})):null},navigate:S,reset:w,set:x}},s=e=>typeof e==`function`?e():e,c=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},l=e=>e.isConnected,u=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,d=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,f=e=>l(e)&&!u(e)&&!d(e),p=(e,t)=>{try{return e.focus({preventScroll:t}),c(e.ownerDocument)===e}catch{return!1}},m=(e,t={})=>{let n;try{n=s(e)}catch{n=null}if(n&&f(n)&&p(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r;try{r=s(t.fallback)}catch{return!1}return!r||!f(r)?!1:p(r,t.preventScroll)},h=(e={})=>{let t=c(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?m(i,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?m(e.fallback,{preventScroll:e.preventScroll}):!1}};exports.captureFocus=h,exports.createListNavigation=o,exports.restoreFocus=m;
|
|
2
2
|
//# sourceMappingURL=focus.cjs.map
|
package/dist/focus.cjs.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"focus.cjs","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\nexport type ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\nexport type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\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 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 if (delay === undefined) return DEFAULT_TYPEAHEAD_DELAY_MS;\n if (!Number.isFinite(delay) || delay <= 0) throw new RangeError('Typeahead delay must be a positive finite number');\n return delay;\n};\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const typeaheadDelay = resolveTypeaheadDelay(options.typeahead?.delayMs);\n const owners = new Map<string, ListNavigationAction>();\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of options.keys?.[action] ?? []) {\n const owner = owners.get(key);\n if (owner && owner !== action) throw new RangeError(`Key \"${key}\" is assigned to both ${owner} and ${action}`);\n owners.set(key, action);\n }\n }\n\n let index = -1;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\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 ): ListNavigationChange<T> | null => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return null;\n }\n\n index = nextIndex;\n\n return Object.freeze({ action, event, index: nextIndex, item: items[nextIndex] });\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 = (\n items: readonly T[],\n direction: 'forward' | 'backward',\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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 null;\n };\n\n const navigateWithItems = (\n items: readonly T[],\n action: ListNavigationAction,\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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[]): ListNavigationChange<T> | null => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return null;\n\n const now = performance.now();\n const key = event.key.toLocaleLowerCase();\n\n if (now - lastTypeaheadAt >= typeaheadDelay) 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 null;\n\n return commitIndex(items, nextIndex, 'typeahead', event);\n };\n\n const set = (nextIndex: number): number => {\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (!Number.isInteger(nextIndex) || 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): ListNavigationChange<T> | null => {\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\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 index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n return normalizeIndex(options.getItems());\n };\n\n const resolve = <V>(value: MaybeGetter<V> | undefined, fallback: V): V =>\n typeof value === 'function' ? (value as () => V)() : (value ?? fallback);\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = resolve(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[resolve(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): ListKeyResult<T> | null => {\n if (event.defaultPrevented || event.isComposing || resolve(options.disabled, false)) return null;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n const change = navigateWithItems(items, action, event);\n event.preventDefault();\n return Object.freeze({ change, handled: true });\n }\n\n const change = handleTypeahead(event, items);\n if (!change) return null;\n if (options.typeahead?.preventDefault) event.preventDefault();\n return Object.freeze({ change, handled: true });\n };\n\n return {\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\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 try {\n target.focus({ preventScroll });\n return getDeepActiveElement(target.ownerDocument) === target;\n } catch {\n return false;\n }\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n let next: FocusableElement | null | undefined;\n try {\n next = resolveTarget(target);\n } catch {\n next = null;\n }\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n let fallback: FocusableElement | null | undefined;\n try {\n fallback = resolveTarget(options.fallback);\n } catch {\n return false;\n }\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.iife.js
CHANGED
|
@@ -1,2 +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
|
|
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,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=>{if(e===void 0)return r;if(!Number.isFinite(e)||e<=0)throw RangeError(`Typeahead delay must be a positive finite number`);return e},s=e=>{let r=o(e.typeahead?.delayMs),s=new Map;for(let t of[`next`,`prev`,`first`,`last`])for(let n of e.keys?.[t]??[]){let e=s.get(n);if(e&&e!==t)throw RangeError(`Key "${n}" is assigned to both ${e} and ${t}`);s.set(n,t)}let c=-1,l=``,u=0,d=(t,n)=>e.isItemDisabled?.(t,n)??!1,f=(e,t)=>t>=0&&t<e.length&&!d(e[t],t),p=e=>(f(e,c)||(c=-1),c),m=()=>{l=``,u=0},h=(e,t,n,r)=>f(e,t)?(c=t,Object.freeze({action:n,event:r,index:t,item:e[t]})):(c=-1,null),g=(e,t,n)=>n===`forward`?i(e,t,(e,t)=>!d(e,t)):a(e,t,(e,t)=>!d(e,t)),_=(t,n,r)=>{let i=p(t);if(!t.length)return c=-1,null;let a=i<0?n===`forward`?0:t.length-1:n===`forward`?i+1:i-1,o=g(t,a,n),s=n===`forward`?`next`:`prev`;if(o>=0)return h(t,o,s,r);if(e.loop){let e=n===`forward`?0:t.length-1,i=g(t,e,n);if(i>=0)return h(t,i,s,r)}return null},v=(e,t,n)=>(m(),p(e),e.length?t===`first`?h(e,g(e,0,`forward`),t,n):t===`last`?h(e,g(e,e.length-1,`backward`),t,n):_(e,t===`next`?`forward`:`backward`,n):(c=-1,null)),y=(t,n,r)=>{let i=p(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(!d(o,i)&&e.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(n))return i}return-1},b=(t,n)=>{if(!e.typeahead||t.key.length!==1||t.ctrlKey||t.altKey||t.metaKey)return null;let i=performance.now(),a=t.key.toLocaleLowerCase();i-u>=r&&(l=``),u=i;let o=l+a;l=o.length>1&&[...o].every(e=>e===a)?a:o;let s=y(n,l,l.length>1);return s<0&&l.length>1&&(l=a,s=y(n,l,!1)),s<0?null:h(n,s,`typeahead`,t)},x=t=>{let n=e.getItems();if(p(n),m(),!Number.isInteger(t)||t<0||!n.length)return c=-1,c;let r=Math.min(t,n.length-1);return f(n,r)?(c=r,c):(c=-1,c)},S=t=>v(e.getItems(),t),C=()=>{let t=e.getItems(),n=p(t);return n>=0?t[n]:void 0},w=()=>{c=-1,m()},T=()=>p(e.getItems()),E=(e,t)=>typeof e==`function`?e():e??t,D=r=>{let i=e.keys,a=(E(e.direction,`ltr`)===`rtl`?n:t)[E(e.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((i?.[e]??a[e]).includes(r))return e};return{getActiveItem:C,getIndex:T,handleKeydown:t=>{if(t.defaultPrevented||t.isComposing||E(e.disabled,!1))return null;let n=e.getItems();p(n);let r=D(t.key);if(r){let e=v(n,r,t);return t.preventDefault(),Object.freeze({change:e,handled:!0})}let i=b(t,n);return i?(e.typeahead?.preventDefault&&t.preventDefault(),Object.freeze({change:i,handled:!0})):null},navigate:S,reset:w,set:x}},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)=>{try{return e.focus({preventScroll:t}),l(e.ownerDocument)===e}catch{return!1}},h=(e,t={})=>{let n;try{n=c(e)}catch{n=null}if(n&&p(n)&&m(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r;try{r=c(t.fallback)}catch{return!1}return!r||!p(r)?!1:m(r,t.preventScroll)};return e.captureFocus=(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}},e.createListNavigation=s,e.restoreFocus=h,e})({});
|
|
2
2
|
//# sourceMappingURL=focus.iife.js.map
|
package/dist/focus.iife.js.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"focus.iife.js","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\nexport type ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\nexport type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\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 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 if (delay === undefined) return DEFAULT_TYPEAHEAD_DELAY_MS;\n if (!Number.isFinite(delay) || delay <= 0) throw new RangeError('Typeahead delay must be a positive finite number');\n return delay;\n};\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const typeaheadDelay = resolveTypeaheadDelay(options.typeahead?.delayMs);\n const owners = new Map<string, ListNavigationAction>();\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of options.keys?.[action] ?? []) {\n const owner = owners.get(key);\n if (owner && owner !== action) throw new RangeError(`Key \"${key}\" is assigned to both ${owner} and ${action}`);\n owners.set(key, action);\n }\n }\n\n let index = -1;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\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 ): ListNavigationChange<T> | null => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return null;\n }\n\n index = nextIndex;\n\n return Object.freeze({ action, event, index: nextIndex, item: items[nextIndex] });\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 = (\n items: readonly T[],\n direction: 'forward' | 'backward',\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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 null;\n };\n\n const navigateWithItems = (\n items: readonly T[],\n action: ListNavigationAction,\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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[]): ListNavigationChange<T> | null => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return null;\n\n const now = performance.now();\n const key = event.key.toLocaleLowerCase();\n\n if (now - lastTypeaheadAt >= typeaheadDelay) 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 null;\n\n return commitIndex(items, nextIndex, 'typeahead', event);\n };\n\n const set = (nextIndex: number): number => {\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (!Number.isInteger(nextIndex) || 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): ListNavigationChange<T> | null => {\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\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 index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n return normalizeIndex(options.getItems());\n };\n\n const resolve = <V>(value: MaybeGetter<V> | undefined, fallback: V): V =>\n typeof value === 'function' ? (value as () => V)() : (value ?? fallback);\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = resolve(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[resolve(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): ListKeyResult<T> | null => {\n if (event.defaultPrevented || event.isComposing || resolve(options.disabled, false)) return null;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n const change = navigateWithItems(items, action, event);\n event.preventDefault();\n return Object.freeze({ change, handled: true });\n }\n\n const change = handleTypeahead(event, items);\n if (!change) return null;\n if (options.typeahead?.preventDefault) event.preventDefault();\n return Object.freeze({ change, handled: true });\n };\n\n return {\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\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 try {\n target.focus({ preventScroll });\n return getDeepActiveElement(target.ownerDocument) === target;\n } catch {\n return false;\n }\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n let next: FocusableElement | null | undefined;\n try {\n next = resolveTarget(target);\n } catch {\n next = null;\n }\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n let fallback: FocusableElement | null | undefined;\n try {\n fallback = resolveTarget(options.fallback);\n } catch {\n return false;\n }\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
CHANGED
|
@@ -1,2 +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
|
|
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,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},i=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},a=e=>{if(e===void 0)return n;if(!Number.isFinite(e)||e<=0)throw RangeError(`Typeahead delay must be a positive finite number`);return e},o=n=>{let o=a(n.typeahead?.delayMs),s=new Map;for(let e of[`next`,`prev`,`first`,`last`])for(let t of n.keys?.[e]??[]){let n=s.get(t);if(n&&n!==e)throw RangeError(`Key "${t}" is assigned to both ${n} and ${e}`);s.set(t,e)}let c=-1,l=``,u=0,d=(e,t)=>n.isItemDisabled?.(e,t)??!1,f=(e,t)=>t>=0&&t<e.length&&!d(e[t],t),p=e=>(f(e,c)||(c=-1),c),m=()=>{l=``,u=0},h=(e,t,n,r)=>f(e,t)?(c=t,Object.freeze({action:n,event:r,index:t,item:e[t]})):(c=-1,null),g=(e,t,n)=>n===`forward`?r(e,t,(e,t)=>!d(e,t)):i(e,t,(e,t)=>!d(e,t)),_=(e,t,r)=>{let i=p(e);if(!e.length)return c=-1,null;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=g(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return h(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=g(e,n,t);if(i>=0)return h(e,i,s,r)}return null},v=(e,t,n)=>(m(),p(e),e.length?t===`first`?h(e,g(e,0,`forward`),t,n):t===`last`?h(e,g(e,e.length-1,`backward`),t,n):_(e,t===`next`?`forward`:`backward`,n):(c=-1,null)),y=(e,t,r)=>{let i=p(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(!d(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},b=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return null;let r=performance.now(),i=e.key.toLocaleLowerCase();r-u>=o&&(l=``),u=r;let a=l+i;l=a.length>1&&[...a].every(e=>e===i)?i:a;let s=y(t,l,l.length>1);return s<0&&l.length>1&&(l=i,s=y(t,l,!1)),s<0?null:h(t,s,`typeahead`,e)},x=e=>{let t=n.getItems();if(p(t),m(),!Number.isInteger(e)||e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return f(t,r)?(c=r,c):(c=-1,c)},S=e=>v(n.getItems(),e),C=()=>{let e=n.getItems(),t=p(e);return t>=0?e[t]:void 0},w=()=>{c=-1,m()},T=()=>p(n.getItems()),E=(e,t)=>typeof e==`function`?e():e??t,D=r=>{let i=n.keys,a=(E(n.direction,`ltr`)===`rtl`?t:e)[E(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((i?.[e]??a[e]).includes(r))return e};return{getActiveItem:C,getIndex:T,handleKeydown:e=>{if(e.defaultPrevented||e.isComposing||E(n.disabled,!1))return null;let t=n.getItems();p(t);let r=D(e.key);if(r){let n=v(t,r,e);return e.preventDefault(),Object.freeze({change:n,handled:!0})}let i=b(e,t);return i?(n.typeahead?.preventDefault&&e.preventDefault(),Object.freeze({change:i,handled:!0})):null},navigate:S,reset:w,set:x}},s=e=>typeof e==`function`?e():e,c=e=>{let t=e.activeElement;for(;t instanceof HTMLElement&&t.shadowRoot?.activeElement;)t=t.shadowRoot.activeElement;return t},l=e=>e.isConnected,u=e=>e instanceof HTMLElement&&`disabled`in e&&!!e.disabled,d=e=>e.closest(`[inert]`)?!0:e instanceof HTMLElement&&e.inert,f=e=>l(e)&&!u(e)&&!d(e),p=(e,t)=>{try{return e.focus({preventScroll:t}),c(e.ownerDocument)===e}catch{return!1}},m=(e,t={})=>{let n;try{n=s(e)}catch{n=null}if(n&&f(n)&&p(n,t.preventScroll))return!0;if(!t.fallback)return!1;let r;try{r=s(t.fallback)}catch{return!1}return!r||!f(r)?!1:p(r,t.preventScroll)},h=(e={})=>{let t=c(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?m(i,{fallback:e.fallback,preventScroll:e.preventScroll}):e.fallback?m(e.fallback,{preventScroll:e.preventScroll}):!1}};export{h as captureFocus,o as createListNavigation,m as restoreFocus};
|
|
2
2
|
//# sourceMappingURL=focus.js.map
|
package/dist/focus.js.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"focus.js","names":[],"sources":["../src/list-navigation.ts","../src/restore-focus.ts"],"sourcesContent":["export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\nexport type ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\nexport type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\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 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 if (delay === undefined) return DEFAULT_TYPEAHEAD_DELAY_MS;\n if (!Number.isFinite(delay) || delay <= 0) throw new RangeError('Typeahead delay must be a positive finite number');\n return delay;\n};\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const typeaheadDelay = resolveTypeaheadDelay(options.typeahead?.delayMs);\n const owners = new Map<string, ListNavigationAction>();\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of options.keys?.[action] ?? []) {\n const owner = owners.get(key);\n if (owner && owner !== action) throw new RangeError(`Key \"${key}\" is assigned to both ${owner} and ${action}`);\n owners.set(key, action);\n }\n }\n\n let index = -1;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\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 ): ListNavigationChange<T> | null => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return null;\n }\n\n index = nextIndex;\n\n return Object.freeze({ action, event, index: nextIndex, item: items[nextIndex] });\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 = (\n items: readonly T[],\n direction: 'forward' | 'backward',\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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 null;\n };\n\n const navigateWithItems = (\n items: readonly T[],\n action: ListNavigationAction,\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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[]): ListNavigationChange<T> | null => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return null;\n\n const now = performance.now();\n const key = event.key.toLocaleLowerCase();\n\n if (now - lastTypeaheadAt >= typeaheadDelay) 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 null;\n\n return commitIndex(items, nextIndex, 'typeahead', event);\n };\n\n const set = (nextIndex: number): number => {\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (!Number.isInteger(nextIndex) || 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): ListNavigationChange<T> | null => {\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\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 index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n return normalizeIndex(options.getItems());\n };\n\n const resolve = <V>(value: MaybeGetter<V> | undefined, fallback: V): V =>\n typeof value === 'function' ? (value as () => V)() : (value ?? fallback);\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = resolve(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[resolve(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): ListKeyResult<T> | null => {\n if (event.defaultPrevented || event.isComposing || resolve(options.disabled, false)) return null;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n const change = navigateWithItems(items, action, event);\n event.preventDefault();\n return Object.freeze({ change, handled: true });\n }\n\n const change = handleTypeahead(event, items);\n if (!change) return null;\n if (options.typeahead?.preventDefault) event.preventDefault();\n return Object.freeze({ change, handled: true });\n };\n\n return {\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\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 try {\n target.focus({ preventScroll });\n return getDeepActiveElement(target.ownerDocument) === target;\n } catch {\n return false;\n }\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n let next: FocusableElement | null | undefined;\n try {\n next = resolveTarget(target);\n } catch {\n next = null;\n }\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n let fallback: FocusableElement | null | undefined;\n try {\n fallback = resolveTarget(options.fallback);\n } catch {\n return false;\n }\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.d.ts
CHANGED
|
@@ -1,5 +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';
|
|
1
|
+
export type { ListKeyAction, ListKeyResult, ListNavigation, ListNavigationAction, ListNavigationChange, ListNavigationOptions, ListNavigationTypeaheadOptions, MaybeGetter, } from './list-navigation.js';
|
|
2
|
+
export { createListNavigation } from './list-navigation.js';
|
|
3
|
+
export type { CaptureFocusOptions, FocusRestorer, FocusTarget, RestoreFocusOptions, } from './restore-focus.js';
|
|
4
|
+
export { captureFocus, restoreFocus } from './restore-focus.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,8BAA8B,EAC9B,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/list-navigation.cjs
CHANGED
|
@@ -1,2 +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
|
|
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,n)=>{for(let r=t;r<e.length;r++)if(n(e[r],r))return r;return-1},i=(e,t,n)=>{for(let r=t;r>=0;r--)if(n(e[r],r))return r;return-1},a=e=>{if(e===void 0)return n;if(!Number.isFinite(e)||e<=0)throw RangeError(`Typeahead delay must be a positive finite number`);return e},o=n=>{let o=a(n.typeahead?.delayMs),s=new Map;for(let e of[`next`,`prev`,`first`,`last`])for(let t of n.keys?.[e]??[]){let n=s.get(t);if(n&&n!==e)throw RangeError(`Key "${t}" is assigned to both ${n} and ${e}`);s.set(t,e)}let c=-1,l=``,u=0,d=(e,t)=>n.isItemDisabled?.(e,t)??!1,f=(e,t)=>t>=0&&t<e.length&&!d(e[t],t),p=e=>(f(e,c)||(c=-1),c),m=()=>{l=``,u=0},h=(e,t,n,r)=>f(e,t)?(c=t,Object.freeze({action:n,event:r,index:t,item:e[t]})):(c=-1,null),g=(e,t,n)=>n===`forward`?r(e,t,(e,t)=>!d(e,t)):i(e,t,(e,t)=>!d(e,t)),_=(e,t,r)=>{let i=p(e);if(!e.length)return c=-1,null;let a=i<0?t===`forward`?0:e.length-1:t===`forward`?i+1:i-1,o=g(e,a,t),s=t===`forward`?`next`:`prev`;if(o>=0)return h(e,o,s,r);if(n.loop){let n=t===`forward`?0:e.length-1,i=g(e,n,t);if(i>=0)return h(e,i,s,r)}return null},v=(e,t,n)=>(m(),p(e),e.length?t===`first`?h(e,g(e,0,`forward`),t,n):t===`last`?h(e,g(e,e.length-1,`backward`),t,n):_(e,t===`next`?`forward`:`backward`,n):(c=-1,null)),y=(e,t,r)=>{let i=p(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(!d(o,i)&&n.typeahead?.getLabel(o,i).toLocaleLowerCase().startsWith(t))return i}return-1},b=(e,t)=>{if(!n.typeahead||e.key.length!==1||e.ctrlKey||e.altKey||e.metaKey)return null;let r=performance.now(),i=e.key.toLocaleLowerCase();r-u>=o&&(l=``),u=r;let a=l+i;l=a.length>1&&[...a].every(e=>e===i)?i:a;let s=y(t,l,l.length>1);return s<0&&l.length>1&&(l=i,s=y(t,l,!1)),s<0?null:h(t,s,`typeahead`,e)},x=e=>{let t=n.getItems();if(p(t),m(),!Number.isInteger(e)||e<0||!t.length)return c=-1,c;let r=Math.min(e,t.length-1);return f(t,r)?(c=r,c):(c=-1,c)},S=e=>v(n.getItems(),e),C=()=>{let e=n.getItems(),t=p(e);return t>=0?e[t]:void 0},w=()=>{c=-1,m()},T=()=>p(n.getItems()),E=(e,t)=>typeof e==`function`?e():e??t,D=r=>{let i=n.keys,a=(E(n.direction,`ltr`)===`rtl`?t:e)[E(n.orientation,`vertical`)];for(let e of[`next`,`prev`,`first`,`last`])if((i?.[e]??a[e]).includes(r))return e};return{getActiveItem:C,getIndex:T,handleKeydown:e=>{if(e.defaultPrevented||e.isComposing||E(n.disabled,!1))return null;let t=n.getItems();p(t);let r=D(e.key);if(r){let n=v(t,r,e);return e.preventDefault(),Object.freeze({change:n,handled:!0})}let i=b(e,t);return i?(n.typeahead?.preventDefault&&e.preventDefault(),Object.freeze({change:i,handled:!0})):null},navigate:S,reset:w,set:x}};exports.createListNavigation=o;
|
|
2
2
|
//# sourceMappingURL=list-navigation.cjs.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"list-navigation.cjs","names":[],"sources":["../src/list-navigation.ts"],"sourcesContent":["export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\nexport type ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\nexport type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\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 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 if (delay === undefined) return DEFAULT_TYPEAHEAD_DELAY_MS;\n if (!Number.isFinite(delay) || delay <= 0) throw new RangeError('Typeahead delay must be a positive finite number');\n return delay;\n};\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const typeaheadDelay = resolveTypeaheadDelay(options.typeahead?.delayMs);\n const owners = new Map<string, ListNavigationAction>();\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of options.keys?.[action] ?? []) {\n const owner = owners.get(key);\n if (owner && owner !== action) throw new RangeError(`Key \"${key}\" is assigned to both ${owner} and ${action}`);\n owners.set(key, action);\n }\n }\n\n let index = -1;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\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 ): ListNavigationChange<T> | null => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return null;\n }\n\n index = nextIndex;\n\n return Object.freeze({ action, event, index: nextIndex, item: items[nextIndex] });\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 = (\n items: readonly T[],\n direction: 'forward' | 'backward',\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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 null;\n };\n\n const navigateWithItems = (\n items: readonly T[],\n action: ListNavigationAction,\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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[]): ListNavigationChange<T> | null => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return null;\n\n const now = performance.now();\n const key = event.key.toLocaleLowerCase();\n\n if (now - lastTypeaheadAt >= typeaheadDelay) 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 null;\n\n return commitIndex(items, nextIndex, 'typeahead', event);\n };\n\n const set = (nextIndex: number): number => {\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (!Number.isInteger(nextIndex) || 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): ListNavigationChange<T> | null => {\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\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 index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n return normalizeIndex(options.getItems());\n };\n\n const resolve = <V>(value: MaybeGetter<V> | undefined, fallback: V): V =>\n typeof value === 'function' ? (value as () => V)() : (value ?? fallback);\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = resolve(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[resolve(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): ListKeyResult<T> | null => {\n if (event.defaultPrevented || event.isComposing || resolve(options.disabled, false)) return null;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n const change = navigateWithItems(items, action, event);\n event.preventDefault();\n return Object.freeze({ change, handled: true });\n }\n\n const change = handleTypeahead(event, items);\n if (!change) return null;\n if (options.typeahead?.preventDefault) event.preventDefault();\n return Object.freeze({ change, handled: true });\n };\n\n return {\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n };\n};\n"],"mappings":"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"}
|
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
export type MaybeGetter<T> = T | (() => T);
|
|
2
1
|
export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';
|
|
3
2
|
export type ListKeyAction = ListNavigationAction | 'typeahead';
|
|
4
3
|
export type ListNavigationChange<T> = {
|
|
5
|
-
action: ListKeyAction;
|
|
6
|
-
event?: KeyboardEvent;
|
|
7
|
-
index: number;
|
|
8
|
-
item: T;
|
|
4
|
+
readonly action: ListKeyAction;
|
|
5
|
+
readonly event?: KeyboardEvent;
|
|
6
|
+
readonly index: number;
|
|
7
|
+
readonly item: T;
|
|
8
|
+
};
|
|
9
|
+
export type ListKeyResult<T> = {
|
|
10
|
+
readonly change: ListNavigationChange<T> | null;
|
|
11
|
+
readonly handled: true;
|
|
9
12
|
};
|
|
10
13
|
export type ListNavigationTypeaheadOptions<T> = {
|
|
11
14
|
delayMs?: number;
|
|
12
15
|
getLabel: (item: T, index: number) => string;
|
|
16
|
+
preventDefault?: boolean;
|
|
13
17
|
};
|
|
18
|
+
export type MaybeGetter<T> = T | (() => T);
|
|
14
19
|
export type ListNavigationOptions<T> = {
|
|
15
20
|
direction?: MaybeGetter<'ltr' | 'rtl'>;
|
|
16
|
-
disabled?: MaybeGetter<boolean
|
|
21
|
+
disabled?: MaybeGetter<boolean>;
|
|
17
22
|
getItems: () => readonly T[];
|
|
18
23
|
isItemDisabled?: (item: T, index: number) => boolean;
|
|
19
24
|
keys?: Partial<Record<ListNavigationAction, readonly string[]>>;
|
|
20
25
|
loop?: boolean;
|
|
21
|
-
onNavigate?: (change: ListNavigationChange<T>) => void;
|
|
22
26
|
orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;
|
|
23
|
-
signal?: AbortSignal;
|
|
24
27
|
typeahead?: ListNavigationTypeaheadOptions<T>;
|
|
25
28
|
};
|
|
26
29
|
export type ListNavigation<T> = {
|
|
27
|
-
[Symbol.dispose](): void;
|
|
28
|
-
readonly disposalSignal: AbortSignal;
|
|
29
|
-
dispose(): void;
|
|
30
|
-
readonly disposed: boolean;
|
|
31
30
|
getActiveItem(): T | undefined;
|
|
32
31
|
getIndex(): number;
|
|
33
|
-
handleKeydown(event: KeyboardEvent):
|
|
34
|
-
navigate(action: ListNavigationAction):
|
|
32
|
+
handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;
|
|
33
|
+
navigate(action: ListNavigationAction): ListNavigationChange<T> | null;
|
|
35
34
|
reset(): void;
|
|
36
35
|
set(index: number): number;
|
|
37
36
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-navigation.d.ts","sourceRoot":"","sources":["../src/list-navigation.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"list-navigation.d.ts","sourceRoot":"","sources":["../src/list-navigation.ts"],"names":[],"mappings":"AAAA,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,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;CACxB,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;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAE3C,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChC,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,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;IAC9D,SAAS,CAAC,EAAE,8BAA8B,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,QAAQ,IAAI,MAAM,CAAC;IACnB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACvE,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,CAyP3F,CAAC"}
|
package/dist/list-navigation.js
CHANGED
|
@@ -32,92 +32,106 @@ var e = {
|
|
|
32
32
|
prev: ["ArrowRight"]
|
|
33
33
|
},
|
|
34
34
|
vertical: e.vertical
|
|
35
|
-
}, n = 500, r = (e, t
|
|
35
|
+
}, n = 500, r = (e, t, n) => {
|
|
36
36
|
for (let r = t; r < e.length; r++) if (n(e[r], r)) return r;
|
|
37
37
|
return -1;
|
|
38
|
-
},
|
|
38
|
+
}, i = (e, t, n) => {
|
|
39
39
|
for (let r = t; r >= 0; r--) if (n(e[r], r)) return r;
|
|
40
40
|
return -1;
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
}, a = (e) => {
|
|
42
|
+
if (e === void 0) return n;
|
|
43
|
+
if (!Number.isFinite(e) || e <= 0) throw RangeError("Typeahead delay must be a positive finite number");
|
|
44
|
+
return e;
|
|
45
|
+
}, o = (n) => {
|
|
46
|
+
let o = a(n.typeahead?.delayMs), s = /* @__PURE__ */ new Map();
|
|
47
|
+
for (let e of [
|
|
48
|
+
"next",
|
|
49
|
+
"prev",
|
|
50
|
+
"first",
|
|
51
|
+
"last"
|
|
52
|
+
]) for (let t of n.keys?.[e] ?? []) {
|
|
53
|
+
let n = s.get(t);
|
|
54
|
+
if (n && n !== e) throw RangeError(`Key "${t}" is assigned to both ${n} and ${e}`);
|
|
55
|
+
s.set(t, e);
|
|
56
|
+
}
|
|
57
|
+
let c = -1, l = "", u = 0, d = (e, t) => n.isItemDisabled?.(e, t) ?? !1, f = (e, t) => t >= 0 && t < e.length && !d(e[t], t), p = (e) => (f(e, c) || (c = -1), c), m = () => {
|
|
58
|
+
l = "", u = 0;
|
|
59
|
+
}, h = (e, t, n, r) => f(e, t) ? (c = t, Object.freeze({
|
|
60
|
+
action: n,
|
|
61
|
+
event: r,
|
|
47
62
|
index: t,
|
|
48
63
|
item: e[t]
|
|
49
|
-
})
|
|
50
|
-
let i =
|
|
51
|
-
if (!e.length) return c = -1,
|
|
52
|
-
let a = i < 0 ? t === "forward" ? 0 : e.length - 1 : t === "forward" ? i + 1 : i - 1, o =
|
|
53
|
-
if (o >= 0) return
|
|
64
|
+
})) : (c = -1, null), g = (e, t, n) => n === "forward" ? r(e, t, (e, t) => !d(e, t)) : i(e, t, (e, t) => !d(e, t)), _ = (e, t, r) => {
|
|
65
|
+
let i = p(e);
|
|
66
|
+
if (!e.length) return c = -1, null;
|
|
67
|
+
let a = i < 0 ? t === "forward" ? 0 : e.length - 1 : t === "forward" ? i + 1 : i - 1, o = g(e, a, t), s = t === "forward" ? "next" : "prev";
|
|
68
|
+
if (o >= 0) return h(e, o, s, r);
|
|
54
69
|
if (n.loop) {
|
|
55
|
-
let n = t === "forward" ? 0 : e.length - 1, i =
|
|
56
|
-
if (i >= 0) return
|
|
70
|
+
let n = t === "forward" ? 0 : e.length - 1, i = g(e, n, t);
|
|
71
|
+
if (i >= 0) return h(e, i, s, r);
|
|
57
72
|
}
|
|
58
|
-
return
|
|
59
|
-
},
|
|
60
|
-
let i =
|
|
73
|
+
return null;
|
|
74
|
+
}, v = (e, t, n) => (m(), p(e), e.length ? t === "first" ? h(e, g(e, 0, "forward"), t, n) : t === "last" ? h(e, g(e, e.length - 1, "backward"), t, n) : _(e, t === "next" ? "forward" : "backward", n) : (c = -1, null)), y = (e, t, r) => {
|
|
75
|
+
let i = p(e), a = i < 0 ? 0 : r ? i : i + 1;
|
|
61
76
|
for (let r = 0; r < e.length; r++) {
|
|
62
77
|
let i = (a + r) % e.length, o = e[i];
|
|
63
|
-
if (!
|
|
78
|
+
if (!d(o, i) && n.typeahead?.getLabel(o, i).toLocaleLowerCase().startsWith(t)) return i;
|
|
64
79
|
}
|
|
65
80
|
return -1;
|
|
66
|
-
},
|
|
67
|
-
if (!n.typeahead || e.key.length !== 1 || e.ctrlKey || e.altKey || e.metaKey) return
|
|
68
|
-
let r =
|
|
69
|
-
r -
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
return
|
|
74
|
-
},
|
|
75
|
-
if (l) return -1;
|
|
81
|
+
}, b = (e, t) => {
|
|
82
|
+
if (!n.typeahead || e.key.length !== 1 || e.ctrlKey || e.altKey || e.metaKey) return null;
|
|
83
|
+
let r = performance.now(), i = e.key.toLocaleLowerCase();
|
|
84
|
+
r - u >= o && (l = ""), u = r;
|
|
85
|
+
let a = l + i;
|
|
86
|
+
l = a.length > 1 && [...a].every((e) => e === i) ? i : a;
|
|
87
|
+
let s = y(t, l, l.length > 1);
|
|
88
|
+
return s < 0 && l.length > 1 && (l = i, s = y(t, l, !1)), s < 0 ? null : h(t, s, "typeahead", e);
|
|
89
|
+
}, x = (e) => {
|
|
76
90
|
let t = n.getItems();
|
|
77
|
-
if (
|
|
91
|
+
if (p(t), m(), !Number.isInteger(e) || e < 0 || !t.length) return c = -1, c;
|
|
78
92
|
let r = Math.min(e, t.length - 1);
|
|
79
|
-
return
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
let e = n.getItems(), t = h(e);
|
|
93
|
+
return f(t, r) ? (c = r, c) : (c = -1, c);
|
|
94
|
+
}, S = (e) => v(n.getItems(), e), C = () => {
|
|
95
|
+
let e = n.getItems(), t = p(e);
|
|
83
96
|
return t >= 0 ? e[t] : void 0;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
let
|
|
97
|
+
}, w = () => {
|
|
98
|
+
c = -1, m();
|
|
99
|
+
}, T = () => p(n.getItems()), E = (e, t) => typeof e == "function" ? e() : e ?? t, D = (r) => {
|
|
100
|
+
let i = n.keys, a = (E(n.direction, "ltr") === "rtl" ? t : e)[E(n.orientation, "vertical")];
|
|
88
101
|
for (let e of [
|
|
89
102
|
"next",
|
|
90
103
|
"prev",
|
|
91
104
|
"first",
|
|
92
105
|
"last"
|
|
93
|
-
]) if ((
|
|
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());
|
|
106
|
+
]) if ((i?.[e] ?? a[e]).includes(r)) return e;
|
|
102
107
|
};
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
return {
|
|
109
|
+
getActiveItem: C,
|
|
110
|
+
getIndex: T,
|
|
111
|
+
handleKeydown: (e) => {
|
|
112
|
+
if (e.defaultPrevented || e.isComposing || E(n.disabled, !1)) return null;
|
|
113
|
+
let t = n.getItems();
|
|
114
|
+
p(t);
|
|
115
|
+
let r = D(e.key);
|
|
116
|
+
if (r) {
|
|
117
|
+
let n = v(t, r, e);
|
|
118
|
+
return e.preventDefault(), Object.freeze({
|
|
119
|
+
change: n,
|
|
120
|
+
handled: !0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
let i = b(e, t);
|
|
124
|
+
return i ? (n.typeahead?.preventDefault && e.preventDefault(), Object.freeze({
|
|
125
|
+
change: i,
|
|
126
|
+
handled: !0
|
|
127
|
+
})) : null;
|
|
110
128
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
navigate: w,
|
|
115
|
-
reset: E,
|
|
116
|
-
set: C,
|
|
117
|
-
[Symbol.dispose]: A
|
|
129
|
+
navigate: S,
|
|
130
|
+
reset: w,
|
|
131
|
+
set: x
|
|
118
132
|
};
|
|
119
133
|
};
|
|
120
134
|
//#endregion
|
|
121
|
-
export {
|
|
135
|
+
export { o as createListNavigation };
|
|
122
136
|
|
|
123
137
|
//# sourceMappingURL=list-navigation.js.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"list-navigation.js","names":[],"sources":["../src/list-navigation.ts"],"sourcesContent":["export type ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\nexport type ListKeyAction = ListNavigationAction | 'typeahead';\n\nexport type ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\nexport type ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\nexport type ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\nexport type MaybeGetter<T> = T | (() => T);\n\nexport type ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\nexport type ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\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 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 if (delay === undefined) return DEFAULT_TYPEAHEAD_DELAY_MS;\n if (!Number.isFinite(delay) || delay <= 0) throw new RangeError('Typeahead delay must be a positive finite number');\n return delay;\n};\n\nexport const createListNavigation = <T>(options: ListNavigationOptions<T>): ListNavigation<T> => {\n const typeaheadDelay = resolveTypeaheadDelay(options.typeahead?.delayMs);\n const owners = new Map<string, ListNavigationAction>();\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of options.keys?.[action] ?? []) {\n const owner = owners.get(key);\n if (owner && owner !== action) throw new RangeError(`Key \"${key}\" is assigned to both ${owner} and ${action}`);\n owners.set(key, action);\n }\n }\n\n let index = -1;\n let typeaheadBuffer = '';\n let lastTypeaheadAt = 0;\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 ): ListNavigationChange<T> | null => {\n if (!isUsableIndex(items, nextIndex)) {\n index = -1;\n\n return null;\n }\n\n index = nextIndex;\n\n return Object.freeze({ action, event, index: nextIndex, item: items[nextIndex] });\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 = (\n items: readonly T[],\n direction: 'forward' | 'backward',\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n const current = normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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 null;\n };\n\n const navigateWithItems = (\n items: readonly T[],\n action: ListNavigationAction,\n event?: KeyboardEvent,\n ): ListNavigationChange<T> | null => {\n resetTypeahead();\n normalizeIndex(items);\n\n if (!items.length) {\n index = -1;\n\n return null;\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[]): ListNavigationChange<T> | null => {\n if (!options.typeahead || event.key.length !== 1 || event.ctrlKey || event.altKey || event.metaKey) return null;\n\n const now = performance.now();\n const key = event.key.toLocaleLowerCase();\n\n if (now - lastTypeaheadAt >= typeaheadDelay) 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 null;\n\n return commitIndex(items, nextIndex, 'typeahead', event);\n };\n\n const set = (nextIndex: number): number => {\n const items = options.getItems();\n normalizeIndex(items);\n resetTypeahead();\n\n if (!Number.isInteger(nextIndex) || 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): ListNavigationChange<T> | null => {\n return navigateWithItems(options.getItems(), action);\n };\n\n const getActiveItem = (): T | undefined => {\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 index = -1;\n resetTypeahead();\n };\n\n const getIndex = (): number => {\n return normalizeIndex(options.getItems());\n };\n\n const resolve = <V>(value: MaybeGetter<V> | undefined, fallback: V): V =>\n typeof value === 'function' ? (value as () => V)() : (value ?? fallback);\n\n const resolveKeyAction = (eventKey: string): ListNavigationAction | undefined => {\n const keys = options.keys;\n const keyTable = resolve(options.direction, 'ltr') === 'rtl' ? DEFAULT_KEYS_RTL : DEFAULT_KEYS;\n const defaults = keyTable[resolve(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): ListKeyResult<T> | null => {\n if (event.defaultPrevented || event.isComposing || resolve(options.disabled, false)) return null;\n\n const items = options.getItems();\n normalizeIndex(items);\n\n const action = resolveKeyAction(event.key);\n\n if (action) {\n const change = navigateWithItems(items, action, event);\n event.preventDefault();\n return Object.freeze({ change, handled: true });\n }\n\n const change = handleTypeahead(event, items);\n if (!change) return null;\n if (options.typeahead?.preventDefault) event.preventDefault();\n return Object.freeze({ change, handled: true });\n };\n\n return {\n getActiveItem,\n getIndex,\n handleKeydown,\n navigate,\n reset,\n set,\n };\n};\n"],"mappings":"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"}
|
package/dist/restore-focus.cjs
CHANGED
|
@@ -1,2 +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)=>
|
|
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)=>{try{return e.focus({preventScroll:n}),t(e.ownerDocument)===e}catch{return!1}},s=(t,n={})=>{let r;try{r=e(t)}catch{r=null}if(r&&a(r)&&o(r,n.preventScroll))return!0;if(!n.fallback)return!1;let i;try{i=e(n.fallback)}catch{return!1}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
2
|
//# sourceMappingURL=restore-focus.cjs.map
|
|
@@ -1 +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
|
|
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 try {\n target.focus({ preventScroll });\n return getDeepActiveElement(target.ownerDocument) === target;\n } catch {\n return false;\n }\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n let next: FocusableElement | null | undefined;\n try {\n next = resolveTarget(target);\n } catch {\n next = null;\n }\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n let fallback: FocusableElement | null | undefined;\n try {\n fallback = resolveTarget(options.fallback);\n } catch {\n return false;\n }\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,IAAgD,CAChG,GAAI,CAEF,OADA,EAAO,MAAM,CAAE,eAAc,CAAC,EACvB,EAAqB,EAAO,aAAa,IAAM,CACxD,MAAQ,CACN,MAAO,EACT,CACF,EAEa,GAAgB,EAAqB,EAA+B,CAAC,IAAe,CAC/F,IAAI,EACJ,GAAI,CACF,EAAO,EAAc,CAAM,CAC7B,MAAQ,CACN,EAAO,IACT,CAEA,GAAI,GAAQ,EAAa,CAAI,GAAK,EAAe,EAAM,EAAQ,aAAa,EAC1E,MAAO,GAGT,GAAI,CAAC,EAAQ,SAAU,MAAO,GAE9B,IAAI,EACJ,GAAI,CACF,EAAW,EAAc,EAAQ,QAAQ,CAC3C,MAAQ,CACN,MAAO,EACT,CAIA,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"}
|
|
@@ -1 +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;
|
|
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;AA6C1C,eAAO,MAAM,YAAY,GAAI,QAAQ,WAAW,EAAE,UAAS,mBAAwB,KAAG,OAwBrF,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,UAAS,mBAAwB,KAAG,aAuChE,CAAC"}
|
package/dist/restore-focus.js
CHANGED
|
@@ -3,11 +3,27 @@ var e = (e) => typeof e == "function" ? e() : e, t = (e) => {
|
|
|
3
3
|
let t = e.activeElement;
|
|
4
4
|
for (; t instanceof HTMLElement && t.shadowRoot?.activeElement;) t = t.shadowRoot.activeElement;
|
|
5
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) =>
|
|
7
|
-
|
|
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) => {
|
|
7
|
+
try {
|
|
8
|
+
return e.focus({ preventScroll: n }), t(e.ownerDocument) === e;
|
|
9
|
+
} catch {
|
|
10
|
+
return !1;
|
|
11
|
+
}
|
|
12
|
+
}, s = (t, n = {}) => {
|
|
13
|
+
let r;
|
|
14
|
+
try {
|
|
15
|
+
r = e(t);
|
|
16
|
+
} catch {
|
|
17
|
+
r = null;
|
|
18
|
+
}
|
|
8
19
|
if (r && a(r) && o(r, n.preventScroll)) return !0;
|
|
9
20
|
if (!n.fallback) return !1;
|
|
10
|
-
let i
|
|
21
|
+
let i;
|
|
22
|
+
try {
|
|
23
|
+
i = e(n.fallback);
|
|
24
|
+
} catch {
|
|
25
|
+
return !1;
|
|
26
|
+
}
|
|
11
27
|
return !i || !a(i) ? !1 : o(i, n.preventScroll);
|
|
12
28
|
}, c = (e = {}) => {
|
|
13
29
|
let n = t(document), r = !e.signal?.aborted;
|
|
@@ -1 +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
|
|
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 try {\n target.focus({ preventScroll });\n return getDeepActiveElement(target.ownerDocument) === target;\n } catch {\n return false;\n }\n};\n\nexport const restoreFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n let next: FocusableElement | null | undefined;\n try {\n next = resolveTarget(target);\n } catch {\n next = null;\n }\n\n if (next && canRestoreTo(next) && focusAndVerify(next, options.preventScroll)) {\n return true;\n }\n\n if (!options.fallback) return false;\n\n let fallback: FocusableElement | null | undefined;\n try {\n fallback = resolveTarget(options.fallback);\n } catch {\n return false;\n }\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,MAAgD;CAChG,IAAI;EAEF,OADA,EAAO,MAAM,EAAE,iBAAc,CAAC,GACvB,EAAqB,EAAO,aAAa,MAAM;CACxD,QAAQ;EACN,OAAO;CACT;AACF,GAEa,KAAgB,GAAqB,IAA+B,CAAC,MAAe;CAC/F,IAAI;CACJ,IAAI;EACF,IAAO,EAAc,CAAM;CAC7B,QAAQ;EACN,IAAO;CACT;CAEA,IAAI,KAAQ,EAAa,CAAI,KAAK,EAAe,GAAM,EAAQ,aAAa,GAC1E,OAAO;CAGT,IAAI,CAAC,EAAQ,UAAU,OAAO;CAE9B,IAAI;CACJ,IAAI;EACF,IAAW,EAAc,EAAQ,QAAQ;CAC3C,QAAQ;EACN,OAAO;CACT;CAIA,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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/focus",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Framework-neutral list navigation and focus restoration primitives",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,23 +24,26 @@
|
|
|
24
24
|
"types": "./dist/index.d.ts"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
+
"sideEffects": false,
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public",
|
|
29
30
|
"registry": "https://registry.npmjs.org/"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
|
-
"build": "vite build && pnpm run build:bundle && pnpm run build:types",
|
|
33
|
+
"build": "vite build && pnpm --silent run build:bundle && pnpm --silent run build:types && pnpm --silent run test:types && pnpm --silent run verify:exports",
|
|
33
34
|
"build:bundle": "vite build --config vite.bundle.config.ts",
|
|
34
35
|
"build:types": "tsc -p tsconfig.declarations.json",
|
|
35
36
|
"fix": "biome check --write src",
|
|
36
37
|
"lint": "biome ci src",
|
|
37
|
-
"prepublishOnly": "pnpm run build",
|
|
38
|
-
"test": "vitest"
|
|
38
|
+
"prepublishOnly": "pnpm --silent run build",
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"test:types": "tsc -p tsconfig.type-tests.json",
|
|
41
|
+
"verify:exports": "node verify-exports.mjs"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^26.
|
|
44
|
+
"@types/node": "^26.4.1",
|
|
42
45
|
"typescript": "^6.0.3",
|
|
43
|
-
"vite": "^8.2.
|
|
44
|
-
"vitest": "^
|
|
46
|
+
"vite": "^8.2.2",
|
|
47
|
+
"vitest": "^5.0.0"
|
|
45
48
|
}
|
|
46
49
|
}
|