Skip to content

feat(Slideovers): Allow multiple instances of programmatic slideover #1758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f7a8b6f
feat(Slideover): Allow opening multiple independent instances
genu May 7, 2024
ff159bc
docs(Slideover): Update example to demonstrate multiple instances
genu May 7, 2024
5acec96
chose(Slideover): Cleanup
genu May 7, 2024
4315197
chore: Cleanup playground
genu May 7, 2024
5ea2d2a
chore: restore playground app.vue
genu May 7, 2024
b3ede0e
refactor(Slideovers): Update Slideover component and related files
genu May 8, 2024
117059b
refactor(Slideover): Rename state to instance
genu May 8, 2024
9c1395e
refactor(Slideover): update
genu May 8, 2024
7253ac5
refactor(Slideover): Use instance id
genu May 9, 2024
99581d2
refactor(Slideover): rename `reset` to `remove`
genu May 9, 2024
f0847e6
refactor(Slideover): Remove unnecessary code in useSlideover.ts
genu May 9, 2024
1b30957
refactor(Slideover): Simplify useSlideover.ts code
genu May 9, 2024
d8520ef
refactor(Slideover): Don't make close async
genu May 9, 2024
3341393
refactor(Slideover): Cleanup
genu May 9, 2024
d4ba9e6
refactor(Slideover): Improve useSlideover.ts code readability and mai…
genu May 9, 2024
2f1938e
Merge branch 'dev' into feature/multi-slideover
genu May 9, 2024
f9eae35
refactor(Slideover): Add close event listener for slideover instances
genu May 13, 2024
af320c2
Merge branch 'dev' into feature/multi-slideover
genu May 13, 2024
de32d4e
refactor(Slideovers): Remove unused code
genu May 13, 2024
e08f823
Merge branch 'dev' into feature/multi-slideover
genu May 26, 2024
67cee82
Merge branch 'dev' into feature/multi-slideover
genu Aug 23, 2024
fbc6038
fix: Handle the preventClose event
genu Aug 23, 2024
a0ce2ed
chore: restore app.vue
genu Aug 23, 2024
00469d0
chore: fix formatting in app.vue
genu Aug 24, 2024
b5a2a64
Merge branch 'dev' into feature/multi-slideover
genu Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions docs/components/content/examples/SlideoverExampleComponent.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
<script lang="ts" setup>
import { SlideoverExampleComponentB } from '#components'

const props = defineProps({
count: {
type: Number,
default: 0
},
side: {
type: String as PropType<'left' | 'right'>,
default: 'right'
}
})

const emits = defineEmits<{
close: [];
}>()

const slideover = useSlideover()
const anotherCount = ref(0)


function openAnotherSlideover () {
anotherCount.value += 1

const instance = slideover.open(SlideoverExampleComponentB, {
count: anotherCount.value,
side: 'left',
onClose: () => {
instance.close()
}
})
}
</script>

<template>
<USlideover>
<USlideover :side="props.side">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
Expand All @@ -24,7 +45,12 @@ const emits = defineEmits<{
</div>
</template>

<Placeholder class="h-full" />
<div class="flex gap-2 flex-col h-full">
<div class="flex justify-around">
<UButton label="Reveal another slideover" @click="openAnotherSlideover" />
</div>
<Placeholder class="flex-grow" />
</div>
</UCard>
</USlideover>
</template>
33 changes: 33 additions & 0 deletions docs/components/content/examples/SlideoverExampleComponentB.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts" setup>
const props = defineProps({
count: {
type: Number,
default: 0
},
side: {
type: String as PropType<'left' | 'right'>,
default: 'right'
}
})

const emits = defineEmits<{
close: [];
}>()

</script>

<template>
<USlideover :side="props.side">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Opened programmatically: {{ props.count }} times
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
</div>
</template>
<Placeholder class="h-full" />
</UCard>
</USlideover>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ const slideover = useSlideover()
const count = ref(0)
function openSlideover () {
count.value += 1
slideover.open(SlideoverExampleComponent, {
const instance = slideover.open(SlideoverExampleComponent, {
count: count.value,
onClose: slideover.close
side: 'right',
onClose: () => {
instance.close()
}
})
}
</script>
Expand Down
53 changes: 53 additions & 0 deletions playground/components/SlideoverExampleComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { SlideoverExampleComponentB } from '#components'

const props = defineProps({
count: {
type: Number,
default: 0
},
side: {
type: String as PropType<'left' | 'right'>,
default: 'right'
}
})

const emits = defineEmits<{
close: [];
switch: [];
}>()

const slideover = useSlideover()
const anotherCount = ref(0)


function openAnotherSlideover () {
anotherCount.value += 1

slideover.open(SlideoverExampleComponentB)

}
</script>

<template>
<USlideover :side="props.side">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Opened programmatically: {{ props.count }} times
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
</div>
</template>

<div class="flex gap-2 flex-col h-full">
<div class="flex justify-around flex-col">
<pre>{{ $props }}</pre>
<UButton label="Reveal another slideover" @click="openAnotherSlideover" />
<UButton label="Switch" @click="$emit('switch')" />
</div>
</div>
</UCard>
</USlideover>
</template>
32 changes: 32 additions & 0 deletions playground/components/SlideoverExampleComponentB.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts" setup>
const props = defineProps({
count: {
type: Number,
default: 0
},
side: {
type: String as PropType<'left' | 'right'>,
default: 'right'
}
})

const emits = defineEmits<{
close: [];
}>()

</script>

<template>
<USlideover :side="props.side">
<UCard class="flex flex-col flex-1" :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Opened programmatically: {{ props.count }} times
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
</div>
</template>
</UCard>
</USlideover>
</template>
33 changes: 26 additions & 7 deletions src/runtime/components/overlays/Slideovers.client.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
<template>
<component
:is="slideoverState.component"
v-if="slideoverState"
v-bind="slideoverState.props"
v-model="isOpen"
@after-leave="reset"
:is="instance.component"
v-for="instance in slideoverInstances"
:key="instance.id"
v-bind="instance.props"
:model-value="instance.isOpen"
:prevent-close="true"
@close-prevented="onClosePrevented(instance)"
@after-leave="remove(instance.id)"
@update:model-value="close(instance.id)"
/>
</template>

<script lang="ts" setup>
import { inject } from 'vue'
import { useSlideover, slidOverInjectionKey } from '../../composables/useSlideover'

const slideoverState = inject(slidOverInjectionKey)
const slideoverInstances = inject(slidOverInjectionKey)

const { isOpen, reset } = useSlideover()
const { remove, close } = useSlideover()

/**
* We must set preventClose: true to prevent closing all of the instances.
* Instead, we listen for the close-prevented event and remove the top level instance only.
*
* @param instance the instance that is being closed
*/
const onClosePrevented = (instance) => {
if (instance.props.preventClose) {
return
}

// Close the top level instance only
close(slideoverInstances.value[slideoverInstances.value.length - 1].id)
}
</script>
92 changes: 60 additions & 32 deletions src/runtime/composables/useSlideover.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,91 @@
import { ref, inject } from 'vue'
import { inject } from 'vue'
import type { ShallowRef, Component, InjectionKey } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import type { ComponentProps } from '../types/component'
import type { Slideover, SlideoverState } from '../types/slideover'
import type { Slideover, SlideoverInstance } from '../types/slideover'

export const slidOverInjectionKey: InjectionKey<ShallowRef<SlideoverState>> = Symbol('nuxt-ui.slideover')
export const slidOverInjectionKey: InjectionKey<ShallowRef<SlideoverInstance<any>[]>> = Symbol('nuxt-ui.slideover')

function _useSlideover () {
const slideoverState = inject(slidOverInjectionKey)

const isOpen = ref(false)
const slideoverInstances = inject(slidOverInjectionKey)

function open<T extends Component> (component: T, props?: Slideover & ComponentProps<T>) {
if (!slideoverState) {
if (!slideoverInstances) {
throw new Error('useSlideover() is called without provider')
}

slideoverState.value = {
component,
props: props ?? {}
}
const instance = createInstance(component, props)

isOpen.value = true
slideoverInstances.value = [
...slideoverInstances.value,
instance
]

return instance
}

async function close () {
if (!slideoverState) return
function close (id: number) {
const slideoverInstance = slideoverInstances.value.find((slideover) => slideover.id === id)

if (!slideoverInstance) return

isOpen.value = false
slideoverInstance.isOpen = false

slideoverInstances.value = [
...slideoverInstances.value.filter((slideover) => slideover.id !== id),
slideoverInstance
]
}

function reset () {
slideoverState.value = {
component: 'div',
props: {}
}
function remove (id: number) {
slideoverInstances.value = slideoverInstances.value.filter((slideover) => slideover.id !== id)
}

/**
* Allows updating the slideover props
*/
function patch<T extends Component = {}> (props: Partial<Slideover & ComponentProps<T>>) {
if (!slideoverState) return

slideoverState.value = {
...slideoverState.value,
props: {
...slideoverState.value.props,
...props
function patch<T extends Component = {}> (id: number, props: Partial<Slideover & ComponentProps<T>>) {
const slideoverInstance = slideoverInstances.value.find((slideover) => slideover.id === id)

if (!slideoverInstance) return

slideoverInstances.value = [
...slideoverInstances.value.filter((slideover) => slideover.id !== id),
{
...slideoverInstance,
props: {
...slideoverInstance.props,
...props
}
}
}
]
}

function createInstance<T extends Component> (component: T, props?: Slideover & ComponentProps<T>): SlideoverInstance<ComponentProps<T>> {
// Random short id
const id = Math.floor(Math.random() * 1000000)

return {
id,
isOpen: true,
component,
props: {
...props,
onClose () {
props?.onClose?.()
close(id)
}
},
patch: (props) => patch(id, props),
close: () => close(id)
}
}

return {
open,
close,
reset,
patch,
isOpen
remove,
patch
}
}

Expand Down
9 changes: 3 additions & 6 deletions src/runtime/plugins/slideovers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { defineNuxtPlugin } from '#imports'
import { shallowRef } from 'vue'
import { slidOverInjectionKey } from '../composables/useSlideover'
import type { SlideoverState } from '../types/slideover'
import type { SlideoverInstance } from '../types/slideover'

export default defineNuxtPlugin((nuxtApp) => {
const slideoverState = shallowRef<SlideoverState>({
component: 'div',
props: {}
})
const slideoverInstances = shallowRef<SlideoverInstance<any>[]>([])

nuxtApp.vueApp.provide(slidOverInjectionKey, slideoverState)
nuxtApp.vueApp.provide(slidOverInjectionKey, slideoverInstances)
})
12 changes: 8 additions & 4 deletions src/runtime/types/slideover.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export interface Slideover {
modelValue?: boolean
}

export interface SlideoverState {
component: Component | string
props: Slideover
}
export interface SlideoverInstance<T> {
id: number,
isOpen: boolean
component: Component
props: Slideover & T
patch (props: Partial<Slideover & T>): void
close: () => void,
}