Skip to content

Commit 23d67b2

Browse files
feat: Dialog
1 parent 3840dc7 commit 23d67b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1629
-61
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Enter the component you want most in the components, leave the emojis and follow
3939
| [Avatar](https://vue-primitives.netlify.app/?path=/story/components-avatar--styled) ||
4040
| [Checkbox](https://vue-primitives.netlify.app/?path=/story/components-checkbox--styled) ||
4141
| [Collapsible](https://vue-primitives.netlify.app/?path=/story/components-collapsible--styled) ||
42-
| Dialog | 🚧 |
42+
| [Dialog](https://vue-primitives.netlify.app/?path=/story/components-dialog--styled) | |
4343
| DropdownMenu | 🚧 |
4444
| Form | 🚧 |
4545
| HoverCard | 🚧 |
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Ref } from 'vue'
2+
import { type MutableRefObject, createContext } from '../hooks/index.ts'
3+
4+
export interface DialogProps {
5+
open?: boolean
6+
defaultOpen?: boolean
7+
modal?: boolean
8+
}
9+
10+
// eslint-disable-next-line ts/consistent-type-definitions
11+
export type DialogEmits = {
12+
'update:open': [open: boolean]
13+
}
14+
15+
export type DialogContentElement = HTMLDivElement
16+
17+
interface DialogContextValue {
18+
triggerRef: MutableRefObject<HTMLButtonElement | undefined>
19+
contentRef: MutableRefObject<DialogContentElement | undefined>
20+
contentId?: string
21+
titleId?: string
22+
descriptionId?: string
23+
open: Ref<boolean>
24+
onOpenChange: (open: boolean) => void
25+
onOpenToggle: () => void
26+
modal: boolean
27+
}
28+
29+
export const [provideDialogContext, useDialogContext] = createContext<DialogContextValue>('Dialog')
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
import { useControllableState, useRef } from '../hooks/index.ts'
3+
import { type DialogContentElement, type DialogEmits, type DialogProps, provideDialogContext } from './Dialog.ts'
4+
5+
defineOptions({
6+
name: 'OkuDialog',
7+
})
8+
9+
const props = withDefaults(defineProps<DialogProps>(), {
10+
open: undefined,
11+
defaultOpen: false,
12+
modal: true,
13+
})
14+
15+
const emit = defineEmits<DialogEmits>()
16+
17+
const triggerRef = useRef<HTMLButtonElement>()
18+
const contentRef = useRef<DialogContentElement>()
19+
20+
const open = useControllableState(props, v => emit('update:open', v), 'open', props.defaultOpen)
21+
22+
provideDialogContext({
23+
triggerRef,
24+
contentRef,
25+
contentId: undefined,
26+
titleId: undefined,
27+
descriptionId: undefined,
28+
open,
29+
modal: props.modal,
30+
onOpenChange(value) {
31+
open.value = value
32+
},
33+
onOpenToggle() {
34+
open.value = !open.value
35+
},
36+
})
37+
</script>
38+
39+
<template>
40+
<slot />
41+
</template>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { PrimitiveProps } from '../primitive/index.ts'
2+
3+
export interface DialogCloseProps extends PrimitiveProps {}
4+
5+
// eslint-disable-next-line ts/consistent-type-definitions
6+
export type DialogCloseEmits = {
7+
click: [event: Event]
8+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
import { Primitive } from '../primitive/index.ts'
3+
import { composeEventHandlers } from '../utils/vue.ts'
4+
import { useDialogContext } from './Dialog.ts'
5+
import type { DialogCloseEmits, DialogCloseProps } from './DialogClose.ts'
6+
7+
defineOptions({
8+
name: 'DialogClose',
9+
})
10+
11+
withDefaults(defineProps<DialogCloseProps>(), {
12+
as: 'button',
13+
})
14+
const emit = defineEmits<DialogCloseEmits>()
15+
16+
const context = useDialogContext('DialogClose')
17+
18+
const onClick = composeEventHandlers((event: Event) => {
19+
emit('click', event)
20+
}, () => context.onOpenChange(false))
21+
</script>
22+
23+
<template>
24+
<Primitive :as="as" :as-child="asChild" type="button" @click="onClick">
25+
<slot />
26+
</Primitive>
27+
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface DialogContentProps {
2+
/**
3+
* Used to force mounting when more control is needed. Useful when
4+
* controlling transntion with Vue native transition or other animation libraries.
5+
*/
6+
forceMount?: boolean
7+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script setup lang="ts">
2+
import { shallowRef } from 'vue'
3+
import { forwardRef } from '../utils/vue.ts'
4+
import { usePresence } from '../presence/usePresence.ts'
5+
import { useDialogContext } from './Dialog.ts'
6+
import DialogContentModal from './DialogContentModal.vue'
7+
import DialogContentNonModal from './DialogContentNonModal.vue'
8+
import type { DialogContentProps } from './DialogContent.ts'
9+
10+
defineOptions({
11+
name: 'DialogContent',
12+
})
13+
14+
const props = defineProps<DialogContentProps>()
15+
16+
const $el = shallowRef<HTMLElement>()
17+
const forwardedRef = forwardRef($el)
18+
19+
const context = useDialogContext('DialogContent')
20+
21+
const isPresent = usePresence($el, () => props.forceMount || context.open.value)
22+
23+
const Comp = context.modal ? DialogContentModal : DialogContentNonModal
24+
</script>
25+
26+
<template>
27+
<Comp
28+
v-if="isPresent"
29+
:ref="forwardedRef"
30+
>
31+
<slot />
32+
</Comp>
33+
</template>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { PrimitiveProps } from '../primitive'
2+
3+
export interface DialogContentImplProps extends PrimitiveProps {
4+
/**
5+
* When `true`, focus cannot escape the `Content` via keyboard,
6+
* pointer, or a programmatic focus.
7+
* @defaultValue false
8+
*/
9+
trapFocus?: boolean
10+
}
11+
12+
// eslint-disable-next-line ts/consistent-type-definitions
13+
export type DialogContentImplEmits = {
14+
/**
15+
* Event handler called when auto-focusing on open.
16+
* Can be prevented.
17+
*/
18+
openAutoFocus: [event: Event]
19+
/**
20+
* Event handler called when auto-focusing on close.
21+
* Can be prevented.
22+
*/
23+
closeAutoFocus: [event: Event]
24+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import { DismissableLayer } from '../dismissable-layer/index.ts'
3+
import { useFocusGuards } from '../focus-guards/index.ts'
4+
import { FocusScope } from '../focus-scope/index.ts'
5+
import { useDialogContext } from './Dialog.ts'
6+
import type { DialogContentImplEmits, DialogContentImplProps } from './DialogContentImpl.ts'
7+
import { getState } from './utils.ts'
8+
9+
defineOptions({
10+
name: 'DialogContentImpl',
11+
})
12+
13+
defineProps<DialogContentImplProps>()
14+
const emit = defineEmits<DialogContentImplEmits>()
15+
16+
const context = useDialogContext()
17+
18+
// Make sure the whole tree has focus guards as our `Dialog` will be
19+
// the last element in the DOM (because of the `Portal`)
20+
useFocusGuards()
21+
</script>
22+
23+
<template>
24+
<FocusScope
25+
as-child
26+
loop
27+
:trapped="trapFocus"
28+
@mount-auto-focus="emit('openAutoFocus', $event)"
29+
@unmount-auto-focus="emit('closeAutoFocus', $event)"
30+
>
31+
<DismissableLayer
32+
:id="context.contentId"
33+
:as="as"
34+
:as-child="asChild"
35+
role="dialog"
36+
:aria-describedby="context.descriptionId"
37+
:aria-labelledby="context.titleId"
38+
:data-state="getState(context.open.value)"
39+
v-bind="$attrs"
40+
@dismiss="context.onOpenChange(false)"
41+
>
42+
<slot />
43+
</DismissableLayer>
44+
</FocusScope>
45+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { DismissableLayerEmits } from '../dismissable-layer/index.ts'
2+
import type { DialogContentImplEmits } from './DialogContentImpl.ts'
3+
4+
// eslint-disable-next-line ts/consistent-type-definitions
5+
export type DialogContentModal = {
6+
closeAutoFocus: DialogContentImplEmits['closeAutoFocus']
7+
8+
pointerdownOutside: DismissableLayerEmits['pointerdownOutside']
9+
focusOutside: DismissableLayerEmits['focusOutside']
10+
}

0 commit comments

Comments
 (0)