@marianmeres/stuic 3.151.0 → 3.152.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.
- package/dist/actions/dim-behind/dim-behind.fixture.svelte +54 -0
- package/dist/actions/dim-behind/dim-behind.fixture.svelte.d.ts +9 -0
- package/dist/actions/dim-behind/dim-behind.svelte.d.ts +10 -0
- package/dist/actions/dim-behind/dim-behind.svelte.js +72 -41
- package/dist/actions/popover/README.md +37 -17
- package/dist/actions/popover/popover.container.fixture.svelte +26 -0
- package/dist/actions/popover/popover.container.fixture.svelte.d.ts +7 -0
- package/dist/actions/popover/popover.svelte.d.ts +10 -0
- package/dist/actions/popover/popover.svelte.js +20 -7
- package/dist/actions/spotlight/spotlight.container.fixture.svelte +33 -0
- package/dist/actions/spotlight/spotlight.container.fixture.svelte.d.ts +7 -0
- package/dist/actions/spotlight/spotlight.svelte.d.ts +9 -0
- package/dist/actions/spotlight/spotlight.svelte.js +95 -37
- package/dist/components/DropdownMenu/DropdownMenu.svelte +14 -7
- package/dist/components/DropdownMenu/README.md +1 -0
- package/dist/components/Float/Float.svelte +21 -0
- package/dist/components/Float/README.md +1 -1
- package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte +30 -5
- package/dist/utils/anchor-position.d.ts +12 -3
- package/dist/utils/anchor-position.js +35 -15
- package/dist/utils/containing-block.d.ts +55 -0
- package/dist/utils/containing-block.js +131 -0
- package/dist/utils/overlay-container.d.ts +16 -0
- package/dist/utils/overlay-container.js +12 -0
- package/package.json +12 -12
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { dimBehind } from "./dim-behind.svelte.js";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
aOpen = false,
|
|
6
|
+
bOpen = false,
|
|
7
|
+
useContainers = false,
|
|
8
|
+
sameContainer = false,
|
|
9
|
+
}: {
|
|
10
|
+
aOpen?: boolean;
|
|
11
|
+
bOpen?: boolean;
|
|
12
|
+
useContainers?: boolean;
|
|
13
|
+
sameContainer?: boolean;
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
let containerA = $state<HTMLDivElement>();
|
|
17
|
+
let containerB = $state<HTMLDivElement>();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<!-- Two independent shells, each a fixed containing block + stacking context
|
|
21
|
+
(the framed-app case the `container` option exists for). -->
|
|
22
|
+
<div
|
|
23
|
+
bind:this={containerA}
|
|
24
|
+
data-testid="container-a"
|
|
25
|
+
style="contain: layout paint; width: 200px; height: 120px;"
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
data-testid="target-a"
|
|
29
|
+
use:dimBehind={() => ({
|
|
30
|
+
open: aOpen,
|
|
31
|
+
container: useContainers ? () => containerA ?? null : undefined,
|
|
32
|
+
})}
|
|
33
|
+
>
|
|
34
|
+
A
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
bind:this={containerB}
|
|
40
|
+
data-testid="container-b"
|
|
41
|
+
style="contain: layout paint; width: 200px; height: 120px;"
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
data-testid="target-b"
|
|
45
|
+
use:dimBehind={() => ({
|
|
46
|
+
open: bOpen,
|
|
47
|
+
container: useContainers
|
|
48
|
+
? () => (sameContainer ? (containerA ?? null) : (containerB ?? null))
|
|
49
|
+
: undefined,
|
|
50
|
+
})}
|
|
51
|
+
>
|
|
52
|
+
B
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
aOpen?: boolean;
|
|
3
|
+
bOpen?: boolean;
|
|
4
|
+
useContainers?: boolean;
|
|
5
|
+
sameContainer?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const DimBehind: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type DimBehind = ReturnType<typeof DimBehind>;
|
|
9
|
+
export default DimBehind;
|
|
@@ -48,6 +48,16 @@ export interface DimBehindOptions {
|
|
|
48
48
|
onHide?: () => void;
|
|
49
49
|
/** Debug mode */
|
|
50
50
|
debug?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Where to append the backdrop. Defaults to the current behavior:
|
|
53
|
+
* `document.body`. Pass a bounded shell (framed app, embedded widget,
|
|
54
|
+
* dashboard pane) to keep the backdrop inside it — required when that shell
|
|
55
|
+
* is a stacking context, since the elevated target's z-index can then only
|
|
56
|
+
* compete with a backdrop living in the same context. Each distinct
|
|
57
|
+
* container gets its own ref-counted backdrop. A function returning `null`
|
|
58
|
+
* falls back to the default.
|
|
59
|
+
*/
|
|
60
|
+
container?: HTMLElement | (() => HTMLElement | null);
|
|
51
61
|
}
|
|
52
62
|
/**
|
|
53
63
|
* A Svelte action that dims everything behind a target element.
|
|
@@ -1,46 +1,60 @@
|
|
|
1
1
|
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { resolveContainerOption } from "../../utils/overlay-container.js";
|
|
3
|
+
// --- Shared Backdrop Manager (one ref-counted backdrop per container) ---
|
|
4
|
+
const backdrops = new Map();
|
|
5
5
|
const TRANSITION_SAFETY_MARGIN = 50;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Actual transition duration of the backdrop element in ms — read from its
|
|
8
|
+
* computed style, which resolves the whole var chain
|
|
9
|
+
* (`--stuic-dim-behind-transition-duration`, `--stuic-transition`, …), unlike
|
|
10
|
+
* reading a single custom property off the root. Computed values serialize in
|
|
11
|
+
* seconds ("0.15s").
|
|
12
|
+
*/
|
|
13
|
+
function getTransitionDurationMs(el) {
|
|
14
|
+
const raw = getComputedStyle(el).transitionDuration;
|
|
15
|
+
const v = parseFloat(raw);
|
|
16
|
+
if (!Number.isFinite(v) || v < 0)
|
|
17
|
+
return 150;
|
|
18
|
+
return raw.trim().endsWith("ms") ? v : v * 1000;
|
|
11
19
|
}
|
|
12
20
|
function getElementZIndex() {
|
|
13
21
|
return (getComputedStyle(document.documentElement)
|
|
14
22
|
.getPropertyValue("--stuic-dim-behind-element-z-index")
|
|
15
23
|
.trim() || "41");
|
|
16
24
|
}
|
|
17
|
-
function showBackdrop(classBackdrop) {
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
function showBackdrop(container, classBackdrop) {
|
|
26
|
+
let entry = backdrops.get(container);
|
|
27
|
+
if (!entry) {
|
|
28
|
+
const el = document.createElement("div");
|
|
29
|
+
el.classList.add("stuic-dim-behind-backdrop");
|
|
22
30
|
if (classBackdrop) {
|
|
23
|
-
|
|
31
|
+
el.classList.add(...classBackdrop.split(/\s+/).filter(Boolean));
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
container.appendChild(el);
|
|
26
34
|
// Force reflow for transition
|
|
27
|
-
void
|
|
28
|
-
|
|
35
|
+
void el.offsetHeight;
|
|
36
|
+
el.classList.add("dim-visible");
|
|
37
|
+
entry = { el, count: 0 };
|
|
38
|
+
backdrops.set(container, entry);
|
|
29
39
|
}
|
|
40
|
+
entry.count++;
|
|
41
|
+
return entry.el;
|
|
30
42
|
}
|
|
31
|
-
function hideBackdrop() {
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
43
|
+
function hideBackdrop(container) {
|
|
44
|
+
const entry = backdrops.get(container);
|
|
45
|
+
if (!entry)
|
|
46
|
+
return;
|
|
47
|
+
entry.count = Math.max(0, entry.count - 1);
|
|
48
|
+
if (entry.count === 0) {
|
|
49
|
+
// Drop the registry entry immediately so a show() during the fade-out
|
|
50
|
+
// creates a fresh backdrop instead of resurrecting the dying one.
|
|
51
|
+
backdrops.delete(container);
|
|
52
|
+
const el = entry.el;
|
|
35
53
|
el.classList.remove("dim-visible");
|
|
36
|
-
const cleanup = () =>
|
|
37
|
-
el.remove();
|
|
38
|
-
if (backdropEl === el)
|
|
39
|
-
backdropEl = null;
|
|
40
|
-
};
|
|
54
|
+
const cleanup = () => el.remove();
|
|
41
55
|
el.addEventListener("transitionend", cleanup, { once: true });
|
|
42
56
|
// Safety fallback in case transitionend doesn't fire
|
|
43
|
-
setTimeout(cleanup,
|
|
57
|
+
setTimeout(cleanup, getTransitionDurationMs(el) + TRANSITION_SAFETY_MARGIN);
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
// --- Registry ---
|
|
@@ -115,6 +129,12 @@ export function dimBehind(node, fn) {
|
|
|
115
129
|
let savedZIndex = "";
|
|
116
130
|
let currentOptions = {};
|
|
117
131
|
let do_debug = false;
|
|
132
|
+
// The container/backdrop/locks this instance acquired — captured at show()
|
|
133
|
+
// time so hide() releases exactly what was acquired even if options changed
|
|
134
|
+
// while visible.
|
|
135
|
+
let myContainer = null;
|
|
136
|
+
let myBackdropEl = null;
|
|
137
|
+
let myScrollLocked = false;
|
|
118
138
|
const debug = (...args) => {
|
|
119
139
|
if (do_debug)
|
|
120
140
|
console.debug("[dimBehind]", ...args);
|
|
@@ -128,7 +148,7 @@ export function dimBehind(node, fn) {
|
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
function onBackdropClick(e) {
|
|
131
|
-
if (e.target ===
|
|
151
|
+
if (e.target === myBackdropEl) {
|
|
132
152
|
hide();
|
|
133
153
|
}
|
|
134
154
|
}
|
|
@@ -151,18 +171,20 @@ export function dimBehind(node, fn) {
|
|
|
151
171
|
: getElementZIndex();
|
|
152
172
|
node.style.position = "relative";
|
|
153
173
|
node.style.zIndex = zIndex;
|
|
154
|
-
// Show
|
|
155
|
-
|
|
174
|
+
// Show the (per-container, ref-counted) shared backdrop
|
|
175
|
+
myContainer = resolveContainerOption(currentOptions.container) ?? document.body;
|
|
176
|
+
myBackdropEl = showBackdrop(myContainer, currentOptions.classBackdrop);
|
|
156
177
|
// Optional scroll lock
|
|
157
178
|
if (currentOptions.scrollLock) {
|
|
158
179
|
BodyScroll.lock();
|
|
180
|
+
myScrollLocked = true;
|
|
159
181
|
}
|
|
160
182
|
// Event listeners
|
|
161
183
|
if (currentOptions.closeOnEscape !== false) {
|
|
162
184
|
document.addEventListener("keydown", onEscape);
|
|
163
185
|
}
|
|
164
|
-
if (currentOptions.closeOnBackdropClick !== false
|
|
165
|
-
|
|
186
|
+
if (currentOptions.closeOnBackdropClick !== false) {
|
|
187
|
+
myBackdropEl.addEventListener("click", onBackdropClick);
|
|
166
188
|
}
|
|
167
189
|
currentOptions.onShow?.();
|
|
168
190
|
}
|
|
@@ -179,15 +201,18 @@ export function dimBehind(node, fn) {
|
|
|
179
201
|
node.style.zIndex = savedZIndex;
|
|
180
202
|
// Remove event listeners
|
|
181
203
|
document.removeEventListener("keydown", onEscape);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (currentOptions.scrollLock) {
|
|
204
|
+
myBackdropEl?.removeEventListener("click", onBackdropClick);
|
|
205
|
+
// Release the scroll lock iff THIS instance acquired one (the option may
|
|
206
|
+
// have changed while visible)
|
|
207
|
+
if (myScrollLocked) {
|
|
187
208
|
BodyScroll.unlock();
|
|
209
|
+
myScrollLocked = false;
|
|
188
210
|
}
|
|
189
|
-
//
|
|
190
|
-
|
|
211
|
+
// Release the shared backdrop
|
|
212
|
+
if (myContainer)
|
|
213
|
+
hideBackdrop(myContainer);
|
|
214
|
+
myContainer = null;
|
|
215
|
+
myBackdropEl = null;
|
|
191
216
|
currentOptions.onHide?.();
|
|
192
217
|
}
|
|
193
218
|
// Reactive params effect
|
|
@@ -205,6 +230,7 @@ export function dimBehind(node, fn) {
|
|
|
205
230
|
onShow: opts.onShow,
|
|
206
231
|
onHide: opts.onHide,
|
|
207
232
|
debug: opts.debug,
|
|
233
|
+
container: opts.container,
|
|
208
234
|
};
|
|
209
235
|
do_debug = !!opts.debug;
|
|
210
236
|
// Register in global registry if id provided
|
|
@@ -229,10 +255,15 @@ export function dimBehind(node, fn) {
|
|
|
229
255
|
if (isVisible) {
|
|
230
256
|
node.style.position = savedPosition;
|
|
231
257
|
node.style.zIndex = savedZIndex;
|
|
232
|
-
if (
|
|
258
|
+
if (myScrollLocked) {
|
|
233
259
|
BodyScroll.unlock();
|
|
260
|
+
myScrollLocked = false;
|
|
234
261
|
}
|
|
235
|
-
|
|
262
|
+
myBackdropEl?.removeEventListener("click", onBackdropClick);
|
|
263
|
+
if (myContainer)
|
|
264
|
+
hideBackdrop(myContainer);
|
|
265
|
+
myContainer = null;
|
|
266
|
+
myBackdropEl = null;
|
|
236
267
|
document.removeEventListener("keydown", onEscape);
|
|
237
268
|
}
|
|
238
269
|
// Unregister from registry
|
|
@@ -4,23 +4,24 @@ A Svelte action that displays an anchored popover using CSS Anchor Positioning,
|
|
|
4
4
|
|
|
5
5
|
## Options
|
|
6
6
|
|
|
7
|
-
| Option | Type
|
|
8
|
-
| --------------------- |
|
|
9
|
-
| `enabled` | `boolean`
|
|
10
|
-
| `content` | `THC \| null`
|
|
11
|
-
| `position` | `PopoverPosition`
|
|
12
|
-
| `trigger` | `"click" \| "hover"`
|
|
13
|
-
| `showDelay` | `number`
|
|
14
|
-
| `hideDelay` | `number`
|
|
15
|
-
| `class` | `string`
|
|
16
|
-
| `offset` | `string`
|
|
17
|
-
| `closeOthers` | `boolean`
|
|
18
|
-
| `closeOnClickOutside` | `boolean`
|
|
19
|
-
| `closeOnEscape` | `boolean`
|
|
20
|
-
| `showBackdrop` | `boolean`
|
|
21
|
-
| `forceFallback` | `boolean`
|
|
22
|
-
| `onShow` | `() => void`
|
|
23
|
-
| `onHide` | `() => void`
|
|
7
|
+
| Option | Type | Default | Description |
|
|
8
|
+
| --------------------- | -------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `enabled` | `boolean` | `true` | Enable/disable popover |
|
|
10
|
+
| `content` | `THC \| null` | - | Popover content (string, HTML, component, or snippet) |
|
|
11
|
+
| `position` | `PopoverPosition` | `"bottom"` | Placement relative to anchor |
|
|
12
|
+
| `trigger` | `"click" \| "hover"` | `"click"` | Trigger mode |
|
|
13
|
+
| `showDelay` | `number` | `100` | Delay before showing (ms) |
|
|
14
|
+
| `hideDelay` | `number` | `200` | Delay before hiding (ms) |
|
|
15
|
+
| `class` | `string` | - | Custom CSS for popover container |
|
|
16
|
+
| `offset` | `string` | `"0.25rem"` | Margin from anchor (CSS value) |
|
|
17
|
+
| `closeOthers` | `boolean` | `false` | Close other open popovers |
|
|
18
|
+
| `closeOnClickOutside` | `boolean` | `true` | Close on outside click (click trigger) |
|
|
19
|
+
| `closeOnEscape` | `boolean` | `true` | Close on Escape key |
|
|
20
|
+
| `showBackdrop` | `boolean` | `true` | Show backdrop in fallback mode |
|
|
21
|
+
| `forceFallback` | `boolean` | `false` | Force centered modal mode |
|
|
22
|
+
| `onShow` | `() => void` | - | Callback when popover opens |
|
|
23
|
+
| `onHide` | `() => void` | - | Callback when popover closes |
|
|
24
|
+
| `container` | `HTMLElement \| (() => HTMLElement \| null)` | closest open `<dialog>`, else `document.body` | Where to append the popover — pass a bounded shell to keep the overlay inside its stacking context |
|
|
24
25
|
|
|
25
26
|
## Positions
|
|
26
27
|
|
|
@@ -139,6 +140,25 @@ bottom-left bottom bottom-right
|
|
|
139
140
|
</button>
|
|
140
141
|
```
|
|
141
142
|
|
|
143
|
+
### Custom Container (Bounded Shells)
|
|
144
|
+
|
|
145
|
+
When the app renders inside a bounded shell that is a stacking context and/or a
|
|
146
|
+
fixed containing block (e.g. a centered frame with `contain: layout paint`, an
|
|
147
|
+
embedded widget, a transformed wrapper), a body-level popover can only paint
|
|
148
|
+
entirely above or below that shell. Pass `container` to keep the overlay inside
|
|
149
|
+
it:
|
|
150
|
+
|
|
151
|
+
```svelte
|
|
152
|
+
<button
|
|
153
|
+
use:popover={() => ({
|
|
154
|
+
content: "Stays inside the frame",
|
|
155
|
+
container: () => document.querySelector(".app-frame"),
|
|
156
|
+
})}
|
|
157
|
+
>
|
|
158
|
+
Framed
|
|
159
|
+
</button>
|
|
160
|
+
```
|
|
161
|
+
|
|
142
162
|
## Helper Function
|
|
143
163
|
|
|
144
164
|
```ts
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { popover } from "./popover.svelte.js";
|
|
3
|
+
|
|
4
|
+
let { open = false, useContainer = false }: { open?: boolean; useContainer?: boolean } =
|
|
5
|
+
$props();
|
|
6
|
+
|
|
7
|
+
let shell = $state<HTMLDivElement>();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!-- A bounded shell that is a fixed containing block + stacking context. -->
|
|
11
|
+
<div
|
|
12
|
+
bind:this={shell}
|
|
13
|
+
data-testid="shell"
|
|
14
|
+
style="contain: layout paint; width: 300px; height: 200px;"
|
|
15
|
+
></div>
|
|
16
|
+
|
|
17
|
+
<button
|
|
18
|
+
data-testid="trigger"
|
|
19
|
+
use:popover={() => ({
|
|
20
|
+
content: "Hello",
|
|
21
|
+
open,
|
|
22
|
+
container: useContainer ? () => shell ?? null : undefined,
|
|
23
|
+
})}
|
|
24
|
+
>
|
|
25
|
+
trigger
|
|
26
|
+
</button>
|
|
@@ -101,6 +101,16 @@ export interface PopoverOptions {
|
|
|
101
101
|
open?: boolean;
|
|
102
102
|
/** Unique ID for registry-based programmatic control (use with openPopover/closePopover/togglePopover) */
|
|
103
103
|
id?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Where to append the popover (and its fallback backdrop/wrapper). Defaults
|
|
106
|
+
* to the current behavior: the closest open `<dialog>` ancestor, else
|
|
107
|
+
* `document.body`. Pass a bounded shell (framed app, embedded widget,
|
|
108
|
+
* dashboard pane) to keep the overlay inside it — required when that shell
|
|
109
|
+
* is a stacking context, since a body-level overlay can then only paint
|
|
110
|
+
* entirely above or entirely below it. A function returning `null` falls
|
|
111
|
+
* back to the default.
|
|
112
|
+
*/
|
|
113
|
+
container?: HTMLElement | (() => HTMLElement | null);
|
|
104
114
|
}
|
|
105
115
|
/**
|
|
106
116
|
* A Svelte action that displays a popover anchored to an element using CSS Anchor Positioning.
|
|
@@ -2,6 +2,8 @@ import { mount, unmount } from "svelte";
|
|
|
2
2
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
3
3
|
import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
|
|
4
4
|
import { clampIntoViewport } from "../../utils/anchor-position.js";
|
|
5
|
+
import { fixedContainingBlockRect } from "../../utils/containing-block.js";
|
|
6
|
+
import { resolveContainerOption } from "../../utils/overlay-container.js";
|
|
5
7
|
import { iconX } from "../../icons/index.js";
|
|
6
8
|
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
7
9
|
import PopoverContent from "./PopoverContent.svelte";
|
|
@@ -342,14 +344,19 @@ export function popover(anchorEl, fn) {
|
|
|
342
344
|
anchorEl.setAttribute("aria-expanded", "true");
|
|
343
345
|
const offsetValue = currentOptions.offset || "0.25rem";
|
|
344
346
|
const useAnchorPositioning = isSupported && !currentOptions.forceFallback;
|
|
345
|
-
// Get appropriate container (dialog if inside one,
|
|
346
|
-
// This ensures popover renders in same stacking
|
|
347
|
-
|
|
347
|
+
// Get appropriate container (explicit option, else dialog if inside one,
|
|
348
|
+
// otherwise body). This ensures popover renders in the same stacking
|
|
349
|
+
// context as modal dialogs / the consumer's bounded shell.
|
|
350
|
+
const container = resolveContainerOption(currentOptions.container) ?? getPopoverContainer(anchorEl);
|
|
348
351
|
if (useAnchorPositioning) {
|
|
349
352
|
// CSS Anchor Positioning mode
|
|
350
353
|
popoverEl = document.createElement("div");
|
|
351
354
|
popoverEl.setAttribute("id", id);
|
|
352
355
|
popoverEl.setAttribute("role", "dialog");
|
|
356
|
+
// NOTE: keep `vw`/`vh` here — this is the ANCHORED branch, where the
|
|
357
|
+
// element's containing block is the `position-area` region (a slice of
|
|
358
|
+
// the CB, often much smaller than it), so `%` would shrink the popover.
|
|
359
|
+
// Overflow is handled by @position-try + the CB-aware runtime check.
|
|
353
360
|
popoverEl.style.cssText = `
|
|
354
361
|
position: fixed;
|
|
355
362
|
position-anchor: ${anchorName};
|
|
@@ -390,10 +397,13 @@ export function popover(anchorEl, fn) {
|
|
|
390
397
|
popoverEl = document.createElement("div");
|
|
391
398
|
popoverEl.setAttribute("id", id);
|
|
392
399
|
popoverEl.setAttribute("role", "dialog");
|
|
400
|
+
// `90%` (not `90vw/90vh`): resolves against the inset-0 wrapper, which
|
|
401
|
+
// spans the containing block — identical to viewport units when the CB
|
|
402
|
+
// is the viewport, correct inside a contained/transformed shell.
|
|
393
403
|
popoverEl.style.cssText = `
|
|
394
404
|
position: relative;
|
|
395
|
-
max-width:
|
|
396
|
-
max-height:
|
|
405
|
+
max-width: 90%;
|
|
406
|
+
max-height: 90%;
|
|
397
407
|
overflow: auto;
|
|
398
408
|
transition-duration: ${TRANSITION}ms;
|
|
399
409
|
pointer-events: auto;
|
|
@@ -440,11 +450,13 @@ export function popover(anchorEl, fn) {
|
|
|
440
450
|
// sub-pixel/vertical cases); clamping keeps small edge-anchored
|
|
441
451
|
// popovers anchored instead of switching them to a modal.
|
|
442
452
|
clampIntoViewport(popoverEl);
|
|
453
|
+
// Compare against the containing block (the viewport, unless an
|
|
454
|
+
// ancestor with `transform`/`contain` establishes one).
|
|
443
455
|
const rect = popoverEl.getBoundingClientRect();
|
|
444
|
-
const
|
|
456
|
+
const cb = fixedContainingBlockRect(popoverEl);
|
|
445
457
|
// If it STILL overflows horizontally after clamping, the content
|
|
446
458
|
// is too wide to fit anchored — fall back to the centered modal.
|
|
447
|
-
if (rect.left <
|
|
459
|
+
if (rect.left < cb.left || rect.right > cb.right) {
|
|
448
460
|
debug("overflow detected, switching to fallback mode");
|
|
449
461
|
switchingToFallback = true;
|
|
450
462
|
// Quick cleanup (skip transition)
|
|
@@ -556,6 +568,7 @@ export function popover(anchorEl, fn) {
|
|
|
556
568
|
onHide: opts.onHide,
|
|
557
569
|
debug: opts.debug,
|
|
558
570
|
id: opts.id,
|
|
571
|
+
container: opts.container,
|
|
559
572
|
};
|
|
560
573
|
do_debug = !!opts.debug;
|
|
561
574
|
// Register in global registry if id provided
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { spotlight } from "./spotlight.svelte.js";
|
|
3
|
+
|
|
4
|
+
let { open = false, useContainer = false }: { open?: boolean; useContainer?: boolean } =
|
|
5
|
+
$props();
|
|
6
|
+
|
|
7
|
+
let shell = $state<HTMLDivElement>();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!-- A bounded shell that is a fixed containing block + stacking context. The
|
|
11
|
+
target sits at a known offset inside it so the clip-path hole coordinates can
|
|
12
|
+
be asserted container-locally. -->
|
|
13
|
+
<div
|
|
14
|
+
bind:this={shell}
|
|
15
|
+
data-testid="shell"
|
|
16
|
+
style="contain: layout paint; width: 240px; height: 180px; position: relative;"
|
|
17
|
+
>
|
|
18
|
+
<button
|
|
19
|
+
data-testid="target"
|
|
20
|
+
style="position: absolute; left: 40px; top: 30px; width: 60px; height: 20px;"
|
|
21
|
+
use:spotlight={() => ({
|
|
22
|
+
content: "hi",
|
|
23
|
+
open,
|
|
24
|
+
padding: 4,
|
|
25
|
+
borderRadius: 0,
|
|
26
|
+
scrollIntoView: false,
|
|
27
|
+
autoTrack: false,
|
|
28
|
+
container: useContainer ? () => shell ?? null : undefined,
|
|
29
|
+
})}
|
|
30
|
+
>
|
|
31
|
+
x
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
@@ -98,6 +98,15 @@ export interface SpotlightOptions {
|
|
|
98
98
|
autoTrack?: boolean;
|
|
99
99
|
/** Debug mode */
|
|
100
100
|
debug?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Where to append the overlay elements (backdrop, anchor, annotation).
|
|
103
|
+
* Defaults to the current behavior: `document.body`. Pass a bounded shell
|
|
104
|
+
* (framed app, embedded widget, dashboard pane) to keep the overlay inside
|
|
105
|
+
* it — required when that shell is a stacking context, since a body-level
|
|
106
|
+
* overlay can then only paint entirely above or entirely below it. A
|
|
107
|
+
* function returning `null` falls back to the default.
|
|
108
|
+
*/
|
|
109
|
+
container?: HTMLElement | (() => HTMLElement | null);
|
|
101
110
|
}
|
|
102
111
|
/**
|
|
103
112
|
* A Svelte action that highlights a target element with a spotlight effect.
|