@vielzeug/focus 3.0.0 → 26.10.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.
@@ -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 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"}
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\n/**\n * Hands focus to `target` when focus has been lost to the document body — the state left\n * behind when the focused element unmounts mid-swap (browsing a dialog, paging a list),\n * where keydown would never reach a handler and every shortcut dies. Returns `true` when\n * focus was rescued; `false` when focus is already on a real element or the target cannot\n * take focus (the `fallback` option of {@link RestoreFocusOptions} still applies).\n */\nexport const rescueFocus = (target: FocusTarget, options: RestoreFocusOptions = {}): boolean => {\n const active = getDeepActiveElement(document);\n\n if (active && active !== document.body) return false;\n\n return restoreFocus(target, options);\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,cAAiB,GACd,OAAO,GAAW,WAAa,EAAO,EAAI,EAG7C,qBAAwB,GAA2C,CACvE,IAAI,EAAyB,EAAa,cAE1C,KAAO,aAAkB,aAAe,EAAO,YAAY,eACzD,EAAS,EAAO,WAAW,cAG7B,OAAO,CACT,EAEM,YAAe,GAGZ,EAAO,YAGV,WAAc,GACX,aAAkB,aAAe,aAAc,GAAU,EAAS,EAA4B,SAGjG,QAAW,GACX,EAAO,QAAQ,SAAS,EAAU,GAE/B,aAAkB,aAAe,EAAO,MAG3C,aAAgB,GACb,YAAY,CAAM,GAAK,CAAC,WAAW,CAAM,GAAK,CAAC,QAAQ,CAAM,EAGhE,gBAAkB,EAA0B,IAAgD,CAChG,GAAI,CAEF,OADA,EAAO,MAAM,CAAE,eAAc,CAAC,EACvB,qBAAqB,EAAO,aAAa,IAAM,CACxD,MAAQ,CACN,MAAO,EACT,CACF,EAEa,cAAgB,EAAqB,EAA+B,CAAC,IAAe,CAC/F,IAAI,EACJ,GAAI,CACF,EAAO,cAAc,CAAM,CAC7B,MAAQ,CACN,EAAO,IACT,CAEA,GAAI,GAAQ,aAAa,CAAI,GAAK,eAAe,EAAM,EAAQ,aAAa,EAC1E,MAAO,GAGT,GAAI,CAAC,EAAQ,SAAU,MAAO,GAE9B,IAAI,EACJ,GAAI,CACF,EAAW,cAAc,EAAQ,QAAQ,CAC3C,MAAQ,CACN,MAAO,EACT,CAIA,MAFI,CAAC,GAAY,CAAC,aAAa,CAAQ,EAAU,GAE1C,eAAe,EAAU,EAAQ,aAAa,CACvD,EASa,aAAe,EAAqB,EAA+B,CAAC,IAAe,CAC9F,IAAM,EAAS,qBAAqB,QAAQ,EAI5C,OAFI,GAAU,IAAW,SAAS,KAAa,GAExC,aAAa,EAAQ,CAAO,CACrC,EAEa,cAAgB,EAA+B,CAAC,IAAqB,CAChF,IAAI,EAAW,qBAAqB,QAAQ,EACxC,EAAY,CAAC,EAAQ,QAAQ,QAE5B,IAAW,EAAW,MAE3B,IAAM,WAAqB,CACzB,EAAY,GACZ,EAAW,IACb,EAMA,OAJK,EAAQ,QAAQ,SACnB,EAAQ,QAAQ,iBAAiB,QAAS,OAAQ,CAAE,KAAM,EAAK,CAAC,MAGrD,CACX,GAAI,CAAC,EAAW,MAAO,GAEvB,EAAY,GACZ,EAAQ,QAAQ,oBAAoB,QAAS,MAAM,EAEnD,IAAM,EAAS,EAgBf,MAfA,GAAW,KAEP,EACK,aAAa,EAAQ,CAC1B,SAAU,EAAQ,SAClB,cAAe,EAAQ,aACzB,CAAC,EAGC,EAAQ,SACH,aAAa,EAAQ,SAAU,CACpC,cAAe,EAAQ,aACzB,CAAC,EAGI,EACT,CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/focus",
3
- "version": "3.0.0",
3
+ "version": "26.10.0",
4
4
  "description": "Framework-neutral list navigation and focus restoration primitives",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,10 +40,13 @@
40
40
  "test:types": "tsc -p tsconfig.type-tests.json",
41
41
  "verify:exports": "node verify-exports.mjs"
42
42
  },
43
+ "dependencies": {
44
+ "@vielzeug/keymap": "26.10.0"
45
+ },
43
46
  "devDependencies": {
44
- "@types/node": "^26.4.1",
47
+ "@types/node": "^26.6.2",
45
48
  "typescript": "^6.0.3",
46
- "vite": "^8.2.2",
47
- "vitest": "^5.0.0"
49
+ "vite": "^8.3.0",
50
+ "vitest": "^5.0.1"
48
51
  }
49
52
  }