Skip to content
This repository was archived by the owner on Sep 20, 2024. It is now read-only.

Commit ef58bfe

Browse files
committed
feat(wip): useDialogTransitions for dialog elements
1 parent 161478b commit ef58bfe

File tree

5 files changed

+309
-29
lines changed

5 files changed

+309
-29
lines changed

packages/c-modal/examples/base-c-modal.vue renamed to packages/c-modal/examples/simple-modal.vue

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
w="100%"
88
>
99
<c-button color-scheme="blue" @click="isOpen = true">Open modal</c-button>
10-
<c-button ml="3">Other button</c-button>
10+
<c-button ref="finalFocus" ml="3">Other button</c-button>
1111
<!-- eslint-disable-next-line -->
12-
<c-modal v-model:is-open="isOpen">
12+
<c-modal v-model:is-open="isOpen" :initial-focus-ref="'#initialFocus'" :final-focus-ref="() => $refs.finalFocus">
1313
<c-modal-overlay />
1414
<c-modal-content>
1515
<c-modal-header>Modal header</c-modal-header>
@@ -24,7 +24,9 @@
2424

2525
<c-modal-footer>
2626
<c-button @click="isOpen = false" mr="3"> Close </c-button>
27-
<c-button>Secondary action</c-button>
27+
<c-button id="initialFocus" ref="initialFocus"
28+
>Secondary action</c-button
29+
>
2830
</c-modal-footer>
2931
</c-modal-content>
3032
</c-modal>
@@ -34,4 +36,6 @@
3436
<script setup lang="ts">
3537
import { ref } from 'vue'
3638
const isOpen = ref(false)
39+
const finalFocus = ref()
40+
const initialFocus = ref()
3741
</script>

packages/c-modal/src/c-modal.ts

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ import {
2020
mergeProps,
2121
UnwrapRef,
2222
watch,
23-
withDirectives,
2423
unref,
25-
watchEffect,
24+
getCurrentInstance,
25+
Transition,
26+
ref,
27+
nextTick,
28+
onMounted,
29+
withDirectives,
30+
WatchStopHandle,
2631
} from 'vue'
2732
import {
2833
chakra,
@@ -31,15 +36,20 @@ import {
3136
useMultiStyleConfig,
3237
useStyles,
3338
} from '@chakra-ui/vue-system'
34-
import { createContext, TemplateRef, useRef } from '@chakra-ui/vue-utils'
39+
import {
40+
createContext,
41+
getSelector,
42+
TemplateRef,
43+
useRef,
44+
} from '@chakra-ui/vue-utils'
3545
import { CPortal } from '@chakra-ui/c-portal'
36-
import { CFocusLock, FocusLockProps } from '@chakra-ui/c-focus-lock'
37-
import { CScrollLock, BodyScrollLockDirective } from '@chakra-ui/c-scroll-lock'
46+
import { FocusLockProps } from '@chakra-ui/c-focus-lock'
3847
import { CMotion } from '@chakra-ui/c-motion'
3948
import { CCloseButton } from '@chakra-ui/c-close-button'
40-
import { MotionDirective } from '@vueuse/motion'
49+
import { MotionDirective, useMotion } from '@vueuse/motion'
4150

4251
import { useModal, UseModalOptions, UseModalReturn } from './use-modal'
52+
import { useDialogTransition, useDialogTransitions } from './modal-transitions'
4353

4454
type ScrollBehavior = 'inside' | 'outside'
4555
type MotionPreset = 'slideInBottom' | 'slideInRight' | 'scale' | 'none'
@@ -211,8 +221,19 @@ export const CModal = defineComponent({
211221
},
212222
emits: ['update:is-open', 'escape', 'close'],
213223
setup(props, { slots, attrs, emit }) {
224+
const isOpen = computed(() => props.isOpen)
225+
const { localIsOpen, transitionsExited } = useDialogTransitions(isOpen, {
226+
onChildrenEntered: () => {
227+
console.log('========= TRANSITIONS ENTER ================')
228+
},
229+
onChildrenLeft: () => {
230+
console.log('========= TRANSITIONS LEFT ================')
231+
emit('update:is-open', false)
232+
},
233+
})
234+
214235
const closeModal = () => {
215-
emit('update:is-open', false)
236+
localIsOpen.value = false
216237
}
217238

218239
const handleEscape = (event: KeyboardEvent) => {
@@ -239,7 +260,7 @@ export const CModal = defineComponent({
239260
StylesProvider(styles)
240261
return () =>
241262
h(CPortal, () => [
242-
h(CMotion, () => [
263+
h(CMotion, { type: 'fade' }, () => [
243264
props.isOpen && h(chakra('span'), () => slots?.default?.()),
244265
]),
245266
])
@@ -255,9 +276,14 @@ export const CModalContent = defineComponent({
255276
inheritAttrs: false,
256277
emits: ['click', 'mousedown', 'keydown'],
257278
setup(_, { attrs, slots, emit }) {
258-
const { dialogContainerProps, dialogProps } = unref(useModalContext())
279+
const { dialogContainerProps, dialogProps, dialogRefEl } = unref(
280+
useModalContext()
281+
)
259282
const styles = useStyles()
260283

284+
const { localIsOpen, register } = unref(useDialogTransition())
285+
const modalContentTransition = register('modal-content')
286+
261287
const dialogContainerStyles = computed<SystemStyleObject>(() => ({
262288
display: 'flex',
263289
width: '100vw',
@@ -277,6 +303,17 @@ export const CModalContent = defineComponent({
277303
...styles.value.dialog,
278304
}))
279305

306+
watch(
307+
modalContentTransition,
308+
(content) => {
309+
console.log('modalContentTransition state changed to:', content.status)
310+
},
311+
{
312+
immediate: true,
313+
deep: true,
314+
}
315+
)
316+
280317
return () => {
281318
return h(
282319
chakra('div', {
@@ -286,15 +323,64 @@ export const CModalContent = defineComponent({
286323
dialogContainerProps.value({ emit }),
287324
() => [
288325
h(
289-
chakra('section', {
290-
__css: dialogStyles.value,
291-
label: 'modal__content',
292-
}),
326+
Transition,
293327
{
294-
...attrs,
295-
...dialogProps.value({ emit }),
328+
css: false,
329+
onBeforeEnter: () => {
330+
modalContentTransition.value.status = 'active'
331+
console.log('onBeforeEnter', 'active')
332+
},
333+
onAfterEnter: () => {
334+
modalContentTransition.value.status = 'entered'
335+
console.log('onAfterEnter', 'entered')
336+
},
337+
onBeforeLeave: () => {
338+
modalContentTransition.value.status = 'active'
339+
console.log('onAfterLeave', 'active')
340+
},
341+
onLeave: (el, done) => {
342+
modalContentTransition.value.status = 'exited'
343+
console.log('onAfterLeave', 'exited')
344+
done()
345+
},
296346
},
297-
slots
347+
() => [
348+
localIsOpen.value &&
349+
withDirectives(
350+
h(
351+
chakra('section', {
352+
__css: dialogStyles.value,
353+
label: 'modal__content',
354+
}),
355+
{
356+
...attrs,
357+
...dialogProps.value({ emit }),
358+
},
359+
slots
360+
),
361+
[
362+
[
363+
MotionDirective({
364+
initial: {
365+
scale: 0.8,
366+
opacity: 0,
367+
translateY: 10,
368+
},
369+
enter: {
370+
scale: 1,
371+
opacity: 1,
372+
translateY: 0,
373+
},
374+
leave: {
375+
scale: 0.8,
376+
opacity: 0,
377+
translateY: 10,
378+
},
379+
}),
380+
],
381+
]
382+
),
383+
]
298384
),
299385
]
300386
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useId } from '@chakra-ui/vue-composables'
2+
import { createContext } from '@chakra-ui/vue-utils'
3+
import {
4+
computed,
5+
ComputedRef,
6+
reactive,
7+
ref,
8+
Ref,
9+
watch,
10+
watchEffect,
11+
} from 'vue'
12+
13+
export type DialogTransitionStatus = 'exited' | 'entered' | 'initial' | 'active'
14+
15+
export interface TransitionInstance {
16+
id: number | string
17+
isActive: boolean
18+
el?: HTMLElement
19+
status: DialogTransitionStatus
20+
}
21+
22+
export interface DialogTransitionContext {
23+
/**
24+
* Local state transitioning state
25+
*/
26+
localIsOpen: Ref<boolean>
27+
/**
28+
* Reflects whether all children are currently transitioning
29+
*/
30+
isTransitioning: ComputedRef<boolean>
31+
/**
32+
* Reflects whether all children are currently transitioning in
33+
*/
34+
isTransitioningIn: ComputedRef<boolean>
35+
/**
36+
* Reflects whether all children are currently transitioning out
37+
*/
38+
isTransitioningOut: ComputedRef<boolean>
39+
/**
40+
* Registers a new transition to set of transitions
41+
*/
42+
register: (uid: number | string) => Ref<TransitionInstance>
43+
}
44+
45+
export interface DialogTransitionsOptions {
46+
/**
47+
* Callback invoked when all children have finished transitioning in
48+
*/
49+
onChildrenEntered: () => void
50+
/**
51+
* Callback invoked when all children have finished transitioning out
52+
*/
53+
onChildrenLeft: () => void
54+
}
55+
56+
const [DialogTransitionsProvider, useDialogTransition] = createContext<
57+
ComputedRef<DialogTransitionContext>
58+
>({
59+
strict: true,
60+
name: 'DialogTransitionContext',
61+
errorMessage:
62+
'useDialogTransition: `context` is undefined. Seems you forgot to wrap modal components in `<CModal />`, `<CDrawer />` or `<CAlertDialog />',
63+
})
64+
65+
/**
66+
* Hook used to manage all transitions in a compound component context
67+
* used by CModal, CDrawer and CAlertDialog
68+
*/
69+
export function useDialogTransitions(
70+
isOpen: Ref<boolean> | ComputedRef<boolean>,
71+
{ onChildrenEntered, onChildrenLeft }: DialogTransitionsOptions
72+
) {
73+
const localIsOpen = ref(false)
74+
const transitions = ref<Ref<TransitionInstance>[]>([])
75+
76+
const isTransitioning = computed(() =>
77+
transitions.value.find((transition) =>
78+
['entered', 'exited'].includes(transition.value.status)
79+
)?.value
80+
? true
81+
: false
82+
)
83+
84+
const transitionsStore = reactive({})
85+
86+
const transitionsEntered = computed(() =>
87+
transitions.value.every(
88+
(transition) => transition.value.status === 'entered'
89+
)
90+
)
91+
92+
const transitionsExited = computed(() =>
93+
transitions.value.every(
94+
(transition) => transition.value.status === 'exited'
95+
)
96+
)
97+
98+
const isTransitioningIn = computed(
99+
() => (isOpen.value && !!isTransitioning.value) as boolean
100+
)
101+
102+
const isTransitioningOut = computed(
103+
() => (!isOpen.value && !!isTransitioning.value) as boolean
104+
)
105+
106+
const register: DialogTransitionContext['register'] = (uid) => {
107+
const transition = ref<TransitionInstance>({
108+
id: uid,
109+
isActive: false,
110+
el: undefined, // TODO May include this later
111+
status: 'initial',
112+
})
113+
114+
if (transitionsStore[uid]) {
115+
return transitionsStore[uid]
116+
} else {
117+
transitions.value.push(transition)
118+
transitionsStore[uid] = transition
119+
}
120+
return transition
121+
}
122+
123+
watchEffect(
124+
() => {
125+
if (!isOpen.value) return
126+
else if (!localIsOpen.value) {
127+
localIsOpen.value = true
128+
}
129+
130+
if (transitionsEntered.value) {
131+
onChildrenEntered()
132+
}
133+
134+
if (transitionsExited.value) {
135+
onChildrenLeft()
136+
}
137+
},
138+
{
139+
flush: 'post',
140+
}
141+
)
142+
143+
const dialogTransitionsContext = computed(() => ({
144+
localIsOpen,
145+
isTransitioning,
146+
isTransitioningIn,
147+
isTransitioningOut,
148+
register,
149+
}))
150+
151+
DialogTransitionsProvider(dialogTransitionsContext)
152+
153+
return {
154+
localIsOpen,
155+
isTransitioning,
156+
isTransitioningIn,
157+
isTransitioningOut,
158+
register,
159+
transitionsEntered,
160+
transitionsExited,
161+
}
162+
}
163+
164+
export { useDialogTransition }

0 commit comments

Comments
 (0)