@tamagui/dismissable 2.0.0-rc.8 → 2.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.
Files changed (47) hide show
  1. package/dist/cjs/Dismissable.cjs +331 -158
  2. package/dist/cjs/Dismissable.native.js +46 -28
  3. package/dist/cjs/Dismissable.native.js.map +1 -1
  4. package/dist/cjs/DismissableProps.cjs +7 -5
  5. package/dist/cjs/DismissableProps.native.js +7 -5
  6. package/dist/cjs/DismissableProps.native.js.map +1 -1
  7. package/dist/cjs/index.cjs +7 -5
  8. package/dist/cjs/index.native.js +7 -5
  9. package/dist/cjs/index.native.js.map +1 -1
  10. package/dist/esm/Dismissable.mjs +295 -129
  11. package/dist/esm/Dismissable.mjs.map +1 -1
  12. package/dist/esm/Dismissable.native.js +18 -6
  13. package/dist/esm/Dismissable.native.js.map +1 -1
  14. package/dist/esm/index.js +1 -1
  15. package/dist/esm/index.js.map +1 -6
  16. package/dist/jsx/Dismissable.mjs +295 -129
  17. package/dist/jsx/Dismissable.mjs.map +1 -1
  18. package/dist/jsx/Dismissable.native.js +46 -28
  19. package/dist/jsx/Dismissable.native.js.map +1 -1
  20. package/dist/jsx/DismissableProps.native.js +7 -5
  21. package/dist/jsx/index.js +1 -1
  22. package/dist/jsx/index.js.map +1 -6
  23. package/dist/jsx/index.native.js +7 -5
  24. package/package.json +10 -13
  25. package/src/Dismissable.native.tsx +21 -1
  26. package/src/Dismissable.tsx +187 -33
  27. package/src/DismissableProps.tsx +10 -0
  28. package/types/Dismissable.d.ts +28 -2
  29. package/types/Dismissable.d.ts.map +1 -1
  30. package/types/Dismissable.native.d.ts +4 -0
  31. package/types/Dismissable.native.d.ts.map +1 -1
  32. package/types/DismissableProps.d.ts +10 -0
  33. package/types/DismissableProps.d.ts.map +1 -1
  34. package/dist/cjs/Dismissable.js +0 -179
  35. package/dist/cjs/Dismissable.js.map +0 -6
  36. package/dist/cjs/DismissableProps.js +0 -14
  37. package/dist/cjs/DismissableProps.js.map +0 -6
  38. package/dist/cjs/index.js +0 -15
  39. package/dist/cjs/index.js.map +0 -6
  40. package/dist/esm/Dismissable.js +0 -161
  41. package/dist/esm/Dismissable.js.map +0 -6
  42. package/dist/esm/DismissableProps.js +0 -1
  43. package/dist/esm/DismissableProps.js.map +0 -6
  44. package/dist/jsx/Dismissable.js +0 -161
  45. package/dist/jsx/Dismissable.js.map +0 -6
  46. package/dist/jsx/DismissableProps.js +0 -1
  47. package/dist/jsx/DismissableProps.js.map +0 -6
@@ -4,46 +4,64 @@ var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf,
8
- __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __export = (target, all) => {
10
- for (var name in all) __defProp(target, name, {
11
- get: all[name],
12
- enumerable: !0
13
- });
14
- },
15
- __copyProps = (to, from, except, desc) => {
16
- if (from && typeof from == "object" || typeof from == "function") for (let key of __getOwnPropNames(from)) !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, {
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
18
  get: () => from[key],
18
19
  enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
20
  });
20
- return to;
21
- };
21
+ }
22
+ return to;
23
+ };
22
24
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
- // If the importer is in node compatibility mode or this is not an ESM
24
- // file that has been converted to a CommonJS file using a Babel-
25
- // compatible transform (i.e. "__esModule" has not been set), then set
26
- // "default" to the CommonJS "module.exports" for node compatibility.
27
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
28
- value: mod,
29
- enumerable: !0
30
- }) : target, mod)),
31
- __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
32
- value: !0
33
- }), mod);
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
30
+ value: mod,
31
+ enumerable: true
32
+ }) : target, mod));
33
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
34
+ value: true
35
+ }), mod);
34
36
  var Dismissable_native_exports = {};
35
37
  __export(Dismissable_native_exports, {
36
38
  Dismissable: () => Dismissable,
37
39
  DismissableBranch: () => DismissableBranch,
38
- dispatchDiscreteCustomEvent: () => dispatchDiscreteCustomEvent
40
+ dispatchDiscreteCustomEvent: () => dispatchDiscreteCustomEvent,
41
+ getDismissableLayerCount: () => getDismissableLayerCount,
42
+ useDismissableLayersAbove: () => useDismissableLayersAbove,
43
+ useHasDismissableLayers: () => useHasDismissableLayers,
44
+ useIsInsideDismissable: () => useIsInsideDismissable
39
45
  });
40
46
  module.exports = __toCommonJS(Dismissable_native_exports);
41
47
  var import_react = __toESM(require("react"), 1);
42
48
  function dispatchDiscreteCustomEvent(_target, _event) {}
49
+ function getDismissableLayerCount() {
50
+ return 0;
51
+ }
52
+ function useHasDismissableLayers() {
53
+ return false;
54
+ }
55
+ function useIsInsideDismissable(_ref) {
56
+ return false;
57
+ }
58
+ function useDismissableLayersAbove(_ref) {
59
+ return 0;
60
+ }
43
61
  var Dismissable = /* @__PURE__ */import_react.default.forwardRef(function (props, _ref) {
44
- return props.children;
45
- }),
46
- DismissableBranch = /* @__PURE__ */import_react.default.forwardRef(function (props, _ref) {
47
- return props.children;
48
- });
62
+ return props.children;
63
+ });
64
+ var DismissableBranch = /* @__PURE__ */import_react.default.forwardRef(function (props, _ref) {
65
+ return props.children;
66
+ });
49
67
  //# sourceMappingURL=Dismissable.native.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Dismissable_native_exports","__export","Dismissable","DismissableBranch","dispatchDiscreteCustomEvent","module","exports","__toCommonJS","import_react","__toESM","require","_target","_event","default","forwardRef","props","_ref","children"],"sources":["../../src/Dismissable.native.tsx"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,0BAAA;AAAAC,QAAA,CAAAD,0BAAA;EAAAE,WAAA,EAAAA,CAAA,KAAAA,WAAA;EAAAC,iBAAA,EAAAA,CAAA,KAAAA,iBAAA;EAAAC,2BAAA,EAAAA,CAAA,KAAAA;AAAA;AAAAC,MAAA,CAAAC,OAAA,GAAAC,YAAA,CAAAP,0BAAA;AAAA,IAAAQ,YAAA,GAAkBC,OAAA,CAAAC,OAAA;AAEX,SAASN,4BAA4BO,OAAA,EAASC,MAAA,EAAQ,CAAC;AACvD,IAAIV,WAAA,GAA4B,eAAAM,YAAA,CAAAK,OAAA,CAAMC,UAAA,CAAW,UAASC,KAAA,EAAOC,IAAA,EAAM;IAC1E,OAAOD,KAAA,CAAME,QAAA;EACjB,CAAC;EACUd,iBAAA,GAAkC,eAAAK,YAAA,CAAAK,OAAA,CAAMC,UAAA,CAAW,UAASC,KAAA,EAAOC,IAAA,EAAM;IAChF,OAAOD,KAAA,CAAME,QAAA;EACjB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["Dismissable_native_exports","__export","Dismissable","DismissableBranch","dispatchDiscreteCustomEvent","getDismissableLayerCount","useDismissableLayersAbove","useHasDismissableLayers","useIsInsideDismissable","module","exports","__toCommonJS","import_react","__toESM","require","_target","_event","_ref","default","forwardRef","props","children"],"sources":["../../src/Dismissable.native.tsx"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,0BAAA;AAAAC,QAAA,CAAAD,0BAAA;EAAAE,WAAA,EAAAA,CAAA,KAAAA,WAAA;EAAAC,iBAAA,EAAAA,CAAA,KAAAA,iBAAA;EAAAC,2BAAA,EAAAA,CAAA,KAAAA,2BAAA;EAAAC,wBAAA,EAAAA,CAAA,KAAAA,wBAAA;EAAAC,yBAAA,EAAAA,CAAA,KAAAA,yBAAA;EAAAC,uBAAA,EAAAA,CAAA,KAAAA,uBAAA;EAAAC,sBAAA,EAAAA,CAAA,KAAAA;AAAA;AAAAC,MAAA,CAAAC,OAAA,GAAAC,YAAA,CAAAX,0BAAA;AAAA,IAAAY,YAAA,GAAkBC,OAAA,CAAAC,OAAA;AAEX,SAASV,4BAA4BW,OAAA,EAASC,MAAA,EAAQ,CAAC;AACvD,SAASX,yBAAA,EAA2B;EACvC,OAAO;AACX;AACO,SAASE,wBAAA,EAA0B;EACtC,OAAO;AACX;AACO,SAASC,uBAAuBS,IAAA,EAAM;EACzC,OAAO;AACX;AACO,SAASX,0BAA0BW,IAAA,EAAM;EAC5C,OAAO;AACX;AACO,IAAIf,WAAA,GAA4B,eAAAU,YAAA,CAAAM,OAAA,CAAMC,UAAA,CAAW,UAASC,KAAA,EAAOH,IAAA,EAAM;EAC1E,OAAOG,KAAA,CAAMC,QAAA;AACjB,CAAC;AACM,IAAIlB,iBAAA,GAAkC,eAAAS,YAAA,CAAAM,OAAA,CAAMC,UAAA,CAAW,UAASC,KAAA,EAAOH,IAAA,EAAM;EAChF,OAAOG,KAAA,CAAMC,QAAA;AACjB,CAAC","ignoreList":[]}
@@ -5,14 +5,16 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
7
  var __copyProps = (to, from, except, desc) => {
8
- if (from && typeof from == "object" || typeof from == "function") for (let key of __getOwnPropNames(from)) !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, {
9
- get: () => from[key],
10
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
11
- });
8
+ if (from && typeof from === "object" || typeof from === "function") {
9
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
10
+ get: () => from[key],
11
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
12
+ });
13
+ }
12
14
  return to;
13
15
  };
14
16
  var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
15
- value: !0
17
+ value: true
16
18
  }), mod);
17
19
  var DismissableProps_exports = {};
18
20
  module.exports = __toCommonJS(DismissableProps_exports);
package/dist/jsx/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./Dismissable";
1
+ export * from "./Dismissable.mjs";
2
2
  //# sourceMappingURL=index.js.map
@@ -1,6 +1 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/index.ts"],
4
- "mappings": "AAAA,cAAc;",
5
- "names": []
6
- }
1
+ {"version":3,"names":[],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA,cAAc","ignoreList":[]}
@@ -5,15 +5,17 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
7
  var __copyProps = (to, from, except, desc) => {
8
- if (from && typeof from == "object" || typeof from == "function") for (let key of __getOwnPropNames(from)) !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, {
8
+ if (from && typeof from === "object" || typeof from === "function") {
9
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
9
10
  get: () => from[key],
10
11
  enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
11
12
  });
12
- return to;
13
- },
14
- __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
13
+ }
14
+ return to;
15
+ };
16
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
17
  var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
16
- value: !0
18
+ value: true
17
19
  }), mod);
18
20
  var index_exports = {};
19
21
  module.exports = __toCommonJS(index_exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/dismissable",
3
- "version": "2.0.0-rc.8",
3
+ "version": "2.0.0",
4
4
  "source": "src/index.ts",
5
5
  "files": [
6
6
  "src",
@@ -16,15 +16,12 @@
16
16
  "./package.json": "./package.json",
17
17
  ".": {
18
18
  "types": "./types/index.d.ts",
19
- "react-native": {
20
- "module": "./dist/esm/index.native.js",
21
- "import": "./dist/esm/index.native.js",
22
- "require": "./dist/cjs/index.native.js"
23
- },
19
+ "react-native": "./dist/esm/index.native.js",
20
+ "browser": "./dist/esm/index.mjs",
24
21
  "module": "./dist/esm/index.mjs",
25
22
  "import": "./dist/esm/index.mjs",
26
23
  "require": "./dist/cjs/index.cjs",
27
- "default": "./dist/cjs/index.native.js"
24
+ "default": "./dist/esm/index.mjs"
28
25
  }
29
26
  },
30
27
  "publishConfig": {
@@ -37,14 +34,14 @@
37
34
  "clean:build": "tamagui-build clean:build"
38
35
  },
39
36
  "dependencies": {
40
- "@tamagui/compose-refs": "2.0.0-rc.8",
41
- "@tamagui/core": "2.0.0-rc.8",
42
- "@tamagui/helpers": "2.0.0-rc.8",
43
- "@tamagui/use-escape-keydown": "2.0.0-rc.8",
44
- "@tamagui/use-event": "2.0.0-rc.8"
37
+ "@tamagui/compose-refs": "2.0.0",
38
+ "@tamagui/core": "2.0.0",
39
+ "@tamagui/helpers": "2.0.0",
40
+ "@tamagui/use-escape-keydown": "2.0.0",
41
+ "@tamagui/use-event": "2.0.0"
45
42
  },
46
43
  "devDependencies": {
47
- "@tamagui/build": "2.0.0-rc.8",
44
+ "@tamagui/build": "2.0.0",
48
45
  "react": ">=19",
49
46
  "react-dom": "*"
50
47
  },
@@ -2,12 +2,32 @@ import React from 'react'
2
2
 
3
3
  import type { DismissableBranchProps, DismissableProps } from './DismissableProps'
4
4
 
5
- // stub for native - not used but needed for export compatibility
5
+ // stubs for native - dismissable is a web-only concept
6
6
  export function dispatchDiscreteCustomEvent<E extends CustomEvent>(
7
7
  _target: E['target'],
8
8
  _event: E
9
9
  ) {}
10
10
 
11
+ export function getDismissableLayerCount(): number {
12
+ return 0
13
+ }
14
+
15
+ export function useHasDismissableLayers(): boolean {
16
+ return false
17
+ }
18
+
19
+ export function useIsInsideDismissable(
20
+ _ref: React.RefObject<HTMLElement | null>
21
+ ): boolean {
22
+ return false
23
+ }
24
+
25
+ export function useDismissableLayersAbove(
26
+ _ref: React.RefObject<HTMLElement | null>
27
+ ): number {
28
+ return 0
29
+ }
30
+
11
31
  export const Dismissable = React.forwardRef((props: DismissableProps, _ref) => {
12
32
  return props.children as any
13
33
  })
@@ -2,7 +2,7 @@
2
2
  // https://github.com/radix-ui/primitives/blob/cfd8dcba5fa6a0e751486af418d05a7b88a7f541/packages/react/dismissable-layer/src/DismissableLayer.tsx#L324
3
3
 
4
4
  import { useComposedRefs } from '@tamagui/compose-refs'
5
- import { Slot, View, composeEventHandlers } from '@tamagui/core'
5
+ import { Slot, TamaguiElement, View, composeEventHandlers } from '@tamagui/core'
6
6
  import { useEscapeKeydown } from '@tamagui/use-escape-keydown'
7
7
  import { useEvent } from '@tamagui/use-event'
8
8
  import * as React from 'react'
@@ -28,14 +28,128 @@ const FOCUS_OUTSIDE = 'dismissable.focusOutside'
28
28
 
29
29
  let originalBodyPointerEvents: string
30
30
 
31
+ // global layer tracking
32
+ const globalLayers = new Set<HTMLElement>()
33
+ const layerChangeListeners = new Set<() => void>()
34
+
35
+ // track if any layer has disableOutsidePointerEvents - only then do we need position updates
36
+ let layersWithPointerEventsDisabledCount = 0
37
+
38
+ function notifyLayerChange() {
39
+ for (const listener of layerChangeListeners) {
40
+ listener()
41
+ }
42
+ }
43
+
44
+ /**
45
+ * returns the number of active dismissable layers
46
+ * useful for non-React contexts (e.g. escape key handlers)
47
+ */
48
+ export function getDismissableLayerCount(): number {
49
+ return globalLayers.size
50
+ }
51
+
52
+ /**
53
+ * debug helper - logs what elements are registered as dismissable layers
54
+ */
55
+ export function debugDismissableLayers(): HTMLElement[] {
56
+ const layers = Array.from(globalLayers)
57
+ console.log('[Dismissable] Active layers:', layers.length, layers)
58
+ return layers
59
+ }
60
+
61
+ /**
62
+ * hook that returns true when any dismissable layer is active
63
+ * re-renders when the state changes
64
+ * uses module-level globals, not React context, so works anywhere in tree
65
+ */
66
+ export function useHasDismissableLayers(): boolean {
67
+ const [count, setCount] = React.useState(() => globalLayers.size)
68
+
69
+ React.useEffect(() => {
70
+ setCount(globalLayers.size)
71
+ const update = () => setCount(globalLayers.size)
72
+ layerChangeListeners.add(update)
73
+ return () => {
74
+ layerChangeListeners.delete(update)
75
+ }
76
+ }, [])
77
+
78
+ return count > 0
79
+ }
80
+
31
81
  const DismissableContext = React.createContext({
32
- layers: new Set<HTMLDivElement>(),
33
- layersWithOutsidePointerEventsDisabled: new Set<HTMLDivElement>(),
34
- branches: new Set<HTMLDivElement>(),
82
+ layers: new Set<HTMLElement>(),
83
+ layersWithOutsidePointerEventsDisabled: new Set<HTMLElement>(),
84
+ branches: new Set<HTMLElement>(),
35
85
  })
36
86
 
87
+ /**
88
+ * hook to check if a DOM element is inside an active dismissable layer
89
+ * useful for custom escape handling - if inside a dismissable, you may want to defer
90
+ */
91
+ export function useIsInsideDismissable(ref: React.RefObject<HTMLElement | null>) {
92
+ const context = React.useContext(DismissableContext)
93
+ const [isInside, setIsInside] = React.useState(false)
94
+
95
+ React.useEffect(() => {
96
+ const check = () => {
97
+ const el = ref.current
98
+ if (!el) {
99
+ setIsInside(false)
100
+ return
101
+ }
102
+ for (const layer of context.layers) {
103
+ if (layer.contains(el)) {
104
+ setIsInside(true)
105
+ return
106
+ }
107
+ }
108
+ setIsInside(false)
109
+ }
110
+
111
+ check()
112
+ document.addEventListener(CONTEXT_UPDATE, check)
113
+ return () => document.removeEventListener(CONTEXT_UPDATE, check)
114
+ }, [context.layers, ref])
115
+
116
+ return isInside
117
+ }
118
+
119
+ /**
120
+ * hook to check if there are dismissable layers above a given element
121
+ * returns the count of layers that are ancestors of the element
122
+ */
123
+ export function useDismissableLayersAbove(ref: React.RefObject<HTMLElement | null>) {
124
+ const context = React.useContext(DismissableContext)
125
+ const [count, setCount] = React.useState(0)
126
+
127
+ React.useEffect(() => {
128
+ const check = () => {
129
+ const el = ref.current
130
+ if (!el) {
131
+ setCount(0)
132
+ return
133
+ }
134
+ let above = 0
135
+ for (const layer of context.layers) {
136
+ if (layer.contains(el)) {
137
+ above++
138
+ }
139
+ }
140
+ setCount(above)
141
+ }
142
+
143
+ check()
144
+ document.addEventListener(CONTEXT_UPDATE, check)
145
+ return () => document.removeEventListener(CONTEXT_UPDATE, check)
146
+ }, [context.layers, ref])
147
+
148
+ return count
149
+ }
150
+
37
151
  const Dismissable = React.forwardRef<
38
- HTMLDivElement,
152
+ HTMLElement,
39
153
  DismissableProps & { asChild?: boolean }
40
154
  >((props, forwardedRef) => {
41
155
  const {
@@ -48,13 +162,16 @@ const Dismissable = React.forwardRef<
48
162
  onDismiss,
49
163
  asChild,
50
164
  children,
165
+ branches: branchesProp,
51
166
  ...layerProps
52
167
  } = props
53
- const Comp = (asChild ? Slot : View) as any
168
+ const Comp = asChild ? Slot : View
54
169
  const context = React.useContext(DismissableContext)
55
- const [node, setNode] = React.useState<HTMLDivElement | null>(null)
170
+ const [node, setNode] = React.useState<HTMLElement | null>(null)
56
171
  const [, force] = React.useState({})
57
- const composedRefs = useComposedRefs(forwardedRef, (node) => setNode(node))
172
+ const composedRefs = useComposedRefs(forwardedRef, (node: HTMLElement | null) =>
173
+ setNode(node)
174
+ )
58
175
  const layers = Array.from(context.layers)
59
176
 
60
177
  const [highestLayerWithOutsidePointerEventsDisabled] = [
@@ -71,10 +188,10 @@ const Dismissable = React.forwardRef<
71
188
  index >= highestLayerWithOutsidePointerEventsDisabledIndex
72
189
 
73
190
  const pointerDownOutside = usePointerDownOutside((event) => {
74
- const target = event.target as HTMLDivElement
75
- const isPointerDownOnBranch = [...context.branches].some((branch) =>
76
- branch.contains(target)
77
- )
191
+ const target = event.target as HTMLElement
192
+ // check prop-based branches first (scoped to this dismissable), then global branches
193
+ const branches = branchesProp || context.branches
194
+ const isPointerDownOnBranch = [...branches].some((branch) => branch.contains(target))
78
195
  if (!isPointerEventsEnabled || isPointerDownOnBranch) return
79
196
  onPointerDownOutside?.(event)
80
197
  onInteractOutside?.(event)
@@ -82,17 +199,25 @@ const Dismissable = React.forwardRef<
82
199
  })
83
200
 
84
201
  const focusOutside = useFocusOutside((event) => {
85
- const target = event.target as HTMLDivElement
86
- const isFocusInBranch = [...context.branches].some((branch) =>
87
- branch.contains(target)
88
- )
202
+ const target = event.target as HTMLElement
203
+ // check prop-based branches first (scoped to this dismissable), then global branches
204
+ const branches = branchesProp || context.branches
205
+ const isFocusInBranch = [...branches].some((branch) => branch.contains(target))
89
206
  if (isFocusInBranch) return
90
207
  onFocusOutside?.(event)
91
208
  onInteractOutside?.(event)
92
209
  if (!event.defaultPrevented) onDismiss?.()
93
210
  })
94
211
 
212
+ // track forceUnmount in a ref so escape handler can check it
213
+ const forceUnmountRef = React.useRef(forceUnmount)
214
+ React.useEffect(() => {
215
+ forceUnmountRef.current = forceUnmount
216
+ }, [forceUnmount])
217
+
95
218
  useEscapeKeydown((event) => {
219
+ // skip if this layer is force-unmounted (e.g. dialog closed but still mounted)
220
+ if (forceUnmountRef.current) return
96
221
  // Check layers at callback time, not render time, to avoid stale closures
97
222
  const currentLayers = Array.from(context.layers)
98
223
  const currentIndex = node ? currentLayers.indexOf(node) : -1
@@ -107,24 +232,32 @@ const Dismissable = React.forwardRef<
107
232
 
108
233
  React.useEffect(() => {
109
234
  if (!node) return
235
+ // don't add to layers when force-unmounted (dialog closed but still mounted)
236
+ if (forceUnmount) return
110
237
  if (disableOutsidePointerEvents) {
111
238
  if (context.layersWithOutsidePointerEventsDisabled.size === 0) {
112
239
  originalBodyPointerEvents = document.body.style.pointerEvents
113
240
  document.body.style.pointerEvents = 'none'
114
241
  }
115
242
  context.layersWithOutsidePointerEventsDisabled.add(node)
243
+ layersWithPointerEventsDisabledCount++
116
244
  }
117
245
  context.layers.add(node)
118
- dispatchUpdate()
246
+ globalLayers.add(node)
247
+ // only dispatch position update when pointer-events tracking is active
248
+ if (disableOutsidePointerEvents || layersWithPointerEventsDisabledCount > 0) {
249
+ dispatchUpdate()
250
+ }
251
+ notifyLayerChange()
119
252
  return () => {
120
- if (
121
- disableOutsidePointerEvents &&
122
- context.layersWithOutsidePointerEventsDisabled.size === 1
123
- ) {
124
- document.body.style.pointerEvents = originalBodyPointerEvents
253
+ if (disableOutsidePointerEvents) {
254
+ if (context.layersWithOutsidePointerEventsDisabled.size === 1) {
255
+ document.body.style.pointerEvents = originalBodyPointerEvents
256
+ }
257
+ // decrement AFTER dispatch so other layers still re-render
125
258
  }
126
259
  }
127
- }, [node, disableOutsidePointerEvents, context])
260
+ }, [node, disableOutsidePointerEvents, forceUnmount, context])
128
261
 
129
262
  /**
130
263
  * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect
@@ -136,15 +269,30 @@ const Dismissable = React.forwardRef<
136
269
  if (forceUnmount) return
137
270
  return () => {
138
271
  if (!node) return
272
+ const hadPointerEventsDisabled =
273
+ context.layersWithOutsidePointerEventsDisabled.has(node)
139
274
  context.layers.delete(node)
140
275
  context.layersWithOutsidePointerEventsDisabled.delete(node)
141
- dispatchUpdate()
276
+ globalLayers.delete(node)
277
+ // only dispatch position update when pointer-events tracking is active
278
+ if (layersWithPointerEventsDisabledCount > 0) {
279
+ dispatchUpdate()
280
+ }
281
+ notifyLayerChange()
282
+ // decrement count AFTER dispatch so other layers see count > 0 and re-render
283
+ if (hadPointerEventsDisabled) {
284
+ layersWithPointerEventsDisabledCount--
285
+ }
142
286
  }
143
287
  }, [node, context, forceUnmount])
144
288
 
145
289
  React.useEffect(() => {
146
290
  const handleUpdate = () => {
147
- force({})
291
+ // only force re-render if we need to track layer positions for pointer-events
292
+ // this avoids N^2 re-renders when multiple dismissables mount/unmount
293
+ if (layersWithPointerEventsDisabledCount > 0) {
294
+ force({})
295
+ }
148
296
  }
149
297
  document.addEventListener(CONTEXT_UPDATE, handleUpdate)
150
298
  return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate)
@@ -155,7 +303,9 @@ const Dismissable = React.forwardRef<
155
303
  {...layerProps}
156
304
  // @ts-ignore
157
305
  ref={composedRefs}
158
- display="contents"
306
+ {...(!asChild && {
307
+ display: 'contents',
308
+ })}
159
309
  pointerEvents={
160
310
  isBodyPointerEventsDisabled
161
311
  ? isPointerEventsEnabled
@@ -189,23 +339,27 @@ Dismissable.displayName = DISMISSABLE_LAYER_NAME
189
339
 
190
340
  const BRANCH_NAME = 'DismissableBranch'
191
341
 
192
- const DismissableBranch = React.forwardRef<HTMLDivElement, DismissableBranchProps>(
342
+ const DismissableBranch = React.forwardRef<TamaguiElement, DismissableBranchProps>(
193
343
  (props, forwardedRef) => {
344
+ const { branches: branchesProp, ...rest } = props
194
345
  const context = React.useContext(DismissableContext)
195
- const ref = React.useRef<HTMLDivElement>(null)
346
+ const ref = React.useRef<TamaguiElement>(null)
196
347
  const composedRefs = useComposedRefs(forwardedRef, ref)
197
348
 
198
349
  React.useEffect(() => {
199
350
  const node = ref.current
200
- if (node) {
201
- context.branches.add(node)
351
+ if (!(node instanceof HTMLElement)) return
352
+ // use prop-based branches if provided, otherwise fall back to global context
353
+ const branches = branchesProp || context.branches
354
+ if (node && branches) {
355
+ branches.add(node)
202
356
  return () => {
203
- context.branches.delete(node)
357
+ branches.delete(node)
204
358
  }
205
359
  }
206
- }, [context.branches])
360
+ }, [branchesProp, context.branches])
207
361
 
208
- return <div style={{ display: 'contents' }} {...props} ref={composedRefs} />
362
+ return <View asChild="except-style" {...rest} ref={composedRefs} />
209
363
  }
210
364
  )
211
365
 
@@ -9,6 +9,11 @@ export interface DismissableProps {
9
9
  * interact with them: once to close the `Dismissable`, and again to trigger the element.
10
10
  */
11
11
  disableOutsidePointerEvents?: boolean
12
+ /**
13
+ * Optional Set of branch elements that should not trigger dismissal.
14
+ * Pass the same Set to DismissableBranch components to scope them to this Dismissable.
15
+ */
16
+ branches?: Set<HTMLElement>
12
17
  /**
13
18
  * Event handler called when the escape key is down.
14
19
  * Can be prevented.
@@ -50,4 +55,9 @@ export interface DismissableProps {
50
55
 
51
56
  export interface DismissableBranchProps {
52
57
  children?: React.ReactNode
58
+ /**
59
+ * Optional Set to register this branch with.
60
+ * Pass the same Set to the Dismissable to scope this branch to that specific layer.
61
+ */
62
+ branches?: Set<HTMLElement>
53
63
  }
@@ -1,10 +1,36 @@
1
+ import { TamaguiElement } from '@tamagui/core';
1
2
  import * as React from 'react';
2
3
  import type { DismissableBranchProps, DismissableProps } from './DismissableProps';
3
4
  export declare function dispatchDiscreteCustomEvent<E extends CustomEvent>(target: E['target'], event: E): void;
5
+ /**
6
+ * returns the number of active dismissable layers
7
+ * useful for non-React contexts (e.g. escape key handlers)
8
+ */
9
+ export declare function getDismissableLayerCount(): number;
10
+ /**
11
+ * debug helper - logs what elements are registered as dismissable layers
12
+ */
13
+ export declare function debugDismissableLayers(): HTMLElement[];
14
+ /**
15
+ * hook that returns true when any dismissable layer is active
16
+ * re-renders when the state changes
17
+ * uses module-level globals, not React context, so works anywhere in tree
18
+ */
19
+ export declare function useHasDismissableLayers(): boolean;
20
+ /**
21
+ * hook to check if a DOM element is inside an active dismissable layer
22
+ * useful for custom escape handling - if inside a dismissable, you may want to defer
23
+ */
24
+ export declare function useIsInsideDismissable(ref: React.RefObject<HTMLElement | null>): boolean;
25
+ /**
26
+ * hook to check if there are dismissable layers above a given element
27
+ * returns the count of layers that are ancestors of the element
28
+ */
29
+ export declare function useDismissableLayersAbove(ref: React.RefObject<HTMLElement | null>): number;
4
30
  declare const Dismissable: React.ForwardRefExoticComponent<DismissableProps & {
5
31
  asChild?: boolean;
6
- } & React.RefAttributes<HTMLDivElement>>;
7
- declare const DismissableBranch: React.ForwardRefExoticComponent<DismissableBranchProps & React.RefAttributes<HTMLDivElement>>;
32
+ } & React.RefAttributes<HTMLElement>>;
33
+ declare const DismissableBranch: React.ForwardRefExoticComponent<DismissableBranchProps & React.RefAttributes<TamaguiElement>>;
8
34
  export type PointerDownOutsideEvent = CustomEvent<{
9
35
  originalEvent: PointerEvent;
10
36
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"Dismissable.d.ts","sourceRoot":"","sources":["../src/Dismissable.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAElF,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,WAAW,EAC/D,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,EACnB,KAAK,EAAE,CAAC,QAGT;AAmBD,QAAA,MAAM,WAAW;cAEgB,OAAO;wCA+ItC,CAAA;AAUF,QAAA,MAAM,iBAAiB,+FAkBtB,CAAA;AAMD,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;IAAE,aAAa,EAAE,YAAY,CAAA;CAAE,CAAC,CAAA;AAClF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;IAAE,aAAa,EAAE,UAAU,CAAA;CAAE,CAAC,CAAA;AAsI1E,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAEzC,YAAY,EAAE,gBAAgB,EAAE,CAAA"}
1
+ {"version":3,"file":"Dismissable.d.ts","sourceRoot":"","sources":["../src/Dismissable.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAQ,cAAc,EAA8B,MAAM,eAAe,CAAA;AAGhF,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAElF,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,WAAW,EAC/D,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,EACnB,KAAK,EAAE,CAAC,QAGT;AA0BD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,WAAW,EAAE,CAItD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAajD;AAQD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,WA0B9E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,UA0BjF;AAED,QAAA,MAAM,WAAW;cAEgB,OAAO;qCAmLtC,CAAA;AAUF,QAAA,MAAM,iBAAiB,+FAsBtB,CAAA;AAMD,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;IAAE,aAAa,EAAE,YAAY,CAAA;CAAE,CAAC,CAAA;AAClF,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC;IAAE,aAAa,EAAE,UAAU,CAAA;CAAE,CAAC,CAAA;AAsI1E,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAA;AAEzC,YAAY,EAAE,gBAAgB,EAAE,CAAA"}
@@ -1,6 +1,10 @@
1
1
  import React from 'react';
2
2
  import type { DismissableBranchProps, DismissableProps } from './DismissableProps';
3
3
  export declare function dispatchDiscreteCustomEvent<E extends CustomEvent>(_target: E['target'], _event: E): void;
4
+ export declare function getDismissableLayerCount(): number;
5
+ export declare function useHasDismissableLayers(): boolean;
6
+ export declare function useIsInsideDismissable(_ref: React.RefObject<HTMLElement | null>): boolean;
7
+ export declare function useDismissableLayersAbove(_ref: React.RefObject<HTMLElement | null>): number;
4
8
  export declare const Dismissable: React.ForwardRefExoticComponent<DismissableProps & React.RefAttributes<unknown>>;
5
9
  export declare const DismissableBranch: React.ForwardRefExoticComponent<DismissableBranchProps & React.RefAttributes<unknown>>;
6
10
  //# sourceMappingURL=Dismissable.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dismissable.native.d.ts","sourceRoot":"","sources":["../src/Dismissable.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGlF,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,WAAW,EAC/D,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,EACpB,MAAM,EAAE,CAAC,QACP;AAEJ,eAAO,MAAM,WAAW,kFAEtB,CAAA;AAEF,eAAO,MAAM,iBAAiB,wFAI7B,CAAA"}
1
+ {"version":3,"file":"Dismissable.native.d.ts","sourceRoot":"","sources":["../src/Dismissable.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGlF,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,WAAW,EAC/D,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,EACpB,MAAM,EAAE,CAAC,QACP;AAEJ,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GACxC,OAAO,CAET;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GACxC,MAAM,CAER;AAED,eAAO,MAAM,WAAW,kFAEtB,CAAA;AAEF,eAAO,MAAM,iBAAiB,wFAI7B,CAAA"}