Skip to content

Commit af219fa

Browse files
committed
refactor(use-stack): align with v0 composable patterns
- Replace exported singleton with lazy internal fallback - Add auto-cleanup for tickets registered in component setup - Move ids() helper outside register() to reduce closure overhead - Fix Scrim to call stack.top.value?.dismiss() instead of stack.dismiss() - Update Scrim tests with proper mock structure - Update docs examples to use correct stack.register() pattern
1 parent 4db6583 commit af219fa

File tree

6 files changed

+685
-1167
lines changed

6 files changed

+685
-1167
lines changed

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

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { useHotkey, useStack } from '@vuetify/v0'
3-
import { computed } from 'vue'
3+
import { computed, watch } from 'vue'
44
import { useOverlays } from './context'
55
66
const { overlays, activeCount, open, close, closeAll } = useOverlays()
@@ -12,16 +12,31 @@
1212
{ accent: 'text-amber-500', bg: 'bg-amber-500', badge: 'bg-amber-500/10 text-amber-500' },
1313
]
1414
15-
// Create stack entries for each overlay
16-
const stacks = overlays.map((overlay, index) => ({
17-
overlay,
18-
color: colors[index % colors.length]!,
19-
...useStack(
20-
overlay.isOpen,
21-
() => close(overlay.id),
22-
{ blocking: overlay.blocking },
23-
),
24-
}))
15+
const stack = useStack()
16+
17+
// Register each overlay with the stack and sync selection with isOpen
18+
const tickets = overlays.map((overlay, index) => {
19+
const ticket = stack.register({
20+
id: overlay.id,
21+
onDismiss: () => close(overlay.id),
22+
blocking: overlay.blocking,
23+
})
24+
25+
// Sync selection state with isOpen
26+
watch(overlay.isOpen, isOpen => {
27+
if (isOpen) {
28+
ticket.select()
29+
} else {
30+
ticket.unselect()
31+
}
32+
}, { immediate: true })
33+
34+
return {
35+
overlay,
36+
ticket,
37+
color: colors[index % colors.length]!,
38+
}
39+
})
2540
2641
// Find next closed overlay to open from within an overlay
2742
const nextClosed = computed(() =>
@@ -30,9 +45,9 @@
3045
3146
// Escape key dismisses the topmost non-blocking overlay
3247
useHotkey('Escape', () => {
33-
const topStack = stacks.find(s => s.globalTop.value)
34-
if (topStack && !topStack.overlay.blocking) {
35-
close(topStack.overlay.id)
48+
const topTicket = tickets.find(t => t.ticket.globalTop.value)
49+
if (topTicket && !topTicket.overlay.blocking) {
50+
close(topTicket.overlay.id)
3651
}
3752
})
3853
</script>
@@ -43,17 +58,17 @@
4358
<div class="flex flex-col gap-4">
4459
<div class="grid grid-cols-3 gap-3 max-w-md mx-auto">
4560
<button
46-
v-for="(stack, index) in stacks"
47-
:key="stack.overlay.id"
61+
v-for="({ overlay }, index) in tickets"
62+
:key="overlay.id"
4863
class="px-4 py-2 text-sm font-medium rounded-md transition-colors text-center"
4964
:class="[
50-
stack.overlay.isOpen.value
65+
overlay.isOpen.value
5166
? `${colors[index]!.bg} text-white border border-transparent`
5267
: 'border border-divider hover:bg-surface-tint'
5368
]"
54-
@click="stack.overlay.isOpen.value ? close(stack.overlay.id) : open(stack.overlay.id)"
69+
@click="overlay.isOpen.value ? close(overlay.id) : open(overlay.id)"
5570
>
56-
{{ stack.overlay.title }}
71+
{{ overlay.title }}
5772
</button>
5873
</div>
5974

@@ -62,13 +77,13 @@
6277
<span class="text-on-surface-variant">Stack:</span>
6378
<div class="flex items-center gap-1">
6479
<template v-if="activeCount > 0">
65-
<template v-for="(stack, index) in stacks" :key="stack.overlay.id">
80+
<template v-for="({ overlay, ticket }, index) in tickets" :key="overlay.id">
6681
<span
67-
v-if="stack.overlay.isOpen.value"
82+
v-if="overlay.isOpen.value"
6883
class="px-2 py-0.5 rounded text-xs font-mono"
6984
:class="colors[index]!.badge"
7085
>
71-
{{ stack.zIndex.value }}
86+
{{ ticket.zIndex.value }}
7287
</span>
7388
</template>
7489
</template>
@@ -80,38 +95,38 @@
8095
<!-- Overlays -->
8196
<Teleport to="body">
8297
<TransitionGroup name="modal">
83-
<template v-for="({ overlay, color, styles, globalTop, zIndex, id }, index) in stacks" :key="overlay.id">
98+
<template v-for="({ overlay, ticket, color }, index) in tickets" :key="overlay.id">
8499
<div
85100
v-if="overlay.isOpen.value"
86-
:aria-describedby="`${id}-desc`"
87-
:aria-labelledby="`${id}-title`"
101+
:aria-describedby="`${ticket.id}-desc`"
102+
:aria-labelledby="`${ticket.id}-title`"
88103
aria-modal="true"
89104
class="fixed inset-0 flex items-center justify-center pointer-events-none p-4"
90105
role="dialog"
91106
:style="{
92-
...styles.value,
107+
zIndex: ticket.zIndex.value,
93108
transform: `translate(${index * 16}px, ${index * 16}px)`,
94109
}"
95110
>
96111
<div
97112
class="m-auto rounded-xl bg-surface border border-divider max-w-md w-full pointer-events-auto transition-all duration-200"
98-
:class="globalTop ? 'shadow-xl' : 'shadow-lg opacity-95'"
113+
:class="ticket.globalTop.value ? 'shadow-xl' : 'shadow-lg opacity-95'"
99114
>
100115
<!-- Header -->
101116
<div class="px-4 py-3 border-b border-divider">
102117
<div class="flex items-center justify-between">
103-
<h3 :id="`${id}-title`" class="text-lg font-semibold text-on-surface">
118+
<h3 :id="`${ticket.id}-title`" class="text-lg font-semibold text-on-surface">
104119
{{ overlay.title }}
105120
</h3>
106121
<div class="flex items-center gap-2">
107122
<span class="px-2 py-0.5 rounded text-xs font-mono" :class="color.badge">
108-
z:{{ zIndex.value }}
123+
z:{{ ticket.zIndex.value }}
109124
</span>
110125
<span
111126
class="px-2 py-0.5 rounded text-xs"
112-
:class="globalTop ? 'bg-success/10 text-success' : 'bg-surface-variant text-on-surface-variant'"
127+
:class="ticket.globalTop.value ? 'bg-success/10 text-success' : 'bg-surface-variant text-on-surface-variant'"
113128
>
114-
{{ globalTop ? 'top' : 'behind' }}
129+
{{ ticket.globalTop.value ? 'top' : 'behind' }}
115130
</span>
116131
</div>
117132
</div>
@@ -122,7 +137,7 @@
122137

123138
<!-- Content -->
124139
<div class="p-4 space-y-4">
125-
<p :id="`${id}-desc`" class="text-sm text-on-surface leading-relaxed">
140+
<p :id="`${ticket.id}-desc`" class="text-sm text-on-surface leading-relaxed">
126141
This overlay demonstrates z-index stacking. Open multiple overlays
127142
to see how they layer. The topmost overlay receives focus and
128143
handles the escape key.

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Note: For SSR applications, install the plugin in main.ts:
33
// import { createStackPlugin } from '@vuetify/v0'
44
// app.use(createStackPlugin())
5-
import { stack } from '@vuetify/v0'
5+
import { useStack } from '@vuetify/v0'
66
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
77
import { provideOverlays } from './context'
88
import type { Overlay } from './context'
99
10+
const stack = useStack()
11+
1012
// Block body scroll when overlays are active
1113
watch(() => stack.isActive.value, active => {
1214
document.body.style.overflow = active ? 'hidden' : ''
@@ -43,9 +45,17 @@
4345
}
4446
}
4547
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+
4656
provideOverlays({
4757
overlays,
48-
stack: stack,
58+
stack,
4959
activeCount,
5060
open,
5161
close,
@@ -64,7 +74,7 @@
6474
v-if="stack.isActive.value"
6575
class="fixed inset-0 bg-black/50 transition-opacity"
6676
:style="{ zIndex: stack.scrimZIndex.value }"
67-
@click="stack.dismiss()"
77+
@click="dismissTop"
6878
/>
6979
</Transition>
7080
</Teleport>

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
import { Atom } from '#v0/components/Atom'
5959
6060
// Composables
61-
import { stack as globalStack } from '#v0/composables/useStack'
61+
import { useStack } from '#v0/composables/useStack'
6262
6363
// Utilities
6464
import { toRef, useAttrs } from 'vue'
@@ -71,7 +71,7 @@
7171
7272
const {
7373
as = 'div',
74-
stack = globalStack,
74+
stack = useStack(),
7575
transition = 'fade',
7676
teleport = true,
7777
teleportTo = 'body',
@@ -80,16 +80,17 @@
8080
const attrs = useAttrs()
8181
8282
function onClick () {
83-
if (!stack.isBlocking.value) {
84-
stack.dismiss()
83+
const top = stack.top.value
84+
if (top && !stack.isBlocking.value) {
85+
top.dismiss()
8586
}
8687
}
8788
8889
const slotProps = toRef((): ScrimSlotProps => ({
8990
isActive: stack.isActive.value,
9091
isBlocking: stack.isBlocking.value,
9192
zIndex: stack.scrimZIndex.value,
92-
dismiss: () => stack.dismiss(),
93+
dismiss: onClick,
9394
}))
9495
9596
const style = toRef(() => ({

0 commit comments

Comments
 (0)