Skip to content

Commit

Permalink
feat: Rewrite Slideover with Radix UI
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisGV04 committed Feb 16, 2024
1 parent fd23a55 commit fa90417
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 159 deletions.
48 changes: 21 additions & 27 deletions docs/pages/slideover.vue
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
<script setup lang="ts">
import { UiContainer, UiSlideover } from "#components";
import { ref } from "vue";
const showSlideover = ref(false);
import { UiButton, UiContainer, UiSlideover } from '#components';
import { DialogClose, DialogTitle } from 'radix-vue';
</script>

<template>
<UiContainer class="py-8">
<h1 class="demo-page-title">Slideover</h1>
<p class="demo-page-description">
Modal that slides from the side of the screen to call the attention of the
user.
Modal that slides from the side of the screen to call the attention of the user.
</p>

<UiButton
class="mt-3"
label="Open slideover"
@click="showSlideover = true"
/>
<UiSlideover>
<template #trigger>
<UiButton label="Open slideover" class="mt-4" />
</template>

<template #content>
<div class="px-6 py-8">
<DialogTitle class="text-xl font-medium text-gray-900">This is a slideover</DialogTitle>

<UiSlideover v-model="showSlideover" side="left">
<div class="flex flex-col px-4 py-6">
<h2 class="text-xl font-medium text-gray-900">This is a slideover</h2>
<p class="mt-3 text-gray-600">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Earum fugit
impedit, natus, ea libero vitae soluta expedita ratione mollitia fuga
similique, dicta aliquam? Distinctio nulla, vel quibusdam eligendi
nostrum perferendis!
</p>
<p class="mt-3 text-gray-600">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Earum fugit impedit, natus, ea libero
vitae soluta expedita ratione mollitia fuga similique, dicta aliquam? Distinctio nulla, vel
quibusdam eligendi nostrum perferendis!
</p>

<UiButton
block
class="mt-3"
label="Close slideover"
@click="showSlideover = false"
/>
</div>
<DialogClose as-child>
<UiButton label="Close dialog" class="mt-4" />
</DialogClose>
</div>
</template>
</UiSlideover>
</UiContainer>
</template>
171 changes: 65 additions & 106 deletions src/runtime/components/overlays/Slideover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,87 @@
// @ts-expect-error
import appConfig from '#build/app.config';
import { useId } from '#imports';
import { mergeConfig } from '#ui/utils';
import slideover from '../../ui.config/slideover';
const config = mergeConfig<typeof slideover>(appConfig.ui.strategy, appConfig.ui.slideover, slideover);
</script>

<script setup lang="ts">
import { useUI } from '#ui/composables/useUI';
import type { Strategy } from '#ui/types';
import { slideover } from '#ui/ui.config';
import { mergeConfig } from '#ui/utils';
import {
Dialog as HDialog,
DialogPanel as HDialogPanel,
TransitionChild,
TransitionRoot,
provideUseId,
} from '@headlessui/vue';
import { useVModel } from '@vueuse/core';
import type { PopoverContentProps } from 'radix-vue';
import { Dialog } from 'radix-vue/namespaced';
import { twJoin, twMerge } from 'tailwind-merge';
import type { PropType } from 'vue';
import { computed, defineComponent, toRef } from 'vue';
import { computed, defineOptions, toRef } from 'vue';
const config = mergeConfig<typeof slideover>(appConfig.ui.strategy, appConfig.ui.slideover, slideover);
defineOptions({ inheritAttrs: false });
export default defineComponent({
components: {
HDialog,
HDialogPanel,
TransitionRoot,
TransitionChild,
const props = defineProps({
defaultOpen: Boolean,
open: { type: Boolean, default: undefined }, // v-model:open To use as controlled component
side: {
type: String as PropType<PopoverContentProps['side']>,
default: 'right',
validator: (value: string) => ['top', 'bottom', 'left', 'right'].includes(value),
},
inheritAttrs: false,
props: {
modelValue: { type: Boolean, default: false },
overlay: { type: Boolean, default: true },
preventClose: { type: Boolean, default: false },
transition: { type: Boolean, default: true },
appear: { type: Boolean, default: false },
side: {
type: String as PropType<'left' | 'right'>,
default: 'right',
validator: (value: string) => ['left', 'right'].includes(value),
},
class: {
type: [String, Object, Array] as PropType<any>,
default: undefined,
},
ui: {
type: Object as PropType<Partial<typeof config & { strategy?: Strategy }>>,
default: undefined,
},
ui: {
type: Object as PropType<Partial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
emits: ['update:modelValue', 'close', 'before-leave', 'after-leave', 'before-enter', 'after-enter'],
setup(props, { emit }) {
const { ui, attrs } = useUI('slideover', toRef(props, 'ui'), config, toRef(props, 'class'));
const isOpen = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const transitionClass = computed(() => {
if (!props.transition) return {};
});
const emits = defineEmits({ 'update:open': (value: boolean) => true });
return {
...ui.value.transition,
enterFrom: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
enterTo: 'translate-x-0',
leaveFrom: 'translate-x-0',
leaveTo: props.side === 'left' ? '-translate-x-full' : 'translate-x-full',
};
});
const $open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen,
passive: (props.open === undefined) as any,
});
function close() {
if (props.preventClose) return;
const { ui } = useUI('slideover', toRef(props, 'ui'), config);
isOpen.value = false;
emit('close');
}
const containerClasses = computed(() => {
let position = '';
provideUseId(() => useId());
switch (props.side) {
case 'top':
position = 'inset-x-0 top-0 data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top';
break;
case 'bottom':
position =
'inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom';
break;
case 'left':
position =
'inset-y-0 left-0 h-full w-3/4 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm';
break;
case 'right':
position =
'inset-y-0 right-0 h-full w-3/4 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md';
break;
default:
break;
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
isOpen,
transitionClass,
close,
};
},
return twMerge(twJoin(ui.value.container, ui.value.transition), position, ui.value.size);
});
</script>

<template>
<TransitionRoot
:show="isOpen"
:appear="appear"
as="template"
@after-leave="$emit('after-leave')"
@before-leave="$emit('before-leave')"
@after-enter="$emit('after-enter')"
@before-enter="$emit('before-enter')"
>
<HDialog :class="ui.wrapper" v-bind="attrs" @close="close">
<TransitionChild v-if="overlay" as="template" :appear="appear" v-bind="ui.overlay.transition">
<div :class="[ui.overlay.base, ui.overlay.background]" />
</TransitionChild>
<Dialog.Root v-model:open="$open">
<Dialog.Trigger as-child>
<slot name="trigger" :open="$open">
<UiButton label="Open" />
</slot>
</Dialog.Trigger>

<div :class="ui.inner">
<div :class="[ui.container, side === 'left' && 'flex-row-reverse']">
<TransitionChild as="template" v-bind="ui.overlay.transition">
<div :class="ui.padding">
<slot name="padding" />
</div>
</TransitionChild>
<Dialog.Portal>
<Dialog.Overlay :class="ui.overlay" />

<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
<HDialogPanel :class="[ui.base, ui.background, ui.width, ui.ring, ui.rounded, ui.shadow]">
<slot />
</HDialogPanel>
</TransitionChild>
</div>
</div>
</HDialog>
</TransitionRoot>
<Dialog.Content :data-side="props.side" :class="containerClasses">
<slot name="content" />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</template>
32 changes: 6 additions & 26 deletions src/runtime/ui.config/slideover.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,8 @@
export default /*ui*/ {
wrapper: 'relative z-40',
inner: 'fixed inset-0 z-10',
container: 'flex min-h-full justify-end',
padding: 'min-w-[3rem] px-1 py-4 flex flex-col',
base: 'relative h-screen overflow-y-scroll',
background: 'bg-white',
ring: '',
rounded: '',
shadow: 'shadow-xl',
width: 'flex-1 max-w-md',
overlay: {
base: 'fixed inset-0 transition-opacity',
background: 'bg-gray-900/70 backdrop-blur-sm backdrop-filter',
transition: {
enter: 'ease-out duration-300',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leave: 'ease-in duration-200',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0',
},
},
transition: {
enter: 'transform transition ease-in-out duration-500 sm:duration-700',
leave: 'transform transition ease-in-out duration-500 sm:duration-700',
},
container: 'fixed z-50 bg-white shadow-xl',
size: '',
overlay:
'fixed inset-0 z-40 bg-black/70 backdrop-blur-sm backdrop-filter duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
transition:
'transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
};

0 comments on commit fa90417

Please sign in to comment.