Skip to content

Commit dafa559

Browse files
committed
feat: add overlayBehavior prop to VueFinalModal #405
1 parent 98a1e6f commit dafa559

File tree

15 files changed

+644
-334
lines changed

15 files changed

+644
-334
lines changed

packages/vue-final-modal/src/Modal.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,10 @@ export type Vfm = {
6464
closeAll: () => Promise<PromiseSettledResult<string>[]>
6565
}
6666

67-
export type InternalVfm = {
68-
openLastOverlay: () => Promise<void>
69-
moveToLastOpenedModals: (modal: ComputedRef<Modal>) => void
70-
deleteFromOpenedModals: (modal: ComputedRef<Modal>) => void
71-
moveToLastOpenedModalOverlays: (modal: ComputedRef<Modal>) => void
72-
deleteFromOpenedModalOverlays: (modal: ComputedRef<Modal>) => void
73-
deleteFromModals: (modal: ComputedRef<Modal>) => void
74-
resolvedClosed: (index: number) => void
75-
resolvedOpened: (index: number) => void
76-
}
77-
7867
export type Modal = {
7968
modalId?: ModalId
8069
hideOverlay: Ref<boolean | undefined> | undefined
70+
overlayBehavior: Ref<'auto' | 'persist'>
8171
overlayVisible: Ref<boolean>
8272
toggle: (show?: boolean) => Promise<string>
8373
}

packages/vue-final-modal/src/components/ModalsContainer.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<script setup lang="ts">
22
import { computed, onBeforeUnmount } from 'vue'
33
import { isString } from '~/utils'
4-
import { useInternalVfm, useVfm } from '~/useApi'
4+
import { useVfm } from '~/useApi'
55
66
const { modalsContainers, dynamicModals } = useVfm()
7-
const { resolvedClosed, resolvedOpened } = useInternalVfm()
87
98
const uid = Symbol('ModalsContainer')
109
const shouldMount = computed(() => uid === modalsContainers.value?.[0])
@@ -13,6 +12,16 @@ modalsContainers.value.push(uid)
1312
onBeforeUnmount(() => {
1413
modalsContainers.value = modalsContainers.value.filter(i => i !== uid)
1514
})
15+
16+
function resolvedClosed(index: number) {
17+
dynamicModals[index]?.resolveClosed?.()
18+
if (!dynamicModals[index]?.keepAlive)
19+
dynamicModals.splice(index, 1)
20+
}
21+
22+
function resolvedOpened(index: number) {
23+
dynamicModals[index]?.resolveOpened?.()
24+
}
1625
</script>
1726

1827
<template>

packages/vue-final-modal/src/components/VueFinalModal/VueFinalModal.vue

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { useFocusTrap } from './useFocusTrap'
88
import { useLockScroll } from './useBodyScrollLock'
99
import { useZIndex } from './useZIndex'
1010
import { vVisible } from './vVisible'
11-
import { noop, once } from '~/utils'
11+
import { arrayMoveItemToLast, arrayRemoveItem, noop, once } from '~/utils'
1212
import { type Modal } from '~/Modal'
1313
import { useSwipeToClose } from '~/useSwipeToClose'
14-
import { useInternalVfm, useVfm } from '~/useApi'
14+
import { useVfm } from '~/useApi'
1515
1616
export interface VueFinalModalEmits {
1717
(e: 'update:modelValue', modelValue: boolean): void
@@ -26,30 +26,17 @@ export interface VueFinalModalEmits {
2626
}
2727
2828
const props = defineProps(vueFinalModalProps)
29-
3029
const emit = defineEmits<VueFinalModalEmits>()
31-
3230
const attrs = useAttrs()
3331
34-
defineOptions({
35-
inheritAttrs: false,
36-
})
32+
defineOptions({ inheritAttrs: false })
3733
3834
defineSlots<{
3935
'default'(): void
4036
'swipe-banner'(): void
4137
}>()
4238
43-
const { modals, openedModals } = useVfm()
44-
45-
const {
46-
openLastOverlay,
47-
moveToLastOpenedModals,
48-
deleteFromOpenedModals,
49-
moveToLastOpenedModalOverlays,
50-
deleteFromOpenedModalOverlays,
51-
deleteFromModals,
52-
} = useInternalVfm()
39+
const { modals, openedModals, openedModalOverlays } = useVfm()
5340
5441
const vfmRootEl = ref<HTMLDivElement>()
5542
const vfmContentEl = ref<HTMLDivElement>()
@@ -90,7 +77,7 @@ const {
9077
resolveToggle('opened')
9178
},
9279
onLeave() {
93-
deleteFromOpenedModals(getModalInstance())
80+
arrayRemoveItem(openedModals, modalInstance)
9481
resetZIndex()
9582
enableBodyScroll()
9683
emit('closed')
@@ -107,35 +94,32 @@ const {
10794
} = useSwipeToClose(props, { vfmContentEl, modelValueLocal })
10895
10996
const hideOverlay = toRef(props, 'hideOverlay')
97+
const overlayBehavior = toRef(props, 'overlayBehavior')
11098
const modalInstance = computed<Modal>(() => ({
11199
modalId: props.modalId,
112100
hideOverlay,
101+
overlayBehavior,
113102
overlayVisible,
114-
focus,
115103
toggle(show?: boolean): Promise<string> {
116104
return new Promise((resolve) => {
117105
resolveToggle = once((res: string) => resolve(res))
118106
119107
const value = typeof show === 'boolean' ? show : !modelValueLocal.value
120108
modelValueLocal.value = value
121-
emit('update:modelValue', value)
122109
})
123110
},
124111
}))
125112
126-
function getModalInstance() {
127-
return modalInstance
128-
}
129-
130113
const index = computed(() => openedModals.indexOf(modalInstance))
131114
132115
watch([() => props.zIndexFn, index], () => {
133-
if (visible.value)
134-
refreshZIndex(index.value)
116+
if (!visible.value)
117+
return
118+
refreshZIndex(index.value)
135119
})
136120
137121
onMounted(() => {
138-
modals.push(modalInstance)
122+
arrayMoveItemToLast(modals, modalInstance)
139123
})
140124
141125
if (props.modelValue)
@@ -146,9 +130,8 @@ function open(): boolean {
146130
emit('beforeOpen', { stop: () => shouldStop = true })
147131
if (shouldStop)
148132
return false
149-
moveToLastOpenedModals(modalInstance)
150-
moveToLastOpenedModalOverlays(modalInstance)
151-
refreshZIndex(index.value)
133+
arrayMoveItemToLast(openedModals, modalInstance)
134+
arrayMoveItemToLast(openedModalOverlays, modalInstance)
152135
openLastOverlay()
153136
enterTransition()
154137
return true
@@ -159,7 +142,7 @@ function close(): boolean {
159142
emit('beforeClose', { stop: () => shouldStop = true })
160143
if (shouldStop)
161144
return false
162-
deleteFromOpenedModalOverlays(getModalInstance())
145+
arrayRemoveItem(openedModalOverlays, modalInstance)
163146
openLastOverlay()
164147
blur()
165148
leaveTransition()
@@ -168,12 +151,21 @@ function close(): boolean {
168151
169152
onBeforeUnmount(() => {
170153
enableBodyScroll()
171-
deleteFromModals(modalInstance)
172-
deleteFromOpenedModals(modalInstance)
173-
deleteFromOpenedModalOverlays(modalInstance)
154+
arrayRemoveItem(modals, modalInstance)
155+
arrayRemoveItem(openedModals, modalInstance)
174156
blur()
175157
openLastOverlay()
176158
})
159+
160+
async function openLastOverlay() {
161+
await nextTick()
162+
// Found the modals which has overlay and has `auto` overlayBehavior
163+
const openedModalsOverlaysAuto = openedModalOverlays.filter(modal => modal.value.overlayBehavior.value === 'auto' && !modal.value.hideOverlay?.value)
164+
// Only keep the last overlay open
165+
openedModalsOverlaysAuto.forEach((modal, index) => {
166+
modal.value.overlayVisible.value = index === openedModalsOverlaysAuto.length - 1
167+
})
168+
}
177169
</script>
178170

179171
<template>
@@ -193,7 +185,7 @@ onBeforeUnmount(() => {
193185
@mouseup.self="() => onMouseupRoot()"
194186
@mousedown.self="e => onMousedown(e)"
195187
>
196-
<Transition v-if="!hideOverlay" v-bind="overlayTransition" :appear="true" v-on="overlayListeners">
188+
<Transition v-if="!hideOverlay" v-bind="(overlayTransition as object)" v-on="overlayListeners">
197189
<div
198190
v-if="displayDirective !== 'if' || overlayVisible"
199191
v-show="displayDirective !== 'show' || overlayVisible"
@@ -204,7 +196,7 @@ onBeforeUnmount(() => {
204196
aria-hidden="true"
205197
/>
206198
</Transition>
207-
<Transition v-bind="contentTransition" :appear="true" v-on="contentListeners">
199+
<Transition v-bind="(contentTransition as object)" v-on="contentListeners">
208200
<div
209201
v-if="displayDirective !== 'if' || contentVisible"
210202
v-show="displayDirective !== 'show' || contentVisible"

packages/vue-final-modal/src/components/VueFinalModal/VueFinalModalProps.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export const vueFinalModalProps = {
7272
type: Boolean as PropType<boolean>,
7373
default: undefined,
7474
},
75+
/**
76+
* @description Customize the overlay behavior.
77+
*/
78+
overlayBehavior: {
79+
type: String as PropType<'auto' | 'persist'>,
80+
default: 'auto',
81+
validator: (prop: any) => ['auto', 'persist'].includes(prop),
82+
},
7583
/**
7684
* @description Customize the overlay transition.
7785
* @default `undefined`

packages/vue-final-modal/src/components/VueFinalModal/useTransition.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ export function useTransition(
5858

5959
const contentTransition = computed<TransitionProps>(() => {
6060
if (typeof props.contentTransition === 'string')
61-
return { name: props.contentTransition }
62-
return { ...props.contentTransition }
61+
return { name: props.contentTransition, appear: true }
62+
return { appear: true, ...props.contentTransition }
6363
})
6464

6565
const overlayTransition = computed<TransitionProps>(() => {
6666
if (typeof props.overlayTransition === 'string')
67-
return { name: props.overlayTransition }
68-
return { ...props.overlayTransition }
67+
return { name: props.overlayTransition, appear: true }
68+
return { appear: true, ...props.overlayTransition }
6969
})
7070

7171
const isReadyToBeDestroyed = computed(() =>
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { InjectionKey } from 'vue'
2-
import type { InternalVfm, Vfm } from './Modal'
2+
import type { Vfm } from './Modal'
33

44
export const vfmSymbol = Symbol(__DEV__ ? 'vfm' : '') as InjectionKey<Vfm>
5-
export const internalVfmSymbol = Symbol(__DEV__ ? 'internalVfm' : '') as InjectionKey<InternalVfm>
Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { App, ComputedRef } from 'vue'
2-
import { getCurrentInstance, inject, markRaw, nextTick, ref, shallowReactive } from 'vue'
3-
import { internalVfmSymbol, vfmSymbol } from './injectionSymbols'
4-
import type { InternalVfm, Modal, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal'
2+
import { getCurrentInstance, inject, markRaw, ref, shallowReactive } from 'vue'
3+
import { vfmSymbol } from './injectionSymbols'
4+
import type { Modal, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal'
55
import { noop } from './utils'
66

77
// eslint-disable-next-line import/no-mutable-exports
@@ -38,9 +38,6 @@ export function createVfm() {
3838
install(app: App) {
3939
app.provide(vfmSymbol, vfm)
4040
app.config.globalProperties.$vfm = vfm
41-
42-
const internalVfm = createInternalVfm(vfm)
43-
app.provide(internalVfmSymbol, internalVfm)
4441
},
4542
modals,
4643
openedModals,
@@ -69,53 +66,3 @@ export function createVfm() {
6966

7067
return vfm
7168
}
72-
73-
function createInternalVfm(vfm: Vfm) {
74-
const { modals, openedModals, openedModalOverlays, dynamicModals } = vfm
75-
76-
const internalVfm: InternalVfm = {
77-
deleteFromModals(modal: ComputedRef<Modal>) {
78-
const index = modals.findIndex(_modal => _modal.value === modal.value)
79-
if (index !== -1)
80-
modals.splice(index, 1)
81-
},
82-
moveToLastOpenedModals(modal: ComputedRef<Modal>) {
83-
internalVfm.deleteFromOpenedModals(modal)
84-
openedModals.push(modal)
85-
},
86-
deleteFromOpenedModals(modal: ComputedRef<Modal>) {
87-
const index = openedModals.findIndex(_modal => _modal.value === modal.value)
88-
if (index !== -1)
89-
openedModals.splice(index, 1)
90-
},
91-
moveToLastOpenedModalOverlays(modal: ComputedRef<Modal>) {
92-
internalVfm.deleteFromOpenedModalOverlays(modal)
93-
openedModalOverlays.push(modal)
94-
},
95-
deleteFromOpenedModalOverlays(modal: ComputedRef<Modal>) {
96-
const index = openedModalOverlays.findIndex(_modal => _modal.value === modal.value)
97-
if (index !== -1)
98-
openedModalOverlays.splice(index, 1)
99-
},
100-
async openLastOverlay() {
101-
await nextTick()
102-
// Close all overlay first
103-
openedModalOverlays.forEach(modal => modal.value.overlayVisible.value = false)
104-
// Open the last overlay if it has overlay
105-
if (openedModalOverlays.length > 0) {
106-
const modal = openedModalOverlays[openedModalOverlays.length - 1]
107-
!modal.value.hideOverlay?.value && (modal.value.overlayVisible.value = true)
108-
}
109-
},
110-
resolvedClosed(index: number) {
111-
dynamicModals[index]?.resolveClosed?.()
112-
if (!dynamicModals[index]?.keepAlive)
113-
dynamicModals.splice(index, 1)
114-
},
115-
resolvedOpened(index: number) {
116-
dynamicModals[index]?.resolveOpened?.()
117-
},
118-
}
119-
120-
return internalVfm
121-
}

packages/vue-final-modal/src/useApi.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { computed, inject, markRaw, nextTick, reactive, useAttrs } from 'vue'
1+
import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue'
22
import { tryOnUnmounted } from '@vueuse/core'
33
import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue'
4-
import type { ComponentProps, ComponentType, InternalVfm, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
4+
import type { ComponentProps, ComponentType, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
55
import { activeVfm, getActiveVfm } from './plugin'
6-
import { internalVfmSymbol } from './injectionSymbols'
7-
import { isString, noop, noopPromise } from '~/utils'
6+
import { isString } from '~/utils'
87

98
/**
109
* Returns the vfm instance. Equivalent to using `$vfm` inside
@@ -24,21 +23,6 @@ export function useVfm(): Vfm {
2423
return vfm!
2524
}
2625

27-
export const defaultInternalVfm: InternalVfm = {
28-
openLastOverlay: noopPromise,
29-
moveToLastOpenedModals: noop,
30-
deleteFromOpenedModals: noop,
31-
moveToLastOpenedModalOverlays: noop,
32-
deleteFromOpenedModalOverlays: noop,
33-
deleteFromModals: noop,
34-
resolvedClosed: noop,
35-
resolvedOpened: noop,
36-
}
37-
38-
export function useInternalVfm() {
39-
return inject(internalVfmSymbol, defaultInternalVfm)
40-
}
41-
4226
function withMarkRaw<T extends ComponentType>(options: Partial<UseModalOptions<T>>, DefaultComponent: ComponentType = VueFinalModal) {
4327
const { component, slots: innerSlots, ...rest } = options
4428

packages/vue-final-modal/src/utils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,26 @@ export const once
88
}
99

1010
export const noop = () => {}
11-
export const noopPromise = async () => {}
1211

1312
export function clamp(val: number, min: number, max: number) {
1413
return val > max ? max : val < min ? min : val
1514
}
1615

1716
export const isString = (value: unknown): value is string => typeof value === 'string'
17+
18+
/**
19+
* @example
20+
* const arr = [1, 2, 6, 3, 4, 5]
21+
* arrayMoveItemToLast(arr, 6)
22+
* console.log(arr) // [1, 2, 3, 4, 5, 6]
23+
*/
24+
export function arrayMoveItemToLast<T>(arr: T[], item: T) {
25+
const removedItem = arrayRemoveItem(arr, item)?.[0] || item
26+
arr.push(removedItem)
27+
}
28+
29+
export function arrayRemoveItem<T>(arr: T[], item: T) {
30+
const index = arr.indexOf(item)
31+
if (index !== -1)
32+
return arr.splice(index, 1)
33+
}

0 commit comments

Comments
 (0)