@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,211 +1,211 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: getting-started
|
|
3
|
-
description: >
|
|
4
|
-
Set up @jielga/react-popup-window: the usePopupWindow hook, the Popup portal
|
|
5
|
-
component, window controls (open, close, toggle, focus), reactive state
|
|
6
|
-
(isOpen, isBlocked, popupWindow), and hook options (title, name, features,
|
|
7
|
-
center, copyStyles, onOpen, onClose, onBlocked). Load when opening part of a
|
|
8
|
-
React tree in a separate browser window, building an "open in new window" or
|
|
9
|
-
detachable panel, or when a popup is blocked, closes unexpectedly, or loses
|
|
10
|
-
state.
|
|
11
|
-
metadata:
|
|
12
|
-
type: core
|
|
13
|
-
library: '@jielga/react-popup-window'
|
|
14
|
-
library_version: '0.1.
|
|
15
|
-
sources:
|
|
16
|
-
- 'Jielga/react-popup-window:README.md'
|
|
17
|
-
- 'Jielga/react-popup-window:src/usePopupWindow.ts'
|
|
18
|
-
- 'Jielga/react-popup-window:src/types.ts'
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
# react-popup-window — Getting started
|
|
22
|
-
|
|
23
|
-
`usePopupWindow` opens part of a React tree in a separate browser window.
|
|
24
|
-
Content is rendered with a portal into the popup's `about:blank` document,
|
|
25
|
-
so it remains part of the calling component tree: state, context, and event
|
|
26
|
-
handlers work across windows without bridging. The popup document runs no
|
|
27
|
-
JavaScript of its own; the opener's React instance renders into it.
|
|
28
|
-
|
|
29
|
-
## Setup
|
|
30
|
-
|
|
31
|
-
```tsx
|
|
32
|
-
import { usePopupWindow } from '@jielga/react-popup-window'
|
|
33
|
-
|
|
34
|
-
function Dashboard() {
|
|
35
|
-
const { open, close, isOpen, Popup } = usePopupWindow({
|
|
36
|
-
title: 'Detached panel',
|
|
37
|
-
features: { width: 640, height: 480 },
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<>
|
|
42
|
-
<button onClick={open}>Open in new window</button>
|
|
43
|
-
<Popup>
|
|
44
|
-
<MyPanel />
|
|
45
|
-
</Popup>
|
|
46
|
-
</>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`Popup` renders its children into the popup while open and nothing
|
|
52
|
-
otherwise. It has a stable identity; destructuring it from the hook result
|
|
53
|
-
is safe.
|
|
54
|
-
|
|
55
|
-
## Core patterns
|
|
56
|
-
|
|
57
|
-
### Detach a section: hide it inline while popped out
|
|
58
|
-
|
|
59
|
-
```tsx
|
|
60
|
-
const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
|
|
61
|
-
|
|
62
|
-
const table = <DataTable />
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<>
|
|
66
|
-
{isOpen ? (
|
|
67
|
-
<div>
|
|
68
|
-
<button onClick={focus}>Focus window</button>
|
|
69
|
-
<button onClick={close}>Bring back</button>
|
|
70
|
-
</div>
|
|
71
|
-
) : (
|
|
72
|
-
<>
|
|
73
|
-
<button onClick={open}>Open in new window</button>
|
|
74
|
-
{table}
|
|
75
|
-
</>
|
|
76
|
-
)}
|
|
77
|
-
<Popup>{table}</Popup>
|
|
78
|
-
</>
|
|
79
|
-
)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The element is created once and mounted in exactly one of the two homes at
|
|
83
|
-
a time. `isOpen` also flips when the user closes the window by hand, so the
|
|
84
|
-
inline branch restores itself.
|
|
85
|
-
|
|
86
|
-
### Handle blocked popups
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
const { open, isBlocked } = usePopupWindow({ onBlocked: () => notifyUser() })
|
|
90
|
-
// open() returns Window | null; null means blocked (or non-browser environment)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Options
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
interface UsePopupWindowOptions {
|
|
97
|
-
title?: string // popup document title; defaults to the opener's title
|
|
98
|
-
name?: string // window.open target name; same name reuses the window
|
|
99
|
-
features?: PopupWindowFeatures // merged over { popup: true, width: 640, height: 480 }
|
|
100
|
-
center?: boolean // center over the opener window; default true
|
|
101
|
-
copyStyles?: boolean // mirror and live-sync stylesheets; default true
|
|
102
|
-
onOpen?: (popupWindow: Window) => void
|
|
103
|
-
onClose?: () => void // close(), user close, or opener unload
|
|
104
|
-
onBlocked?: () => void
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Return value: `{ open, close, toggle, focus, isOpen, isBlocked, popupWindow,
|
|
109
|
-
Popup }`. `popupWindow` is the raw `Window` while open, otherwise `null`.
|
|
110
|
-
|
|
111
|
-
Lifecycle: the popup closes automatically when the component that owns the
|
|
112
|
-
hook unmounts and when the opener window unloads. Closing the window by
|
|
113
|
-
hand is detected (`pagehide` plus a `closed` poll); `isOpen` flips and the
|
|
114
|
-
portal unmounts.
|
|
115
|
-
|
|
116
|
-
## Common mistakes
|
|
117
|
-
|
|
118
|
-
### HIGH Calling open() outside a user gesture
|
|
119
|
-
|
|
120
|
-
Wrong:
|
|
121
|
-
|
|
122
|
-
```tsx
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
open() // blocked by the popup blocker on mount
|
|
125
|
-
}, [open])
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Correct:
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
<button onClick={open}>Open panel</button>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Browsers only allow `window.open` in response to a user gesture; outside
|
|
135
|
-
one, `open()` returns `null` and sets `isBlocked` without throwing.
|
|
136
|
-
|
|
137
|
-
Source: README.md, src/usePopupWindow.ts (open)
|
|
138
|
-
|
|
139
|
-
### HIGH Expecting component-local state to survive detaching
|
|
140
|
-
|
|
141
|
-
Wrong:
|
|
142
|
-
|
|
143
|
-
```tsx
|
|
144
|
-
function Panel() {
|
|
145
|
-
const [sort, setSort] = useState('name') // resets when the panel moves windows
|
|
146
|
-
return <SortedList sort={sort} onSort={setSort} />
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Correct:
|
|
151
|
-
|
|
152
|
-
```tsx
|
|
153
|
-
function Owner() {
|
|
154
|
-
const [sort, setSort] = useState('name') // owner stays mounted in the main window
|
|
155
|
-
const { open, isOpen, Popup } = usePopupWindow()
|
|
156
|
-
const panel = <SortedList sort={sort} onSort={setSort} />
|
|
157
|
-
return <>{isOpen ? null : panel}<Popup>{panel}</Popup></>
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Popup content unmounts and remounts when it moves between windows, so state
|
|
162
|
-
held inside it is discarded. State held by the component that owns the hook
|
|
163
|
-
persists, because that component never moves.
|
|
164
|
-
|
|
165
|
-
Source: README.md (
|
|
166
|
-
|
|
167
|
-
### MEDIUM Messaging the opener from a portal event handler
|
|
168
|
-
|
|
169
|
-
Wrong:
|
|
170
|
-
|
|
171
|
-
```tsx
|
|
172
|
-
// inside <Popup> content
|
|
173
|
-
<button onClick={() => popupWindow.opener.postMessage(data, '*')}>Send</button>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Correct:
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
// inside <Popup> content — same tree, same realm; call the handler directly
|
|
180
|
-
<button onClick={() => onData(data)}>Send</button>
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Portal content executes in the main window's JavaScript realm, so the
|
|
184
|
-
message is posted by the main window to itself and `event.source` is the
|
|
185
|
-
main window. `postMessage` is only meaningful for scripts hosted in the
|
|
186
|
-
popup document itself; for portal content, props, state, and context are
|
|
187
|
-
the communication channel.
|
|
188
|
-
|
|
189
|
-
Source: README.md (
|
|
190
|
-
|
|
191
|
-
### MEDIUM Keeping the popup open across owner unmount or navigation
|
|
192
|
-
|
|
193
|
-
Wrong:
|
|
194
|
-
|
|
195
|
-
```tsx
|
|
196
|
-
// route A mounts the hook; navigating to route B is expected to keep the popup
|
|
197
|
-
<Route path="/a" element={<PanelWithPopup />} />
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Correct:
|
|
201
|
-
|
|
202
|
-
```tsx
|
|
203
|
-
// mount the hook in a component that stays mounted across the interaction,
|
|
204
|
-
// e.g. a layout component above the route switch
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
The popup is closed deliberately when the owning component unmounts: its
|
|
208
|
-
portal content would unmount anyway, leaving an empty window. Place the
|
|
209
|
-
hook at a level that lives as long as the popup should.
|
|
210
|
-
|
|
211
|
-
Source: src/usePopupWindow.ts (unmount effect)
|
|
1
|
+
---
|
|
2
|
+
name: getting-started
|
|
3
|
+
description: >
|
|
4
|
+
Set up @jielga/react-popup-window: the usePopupWindow hook, the Popup portal
|
|
5
|
+
component, window controls (open, close, toggle, focus), reactive state
|
|
6
|
+
(isOpen, isBlocked, popupWindow), and hook options (title, name, features,
|
|
7
|
+
center, copyStyles, onOpen, onClose, onBlocked). Load when opening part of a
|
|
8
|
+
React tree in a separate browser window, building an "open in new window" or
|
|
9
|
+
detachable panel, or when a popup is blocked, closes unexpectedly, or loses
|
|
10
|
+
state.
|
|
11
|
+
metadata:
|
|
12
|
+
type: core
|
|
13
|
+
library: '@jielga/react-popup-window'
|
|
14
|
+
library_version: '0.1.1'
|
|
15
|
+
sources:
|
|
16
|
+
- 'Jielga/react-popup-window:README.md'
|
|
17
|
+
- 'Jielga/react-popup-window:src/usePopupWindow.ts'
|
|
18
|
+
- 'Jielga/react-popup-window:src/types.ts'
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# react-popup-window — Getting started
|
|
22
|
+
|
|
23
|
+
`usePopupWindow` opens part of a React tree in a separate browser window.
|
|
24
|
+
Content is rendered with a portal into the popup's `about:blank` document,
|
|
25
|
+
so it remains part of the calling component tree: state, context, and event
|
|
26
|
+
handlers work across windows without bridging. The popup document runs no
|
|
27
|
+
JavaScript of its own; the opener's React instance renders into it.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { usePopupWindow } from '@jielga/react-popup-window'
|
|
33
|
+
|
|
34
|
+
function Dashboard() {
|
|
35
|
+
const { open, close, isOpen, Popup } = usePopupWindow({
|
|
36
|
+
title: 'Detached panel',
|
|
37
|
+
features: { width: 640, height: 480 },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<button onClick={open}>Open in new window</button>
|
|
43
|
+
<Popup>
|
|
44
|
+
<MyPanel />
|
|
45
|
+
</Popup>
|
|
46
|
+
</>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`Popup` renders its children into the popup while open and nothing
|
|
52
|
+
otherwise. It has a stable identity; destructuring it from the hook result
|
|
53
|
+
is safe.
|
|
54
|
+
|
|
55
|
+
## Core patterns
|
|
56
|
+
|
|
57
|
+
### Detach a section: hide it inline while popped out
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
const { open, close, focus, isOpen, Popup } = usePopupWindow({ title: 'People' })
|
|
61
|
+
|
|
62
|
+
const table = <DataTable />
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
{isOpen ? (
|
|
67
|
+
<div>
|
|
68
|
+
<button onClick={focus}>Focus window</button>
|
|
69
|
+
<button onClick={close}>Bring back</button>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<>
|
|
73
|
+
<button onClick={open}>Open in new window</button>
|
|
74
|
+
{table}
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
<Popup>{table}</Popup>
|
|
78
|
+
</>
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The element is created once and mounted in exactly one of the two homes at
|
|
83
|
+
a time. `isOpen` also flips when the user closes the window by hand, so the
|
|
84
|
+
inline branch restores itself.
|
|
85
|
+
|
|
86
|
+
### Handle blocked popups
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const { open, isBlocked } = usePopupWindow({ onBlocked: () => notifyUser() })
|
|
90
|
+
// open() returns Window | null; null means blocked (or non-browser environment)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
interface UsePopupWindowOptions {
|
|
97
|
+
title?: string // popup document title; defaults to the opener's title
|
|
98
|
+
name?: string // window.open target name; same name reuses the window
|
|
99
|
+
features?: PopupWindowFeatures // merged over { popup: true, width: 640, height: 480 }
|
|
100
|
+
center?: boolean // center over the opener window; default true
|
|
101
|
+
copyStyles?: boolean // mirror and live-sync stylesheets; default true
|
|
102
|
+
onOpen?: (popupWindow: Window) => void
|
|
103
|
+
onClose?: () => void // close(), user close, or opener unload
|
|
104
|
+
onBlocked?: () => void
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Return value: `{ open, close, toggle, focus, isOpen, isBlocked, popupWindow,
|
|
109
|
+
Popup }`. `popupWindow` is the raw `Window` while open, otherwise `null`.
|
|
110
|
+
|
|
111
|
+
Lifecycle: the popup closes automatically when the component that owns the
|
|
112
|
+
hook unmounts and when the opener window unloads. Closing the window by
|
|
113
|
+
hand is detected (`pagehide` plus a `closed` poll); `isOpen` flips and the
|
|
114
|
+
portal unmounts.
|
|
115
|
+
|
|
116
|
+
## Common mistakes
|
|
117
|
+
|
|
118
|
+
### HIGH Calling open() outside a user gesture
|
|
119
|
+
|
|
120
|
+
Wrong:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
open() // blocked by the popup blocker on mount
|
|
125
|
+
}, [open])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Correct:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<button onClick={open}>Open panel</button>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Browsers only allow `window.open` in response to a user gesture; outside
|
|
135
|
+
one, `open()` returns `null` and sets `isBlocked` without throwing.
|
|
136
|
+
|
|
137
|
+
Source: README.md, src/usePopupWindow.ts (open)
|
|
138
|
+
|
|
139
|
+
### HIGH Expecting component-local state to survive detaching
|
|
140
|
+
|
|
141
|
+
Wrong:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
function Panel() {
|
|
145
|
+
const [sort, setSort] = useState('name') // resets when the panel moves windows
|
|
146
|
+
return <SortedList sort={sort} onSort={setSort} />
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Correct:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
function Owner() {
|
|
154
|
+
const [sort, setSort] = useState('name') // owner stays mounted in the main window
|
|
155
|
+
const { open, isOpen, Popup } = usePopupWindow()
|
|
156
|
+
const panel = <SortedList sort={sort} onSort={setSort} />
|
|
157
|
+
return <>{isOpen ? null : panel}<Popup>{panel}</Popup></>
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Popup content unmounts and remounts when it moves between windows, so state
|
|
162
|
+
held inside it is discarded. State held by the component that owns the hook
|
|
163
|
+
persists, because that component never moves.
|
|
164
|
+
|
|
165
|
+
Source: README.md (Limitations)
|
|
166
|
+
|
|
167
|
+
### MEDIUM Messaging the opener from a portal event handler
|
|
168
|
+
|
|
169
|
+
Wrong:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// inside <Popup> content
|
|
173
|
+
<button onClick={() => popupWindow.opener.postMessage(data, '*')}>Send</button>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Correct:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// inside <Popup> content — same tree, same realm; call the handler directly
|
|
180
|
+
<button onClick={() => onData(data)}>Send</button>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Portal content executes in the main window's JavaScript realm, so the
|
|
184
|
+
message is posted by the main window to itself and `event.source` is the
|
|
185
|
+
main window. `postMessage` is only meaningful for scripts hosted in the
|
|
186
|
+
popup document itself; for portal content, props, state, and context are
|
|
187
|
+
the communication channel.
|
|
188
|
+
|
|
189
|
+
Source: README.md (Communication)
|
|
190
|
+
|
|
191
|
+
### MEDIUM Keeping the popup open across owner unmount or navigation
|
|
192
|
+
|
|
193
|
+
Wrong:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
// route A mounts the hook; navigating to route B is expected to keep the popup
|
|
197
|
+
<Route path="/a" element={<PanelWithPopup />} />
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Correct:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
// mount the hook in a component that stays mounted across the interaction,
|
|
204
|
+
// e.g. a layout component above the route switch
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The popup is closed deliberately when the owning component unmounts: its
|
|
208
|
+
portal content would unmount anyway, leaving an empty window. Place the
|
|
209
|
+
hook at a level that lives as long as the popup should.
|
|
210
|
+
|
|
211
|
+
Source: src/usePopupWindow.ts (unmount effect)
|