@wippy-fe/theme 0.0.47 → 0.0.48
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/package.json +1 -1
- package/primevue-plugin.ts +299 -28
- package/tokens.json +1 -1
package/package.json
CHANGED
package/primevue-plugin.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { createCommentVNode, h, renderSlot, Teleport } from 'vue'
|
|
|
5
5
|
|
|
6
6
|
const LAYER_ATTR = 'data-wippy-overlay-layer'
|
|
7
7
|
|
|
8
|
+
/** A per-overlay layer div carrying its overlay's trigger anchor-name for the observer. */
|
|
9
|
+
interface LayerWithAnchor extends HTMLElement { __wippyAnchorName?: string }
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Client (browser) guard, equivalent to `@primeuix/utils`' `isClient` — inlined so
|
|
10
13
|
* this plugin (which ships as raw `.ts` that every consumer typechecks) takes no
|
|
@@ -15,6 +18,135 @@ function isClient(): boolean {
|
|
|
15
18
|
return typeof window !== 'undefined' && typeof window.document !== 'undefined'
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The browser Popover API (and thus the top layer) is available.
|
|
23
|
+
*
|
|
24
|
+
* We promote each overlay's own layer into the document's **top layer**
|
|
25
|
+
* so overlays escape every ancestor stacking context in that document — nested
|
|
26
|
+
* shadow hosts, sticky toolbars, a later sibling's positive `z-index` — for BOTH
|
|
27
|
+
* paint and hit-testing, with no `z-index` anywhere. The overlay stays inside its
|
|
28
|
+
* owning shadow root, so module-local CSS and inherited `--p-*` tokens are
|
|
29
|
+
* untouched (inheritance follows the flat tree, not the top-layer position). Widely
|
|
30
|
+
* available in modern engines (Chrome/Edge 114+, Firefox 125+, Safari 17+); older
|
|
31
|
+
* engines fall back to the pinned-layer-only behavior below (stacking imperfect,
|
|
32
|
+
* everything else unchanged). Evaluated lazily so SSR never touches `HTMLElement`.
|
|
33
|
+
*/
|
|
34
|
+
function supportsTopLayer(): boolean {
|
|
35
|
+
return (
|
|
36
|
+
isClient()
|
|
37
|
+
&& typeof HTMLElement !== 'undefined'
|
|
38
|
+
&& Object.hasOwn(HTMLElement.prototype, 'popover')
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CSS anchor positioning is available (S2). Implies the top layer. Lets a promoted
|
|
44
|
+
* overlay anchor to its trigger with pure CSS — no JS reposition loop — for the
|
|
45
|
+
* SELF-ANCHORED overlay families whose PrimeVue panel aligns to the component root,
|
|
46
|
+
* which is the Portal placeholder's DOM parent (`Select`, `MultiSelect`, `TreeSelect`,
|
|
47
|
+
* `CascadeSelect` — live-verified on `Select`; the others share the same align-to-root
|
|
48
|
+
* shape. `AutoComplete` is excluded — see {@link SELF_ANCHORED_OVERLAY}). Chrome/Edge
|
|
49
|
+
* 125+, Firefox 147+, Safari 26+. Anything else (older engine, or menus / popovers that
|
|
50
|
+
* align to an arbitrary caller-supplied target) keeps S1's pinned-layer positioning. S2 runs
|
|
51
|
+
* only behind this feature gate, and `applyAnchor` restores PrimeVue's own inline positioning
|
|
52
|
+
* verbatim (S1) if a promoted overlay can't enter the top layer — so the stacking fix always
|
|
53
|
+
* holds and positioning never regresses.
|
|
54
|
+
* (Tooltips never reach this plugin at all: PrimeVue's Tooltip is a directive that
|
|
55
|
+
* appends to `document.body`, outside `primevue/portal`.)
|
|
56
|
+
*/
|
|
57
|
+
function supportsAnchor(): boolean {
|
|
58
|
+
return (
|
|
59
|
+
supportsTopLayer()
|
|
60
|
+
&& typeof CSS !== 'undefined'
|
|
61
|
+
&& typeof CSS.supports === 'function'
|
|
62
|
+
&& CSS.supports('anchor-name: --a')
|
|
63
|
+
&& CSS.supports('top', 'anchor(--a bottom)')
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Overlay classes whose panel aligns to the COMPONENT ROOT — the Portal placeholder's
|
|
69
|
+
* DOM parent — so a unique `anchor-name` stamped on that root is the correct CSS anchor.
|
|
70
|
+
* `AutoComplete` is deliberately EXCLUDED: it aligns its panel to an inner element
|
|
71
|
+
* (`$refs.focusInput`/`multiContainer`), not the root, so anchoring to the root would
|
|
72
|
+
* offset it; it keeps S1 (PrimeVue's own positioning against the correct element).
|
|
73
|
+
*/
|
|
74
|
+
const SELF_ANCHORED_OVERLAY = /\bp-(?:select|multiselect|treeselect|cascadeselect)-overlay\b/
|
|
75
|
+
|
|
76
|
+
/** Sequential id for per-trigger `anchor-name`s — collision-free and readable (Math.random isn't). */
|
|
77
|
+
let anchorSeq = 0
|
|
78
|
+
|
|
79
|
+
/** Per-overlay observer that re-applies S2 styles when PrimeVue re-aligns the overlay. */
|
|
80
|
+
const anchorStyleObservers = new WeakMap<HTMLElement, MutationObserver>()
|
|
81
|
+
|
|
82
|
+
/** Write the S2 positioning declarations that make the overlay a fixed, anchored popover. */
|
|
83
|
+
function setAnchorStyles(overlay: HTMLElement, anchorName: string): void {
|
|
84
|
+
const s = overlay.style
|
|
85
|
+
// Own `position:fixed` top-layer popover: promotion needs a popover, and `fixed` gives
|
|
86
|
+
// the viewport containing block that `anchor()` and the top layer resolve against
|
|
87
|
+
// (instead of the pinned 0×0 layer). PrimeVue's theme border/background/padding still
|
|
88
|
+
// win over the UA `[popover]` box (author > UA); we override only positioning.
|
|
89
|
+
s.setProperty('position', 'fixed', 'important')
|
|
90
|
+
s.setProperty('inset', 'auto', 'important')
|
|
91
|
+
s.setProperty('margin', '0', 'important')
|
|
92
|
+
s.setProperty('position-anchor', anchorName)
|
|
93
|
+
s.setProperty('top', `anchor(${anchorName} bottom)`, 'important')
|
|
94
|
+
s.setProperty('left', `anchor(${anchorName} left)`, 'important')
|
|
95
|
+
s.setProperty('position-try-fallbacks', 'flip-block')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* S2: make a self-anchored overlay its own fixed, CSS-anchored top-layer popover (no-op
|
|
100
|
+
* for other overlays — they keep S1).
|
|
101
|
+
*
|
|
102
|
+
* PrimeVue keeps RE-aligning the overlay while it is open (`alignOverlay()` on every
|
|
103
|
+
* filter keystroke), plain-writing `top`/`margin` on the same
|
|
104
|
+
* inline `style`. Since an inline style has ONE slot per property, that plain write
|
|
105
|
+
* REPLACES our `!important` value+priority — so a one-shot write is not durable. We
|
|
106
|
+
* therefore also watch the overlay's own `style` and re-apply whenever PrimeVue clobbers
|
|
107
|
+
* it; the guard (`top` is no longer `!important`) makes the re-apply idempotent (never a
|
|
108
|
+
* loop). The observer is released when the overlay leaves its layer ({@link releaseAnchor}).
|
|
109
|
+
*/
|
|
110
|
+
function applyAnchor(overlay: HTMLElement, anchorName: string): void {
|
|
111
|
+
if (!SELF_ANCHORED_OVERLAY.test(overlay.className))
|
|
112
|
+
return
|
|
113
|
+
const restore = overlay.style.cssText
|
|
114
|
+
setAnchorStyles(overlay, anchorName)
|
|
115
|
+
if (overlay.getAttribute('popover') !== 'manual')
|
|
116
|
+
overlay.setAttribute('popover', 'manual')
|
|
117
|
+
try {
|
|
118
|
+
if (!overlay.matches(':popover-open'))
|
|
119
|
+
overlay.showPopover()
|
|
120
|
+
}
|
|
121
|
+
catch { /* not connected / already open */ }
|
|
122
|
+
if (!overlay.matches(':popover-open')) {
|
|
123
|
+
// Promotion didn't take — restore PrimeVue's ORIGINAL inline styles verbatim (rather
|
|
124
|
+
// than clearing the S2 properties piecemeal, which would also wipe the top/left/margin
|
|
125
|
+
// PrimeVue computed for the overlay), so it degrades to a correctly-positioned S1
|
|
126
|
+
// overlay inside the layer.
|
|
127
|
+
overlay.removeAttribute('popover')
|
|
128
|
+
overlay.style.cssText = restore
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (!anchorStyleObservers.has(overlay)) {
|
|
132
|
+
const obs = new MutationObserver(() => {
|
|
133
|
+
if (overlay.style.getPropertyPriority('top') !== 'important')
|
|
134
|
+
setAnchorStyles(overlay, anchorName)
|
|
135
|
+
})
|
|
136
|
+
obs.observe(overlay, { attributes: true, attributeFilter: ['style'] })
|
|
137
|
+
anchorStyleObservers.set(overlay, obs)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Detach an overlay's S2 style observer once it leaves its layer (closed) or unmounts. */
|
|
142
|
+
function releaseAnchor(overlay: HTMLElement): void {
|
|
143
|
+
const obs = anchorStyleObservers.get(overlay)
|
|
144
|
+
if (obs) {
|
|
145
|
+
obs.disconnect()
|
|
146
|
+
anchorStyleObservers.delete(overlay)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
18
150
|
/** The `ShadowRoot` a node lives in, or null (light DOM). */
|
|
19
151
|
function shadowRootOf(el: Node | null): ShadowRoot | null {
|
|
20
152
|
const root = el && typeof el.getRootNode === 'function' ? el.getRootNode() : null
|
|
@@ -40,7 +172,10 @@ function shadowRootOf(el: Node | null): ShadowRoot | null {
|
|
|
40
172
|
* origin; we then offset by it plus the scroll. This holds for any
|
|
41
173
|
* TRANSLATION-only containing block and any window scroll.
|
|
42
174
|
*
|
|
43
|
-
* Residuals (documented)
|
|
175
|
+
* Residuals (documented) — these apply ONLY to the degraded, non-top-layer fallback
|
|
176
|
+
* (older engines, {@link supportsTopLayer} false). Once the layer is promoted (the
|
|
177
|
+
* primary path), its containing block is unconditionally the viewport, so the measured
|
|
178
|
+
* rect at `top:0;left:0` is (0,0) and none of these arise:
|
|
44
179
|
* - SCALE / ROTATE / SKEW containing block: compensating via CSS `top`/`left`
|
|
45
180
|
* only cancels the containing block's translation, not a non-identity linear
|
|
46
181
|
* transform, so a `scale()`/`rotate()`/`skew()` ancestor mispositions the
|
|
@@ -67,10 +202,11 @@ function syncLayer(layer: HTMLElement): void {
|
|
|
67
202
|
layer.style.left = `${-window.scrollX - rect.left}px`
|
|
68
203
|
}
|
|
69
204
|
|
|
70
|
-
// A SINGLE viewport-driven resync loop shared by every
|
|
71
|
-
// listener pair per layer.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
205
|
+
// A SINGLE viewport-driven resync loop shared by every per-overlay layer — not a
|
|
206
|
+
// listener pair per layer. Layers are removed by `teardownOverlayLayer` (on unmount or a
|
|
207
|
+
// switch to inline); this loop is the backstop that also prunes any layer whose shadow
|
|
208
|
+
// root detached out from under it (e.g. a `view.component` torn down), so a cycle leaks
|
|
209
|
+
// no window listener, observer, or detached subtree.
|
|
74
210
|
const activeLayers = new Set<HTMLElement>()
|
|
75
211
|
const layerObservers = new WeakMap<HTMLElement, MutationObserver>()
|
|
76
212
|
let viewportListenersAttached = false
|
|
@@ -80,6 +216,12 @@ function syncActiveLayers(): void {
|
|
|
80
216
|
viewportSyncScheduled = false
|
|
81
217
|
for (const layer of activeLayers) {
|
|
82
218
|
if (!layer.isConnected) {
|
|
219
|
+
// Backstop for a layer whose shadow root detached without the Portal's `unmounted`
|
|
220
|
+
// running: release any overlay's S2 observer too (symmetry with `unmounted`).
|
|
221
|
+
for (const child of Array.from(layer.children)) {
|
|
222
|
+
if (child instanceof HTMLElement)
|
|
223
|
+
releaseAnchor(child)
|
|
224
|
+
}
|
|
83
225
|
activeLayers.delete(layer)
|
|
84
226
|
layerObservers.get(layer)?.disconnect()
|
|
85
227
|
continue
|
|
@@ -97,19 +239,60 @@ function onViewportChange(): void {
|
|
|
97
239
|
}
|
|
98
240
|
|
|
99
241
|
/**
|
|
100
|
-
*
|
|
101
|
-
* kept in sync as
|
|
102
|
-
* scrolls/resizes (the one shared listener pair over
|
|
242
|
+
* Create a fresh per-OVERLAY layer (one per Portal instance, NOT shared per shadow root),
|
|
243
|
+
* pinned to viewport origin and kept in sync as its overlay opens (a per-layer
|
|
244
|
+
* `MutationObserver`) and as the page scrolls/resizes (the one shared listener pair over
|
|
245
|
+
* {@link activeLayers}). One overlay per layer keeps S2 anchor routing unambiguous even
|
|
246
|
+
* for components that open several overlays at once; {@link teardownOverlayLayer} removes
|
|
247
|
+
* it on unmount or a switch to inline.
|
|
103
248
|
*/
|
|
104
|
-
function
|
|
105
|
-
const existing = root.querySelector<HTMLElement>(`[${LAYER_ATTR}]`)
|
|
106
|
-
if (existing)
|
|
107
|
-
return existing
|
|
108
|
-
|
|
249
|
+
function createLayer(root: ShadowRoot): HTMLElement {
|
|
109
250
|
const layer = document.createElement('div')
|
|
110
251
|
layer.setAttribute(LAYER_ATTR, '')
|
|
111
|
-
|
|
252
|
+
const topLayer = supportsTopLayer()
|
|
253
|
+
// Inline reset of the UA `[popover]` styling — scoped to this one private element,
|
|
254
|
+
// NEVER a global rule that could leak across modules. A shown popover gets the UA
|
|
255
|
+
// box (position:fixed; inset:0; margin:auto; width/height:fit-content; border:solid;
|
|
256
|
+
// padding; background:Canvas; color:CanvasText). Left unreset, our empty 0×0 layer
|
|
257
|
+
// paints as a small white BORDERED box centered in the viewport (margin:auto + inset:0),
|
|
258
|
+
// and recolors the teleported overlay's text through inheritance. We zero every UA-set
|
|
259
|
+
// box property (including margin, so our syncLayer-owned top/left position it at origin);
|
|
260
|
+
// `color:inherit` restores the shadow-host inheritance chain for the teleported
|
|
261
|
+
// overlay; `overflow:visible` lets the absolutely-positioned overlay paint outside
|
|
262
|
+
// the 0×0 box. `!important` guards against a non-compliant theme's `[popover]` rule.
|
|
263
|
+
// `top`/`left` are deliberately OMITTED and left author-normal — syncLayer owns them,
|
|
264
|
+
// and its plain setProperty must keep winning (so we also never set `inset`, whose
|
|
265
|
+
// longhands would otherwise fight syncLayer's top/left).
|
|
266
|
+
layer.style.cssText = topLayer
|
|
267
|
+
? 'position:fixed;width:0 !important;height:0 !important;margin:0 !important;border:0 !important;padding:0 !important;background:transparent !important;color:inherit !important;overflow:visible !important;'
|
|
268
|
+
: 'position:fixed;top:0;left:0;width:0;height:0;'
|
|
269
|
+
if (topLayer)
|
|
270
|
+
layer.setAttribute('popover', 'manual')
|
|
112
271
|
root.appendChild(layer)
|
|
272
|
+
if (topLayer) {
|
|
273
|
+
// Enter the top layer. `manual` never light-dismisses, so PrimeVue stays the sole
|
|
274
|
+
// controller of open/close, Escape, and outside-click; focus stays untouched too, since
|
|
275
|
+
// neither the bare layer div nor PrimeVue's overlay markup carries an `autofocus` target
|
|
276
|
+
// for the popover focusing steps to act on (those run regardless of popover type).
|
|
277
|
+
// `teardownOverlayLayer` removes the layer (on unmount or a switch to inline),
|
|
278
|
+
// evicting it from the top layer; syncActiveLayers is the backstop if the shadow root
|
|
279
|
+
// detaches first.
|
|
280
|
+
try {
|
|
281
|
+
layer.showPopover()
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// showPopover only throws if disconnected/already-open; the degrade below covers
|
|
285
|
+
// a layer that ended up not promoted regardless of throw-vs-silent-noop.
|
|
286
|
+
}
|
|
287
|
+
if (!layer.matches(':popover-open')) {
|
|
288
|
+
// Promotion did not take. NEVER leave `popover` set on a not-open element: the
|
|
289
|
+
// UA rule `[popover]:not(:popover-open){display:none}` would hide the layer AND
|
|
290
|
+
// every overlay teleported into it — silently worse than the original bug.
|
|
291
|
+
// Degrade cleanly to the pinned-layer-only behavior (syncLayer still positions).
|
|
292
|
+
layer.removeAttribute('popover')
|
|
293
|
+
layer.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;'
|
|
294
|
+
}
|
|
295
|
+
}
|
|
113
296
|
syncLayer(layer)
|
|
114
297
|
|
|
115
298
|
// Re-pin whenever an overlay is inserted — it may open after the page scrolled or
|
|
@@ -117,7 +300,23 @@ function ensureLayer(root: ShadowRoot): HTMLElement {
|
|
|
117
300
|
// the layer's own style, so it never re-triggers this childList observer. The
|
|
118
301
|
// observer is referenced only via the WeakMap, so it is disconnected when the layer
|
|
119
302
|
// detaches (in syncActiveLayers) and otherwise GC'd with the layer.
|
|
120
|
-
const observer = new MutationObserver(() => {
|
|
303
|
+
const observer = new MutationObserver((records) => {
|
|
304
|
+
// S2: this layer serves exactly ONE overlay (one Portal instance), so its
|
|
305
|
+
// `__wippyAnchorName` — the trigger anchor-name stamped by `setupOverlayLayer` — is unambiguous.
|
|
306
|
+
// Anchor the overlay when it teleports in; release its style observer when it leaves.
|
|
307
|
+
const anchorName = (layer as LayerWithAnchor).__wippyAnchorName
|
|
308
|
+
for (const rec of records) {
|
|
309
|
+
if (anchorName) {
|
|
310
|
+
for (const node of Array.from(rec.addedNodes)) {
|
|
311
|
+
if (node instanceof HTMLElement)
|
|
312
|
+
applyAnchor(node, anchorName)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const node of Array.from(rec.removedNodes)) {
|
|
316
|
+
if (node instanceof HTMLElement)
|
|
317
|
+
releaseAnchor(node)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
121
320
|
if (layer.isConnected && layer.childElementCount > 0)
|
|
122
321
|
syncLayer(layer)
|
|
123
322
|
})
|
|
@@ -143,6 +342,7 @@ interface PortalOptions {
|
|
|
143
342
|
computed?: Record<string, (this: PortalInstance) => unknown>
|
|
144
343
|
data?: (this: PortalInstance) => Record<string, unknown>
|
|
145
344
|
mounted?: (this: PortalInstance) => void
|
|
345
|
+
unmounted?: (this: PortalInstance) => void
|
|
146
346
|
render?: (this: PortalInstance) => VNode
|
|
147
347
|
}
|
|
148
348
|
|
|
@@ -157,7 +357,54 @@ interface PortalInstance {
|
|
|
157
357
|
mounted: boolean
|
|
158
358
|
/** The shadow root this overlay lives in (captured once on mount), or null. */
|
|
159
359
|
hostRoot: ShadowRoot | null
|
|
360
|
+
/** This overlay's OWN layer div (one per Portal instance); null in light DOM or while inline. */
|
|
361
|
+
layer: HTMLElement | null
|
|
362
|
+
/** Unique `anchor-name` wired onto this overlay's trigger for S2, or null. */
|
|
363
|
+
anchorName: string | null
|
|
364
|
+
/** The trigger element the anchor-name was stamped on (to clear it on teardown). */
|
|
365
|
+
anchorTrigger: HTMLElement | null
|
|
160
366
|
resolvedTarget: Element | string | null
|
|
367
|
+
/** Vue instance watcher — used to react to `inline` toggling after mount. */
|
|
368
|
+
$watch: (source: string, cb: (value: boolean) => void) => () => void
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Create this overlay's layer and wire its S2 anchor. Idempotent per instance. */
|
|
372
|
+
function setupOverlayLayer(inst: PortalInstance): void {
|
|
373
|
+
if (!inst.hostRoot || inst.layer)
|
|
374
|
+
return
|
|
375
|
+
// Eager: the layer is promoted to the top layer BEFORE the overlay opens, so PrimeVue
|
|
376
|
+
// never measures a not-yet-promoted box.
|
|
377
|
+
inst.layer = createLayer(inst.hostRoot)
|
|
378
|
+
// S2: stamp a unique `anchor-name` on the trigger (the placeholder's parent is the
|
|
379
|
+
// component root the self-anchored families align to) and hand it to this layer.
|
|
380
|
+
if (supportsAnchor()) {
|
|
381
|
+
const trigger = inst.$el ? (inst.$el as Node).parentElement : null
|
|
382
|
+
if (trigger) {
|
|
383
|
+
inst.anchorName = `--wippy-a-${++anchorSeq}`
|
|
384
|
+
inst.anchorTrigger = trigger
|
|
385
|
+
trigger.style.setProperty('anchor-name', inst.anchorName)
|
|
386
|
+
;(inst.layer as LayerWithAnchor).__wippyAnchorName = inst.anchorName
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Remove this overlay's layer and undo its S2 wiring (on unmount or a switch to inline). */
|
|
392
|
+
function teardownOverlayLayer(inst: PortalInstance): void {
|
|
393
|
+
if (inst.anchorTrigger) {
|
|
394
|
+
inst.anchorTrigger.style.removeProperty('anchor-name')
|
|
395
|
+
inst.anchorTrigger = null
|
|
396
|
+
}
|
|
397
|
+
inst.anchorName = null
|
|
398
|
+
if (inst.layer) {
|
|
399
|
+
for (const child of Array.from(inst.layer.children)) {
|
|
400
|
+
if (child instanceof HTMLElement)
|
|
401
|
+
releaseAnchor(child)
|
|
402
|
+
}
|
|
403
|
+
activeLayers.delete(inst.layer)
|
|
404
|
+
layerObservers.get(inst.layer)?.disconnect()
|
|
405
|
+
inst.layer.remove()
|
|
406
|
+
inst.layer = null
|
|
407
|
+
}
|
|
161
408
|
}
|
|
162
409
|
|
|
163
410
|
let patched = false
|
|
@@ -169,8 +416,8 @@ let patched = false
|
|
|
169
416
|
* PrimeVue 4 has no global `appendTo` config, and the `@primeuix/utils` positioning
|
|
170
417
|
* helpers are immutable ESM bindings. But every overlay teleports through the ONE
|
|
171
418
|
* shared `primevue/portal`, whose options object is a mutable singleton. We rewrite it
|
|
172
|
-
* so the DEFAULT `appendTo` (`'body'`, or null/undefined) teleports to a per-
|
|
173
|
-
* layer ({@link
|
|
419
|
+
* so the DEFAULT `appendTo` (`'body'`, or null/undefined) teleports to a per-overlay
|
|
420
|
+
* layer ({@link createLayer}) that is measured-and-pinned to true viewport origin
|
|
174
421
|
* ({@link syncLayer}) instead of `document.body`.
|
|
175
422
|
*
|
|
176
423
|
* Because `appendTo` is then not `'self'`, PrimeVue positions with `absolutePosition`
|
|
@@ -194,44 +441,68 @@ export function patchOverlayPortal(): void {
|
|
|
194
441
|
portal.computed.inline = function (this: PortalInstance) {
|
|
195
442
|
return Boolean(this.disabled) || this.appendTo === 'self'
|
|
196
443
|
}
|
|
197
|
-
// Reactive to `appendTo`: the default ('body'/null) resolves to
|
|
444
|
+
// Reactive to `appendTo`: the default ('body'/null) resolves to this overlay's own
|
|
198
445
|
// layer (or document.body in light DOM); an explicit target passes through.
|
|
199
446
|
portal.computed.resolvedTarget = function (this: PortalInstance) {
|
|
200
447
|
if (this.appendTo == null || this.appendTo === 'body')
|
|
201
|
-
return this.hostRoot ?
|
|
448
|
+
return this.hostRoot ? this.layer : document.body
|
|
202
449
|
return this.appendTo as Element | string
|
|
203
450
|
}
|
|
204
451
|
|
|
205
452
|
portal.data = function (this: PortalInstance) {
|
|
206
|
-
return { mounted: false, hostRoot: null }
|
|
453
|
+
return { mounted: false, hostRoot: null, layer: null, anchorName: null, anchorTrigger: null }
|
|
207
454
|
}
|
|
208
455
|
|
|
209
456
|
portal.mounted = function (this: PortalInstance) {
|
|
210
457
|
// Match upstream Portal: teleport is client-only. Guards DOM access below
|
|
211
|
-
// (
|
|
458
|
+
// (createLayer → createElement) if this ever runs without a document.
|
|
212
459
|
if (!isClient())
|
|
213
460
|
return
|
|
214
461
|
// `$el` here is the placeholder comment node — capture its shadow root once.
|
|
215
462
|
this.hostRoot = shadowRootOf(this.$el)
|
|
216
463
|
this.mounted = true
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if
|
|
221
|
-
|
|
464
|
+
// Light DOM (no shadow root) always teleports to document.body — no layer, ever.
|
|
465
|
+
if (!this.hostRoot)
|
|
466
|
+
return
|
|
467
|
+
// Create the layer now if this overlay teleports; skip it while inline
|
|
468
|
+
// (`appendTo:'self'`/`disabled`) so nothing is allocated unused.
|
|
469
|
+
if (!this.inline)
|
|
470
|
+
setupOverlayLayer(this)
|
|
471
|
+
// `inline` is a computed over reactive props, and consumers DO flip `appendTo`
|
|
472
|
+
// ('body'↔'self') after mount (e.g. `use-shadow-append-to`). Mirror the layer
|
|
473
|
+
// lifecycle on that toggle: a switch to inline tears down the now-unused layer
|
|
474
|
+
// instead of orphaning it; a switch back re-creates it (else `resolvedTarget` would
|
|
475
|
+
// fall back to document.body, breaking out of the shadow root). Auto-cleaned on unmount.
|
|
476
|
+
this.$watch('inline', (nowInline) => {
|
|
477
|
+
if (nowInline)
|
|
478
|
+
teardownOverlayLayer(this)
|
|
479
|
+
else
|
|
480
|
+
setupOverlayLayer(this)
|
|
481
|
+
})
|
|
222
482
|
}
|
|
223
483
|
|
|
224
484
|
portal.render = function (this: PortalInstance) {
|
|
225
|
-
//
|
|
226
|
-
//
|
|
485
|
+
// The inline / teleport / not-yet-mounted branches return different vnode TYPES
|
|
486
|
+
// (slot fragment vs Teleport vs comment), so Vue replaces (never patches) across them
|
|
487
|
+
// if `inline` toggles reactively; the `key`s mirror upstream Portal's for parity.
|
|
227
488
|
if (this.inline)
|
|
228
489
|
return renderSlot(this.$slots as never, 'default', { key: 0 }) as unknown as VNode
|
|
229
490
|
if (!this.mounted)
|
|
230
491
|
return createCommentVNode('', true)
|
|
492
|
+
// Teleport the overlay AS-IS, a direct child of this overlay's own layer. Its
|
|
493
|
+
// anchor-name rides on the layer (set by `setupOverlayLayer`), so the observer anchors
|
|
494
|
+
// it — no per-render vnode surgery, so PrimeVue's overlay markup is untouched.
|
|
231
495
|
return h(Teleport, { key: 1, to: this.resolvedTarget || document.body } as never, [
|
|
232
496
|
renderSlot(this.$slots as never, 'default'),
|
|
233
497
|
])
|
|
234
498
|
}
|
|
499
|
+
|
|
500
|
+
portal.unmounted = function (this: PortalInstance) {
|
|
501
|
+
// Each overlay OWNS its layer, so it removes it (and undoes its S2 wiring) on unmount
|
|
502
|
+
// — no orphaned 0×0 popovers, observers, listeners, or stale `anchor-name` across
|
|
503
|
+
// mount/unmount cycles. (The old shared per-root layer detached with the shadow root.)
|
|
504
|
+
teardownOverlayLayer(this)
|
|
505
|
+
}
|
|
235
506
|
}
|
|
236
507
|
|
|
237
508
|
export const PrimeVuePlugin = {
|
package/tokens.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"generatedFrom": "theme-config.css",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.48",
|
|
4
4
|
"grammar": "every color family = --p-<fam> + --p-<fam>-{50..950} + --p-<fam>-{color,contrast-color,hover-color,active-color}; --p-surface-<shade>; text/content/highlight semantic aliases; --p-font-<role>-<prop>; explicitly enumerated runtime appearance tokens",
|
|
5
5
|
"families": [
|
|
6
6
|
"primary",
|