Skip to content

Commit 8dda5e9

Browse files
committed
feat(scrim): render per-ticket for cumulative opacity stacking
- Remove stack prop, require createStackPlugin to be installed - Render one scrim element per active overlay using selectedItems - Each layer positioned at (ticket.zIndex - 1) for natural stacking - Update slot props to include ticket, zIndex, isBlocking, dismiss - Use TransitionGroup for proper per-layer animations - Update docs example to use new Scrim component
1 parent af219fa commit 8dda5e9

File tree

3 files changed

+226
-255
lines changed

3 files changed

+226
-255
lines changed

apps/docs/src/examples/composables/use-stack/StackProvider.vue

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Note: For SSR applications, install the plugin in main.ts:
33
// import { createStackPlugin } from '@vuetify/v0'
44
// app.use(createStackPlugin())
5-
import { useStack } from '@vuetify/v0'
5+
import { Scrim, useStack } from '@vuetify/v0'
66
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
77
import { provideOverlays } from './context'
88
import type { Overlay } from './context'
@@ -45,14 +45,6 @@
4545
}
4646
}
4747
48-
// Dismiss topmost non-blocking overlay (for scrim click)
49-
function dismissTop () {
50-
const top = stack.top.value
51-
if (top && !stack.isBlocking.value) {
52-
top.dismiss()
53-
}
54-
}
55-
5648
provideOverlays({
5749
overlays,
5850
stack,
@@ -67,17 +59,8 @@
6759
<div class="relative">
6860
<slot />
6961

70-
<!-- Scrim (shared backdrop for all overlays) -->
71-
<Teleport to="body">
72-
<Transition name="fade">
73-
<div
74-
v-if="stack.isActive.value"
75-
class="fixed inset-0 bg-black/50 transition-opacity"
76-
:style="{ zIndex: stack.scrimZIndex.value }"
77-
@click="dismissTop"
78-
/>
79-
</Transition>
80-
</Teleport>
62+
<!-- Scrim renders one backdrop per overlay for cumulative opacity -->
63+
<Scrim class="fixed inset-0 bg-black/30 transition-opacity" />
8164
</div>
8265
</template>
8366

packages/0/src/components/Scrim/Scrim.vue

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,21 @@
22
* @module Scrim
33
*
44
* @remarks
5-
* Scrim/backdrop component for overlays. Integrates with createStack
6-
* to provide a shared backdrop for all active overlays. Automatically
7-
* positions itself behind the topmost overlay and handles dismiss behavior.
5+
* Scrim/backdrop component for overlays. Integrates with useStack
6+
* to render one backdrop per active overlay. The cumulative visual
7+
* effect creates natural opacity stacking.
8+
*
9+
* Requires createStackPlugin to be installed at app level.
810
*
911
* @see https://0.vuetifyjs.com/components/providers/scrim
1012
*/
1113

1214
<script lang="ts">
1315
// Types
1416
import type { AtomProps } from '#v0/components/Atom'
15-
import type { StackContext } from '#v0/composables/useStack'
17+
import type { StackTicket } from '#v0/composables/useStack'
1618
1719
export interface ScrimProps extends AtomProps {
18-
/**
19-
* Custom stack context to use instead of the global stack
20-
*
21-
* @remarks Useful when multiple independent overlay systems exist in an app.
22-
*/
23-
stack?: StackContext
2420
/**
2521
* Transition name for enter/leave animations
2622
*
@@ -42,13 +38,13 @@
4238
}
4339
4440
export interface ScrimSlotProps {
45-
/** Whether any overlays are active */
46-
isActive: boolean
47-
/** Whether the topmost overlay is blocking */
48-
isBlocking: boolean
49-
/** Z-index for the scrim (one below top overlay) */
41+
/** The ticket for this scrim layer */
42+
ticket: StackTicket
43+
/** Z-index for this scrim layer (one below the overlay) */
5044
zIndex: number
51-
/** Dismiss the topmost non-blocking overlay */
45+
/** Whether this ticket's overlay blocks scrim dismissal */
46+
isBlocking: boolean
47+
/** Dismiss this overlay */
5248
dismiss: () => void
5349
}
5450
</script>
@@ -61,7 +57,7 @@
6157
import { useStack } from '#v0/composables/useStack'
6258
6359
// Utilities
64-
import { toRef, useAttrs } from 'vue'
60+
import { computed, useAttrs } from 'vue'
6561
6662
defineOptions({ name: 'Scrim', inheritAttrs: false })
6763
@@ -71,63 +67,68 @@
7167
7268
const {
7369
as = 'div',
74-
stack = useStack(),
7570
transition = 'fade',
7671
teleport = true,
7772
teleportTo = 'body',
7873
} = defineProps<ScrimProps>()
7974
8075
const attrs = useAttrs()
76+
const stack = useStack()
77+
78+
const tickets = computed(() => Array.from(stack.selectedItems.value))
8179
82-
function onClick () {
83-
const top = stack.top.value
84-
if (top && !stack.isBlocking.value) {
85-
top.dismiss()
80+
function onDismiss (ticket: StackTicket) {
81+
if (!ticket.blocking) {
82+
ticket.dismiss()
8683
}
8784
}
8885
89-
const slotProps = toRef((): ScrimSlotProps => ({
90-
isActive: stack.isActive.value,
91-
isBlocking: stack.isBlocking.value,
92-
zIndex: stack.scrimZIndex.value,
93-
dismiss: onClick,
94-
}))
86+
function getSlotProps (ticket: StackTicket): ScrimSlotProps {
87+
return {
88+
ticket,
89+
zIndex: ticket.zIndex.value - 1,
90+
isBlocking: ticket.blocking,
91+
dismiss: () => onDismiss(ticket),
92+
}
93+
}
9594
96-
const style = toRef(() => ({
97-
zIndex: stack.scrimZIndex.value,
98-
}))
95+
function getStyle (ticket: StackTicket) {
96+
return { zIndex: ticket.zIndex.value - 1 }
97+
}
9998
</script>
10099

101100
<template>
102101
<Teleport
103102
v-if="teleport"
104103
:to="teleportTo"
105104
>
106-
<Transition :name="transition">
105+
<TransitionGroup :name="transition">
107106
<Atom
108-
v-if="stack.isActive.value"
107+
v-for="ticket in tickets"
108+
:key="ticket.id"
109109
:as
110-
:style
110+
:style="getStyle(ticket)"
111111
v-bind="attrs"
112-
@click="onClick"
112+
@click="() => onDismiss(ticket)"
113113
>
114-
<slot v-bind="slotProps" />
114+
<slot v-bind="getSlotProps(ticket)" />
115115
</Atom>
116-
</Transition>
116+
</TransitionGroup>
117117
</Teleport>
118118

119-
<Transition
119+
<TransitionGroup
120120
v-else
121121
:name="transition"
122122
>
123123
<Atom
124-
v-if="stack.isActive.value"
124+
v-for="ticket in tickets"
125+
:key="ticket.id"
125126
:as
126-
:style
127+
:style="getStyle(ticket)"
127128
v-bind="attrs"
128-
@click="onClick"
129+
@click="() => onDismiss(ticket)"
129130
>
130-
<slot v-bind="slotProps" />
131+
<slot v-bind="getSlotProps(ticket)" />
131132
</Atom>
132-
</Transition>
133+
</TransitionGroup>
133134
</template>

0 commit comments

Comments
 (0)