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