Skip to content

Commit

Permalink
feat: Use v-model for insert component
Browse files Browse the repository at this point in the history
  • Loading branch information
hoiheart committed Apr 23, 2021
1 parent da24bb2 commit 652fad5
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 184 deletions.
5 changes: 1 addition & 4 deletions __tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ describe('Unit test', () => {
transition: false,
closeClickDimmed: false,
closeKeyCode: false,
styleModal: { backgroundColor: 'rgba(255, 255, 0, 0.3)' },
styleModalContent: { justifyContent: 'flex-start' }
}
return {
Expand All @@ -70,11 +69,10 @@ describe('Unit test', () => {
<Modal
id="modal"
class="modal"
ariaLabelledby="title"
aria-labelledby="title"
:close="() => close = true"
:disabled="disabled"
:options="options"
v-slot="{ emitClose }"
>
<h2 id="title">title</h2>
<span class="close-status">{{ close }}</span>
Expand All @@ -96,7 +94,6 @@ describe('Unit test', () => {
expect(modal).toBeTruthy()
expect(modal.attributes('aria-labelledby')).toBe('title')
expect((modal.element as HTMLElement).style.transitionDuration).toBe('false')
expect((modal.element as HTMLElement).style.backgroundColor).toBe('rgba(255, 255, 0, 0.3)')
expect((modal.find('.vue-universal-modal-content').element as HTMLElement).style.justifyContent).toBe('flex-start')
await wrapper.find('.close').trigger('click')
expect(modal.find('.close-status').text()).toBe('true')
Expand Down
213 changes: 72 additions & 141 deletions src/Modal.vue
Original file line number Diff line number Diff line change
@@ -1,216 +1,147 @@
<template>
<teleport
v-if="inserted"
:to="teleportTarget"
:disabled="disabled"
>
<transition
:name="CLASS_NAME"
appear
@before-enter="$emit('before-enter')"
@after-enter="$emit('after-enter')"
@before-leave="$emit('before-leave')"
@after-leave="emitAfterLeave"
:name="CLASS_NAME"
v-on="onTransitionEmit"
>
<div
v-show="show"
:id="id"
ref="modalRef"
role="dialog"
tabindex="-1"
aria-modal="true"
aria-label="Modal window"
:class="[
CLASS_NAME,
className,
{ [`${CLASS_NAME}-show`]: show },
{ [`${CLASS_NAME}-latest`]: latest },
]"
:style="{
transitionDuration: transition,
...mergeOptions.styleModal
}"
role="dialog"
aria-modal="true"
:aria-label="!ariaLabelledby && 'Modal window'"
:aria-labelledby="ariaLabelledby"
tabindex="-1"
:style="{ transitionDuration: transition }"
v-bind="$attrs"
>
<div
:class="`${CLASS_NAME}-content`"
:style="{
transitionDuration: transition,
...mergeOptions.styleModalContent
...mergeOptions?.styleModalContent
}"
@click.self="onClickDimmed"
>
<slot :emitClose="emitClose" />
<slot name="close" />
</div>
</div>
</transition>
</teleport>
</template>
<script lang="ts">
import { defineComponent, inject, getCurrentInstance, ref, watch, computed, onMounted, onUnmounted } from 'vue'
import { defineComponent, inject, ref, toRefs, watch } from 'vue'
import { PLUGIN_NAME, CLASS_NAME } from './index'
import { useA11Y, useClose, useOrder } from './hooks'
import type { Provide } from './index'
interface Options {
transition: number | false;
closeKeyCode: number | false;
closeClickDimmed: boolean;
styleModal: {[key: string]: string};
styleModalContent: {[key: string]: string};
}
export default defineComponent({
inheritAttrs: false,
props: {
close: {
type: Function,
default: () => {
return undefined
}
},
options: {
type: Object,
default: () => {
return {}
}
},
disabled: {
type: Boolean,
default: false
},
id: {
type: String,
default: ''
},
class: {
type: String,
default: ''
modelValue: {
type: Boolean,
default: true
},
ariaLabelledby: {
type: String,
default: ''
options: {
type: Object,
default: () => {
return {}
}
}
},
emits: [
'before-enter',
'enter',
'after-enter',
'enter-cancelled',
'before-leave',
'after-leave'
'leave',
'after-leave',
'leave-cancelled'
],
setup (props, context) {
const { teleportTarget, visibleModals, addVisibleModals, removeVisibleModals } = inject(PLUGIN_NAME) as Provide
const { uid } = getCurrentInstance() || {}
const modalRef = ref()
const show = ref()
const latest = computed(() => {
if (!uid || !visibleModals.value.length) return false
return uid === visibleModals.value[visibleModals.value.length - 1]
})
const { teleportTarget } = inject(PLUGIN_NAME) as Provide
const { close, disabled, options, modelValue } = toRefs(props)
watch(() => props.disabled, () => {
show.value = !props.disabled
}, { immediate: true })
const inserted = ref(modelValue.value === undefined ? true : modelValue.value)
const modalRef = ref(null)
const show = ref(!disabled.value)
watch(() => show.value, (value) => {
if (!uid) return
watch([
() => modelValue.value,
() => disabled.value
], () => {
const isShow = modelValue.value && !disabled.value
if (value && visibleModals.value.indexOf(uid) < 0) {
addVisibleModals(uid)
}
show.value = isShow
if (!value && visibleModals.value.indexOf(uid) > -1) {
removeVisibleModals(uid)
if (modelValue.value) {
inserted.value = modelValue.value
}
}, { immediate: true })
const mergeOptions = {
transition: 300,
closeClickDimmed: true,
closeKeyCode: 27,
styleModal: {},
styleModalContent: {},
...props.options
} as Options
const transition = mergeOptions.transition ? mergeOptions.transition / 1000 + 's' : false
function emitClose () {
show.value = false
}
function emitAfterLeave () {
context.emit('after-leave')
props.close()
}
function onClickDimmed () {
if (mergeOptions.closeClickDimmed) {
emitClose()
}
}
function closeKeyEvent (event: KeyboardEvent) {
if (event.keyCode === mergeOptions.closeKeyCode && latest.value) {
emitClose()
}
const { latest } = useOrder({ modelValue, show })
const { mergeOptions, onClickDimmed } = useClose({ close, latest, options })
useA11Y({ latest, modalRef, show })
const onTransitionEmit = {
beforeEnter: () => context.emit('before-enter'),
enter: () => context.emit('enter'),
afterEnter: () => context.emit('after-enter'),
enterCancelled: () => context.emit('enter-cancelled'),
beforeLeave: () => context.emit('before-leave'),
leave: () => context.emit('leave'),
afterLeave: () => {
context.emit('after-leave')
if (modelValue.value === false) {
inserted.value = false
}
},
leaveCancelled: () => context.emit('leave-cancelled')
}
// wai-aria
let activeElement: Element | null
function setLastActiveElement (event: Event) {
const isModalEvent = (event.target as Element).closest(`.${CLASS_NAME}`)
// skip when this not latest modal
if (!latest.value) return
// 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
}
/**
* @deprecated
*/
const emitClose = () => {
console.warn('emitClose was deprecated.\nhttps://github.com/hoiheart/vue-universal-modal#usage-modal')
close.value()
}
onMounted(() => {
if (mergeOptions.closeKeyCode) {
document.addEventListener('keyup', closeKeyEvent)
}
// wai-aria
document.addEventListener('click', setLastActiveElement)
function setFocus (value: boolean) {
if (value) {
if (modalRef.value) {
(modalRef.value as unknown as HTMLElement).focus()
}
} else {
if (activeElement) {
(activeElement as HTMLElement).focus()
}
}
}
watch(() => show.value, (value) => {
setFocus(value)
}, { immediate: show.value })
})
onUnmounted(() => {
if (mergeOptions.closeKeyCode) {
document.removeEventListener('keyup', closeKeyEvent)
}
// wai-aria
document.removeEventListener('click', setLastActiveElement)
})
return {
CLASS_NAME,
teleportTarget,
modalRef,
show,
latest,
emitClose,
emitAfterLeave,
onClickDimmed,
inserted,
latest,
mergeOptions,
transition,
className: props.class
modalRef,
onClickDimmed,
onTransitionEmit,
show,
teleportTarget,
transition: mergeOptions.transition ? mergeOptions.transition / 1000 + 's' : false
}
}
})
Expand Down

0 comments on commit 652fad5

Please sign in to comment.