@jielga/react-popup-window 0.1.0 → 0.2.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.
- package/LICENSE +21 -21
- package/README.md +371 -249
- package/dist/copyStyles.d.ts +11 -0
- package/dist/copyStyles.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -72
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +26 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/usePopupWindow.d.ts.map +1 -1
- package/dist/whenStylesheetsLoad.d.ts +13 -0
- package/dist/whenStylesheetsLoad.d.ts.map +1 -0
- package/package.json +101 -100
- package/skills/getting-started/SKILL.md +229 -211
- package/skills/popup-content/SKILL.md +428 -227
package/README.md
CHANGED
|
@@ -1,249 +1,371 @@
|
|
|
1
|
-
# @jielga/react-popup-window
|
|
2
|
-
|
|
3
|
-
[](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml)
|
|
4
|
-
[](https://www.npmjs.com/package/@jielga/react-popup-window)
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
|
|
7
|
-
React hook for rendering part of a component tree in a separate browser
|
|
8
|
-
window. Content is rendered through a portal into the popup document, so it
|
|
9
|
-
remains part of the calling tree: state, context, and event handlers work
|
|
10
|
-
across windows without bridging.
|
|
11
|
-
|
|
12
|
-
[Documentation and live examples](https://jielga.github.io/react-popup-window/)
|
|
13
|
-
|
|
14
|
-
## Features
|
|
15
|
-
|
|
16
|
-
- Portal-based rendering — popup content keeps access to all ancestor
|
|
17
|
-
context (state managers, data fetching, theming, routing)
|
|
18
|
-
- Stylesheet synchronization — `<style>` and `<link>` elements, root
|
|
19
|
-
`class`/`data-*` attributes, and `adoptedStyleSheets` are mirrored into
|
|
20
|
-
the popup and kept current while it is open
|
|
21
|
-
- Lifecycle management — detects the user closing the window, closes the
|
|
22
|
-
popup on owner unmount and opener unload, reports blocked popups
|
|
23
|
-
- No dependencies beyond `react` and `react-dom`
|
|
24
|
-
- TypeScript, ESM and CJS builds, SSR-safe
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
npm install @jielga/react-popup-window
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Requires React 19.2 or later.
|
|
33
|
-
|
|
34
|
-
## Usage
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { usePopupWindow } from '@jielga/react-popup-window'
|
|
38
|
-
|
|
39
|
-
function Dashboard() {
|
|
40
|
-
const { open, close, isOpen, Popup } = usePopupWindow({
|
|
41
|
-
title: 'Detached panel',
|
|
42
|
-
features: { width: 640, height: 480 },
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<>
|
|
47
|
-
<button onClick={open}>Open in new window</button>
|
|
48
|
-
<Popup>
|
|
49
|
-
<MyPanel />
|
|
50
|
-
</Popup>
|
|
51
|
-
</>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`Popup` renders its children into the popup window while open and nothing
|
|
57
|
-
otherwise. It has a stable identity and can be destructured from the hook
|
|
58
|
-
result.
|
|
59
|
-
|
|
60
|
-
### Detaching a section
|
|
61
|
-
|
|
62
|
-
To hide a section in the main window while it is popped out, render it in
|
|
63
|
-
one of two places depending on `isOpen`:
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
|
|
67
|
-
|
|
68
|
-
const table = <DataTable />
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<>
|
|
72
|
-
{isOpen ? (
|
|
73
|
-
<div>
|
|
74
|
-
<button onClick={focus}>Focus window</button>
|
|
75
|
-
<button onClick={close}>Bring back</button>
|
|
76
|
-
</div>
|
|
77
|
-
) : (
|
|
78
|
-
<>
|
|
79
|
-
<button onClick={open}>Open in new window</button>
|
|
80
|
-
{table}
|
|
81
|
-
</>
|
|
82
|
-
)}
|
|
83
|
-
<Popup>{table}</Popup>
|
|
84
|
-
</>
|
|
85
|
-
)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`isOpen` also updates when the user closes the window directly, so the
|
|
89
|
-
inline branch is restored in every case.
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1
|
+
# @jielga/react-popup-window
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jielga/react-popup-window/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@jielga/react-popup-window)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
React hook for rendering part of a component tree in a separate browser
|
|
8
|
+
window. Content is rendered through a portal into the popup document, so it
|
|
9
|
+
remains part of the calling tree: state, context, and event handlers work
|
|
10
|
+
across windows without bridging.
|
|
11
|
+
|
|
12
|
+
[Documentation and live examples](https://jielga.github.io/react-popup-window/)
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Portal-based rendering — popup content keeps access to all ancestor
|
|
17
|
+
context (state managers, data fetching, theming, routing)
|
|
18
|
+
- Stylesheet synchronization — `<style>` and `<link>` elements, root
|
|
19
|
+
`class`/`data-*` attributes, and `adoptedStyleSheets` are mirrored into
|
|
20
|
+
the popup and kept current while it is open
|
|
21
|
+
- Lifecycle management — detects the user closing the window, closes the
|
|
22
|
+
popup on owner unmount and opener unload, reports blocked popups
|
|
23
|
+
- No dependencies beyond `react` and `react-dom`
|
|
24
|
+
- TypeScript, ESM and CJS builds, SSR-safe
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @jielga/react-popup-window
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires React 19.2 or later.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { usePopupWindow } from '@jielga/react-popup-window'
|
|
38
|
+
|
|
39
|
+
function Dashboard() {
|
|
40
|
+
const { open, close, isOpen, Popup } = usePopupWindow({
|
|
41
|
+
title: 'Detached panel',
|
|
42
|
+
features: { width: 640, height: 480 },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<button onClick={open}>Open in new window</button>
|
|
48
|
+
<Popup>
|
|
49
|
+
<MyPanel />
|
|
50
|
+
</Popup>
|
|
51
|
+
</>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`Popup` renders its children into the popup window while open and nothing
|
|
57
|
+
otherwise. It has a stable identity and can be destructured from the hook
|
|
58
|
+
result.
|
|
59
|
+
|
|
60
|
+
### Detaching a section
|
|
61
|
+
|
|
62
|
+
To hide a section in the main window while it is popped out, render it in
|
|
63
|
+
one of two places depending on `isOpen`:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
|
|
67
|
+
|
|
68
|
+
const table = <DataTable />
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{isOpen ? (
|
|
73
|
+
<div>
|
|
74
|
+
<button onClick={focus}>Focus window</button>
|
|
75
|
+
<button onClick={close}>Bring back</button>
|
|
76
|
+
</div>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<button onClick={open}>Open in new window</button>
|
|
80
|
+
{table}
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
<Popup>{table}</Popup>
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`isOpen` also updates when the user closes the window directly, so the
|
|
89
|
+
inline branch is restored in every case.
|
|
90
|
+
|
|
91
|
+
### Show your own URL in the address bar
|
|
92
|
+
|
|
93
|
+
By default, the popup loads `about:blank`, and its address bar shows that.
|
|
94
|
+
To show your own domain instead, set `url` to an empty page served on the same origin.
|
|
95
|
+
For a Vite app, add `public/popup.html`:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<!doctype html>
|
|
99
|
+
<html lang="en">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="utf-8" />
|
|
102
|
+
<title>Loading</title>
|
|
103
|
+
</head>
|
|
104
|
+
<body></body>
|
|
105
|
+
</html>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const { open, Popup } = usePopupWindow({ title: 'Panel', url: '/popup.html' })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`isOpen` turns `true` as soon as the window opens; `Popup` renders once the page has loaded.
|
|
113
|
+
Important: do not point `url` at a route of the app, as that starts a second copy of the app inside the popup.
|
|
114
|
+
If the page redirects to another origin, the popup closes and `onBlocked` is called.
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
The hook opens a same-origin window, `about:blank` by default or the page
|
|
119
|
+
set with `url`, and creates a container element in its body. The `Popup` component renders its children with
|
|
120
|
+
`createPortal` into that container. A portal changes where DOM output is
|
|
121
|
+
placed, not where the components sit in the tree, so popup content
|
|
122
|
+
participates in the calling tree's state, context, and event system. React
|
|
123
|
+
supports cross-document portals: it attaches its event delegation to the
|
|
124
|
+
portal container when the container belongs to another document.
|
|
125
|
+
|
|
126
|
+
Physically moving DOM nodes into another document is not a viable
|
|
127
|
+
alternative: React delegates events on the root container in the main
|
|
128
|
+
document, and a node moved elsewhere stops receiving synthetic events.
|
|
129
|
+
|
|
130
|
+
The popup document runs no JavaScript of its own. All rendering and event
|
|
131
|
+
handling execute in the opener window.
|
|
132
|
+
|
|
133
|
+
## API
|
|
134
|
+
|
|
135
|
+
### `usePopupWindow(options?)`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
interface UsePopupWindowOptions {
|
|
139
|
+
/** Popup document title. Defaults to the opener document's title. */
|
|
140
|
+
title?: string
|
|
141
|
+
/** window.open target name. The same name reuses the window. Default: '_blank'. */
|
|
142
|
+
name?: string
|
|
143
|
+
/** Same-origin URL to load instead of about:blank, such as an empty /popup.html. Default: 'about:blank'. */
|
|
144
|
+
url?: string
|
|
145
|
+
/** window.open features, merged over { popup: true, width: 640, height: 480 }. */
|
|
146
|
+
features?: PopupWindowFeatures
|
|
147
|
+
/** Center the popup over the opener when no left/top feature is given. Default: true. */
|
|
148
|
+
center?: boolean
|
|
149
|
+
/** Mirror and synchronize stylesheets into the popup. Default: true. */
|
|
150
|
+
copyStyles?: boolean
|
|
151
|
+
/** Called when Popup starts rendering into the popup window. */
|
|
152
|
+
onOpen?: (popupWindow: Window) => void
|
|
153
|
+
/** Called when the popup closes: close(), user close, or opener unload. */
|
|
154
|
+
onClose?: () => void
|
|
155
|
+
/** Called when the popup is blocked or its document is not scriptable. */
|
|
156
|
+
onBlocked?: () => void
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
|
|
162
|
+
| Member | Type | Description |
|
|
163
|
+
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
164
|
+
| `Popup` | `FC<{ children? }>` | Portal component. Renders children into the popup while open. |
|
|
165
|
+
| `open` | `() => Window \| null` | Opens the popup, or focuses it if already open. Returns `null` when blocked. Requires a user gesture. |
|
|
166
|
+
| `close` | `() => void` | Closes the popup. |
|
|
167
|
+
| `toggle` | `() => void` | Opens if closed, closes if open. |
|
|
168
|
+
| `focus` | `() => void` | Focuses the popup window. |
|
|
169
|
+
| `isOpen` | `boolean` | Whether the popup is open. |
|
|
170
|
+
| `isBlocked` | `boolean` | Whether the last `open()` call was blocked. |
|
|
171
|
+
| `popupWindow` | `Window \| null` | The popup `Window` while open. |
|
|
172
|
+
|
|
173
|
+
### `copyStyles(source, target, watch?)`
|
|
174
|
+
|
|
175
|
+
The style synchronization used by the hook, exported for windows managed
|
|
176
|
+
outside of it. Copies stylesheets from `source` to `target` and, when
|
|
177
|
+
`watch` is true (default), observes the source document for changes.
|
|
178
|
+
Returns a function that stops observing.
|
|
179
|
+
|
|
180
|
+
## Style synchronization
|
|
181
|
+
|
|
182
|
+
While the popup is open, the following are mirrored from the opener
|
|
183
|
+
document and kept current:
|
|
184
|
+
|
|
185
|
+
- `<style>` and `<link rel="stylesheet">` elements. `<style>` contents are
|
|
186
|
+
serialized from the CSSOM, so rules injected with `insertRule` are
|
|
187
|
+
included.
|
|
188
|
+
- Additions, removals, and text edits of style nodes in `<head>`. This
|
|
189
|
+
covers Vite HMR, lazily loaded chunk CSS, and CSS-in-JS libraries.
|
|
190
|
+
- `class`, `style`, and `data-*` attributes on `<html>` and `<body>`.
|
|
191
|
+
Theme systems keyed on root attributes propagate to the popup.
|
|
192
|
+
- `document.adoptedStyleSheets`.
|
|
193
|
+
|
|
194
|
+
The popup loads each copied `<link>` stylesheet again.
|
|
195
|
+
`Popup` renders once they have loaded or failed, so the first frame shown is styled.
|
|
196
|
+
Alternate stylesheets and stylesheets whose `media` does not match are not waited for.
|
|
197
|
+
If a stylesheet takes longer than 3 seconds, `Popup` renders without it.
|
|
198
|
+
Note that a `<link>` added to the opener while the popup is open loads in the popup after the content is shown.
|
|
199
|
+
|
|
200
|
+
Set `copyStyles: false` to disable.
|
|
201
|
+
|
|
202
|
+
## Communication
|
|
203
|
+
|
|
204
|
+
Popup content rendered through `Popup` is part of the calling component
|
|
205
|
+
tree and executes in the opener's JavaScript realm. Props, state, and
|
|
206
|
+
context are the communication mechanism; no message channel is required or
|
|
207
|
+
provided.
|
|
208
|
+
|
|
209
|
+
`postMessage` remains relevant only for scripts hosted in the popup
|
|
210
|
+
document itself (for example, an injected non-React widget). Such scripts
|
|
211
|
+
run in the popup's realm and can post to `window.opener`; the exposed
|
|
212
|
+
`popupWindow` handle can be used from the opener side. Note that calling
|
|
213
|
+
`opener.postMessage` from a portal event handler posts from the opener's
|
|
214
|
+
own realm — the browser reports the main window, not the popup, as
|
|
215
|
+
`event.source`.
|
|
216
|
+
|
|
217
|
+
## Keep overlays in the popup window
|
|
218
|
+
|
|
219
|
+
Component libraries mount overlays - menus, popovers, modals, tooltips -
|
|
220
|
+
through portals into `document.body`. Popup content executes in the
|
|
221
|
+
opener's JavaScript realm, so the bare `document` global is the main
|
|
222
|
+
window's document even for components rendered inside the popup, and the
|
|
223
|
+
overlay opens in the main window. The `ownerDocument` of a node rendered
|
|
224
|
+
inside the popup is the popup's document; the portal container must come
|
|
225
|
+
from there. The rule covers any portal, including a raw `createPortal` in
|
|
226
|
+
application code.
|
|
227
|
+
|
|
228
|
+
### Mantine
|
|
229
|
+
|
|
230
|
+
Mantine reads portal defaults from the theme. The following wrapper
|
|
231
|
+
resolves its own `ownerDocument` after mount and supplies that document's
|
|
232
|
+
body as the default `Portal` target, so `Menu`, `Popover`, `Tooltip`, and
|
|
233
|
+
`Modal` open in the window they are rendered in:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { MantineThemeProvider, Portal } from '@mantine/core'
|
|
237
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
238
|
+
import type { ReactNode } from 'react'
|
|
239
|
+
|
|
240
|
+
export function SameWindowPortals({ children }: { children: ReactNode }) {
|
|
241
|
+
const probeRef = useRef<HTMLDivElement>(null)
|
|
242
|
+
const [target, setTarget] = useState<HTMLElement | null>(null)
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
setTarget(probeRef.current?.ownerDocument.body ?? null)
|
|
246
|
+
}, [])
|
|
247
|
+
|
|
248
|
+
const theme = useMemo(
|
|
249
|
+
() => ({
|
|
250
|
+
components: {
|
|
251
|
+
Portal: Portal.extend({ defaultProps: target ? { target } : {} }),
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
[target],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div ref={probeRef} style={{ display: 'contents' }}>
|
|
259
|
+
<MantineThemeProvider inherit theme={theme}>
|
|
260
|
+
{children}
|
|
261
|
+
</MantineThemeProvider>
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Render the wrapper inside `<Popup>`, around the content that opens
|
|
268
|
+
overlays; it resolves the container from the tree it mounts in, so the
|
|
269
|
+
same component also works inline. The
|
|
270
|
+
[live examples](https://jielga.github.io/react-popup-window/) use it
|
|
271
|
+
around each grid, including a `Modal` opened from a popped-out grid; the
|
|
272
|
+
shipped `popup-content` skill contains the same code and the common
|
|
273
|
+
mistakes around it.
|
|
274
|
+
|
|
275
|
+
Mantine's own portal props interact with the wrapper as follows:
|
|
276
|
+
|
|
277
|
+
- An explicit `target` or `portalProps` on a component overrides the
|
|
278
|
+
theme default, so the overlay opens in the main window again.
|
|
279
|
+
- `withinPortal={false}` needs no wrapper: the overlay renders inline and
|
|
280
|
+
therefore inside the popup document. It then clips against
|
|
281
|
+
`overflow: hidden` ancestors and local stacking contexts, which is why
|
|
282
|
+
the theme default is preferred.
|
|
283
|
+
|
|
284
|
+
### Radix, MUI, and similar
|
|
285
|
+
|
|
286
|
+
Pass the per-component portal `container` prop an element of the popup
|
|
287
|
+
document: `popupWindow.document.body` where the hook's result is in
|
|
288
|
+
scope, or `node.ownerDocument.body` from a ref on any rendered node
|
|
289
|
+
deeper in the tree - the same probe the wrapper above uses.
|
|
290
|
+
|
|
291
|
+
### Window-bound overlay behavior
|
|
292
|
+
|
|
293
|
+
A correctly placed overlay can still bind window-level behavior to the
|
|
294
|
+
main window, because those bindings also run in the opener's realm.
|
|
295
|
+
Mantine's `Modal` listens for Escape with `window.addEventListener` and
|
|
296
|
+
locks scroll on the main window's body, so Escape pressed in the popup
|
|
297
|
+
does not close the modal. This cannot be redirected from outside the
|
|
298
|
+
component; for state you own, add a listener on the popup window:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
// Close our own modal on Escape pressed in the popup.
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (!opened) return
|
|
304
|
+
const win = hostRef.current?.ownerDocument.defaultView
|
|
305
|
+
const onKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && setOpened(false)
|
|
306
|
+
win?.addEventListener('keydown', onKeyDown)
|
|
307
|
+
return () => win?.removeEventListener('keydown', onKeyDown)
|
|
308
|
+
}, [opened])
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Limitations
|
|
312
|
+
|
|
313
|
+
- Browsers do not allow hiding the address bar entirely. `popup: true`
|
|
314
|
+
(the default) requests the minimal window chrome the platform provides.
|
|
315
|
+
By default, the bar shows `about:blank`; `url` replaces that with a page
|
|
316
|
+
of your own, see
|
|
317
|
+
[Show your own URL in the address bar](#show-your-own-url-in-the-address-bar).
|
|
318
|
+
- `open()` must be called from a user gesture; otherwise the browser's
|
|
319
|
+
popup blocker intervenes and `open()` returns `null`.
|
|
320
|
+
- Popup content unmounts and remounts when it moves between windows. State
|
|
321
|
+
that should survive detaching belongs in the component that owns the
|
|
322
|
+
hook, or in an external store.
|
|
323
|
+
- The popup closes when the owning component unmounts and when the opener
|
|
324
|
+
window unloads. The popup document cannot outlive the opener.
|
|
325
|
+
- Sandboxed embedders — VS Code's built-in browser, CodeSandbox and
|
|
326
|
+
StackBlitz previews, iframes sandboxed without
|
|
327
|
+
`allow-popups-to-escape-sandbox` — open popups with an opaque origin the
|
|
328
|
+
opener cannot script, so the portal cannot render into them. `open()`
|
|
329
|
+
detects this, closes the window, reports it through `isBlocked` and
|
|
330
|
+
`onBlocked`, and returns `null`. Use a real browser tab instead.
|
|
331
|
+
|
|
332
|
+
## Agent skills
|
|
333
|
+
|
|
334
|
+
The package ships [Agent Skills](https://agentskills.io) for AI coding
|
|
335
|
+
agents, managed with [`@tanstack/intent`](https://www.npmjs.com/package/@tanstack/intent):
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
npx @tanstack/intent@latest list
|
|
339
|
+
npx @tanstack/intent@latest load @jielga/react-popup-window#getting-started
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
| Skill | Contents |
|
|
343
|
+
| ----------------- | -------------------------------------------------------------------- |
|
|
344
|
+
| `getting-started` | Hook API, options, lifecycle, common mistakes |
|
|
345
|
+
| `popup-content` | Style synchronization, bounded-height layouts, portal-based overlays |
|
|
346
|
+
|
|
347
|
+
## Development
|
|
348
|
+
|
|
349
|
+
| Path | Contents |
|
|
350
|
+
| ----------- | --------------------------------------------------------------- |
|
|
351
|
+
| `src/` | Library source. Vite library mode; ESM, CJS, and declarations. |
|
|
352
|
+
| `docs/` | Documentation site with live examples, deployed to GitHub Pages. |
|
|
353
|
+
| `e2e/` | Playwright tests that exercise real popup windows in Chromium. |
|
|
354
|
+
| `skills/` | Agent skills shipped with the package. |
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
npm install
|
|
358
|
+
npm run dev # documentation site with the library aliased to source
|
|
359
|
+
npm test # unit tests (Vitest, jsdom)
|
|
360
|
+
npm run e2e # end-to-end tests (Playwright)
|
|
361
|
+
npm run build # build the library into dist/
|
|
362
|
+
npm run skills:validate # validate agent skills
|
|
363
|
+
npm run changeset # describe a change for the changelog and the next release
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Releases are published to npm by Changesets when the **chore: version packages**
|
|
367
|
+
pull request is merged — see [RELEASING.md](./RELEASING.md).
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
[MIT](./LICENSE)
|
package/dist/copyStyles.d.ts
CHANGED
|
@@ -15,4 +15,15 @@
|
|
|
15
15
|
* Returns a function that stops observing.
|
|
16
16
|
*/
|
|
17
17
|
export declare function copyStyles(source: Document, target: Document, watch?: boolean): () => void;
|
|
18
|
+
export interface StyleMirror {
|
|
19
|
+
/** Stops observing the source document. */
|
|
20
|
+
stop: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* The `<link>` elements created in `target` by the initial copy. The target
|
|
23
|
+
* fetches each of them asynchronously.
|
|
24
|
+
*/
|
|
25
|
+
links: HTMLLinkElement[];
|
|
26
|
+
}
|
|
27
|
+
/** {@link copyStyles}, plus the `<link>` elements the initial copy created. */
|
|
28
|
+
export declare function mirrorStyles(source: Document, target: Document, watch?: boolean): StyleMirror;
|
|
18
29
|
//# sourceMappingURL=copyStyles.d.ts.map
|