@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 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,2 @@
1
+ export { default } from './useTopLayerElement';
2
+ export type { useTopLayerElementsReturn } from './useTopLayerElement';
@@ -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
+ }