refactor: rewrite DialogContent/DialogScrollContent without reka-ui
Uses Teleport + Transition + useFocusTrap + useDismissableLayer + useScrollLock. The Dialog family no longer imports reka-ui at all.
This commit is contained in:
parent
3d8bdaa903
commit
e7d96cc3ba
2 changed files with 107 additions and 89 deletions
|
|
@ -1,53 +1,63 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XIcon } from '@lucide/vue';
|
import { XIcon } from '@lucide/vue'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
import { inject, ref, watch } from 'vue'
|
||||||
import type { HTMLAttributes } from "vue"
|
import { useScrollLock } from '@vueuse/core'
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import DialogOverlay from "./DialogOverlay.vue"
|
import { useFocusTrap } from '@/lib/use-focus-trap'
|
||||||
|
import { useDismissableLayer } from '@/lib/use-dismissable-layer'
|
||||||
|
import { DialogContextKey } from './context'
|
||||||
|
import DialogOverlay from './DialogOverlay.vue'
|
||||||
|
import DialogClose from './DialogClose.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({ inheritAttrs: false })
|
||||||
inheritAttrs: false,
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{ class?: HTMLAttributes['class'], showCloseButton?: boolean }>(),
|
||||||
|
{ showCloseButton: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const context = inject(DialogContextKey)!
|
||||||
|
const contentRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useFocusTrap(contentRef, context.open)
|
||||||
|
useDismissableLayer(contentRef, context.open, {
|
||||||
|
onDismiss: () => { context.open.value = false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
|
const isLocked = useScrollLock(document.body)
|
||||||
showCloseButton: true,
|
watch(context.open, (value) => { isLocked.value = value }, { immediate: true })
|
||||||
})
|
|
||||||
const emits = defineEmits<DialogContentEmits>()
|
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<Teleport to="body">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogContent
|
<Transition
|
||||||
|
enter-active-class="animate-in fade-in-0 zoom-in-95 duration-100"
|
||||||
|
leave-active-class="animate-out fade-out-0 zoom-out-95 duration-100"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="context.open.value"
|
||||||
|
ref="contentRef"
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
v-bind="{ ...$attrs, ...forwarded }"
|
role="dialog"
|
||||||
:class="cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 border border-border grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm shadow-lg duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', props.class)"
|
aria-modal="true"
|
||||||
|
:aria-labelledby="context.titleId"
|
||||||
|
:aria-describedby="context.descriptionId"
|
||||||
|
tabindex="-1"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:class="cn('bg-popover text-popover-foreground border border-border grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm shadow-lg sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', props.class)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<DialogClose
|
<DialogClose v-if="showCloseButton" as-child>
|
||||||
v-if="showCloseButton"
|
|
||||||
data-slot="dialog-close"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<Button variant="secondary" class="absolute top-2 right-2 size-7 bg-transparent hover:bg-black/10" size="icon">
|
<Button variant="secondary" class="absolute top-2 right-2 size-7 bg-transparent hover:bg-black/10" size="icon">
|
||||||
<XIcon class="size-3.5" />
|
<XIcon class="size-3.5" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</div>
|
||||||
</DialogPortal>
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,68 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XIcon } from '@lucide/vue';
|
import { XIcon } from '@lucide/vue'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { inject, ref, watch } from 'vue'
|
||||||
|
import { useScrollLock } from '@vueuse/core'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useFocusTrap } from '@/lib/use-focus-trap'
|
||||||
|
import { useDismissableLayer } from '@/lib/use-dismissable-layer'
|
||||||
|
import { DialogContextKey } from './context'
|
||||||
|
import DialogClose from './DialogClose.vue'
|
||||||
|
|
||||||
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
defineOptions({ inheritAttrs: false })
|
||||||
import type { HTMLAttributes } from "vue"
|
|
||||||
import { reactiveOmit } from "@vueuse/core"
|
|
||||||
import {
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from "reka-ui"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
defineOptions({
|
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||||
inheritAttrs: false,
|
|
||||||
|
const context = inject(DialogContextKey)!
|
||||||
|
const contentRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useFocusTrap(contentRef, context.open)
|
||||||
|
useDismissableLayer(contentRef, context.open, {
|
||||||
|
onDismiss: () => { context.open.value = false },
|
||||||
|
onPointerDownOutside: (event) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (event.offsetX > target.clientWidth || event.offsetY > target.clientHeight) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
|
const isLocked = useScrollLock(document.body)
|
||||||
const emits = defineEmits<DialogContentEmits>()
|
watch(context.open, (value) => { isLocked.value = value }, { immediate: true })
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogPortal>
|
<Teleport to="body">
|
||||||
<DialogOverlay
|
<Transition
|
||||||
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
enter-active-class="animate-in fade-in-0 duration-100"
|
||||||
|
leave-active-class="animate-out fade-out-0 duration-100"
|
||||||
>
|
>
|
||||||
<DialogContent
|
<div
|
||||||
:class="
|
v-if="context.open.value"
|
||||||
cn(
|
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80"
|
||||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
>
|
||||||
|
<div
|
||||||
|
ref="contentRef"
|
||||||
|
data-slot="dialog-content"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-labelledby="context.titleId"
|
||||||
|
:aria-describedby="context.descriptionId"
|
||||||
|
tabindex="-1"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:class="cn(
|
||||||
|
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg sm:rounded-lg md:w-full',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)"
|
||||||
"
|
|
||||||
v-bind="{ ...$attrs, ...forwarded }"
|
|
||||||
@pointer-down-outside="(event) => {
|
|
||||||
const originalEvent = event.detail.originalEvent;
|
|
||||||
const target = originalEvent.target as HTMLElement;
|
|
||||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<DialogClose
|
<DialogClose class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary">
|
||||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
|
||||||
>
|
|
||||||
<XIcon class="w-4 h-4" />
|
<XIcon class="w-4 h-4" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogContent>
|
</div>
|
||||||
</DialogOverlay>
|
</div>
|
||||||
</DialogPortal>
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue