@rohal12/spindle 0.59.18 → 0.59.19
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +101 -20
- package/package.json +1 -1
- package/src/hooks/use-modal-focus.ts +103 -19
- package/src/markup/render.tsx +37 -10
package/dist/pkg/headless.js
CHANGED
|
@@ -12480,6 +12480,26 @@ function useViewScope() {
|
|
|
12480
12480
|
*/
|
|
12481
12481
|
var SvgContext = createContext(false);
|
|
12482
12482
|
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
12483
|
+
/**
|
|
12484
|
+
* Elements whose children must be elements of particular types: markdown
|
|
12485
|
+
* paragraphs between them would pull the children out of the structure (#423).
|
|
12486
|
+
* Text between the children, when it is only whitespace, is dropped.
|
|
12487
|
+
*/
|
|
12488
|
+
var STRUCTURAL_ELEMENTS = new Set([
|
|
12489
|
+
"table",
|
|
12490
|
+
"thead",
|
|
12491
|
+
"tbody",
|
|
12492
|
+
"tfoot",
|
|
12493
|
+
"tr",
|
|
12494
|
+
"colgroup",
|
|
12495
|
+
"ul",
|
|
12496
|
+
"ol",
|
|
12497
|
+
"menu",
|
|
12498
|
+
"dl",
|
|
12499
|
+
"select",
|
|
12500
|
+
"optgroup",
|
|
12501
|
+
"datalist"
|
|
12502
|
+
]);
|
|
12483
12503
|
/** Elements whose content is literal text, not markdown. */
|
|
12484
12504
|
var PREFORMATTED_ELEMENTS = new Set([
|
|
12485
12505
|
"pre",
|
|
@@ -12940,7 +12960,7 @@ function HtmlNodeRenderer({ node }) {
|
|
|
12940
12960
|
locals,
|
|
12941
12961
|
inline: isInline
|
|
12942
12962
|
});
|
|
12943
|
-
children = summaryAt === -1 ? body(node.children) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
12963
|
+
children = STRUCTURAL_ELEMENTS.has(tag) && node.children.every((n) => n.type !== "text" || n.value.trim() === "") ? node.children.filter((n) => n.type !== "text").map((n) => renderSingleNode(n)) : summaryAt === -1 ? body(node.children) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
12944
12964
|
body(node.children.slice(0, summaryAt)),
|
|
12945
12965
|
renderSingleNode(node.children[summaryAt]),
|
|
12946
12966
|
body(node.children.slice(summaryAt + 1))
|
|
@@ -18487,7 +18507,7 @@ function useModalFocus(panelRef, bodySelector, dismissible, onClose) {
|
|
|
18487
18507
|
const entry = { panel };
|
|
18488
18508
|
openModals.push(entry);
|
|
18489
18509
|
const body = panel.querySelector(bodySelector) ?? panel;
|
|
18490
|
-
(panel.
|
|
18510
|
+
(Array.from(panel.querySelectorAll("[autofocus]")).find(isAvailable) ?? focusables(body)[0] ?? panel).focus();
|
|
18491
18511
|
return () => {
|
|
18492
18512
|
const i = openModals.indexOf(entry);
|
|
18493
18513
|
if (i !== -1) openModals.splice(i, 1);
|
|
@@ -18498,8 +18518,33 @@ function useModalFocus(panelRef, bodySelector, dismissible, onClose) {
|
|
|
18498
18518
|
useLayoutEffect(() => {
|
|
18499
18519
|
const panel = panelRef.current;
|
|
18500
18520
|
if (!panel) return;
|
|
18521
|
+
const top = () => openModals[openModals.length - 1]?.panel === panel;
|
|
18522
|
+
/**
|
|
18523
|
+
* Move focus to the next (or previous) modal control from `from`. The
|
|
18524
|
+
* order is computed here, never left to the browser: its positive-tabindex
|
|
18525
|
+
* sequence spans the whole document and would lead out of the modal
|
|
18526
|
+
* (#424). `from` may be an element of an embedded document, whose frame
|
|
18527
|
+
* element then stands in for it.
|
|
18528
|
+
*/
|
|
18529
|
+
const moveFocus = (from, backwards) => {
|
|
18530
|
+
const items = focusables(panel);
|
|
18531
|
+
const first = items[0];
|
|
18532
|
+
const last = items[items.length - 1];
|
|
18533
|
+
if (!first || !last) {
|
|
18534
|
+
panel.focus();
|
|
18535
|
+
return;
|
|
18536
|
+
}
|
|
18537
|
+
const at = from ? items.indexOf(from) : -1;
|
|
18538
|
+
let next;
|
|
18539
|
+
if (at !== -1) next = items[at + (backwards ? -1 : 1)];
|
|
18540
|
+
else if (from && from !== panel && panel.contains(from)) {
|
|
18541
|
+
const after = (el) => !!(from.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
18542
|
+
next = backwards ? items.filter((el) => !after(el)).pop() : items.find(after);
|
|
18543
|
+
} else next = backwards ? last : first;
|
|
18544
|
+
(next ?? (backwards ? last : first)).focus();
|
|
18545
|
+
};
|
|
18501
18546
|
const onKeyDown = (e) => {
|
|
18502
|
-
if (
|
|
18547
|
+
if (!top()) return;
|
|
18503
18548
|
if (e.key === "Escape") {
|
|
18504
18549
|
if (!dismissible) return;
|
|
18505
18550
|
e.preventDefault();
|
|
@@ -18508,24 +18553,60 @@ function useModalFocus(panelRef, bodySelector, dismissible, onClose) {
|
|
|
18508
18553
|
return;
|
|
18509
18554
|
}
|
|
18510
18555
|
if (e.key !== "Tab") return;
|
|
18511
|
-
|
|
18512
|
-
|
|
18513
|
-
|
|
18514
|
-
|
|
18515
|
-
|
|
18516
|
-
|
|
18517
|
-
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
|
|
18521
|
-
|
|
18522
|
-
|
|
18523
|
-
|
|
18524
|
-
|
|
18525
|
-
|
|
18556
|
+
e.preventDefault();
|
|
18557
|
+
moveFocus(document.activeElement, e.shiftKey);
|
|
18558
|
+
};
|
|
18559
|
+
const controller = new AbortController();
|
|
18560
|
+
const { signal } = controller;
|
|
18561
|
+
const frameDocs = /* @__PURE__ */ new Map();
|
|
18562
|
+
const attachFrame = (frame) => {
|
|
18563
|
+
let doc = null;
|
|
18564
|
+
try {
|
|
18565
|
+
doc = frame.contentDocument;
|
|
18566
|
+
} catch {}
|
|
18567
|
+
if (!doc || frameDocs.get(frame) === doc) return;
|
|
18568
|
+
frameDocs.set(frame, doc);
|
|
18569
|
+
const onFrameKeyDown = (e) => {
|
|
18570
|
+
if (!top()) return;
|
|
18571
|
+
if (e.key !== "Tab") return onKeyDown(e);
|
|
18572
|
+
const inner = Array.from(doc.querySelectorAll(FOCUSABLE)).filter((el) => isAvailable(el) && isTabStop(el));
|
|
18573
|
+
const active = doc.activeElement;
|
|
18574
|
+
const edge = e.shiftKey ? inner[0] : inner[inner.length - 1];
|
|
18575
|
+
if (!edge || active === edge || active === doc.body) {
|
|
18576
|
+
e.preventDefault();
|
|
18577
|
+
moveFocus(frame, e.shiftKey);
|
|
18578
|
+
}
|
|
18579
|
+
};
|
|
18580
|
+
doc.addEventListener("keydown", onFrameKeyDown, { signal });
|
|
18581
|
+
};
|
|
18582
|
+
const attachFrames = () => {
|
|
18583
|
+
panel.querySelectorAll("iframe").forEach(attachFrame);
|
|
18584
|
+
};
|
|
18585
|
+
const onLoad = (e) => {
|
|
18586
|
+
if (e.target instanceof HTMLIFrameElement) attachFrame(e.target);
|
|
18587
|
+
};
|
|
18588
|
+
panel.addEventListener("load", onLoad, {
|
|
18589
|
+
capture: true,
|
|
18590
|
+
signal
|
|
18591
|
+
});
|
|
18592
|
+
const frames = new MutationObserver(attachFrames);
|
|
18593
|
+
frames.observe(panel, {
|
|
18594
|
+
childList: true,
|
|
18595
|
+
subtree: true
|
|
18596
|
+
});
|
|
18597
|
+
attachFrames();
|
|
18598
|
+
const onFocusIn = (e) => {
|
|
18599
|
+
const target = e.target;
|
|
18600
|
+
if (!top() || !(target instanceof Node) || panel.contains(target)) return;
|
|
18601
|
+
if (target === document.body || target === document.documentElement) return;
|
|
18602
|
+
moveFocus(null, false);
|
|
18603
|
+
};
|
|
18604
|
+
document.addEventListener("keydown", onKeyDown, { signal });
|
|
18605
|
+
document.addEventListener("focusin", onFocusIn, { signal });
|
|
18606
|
+
return () => {
|
|
18607
|
+
controller.abort();
|
|
18608
|
+
frames.disconnect();
|
|
18526
18609
|
};
|
|
18527
|
-
document.addEventListener("keydown", onKeyDown);
|
|
18528
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
18529
18610
|
}, [dismissible, onClose]);
|
|
18530
18611
|
}
|
|
18531
18612
|
//#endregion
|
package/package.json
CHANGED
|
@@ -165,8 +165,12 @@ export function useModalFocus(
|
|
|
165
165
|
openModals.push(entry);
|
|
166
166
|
|
|
167
167
|
const body = panel.querySelector<HTMLElement>(bodySelector) ?? panel;
|
|
168
|
+
// An autofocus element that can't take focus (hidden, disabled, inert)
|
|
169
|
+
// must not shadow the eligible controls (#426).
|
|
168
170
|
const initial =
|
|
169
|
-
panel.
|
|
171
|
+
Array.from(panel.querySelectorAll<HTMLElement>('[autofocus]')).find(
|
|
172
|
+
isAvailable,
|
|
173
|
+
) ??
|
|
170
174
|
focusables(body)[0] ??
|
|
171
175
|
panel;
|
|
172
176
|
initial.focus();
|
|
@@ -187,8 +191,44 @@ export function useModalFocus(
|
|
|
187
191
|
const panel = panelRef.current;
|
|
188
192
|
if (!panel) return;
|
|
189
193
|
|
|
194
|
+
const top = () => openModals[openModals.length - 1]?.panel === panel;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Move focus to the next (or previous) modal control from `from`. The
|
|
198
|
+
* order is computed here, never left to the browser: its positive-tabindex
|
|
199
|
+
* sequence spans the whole document and would lead out of the modal
|
|
200
|
+
* (#424). `from` may be an element of an embedded document, whose frame
|
|
201
|
+
* element then stands in for it.
|
|
202
|
+
*/
|
|
203
|
+
const moveFocus = (from: Element | null, backwards: boolean) => {
|
|
204
|
+
const items = focusables(panel);
|
|
205
|
+
const first = items[0];
|
|
206
|
+
const last = items[items.length - 1];
|
|
207
|
+
if (!first || !last) {
|
|
208
|
+
panel.focus();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const at = from ? items.indexOf(from as HTMLElement) : -1;
|
|
212
|
+
let next: HTMLElement | undefined;
|
|
213
|
+
if (at !== -1) {
|
|
214
|
+
next = items[at + (backwards ? -1 : 1)];
|
|
215
|
+
} else if (from && from !== panel && panel.contains(from)) {
|
|
216
|
+
// Inside the panel but not a tab stop: continue from its position
|
|
217
|
+
const after = (el: HTMLElement) =>
|
|
218
|
+
!!(
|
|
219
|
+
from.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
220
|
+
);
|
|
221
|
+
next = backwards
|
|
222
|
+
? items.filter((el) => !after(el)).pop()
|
|
223
|
+
: items.find(after);
|
|
224
|
+
} else {
|
|
225
|
+
next = backwards ? last : first;
|
|
226
|
+
}
|
|
227
|
+
(next ?? (backwards ? last : first)).focus();
|
|
228
|
+
};
|
|
229
|
+
|
|
190
230
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
191
|
-
if (
|
|
231
|
+
if (!top()) return;
|
|
192
232
|
|
|
193
233
|
if (e.key === 'Escape') {
|
|
194
234
|
if (!dismissible) return;
|
|
@@ -199,25 +239,69 @@ export function useModalFocus(
|
|
|
199
239
|
}
|
|
200
240
|
|
|
201
241
|
if (e.key !== 'Tab') return;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
moveFocus(document.activeElement, e.shiftKey);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Key events inside an embedded document never reach this one (#425).
|
|
247
|
+
// Escape closes the dialog from there too; Tab moves on natively within
|
|
248
|
+
// the frame and is taken over at its first and last controls.
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const { signal } = controller;
|
|
251
|
+
const frameDocs = new Map<HTMLIFrameElement, Document>();
|
|
252
|
+
const attachFrame = (frame: HTMLIFrameElement) => {
|
|
253
|
+
let doc: Document | null = null;
|
|
254
|
+
try {
|
|
255
|
+
doc = frame.contentDocument;
|
|
256
|
+
} catch {
|
|
257
|
+
// Not accessible
|
|
217
258
|
}
|
|
259
|
+
// Already listening to this document
|
|
260
|
+
if (!doc || frameDocs.get(frame) === doc) return;
|
|
261
|
+
frameDocs.set(frame, doc);
|
|
262
|
+
const onFrameKeyDown = (e: KeyboardEvent) => {
|
|
263
|
+
if (!top()) return;
|
|
264
|
+
if (e.key !== 'Tab') return onKeyDown(e);
|
|
265
|
+
const inner = Array.from(
|
|
266
|
+
doc!.querySelectorAll<HTMLElement>(FOCUSABLE),
|
|
267
|
+
).filter((el) => isAvailable(el) && isTabStop(el));
|
|
268
|
+
const active = doc!.activeElement;
|
|
269
|
+
const edge = e.shiftKey ? inner[0] : inner[inner.length - 1];
|
|
270
|
+
if (!edge || active === edge || active === doc!.body) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
moveFocus(frame, e.shiftKey);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
doc.addEventListener('keydown', onFrameKeyDown, { signal });
|
|
276
|
+
};
|
|
277
|
+
const attachFrames = () => {
|
|
278
|
+
panel.querySelectorAll('iframe').forEach(attachFrame);
|
|
218
279
|
};
|
|
280
|
+
// load doesn't bubble; a frame's document is replaced when it navigates
|
|
281
|
+
const onLoad = (e: Event) => {
|
|
282
|
+
if (e.target instanceof HTMLIFrameElement) attachFrame(e.target);
|
|
283
|
+
};
|
|
284
|
+
panel.addEventListener('load', onLoad, { capture: true, signal });
|
|
285
|
+
const frames = new MutationObserver(attachFrames);
|
|
286
|
+
frames.observe(panel, { childList: true, subtree: true });
|
|
287
|
+
attachFrames();
|
|
219
288
|
|
|
220
|
-
|
|
221
|
-
|
|
289
|
+
// Focus that lands outside the dialog anyway, e.g. leaving a frame whose
|
|
290
|
+
// document can't be reached, is brought back in.
|
|
291
|
+
const onFocusIn = (e: FocusEvent) => {
|
|
292
|
+
const target = e.target;
|
|
293
|
+
if (!top() || !(target instanceof Node) || panel.contains(target)) return;
|
|
294
|
+
if (target === document.body || target === document.documentElement) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
moveFocus(null, false);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
document.addEventListener('keydown', onKeyDown, { signal });
|
|
301
|
+
document.addEventListener('focusin', onFocusIn, { signal });
|
|
302
|
+
return () => {
|
|
303
|
+
controller.abort();
|
|
304
|
+
frames.disconnect();
|
|
305
|
+
};
|
|
222
306
|
}, [dismissible, onClose]);
|
|
223
307
|
}
|
package/src/markup/render.tsx
CHANGED
|
@@ -84,6 +84,27 @@ const SvgContext = createContext(false);
|
|
|
84
84
|
|
|
85
85
|
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Elements whose children must be elements of particular types: markdown
|
|
89
|
+
* paragraphs between them would pull the children out of the structure (#423).
|
|
90
|
+
* Text between the children, when it is only whitespace, is dropped.
|
|
91
|
+
*/
|
|
92
|
+
const STRUCTURAL_ELEMENTS = new Set([
|
|
93
|
+
'table',
|
|
94
|
+
'thead',
|
|
95
|
+
'tbody',
|
|
96
|
+
'tfoot',
|
|
97
|
+
'tr',
|
|
98
|
+
'colgroup',
|
|
99
|
+
'ul',
|
|
100
|
+
'ol',
|
|
101
|
+
'menu',
|
|
102
|
+
'dl',
|
|
103
|
+
'select',
|
|
104
|
+
'optgroup',
|
|
105
|
+
'datalist',
|
|
106
|
+
]);
|
|
107
|
+
|
|
87
108
|
/** Elements whose content is literal text, not markdown. */
|
|
88
109
|
const PREFORMATTED_ELEMENTS = new Set(['pre', 'textarea', 'style']);
|
|
89
110
|
export const WidgetChildrenContext = createContext<WidgetChildren | null>(null);
|
|
@@ -693,16 +714,22 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
|
693
714
|
tag === 'details' ? leadingSummaryIndex(node.children) : -1;
|
|
694
715
|
const body = (nodes: ASTNode[]) =>
|
|
695
716
|
renderNodes(nodes, { nobr, locals, inline: isInline });
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
717
|
+
const structural =
|
|
718
|
+
STRUCTURAL_ELEMENTS.has(tag) &&
|
|
719
|
+
node.children.every((n) => n.type !== 'text' || n.value.trim() === '');
|
|
720
|
+
children = structural ? (
|
|
721
|
+
node.children
|
|
722
|
+
.filter((n) => n.type !== 'text')
|
|
723
|
+
.map((n) => renderSingleNode(n))
|
|
724
|
+
) : summaryAt === -1 ? (
|
|
725
|
+
body(node.children)
|
|
726
|
+
) : (
|
|
727
|
+
<>
|
|
728
|
+
{body(node.children.slice(0, summaryAt))}
|
|
729
|
+
{renderSingleNode(node.children[summaryAt]!)}
|
|
730
|
+
{body(node.children.slice(summaryAt + 1))}
|
|
731
|
+
</>
|
|
732
|
+
);
|
|
706
733
|
if (isInline !== parentInline) {
|
|
707
734
|
children = (
|
|
708
735
|
<InlineContext.Provider value={isInline}>
|