@txstate-mws/svelte-components 1.6.14 → 1.6.16
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/actions/glue.js
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
import { watchForMutations } from '../util/index.js';
|
|
2
2
|
import { equal } from 'txstate-utils';
|
|
3
|
+
/**
|
|
4
|
+
* All active glue instances share one scroll listener and one requestAnimationFrame
|
|
5
|
+
* per frame. Each frame runs every registered measure() (the layout reads) and only
|
|
6
|
+
* then runs the write thunks they return, so for a page with thousands of glued
|
|
7
|
+
* elements the browser reads layout once per frame and never interleaves reads with
|
|
8
|
+
* writes. Multiple scroll events that arrive before a frame paints coalesce into a
|
|
9
|
+
* single measure pass.
|
|
10
|
+
*/
|
|
11
|
+
const glueInstances = new Set();
|
|
12
|
+
let frameRequested = false;
|
|
13
|
+
let scrollBound = false;
|
|
14
|
+
function runGlueFrame() {
|
|
15
|
+
frameRequested = false;
|
|
16
|
+
const writes = [];
|
|
17
|
+
for (const measure of glueInstances) {
|
|
18
|
+
const write = measure();
|
|
19
|
+
if (write)
|
|
20
|
+
writes.push(write);
|
|
21
|
+
}
|
|
22
|
+
for (const write of writes)
|
|
23
|
+
write();
|
|
24
|
+
}
|
|
25
|
+
function scheduleGlueFrame() {
|
|
26
|
+
if (frameRequested)
|
|
27
|
+
return;
|
|
28
|
+
frameRequested = true;
|
|
29
|
+
requestAnimationFrame(runGlueFrame);
|
|
30
|
+
}
|
|
31
|
+
function onGlueScroll() {
|
|
32
|
+
scheduleGlueFrame();
|
|
33
|
+
}
|
|
34
|
+
function registerGlue(measure) {
|
|
35
|
+
glueInstances.add(measure);
|
|
36
|
+
if (!scrollBound) {
|
|
37
|
+
document.addEventListener('scroll', onGlueScroll, { capture: true, passive: true });
|
|
38
|
+
scrollBound = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function unregisterGlue(measure) {
|
|
42
|
+
glueInstances.delete(measure);
|
|
43
|
+
if (glueInstances.size === 0 && scrollBound) {
|
|
44
|
+
document.removeEventListener('scroll', onGlueScroll, { capture: true });
|
|
45
|
+
scrollBound = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
3
48
|
/**
|
|
4
49
|
* Make this element `position: fixed` and move it around to align with the target element.
|
|
5
50
|
*/
|
|
@@ -21,20 +66,33 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
21
66
|
}
|
|
22
67
|
}
|
|
23
68
|
let lastrect;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
69
|
+
// The containing block (nearest ancestor that establishes a containing block for
|
|
70
|
+
// position: fixed) is determined by the ancestors' computed styles. Scrolling can
|
|
71
|
+
// never change it, but it CAN change when an ancestor gains/loses a transform,
|
|
72
|
+
// filter, will-change, contain, etc. Walking the ancestor chain and calling
|
|
73
|
+
// getComputedStyle on each node is expensive, so cache it across the high-frequency
|
|
74
|
+
// scroll events and recompute only when something might have changed: a DOM/style
|
|
75
|
+
// mutation, a resize, or a target change (see invalidateAndSchedule / update).
|
|
76
|
+
let fixedParent;
|
|
77
|
+
let fixedParentComputed = false;
|
|
78
|
+
// Read-only pass: gather geometry and build the write thunk. Runs inside the shared
|
|
79
|
+
// frame's measure phase, so it must not write to the DOM.
|
|
80
|
+
function measure() {
|
|
81
|
+
if (!fixedParentComputed) {
|
|
82
|
+
fixedParent = fixedContainingBlock(el);
|
|
83
|
+
fixedParentComputed = true;
|
|
84
|
+
}
|
|
27
85
|
let fixedRect = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
28
86
|
if (fixedParent) {
|
|
29
87
|
const tmpRect = fixedParent.getBoundingClientRect();
|
|
30
88
|
fixedRect = { left: tmpRect.left, right: document.documentElement.clientWidth - tmpRect.right, top: tmpRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom };
|
|
31
89
|
}
|
|
32
90
|
if (!target)
|
|
33
|
-
return;
|
|
91
|
+
return undefined;
|
|
34
92
|
const tmpRect = target.getBoundingClientRect();
|
|
35
93
|
const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
|
|
36
94
|
if (equal(rect, lastrect))
|
|
37
|
-
return;
|
|
95
|
+
return undefined;
|
|
38
96
|
lastrect = rect;
|
|
39
97
|
let autoalign = align;
|
|
40
98
|
const leftright = rect.right > rect.left ? 'left' : 'right';
|
|
@@ -84,81 +142,75 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
84
142
|
const elOffsetHeight = el.offsetHeight;
|
|
85
143
|
const parentClientHeight = parent?.clientHeight ?? 0;
|
|
86
144
|
const parentOverflowY = parent?.style.overflowY;
|
|
145
|
+
let position;
|
|
87
146
|
if (autoalign === 'bottomleft') {
|
|
88
|
-
|
|
89
|
-
timer = requestAnimationFrame(() => {
|
|
147
|
+
position = () => {
|
|
90
148
|
el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
|
|
91
149
|
el.style.left = `${rect.left}px`;
|
|
92
150
|
el.style.removeProperty('bottom');
|
|
93
151
|
el.style.removeProperty('right');
|
|
94
152
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
95
|
-
}
|
|
153
|
+
};
|
|
96
154
|
valign = 'bottom';
|
|
97
155
|
halign = 'left';
|
|
98
156
|
}
|
|
99
157
|
else if (autoalign === 'bottomright') {
|
|
100
|
-
|
|
101
|
-
timer = requestAnimationFrame(() => {
|
|
158
|
+
position = () => {
|
|
102
159
|
el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
|
|
103
160
|
el.style.removeProperty('left');
|
|
104
161
|
el.style.removeProperty('bottom');
|
|
105
162
|
el.style.right = `${rect.right}px`;
|
|
106
163
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
107
|
-
}
|
|
164
|
+
};
|
|
108
165
|
valign = 'bottom';
|
|
109
166
|
halign = 'right';
|
|
110
167
|
}
|
|
111
168
|
else if (autoalign === 'topleft') {
|
|
112
|
-
|
|
113
|
-
timer = requestAnimationFrame(() => {
|
|
169
|
+
position = () => {
|
|
114
170
|
el.style.removeProperty('top');
|
|
115
171
|
el.style.left = `${rect.left}px`;
|
|
116
172
|
el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
|
|
117
173
|
el.style.removeProperty('right');
|
|
118
174
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
119
|
-
}
|
|
175
|
+
};
|
|
120
176
|
valign = 'top';
|
|
121
177
|
halign = 'left';
|
|
122
178
|
}
|
|
123
179
|
else if (autoalign === 'topright') {
|
|
124
|
-
|
|
125
|
-
timer = requestAnimationFrame(() => {
|
|
180
|
+
position = () => {
|
|
126
181
|
el.style.removeProperty('top');
|
|
127
182
|
el.style.removeProperty('left');
|
|
128
183
|
el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
|
|
129
184
|
el.style.right = `${rect.right}px`;
|
|
130
185
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
131
|
-
}
|
|
186
|
+
};
|
|
132
187
|
valign = 'top';
|
|
133
188
|
halign = 'right';
|
|
134
189
|
}
|
|
135
190
|
else if (autoalign === 'top') {
|
|
136
|
-
|
|
137
|
-
timer = requestAnimationFrame(() => {
|
|
191
|
+
position = () => {
|
|
138
192
|
el.style.removeProperty('top');
|
|
139
193
|
el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
|
|
140
194
|
el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
|
|
141
195
|
el.style.removeProperty('right');
|
|
142
196
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
143
|
-
}
|
|
197
|
+
};
|
|
144
198
|
valign = 'top';
|
|
145
199
|
halign = 'center';
|
|
146
200
|
}
|
|
147
201
|
else if (autoalign === 'bottom') {
|
|
148
|
-
|
|
149
|
-
timer = requestAnimationFrame(() => {
|
|
202
|
+
position = () => {
|
|
150
203
|
el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
|
|
151
204
|
el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
|
|
152
205
|
el.style.removeProperty('bottom');
|
|
153
206
|
el.style.removeProperty('right');
|
|
154
207
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
155
|
-
}
|
|
208
|
+
};
|
|
156
209
|
valign = 'bottom';
|
|
157
210
|
halign = 'center';
|
|
158
211
|
}
|
|
159
212
|
else if (autoalign === 'left') {
|
|
160
|
-
|
|
161
|
-
timer = requestAnimationFrame(() => {
|
|
213
|
+
position = () => {
|
|
162
214
|
el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
|
|
163
215
|
el.style.removeProperty('bottom');
|
|
164
216
|
if (cover) {
|
|
@@ -170,13 +222,12 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
170
222
|
el.style.right = `${rect.right + rect.width + gap}px`;
|
|
171
223
|
}
|
|
172
224
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
173
|
-
}
|
|
225
|
+
};
|
|
174
226
|
valign = 'center';
|
|
175
227
|
halign = 'left';
|
|
176
228
|
}
|
|
177
229
|
else if (autoalign === 'right') {
|
|
178
|
-
|
|
179
|
-
timer = requestAnimationFrame(() => {
|
|
230
|
+
position = () => {
|
|
180
231
|
el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
|
|
181
232
|
el.style.removeProperty('bottom');
|
|
182
233
|
if (cover) {
|
|
@@ -188,26 +239,35 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
188
239
|
el.style.removeProperty('right');
|
|
189
240
|
}
|
|
190
241
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
191
|
-
}
|
|
242
|
+
};
|
|
192
243
|
valign = 'center';
|
|
193
244
|
halign = 'right';
|
|
194
245
|
}
|
|
195
246
|
else if (autoalign === 'middle') {
|
|
196
|
-
|
|
197
|
-
timer = requestAnimationFrame(() => {
|
|
247
|
+
position = () => {
|
|
198
248
|
el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
|
|
199
249
|
el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
|
|
200
250
|
el.style.removeProperty('bottom');
|
|
201
251
|
el.style.removeProperty('right');
|
|
202
252
|
adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
|
|
203
|
-
}
|
|
253
|
+
};
|
|
204
254
|
valign = 'center';
|
|
205
255
|
halign = 'center';
|
|
206
256
|
}
|
|
207
|
-
|
|
257
|
+
const fvalign = valign;
|
|
258
|
+
const fhalign = halign;
|
|
259
|
+
return () => {
|
|
260
|
+
position?.();
|
|
261
|
+
store?.update(v => ({ ...v, valign: fvalign, halign: fhalign }));
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function invalidateAndSchedule() {
|
|
265
|
+
// A mutation or resize may have changed which ancestor is the containing block.
|
|
266
|
+
fixedParentComputed = false;
|
|
267
|
+
scheduleGlueFrame();
|
|
208
268
|
}
|
|
209
|
-
const { destroy: watchDestroy } = watchForMutations(
|
|
210
|
-
|
|
269
|
+
const { destroy: watchDestroy } = watchForMutations(invalidateAndSchedule);
|
|
270
|
+
registerGlue(measure);
|
|
211
271
|
return {
|
|
212
272
|
update({ target: utarget, align: ualign = 'auto', cover: ucover = false, gap: ugap = 0, store: ustore }) {
|
|
213
273
|
align = ualign;
|
|
@@ -223,13 +283,14 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
223
283
|
el.style.removeProperty('position');
|
|
224
284
|
else {
|
|
225
285
|
el.style.position = 'fixed';
|
|
226
|
-
|
|
286
|
+
fixedParentComputed = false;
|
|
227
287
|
}
|
|
288
|
+
scheduleGlueFrame();
|
|
228
289
|
}
|
|
229
290
|
},
|
|
230
291
|
destroy() {
|
|
292
|
+
unregisterGlue(measure);
|
|
231
293
|
watchDestroy();
|
|
232
|
-
document.removeEventListener('scroll', reposition);
|
|
233
294
|
if (adjustparentheight && parent) {
|
|
234
295
|
if (formerMinHeight)
|
|
235
296
|
parent.style.minHeight = formerMinHeight;
|
|
@@ -13,7 +13,7 @@ When they do, the escape event will be fired, allowing you to remove the dialog.
|
|
|
13
13
|
When it goes away, they'll be trapped inside the previous `FocusLock`, and so on.
|
|
14
14
|
-->
|
|
15
15
|
<script context="module" lang="ts">
|
|
16
|
-
export const FocusLockStack: { focusId: string, pause: () => void, unpause: () => void, update: () => void }[] = []
|
|
16
|
+
export const FocusLockStack: { focusId: string, pause: () => void, unpause: () => void, update: () => void, inheritReturnFocus: (el: HTMLElement) => void }[] = []
|
|
17
17
|
const waitAtick = typeof requestAnimationFrame !== 'undefined' ? (resolve: (value: unknown) => void) => requestAnimationFrame(resolve) : (resolve: (value: unknown) => void) => resolve(0)
|
|
18
18
|
</script>
|
|
19
19
|
|
|
@@ -49,7 +49,13 @@ When it goes away, they'll be trapped inside the previous `FocusLock`, and so on
|
|
|
49
49
|
focusId,
|
|
50
50
|
pause: () => { state = 'paused' },
|
|
51
51
|
unpause: () => { if (state === 'paused') state = 'active' },
|
|
52
|
-
update: () => { dispatch('focuslockupdate') }
|
|
52
|
+
update: () => { dispatch('focuslockupdate') },
|
|
53
|
+
inheritReturnFocus: el => {
|
|
54
|
+
// only accept a hand-me-down if our own recorded target is useless - we
|
|
55
|
+
// captured document.activeElement after the previous lock's content was
|
|
56
|
+
// already removed, so it's usually document.body or a disconnected element
|
|
57
|
+
if (!returnfocusto || returnfocusto === document.body || !returnfocusto.isConnected) returnfocusto = el
|
|
58
|
+
}
|
|
53
59
|
})
|
|
54
60
|
for (const entry of FocusLockStack) entry.update()
|
|
55
61
|
if (typeof returnfocusto === 'undefined') {
|
|
@@ -76,8 +82,17 @@ When it goes away, they'll be trapped inside the previous `FocusLock`, and so on
|
|
|
76
82
|
// will think it was an "outside" click and kill itself
|
|
77
83
|
await new Promise(waitAtick)
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const top = FocusLockStack.slice(-1)[0]
|
|
86
|
+
if (top?.focusId === focusId) {
|
|
87
|
+
// never restore focus to document.body - it accomplishes nothing except
|
|
88
|
+
// blurring whatever the user has focused since we began closing
|
|
89
|
+
if (returnfocusto && wasactive && returnfocusto !== document.body) returnfocusto.focus()
|
|
90
|
+
} else if (top && returnfocusto?.isConnected && returnfocusto !== document.body) {
|
|
91
|
+
// another focus lock mounted while we were waiting (e.g. one modal replaced
|
|
92
|
+
// by another in the same update) and has taken focus - restoring ours would
|
|
93
|
+
// steal it back out of the new dialog. instead, hand our return target down
|
|
94
|
+
// the chain so the final modal can return focus to the original trigger
|
|
95
|
+
top.inheritReturnFocus(returnfocusto)
|
|
81
96
|
}
|
|
82
97
|
const idx = FocusLockStack.findIndex(f => f.focusId === focusId)
|
|
83
98
|
if (idx > -1) FocusLockStack.splice(idx, 1)
|
|
@@ -98,7 +113,7 @@ When it goes away, they'll be trapped inside the previous `FocusLock`, and so on
|
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
const keydown = (e: KeyboardEvent) => {
|
|
101
|
-
if (state === 'active' && e.key === 'Escape' && escapable) {
|
|
116
|
+
if ((state === 'active' || state === 'init') && e.key === 'Escape' && escapable) {
|
|
102
117
|
e.preventDefault()
|
|
103
118
|
dispatch('escape')
|
|
104
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@txstate-mws/svelte-components",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.16",
|
|
4
4
|
"description": "Svelte components that are generically useful.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"prepublishOnly": "svelte-package",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@sveltejs/package": "^2.0.2",
|
|
30
30
|
"eslint-config-love": "^154.0.0",
|
|
31
31
|
"eslint-plugin-svelte": "^3.0.0",
|
|
32
|
-
"svelte": "^5.
|
|
32
|
+
"svelte": "^5.56.3",
|
|
33
33
|
"svelte-check": "^4.0.0",
|
|
34
34
|
"tslib": "^2.3.1",
|
|
35
35
|
"typescript": "^6.0.3"
|