@blocdigital/usetoplayerelement 0.0.1
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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/useTopLayerElement.js +55 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/useTopLayerElement.js +52 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/useTopLayerElement.d.ts +9 -0
- package/package.json +83 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Bloc Digital
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# @blocdigital/usetoplayerelement
|
2
|
+
|
3
|
+
> React hook for monitoring the top layer.
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
```bash
|
8
|
+
npm install --save @blocdigital/usetoplayerelement
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
### Hook usage
|
14
|
+
|
15
|
+
```tsx
|
16
|
+
import { useEffect, createPortal } from 'react';
|
17
|
+
|
18
|
+
// Hooks
|
19
|
+
import useTopLayerElement from '@blocdigital/usetoplayerelement';
|
20
|
+
|
21
|
+
export default function GreatContent({ show = false, className, children }) {
|
22
|
+
// Get the full list of elements in the top layer
|
23
|
+
const { topLayerList } = useTopLayerElement();
|
24
|
+
|
25
|
+
// If there is anything in the top layer prevent scrolling
|
26
|
+
return <SomeComponent preventScroll={Boolean(topLayerList.length)} />;
|
27
|
+
}
|
28
|
+
```
|
29
|
+
|
30
|
+
Example for moving popover element so it is always interactable and on top
|
31
|
+
_**Warning:** It's possible to get caught in an infinite loop if multiple elements are fighting for the top spot._
|
32
|
+
|
33
|
+
```tsx
|
34
|
+
import { useEffect } from 'react';
|
35
|
+
import { createPortal } from 'react-dom';
|
36
|
+
|
37
|
+
// Hooks
|
38
|
+
import useTopLayerElement from '@blocdigital/usetoplayerelement';
|
39
|
+
|
40
|
+
export default function GreatContent({ show = false, children }) {
|
41
|
+
// We get a ref from useTopLayerElement to do specific checks
|
42
|
+
const { ref, isTopElement, topDialog } = useTopLayerElement();
|
43
|
+
|
44
|
+
// Handle show/hide popover
|
45
|
+
useEffect(() => {
|
46
|
+
const { current: el } = ref;
|
47
|
+
|
48
|
+
if (!el) return;
|
49
|
+
|
50
|
+
show ? el.showPopover() : el.hidePopover();
|
51
|
+
}, [show]);
|
52
|
+
|
53
|
+
// Keep element on top
|
54
|
+
useEffect(() => {
|
55
|
+
const { current: el } = ref;
|
56
|
+
|
57
|
+
if (!el || !show || isTopElement) return;
|
58
|
+
|
59
|
+
// Move the popover back on top
|
60
|
+
el.hidePopover();
|
61
|
+
el.showPopover();
|
62
|
+
}, [show, isTopElement]);
|
63
|
+
|
64
|
+
return createPortal(
|
65
|
+
<div ref={ref} popover="manual">
|
66
|
+
{children}
|
67
|
+
</div>,
|
68
|
+
topDialog || document.body,
|
69
|
+
);
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
#### Event
|
74
|
+
|
75
|
+
As an added bonus a `topLayer` event is now fired, you can listen for it on the document and it will tell you which element has entered or left the top layer provided the element is still in the dom. If the element is no longer in the dom the target will be document.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.default = void 0;
|
7
|
+
var useTopLayerElement_1 = require("./useTopLayerElement");
|
8
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(useTopLayerElement_1).default; } });
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.default = useTopLayerElements;
|
4
|
+
const react_1 = require("react");
|
5
|
+
const topLayerElements = new Set();
|
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") {
|
9
|
+
// Listen for elements being added/removed from the top layer
|
10
|
+
document.addEventListener("toggle", ({ target }) => {
|
11
|
+
if (!target)
|
12
|
+
return;
|
13
|
+
const el = target;
|
14
|
+
if (!(el instanceof HTMLDialogElement || el.hasAttribute("popover")))
|
15
|
+
return;
|
16
|
+
if (el.matches(":modal, :popover-open") && document.contains(el)) {
|
17
|
+
topLayerElements.add(el);
|
18
|
+
el.dispatchEvent(topLayerEvent(true));
|
19
|
+
}
|
20
|
+
else {
|
21
|
+
if (topLayerElements.delete(el))
|
22
|
+
el.dispatchEvent(topLayerEvent(false));
|
23
|
+
}
|
24
|
+
}, { capture: true });
|
25
|
+
// MutationObserver for automatic cleanup
|
26
|
+
const observer = new MutationObserver((mutations) => {
|
27
|
+
const nodes = mutations.flatMap(({ removedNodes }) => [...removedNodes]);
|
28
|
+
for (const node of nodes)
|
29
|
+
if (topLayerElements.delete(node))
|
30
|
+
document.dispatchEvent(topLayerEvent(false));
|
31
|
+
});
|
32
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
33
|
+
}
|
34
|
+
function useTopLayerElements() {
|
35
|
+
const ref = (0, react_1.useRef)(null);
|
36
|
+
const [topLayerList, setTopLayerList] = (0, react_1.useState)([
|
37
|
+
...topLayerElements,
|
38
|
+
]);
|
39
|
+
const derivedState = (0, react_1.useMemo)(() => {
|
40
|
+
const topElement = topLayerList.length ? topLayerList.at(-1) || null : null;
|
41
|
+
const topDialog = topLayerList.findLast((el) => el.tagName === "DIALOG") || null;
|
42
|
+
const isTopElement = ref.current ? topElement === ref.current : false;
|
43
|
+
const isInTopLayer = ref.current
|
44
|
+
? topLayerList.includes(ref.current)
|
45
|
+
: false;
|
46
|
+
return { topElement, topDialog, isTopElement, isInTopLayer };
|
47
|
+
}, [topLayerList]);
|
48
|
+
// Listen for top layer changes
|
49
|
+
(0, react_1.useEffect)(() => {
|
50
|
+
const ac = new AbortController();
|
51
|
+
document.addEventListener("topLayer", () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
|
52
|
+
return () => ac.abort();
|
53
|
+
}, []);
|
54
|
+
return (0, react_1.useMemo)(() => ({ ref, ...derivedState, topLayerList }), [derivedState, topLayerList]);
|
55
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default } from './useTopLayerElement';
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
2
|
+
const topLayerElements = new Set();
|
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") {
|
6
|
+
// Listen for elements being added/removed from the top layer
|
7
|
+
document.addEventListener("toggle", ({ target }) => {
|
8
|
+
if (!target)
|
9
|
+
return;
|
10
|
+
const el = target;
|
11
|
+
if (!(el instanceof HTMLDialogElement || el.hasAttribute("popover")))
|
12
|
+
return;
|
13
|
+
if (el.matches(":modal, :popover-open") && document.contains(el)) {
|
14
|
+
topLayerElements.add(el);
|
15
|
+
el.dispatchEvent(topLayerEvent(true));
|
16
|
+
}
|
17
|
+
else {
|
18
|
+
if (topLayerElements.delete(el))
|
19
|
+
el.dispatchEvent(topLayerEvent(false));
|
20
|
+
}
|
21
|
+
}, { capture: true });
|
22
|
+
// MutationObserver for automatic cleanup
|
23
|
+
const observer = new MutationObserver((mutations) => {
|
24
|
+
const nodes = mutations.flatMap(({ removedNodes }) => [...removedNodes]);
|
25
|
+
for (const node of nodes)
|
26
|
+
if (topLayerElements.delete(node))
|
27
|
+
document.dispatchEvent(topLayerEvent(false));
|
28
|
+
});
|
29
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
30
|
+
}
|
31
|
+
export default function useTopLayerElements() {
|
32
|
+
const ref = useRef(null);
|
33
|
+
const [topLayerList, setTopLayerList] = useState([
|
34
|
+
...topLayerElements,
|
35
|
+
]);
|
36
|
+
const derivedState = useMemo(() => {
|
37
|
+
const topElement = topLayerList.length ? topLayerList.at(-1) || null : null;
|
38
|
+
const topDialog = topLayerList.findLast((el) => el.tagName === "DIALOG") || null;
|
39
|
+
const isTopElement = ref.current ? topElement === ref.current : false;
|
40
|
+
const isInTopLayer = ref.current
|
41
|
+
? topLayerList.includes(ref.current)
|
42
|
+
: false;
|
43
|
+
return { topElement, topDialog, isTopElement, isInTopLayer };
|
44
|
+
}, [topLayerList]);
|
45
|
+
// Listen for top layer changes
|
46
|
+
useEffect(() => {
|
47
|
+
const ac = new AbortController();
|
48
|
+
document.addEventListener("topLayer", () => setTopLayerList([...topLayerElements]), { signal: ac.signal });
|
49
|
+
return () => ac.abort();
|
50
|
+
}, []);
|
51
|
+
return useMemo(() => ({ ref, ...derivedState, topLayerList }), [derivedState, topLayerList]);
|
52
|
+
}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
export interface useTopLayerElementsReturn {
|
2
|
+
ref: React.RefObject<HTMLElement | null>;
|
3
|
+
topElement: HTMLElement | null;
|
4
|
+
topDialog: HTMLElement | null;
|
5
|
+
isInTopLayer: boolean;
|
6
|
+
isTopElement: boolean;
|
7
|
+
topLayerList: HTMLElement[];
|
8
|
+
}
|
9
|
+
export default function useTopLayerElements(): useTopLayerElementsReturn;
|
package/package.json
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
{
|
2
|
+
"name": "@blocdigital/usetoplayerelement",
|
3
|
+
"type": "module",
|
4
|
+
"version": "0.0.1",
|
5
|
+
"description": "React hook for monitoring the top layer",
|
6
|
+
"author": "Bloc Digital <web@bloc.digital>",
|
7
|
+
"license": "MIT",
|
8
|
+
"keywords": [
|
9
|
+
"react",
|
10
|
+
"react",
|
11
|
+
"hooks",
|
12
|
+
"top",
|
13
|
+
"layer",
|
14
|
+
"modal",
|
15
|
+
"dialog",
|
16
|
+
"popover"
|
17
|
+
],
|
18
|
+
"repository": {
|
19
|
+
"type": "git",
|
20
|
+
"url": "git+https://github.com/bloc-digital/usetoplayerelement.git"
|
21
|
+
},
|
22
|
+
"homepage": "https://github.com/bloc-digital/usetoplayerelement#readme",
|
23
|
+
"jest": {
|
24
|
+
"testEnvironment": "jsdom"
|
25
|
+
},
|
26
|
+
"types": "dist/types/index.d.ts",
|
27
|
+
"main": "dist/cjs/index.js",
|
28
|
+
"module": "dist/esm/index.js",
|
29
|
+
"exports": {
|
30
|
+
".": {
|
31
|
+
"import": {
|
32
|
+
"types": "./dist/types/index.d.ts",
|
33
|
+
"default": "./dist/esm/index.js"
|
34
|
+
},
|
35
|
+
"require": {
|
36
|
+
"types": "./dist/types/index.d.ts",
|
37
|
+
"default": "./dist/cjs/index.js"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"./package.json": "./package.json"
|
41
|
+
},
|
42
|
+
"files": [
|
43
|
+
"dist"
|
44
|
+
],
|
45
|
+
"scripts": {
|
46
|
+
"dev": "vite",
|
47
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && tsc -p tsconfig-types.json",
|
48
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
49
|
+
"preview": "vite preview",
|
50
|
+
"test": "jest"
|
51
|
+
},
|
52
|
+
"dependencies": {
|
53
|
+
"react": "^19.0.0",
|
54
|
+
"react-dom": "^19.0.0"
|
55
|
+
},
|
56
|
+
"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.24.1",
|
65
|
+
"@typescript-eslint/parser": "^8.24.1",
|
66
|
+
"@vitejs/plugin-react-swc": "^3.8.0",
|
67
|
+
"babel-jest": "^29.7.0",
|
68
|
+
"eslint": "^9.20.1",
|
69
|
+
"eslint-config-prettier": "^10.0.1",
|
70
|
+
"eslint-plugin-prettier": "^5.2.3",
|
71
|
+
"eslint-plugin-react": "^7.37.4",
|
72
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
73
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
74
|
+
"jest": "^29.7.0",
|
75
|
+
"jest-environment-jsdom": "^29.7.0",
|
76
|
+
"prettier": "^3.5.1",
|
77
|
+
"typescript": "^5.7.3",
|
78
|
+
"vite": "^6.1.0"
|
79
|
+
},
|
80
|
+
"bugs": {
|
81
|
+
"url": "https://github.com/bloc-digital/usetoplayerelement/issues"
|
82
|
+
}
|
83
|
+
}
|