Skip to content

Commit

Permalink
refactor: Order logic
Browse files Browse the repository at this point in the history
  • Loading branch information
hoiheart committed Apr 26, 2021
1 parent 2231a55 commit 428436c
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 107 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Universal modal plugin for Vue@3
- [Install plugin](#install-plugin)
* [Options](#options)
- [Usage modal](#usage-modal)
* [v1.0.x -> v1.1.x change point](#v10x----v11x-change-point)
* [props](#props)
+ [props.options](#propsoptions)
* [slot arguments](#slot-arguments)
* [emit events](#emit-events)
- [Handle global CSS](#handle-global-css)
- [Example](#example)
Expand Down Expand Up @@ -145,6 +145,7 @@ export default defineComponent({

> ### v1.0.x -> v1.1.x change point
> * Use `v-model` instead of v-if for modal component insertion
> * If you control the insertion of components with v-if, the close animation will not work.
> * `emitClose` slot argument was deprecated.
### props
Expand Down
32 changes: 24 additions & 8 deletions src/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,19 @@ import { useA11Y, useClose, useOrder } from './hooks'
import type { Provide } from './index'
interface MergeOptions {
transition: number | false;
closeClickDimmed: boolean;
closeKeyCode: number | false;
styleModalContent: { [key: string]: unknown };
}
export default defineComponent({
inheritAttrs: false,
props: {
close: {
type: Function,
default: () => {
return undefined
}
default: () => undefined
},
disabled: {
type: Boolean,
Expand All @@ -66,9 +71,7 @@ export default defineComponent({
},
options: {
type: Object,
default: () => {
return {}
}
default: () => ({})
}
},
emits: [
Expand All @@ -89,6 +92,14 @@ export default defineComponent({
const modalRef = ref(null)
const show = ref(!disabled.value)
const mergeOptions: MergeOptions = {
transition: 300,
closeClickDimmed: true,
closeKeyCode: 27,
styleModalContent: {},
...options.value
}
watch([
() => modelValue.value,
() => disabled.value
Expand All @@ -102,9 +113,14 @@ export default defineComponent({
}
}, { immediate: true })
const { latest } = useOrder({ modelValue, show })
const { mergeOptions, onClickDimmed } = useClose({ close, latest, options })
const { latest } = useOrder({ modalRef, show })
useA11Y({ latest, modalRef, show })
const { onClickDimmed } = useClose({
close,
closeClickDimmed: mergeOptions.closeClickDimmed,
closeKeyCode: mergeOptions.closeKeyCode,
latest
})
const onTransitionEmit = {
beforeEnter: () => context.emit('before-enter'),
Expand Down
166 changes: 78 additions & 88 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,140 +1,130 @@
import { computed, getCurrentInstance, inject, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { computed, inject, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { PLUGIN_NAME, CLASS_NAME } from './index'

import type { ComputedRef, Ref } from 'vue'
import type { Provide } from './index'

type UseOrder = ({ modelValue, show }: {
modelValue: Ref<boolean|undefined>;
type UseA11Y = ({ modalRef, latest, show }: {
modalRef: Ref<null|HTMLElement>;
latest: ComputedRef<boolean>;
show: Ref<boolean>;
}) => void

type UseClose = ({ close, closeKeyCode, latest }: {
close: Ref;
closeClickDimmed: boolean;
closeKeyCode: number | false;
latest: ComputedRef<boolean>;
}) => {
onClickDimmed: () => void
}

type UseOrder = ({ modalRef, show }: {
modalRef: Ref<HTMLElement|null>
show: Ref<boolean>;
}) => {
latest: ComputedRef<boolean>
}
export const useOrder: UseOrder = ({ modelValue, show }) => {
const { visibleModals, addVisibleModals, removeVisibleModals } = inject(PLUGIN_NAME) as Provide
const { uid } = getCurrentInstance() || {}

const latest = computed(() => {
if (!uid || !visibleModals.value.length) return false
return uid === visibleModals.value[visibleModals.value.length - 1]
})
export const useA11Y: UseA11Y = ({ modalRef, latest, show }) => {
let activeElement: Element | null

watch([
() => modelValue.value,
() => show.value
], () => {
if (!uid) {
return
}
function setLastActiveElement (event: Event) {
const isModalEvent = (event.target as Element).closest(`.${CLASS_NAME}`)

const isShow = modelValue.value && show.value
// skip when this not latest modal
if (!latest.value) return

if (isShow && visibleModals.value.indexOf(uid) < 0) {
addVisibleModals(uid)
// set activeElement when fired outside this modal
if (!isModalEvent || (isModalEvent !== modalRef.value)) {
// skip when modal status is closing
if (isModalEvent && !isModalEvent.classList.contains(`${CLASS_NAME}-show`)) {
return
}
activeElement = event.target as Element
}
}

if (!isShow && visibleModals.value.indexOf(uid) > -1) {
removeVisibleModals(uid)
function setFocus (value: boolean) {
if (value) {
if (modalRef.value) {
(modalRef.value as unknown as HTMLElement).focus()
}
} else {
if (activeElement) {
(activeElement as HTMLElement).focus()
}
}
}, { immediate: true })

return {
latest
}
}

type UseColse = ({ close, options, latest }: {
close: Ref;
options: Ref<{ [key: string]: unknown }>;
latest: ComputedRef<boolean>
}) => {
mergeOptions: {
transition: number | false,
closeClickDimmed: boolean,
closeKeyCode: number | false,
styleModalContent: { [key: string]: unknown }
};
onClickDimmed: () => void
onMounted(() => {
document.addEventListener('click', setLastActiveElement)
watch(() => show.value, (value) => {
nextTick(() => setFocus(value))
}, { immediate: show.value })
})

onUnmounted(() => {
document.removeEventListener('click', setLastActiveElement)
})
}
export const useClose: UseColse = ({ close, options, latest }) => {
const mergeOptions = {
transition: 300,
closeClickDimmed: true,
closeKeyCode: 27,
styleModalContent: {},
...options.value
}

export const useClose: UseClose = ({ close, closeClickDimmed, closeKeyCode, latest }) => {
function onClickDimmed () {
if (mergeOptions.closeClickDimmed) {
if (closeClickDimmed) {
close.value()
}
}

function closeKeyEvent (event: KeyboardEvent) {
if (event.keyCode === mergeOptions.closeKeyCode && latest.value) {
if (event.keyCode === closeKeyCode && latest.value) {
close.value()
}
}

onMounted(() => {
if (mergeOptions.closeKeyCode) {
if (closeKeyCode) {
document.addEventListener('keyup', closeKeyEvent)
}
})

onUnmounted(() => {
if (mergeOptions.closeKeyCode) {
if (closeKeyCode) {
document.removeEventListener('keyup', closeKeyEvent)
}
})

return {
mergeOptions,
onClickDimmed
}
}

type UseA11Y = ({ modalRef, latest, show }: {
modalRef: Ref<null|HTMLElement>;
latest: ComputedRef<boolean>;
show: Ref<boolean>;
}) => void
export const useA11Y: UseA11Y = ({ modalRef, latest, show }) => {
let activeElement: Element | null

function setLastActiveElement (event: Event) {
const isModalEvent = (event.target as Element).closest(`.${CLASS_NAME}`)
export const useOrder: UseOrder = ({ modalRef, show }) => {
const { visibleModals, addVisibleModals, removeVisibleModals } = inject(PLUGIN_NAME) as Provide

// skip when this not latest modal
if (!latest.value) return
const latest = computed(() => {
const arr = [...visibleModals.value.values()]

// set activeElement when fired outside this modal
if (!isModalEvent || (isModalEvent !== modalRef.value)) {
// skip when modal status is closing
if (isModalEvent && !isModalEvent.classList.contains(`${CLASS_NAME}-show`)) return
activeElement = event.target as Element
if (!arr.length || !modalRef.value) {
return false
}
}

onMounted(() => {
document.addEventListener('click', setLastActiveElement)
function setFocus (value: boolean) {
if (value) {
if (modalRef.value) {
(modalRef.value as unknown as HTMLElement).focus()
}
return arr[arr.length - 1] === modalRef.value
})

watch(() => show.value, () => {
nextTick(() => {
if (!modalRef.value) return

if (show.value) {
addVisibleModals(modalRef.value)
} else {
if (activeElement) {
(activeElement as HTMLElement).focus()
}
removeVisibleModals(modalRef.value)
}
}
watch(() => show.value, (value) => {
nextTick(() => setFocus(value))
}, { immediate: show.value })
})
})
}, { immediate: true })

onUnmounted(() => {
document.removeEventListener('click', setLastActiveElement)
})
return {
latest
}
}
18 changes: 8 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ interface PluginOptions {
}
interface Provide {
teleportTarget: string,
visibleModals: Ref<number[]>;
addVisibleModals: (id: number) => void
removeVisibleModals: (id: number) => void
visibleModals: Ref<Set<HTMLElement>>;
addVisibleModals: (el: HTMLElement) => void
removeVisibleModals: (el: HTMLElement) => void
}

const PLUGIN_NAME = 'VueUniversalModal'
Expand All @@ -35,14 +35,12 @@ const install: (app: App, options: PluginOptions) => void = (app, options = {})
return console.error('teleportComponent, teleportComponentId was deprecated. use teleportTarget instead. (https://github.com/hoiheart/vue-universal-modal)')
}

const visibleModals: Ref<number[]> = ref([])
const addVisibleModals = (id: number) => {
visibleModals.value = [...visibleModals.value, id]
const visibleModals: Provide['visibleModals'] = ref(new Set())
const addVisibleModals: Provide['addVisibleModals'] = (el) => {
visibleModals.value.add(el)
}
const removeVisibleModals = (id: number) => {
const modals = [...visibleModals.value]
modals.splice(visibleModals.value.indexOf(id), 1)
visibleModals.value = [...modals]
const removeVisibleModals: Provide['removeVisibleModals'] = (el) => {
visibleModals.value.delete(el)
}

app.provide(PLUGIN_NAME, {
Expand Down

0 comments on commit 428436c

Please sign in to comment.