@cck-ui/hooks 0.22.0 → 0.23.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/cjs/index.cjs CHANGED
@@ -1,10 +1,14 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_use_focus_trap = require("./use-focus-trap/use-focus-trap.cjs");
2
3
  const require_use_timeout_fn = require("./use-timeout-fn/use-timeout-fn.cjs");
3
4
  const require_use_clipboard = require("./use-clipboard/use-clipboard.cjs");
5
+ const require_use_disclosure = require("./use-disclosure/use-disclosure.cjs");
4
6
  const require_use_id = require("./use-id/use-id.cjs");
5
7
  const require_use_isomorphic_effect = require("./use-isomorphic-effect/use-isomorphic-effect.cjs");
6
8
  const require_use_splitter = require("./use-splitter/use-splitter.cjs");
7
9
  exports.useClipboard = require_use_clipboard.useClipboard;
10
+ exports.useDisclosure = require_use_disclosure.useDisclosure;
11
+ exports.useFocusTrap = require_use_focus_trap.useFocusTrap;
8
12
  exports.useId = require_use_id.useId;
9
13
  exports.useIsomorphicEffect = require_use_isomorphic_effect.useIsomorphicEffect;
10
14
  exports.useSplitter = require_use_splitter.useSplitter;
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ let vue = require("vue");
3
+ //#region packages/@cck-ui/hooks/src/use-disclosure/use-disclosure.ts
4
+ function useDisclosure(initialState = false, options = {}) {
5
+ const state = (0, vue.ref)((0, vue.toValue)(initialState));
6
+ const getOptions = () => (0, vue.toValue)(options);
7
+ const open = () => {
8
+ if (!state.value) {
9
+ getOptions().onOpen?.();
10
+ state.value = true;
11
+ }
12
+ };
13
+ const close = () => {
14
+ if (state.value) {
15
+ getOptions().onClose?.();
16
+ state.value = false;
17
+ }
18
+ };
19
+ const toggle = () => {
20
+ state.value ? close() : open();
21
+ };
22
+ const set = (value) => {
23
+ state.value = value;
24
+ };
25
+ return {
26
+ state,
27
+ handlers: {
28
+ open,
29
+ close,
30
+ toggle,
31
+ set
32
+ }
33
+ };
34
+ }
35
+ //#endregion
36
+ exports.useDisclosure = useDisclosure;
37
+
38
+ //# sourceMappingURL=use-disclosure.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-disclosure.cjs","names":[],"sources":["../../src/use-disclosure/use-disclosure.ts"],"sourcesContent":["import { MaybeRefOrGetter, ref, Ref, toValue } from 'vue'\n\nexport interface UseDisclosureOptions {\n /**\n * @description Callback invoked when the state becomes true\n */\n onOpen?: () => void\n /**\n * @description Callback invoked when the state becomes false\n */\n onClose?: () => void\n}\n\nexport interface UseDisclosureHandlers {\n /**\n * Directly set the state without triggering callbacks\n * @param value - The target boolean value.\n */\n set: (value: boolean) => void\n\n /** Open the state (no-op if already open) */\n open: () => void\n\n /** Close the state (no-op if already closed) */\n close: () => void\n\n /** Toggle the current state */\n toggle: () => void\n}\n\nexport type UseDisclosureReturnValue = { state: Ref<boolean>; handlers: UseDisclosureHandlers }\n\nexport function useDisclosure(\n initialState: MaybeRefOrGetter<boolean> = false,\n options: MaybeRefOrGetter<UseDisclosureOptions> = {}\n): UseDisclosureReturnValue {\n const state = ref(toValue(initialState))\n\n const getOptions = () => toValue(options)\n\n const open = () => {\n if (!state.value) {\n const opts = getOptions()\n opts.onOpen?.()\n state.value = true\n }\n }\n\n const close = () => {\n if (state.value) {\n const opts = getOptions()\n opts.onClose?.()\n state.value = false\n }\n }\n\n const toggle = () => {\n state.value ? close() : open()\n }\n\n const set = (value: boolean) => {\n state.value = value\n }\n\n return {\n state,\n handlers: {\n open,\n close,\n toggle,\n set,\n },\n }\n}\n"],"mappings":";;;AAgCA,SAAgB,cACd,eAA0C,OAC1C,UAAkD,CAAC,GACzB;CAC1B,MAAM,SAAA,GAAA,IAAA,IAAA,EAAA,GAAA,IAAA,QAAA,CAAoB,YAAY,CAAC;CAEvC,MAAM,oBAAA,GAAA,IAAA,QAAA,CAA2B,OAAO;CAExC,MAAM,aAAa;EACjB,IAAI,CAAC,MAAM,OAAO;GAEhB,WAAG,CAAC,CAAC,SAAS;GACd,MAAM,QAAQ;EAChB;CACF;CAEA,MAAM,cAAc;EAClB,IAAI,MAAM,OAAO;GAEf,WAAG,CAAC,CAAC,UAAU;GACf,MAAM,QAAQ;EAChB;CACF;CAEA,MAAM,eAAe;EACnB,MAAM,QAAQ,MAAM,IAAI,KAAK;CAC/B;CAEA,MAAM,OAAO,UAAmB;EAC9B,MAAM,QAAQ;CAChB;CAEA,OAAO;EACL;EACA,UAAU;GACR;GACA;GACA;GACA;EACF;CACF;AACF"}
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ //#region packages/@cck-ui/hooks/src/use-focus-trap/tabbable.ts
3
+ const FOCUS_SELECTOR = "a, input, select, textarea, button, object, [tabindex]";
4
+ function hidden(element) {
5
+ if (process.env.NODE_ENV === "test") return false;
6
+ return element.style.display === "none";
7
+ }
8
+ function visible(element) {
9
+ if (element.getAttribute("aria-hidden") || element.getAttribute("hidden") || element.getAttribute("type") === "hidden") return false;
10
+ let parent = element;
11
+ while (parent) {
12
+ if (parent === document.body || parent.nodeType === 11) break;
13
+ if (hidden(parent)) return false;
14
+ parent = parent.parentNode;
15
+ }
16
+ return true;
17
+ }
18
+ function getElementTabIndex(element) {
19
+ const tabIndex = element.getAttribute("tabindex");
20
+ return tabIndex === null ? NaN : parseInt(tabIndex, 10);
21
+ }
22
+ function focusable(element) {
23
+ const nodeName = element.nodeName.toLowerCase();
24
+ const isTabIndexNotNaN = !Number.isNaN(getElementTabIndex(element));
25
+ return ([
26
+ "input",
27
+ "select",
28
+ "textarea",
29
+ "button",
30
+ "object"
31
+ ].includes(nodeName) && !element.disabled || (element instanceof HTMLAnchorElement ? element.href || isTabIndexNotNaN : isTabIndexNotNaN)) && visible(element);
32
+ }
33
+ function tabbable(element) {
34
+ const tabIndex = getElementTabIndex(element);
35
+ return (Number.isNaN(tabIndex) || tabIndex >= 0) && focusable(element);
36
+ }
37
+ function findTabbableDescendants(element) {
38
+ return Array.from(element.querySelectorAll(FOCUS_SELECTOR)).filter(tabbable);
39
+ }
40
+ function scopeTab(node, event) {
41
+ const tabbableElements = findTabbableDescendants(node);
42
+ if (!tabbableElements.length) {
43
+ event.preventDefault();
44
+ return;
45
+ }
46
+ const finalElement = tabbableElements[event.shiftKey ? 0 : tabbableElements.length - 1];
47
+ const root = node.getRootNode();
48
+ let leavingFinal = finalElement === root.activeElement || node === root.activeElement;
49
+ const activeEl = root.activeElement;
50
+ if (activeEl.tagName === "INPUT" && activeEl.getAttribute("type") === "radio") leavingFinal = tabbableElements.filter((el) => el.getAttribute("type") === "radio" && el.getAttribute("name") === activeEl.getAttribute("name")).includes(finalElement);
51
+ if (!leavingFinal) return;
52
+ event.preventDefault();
53
+ tabbableElements[event.shiftKey ? tabbableElements.length - 1 : 0]?.focus();
54
+ }
55
+ //#endregion
56
+ exports.FOCUS_SELECTOR = FOCUS_SELECTOR;
57
+ exports.findTabbableDescendants = findTabbableDescendants;
58
+ exports.focusable = focusable;
59
+ exports.scopeTab = scopeTab;
60
+ exports.tabbable = tabbable;
61
+
62
+ //# sourceMappingURL=tabbable.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabbable.cjs","names":[],"sources":["../../src/use-focus-trap/tabbable.ts"],"sourcesContent":["export const FOCUS_SELECTOR = 'a, input, select, textarea, button, object, [tabindex]'\n\nfunction hidden(element: HTMLElement) {\n if (process.env.NODE_ENV === 'test') {\n return false\n }\n return element.style.display === 'none'\n}\n\nfunction visible(element: HTMLElement) {\n const isHidden =\n element.getAttribute('aria-hidden') ||\n element.getAttribute('hidden') ||\n element.getAttribute('type') === 'hidden'\n if (isHidden) {\n return false\n }\n let parent: HTMLElement | null = element\n while (parent) {\n if (parent === document.body || parent.nodeType === 11) {\n break\n }\n if (hidden(parent)) {\n return false\n }\n parent = parent.parentNode as HTMLElement\n }\n return true\n}\n\nfunction getElementTabIndex(element: HTMLElement) {\n const tabIndex = element.getAttribute('tabindex')\n return tabIndex === null ? NaN : parseInt(tabIndex, 10)\n}\n\nexport function focusable(element: HTMLElement) {\n const nodeName = element.nodeName.toLowerCase()\n const isTabIndexNotNaN = !Number.isNaN(getElementTabIndex(element))\n const res =\n (['input', 'select', 'textarea', 'button', 'object'].includes(nodeName) &&\n !(element as HTMLButtonElement).disabled) ||\n (element instanceof HTMLAnchorElement ? element.href || isTabIndexNotNaN : isTabIndexNotNaN)\n return res && visible(element)\n}\n\nexport function tabbable(element: HTMLElement) {\n const tabIndex = getElementTabIndex(element)\n const isTabIndexNaN = Number.isNaN(tabIndex)\n return (isTabIndexNaN || tabIndex >= 0) && focusable(element)\n}\n\nexport function findTabbableDescendants(element: HTMLElement): HTMLElement[] {\n return Array.from(element.querySelectorAll<HTMLElement>(FOCUS_SELECTOR)).filter(tabbable)\n}\n\nexport function scopeTab(node: HTMLElement, event: KeyboardEvent) {\n const tabbableElements = findTabbableDescendants(node)\n if (!tabbableElements.length) {\n event.preventDefault()\n return\n }\n const finalElement = tabbableElements[event.shiftKey ? 0 : tabbableElements.length - 1]\n const root = node.getRootNode() as unknown as DocumentOrShadowRoot\n let leavingFinal = finalElement === root.activeElement || node === root.activeElement\n\n const activeEl = root.activeElement as Element\n const isRadio = activeEl.tagName === 'INPUT' && activeEl.getAttribute('type') === 'radio'\n if (isRadio) {\n const radioGroup = tabbableElements.filter(\n (el) =>\n el.getAttribute('type') === 'radio' &&\n el.getAttribute('name') === activeEl.getAttribute('name')\n )\n leavingFinal = radioGroup.includes(finalElement)\n }\n\n if (!leavingFinal) {\n return\n }\n\n event.preventDefault()\n const target = tabbableElements[event.shiftKey ? tabbableElements.length - 1 : 0]\n target?.focus()\n}\n"],"mappings":";;AAAA,MAAa,iBAAiB;AAE9B,SAAS,OAAO,SAAsB;CACpC,IAAI,QAAQ,IAAI,aAAa,QAC3B,OAAO;CAET,OAAO,QAAQ,MAAM,YAAY;AACnC;AAEA,SAAS,QAAQ,SAAsB;CAKrC,IAHE,QAAQ,aAAa,aAAa,KAClC,QAAQ,aAAa,QAAQ,KAC7B,QAAQ,aAAa,MAAM,MAAM,UAEjC,OAAO;CAET,IAAI,SAA6B;CACjC,OAAO,QAAQ;EACb,IAAI,WAAW,SAAS,QAAQ,OAAO,aAAa,IAClD;EAEF,IAAI,OAAO,MAAM,GACf,OAAO;EAET,SAAS,OAAO;CAClB;CACA,OAAO;AACT;AAEA,SAAS,mBAAmB,SAAsB;CAChD,MAAM,WAAW,QAAQ,aAAa,UAAU;CAChD,OAAO,aAAa,OAAO,MAAM,SAAS,UAAU,EAAE;AACxD;AAEA,SAAgB,UAAU,SAAsB;CAC9C,MAAM,WAAW,QAAQ,SAAS,YAAY;CAC9C,MAAM,mBAAmB,CAAC,OAAO,MAAM,mBAAmB,OAAO,CAAC;CAKlE,QAHG;EAAC;EAAS;EAAU;EAAY;EAAU;CAAQ,CAAC,CAAC,SAAS,QAAQ,KACpE,CAAE,QAA8B,aACjC,mBAAmB,oBAAoB,QAAQ,QAAQ,mBAAmB,sBAC/D,QAAQ,OAAO;AAC/B;AAEA,SAAgB,SAAS,SAAsB;CAC7C,MAAM,WAAW,mBAAmB,OAAO;CAE3C,QADsB,OAAO,MAAM,QACf,KAAK,YAAY,MAAM,UAAU,OAAO;AAC9D;AAEA,SAAgB,wBAAwB,SAAqC;CAC3E,OAAO,MAAM,KAAK,QAAQ,iBAA8B,cAAc,CAAC,CAAC,CAAC,OAAO,QAAQ;AAC1F;AAEA,SAAgB,SAAS,MAAmB,OAAsB;CAChE,MAAM,mBAAmB,wBAAwB,IAAI;CACrD,IAAI,CAAC,iBAAiB,QAAQ;EAC5B,MAAM,eAAe;EACrB;CACF;CACA,MAAM,eAAe,iBAAiB,MAAM,WAAW,IAAI,iBAAiB,SAAS;CACrF,MAAM,OAAO,KAAK,YAAY;CAC9B,IAAI,eAAe,iBAAiB,KAAK,iBAAiB,SAAS,KAAK;CAExE,MAAM,WAAW,KAAK;CAEtB,IADgB,SAAS,YAAY,WAAW,SAAS,aAAa,MAAM,MAAM,SAOhF,eALmB,iBAAiB,QACjC,OACC,GAAG,aAAa,MAAM,MAAM,WAC5B,GAAG,aAAa,MAAM,MAAM,SAAS,aAAa,MAAM,CAEpC,CAAC,CAAC,SAAS,YAAY;CAGjD,IAAI,CAAC,cACH;CAGF,MAAM,eAAe;CAErB,iBADgC,MAAM,WAAW,iBAAiB,SAAS,IAAI,EACzE,EAAE,MAAM;AAChB"}
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ const require_tabbable = require("./tabbable.cjs");
3
+ let vue = require("vue");
4
+ //#region packages/@cck-ui/hooks/src/use-focus-trap/use-focus-trap.ts
5
+ function useFocusTrap(active = true) {
6
+ const trapRef = (0, vue.shallowRef)(null);
7
+ let cleanup = null;
8
+ const focusNode = (node) => {
9
+ let focusElement = node.querySelector("[data-autofocus]");
10
+ if (!focusElement) {
11
+ const children = Array.from(node.querySelectorAll(require_tabbable.FOCUS_SELECTOR));
12
+ focusElement = children.find(require_tabbable.tabbable) || children.find(require_tabbable.focusable) || null;
13
+ if (!focusElement && require_tabbable.focusable(node)) focusElement = node;
14
+ }
15
+ if (focusElement) focusElement.focus({ preventScroll: true });
16
+ else if (process.env.NODE_ENV === "development") console.warn("[CCK-UI/hooks/use-focus-trap] No focusable element found", node);
17
+ };
18
+ const activate = (node) => {
19
+ (0, vue.nextTick)(() => focusNode(node));
20
+ const handler = (event) => {
21
+ if (event.key === "Tab") require_tabbable.scopeTab(node, event);
22
+ };
23
+ document.addEventListener("keydown", handler);
24
+ return () => document.removeEventListener("keydown", handler);
25
+ };
26
+ const deactivate = () => {
27
+ if (cleanup) {
28
+ cleanup();
29
+ cleanup = null;
30
+ }
31
+ };
32
+ (0, vue.watchEffect)((onCleanup) => {
33
+ if (!(0, vue.toValue)(active)) {
34
+ deactivate();
35
+ return;
36
+ }
37
+ const node = trapRef.value;
38
+ if (node) {
39
+ deactivate();
40
+ cleanup = activate(node);
41
+ }
42
+ onCleanup(() => deactivate());
43
+ });
44
+ (0, vue.onBeforeUnmount)(deactivate);
45
+ return trapRef;
46
+ }
47
+ //#endregion
48
+ exports.useFocusTrap = useFocusTrap;
49
+
50
+ //# sourceMappingURL=use-focus-trap.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-focus-trap.cjs","names":["FOCUS_SELECTOR","tabbable","focusable"],"sources":["../../src/use-focus-trap/use-focus-trap.ts"],"sourcesContent":["import { MaybeRefOrGetter, nextTick, onBeforeUnmount, shallowRef, toValue, watchEffect } from 'vue'\nimport { FOCUS_SELECTOR, focusable, scopeTab, tabbable } from './tabbable'\n\nexport function useFocusTrap(active: MaybeRefOrGetter<boolean> = true) {\n const trapRef = shallowRef<HTMLElement | null>(null)\n let cleanup: (() => void) | null = null\n\n const focusNode = (node: HTMLElement) => {\n let focusElement: HTMLElement | null = node.querySelector('[data-autofocus]')\n if (!focusElement) {\n const children = Array.from(node.querySelectorAll<HTMLElement>(FOCUS_SELECTOR))\n focusElement = children.find(tabbable) || children.find(focusable) || null\n if (!focusElement && focusable(node)) {\n focusElement = node\n }\n }\n if (focusElement) {\n focusElement.focus({ preventScroll: true })\n } else if (process.env.NODE_ENV === 'development') {\n console.warn('[CCK-UI/hooks/use-focus-trap] No focusable element found', node)\n }\n }\n\n const activate = (node: HTMLElement) => {\n nextTick(() => focusNode(node))\n const handler = (event: KeyboardEvent) => {\n if (event.key === 'Tab') {\n scopeTab(node, event)\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }\n\n const deactivate = () => {\n if (cleanup) {\n cleanup()\n cleanup = null\n }\n }\n\n watchEffect((onCleanup) => {\n const isActive = toValue(active)\n if (!isActive) {\n deactivate()\n return\n }\n const node = trapRef.value\n if (node) {\n deactivate()\n cleanup = activate(node)\n }\n onCleanup(() => deactivate())\n })\n\n onBeforeUnmount(deactivate)\n\n return trapRef\n}\n"],"mappings":";;;;AAGA,SAAgB,aAAa,SAAoC,MAAM;CACrE,MAAM,WAAA,GAAA,IAAA,WAAA,CAAyC,IAAI;CACnD,IAAI,UAA+B;CAEnC,MAAM,aAAa,SAAsB;EACvC,IAAI,eAAmC,KAAK,cAAc,kBAAkB;EAC5E,IAAI,CAAC,cAAc;GACjB,MAAM,WAAW,MAAM,KAAK,KAAK,iBAA8BA,iBAAAA,cAAc,CAAC;GAC9E,eAAe,SAAS,KAAKC,iBAAAA,QAAQ,KAAK,SAAS,KAAKC,iBAAAA,SAAS,KAAK;GACtE,IAAI,CAAC,gBAAgBA,iBAAAA,UAAU,IAAI,GACjC,eAAe;EAEnB;EACA,IAAI,cACF,aAAa,MAAM,EAAE,eAAe,KAAK,CAAC;OACrC,IAAI,QAAQ,IAAI,aAAa,eAClC,QAAQ,KAAK,4DAA4D,IAAI;CAEjF;CAEA,MAAM,YAAY,SAAsB;EACtC,CAAA,GAAA,IAAA,SAAA,OAAe,UAAU,IAAI,CAAC;EAC9B,MAAM,WAAW,UAAyB;GACxC,IAAI,MAAM,QAAQ,OAChB,iBAAA,SAAS,MAAM,KAAK;EAExB;EACA,SAAS,iBAAiB,WAAW,OAAO;EAC5C,aAAa,SAAS,oBAAoB,WAAW,OAAO;CAC9D;CAEA,MAAM,mBAAmB;EACvB,IAAI,SAAS;GACX,QAAQ;GACR,UAAU;EACZ;CACF;CAEA,CAAA,GAAA,IAAA,YAAA,EAAa,cAAc;EAEzB,IAAI,EAAA,GAAA,IAAA,QAAA,CADqB,MACb,GAAG;GACb,WAAW;GACX;EACF;EACA,MAAM,OAAO,QAAQ;EACrB,IAAI,MAAM;GACR,WAAW;GACX,UAAU,SAAS,IAAI;EACzB;EACA,gBAAgB,WAAW,CAAC;CAC9B,CAAC;CAED,CAAA,GAAA,IAAA,gBAAA,CAAgB,UAAU;CAE1B,OAAO;AACT"}
package/esm/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
+ import { useFocusTrap } from "./use-focus-trap/use-focus-trap.mjs";
1
2
  import { useTimeoutFn } from "./use-timeout-fn/use-timeout-fn.mjs";
2
3
  import { useClipboard } from "./use-clipboard/use-clipboard.mjs";
4
+ import { useDisclosure } from "./use-disclosure/use-disclosure.mjs";
3
5
  import { useId } from "./use-id/use-id.mjs";
4
6
  import { useIsomorphicEffect } from "./use-isomorphic-effect/use-isomorphic-effect.mjs";
5
7
  import { useSplitter } from "./use-splitter/use-splitter.mjs";
6
- export { useClipboard, useId, useIsomorphicEffect, useSplitter, useTimeoutFn };
8
+ export { useClipboard, useDisclosure, useFocusTrap, useId, useIsomorphicEffect, useSplitter, useTimeoutFn };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { ref, toValue } from "vue";
3
+ //#region packages/@cck-ui/hooks/src/use-disclosure/use-disclosure.ts
4
+ function useDisclosure(initialState = false, options = {}) {
5
+ const state = ref(toValue(initialState));
6
+ const getOptions = () => toValue(options);
7
+ const open = () => {
8
+ if (!state.value) {
9
+ getOptions().onOpen?.();
10
+ state.value = true;
11
+ }
12
+ };
13
+ const close = () => {
14
+ if (state.value) {
15
+ getOptions().onClose?.();
16
+ state.value = false;
17
+ }
18
+ };
19
+ const toggle = () => {
20
+ state.value ? close() : open();
21
+ };
22
+ const set = (value) => {
23
+ state.value = value;
24
+ };
25
+ return {
26
+ state,
27
+ handlers: {
28
+ open,
29
+ close,
30
+ toggle,
31
+ set
32
+ }
33
+ };
34
+ }
35
+ //#endregion
36
+ export { useDisclosure };
37
+
38
+ //# sourceMappingURL=use-disclosure.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-disclosure.mjs","names":[],"sources":["../../src/use-disclosure/use-disclosure.ts"],"sourcesContent":["import { MaybeRefOrGetter, ref, Ref, toValue } from 'vue'\n\nexport interface UseDisclosureOptions {\n /**\n * @description Callback invoked when the state becomes true\n */\n onOpen?: () => void\n /**\n * @description Callback invoked when the state becomes false\n */\n onClose?: () => void\n}\n\nexport interface UseDisclosureHandlers {\n /**\n * Directly set the state without triggering callbacks\n * @param value - The target boolean value.\n */\n set: (value: boolean) => void\n\n /** Open the state (no-op if already open) */\n open: () => void\n\n /** Close the state (no-op if already closed) */\n close: () => void\n\n /** Toggle the current state */\n toggle: () => void\n}\n\nexport type UseDisclosureReturnValue = { state: Ref<boolean>; handlers: UseDisclosureHandlers }\n\nexport function useDisclosure(\n initialState: MaybeRefOrGetter<boolean> = false,\n options: MaybeRefOrGetter<UseDisclosureOptions> = {}\n): UseDisclosureReturnValue {\n const state = ref(toValue(initialState))\n\n const getOptions = () => toValue(options)\n\n const open = () => {\n if (!state.value) {\n const opts = getOptions()\n opts.onOpen?.()\n state.value = true\n }\n }\n\n const close = () => {\n if (state.value) {\n const opts = getOptions()\n opts.onClose?.()\n state.value = false\n }\n }\n\n const toggle = () => {\n state.value ? close() : open()\n }\n\n const set = (value: boolean) => {\n state.value = value\n }\n\n return {\n state,\n handlers: {\n open,\n close,\n toggle,\n set,\n },\n }\n}\n"],"mappings":";;;AAgCA,SAAgB,cACd,eAA0C,OAC1C,UAAkD,CAAC,GACzB;CAC1B,MAAM,QAAQ,IAAI,QAAQ,YAAY,CAAC;CAEvC,MAAM,mBAAmB,QAAQ,OAAO;CAExC,MAAM,aAAa;EACjB,IAAI,CAAC,MAAM,OAAO;GAEhB,WAAG,CAAC,CAAC,SAAS;GACd,MAAM,QAAQ;EAChB;CACF;CAEA,MAAM,cAAc;EAClB,IAAI,MAAM,OAAO;GAEf,WAAG,CAAC,CAAC,UAAU;GACf,MAAM,QAAQ;EAChB;CACF;CAEA,MAAM,eAAe;EACnB,MAAM,QAAQ,MAAM,IAAI,KAAK;CAC/B;CAEA,MAAM,OAAO,UAAmB;EAC9B,MAAM,QAAQ;CAChB;CAEA,OAAO;EACL;EACA,UAAU;GACR;GACA;GACA;GACA;EACF;CACF;AACF"}
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ //#region packages/@cck-ui/hooks/src/use-focus-trap/tabbable.ts
3
+ const FOCUS_SELECTOR = "a, input, select, textarea, button, object, [tabindex]";
4
+ function hidden(element) {
5
+ return element.style.display === "none";
6
+ }
7
+ function visible(element) {
8
+ if (element.getAttribute("aria-hidden") || element.getAttribute("hidden") || element.getAttribute("type") === "hidden") return false;
9
+ let parent = element;
10
+ while (parent) {
11
+ if (parent === document.body || parent.nodeType === 11) break;
12
+ if (hidden(parent)) return false;
13
+ parent = parent.parentNode;
14
+ }
15
+ return true;
16
+ }
17
+ function getElementTabIndex(element) {
18
+ const tabIndex = element.getAttribute("tabindex");
19
+ return tabIndex === null ? NaN : parseInt(tabIndex, 10);
20
+ }
21
+ function focusable(element) {
22
+ const nodeName = element.nodeName.toLowerCase();
23
+ const isTabIndexNotNaN = !Number.isNaN(getElementTabIndex(element));
24
+ return ([
25
+ "input",
26
+ "select",
27
+ "textarea",
28
+ "button",
29
+ "object"
30
+ ].includes(nodeName) && !element.disabled || (element instanceof HTMLAnchorElement ? element.href || isTabIndexNotNaN : isTabIndexNotNaN)) && visible(element);
31
+ }
32
+ function tabbable(element) {
33
+ const tabIndex = getElementTabIndex(element);
34
+ return (Number.isNaN(tabIndex) || tabIndex >= 0) && focusable(element);
35
+ }
36
+ function findTabbableDescendants(element) {
37
+ return Array.from(element.querySelectorAll(FOCUS_SELECTOR)).filter(tabbable);
38
+ }
39
+ function scopeTab(node, event) {
40
+ const tabbableElements = findTabbableDescendants(node);
41
+ if (!tabbableElements.length) {
42
+ event.preventDefault();
43
+ return;
44
+ }
45
+ const finalElement = tabbableElements[event.shiftKey ? 0 : tabbableElements.length - 1];
46
+ const root = node.getRootNode();
47
+ let leavingFinal = finalElement === root.activeElement || node === root.activeElement;
48
+ const activeEl = root.activeElement;
49
+ if (activeEl.tagName === "INPUT" && activeEl.getAttribute("type") === "radio") leavingFinal = tabbableElements.filter((el) => el.getAttribute("type") === "radio" && el.getAttribute("name") === activeEl.getAttribute("name")).includes(finalElement);
50
+ if (!leavingFinal) return;
51
+ event.preventDefault();
52
+ tabbableElements[event.shiftKey ? tabbableElements.length - 1 : 0]?.focus();
53
+ }
54
+ //#endregion
55
+ export { FOCUS_SELECTOR, findTabbableDescendants, focusable, scopeTab, tabbable };
56
+
57
+ //# sourceMappingURL=tabbable.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabbable.mjs","names":[],"sources":["../../src/use-focus-trap/tabbable.ts"],"sourcesContent":["export const FOCUS_SELECTOR = 'a, input, select, textarea, button, object, [tabindex]'\n\nfunction hidden(element: HTMLElement) {\n if (process.env.NODE_ENV === 'test') {\n return false\n }\n return element.style.display === 'none'\n}\n\nfunction visible(element: HTMLElement) {\n const isHidden =\n element.getAttribute('aria-hidden') ||\n element.getAttribute('hidden') ||\n element.getAttribute('type') === 'hidden'\n if (isHidden) {\n return false\n }\n let parent: HTMLElement | null = element\n while (parent) {\n if (parent === document.body || parent.nodeType === 11) {\n break\n }\n if (hidden(parent)) {\n return false\n }\n parent = parent.parentNode as HTMLElement\n }\n return true\n}\n\nfunction getElementTabIndex(element: HTMLElement) {\n const tabIndex = element.getAttribute('tabindex')\n return tabIndex === null ? NaN : parseInt(tabIndex, 10)\n}\n\nexport function focusable(element: HTMLElement) {\n const nodeName = element.nodeName.toLowerCase()\n const isTabIndexNotNaN = !Number.isNaN(getElementTabIndex(element))\n const res =\n (['input', 'select', 'textarea', 'button', 'object'].includes(nodeName) &&\n !(element as HTMLButtonElement).disabled) ||\n (element instanceof HTMLAnchorElement ? element.href || isTabIndexNotNaN : isTabIndexNotNaN)\n return res && visible(element)\n}\n\nexport function tabbable(element: HTMLElement) {\n const tabIndex = getElementTabIndex(element)\n const isTabIndexNaN = Number.isNaN(tabIndex)\n return (isTabIndexNaN || tabIndex >= 0) && focusable(element)\n}\n\nexport function findTabbableDescendants(element: HTMLElement): HTMLElement[] {\n return Array.from(element.querySelectorAll<HTMLElement>(FOCUS_SELECTOR)).filter(tabbable)\n}\n\nexport function scopeTab(node: HTMLElement, event: KeyboardEvent) {\n const tabbableElements = findTabbableDescendants(node)\n if (!tabbableElements.length) {\n event.preventDefault()\n return\n }\n const finalElement = tabbableElements[event.shiftKey ? 0 : tabbableElements.length - 1]\n const root = node.getRootNode() as unknown as DocumentOrShadowRoot\n let leavingFinal = finalElement === root.activeElement || node === root.activeElement\n\n const activeEl = root.activeElement as Element\n const isRadio = activeEl.tagName === 'INPUT' && activeEl.getAttribute('type') === 'radio'\n if (isRadio) {\n const radioGroup = tabbableElements.filter(\n (el) =>\n el.getAttribute('type') === 'radio' &&\n el.getAttribute('name') === activeEl.getAttribute('name')\n )\n leavingFinal = radioGroup.includes(finalElement)\n }\n\n if (!leavingFinal) {\n return\n }\n\n event.preventDefault()\n const target = tabbableElements[event.shiftKey ? tabbableElements.length - 1 : 0]\n target?.focus()\n}\n"],"mappings":";;AAAA,MAAa,iBAAiB;AAE9B,SAAS,OAAO,SAAsB;CAIpC,OAAO,QAAQ,MAAM,YAAY;AACnC;AAEA,SAAS,QAAQ,SAAsB;CAKrC,IAHE,QAAQ,aAAa,aAAa,KAClC,QAAQ,aAAa,QAAQ,KAC7B,QAAQ,aAAa,MAAM,MAAM,UAEjC,OAAO;CAET,IAAI,SAA6B;CACjC,OAAO,QAAQ;EACb,IAAI,WAAW,SAAS,QAAQ,OAAO,aAAa,IAClD;EAEF,IAAI,OAAO,MAAM,GACf,OAAO;EAET,SAAS,OAAO;CAClB;CACA,OAAO;AACT;AAEA,SAAS,mBAAmB,SAAsB;CAChD,MAAM,WAAW,QAAQ,aAAa,UAAU;CAChD,OAAO,aAAa,OAAO,MAAM,SAAS,UAAU,EAAE;AACxD;AAEA,SAAgB,UAAU,SAAsB;CAC9C,MAAM,WAAW,QAAQ,SAAS,YAAY;CAC9C,MAAM,mBAAmB,CAAC,OAAO,MAAM,mBAAmB,OAAO,CAAC;CAKlE,QAHG;EAAC;EAAS;EAAU;EAAY;EAAU;CAAQ,CAAC,CAAC,SAAS,QAAQ,KACpE,CAAE,QAA8B,aACjC,mBAAmB,oBAAoB,QAAQ,QAAQ,mBAAmB,sBAC/D,QAAQ,OAAO;AAC/B;AAEA,SAAgB,SAAS,SAAsB;CAC7C,MAAM,WAAW,mBAAmB,OAAO;CAE3C,QADsB,OAAO,MAAM,QACf,KAAK,YAAY,MAAM,UAAU,OAAO;AAC9D;AAEA,SAAgB,wBAAwB,SAAqC;CAC3E,OAAO,MAAM,KAAK,QAAQ,iBAA8B,cAAc,CAAC,CAAC,CAAC,OAAO,QAAQ;AAC1F;AAEA,SAAgB,SAAS,MAAmB,OAAsB;CAChE,MAAM,mBAAmB,wBAAwB,IAAI;CACrD,IAAI,CAAC,iBAAiB,QAAQ;EAC5B,MAAM,eAAe;EACrB;CACF;CACA,MAAM,eAAe,iBAAiB,MAAM,WAAW,IAAI,iBAAiB,SAAS;CACrF,MAAM,OAAO,KAAK,YAAY;CAC9B,IAAI,eAAe,iBAAiB,KAAK,iBAAiB,SAAS,KAAK;CAExE,MAAM,WAAW,KAAK;CAEtB,IADgB,SAAS,YAAY,WAAW,SAAS,aAAa,MAAM,MAAM,SAOhF,eALmB,iBAAiB,QACjC,OACC,GAAG,aAAa,MAAM,MAAM,WAC5B,GAAG,aAAa,MAAM,MAAM,SAAS,aAAa,MAAM,CAEpC,CAAC,CAAC,SAAS,YAAY;CAGjD,IAAI,CAAC,cACH;CAGF,MAAM,eAAe;CAErB,iBADgC,MAAM,WAAW,iBAAiB,SAAS,IAAI,EACzE,EAAE,MAAM;AAChB"}
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { FOCUS_SELECTOR, focusable, scopeTab, tabbable } from "./tabbable.mjs";
3
+ import { nextTick, onBeforeUnmount, shallowRef, toValue, watchEffect } from "vue";
4
+ //#region packages/@cck-ui/hooks/src/use-focus-trap/use-focus-trap.ts
5
+ function useFocusTrap(active = true) {
6
+ const trapRef = shallowRef(null);
7
+ let cleanup = null;
8
+ const focusNode = (node) => {
9
+ let focusElement = node.querySelector("[data-autofocus]");
10
+ if (!focusElement) {
11
+ const children = Array.from(node.querySelectorAll(FOCUS_SELECTOR));
12
+ focusElement = children.find(tabbable) || children.find(focusable) || null;
13
+ if (!focusElement && focusable(node)) focusElement = node;
14
+ }
15
+ if (focusElement) focusElement.focus({ preventScroll: true });
16
+ else console.warn("[CCK-UI/hooks/use-focus-trap] No focusable element found", node);
17
+ };
18
+ const activate = (node) => {
19
+ nextTick(() => focusNode(node));
20
+ const handler = (event) => {
21
+ if (event.key === "Tab") scopeTab(node, event);
22
+ };
23
+ document.addEventListener("keydown", handler);
24
+ return () => document.removeEventListener("keydown", handler);
25
+ };
26
+ const deactivate = () => {
27
+ if (cleanup) {
28
+ cleanup();
29
+ cleanup = null;
30
+ }
31
+ };
32
+ watchEffect((onCleanup) => {
33
+ if (!toValue(active)) {
34
+ deactivate();
35
+ return;
36
+ }
37
+ const node = trapRef.value;
38
+ if (node) {
39
+ deactivate();
40
+ cleanup = activate(node);
41
+ }
42
+ onCleanup(() => deactivate());
43
+ });
44
+ onBeforeUnmount(deactivate);
45
+ return trapRef;
46
+ }
47
+ //#endregion
48
+ export { useFocusTrap };
49
+
50
+ //# sourceMappingURL=use-focus-trap.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-focus-trap.mjs","names":[],"sources":["../../src/use-focus-trap/use-focus-trap.ts"],"sourcesContent":["import { MaybeRefOrGetter, nextTick, onBeforeUnmount, shallowRef, toValue, watchEffect } from 'vue'\nimport { FOCUS_SELECTOR, focusable, scopeTab, tabbable } from './tabbable'\n\nexport function useFocusTrap(active: MaybeRefOrGetter<boolean> = true) {\n const trapRef = shallowRef<HTMLElement | null>(null)\n let cleanup: (() => void) | null = null\n\n const focusNode = (node: HTMLElement) => {\n let focusElement: HTMLElement | null = node.querySelector('[data-autofocus]')\n if (!focusElement) {\n const children = Array.from(node.querySelectorAll<HTMLElement>(FOCUS_SELECTOR))\n focusElement = children.find(tabbable) || children.find(focusable) || null\n if (!focusElement && focusable(node)) {\n focusElement = node\n }\n }\n if (focusElement) {\n focusElement.focus({ preventScroll: true })\n } else if (process.env.NODE_ENV === 'development') {\n console.warn('[CCK-UI/hooks/use-focus-trap] No focusable element found', node)\n }\n }\n\n const activate = (node: HTMLElement) => {\n nextTick(() => focusNode(node))\n const handler = (event: KeyboardEvent) => {\n if (event.key === 'Tab') {\n scopeTab(node, event)\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }\n\n const deactivate = () => {\n if (cleanup) {\n cleanup()\n cleanup = null\n }\n }\n\n watchEffect((onCleanup) => {\n const isActive = toValue(active)\n if (!isActive) {\n deactivate()\n return\n }\n const node = trapRef.value\n if (node) {\n deactivate()\n cleanup = activate(node)\n }\n onCleanup(() => deactivate())\n })\n\n onBeforeUnmount(deactivate)\n\n return trapRef\n}\n"],"mappings":";;;;AAGA,SAAgB,aAAa,SAAoC,MAAM;CACrE,MAAM,UAAU,WAA+B,IAAI;CACnD,IAAI,UAA+B;CAEnC,MAAM,aAAa,SAAsB;EACvC,IAAI,eAAmC,KAAK,cAAc,kBAAkB;EAC5E,IAAI,CAAC,cAAc;GACjB,MAAM,WAAW,MAAM,KAAK,KAAK,iBAA8B,cAAc,CAAC;GAC9E,eAAe,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,KAAK;GACtE,IAAI,CAAC,gBAAgB,UAAU,IAAI,GACjC,eAAe;EAEnB;EACA,IAAI,cACF,aAAa,MAAM,EAAE,eAAe,KAAK,CAAC;OAE1C,QAAQ,KAAK,4DAA4D,IAAI;CAEjF;CAEA,MAAM,YAAY,SAAsB;EACtC,eAAe,UAAU,IAAI,CAAC;EAC9B,MAAM,WAAW,UAAyB;GACxC,IAAI,MAAM,QAAQ,OAChB,SAAS,MAAM,KAAK;EAExB;EACA,SAAS,iBAAiB,WAAW,OAAO;EAC5C,aAAa,SAAS,oBAAoB,WAAW,OAAO;CAC9D;CAEA,MAAM,mBAAmB;EACvB,IAAI,SAAS;GACX,QAAQ;GACR,UAAU;EACZ;CACF;CAEA,aAAa,cAAc;EAEzB,IAAI,CADa,QAAQ,MACb,GAAG;GACb,WAAW;GACX;EACF;EACA,MAAM,OAAO,QAAQ;EACrB,IAAI,MAAM;GACR,WAAW;GACX,UAAU,SAAS,IAAI;EACzB;EACA,gBAAgB,WAAW,CAAC;CAC9B,CAAC;CAED,gBAAgB,UAAU;CAE1B,OAAO;AACT"}
package/lib/index.d.mts CHANGED
@@ -1,6 +1,9 @@
1
+ export { useFocusTrap } from './use-focus-trap/use-focus-trap';
1
2
  export { useClipboard } from './use-clipboard/use-clipboard';
3
+ export { useDisclosure } from './use-disclosure/use-disclosure';
2
4
  export { useId } from './use-id/use-id';
3
5
  export { useIsomorphicEffect } from './use-isomorphic-effect/use-isomorphic-effect';
4
6
  export { useSplitter } from './use-splitter/use-splitter';
5
7
  export { useTimeoutFn } from './use-timeout-fn/use-timeout-fn';
8
+ export type { UseDisclosureHandlers, UseDisclosureOptions, UseDisclosureReturnValue, } from './use-disclosure/use-disclosure';
6
9
  export type { UseSplitterPanel, UseSplitterOptions, UseSplitterReturnValue, UseSplitterRedistributeInput, UseSplitterRedistributeFn, UseSplitterResolvedPanel, SplitterPaneSize, SplitterStep, } from './use-splitter/use-splitter';
package/lib/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
+ export { useFocusTrap } from './use-focus-trap/use-focus-trap';
1
2
  export { useClipboard } from './use-clipboard/use-clipboard';
3
+ export { useDisclosure } from './use-disclosure/use-disclosure';
2
4
  export { useId } from './use-id/use-id';
3
5
  export { useIsomorphicEffect } from './use-isomorphic-effect/use-isomorphic-effect';
4
6
  export { useSplitter } from './use-splitter/use-splitter';
5
7
  export { useTimeoutFn } from './use-timeout-fn/use-timeout-fn';
8
+ export type { UseDisclosureHandlers, UseDisclosureOptions, UseDisclosureReturnValue, } from './use-disclosure/use-disclosure';
6
9
  export type { UseSplitterPanel, UseSplitterOptions, UseSplitterReturnValue, UseSplitterRedistributeInput, UseSplitterRedistributeFn, UseSplitterResolvedPanel, SplitterPaneSize, SplitterStep, } from './use-splitter/use-splitter';
@@ -0,0 +1,29 @@
1
+ import { MaybeRefOrGetter, Ref } from 'vue';
2
+ export interface UseDisclosureOptions {
3
+ /**
4
+ * @description Callback invoked when the state becomes true
5
+ */
6
+ onOpen?: () => void;
7
+ /**
8
+ * @description Callback invoked when the state becomes false
9
+ */
10
+ onClose?: () => void;
11
+ }
12
+ export interface UseDisclosureHandlers {
13
+ /**
14
+ * Directly set the state without triggering callbacks
15
+ * @param value - The target boolean value.
16
+ */
17
+ set: (value: boolean) => void;
18
+ /** Open the state (no-op if already open) */
19
+ open: () => void;
20
+ /** Close the state (no-op if already closed) */
21
+ close: () => void;
22
+ /** Toggle the current state */
23
+ toggle: () => void;
24
+ }
25
+ export type UseDisclosureReturnValue = {
26
+ state: Ref<boolean>;
27
+ handlers: UseDisclosureHandlers;
28
+ };
29
+ export declare function useDisclosure(initialState?: MaybeRefOrGetter<boolean>, options?: MaybeRefOrGetter<UseDisclosureOptions>): UseDisclosureReturnValue;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare const FOCUS_SELECTOR = "a, input, select, textarea, button, object, [tabindex]";
2
+ export declare function focusable(element: HTMLElement): boolean | "";
3
+ export declare function tabbable(element: HTMLElement): boolean | "";
4
+ export declare function findTabbableDescendants(element: HTMLElement): HTMLElement[];
5
+ export declare function scopeTab(node: HTMLElement, event: KeyboardEvent): void;
@@ -0,0 +1,2 @@
1
+ import { MaybeRefOrGetter } from 'vue';
2
+ export declare function useFocusTrap(active?: MaybeRefOrGetter<boolean>): import("vue").ShallowRef<HTMLElement | null, HTMLElement | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cck-ui/hooks",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "A collection of hooks for state and UI management",
5
5
  "homepage": "https://cck-ui.jackatlas.xyz",
6
6
  "license": "ISC",