@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(
|
|
8
|
-
if (typeof document !==
|
|
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(
|
|
10
|
+
document.addEventListener('toggle', ({ target }) => {
|
|
11
11
|
if (!target)
|
|
12
12
|
return;
|
|
13
13
|
const el = target;
|
|
14
|
-
if (!(el instanceof HTMLDialogElement || el.hasAttribute(
|
|
14
|
+
if (!(el instanceof HTMLDialogElement || el.hasAttribute('popover')))
|
|
15
15
|
return;
|
|
16
|
-
if (el.matches(
|
|
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 }) =>
|
|
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 ===
|
|
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(
|
|
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
|
|
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(
|
|
5
|
-
if (typeof document !==
|
|
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(
|
|
7
|
+
document.addEventListener('toggle', ({ target }) => {
|
|
8
8
|
if (!target)
|
|
9
9
|
return;
|
|
10
10
|
const el = target;
|
|
11
|
-
if (!(el instanceof HTMLDialogElement || el.hasAttribute(
|
|
11
|
+
if (!(el instanceof HTMLDialogElement || el.hasAttribute('popover')))
|
|
12
12
|
return;
|
|
13
|
-
if (el.matches(
|
|
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 }) =>
|
|
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 ===
|
|
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(
|
|
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<
|
|
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
|
-
|
|
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.
|
|
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.
|
|
58
|
-
"@babel/preset-env": "^7.
|
|
59
|
-
"@babel/preset-typescript": "^7.
|
|
60
|
-
"@testing-library/react": "^16.
|
|
61
|
-
"@types/jest": "^
|
|
62
|
-
"@types/react": "^19.
|
|
63
|
-
"@types/react-dom": "^19.
|
|
64
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
65
|
-
"@typescript-eslint/parser": "^8.
|
|
66
|
-
"@vitejs/plugin-react-swc": "^
|
|
67
|
-
"babel-jest": "^
|
|
68
|
-
"eslint": "^9.
|
|
69
|
-
"eslint-config-prettier": "^10.
|
|
70
|
-
"eslint-plugin-prettier": "^5.
|
|
71
|
-
"eslint-plugin-react-hooks": "^5.
|
|
72
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
73
|
-
"globals": "^16.
|
|
74
|
-
"jest": "^
|
|
75
|
-
"jest-environment-jsdom": "^
|
|
76
|
-
"prettier": "^3.
|
|
77
|
-
"typescript": "^5.
|
|
78
|
-
"vite": "^
|
|
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"
|