@classic-homes/theme-svelte 1.41.0 → 1.42.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.
|
@@ -356,6 +356,23 @@
|
|
|
356
356
|
}
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Clamp a detached position so the panel (its header above all) stays inside
|
|
361
|
+
* the current viewport. The panel renders at `min(size, 100%)`, so clamp
|
|
362
|
+
* against that visible size: an oversized panel then pins to 0,0.
|
|
363
|
+
* Mirrors the React DataPanel's restore and window-resize clamping.
|
|
364
|
+
*/
|
|
365
|
+
function clampDetachedPosition(position: Position, size: Size): Position {
|
|
366
|
+
const vw = layoutViewportWidth();
|
|
367
|
+
const vh = window.innerHeight;
|
|
368
|
+
return constrainPosition(
|
|
369
|
+
position,
|
|
370
|
+
{ width: Math.min(size.width, vw), height: Math.min(size.height, vh) },
|
|
371
|
+
vw,
|
|
372
|
+
vh
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
359
376
|
// Initialize from persistence on mount
|
|
360
377
|
onMount(() => {
|
|
361
378
|
// Update viewport dimensions
|
|
@@ -370,6 +387,13 @@
|
|
|
370
387
|
viewportWidth = layoutViewportWidth();
|
|
371
388
|
viewportHeight = window.innerHeight;
|
|
372
389
|
cardExpandedHeight = viewportHeight * resolvedCardConfig.expandedRatio;
|
|
390
|
+
// Keep a detached panel reachable after the window shrinks
|
|
391
|
+
if (mode === 'detached') {
|
|
392
|
+
const clamped = clampDetachedPosition(detachedPosition, detachedSize);
|
|
393
|
+
if (clamped.x !== detachedPosition.x || clamped.y !== detachedPosition.y) {
|
|
394
|
+
detachedPosition = clamped;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
373
397
|
};
|
|
374
398
|
window.addEventListener('resize', handleResize);
|
|
375
399
|
|
|
@@ -402,6 +426,11 @@
|
|
|
402
426
|
}
|
|
403
427
|
}
|
|
404
428
|
|
|
429
|
+
// A position restored from storage may have been saved on a larger screen
|
|
430
|
+
if (persistKey) {
|
|
431
|
+
detachedPosition = clampDetachedPosition(detachedPosition, detachedSize);
|
|
432
|
+
}
|
|
433
|
+
|
|
405
434
|
// Escape-to-close is handled by useEscapeKey; focus trapping by useFocusTrap.
|
|
406
435
|
|
|
407
436
|
return () => {
|
|
@@ -15,13 +15,19 @@ function getFocusableElements(container) {
|
|
|
15
15
|
const elements = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
16
16
|
return Array.from(elements).filter((el) =>
|
|
17
17
|
// tabindex="-1" on a natively focusable element (roving-tabindex radios,
|
|
18
|
-
// toolbar buttons) takes it out of the Tab order,
|
|
19
|
-
// trap's first or
|
|
18
|
+
// toolbar buttons) takes it out of the Tab order, as do `inert`, `hidden`
|
|
19
|
+
// and a disabled <fieldset> ancestor, so none can be the trap's first or
|
|
20
|
+
// last stop.
|
|
20
21
|
el.tabIndex >= 0 &&
|
|
21
|
-
!el.
|
|
22
|
+
!el.matches(':disabled') &&
|
|
23
|
+
!el.closest('[inert],[hidden]') &&
|
|
22
24
|
el.offsetParent !== null &&
|
|
23
25
|
getComputedStyle(el).visibility !== 'hidden');
|
|
24
26
|
}
|
|
27
|
+
/** True when `a` comes before `b` in document order. */
|
|
28
|
+
function precedes(a, b) {
|
|
29
|
+
return Boolean(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
30
|
+
}
|
|
25
31
|
/**
|
|
26
32
|
* Creates a focus trap action for use with Svelte's use: directive.
|
|
27
33
|
*
|
|
@@ -53,6 +59,7 @@ export function useFocusTrap(options = {}) {
|
|
|
53
59
|
let container = $state(null);
|
|
54
60
|
let previousActiveElement = null;
|
|
55
61
|
let trapped = false;
|
|
62
|
+
let addedTabIndexTo = null;
|
|
56
63
|
function isActive() {
|
|
57
64
|
const active = currentOptions.active;
|
|
58
65
|
return typeof active === 'function' ? active() : (active ?? true);
|
|
@@ -61,21 +68,39 @@ export function useFocusTrap(options = {}) {
|
|
|
61
68
|
if (!trapped || event.key !== 'Tab' || !container)
|
|
62
69
|
return;
|
|
63
70
|
const preventScroll = currentOptions.preventScroll ?? false;
|
|
71
|
+
// Focus outside the container (e.g. a portaled nested popover) is not
|
|
72
|
+
// ours to move; the nested overlay manages its own Tab order.
|
|
73
|
+
const current = document.activeElement;
|
|
74
|
+
if (!current || !container.contains(current))
|
|
75
|
+
return;
|
|
64
76
|
const focusableElements = getFocusableElements(container);
|
|
65
|
-
|
|
77
|
+
// Nothing tabbable: keep focus on the container instead of letting Tab
|
|
78
|
+
// walk out of the trap.
|
|
79
|
+
if (focusableElements.length === 0) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
container.focus({ preventScroll });
|
|
66
82
|
return;
|
|
83
|
+
}
|
|
67
84
|
const firstElement = focusableElements[0];
|
|
68
85
|
const lastElement = focusableElements[focusableElements.length - 1];
|
|
86
|
+
// Focus on the container itself: enter the tab sequence at the matching end.
|
|
87
|
+
if (current === container) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
(event.shiftKey ? lastElement : firstElement).focus({ preventScroll });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
69
92
|
if (event.shiftKey) {
|
|
70
|
-
// Shift + Tab
|
|
71
|
-
|
|
93
|
+
// Shift + Tab from the first stop, or from a non-tabbable element before
|
|
94
|
+
// it (e.g. a clicked tabindex=-1 control), wraps to the last.
|
|
95
|
+
if (current === firstElement || precedes(current, firstElement)) {
|
|
72
96
|
event.preventDefault();
|
|
73
97
|
lastElement.focus({ preventScroll });
|
|
74
98
|
}
|
|
75
99
|
}
|
|
76
100
|
else {
|
|
77
|
-
// Tab
|
|
78
|
-
|
|
101
|
+
// Tab from the last stop, or from a non-tabbable element after it,
|
|
102
|
+
// wraps to the first.
|
|
103
|
+
if (current === lastElement || precedes(lastElement, current)) {
|
|
79
104
|
event.preventDefault();
|
|
80
105
|
firstElement.focus({ preventScroll });
|
|
81
106
|
}
|
|
@@ -87,21 +112,27 @@ export function useFocusTrap(options = {}) {
|
|
|
87
112
|
trapped = true;
|
|
88
113
|
// Save current active element to return focus later
|
|
89
114
|
previousActiveElement = document.activeElement;
|
|
90
|
-
// Focus initial element
|
|
115
|
+
// Focus initial element; an empty trap focuses the container itself,
|
|
116
|
+
// which needs a tabindex for that (removed again on deactivate).
|
|
91
117
|
const preventScroll = currentOptions.preventScroll ?? false;
|
|
92
118
|
const focusableElements = getFocusableElements(container);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
let elementToFocus = null;
|
|
120
|
+
if (currentOptions.initialFocus) {
|
|
121
|
+
elementToFocus = container.querySelector(currentOptions.initialFocus);
|
|
122
|
+
}
|
|
123
|
+
if (!elementToFocus) {
|
|
124
|
+
elementToFocus = focusableElements[0] ?? null;
|
|
125
|
+
}
|
|
126
|
+
if (!elementToFocus) {
|
|
127
|
+
if (!container.hasAttribute('tabindex')) {
|
|
128
|
+
container.setAttribute('tabindex', '-1');
|
|
129
|
+
addedTabIndexTo = container;
|
|
100
130
|
}
|
|
101
|
-
|
|
102
|
-
elementToFocus?.focus({ preventScroll });
|
|
103
|
-
});
|
|
131
|
+
elementToFocus = container;
|
|
104
132
|
}
|
|
133
|
+
requestAnimationFrame(() => {
|
|
134
|
+
elementToFocus?.focus({ preventScroll });
|
|
135
|
+
});
|
|
105
136
|
document.addEventListener('keydown', handleKeyDown);
|
|
106
137
|
}
|
|
107
138
|
function deactivate() {
|
|
@@ -109,6 +140,8 @@ export function useFocusTrap(options = {}) {
|
|
|
109
140
|
return;
|
|
110
141
|
trapped = false;
|
|
111
142
|
document.removeEventListener('keydown', handleKeyDown);
|
|
143
|
+
addedTabIndexTo?.removeAttribute('tabindex');
|
|
144
|
+
addedTabIndexTo = null;
|
|
112
145
|
// Return focus when trap is deactivated
|
|
113
146
|
const preventScroll = currentOptions.preventScroll ?? false;
|
|
114
147
|
const elementToReturn = currentOptions.returnFocusTo ?? previousActiveElement;
|