@seed-design/react-dismissible-layer 0.0.0-alpha-20260511052324 → 1.0.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.
@@ -3,8 +3,8 @@ import { jsx } from 'react/jsx-runtime';
3
3
  import { composeRefs } from '@radix-ui/react-compose-refs';
4
4
  import { Slot } from '@radix-ui/react-slot';
5
5
  import { forwardRef } from 'react';
6
- import { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
7
- import { D as DismissibleParentContext } from './layer-stack-12s-DjRVp1_u.js';
6
+ import { u as useDismissibleLayer } from './useDismissibleLayer-12s-sWGJcPBy.js';
7
+ import { D as DismissibleParentContext } from './layer-stack-12s-BYEm6Mii.js';
8
8
 
9
9
  const DismissibleLayer = /*#__PURE__*/ forwardRef((props, ref)=>{
10
10
  const { children, ...options } = props;
@@ -3,8 +3,8 @@ var jsxRuntime = require('react/jsx-runtime');
3
3
  var reactComposeRefs = require('@radix-ui/react-compose-refs');
4
4
  var reactSlot = require('@radix-ui/react-slot');
5
5
  var react = require('react');
6
- var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
7
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
6
+ var useDismissibleLayer12s = require('./useDismissibleLayer-12s-Bt4Fi-Z1.cjs');
7
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
8
8
 
9
9
  const DismissibleLayer = /*#__PURE__*/ react.forwardRef((props, ref)=>{
10
10
  const { children, ...options } = props;
package/lib/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
- var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
2
- var DismissibleLayer12s = require('./DismissibleLayer-12s-ZXi0vrlt.cjs');
3
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
1
+ var useDismissibleLayer12s = require('./useDismissibleLayer-12s-Bt4Fi-Z1.cjs');
2
+ var DismissibleLayer12s = require('./DismissibleLayer-12s-ClcF8WmX.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
4
4
 
5
5
 
6
6
 
package/lib/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as react from 'react';
2
- import { CSSProperties } from 'react';
3
2
 
4
3
  interface CascadeDismissDetail {
5
4
  /** The layer node that was removed, causing this cascade dismiss. */
@@ -8,7 +7,6 @@ interface CascadeDismissDetail {
8
7
  interface Layer {
9
8
  node: HTMLElement;
10
9
  dismiss: (detail: CascadeDismissDetail) => void;
11
- blockPointerEvents?: boolean;
12
10
  /**
13
11
  * The parent layer's node. Used to determine cascade-dismiss scope.
14
12
  * Only layers whose parentNode matches the removed layer's node will be
@@ -39,11 +37,6 @@ interface UseDismissibleLayerOptions {
39
37
  * registered in the stack and no event listeners are attached.
40
38
  */
41
39
  enabled: boolean;
42
- /**
43
- * When true, disables pointer events on elements outside this layer.
44
- * Used for modal overlays (Dialog, BottomSheet).
45
- */
46
- blockPointerEvents?: boolean;
47
40
  /**
48
41
  * Called when escape key is pressed while this layer is topmost.
49
42
  * Call `event.preventDefault()` to prevent dismiss.
@@ -85,18 +78,6 @@ declare function useDismissibleLayer(options: UseDismissibleLayerOptions): {
85
78
  onPointerDownCapture: () => void;
86
79
  onFocusCapture: () => void;
87
80
  onBlurCapture: () => void;
88
- style?: undefined;
89
- };
90
- isTopLayer: boolean;
91
- /** The current layer node, for providing DismissibleParentContext to children. */
92
- layerNode: HTMLElement | null;
93
- } | {
94
- dismissibleRef: (el: HTMLElement | null) => void;
95
- dismissibleProps: {
96
- onPointerDownCapture: () => void;
97
- onFocusCapture: () => void;
98
- onBlurCapture: () => void;
99
- style: CSSProperties;
100
81
  };
101
82
  isTopLayer: boolean;
102
83
  /** The current layer node, for providing DismissibleParentContext to children. */
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
2
- export { D as DismissibleLayer } from './DismissibleLayer-12s-Cz1jvGI2.js';
3
- export { f as LayerStackContext, h as addBranch, i as isTopMost, j as removeBranch, u as useLayerStackContext } from './layer-stack-12s-DjRVp1_u.js';
1
+ export { u as useDismissibleLayer } from './useDismissibleLayer-12s-sWGJcPBy.js';
2
+ export { D as DismissibleLayer } from './DismissibleLayer-12s-BtXreIO0.js';
3
+ export { e as LayerStackContext, f as addBranch, i as isTopMost, g as removeBranch, u as useLayerStackContext } from './layer-stack-12s-BYEm6Mii.js';
@@ -35,24 +35,6 @@ function isInNestedLayer(ctx, node, target) {
35
35
  function isInBranch(ctx, target) {
36
36
  return ctx.branches.some((branch)=>contains(branch, target));
37
37
  }
38
- function isBelowPointerBlockingLayer(ctx, node) {
39
- const index = ctx.layers.findIndex((l)=>l.node === node);
40
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
41
- const highestBlocking = blockingLayers.at(-1);
42
- if (!highestBlocking) return false;
43
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
44
- return index < highestBlockingIndex;
45
- }
46
- function getPointerEventsEnabled(ctx, node) {
47
- const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
48
- if (!hasBlocking) return true;
49
- const index = ctx.layers.findIndex((l)=>l.node === node);
50
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
51
- const highestBlocking = blockingLayers.at(-1);
52
- if (!highestBlocking) return true;
53
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
54
- return index >= highestBlockingIndex;
55
- }
56
38
  function notifyLayerChange() {
57
39
  document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
58
40
  }
@@ -109,4 +91,4 @@ function removeBranch(ctx, node) {
109
91
  if (index >= 0) ctx.branches.splice(index, 1);
110
92
  }
111
93
 
112
- export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b, isBelowPointerBlockingLayer as c, useDismissibleParentNode as d, addLayer as e, LayerStackContext as f, getPointerEventsEnabled as g, addBranch as h, isTopMost as i, removeBranch as j, removeLayer as r, useLayerStackContext as u };
94
+ export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b, useDismissibleParentNode as c, addLayer as d, LayerStackContext as e, addBranch as f, removeBranch as g, isTopMost as i, removeLayer as r, useLayerStackContext as u };
@@ -35,24 +35,6 @@ function isInNestedLayer(ctx, node, target) {
35
35
  function isInBranch(ctx, target) {
36
36
  return ctx.branches.some((branch)=>contains(branch, target));
37
37
  }
38
- function isBelowPointerBlockingLayer(ctx, node) {
39
- const index = ctx.layers.findIndex((l)=>l.node === node);
40
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
41
- const highestBlocking = blockingLayers.at(-1);
42
- if (!highestBlocking) return false;
43
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
44
- return index < highestBlockingIndex;
45
- }
46
- function getPointerEventsEnabled(ctx, node) {
47
- const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
48
- if (!hasBlocking) return true;
49
- const index = ctx.layers.findIndex((l)=>l.node === node);
50
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
51
- const highestBlocking = blockingLayers.at(-1);
52
- if (!highestBlocking) return true;
53
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
54
- return index >= highestBlockingIndex;
55
- }
56
38
  function notifyLayerChange() {
57
39
  document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
58
40
  }
@@ -114,8 +96,6 @@ exports.LAYER_UPDATE_EVENT = LAYER_UPDATE_EVENT;
114
96
  exports.LayerStackContext = LayerStackContext;
115
97
  exports.addBranch = addBranch;
116
98
  exports.addLayer = addLayer;
117
- exports.getPointerEventsEnabled = getPointerEventsEnabled;
118
- exports.isBelowPointerBlockingLayer = isBelowPointerBlockingLayer;
119
99
  exports.isInBranch = isInBranch;
120
100
  exports.isInNestedLayer = isInNestedLayer;
121
101
  exports.isTopMost = isTopMost;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { useRef, useEffect } from 'react';
3
- import { i as isTopMost } from './layer-stack-12s-DjRVp1_u.js';
3
+ import { i as isTopMost } from './layer-stack-12s-BYEm6Mii.js';
4
4
 
5
5
  /**
6
6
  * Tracks escape keydown on the document (capture phase).
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  var react = require('react');
3
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
4
4
 
5
5
  /**
6
6
  * Tracks escape keydown on the document (capture phase).
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { useRef, useEffect } from 'react';
3
- import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-DjRVp1_u.js';
3
+ import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-BYEm6Mii.js';
4
4
 
5
5
  /**
6
6
  * Detects focus events outside a React subtree.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  var react = require('react');
3
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
4
4
 
5
5
  /**
6
6
  * Detects focus events outside a React subtree.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  var react = require('react');
3
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
4
4
 
5
5
  /**
6
6
  * Detects pointer-down events outside a React subtree.
@@ -26,7 +26,6 @@ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
26
26
  const ownerDocument = node.ownerDocument ?? document;
27
27
  function isOutsideLayer(target) {
28
28
  if (!layerStack12s.isTopMost(ctx, node)) return false;
29
- if (layerStack12s.isBelowPointerBlockingLayer(ctx, node)) return false;
30
29
  if (layerStack12s.isInBranch(ctx, target)) return false;
31
30
  if (layerStack12s.isInNestedLayer(ctx, node, target)) return false;
32
31
  return true;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { useRef, useEffect } from 'react';
3
- import { i as isTopMost, c as isBelowPointerBlockingLayer, a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-DjRVp1_u.js';
3
+ import { i as isTopMost, a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-BYEm6Mii.js';
4
4
 
5
5
  /**
6
6
  * Detects pointer-down events outside a React subtree.
@@ -26,7 +26,6 @@ import { i as isTopMost, c as isBelowPointerBlockingLayer, a as isInBranch, b as
26
26
  const ownerDocument = node.ownerDocument ?? document;
27
27
  function isOutsideLayer(target) {
28
28
  if (!isTopMost(ctx, node)) return false;
29
- if (isBelowPointerBlockingLayer(ctx, node)) return false;
30
29
  if (isInBranch(ctx, target)) return false;
31
30
  if (isInNestedLayer(ctx, node, target)) return false;
32
31
  return true;
@@ -1,18 +1,13 @@
1
1
  'use client';
2
2
  var react = require('react');
3
- var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
4
- var useEscapeKeydown12s = require('./use-escape-keydown-12s-CNZqs1dI.cjs');
5
- var usePointerDownOutside12s = require('./use-pointer-down-outside-12s-nlxC-DrD.cjs');
6
- var useFocusOutside12s = require('./use-focus-outside-12s-Ch-SoiiA.cjs');
3
+ var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
4
+ var useEscapeKeydown12s = require('./use-escape-keydown-12s-XPuBxyd9.cjs');
5
+ var usePointerDownOutside12s = require('./use-pointer-down-outside-12s-Bj7Jt4l5.cjs');
6
+ var useFocusOutside12s = require('./use-focus-outside-12s-C_884sbw.cjs');
7
7
 
8
8
  const NOOP = ()=>{};
9
- // Module-level storage for the original body pointer-events value.
10
- // Follows Radix's pattern: save once when the first blocking layer mounts,
11
- // restore when the last blocking layer unmounts. This decouples save/restore
12
- // from React's bottom-up cleanup order.
13
- let originalBodyPointerEvents;
14
9
  function useDismissibleLayer(options) {
15
- const { enabled, blockPointerEvents = false, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
10
+ const { enabled, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
16
11
  const ctx = layerStack12s.useLayerStackContext();
17
12
  const parentNode = layerStack12s.useDismissibleParentNode();
18
13
  const [node, setNode] = react.useState(null);
@@ -33,7 +28,6 @@ function useDismissibleLayer(options) {
33
28
  const layer = {
34
29
  node,
35
30
  dismiss: (detail)=>onCascadeDismissRef.current?.(detail),
36
- blockPointerEvents,
37
31
  parentNode
38
32
  };
39
33
  layerStack12s.addLayer(ctx, layer);
@@ -43,38 +37,10 @@ function useDismissibleLayer(options) {
43
37
  }, [
44
38
  node,
45
39
  enabled,
46
- blockPointerEvents,
47
40
  ctx,
48
41
  parentNode
49
42
  ]);
50
- // -- Pointer event blocking --
51
- // Follows Radix's pattern: save original value once when the first blocking
52
- // layer mounts, restore when the last unmounts. The module-level variable
53
- // decouples save/restore from React's bottom-up cleanup order.
54
- react.useEffect(()=>{
55
- if (!node || !enabled || !blockPointerEvents) return;
56
- const ownerDocument = node.ownerDocument ?? document;
57
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
58
- if (blockingLayers.length === 1) {
59
- // First blocking layer — save the original value and block
60
- originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
61
- ownerDocument.body.style.pointerEvents = "none";
62
- }
63
- return ()=>{
64
- // Check count BEFORE this layer is removed from ctx.layers.
65
- // At cleanup time, this layer is still in the array.
66
- const remainingBlocking = ctx.layers.filter((l)=>l.blockPointerEvents && l.node !== node);
67
- if (remainingBlocking.length === 0) {
68
- ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
69
- }
70
- };
71
- }, [
72
- node,
73
- enabled,
74
- blockPointerEvents,
75
- ctx
76
- ]);
77
- // -- Subscribe to layer changes for style updates --
43
+ // -- Subscribe to layer changes to keep isTopLayer fresh --
78
44
  react.useEffect(()=>{
79
45
  if (!enabled) return;
80
46
  const handler = ()=>forceRender({});
@@ -111,12 +77,6 @@ function useDismissibleLayer(options) {
111
77
  exclude,
112
78
  onFocusOutside: handleFocusOutside
113
79
  });
114
- // -- Compute pointer-events style --
115
- const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
116
- const pointerEventsEnabled = node ? layerStack12s.getPointerEventsEnabled(ctx, node) : true;
117
- const style = hasBlocking ? {
118
- pointerEvents: pointerEventsEnabled ? "auto" : "none"
119
- } : {};
120
80
  const topLayer = node ? layerStack12s.isTopMost(ctx, node) : true;
121
81
  if (!enabled) {
122
82
  return {
@@ -135,8 +95,7 @@ function useDismissibleLayer(options) {
135
95
  dismissibleProps: {
136
96
  onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
137
97
  onFocusCapture: focusOutsideProps.onFocusCapture,
138
- onBlurCapture: focusOutsideProps.onBlurCapture,
139
- style
98
+ onBlurCapture: focusOutsideProps.onBlurCapture
140
99
  },
141
100
  isTopLayer: topLayer,
142
101
  /** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
@@ -1,18 +1,13 @@
1
1
  'use client';
2
2
  import { useState, useCallback, useRef, useEffect } from 'react';
3
- import { u as useLayerStackContext, d as useDismissibleParentNode, e as addLayer, r as removeLayer, L as LAYER_UPDATE_EVENT, g as getPointerEventsEnabled, i as isTopMost } from './layer-stack-12s-DjRVp1_u.js';
4
- import { u as useEscapeKeydown } from './use-escape-keydown-12s-Bl_her-_.js';
5
- import { u as usePointerDownOutside } from './use-pointer-down-outside-12s-BLctGw0m.js';
6
- import { u as useFocusOutside } from './use-focus-outside-12s-LpFt7UjO.js';
3
+ import { u as useLayerStackContext, c as useDismissibleParentNode, d as addLayer, r as removeLayer, L as LAYER_UPDATE_EVENT, i as isTopMost } from './layer-stack-12s-BYEm6Mii.js';
4
+ import { u as useEscapeKeydown } from './use-escape-keydown-12s-DHQq12u5.js';
5
+ import { u as usePointerDownOutside } from './use-pointer-down-outside-12s-Bk1SE4lm.js';
6
+ import { u as useFocusOutside } from './use-focus-outside-12s--rnlvSkk.js';
7
7
 
8
8
  const NOOP = ()=>{};
9
- // Module-level storage for the original body pointer-events value.
10
- // Follows Radix's pattern: save once when the first blocking layer mounts,
11
- // restore when the last blocking layer unmounts. This decouples save/restore
12
- // from React's bottom-up cleanup order.
13
- let originalBodyPointerEvents;
14
9
  function useDismissibleLayer(options) {
15
- const { enabled, blockPointerEvents = false, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
10
+ const { enabled, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
16
11
  const ctx = useLayerStackContext();
17
12
  const parentNode = useDismissibleParentNode();
18
13
  const [node, setNode] = useState(null);
@@ -33,7 +28,6 @@ function useDismissibleLayer(options) {
33
28
  const layer = {
34
29
  node,
35
30
  dismiss: (detail)=>onCascadeDismissRef.current?.(detail),
36
- blockPointerEvents,
37
31
  parentNode
38
32
  };
39
33
  addLayer(ctx, layer);
@@ -43,38 +37,10 @@ function useDismissibleLayer(options) {
43
37
  }, [
44
38
  node,
45
39
  enabled,
46
- blockPointerEvents,
47
40
  ctx,
48
41
  parentNode
49
42
  ]);
50
- // -- Pointer event blocking --
51
- // Follows Radix's pattern: save original value once when the first blocking
52
- // layer mounts, restore when the last unmounts. The module-level variable
53
- // decouples save/restore from React's bottom-up cleanup order.
54
- useEffect(()=>{
55
- if (!node || !enabled || !blockPointerEvents) return;
56
- const ownerDocument = node.ownerDocument ?? document;
57
- const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
58
- if (blockingLayers.length === 1) {
59
- // First blocking layer — save the original value and block
60
- originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
61
- ownerDocument.body.style.pointerEvents = "none";
62
- }
63
- return ()=>{
64
- // Check count BEFORE this layer is removed from ctx.layers.
65
- // At cleanup time, this layer is still in the array.
66
- const remainingBlocking = ctx.layers.filter((l)=>l.blockPointerEvents && l.node !== node);
67
- if (remainingBlocking.length === 0) {
68
- ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
69
- }
70
- };
71
- }, [
72
- node,
73
- enabled,
74
- blockPointerEvents,
75
- ctx
76
- ]);
77
- // -- Subscribe to layer changes for style updates --
43
+ // -- Subscribe to layer changes to keep isTopLayer fresh --
78
44
  useEffect(()=>{
79
45
  if (!enabled) return;
80
46
  const handler = ()=>forceRender({});
@@ -111,12 +77,6 @@ function useDismissibleLayer(options) {
111
77
  exclude,
112
78
  onFocusOutside: handleFocusOutside
113
79
  });
114
- // -- Compute pointer-events style --
115
- const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
116
- const pointerEventsEnabled = node ? getPointerEventsEnabled(ctx, node) : true;
117
- const style = hasBlocking ? {
118
- pointerEvents: pointerEventsEnabled ? "auto" : "none"
119
- } : {};
120
80
  const topLayer = node ? isTopMost(ctx, node) : true;
121
81
  if (!enabled) {
122
82
  return {
@@ -135,8 +95,7 @@ function useDismissibleLayer(options) {
135
95
  dismissibleProps: {
136
96
  onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
137
97
  onFocusCapture: focusOutsideProps.onFocusCapture,
138
- onBlurCapture: focusOutsideProps.onBlurCapture,
139
- style
98
+ onBlurCapture: focusOutsideProps.onBlurCapture
140
99
  },
141
100
  isTopLayer: topLayer,
142
101
  /** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/react-dismissible-layer",
3
- "version": "0.0.0-alpha-20260511052324",
3
+ "version": "1.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -7,8 +7,6 @@ import {
7
7
  isTopMost,
8
8
  isInNestedLayer,
9
9
  isInBranch,
10
- isBelowPointerBlockingLayer,
11
- getPointerEventsEnabled,
12
10
  type LayerStackContextValue,
13
11
  type Layer,
14
12
  } from "./layer-stack";
@@ -58,12 +56,12 @@ describe("layer-stack", () => {
58
56
 
59
57
  it("replaces existing layer when same node is added twice", () => {
60
58
  const node = createNode();
61
- const first = createLayer(node, { blockPointerEvents: false });
62
- const second = createLayer(node, { blockPointerEvents: true });
59
+ const first = createLayer(node);
60
+ const second = createLayer(node);
63
61
  addLayer(ctx, first);
64
62
  addLayer(ctx, second);
65
63
  expect(ctx.layers).toHaveLength(1);
66
- expect(ctx.layers[0].blockPointerEvents).toBe(true);
64
+ expect(ctx.layers[0].dismiss).toBe(second.dismiss);
67
65
  });
68
66
  });
69
67
 
@@ -257,35 +255,4 @@ describe("layer-stack", () => {
257
255
  expect(ctx.branches).toHaveLength(0);
258
256
  });
259
257
  });
260
- describe("pointer blocking", () => {
261
- it("isBelowPointerBlockingLayer returns false when no blocking layers", () => {
262
- const node = createNode();
263
- addLayer(ctx, createLayer(node));
264
- expect(isBelowPointerBlockingLayer(ctx, node)).toBe(false);
265
- });
266
-
267
- it("isBelowPointerBlockingLayer returns true when below a blocking layer", () => {
268
- const dialog = createNode("dialog");
269
- const menu = createNode("menu");
270
- addLayer(ctx, createLayer(dialog, { blockPointerEvents: true }));
271
- addLayer(ctx, createLayer(menu, { blockPointerEvents: true }));
272
- expect(isBelowPointerBlockingLayer(ctx, dialog)).toBe(true);
273
- expect(isBelowPointerBlockingLayer(ctx, menu)).toBe(false);
274
- });
275
-
276
- it("getPointerEventsEnabled returns true when no blocking layers", () => {
277
- const node = createNode();
278
- addLayer(ctx, createLayer(node));
279
- expect(getPointerEventsEnabled(ctx, node)).toBe(true);
280
- });
281
-
282
- it("getPointerEventsEnabled returns false for layers below the highest blocking layer", () => {
283
- const dialog = createNode("dialog");
284
- const menu = createNode("menu");
285
- addLayer(ctx, createLayer(dialog, { blockPointerEvents: true }));
286
- addLayer(ctx, createLayer(menu, { blockPointerEvents: true }));
287
- expect(getPointerEventsEnabled(ctx, dialog)).toBe(false);
288
- expect(getPointerEventsEnabled(ctx, menu)).toBe(true);
289
- });
290
- });
291
258
  });
@@ -10,7 +10,6 @@ export interface CascadeDismissDetail {
10
10
  export interface Layer {
11
11
  node: HTMLElement;
12
12
  dismiss: (detail: CascadeDismissDetail) => void;
13
- blockPointerEvents?: boolean;
14
13
  /**
15
14
  * The parent layer's node. Used to determine cascade-dismiss scope.
16
15
  * Only layers whose parentNode matches the removed layer's node will be
@@ -75,32 +74,6 @@ export function isInBranch(ctx: LayerStackContextValue, target: EventTarget | nu
75
74
  return ctx.branches.some((branch) => contains(branch, target));
76
75
  }
77
76
 
78
- export function isBelowPointerBlockingLayer(
79
- ctx: LayerStackContextValue,
80
- node: HTMLElement,
81
- ): boolean {
82
- const index = ctx.layers.findIndex((l) => l.node === node);
83
- const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
84
- const highestBlocking = blockingLayers.at(-1);
85
- if (!highestBlocking) return false;
86
-
87
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
88
- return index < highestBlockingIndex;
89
- }
90
-
91
- export function getPointerEventsEnabled(ctx: LayerStackContextValue, node: HTMLElement): boolean {
92
- const hasBlocking = ctx.layers.some((l) => l.blockPointerEvents);
93
- if (!hasBlocking) return true;
94
-
95
- const index = ctx.layers.findIndex((l) => l.node === node);
96
- const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
97
- const highestBlocking = blockingLayers.at(-1);
98
- if (!highestBlocking) return true;
99
-
100
- const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
101
- return index >= highestBlockingIndex;
102
- }
103
-
104
77
  export function notifyLayerChange() {
105
78
  document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
106
79
  }
@@ -1,13 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useRef } from "react";
4
- import {
5
- isBelowPointerBlockingLayer,
6
- isInBranch,
7
- isInNestedLayer,
8
- isTopMost,
9
- type LayerStackContextValue,
10
- } from "./layer-stack";
4
+ import { isInBranch, isInNestedLayer, isTopMost, type LayerStackContextValue } from "./layer-stack";
11
5
 
12
6
  export interface UsePointerDownOutsideOptions {
13
7
  enabled: boolean;
@@ -74,7 +68,6 @@ export function usePointerDownOutside(
74
68
 
75
69
  function isOutsideLayer(target: HTMLElement): boolean {
76
70
  if (!isTopMost(ctx, node!)) return false;
77
- if (isBelowPointerBlockingLayer(ctx, node!)) return false;
78
71
  if (isInBranch(ctx, target)) return false;
79
72
  if (isInNestedLayer(ctx, node!, target)) return false;
80
73
  return true;
@@ -631,102 +631,4 @@ describe("useDismissibleLayer", () => {
631
631
  expect(onFocusOutside).toHaveBeenCalledTimes(1);
632
632
  });
633
633
  });
634
-
635
- describe("pointer event blocking", () => {
636
- it("restores body.style.pointerEvents after nested blocking layers unmount", async () => {
637
- const original = document.body.style.pointerEvents;
638
-
639
- function Harness() {
640
- const [showBoth, setShowBoth] = React.useState(true);
641
-
642
- return (
643
- <>
644
- {showBoth && (
645
- <DismissibleBox
646
- options={{ enabled: true, blockPointerEvents: true }}
647
- data-testid="outer"
648
- >
649
- <DismissibleBox
650
- options={{ enabled: true, blockPointerEvents: true }}
651
- data-testid="inner"
652
- />
653
- </DismissibleBox>
654
- )}
655
- <button type="button" data-testid="unmount" onClick={() => setShowBoth(false)}>
656
- Unmount
657
- </button>
658
- </>
659
- );
660
- }
661
-
662
- const { getByTestId } = render(<Harness />);
663
-
664
- // Both blocking layers are mounted → body should be "none"
665
- expect(document.body.style.pointerEvents).toBe("none");
666
-
667
- // Unmount both layers — React cleans up children before parents
668
- await act(async () => getByTestId("unmount").click());
669
-
670
- // body.style.pointerEvents must be restored
671
- expect(document.body.style.pointerEvents).toBe(original);
672
- });
673
-
674
- it("restores body.style.pointerEvents when blocking layers mount sequentially then unmount together", async () => {
675
- const original = document.body.style.pointerEvents;
676
-
677
- function Harness() {
678
- const [showFirst, setShowFirst] = React.useState(false);
679
- const [showSecond, setShowSecond] = React.useState(false);
680
-
681
- return (
682
- <>
683
- {showFirst && (
684
- <DismissibleBox
685
- options={{ enabled: true, blockPointerEvents: true }}
686
- data-testid="first"
687
- />
688
- )}
689
- {showSecond && (
690
- <DismissibleBox
691
- options={{ enabled: true, blockPointerEvents: true }}
692
- data-testid="second"
693
- />
694
- )}
695
- <button type="button" data-testid="mount-first" onClick={() => setShowFirst(true)}>
696
- Mount First
697
- </button>
698
- <button type="button" data-testid="mount-second" onClick={() => setShowSecond(true)}>
699
- Mount Second
700
- </button>
701
- <button
702
- type="button"
703
- data-testid="unmount-all"
704
- onClick={() => {
705
- setShowFirst(false);
706
- setShowSecond(false);
707
- }}
708
- >
709
- Unmount All
710
- </button>
711
- </>
712
- );
713
- }
714
-
715
- const { getByTestId } = render(<Harness />);
716
-
717
- // Mount first blocking layer
718
- await act(async () => getByTestId("mount-first").click());
719
- expect(document.body.style.pointerEvents).toBe("none");
720
-
721
- // Mount second blocking layer
722
- await act(async () => getByTestId("mount-second").click());
723
- expect(document.body.style.pointerEvents).toBe("none");
724
-
725
- // Unmount both at once
726
- await act(async () => getByTestId("unmount-all").click());
727
-
728
- // body.style.pointerEvents must be restored
729
- expect(document.body.style.pointerEvents).toBe(original);
730
- });
731
- });
732
634
  });
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import {
5
5
  addLayer,
6
6
  removeLayer,
7
7
  isTopMost,
8
- getPointerEventsEnabled,
9
8
  useLayerStackContext,
10
9
  useDismissibleParentNode,
11
10
  LAYER_UPDATE_EVENT,
@@ -22,12 +21,6 @@ export interface UseDismissibleLayerOptions {
22
21
  */
23
22
  enabled: boolean;
24
23
 
25
- /**
26
- * When true, disables pointer events on elements outside this layer.
27
- * Used for modal overlays (Dialog, BottomSheet).
28
- */
29
- blockPointerEvents?: boolean;
30
-
31
24
  /**
32
25
  * Called when escape key is pressed while this layer is topmost.
33
26
  * Call `event.preventDefault()` to prevent dismiss.
@@ -71,16 +64,9 @@ export interface UseDismissibleLayerOptions {
71
64
 
72
65
  const NOOP = () => {};
73
66
 
74
- // Module-level storage for the original body pointer-events value.
75
- // Follows Radix's pattern: save once when the first blocking layer mounts,
76
- // restore when the last blocking layer unmounts. This decouples save/restore
77
- // from React's bottom-up cleanup order.
78
- let originalBodyPointerEvents: string;
79
-
80
67
  export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
81
68
  const {
82
69
  enabled,
83
- blockPointerEvents = false,
84
70
  onEscapeKeyDown,
85
71
  onPressOutside,
86
72
  onFocusOutside,
@@ -114,7 +100,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
114
100
  const layer = {
115
101
  node,
116
102
  dismiss: (detail: CascadeDismissDetail) => onCascadeDismissRef.current?.(detail),
117
- blockPointerEvents,
118
103
  parentNode,
119
104
  };
120
105
 
@@ -123,35 +108,9 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
123
108
  return () => {
124
109
  removeLayer(ctx, node);
125
110
  };
126
- }, [node, enabled, blockPointerEvents, ctx, parentNode]);
111
+ }, [node, enabled, ctx, parentNode]);
127
112
 
128
- // -- Pointer event blocking --
129
- // Follows Radix's pattern: save original value once when the first blocking
130
- // layer mounts, restore when the last unmounts. The module-level variable
131
- // decouples save/restore from React's bottom-up cleanup order.
132
- useEffect(() => {
133
- if (!node || !enabled || !blockPointerEvents) return;
134
-
135
- const ownerDocument = node.ownerDocument ?? document;
136
- const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
137
-
138
- if (blockingLayers.length === 1) {
139
- // First blocking layer — save the original value and block
140
- originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
141
- ownerDocument.body.style.pointerEvents = "none";
142
- }
143
-
144
- return () => {
145
- // Check count BEFORE this layer is removed from ctx.layers.
146
- // At cleanup time, this layer is still in the array.
147
- const remainingBlocking = ctx.layers.filter((l) => l.blockPointerEvents && l.node !== node);
148
- if (remainingBlocking.length === 0) {
149
- ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
150
- }
151
- };
152
- }, [node, enabled, blockPointerEvents, ctx]);
153
-
154
- // -- Subscribe to layer changes for style updates --
113
+ // -- Subscribe to layer changes to keep isTopLayer fresh --
155
114
  useEffect(() => {
156
115
  if (!enabled) return;
157
116
  const handler = () => forceRender({});
@@ -195,14 +154,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
195
154
  onFocusOutside: handleFocusOutside,
196
155
  });
197
156
 
198
- // -- Compute pointer-events style --
199
- const hasBlocking = ctx.layers.some((l) => l.blockPointerEvents);
200
- const pointerEventsEnabled = node ? getPointerEventsEnabled(ctx, node) : true;
201
-
202
- const style: CSSProperties = hasBlocking
203
- ? { pointerEvents: pointerEventsEnabled ? "auto" : "none" }
204
- : {};
205
-
206
157
  const topLayer = node ? isTopMost(ctx, node) : true;
207
158
 
208
159
  if (!enabled) {
@@ -225,7 +176,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
225
176
  onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
226
177
  onFocusCapture: focusOutsideProps.onFocusCapture,
227
178
  onBlurCapture: focusOutsideProps.onBlurCapture,
228
- style,
229
179
  },
230
180
  isTopLayer: topLayer,
231
181
  /** The current layer node, for providing DismissibleParentContext to children. */