Skip to content

Commit

Permalink
feat(Overlays): Improve the Dialog and Slideover triggers and transit…
Browse files Browse the repository at this point in the history
…ions (#6)
  • Loading branch information
ChrisGV04 authored Mar 14, 2024
1 parent 1056a9f commit f8edf2b
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 64 deletions.
75 changes: 55 additions & 20 deletions docs/pages/dialog.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
<script setup lang="ts">
import { UiContainer, UiDialog } from '#components';
import { DialogClose, DialogTitle } from 'radix-vue';
import { ref } from 'vue';
const show = ref(false);
</script>

<template>
<UiContainer class="py-8">
<h1 class="demo-page-title">Dialog</h1>
<p class="demo-page-description">Modal that pops-up to call the attention of the user.</p>

<UiDialog prevent-close>
<template #trigger>
<UiButton label="Open dialog" class="mt-4" />
</template>

<template #content>
<div class="px-6 py-8">
<DialogTitle class="text-xl font-medium text-gray-900">This is a dialog</DialogTitle>
<p class="mt-3 text-gray-600">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus libero quo voluptatem perspiciatis
similique, omnis explicabo quos consequatur, ab adipisci itaque excepturi, voluptatum voluptatibus
a unde deserunt inventore veritatis quod delectus esse hic?
</p>

<DialogClose as-child>
<UiButton label="Close dialog" class="mt-4" />
</DialogClose>
</div>
</template>
</UiDialog>
<div class="demo-category-container mt-4 items-start">
<span class="demo-category-title"
>Controlled:
<code class="demo-code-line">"{{ show }}"</code>
</span>

<UiButton label="Open dialog" class="mt-4" @click="show = true" />

<UiDialog v-model:open="show">
<template #content>
<div class="px-6 py-8">
<DialogTitle class="text-xl font-medium text-gray-900">This is a controlled dialog</DialogTitle>
<p class="mt-3 text-gray-600">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus libero quo voluptatem
perspiciatis similique, omnis explicabo quos consequatur, ab adipisci itaque excepturi,
voluptatum voluptatibus a unde deserunt inventore veritatis quod delectus esse hic?
</p>

<DialogClose as-child>
<UiButton label="Close dialog" class="mt-4" />
</DialogClose>
</div>
</template>
</UiDialog>
</div>

<div class="demo-category-container mt-4 items-start">
<span class="demo-category-title">Uncontrolled</span>

<UiDialog>
<template #trigger>
<UiButton label="Open dialog" class="mt-4" />
</template>

<template #content>
<div class="px-6 py-8">
<DialogTitle class="text-xl font-medium text-gray-900"
>This is an uncontrolled dialog</DialogTitle
>
<p class="mt-3 text-gray-600">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus libero quo voluptatem
perspiciatis similique, omnis explicabo quos consequatur, ab adipisci itaque excepturi,
voluptatum voluptatibus a unde deserunt inventore veritatis quod delectus esse hic?
</p>

<DialogClose as-child>
<UiButton label="Close dialog" class="mt-4" />
</DialogClose>
</div>
</template>
</UiDialog>
</div>
</UiContainer>
</template>
6 changes: 3 additions & 3 deletions docs/pages/slideover.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { UiButton, UiContainer, UiSlideover } from '#components';
import { DialogClose, DialogTitle } from 'radix-vue';
import { DialogClose, DialogDescription, DialogTitle } from 'radix-vue';
</script>

<template>
Expand All @@ -19,11 +19,11 @@ import { DialogClose, DialogTitle } from 'radix-vue';
<div class="px-6 py-8">
<DialogTitle class="text-xl font-medium text-gray-900">This is a slideover</DialogTitle>

<p class="mt-3 text-gray-600">
<DialogDescription 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>
</DialogDescription>

<DialogClose as-child>
<UiButton label="Close dialog" class="mt-4" />
Expand Down
47 changes: 35 additions & 12 deletions src/runtime/components/overlays/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { useUI } from '#ui/composables/useUI';
import type { Strategy } from '#ui/types';
import { dialog } from '#ui/ui.config';
import { mergeConfig } from '#ui/utils';
import { useVModel } from '@vueuse/core';
import { uiToTransitionProps } from '#ui/utils/transitions';
import { usePreferredReducedMotion, useVModel } from '@vueuse/core';
import { Dialog } from 'radix-vue/namespaced';
import { twMerge } from 'tailwind-merge';
import type { PropType } from 'vue';
import { defineOptions, toRef } from 'vue';
import { computed, defineOptions, toRef } from 'vue';
const config = mergeConfig<typeof dialog>(appConfig.ui?.dialog?.strategy, appConfig.ui?.dialog, dialog);
type UiConfig = Partial<typeof config> & { strategy?: Strategy };
Expand All @@ -25,30 +25,53 @@ const props = defineProps({
default: () => ({}) as UiConfig,
},
});
const emits = defineEmits<{ (e: 'update:open', value: boolean): void }>();
const emits = defineEmits<{
(e: 'update:open', value: boolean): void;
(e: 'before-enter'): void;
(e: 'after-enter'): void;
(e: 'before-leave'): void;
(e: 'after-leave'): void;
}>();
const $open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen,
passive: (props.open === undefined) as any,
});
const { ui } = useUI('dialog', toRef(props, 'ui'), config);
// Disable transitions when prefered reduced motion
const reduceMotion = usePreferredReducedMotion();
const contentTransition = computed(() =>
reduceMotion.value === 'no-preference' ? uiToTransitionProps(ui.value.transition) : {},
);
const overlayTransition = computed(() =>
reduceMotion.value === 'no-preference' ? uiToTransitionProps(ui.value.overlay.transition) : {},
);
</script>

<template>
<Dialog.Root v-model:open="$open">
<Dialog.Trigger as-child>
<slot name="trigger" :open="$open">
<UiButton label="Open" />
</slot>
<Dialog.Trigger v-if="$slots.trigger" as-child>
<slot name="trigger" :open="$open" />
</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Overlay :class="ui.overlay" />
<Transition v-bind="overlayTransition">
<Dialog.Overlay :class="ui.overlay.base" />
</Transition>

<Dialog.Content :class="twMerge(ui.container, ui.size, ui.transition)">
<slot name="content" />
</Dialog.Content>
<Transition
v-bind="contentTransition"
@before-enter="emits('before-enter')"
@after-enter="emits('after-enter')"
@before-leave="emits('before-leave')"
@after-leave="emits('after-leave')"
>
<Dialog.Content :class="[ui.container, ui.size]">
<slot name="content" />
</Dialog.Content>
</Transition>
</Dialog.Portal>
</Dialog.Root>
</template>
76 changes: 55 additions & 21 deletions src/runtime/components/overlays/Slideover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { useUI } from '#ui/composables/useUI';
import type { Strategy } from '#ui/types';
import { slideover } from '#ui/ui.config';
import { mergeConfig } from '#ui/utils';
import { useVModel } from '@vueuse/core';
import { uiToTransitionProps } from '#ui/utils/transitions';
import { usePreferredReducedMotion, 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 { twMerge } from 'tailwind-merge';
import type { PropType, TransitionProps } from 'vue';
import { computed, defineOptions, toRef } from 'vue';
const config = mergeConfig<typeof slideover>(
Expand All @@ -35,7 +36,13 @@ const props = defineProps({
default: () => ({}) as UiConfig,
},
});
const emits = defineEmits<{ (e: 'update:open', value: boolean): void }>();
const emits = defineEmits<{
(e: 'update:open', value: boolean): void;
(e: 'before-enter'): void;
(e: 'after-enter'): void;
(e: 'before-leave'): void;
(e: 'after-leave'): void;
}>();
const $open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen,
Expand All @@ -44,47 +51,74 @@ const $open = useVModel(props, 'open', emits, {
const { ui } = useUI('slideover', toRef(props, 'ui'), config);
const containerClasses = computed(() => {
// Disable transitions when prefered reduced motion
const reduceMotion = usePreferredReducedMotion();
const overlayTransition = computed(() =>
reduceMotion.value === 'no-preference' ? uiToTransitionProps(ui.value.overlay.transition) : {},
);
const containerValues = computed<{ classes: string; transition: TransitionProps }>(() => {
let position = '';
let outPos = '';
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';
position = 'inset-x-0 top-0';
outPos = '-translate-y-full';
break;
case 'bottom':
position =
'inset-x-0 bottom-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom';
position = 'inset-x-0 bottom-0';
outPos = 'translate-y-full';
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';
position = 'inset-y-0 left-0 h-full w-3/4 sm:max-w-sm';
outPos = '-translate-x-full';
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';
position = 'inset-y-0 right-0 h-full w-3/4 sm:max-w-sm';
outPos = 'translate-x-full';
break;
default:
break;
}
return twMerge(twJoin(ui.value.container, ui.value.transition), position, ui.value.size);
return {
classes: twMerge(ui.value.container, position, ui.value.size),
transition:
reduceMotion.value === 'reduce'
? {}
: {
enterActiveClass: ui.value.transition.enterActive,
leaveActiveClass: ui.value.transition.leaveActive,
enterFromClass: outPos,
leaveToClass: outPos,
},
};
});
</script>

<template>
<Dialog.Root v-model:open="$open">
<Dialog.Trigger as-child>
<slot name="trigger" :open="$open">
<UiButton label="Open" />
</slot>
<Dialog.Trigger v-if="$slots.trigger" as-child>
<slot name="trigger" :open="$open" />
</Dialog.Trigger>

<Dialog.Portal>
<Dialog.Overlay :class="ui.overlay" />
<Transition v-bind="overlayTransition">
<Dialog.Overlay :class="ui.overlay.base" />
</Transition>

<Dialog.Content :data-side="props.side" :class="containerClasses">
<slot name="content" />
</Dialog.Content>
<Transition
v-bind="containerValues.transition"
@before-enter="emits('before-enter')"
@after-enter="emits('after-enter')"
@before-leave="emits('before-leave')"
@after-leave="emits('after-leave')"
>
<Dialog.Content :data-side="props.side" :class="containerValues.classes">
<slot name="content" />
</Dialog.Content>
</Transition>
</Dialog.Portal>
</Dialog.Root>
</template>
23 changes: 19 additions & 4 deletions src/runtime/ui.config/dialog.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
export default /*ui*/ {
container: 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white shadow-xl',
size: 'w-full max-w-lg',
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:
'duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
overlay: {
base: 'fixed inset-0 z-40 bg-black/70 backdrop-blur-sm backdrop-filter',
transition: {
enterActive: 'ease-out duration-200',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leaveActive: 'ease-in duration-200',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0',
},
},
transition: {
enterActive: 'transition-[opacity,transform] ease-out duration-300',
enterFrom: 'opacity-0 -translate-y-[40%] scale-95',
enterTo: 'opacity-100 scale-100',
leaveActive: 'transition-[opacity,transform] ease-in duration-200',
leaveFrom: 'opacity-100 scale-100',
leaveTo: 'opacity-0 scale-95',
},
};
19 changes: 15 additions & 4 deletions src/runtime/ui.config/slideover.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
export default /*ui*/ {
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',
overlay: {
base: 'fixed inset-0 z-40 bg-black/70 backdrop-blur-sm backdrop-filter',
transition: {
enterActive: 'ease-out duration-200',
enterFrom: 'opacity-0',
enterTo: 'opacity-100',
leaveActive: 'ease-in duration-200',
leaveFrom: 'opacity-100',
leaveTo: 'opacity-0',
},
},
transition: {
enterActive: 'transition-transform ease-in-out duration-500',
leaveActive: 'transition-transform ease-in-out duration-300',
},
};
22 changes: 22 additions & 0 deletions src/runtime/utils/transitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { TransitionProps } from 'vue';

export type UiTransition = {
enterActive?: string;
enterFrom?: string;
enterTo?: string;
leaveActive?: string;
leaveFrom?: string;
leaveTo?: string;
};

/** Converts our simplified transition props into <Transition> props */
export function uiToTransitionProps(transition: UiTransition): TransitionProps {
return {
enterActiveClass: transition.enterActive,
enterFromClass: transition.enterFrom,
enterToClass: transition.enterTo,
leaveActiveClass: transition.leaveActive,
leaveFromClass: transition.leaveFrom,
leaveToClass: transition.leaveTo,
};
}

0 comments on commit f8edf2b

Please sign in to comment.