@jielga/react-popup-window 0.1.0 → 0.1.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 -21
- package/README.md +335 -249
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/usePopupWindow.d.ts.map +1 -1
- package/package.json +101 -100
- package/skills/getting-started/SKILL.md +211 -211
- package/skills/popup-content/SKILL.md +422 -227
|
@@ -1,227 +1,422 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: popup-content
|
|
3
|
-
description: >
|
|
4
|
-
Render real application UI inside the popup window: stylesheet synchronization
|
|
5
|
-
(copyStyles, CSSOM serialization, root class and data-* attributes,
|
|
6
|
-
adoptedStyleSheets), theme switching across windows, bounded-height layouts
|
|
7
|
-
for virtualized content, and third-party component libraries whose overlays
|
|
8
|
-
portal to document.body (Mantine, Radix, MUI). Load when popup content renders
|
|
9
|
-
unstyled, a theme change does not reach the popup, or menus, popovers, modals,
|
|
10
|
-
and tooltips open in the main window instead of the popup
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- 'Jielga/react-popup-window:
|
|
20
|
-
- 'Jielga/react-popup-window:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1
|
+
---
|
|
2
|
+
name: popup-content
|
|
3
|
+
description: >
|
|
4
|
+
Render real application UI inside the popup window: stylesheet synchronization
|
|
5
|
+
(copyStyles, CSSOM serialization, root class and data-* attributes,
|
|
6
|
+
adoptedStyleSheets), theme switching across windows, bounded-height layouts
|
|
7
|
+
for virtualized content, and third-party component libraries whose overlays
|
|
8
|
+
portal to document.body (Mantine, Radix, MUI). Load when popup content renders
|
|
9
|
+
unstyled, a theme change does not reach the popup, or menus, popovers, modals,
|
|
10
|
+
and tooltips open in the main window instead of the popup — including when a
|
|
11
|
+
portal redirect is already in place but has no effect.
|
|
12
|
+
metadata:
|
|
13
|
+
type: sub-skill
|
|
14
|
+
library: '@jielga/react-popup-window'
|
|
15
|
+
library_version: '0.1.1'
|
|
16
|
+
requires:
|
|
17
|
+
- '@jielga/react-popup-window/getting-started'
|
|
18
|
+
sources:
|
|
19
|
+
- 'Jielga/react-popup-window:README.md'
|
|
20
|
+
- 'Jielga/react-popup-window:src/copyStyles.ts'
|
|
21
|
+
- 'Jielga/react-popup-window:docs/src/examples/SameWindowPortals.tsx'
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# react-popup-window — Popup content
|
|
25
|
+
|
|
26
|
+
This skill builds on getting-started. Read it first for the portal model
|
|
27
|
+
and hook API.
|
|
28
|
+
|
|
29
|
+
The popup document starts as an unstyled `about:blank` page. While it is
|
|
30
|
+
open, the library mirrors the opener's styling into it and keeps the mirror
|
|
31
|
+
current:
|
|
32
|
+
|
|
33
|
+
- `<style>` and `<link rel="stylesheet">` elements are copied into the
|
|
34
|
+
popup `<head>`. `<style>` contents are serialized from the CSSOM, so
|
|
35
|
+
rules injected with `insertRule` (CSS-in-JS) are included at copy time.
|
|
36
|
+
- Additions, removals, and edits of style nodes in the opener's `<head>`
|
|
37
|
+
are observed and re-mirrored. This covers Vite HMR and lazily loaded
|
|
38
|
+
chunk CSS.
|
|
39
|
+
- `class`, `style`, and `data-*` attributes on `<html>` and `<body>` are
|
|
40
|
+
mirrored and kept in sync. Theme systems keyed on a root class or data
|
|
41
|
+
attribute (for example Mantine's `data-mantine-color-scheme`) follow
|
|
42
|
+
automatically.
|
|
43
|
+
- `document.adoptedStyleSheets` are reconstructed in the popup document.
|
|
44
|
+
|
|
45
|
+
`copyStyles: false` disables all of it. The mechanism is also exported
|
|
46
|
+
standalone as `copyStyles(source, target, watch?)`, returning a `stop`
|
|
47
|
+
function, for windows managed outside the hook.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
Redirect portal-based overlays into the popup document. Component libraries
|
|
52
|
+
mount menus, popovers, modals, and tooltips into `document.body`. Popup
|
|
53
|
+
content executes in the opener's realm, so the bare `document` global is
|
|
54
|
+
the main window's document even for components rendered in the popup,
|
|
55
|
+
while `ownerDocument` of a mounted node is the popup's document. For
|
|
56
|
+
Mantine, a wrapper can supply the correct body through theme default
|
|
57
|
+
props:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { MantineThemeProvider, Portal } from '@mantine/core'
|
|
61
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
62
|
+
import type { ReactNode } from 'react'
|
|
63
|
+
|
|
64
|
+
export function SameWindowPortals({ children }: { children: ReactNode }) {
|
|
65
|
+
const probeRef = useRef<HTMLDivElement>(null)
|
|
66
|
+
const [target, setTarget] = useState<HTMLElement | null>(null)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setTarget(probeRef.current?.ownerDocument.body ?? null)
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const theme = useMemo(
|
|
73
|
+
() => ({
|
|
74
|
+
components: {
|
|
75
|
+
Portal: Portal.extend({ defaultProps: target ? { target } : {} }),
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
[target],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div ref={probeRef} style={{ display: 'contents' }}>
|
|
83
|
+
<MantineThemeProvider inherit theme={theme}>
|
|
84
|
+
{children}
|
|
85
|
+
</MantineThemeProvider>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The wrapper resolves its own `ownerDocument` after mount, so the same
|
|
92
|
+
component works inline (main body, the default behavior) and inside
|
|
93
|
+
`<Popup>` (popup body). This covers `Modal` too: it portals through the
|
|
94
|
+
same theme-resolved `Portal`. Because the target resolves in an effect, an
|
|
95
|
+
overlay that opens during the very first render still lands in the main
|
|
96
|
+
body; user-triggered overlays open after mount and are unaffected.
|
|
97
|
+
|
|
98
|
+
For other libraries, use their per-component portal container prop
|
|
99
|
+
(`container` in Radix and MUI) with an element of the popup document:
|
|
100
|
+
`popupWindow.document.body` where the hook's result is in scope, or
|
|
101
|
+
`node.ownerDocument.body` from a ref on any rendered node deeper in the
|
|
102
|
+
tree (the same probe the wrapper uses).
|
|
103
|
+
|
|
104
|
+
The rule is the same for any portal, including a raw `createPortal` in
|
|
105
|
+
application code: the container element must belong to the popup document.
|
|
106
|
+
|
|
107
|
+
To verify a redirect, open the overlay and check its mounted node's
|
|
108
|
+
`ownerDocument`. With an explicit `target`, Mantine portals directly into
|
|
109
|
+
the target and appends no `[data-portal]` node; that node exists only for
|
|
110
|
+
the default, untargeted portal.
|
|
111
|
+
|
|
112
|
+
## Core patterns
|
|
113
|
+
|
|
114
|
+
### Full-height popup layout
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<Popup>
|
|
118
|
+
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
119
|
+
<VirtualizedGrid style={{ flex: 1, minHeight: 0 }} />
|
|
120
|
+
</div>
|
|
121
|
+
</Popup>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The portal container is an unsized `<div>` in the popup body. Content that
|
|
125
|
+
measures itself — virtualized lists, grids, editors — needs an explicit
|
|
126
|
+
bounded height; the popup viewport (`100vh`) is the natural bound.
|
|
127
|
+
|
|
128
|
+
### Theme switching across windows
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
// A root-attribute theme reaches the popup with no additional wiring:
|
|
132
|
+
document.documentElement.classList.toggle('dark', dark)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Root `class` and `data-*` attributes are mirrored while the popup is open,
|
|
136
|
+
so any CSS keyed on them applies in both windows.
|
|
137
|
+
|
|
138
|
+
## Common mistakes
|
|
139
|
+
|
|
140
|
+
### HIGH Overlays from UI libraries open in the main window
|
|
141
|
+
|
|
142
|
+
Wrong:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<Popup>
|
|
146
|
+
<DataGrid /> {/* column menu portals to the main window's document.body */}
|
|
147
|
+
</Popup>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Correct:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<Popup>
|
|
154
|
+
<SameWindowPortals>
|
|
155
|
+
<DataGrid />
|
|
156
|
+
</SameWindowPortals>
|
|
157
|
+
</Popup>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Portal-based overlays default to the global `document.body`. The content
|
|
161
|
+
sits in the popup document, but the overlay mounts — and positions itself —
|
|
162
|
+
in the main window. Supply a portal target inside the popup document.
|
|
163
|
+
|
|
164
|
+
Source: docs/src/examples/SameWindowPortals.tsx
|
|
165
|
+
|
|
166
|
+
### HIGH Portal target resolved outside the Popup
|
|
167
|
+
|
|
168
|
+
Wrong:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
<SameWindowPortals> {/* probe mounts in the MAIN document */}
|
|
172
|
+
<Popup>
|
|
173
|
+
<DataGrid />
|
|
174
|
+
</Popup>
|
|
175
|
+
</SameWindowPortals>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Correct:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Popup>
|
|
182
|
+
<SameWindowPortals>
|
|
183
|
+
<DataGrid />
|
|
184
|
+
</SameWindowPortals>
|
|
185
|
+
</Popup>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Whatever supplies the portal container (a wrapper like this, a ref, a
|
|
189
|
+
`useMemo`) must itself run inside `<Popup>`, because it resolves the
|
|
190
|
+
container from the tree it mounts in.
|
|
191
|
+
Outside `<Popup>` it resolves the main document's body, which is the
|
|
192
|
+
default behavior, so overlays still open in the main window.
|
|
193
|
+
|
|
194
|
+
Source: docs/src/examples/SameWindowPortals.tsx
|
|
195
|
+
|
|
196
|
+
### HIGH Provider between the redirect and the overlay resets it
|
|
197
|
+
|
|
198
|
+
Wrong:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<Popup>
|
|
202
|
+
<SameWindowPortals>
|
|
203
|
+
<MantineProvider theme={theme}> {/* replaces theme.components */}
|
|
204
|
+
<DataGrid />
|
|
205
|
+
</MantineProvider>
|
|
206
|
+
</SameWindowPortals>
|
|
207
|
+
</Popup>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Correct:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<Popup>
|
|
214
|
+
<SameWindowPortals>
|
|
215
|
+
{/* the main window's providers reach popup content through context */}
|
|
216
|
+
<DataGrid />
|
|
217
|
+
</SameWindowPortals>
|
|
218
|
+
</Popup>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A context-based portal redirect is lost when a provider below it replaces
|
|
222
|
+
that context instead of merging with it.
|
|
223
|
+
In Mantine, `MantineProvider` (and `MantineThemeProvider` without
|
|
224
|
+
`inherit`) rebuilds the theme and drops the `Portal` default props.
|
|
225
|
+
Popup content already receives the main window's providers through context;
|
|
226
|
+
for a local theme override, use `MantineThemeProvider` with `inherit`
|
|
227
|
+
inside the wrapper.
|
|
228
|
+
|
|
229
|
+
Source: docs/src/examples/SameWindowPortals.tsx
|
|
230
|
+
|
|
231
|
+
### HIGH Expecting Escape and scroll lock to follow a redirected modal
|
|
232
|
+
|
|
233
|
+
Wrong:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
// inside <Popup>, portal redirect in place
|
|
237
|
+
<Modal opened={opened} onClose={closeModal} />
|
|
238
|
+
{/* renders in the popup, but Escape pressed there does not close it */}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Correct:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
<Modal opened={opened} onClose={closeModal} />
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
// plus: bind Escape on the window the content is rendered in
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!opened) return
|
|
251
|
+
const win = hostRef.current?.ownerDocument.defaultView
|
|
252
|
+
const onKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && closeModal()
|
|
253
|
+
win?.addEventListener('keydown', onKeyDown)
|
|
254
|
+
return () => win?.removeEventListener('keydown', onKeyDown)
|
|
255
|
+
}, [opened, closeModal])
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A redirected portal moves only the overlay's DOM. Handlers the component
|
|
259
|
+
binds on the bare `window` still attach to the opener: Mantine's `Modal`
|
|
260
|
+
listens for Escape there and locks scroll on the main window's body, so
|
|
261
|
+
Escape pressed in the popup does not close a modal that renders correctly
|
|
262
|
+
in it. (The popup body appears scroll-locked as well only because root
|
|
263
|
+
attributes are mirrored.)
|
|
264
|
+
This cannot be redirected from outside the component; re-implement
|
|
265
|
+
window-level behavior you own with listeners on the popup window.
|
|
266
|
+
Same failure family as "Listening on the wrong window object" below; here
|
|
267
|
+
the listener sits inside the third-party component.
|
|
268
|
+
|
|
269
|
+
Source: docs/src/examples/DataTableExample.tsx, @mantine/core ModalBase (useWindowEvent)
|
|
270
|
+
|
|
271
|
+
### HIGH Unbounded height collapses measured content
|
|
272
|
+
|
|
273
|
+
Wrong:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<Popup>
|
|
277
|
+
<VirtualizedGrid style={{ flex: 1, minHeight: 0 }} /> {/* parent has no height */}
|
|
278
|
+
</Popup>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Correct:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<Popup>
|
|
285
|
+
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
286
|
+
<VirtualizedGrid style={{ flex: 1, minHeight: 0 }} />
|
|
287
|
+
</div>
|
|
288
|
+
</Popup>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`flex: 1` resolves against a sized parent. Without one, a virtualizer
|
|
292
|
+
measures zero height and renders no rows, or the content renders at full
|
|
293
|
+
natural height and scrolls the popup body instead.
|
|
294
|
+
|
|
295
|
+
Source: README.md
|
|
296
|
+
|
|
297
|
+
### MEDIUM Per-component portal props override the redirect
|
|
298
|
+
|
|
299
|
+
Wrong:
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
// inside <SameWindowPortals>
|
|
303
|
+
<Menu portalProps={{ target: document.body }}> {/* the MAIN window's body */}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Correct:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<Menu> {/* no portal props; the redirected default applies */}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A theme- or context-level default applies only where the component does not
|
|
313
|
+
set the prop itself.
|
|
314
|
+
An explicit `portalProps`, `container`, or `target` overrides the
|
|
315
|
+
redirect, including one hardcoded inside an intermediate library
|
|
316
|
+
component.
|
|
317
|
+
`withinPortal={false}` is safe: the overlay renders inline, inside the
|
|
318
|
+
popup document.
|
|
319
|
+
|
|
320
|
+
Source: @mantine/core Portal (theme defaultProps resolution)
|
|
321
|
+
|
|
322
|
+
### MEDIUM Duplicate copies of the UI library
|
|
323
|
+
|
|
324
|
+
Wrong:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
$ npm ls @mantine/core
|
|
328
|
+
├── @mantine/core@9.5.1
|
|
329
|
+
└─┬ some-grid-library@2.0.0
|
|
330
|
+
└── @mantine/core@9.4.0
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Correct:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
$ npm ls @mantine/core
|
|
337
|
+
└── @mantine/core@9.5.1 # one copy, deduped everywhere
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Each installed copy of a UI library creates its own React context, so the
|
|
341
|
+
redirect is written into one copy's context and the overlay reads the
|
|
342
|
+
other's.
|
|
343
|
+
No error is raised; the redirect is silently ignored.
|
|
344
|
+
|
|
345
|
+
Source: @mantine/core (one theme context per installed copy)
|
|
346
|
+
|
|
347
|
+
### MEDIUM Selector string as portal target
|
|
348
|
+
|
|
349
|
+
Wrong:
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
Portal.extend({ defaultProps: { target: '#popup-root' } }) // resolved in the MAIN document
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Correct:
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
Portal.extend({ defaultProps: { target: probeRef.current.ownerDocument.body } })
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Portal code runs in the opener's realm, so a selector string is resolved
|
|
362
|
+
with the main document's `querySelector` regardless of where the component
|
|
363
|
+
renders.
|
|
364
|
+
Pass an element that belongs to the popup document.
|
|
365
|
+
|
|
366
|
+
Source: @mantine/core Portal (getTargetNode)
|
|
367
|
+
|
|
368
|
+
### MEDIUM Listening on the wrong window object
|
|
369
|
+
|
|
370
|
+
Wrong:
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
// inside <Popup> content
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
window.addEventListener('resize', onResize) // main window: closures keep the opener's globals
|
|
376
|
+
return () => window.removeEventListener('resize', onResize)
|
|
377
|
+
}, [])
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Correct:
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
const { popupWindow } = usePopupWindow(/* ... */)
|
|
384
|
+
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!popupWindow) return
|
|
387
|
+
popupWindow.addEventListener('resize', onResize)
|
|
388
|
+
return () => popupWindow.removeEventListener('resize', onResize)
|
|
389
|
+
}, [popupWindow])
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Portal content executes in the main window's realm; `window` in its
|
|
393
|
+
closures is the opener. Window-level events of the popup — resize, scroll,
|
|
394
|
+
message — require listeners on the `popupWindow` object.
|
|
395
|
+
|
|
396
|
+
Source: README.md (Communication)
|
|
397
|
+
|
|
398
|
+
### MEDIUM Expecting CSSOM-only rule changes to sync after open
|
|
399
|
+
|
|
400
|
+
Wrong:
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
// after the popup is open
|
|
404
|
+
someStyleSheet.insertRule('.late { color: red }') // no DOM mutation; not observed
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Correct:
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
// inject a new <style> element instead; node additions are observed
|
|
411
|
+
const el = document.createElement('style')
|
|
412
|
+
el.textContent = '.late { color: red }'
|
|
413
|
+
document.head.appendChild(el)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Style synchronization serializes each sheet when it is copied and re-reads
|
|
417
|
+
it when its DOM node changes. A rule inserted directly into an existing
|
|
418
|
+
sheet's CSSOM after the popup opened produces no mutation and is not
|
|
419
|
+
re-mirrored. Most CSS-in-JS libraries create or update style elements and
|
|
420
|
+
are unaffected.
|
|
421
|
+
|
|
422
|
+
Source: src/copyStyles.ts
|