Skip to content

Commit b7d2cf8

Browse files
authored
fix(Scrim,Snackbar): forward renderless through the teleport hosts and expose the element contract in slot attrs (#335)
1 parent 62a313a commit b7d2cf8

4 files changed

Lines changed: 115 additions & 14 deletions

File tree

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
isBlocking: boolean
4747
/** Dismiss this overlay */
4848
dismiss: () => void
49+
/** Attributes to bind to the scrim element */
50+
attrs: {
51+
style: { zIndex: number }
52+
onClick: () => void
53+
}
4954
}
5055
</script>
5156

@@ -57,7 +62,7 @@
5762
import { useStack } from '#v0/composables/useStack'
5863
5964
// Utilities
60-
import { computed, useAttrs } from 'vue'
65+
import { computed, mergeProps, useAttrs } from 'vue'
6166
6267
defineOptions({ name: 'Scrim', inheritAttrs: false })
6368
@@ -67,6 +72,7 @@
6772
6873
const {
6974
as = 'div',
75+
renderless,
7076
transition = 'fade',
7177
teleport = true,
7278
teleportTo = 'body',
@@ -84,17 +90,18 @@
8490
}
8591
8692
function getSlotProps (ticket: StackTicket): ScrimSlotProps {
93+
const zIndex = ticket.zIndex.value - 1
8794
return {
8895
ticket,
89-
zIndex: ticket.zIndex.value - 1,
96+
zIndex,
9097
isBlocking: ticket.blocking,
9198
dismiss: () => onDismiss(ticket),
99+
attrs: {
100+
style: { zIndex },
101+
onClick: () => onDismiss(ticket),
102+
},
92103
}
93104
}
94-
95-
function getStyle (ticket: StackTicket) {
96-
return { zIndex: ticket.zIndex.value - 1 }
97-
}
98105
</script>
99106

100107
<template>
@@ -107,9 +114,8 @@
107114
v-for="ticket in tickets"
108115
:key="ticket.id"
109116
:as
110-
:style="getStyle(ticket)"
111-
v-bind="attrs"
112-
@click="() => onDismiss(ticket)"
117+
:renderless
118+
v-bind="mergeProps(attrs, getSlotProps(ticket).attrs)"
113119
>
114120
<slot v-bind="getSlotProps(ticket)" />
115121
</Atom>
@@ -124,9 +130,8 @@
124130
v-for="ticket in tickets"
125131
:key="ticket.id"
126132
:as
127-
:style="getStyle(ticket)"
128-
v-bind="attrs"
129-
@click="() => onDismiss(ticket)"
133+
:renderless
134+
v-bind="mergeProps(attrs, getSlotProps(ticket).attrs)"
130135
>
131136
<slot v-bind="getSlotProps(ticket)" />
132137
</Atom>

packages/0/src/components/Scrim/index.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,64 @@ describe('scrim', () => {
322322
})
323323
})
324324

325+
describe('renderless', () => {
326+
it('should render no wrapper and carry the element contract in slot attrs', async () => {
327+
let captured: any
328+
const ticket = createMockTicket({ zIndex: 2500 })
329+
mockSelectedItems.value = new Set([ticket])
330+
331+
const wrapper = mount(Scrim, {
332+
props: { teleport: false, renderless: true },
333+
slots: {
334+
default: (props: any) => {
335+
captured = props
336+
return h('aside', { class: 'custom-scrim', ...props.attrs })
337+
},
338+
},
339+
})
340+
341+
await nextTick()
342+
const custom = wrapper.find('.custom-scrim')
343+
expect(custom.exists()).toBe(true)
344+
expect(wrapper.findAll('.custom-scrim')).toHaveLength(1)
345+
expect(wrapper.findAll('div')).toHaveLength(0)
346+
expect(captured.attrs.style.zIndex).toBe(2499)
347+
expect((custom.element as HTMLElement).style.zIndex).toBe('2499')
348+
349+
await custom.trigger('click')
350+
expect(ticket.dismiss).toHaveBeenCalledTimes(1)
351+
})
352+
353+
it('should dismiss from the consumer element when teleported', async () => {
354+
const target = document.createElement('div')
355+
target.id = 'renderless-target'
356+
document.body.append(target)
357+
358+
const ticket = createMockTicket({ zIndex: 3000 })
359+
mockSelectedItems.value = new Set([ticket])
360+
361+
mount(Scrim, {
362+
props: { teleportTo: '#renderless-target', renderless: true },
363+
attachTo: document.body,
364+
slots: {
365+
default: (props: any) => h('aside', { class: 'custom-scrim', ...props.attrs }),
366+
},
367+
})
368+
369+
await nextTick()
370+
const custom = target.querySelector('.custom-scrim') as HTMLElement
371+
expect(custom).toBeTruthy()
372+
expect(target.contains(custom)).toBe(true)
373+
expect(target.querySelector('div')).toBeNull()
374+
expect(custom.style.zIndex).toBe('2999')
375+
376+
custom.dispatchEvent(new MouseEvent('click', { bubbles: true }))
377+
expect(ticket.dismiss).toHaveBeenCalledTimes(1)
378+
379+
target.remove()
380+
})
381+
})
382+
325383
describe('transition', () => {
326384
it('should use fade transition by default', async () => {
327385
const ticket = createMockTicket()

packages/0/src/components/Snackbar/SnackbarPortal.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import { Atom } from '#v0/components/Atom'
1717
import { Portal } from '#v0/components/Portal'
1818
19+
// Utilities
20+
import { mergeProps } from 'vue'
21+
1922
// Types
2023
import type { AtomProps } from '#v0/components/Atom'
2124
@@ -27,6 +30,10 @@
2730
export interface SnackbarPortalSlotProps {
2831
/** Calculated z-index from useStack */
2932
zIndex: number
33+
/** Attributes to bind to the portal element */
34+
attrs: {
35+
style: { zIndex: number }
36+
}
3037
}
3138
</script>
3239

@@ -39,15 +46,25 @@
3946
4047
const {
4148
as = 'div',
49+
renderless,
4250
teleport = 'body',
4351
} = defineProps<SnackbarPortalProps>()
52+
53+
function getSlotProps (zIndex: number): SnackbarPortalSlotProps {
54+
return {
55+
zIndex,
56+
attrs: {
57+
style: { zIndex },
58+
},
59+
}
60+
}
4461
</script>
4562

4663
<template>
4764
<Portal :disabled="teleport === false" :to="teleport || 'body'">
4865
<template #default="{ zIndex }">
49-
<Atom :as :style="{ zIndex }" v-bind="$attrs">
50-
<slot v-bind="{ zIndex }" />
66+
<Atom :as :renderless v-bind="mergeProps($attrs, getSlotProps(zIndex).attrs)">
67+
<slot v-bind="getSlotProps(zIndex)" />
5168
</Atom>
5269
</template>
5370
</Portal>

packages/0/src/components/Snackbar/index.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,27 @@ describe('snackbar', () => {
470470
})
471471

472472
describe('renderless', () => {
473+
it('should render no wrapper and carry zIndex style in slot attrs on portal', () => {
474+
let captured: any
475+
476+
const wrapper = mountWithStack(Snackbar.Portal, {
477+
props: { teleport: false, renderless: true },
478+
slots: {
479+
default: (props: any) => {
480+
captured = props
481+
return h('section', { 'data-testid': 'custom-portal', ...props.attrs }, 'Toasts')
482+
},
483+
},
484+
})
485+
486+
const custom = wrapper.find('[data-testid="custom-portal"]')
487+
expect(custom.exists()).toBe(true)
488+
expect(wrapper.findAll('[data-testid="custom-portal"]')).toHaveLength(1)
489+
expect(wrapper.findAll('div')).toHaveLength(0)
490+
expect(captured.attrs.style.zIndex).toBe(captured.zIndex)
491+
expect((custom.element as HTMLElement).style.zIndex).toBe(String(captured.zIndex))
492+
})
493+
473494
it('should render no wrapper and carry role in slot attrs on root', () => {
474495
let captured: any
475496

0 commit comments

Comments
 (0)