@blocdigital/usetoplayerelement 0.0.2 → 0.0.4

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.
@@ -4,16 +4,16 @@ exports.default = useTopLayerElements;
4
4
  const react_1 = require("react");
5
5
  const topLayerElements = new Set();
6
6
  // Custom event to notify elements when they are added/removed from the top layer
7
- const topLayerEvent = (inTopLayer) => new CustomEvent("topLayer", { bubbles: true, detail: { inTopLayer } });
8
- if (typeof document !== "undefined") {
7
+ const topLayerEvent = (inTopLayer) => new CustomEvent('topLayer', { bubbles: true, detail: { inTopLayer } });
8
+ if (typeof document !== 'undefined') {
9
9
  // Listen for elements being added/removed from the top layer
10
- document.addEventListener("toggle", ({ target }) => {
10
+ document.addEventListener('toggle', ({ target }) => {
11
11
  if (!target)
12
12
  return;
13
13
  const el = target;
14
- if (!(el instanceof HTMLDialogElement || el.hasAttribute("popover")))
14
+ if (!(el instanceof HTMLDialogElement || el.hasAttribute('popover')))
15
15
  return;
16
- if (el.matches(":modal, :popover-open") && document.contains(el)) {
16
+ if (el.matches(':modal, :popover-open') && document.contains(el)) {
17
17
  topLayerElements.add(el);
18
18
  el.dispatchEvent(topLayerEvent(true));
19
19
  }
@@ -24,31 +24,76 @@ if (typeof document !== "undefined") {
24
24
  }, { capture: true });
25
25
  // MutationObserver for automatic cleanup
26
26
  const observer = new MutationObserver((mutations) => {
27
- const nodes = mutations.flatMap(({ removedNodes }) => [...removedNodes]);
27
+ const nodes = mutations.flatMap(({ removedNodes }) => Array.from(removedNodes).filter((node) => node instanceof HTMLElement));
28
28
  for (const node of nodes)
29
29
  if (topLayerElements.delete(node))
30
30
  document.dispatchEvent(topLayerEvent(false));
31
31
  });
32
32
  observer.observe(document.body, { childList: true, subtree: true });
33
33
  }
34
+ /**
35
+ * React hook to track and interact with elements in the "top layer" (such as dialogs and popovers).
36
+ *
37
+ * This hook provides a ref to attach to your element, and returns information about its position
38
+ * and presence in the top layer, as well as the current list of top layer elements.
39
+ *
40
+ * The top layer is determined by listening for the `toggle` event on dialogs and popovers,
41
+ * and by observing DOM mutations for automatic cleanup.
42
+ *
43
+ * @template T - The type of HTMLElement to track.
44
+ * @returns {useTopLayerElementsReturn<T>} An object containing:
45
+ * - `ref`: React ref to attach to your element.
46
+ * - `topElement`: The topmost element in the top layer.
47
+ * - `topDialog`: The topmost dialog element in the top layer.
48
+ * - `isInTopLayer`: Whether your element is currently in the top layer.
49
+ * - `isTopElement`: Whether your element is the topmost element in the top layer.
50
+ * - `topLayerList`: Array of all elements currently in the top layer.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import useTopLayerElements from './useTopLayerElement';
55
+ *
56
+ * function MyDialog() {
57
+ * const { ref, isInTopLayer, isTopElement } = useTopLayerElements<HTMLDialogElement>();
58
+ *
59
+ * return (
60
+ * <dialog ref={ref} open>
61
+ * {isInTopLayer && <span>I'm in the top layer!</span>}
62
+ * {isTopElement && <span>I'm the topmost dialog!</span>}
63
+ * </dialog>
64
+ * );
65
+ * }
66
+ * ```
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * import useTopLayerElements from './useTopLayerElement';
71
+ *
72
+ * function MyPopover() {
73
+ * const { ref, topLayerList } = useTopLayerElements<HTMLDivElement>();
74
+ *
75
+ * return (
76
+ * <div ref={ref} popover="auto">
77
+ * <span>Current top layer count: {topLayerList.length}</span>
78
+ * </div>
79
+ * );
80
+ * }
81
+ * ```
82
+ */
34
83
  function useTopLayerElements() {
35
84
  const ref = (0, react_1.useRef)(null);
36
- const [topLayerList, setTopLayerList] = (0, react_1.useState)([
37
- ...topLayerElements,
38
- ]);
85
+ const [topLayerList, setTopLayerList] = (0, react_1.useState)([...topLayerElements]);
39
86
  const derivedState = (0, react_1.useMemo)(() => {
40
87
  const topElement = topLayerList.length ? topLayerList.at(-1) || null : null;
41
- const topDialog = topLayerList.findLast((el) => el.tagName === "DIALOG") || null;
88
+ const topDialog = topLayerList.findLast((el) => el.tagName === 'DIALOG') || null;
42
89
  const isTopElement = ref.current ? topElement === ref.current : false;
43
- const isInTopLayer = ref.current
44
- ? topLayerList.includes(ref.current)
45
- : false;
90
+ const isInTopLayer = ref.current ? topLayerList.includes(ref.current) : false;
46
91
  return { topElement, topDialog, isTopElement, isInTopLayer };
47
92
  }, [topLayerList]);
48
93
  // Listen for top layer changes
49
94
  (0, react_1.useEffect)(() => {
50
95
  const ac = new AbortController();
51
- document.addEventListener("topLayer", () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
96
+ document.addEventListener('topLayer', () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
52
97
  return () => ac.abort();
53
98
  }, []);
54
99
  return (0, react_1.useMemo)(() => ({ ref, ...derivedState, topLayerList }), [derivedState, topLayerList]);
@@ -1,16 +1,16 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  const topLayerElements = new Set();
3
3
  // Custom event to notify elements when they are added/removed from the top layer
4
- const topLayerEvent = (inTopLayer) => new CustomEvent("topLayer", { bubbles: true, detail: { inTopLayer } });
5
- if (typeof document !== "undefined") {
4
+ const topLayerEvent = (inTopLayer) => new CustomEvent('topLayer', { bubbles: true, detail: { inTopLayer } });
5
+ if (typeof document !== 'undefined') {
6
6
  // Listen for elements being added/removed from the top layer
7
- document.addEventListener("toggle", ({ target }) => {
7
+ document.addEventListener('toggle', ({ target }) => {
8
8
  if (!target)
9
9
  return;
10
10
  const el = target;
11
- if (!(el instanceof HTMLDialogElement || el.hasAttribute("popover")))
11
+ if (!(el instanceof HTMLDialogElement || el.hasAttribute('popover')))
12
12
  return;
13
- if (el.matches(":modal, :popover-open") && document.contains(el)) {
13
+ if (el.matches(':modal, :popover-open') && document.contains(el)) {
14
14
  topLayerElements.add(el);
15
15
  el.dispatchEvent(topLayerEvent(true));
16
16
  }
@@ -21,31 +21,76 @@ if (typeof document !== "undefined") {
21
21
  }, { capture: true });
22
22
  // MutationObserver for automatic cleanup
23
23
  const observer = new MutationObserver((mutations) => {
24
- const nodes = mutations.flatMap(({ removedNodes }) => [...removedNodes]);
24
+ const nodes = mutations.flatMap(({ removedNodes }) => Array.from(removedNodes).filter((node) => node instanceof HTMLElement));
25
25
  for (const node of nodes)
26
26
  if (topLayerElements.delete(node))
27
27
  document.dispatchEvent(topLayerEvent(false));
28
28
  });
29
29
  observer.observe(document.body, { childList: true, subtree: true });
30
30
  }
31
+ /**
32
+ * React hook to track and interact with elements in the "top layer" (such as dialogs and popovers).
33
+ *
34
+ * This hook provides a ref to attach to your element, and returns information about its position
35
+ * and presence in the top layer, as well as the current list of top layer elements.
36
+ *
37
+ * The top layer is determined by listening for the `toggle` event on dialogs and popovers,
38
+ * and by observing DOM mutations for automatic cleanup.
39
+ *
40
+ * @template T - The type of HTMLElement to track.
41
+ * @returns {useTopLayerElementsReturn<T>} An object containing:
42
+ * - `ref`: React ref to attach to your element.
43
+ * - `topElement`: The topmost element in the top layer.
44
+ * - `topDialog`: The topmost dialog element in the top layer.
45
+ * - `isInTopLayer`: Whether your element is currently in the top layer.
46
+ * - `isTopElement`: Whether your element is the topmost element in the top layer.
47
+ * - `topLayerList`: Array of all elements currently in the top layer.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * import useTopLayerElements from './useTopLayerElement';
52
+ *
53
+ * function MyDialog() {
54
+ * const { ref, isInTopLayer, isTopElement } = useTopLayerElements<HTMLDialogElement>();
55
+ *
56
+ * return (
57
+ * <dialog ref={ref} open>
58
+ * {isInTopLayer && <span>I'm in the top layer!</span>}
59
+ * {isTopElement && <span>I'm the topmost dialog!</span>}
60
+ * </dialog>
61
+ * );
62
+ * }
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * import useTopLayerElements from './useTopLayerElement';
68
+ *
69
+ * function MyPopover() {
70
+ * const { ref, topLayerList } = useTopLayerElements<HTMLDivElement>();
71
+ *
72
+ * return (
73
+ * <div ref={ref} popover="auto">
74
+ * <span>Current top layer count: {topLayerList.length}</span>
75
+ * </div>
76
+ * );
77
+ * }
78
+ * ```
79
+ */
31
80
  export default function useTopLayerElements() {
32
81
  const ref = useRef(null);
33
- const [topLayerList, setTopLayerList] = useState([
34
- ...topLayerElements,
35
- ]);
82
+ const [topLayerList, setTopLayerList] = useState([...topLayerElements]);
36
83
  const derivedState = useMemo(() => {
37
84
  const topElement = topLayerList.length ? topLayerList.at(-1) || null : null;
38
- const topDialog = topLayerList.findLast((el) => el.tagName === "DIALOG") || null;
85
+ const topDialog = topLayerList.findLast((el) => el.tagName === 'DIALOG') || null;
39
86
  const isTopElement = ref.current ? topElement === ref.current : false;
40
- const isInTopLayer = ref.current
41
- ? topLayerList.includes(ref.current)
42
- : false;
87
+ const isInTopLayer = ref.current ? topLayerList.includes(ref.current) : false;
43
88
  return { topElement, topDialog, isTopElement, isInTopLayer };
44
89
  }, [topLayerList]);
45
90
  // Listen for top layer changes
46
91
  useEffect(() => {
47
92
  const ac = new AbortController();
48
- document.addEventListener("topLayer", () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
93
+ document.addEventListener('topLayer', () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
49
94
  return () => ac.abort();
50
95
  }, []);
51
96
  return useMemo(() => ({ ref, ...derivedState, topLayerList }), [derivedState, topLayerList]);
@@ -1,9 +1,58 @@
1
- export interface useTopLayerElementsReturn {
2
- ref: React.RefObject<HTMLElement | null>;
1
+ export interface useTopLayerElementsReturn<T extends HTMLElement> {
2
+ ref: React.RefObject<T | null>;
3
3
  topElement: HTMLElement | null;
4
4
  topDialog: HTMLElement | null;
5
5
  isInTopLayer: boolean;
6
6
  isTopElement: boolean;
7
7
  topLayerList: HTMLElement[];
8
8
  }
9
- export default function useTopLayerElements(): useTopLayerElementsReturn;
9
+ /**
10
+ * React hook to track and interact with elements in the "top layer" (such as dialogs and popovers).
11
+ *
12
+ * This hook provides a ref to attach to your element, and returns information about its position
13
+ * and presence in the top layer, as well as the current list of top layer elements.
14
+ *
15
+ * The top layer is determined by listening for the `toggle` event on dialogs and popovers,
16
+ * and by observing DOM mutations for automatic cleanup.
17
+ *
18
+ * @template T - The type of HTMLElement to track.
19
+ * @returns {useTopLayerElementsReturn<T>} An object containing:
20
+ * - `ref`: React ref to attach to your element.
21
+ * - `topElement`: The topmost element in the top layer.
22
+ * - `topDialog`: The topmost dialog element in the top layer.
23
+ * - `isInTopLayer`: Whether your element is currently in the top layer.
24
+ * - `isTopElement`: Whether your element is the topmost element in the top layer.
25
+ * - `topLayerList`: Array of all elements currently in the top layer.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * import useTopLayerElements from './useTopLayerElement';
30
+ *
31
+ * function MyDialog() {
32
+ * const { ref, isInTopLayer, isTopElement } = useTopLayerElements<HTMLDialogElement>();
33
+ *
34
+ * return (
35
+ * <dialog ref={ref} open>
36
+ * {isInTopLayer && <span>I'm in the top layer!</span>}
37
+ * {isTopElement && <span>I'm the topmost dialog!</span>}
38
+ * </dialog>
39
+ * );
40
+ * }
41
+ * ```
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * import useTopLayerElements from './useTopLayerElement';
46
+ *
47
+ * function MyPopover() {
48
+ * const { ref, topLayerList } = useTopLayerElements<HTMLDivElement>();
49
+ *
50
+ * return (
51
+ * <div ref={ref} popover="auto">
52
+ * <span>Current top layer count: {topLayerList.length}</span>
53
+ * </div>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export default function useTopLayerElements<T extends HTMLElement>(): useTopLayerElementsReturn<T>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blocdigital/usetoplayerelement",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "description": "React hook for monitoring the top layer",
6
6
  "author": "Bloc Digital <web@bloc.digital>",
7
7
  "license": "MIT",
@@ -49,33 +49,29 @@
49
49
  "preview": "vite preview",
50
50
  "test": "jest"
51
51
  },
52
- "dependencies": {
53
- "react": "^19.0.0",
54
- "react-dom": "^19.0.0"
55
- },
56
52
  "devDependencies": {
57
- "@babel/core": "^7.26.9",
58
- "@babel/preset-env": "^7.26.9",
59
- "@babel/preset-typescript": "^7.26.0",
60
- "@testing-library/react": "^16.2.0",
61
- "@types/jest": "^29.5.14",
62
- "@types/react": "^19.0.10",
63
- "@types/react-dom": "^19.0.4",
64
- "@typescript-eslint/eslint-plugin": "^8.25.0",
65
- "@typescript-eslint/parser": "^8.25.0",
66
- "@vitejs/plugin-react-swc": "^3.8.0",
67
- "babel-jest": "^29.7.0",
68
- "eslint": "^9.21.0",
69
- "eslint-config-prettier": "^10.0.2",
70
- "eslint-plugin-prettier": "^5.2.3",
71
- "eslint-plugin-react-hooks": "^5.1.0",
72
- "eslint-plugin-react-refresh": "^0.4.19",
73
- "globals": "^16.0.0",
74
- "jest": "^29.7.0",
75
- "jest-environment-jsdom": "^29.7.0",
76
- "prettier": "^3.5.2",
77
- "typescript": "^5.7.3",
78
- "vite": "^6.2.0"
53
+ "@babel/core": "^7.28.3",
54
+ "@babel/preset-env": "^7.28.3",
55
+ "@babel/preset-typescript": "^7.27.1",
56
+ "@testing-library/react": "^16.3.0",
57
+ "@types/jest": "^30.0.0",
58
+ "@types/react": "^19.1.11",
59
+ "@types/react-dom": "^19.1.8",
60
+ "@typescript-eslint/eslint-plugin": "^8.41.0",
61
+ "@typescript-eslint/parser": "^8.41.0",
62
+ "@vitejs/plugin-react-swc": "^4.0.1",
63
+ "babel-jest": "^30.0.5",
64
+ "eslint": "^9.34.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "eslint-plugin-prettier": "^5.5.4",
67
+ "eslint-plugin-react-hooks": "^5.2.0",
68
+ "eslint-plugin-react-refresh": "^0.4.20",
69
+ "globals": "^16.3.0",
70
+ "jest": "^30.0.5",
71
+ "jest-environment-jsdom": "^30.0.5",
72
+ "prettier": "^3.6.2",
73
+ "typescript": "^5.9.2",
74
+ "vite": "^7.1.3"
79
75
  },
80
76
  "bugs": {
81
77
  "url": "https://github.com/bloc-digital/usetoplayerelement/issues"