### feat: add dialog (#197)

* Add overlay component; Dialog component almost done

* feat(dialog): add use-lockscreen

* feat(dialog): coding completed awaiting tests

* feat(dialog): finish writing test cases

* fix test failures

* Address PR comments

* fallback some changes
This commit is contained in:
jeremywu 2020-09-09 21:18:08 +08:00 committed by GitHub
parent 90ff286ac0
commit ef92b6c11c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1005 additions and 25 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ yarn-error.log
storybook-static
coverage/
website-dist
website/play/

View File

@ -0,0 +1,187 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import Dialog from '../src/index'
const AXIOM = 'Rem is the best girl'
const _mount = ({
slots,
...rest
}: Indexable<any>) => {
return mount(Dialog, {
slots: {
default: AXIOM,
...slots,
},
...rest,
})
}
jest.useFakeTimers()
describe('Dialog.vue', () => {
test('render test', () => {
const wrapper = _mount({
slots: {
default: AXIOM,
},
})
expect(wrapper.text()).toEqual(AXIOM)
})
test('dialog should have a title when title has been given', () => {
const HEADER = 'I am header'
let wrapper = _mount({
slots: {
header: HEADER,
},
})
expect(wrapper.find('.el-dialog__header').text()).toBe(HEADER)
wrapper = _mount({
props: {
title: HEADER,
},
})
expect(wrapper.find('.el-dialog__header').text()).toBe(HEADER)
})
test('dialog should have a footer when footer has been given', () => {
const wrapper = _mount({
slots: {
footer: AXIOM,
},
})
expect(wrapper.find('.el-dialog__footer').exists()).toBe(true)
expect(wrapper.find('.el-dialog__footer').text()).toBe(AXIOM)
})
test('should append dialog to body when appendToBody is true', () => {
const wrapper = _mount({
props: {
appendToBody: true,
},
})
expect(document.body.firstElementChild.classList.contains('el-overlay')).toBe(true)
wrapper.unmount()
})
test('should center dialog', () => {
const wrapper = _mount({
props: {
center: true,
},
})
expect(wrapper.find('.el-dialog--center').exists()).toBe(true)
})
test('should show close button', () => {
const wrapper = _mount({})
expect(wrapper.find('.el-dialog__close').exists()).toBe(true)
})
test('should close dialog when click on close button', async () => {
const wrapper = _mount({
props: {
modelValue: true,
},
})
await wrapper.find('.el-dialog__headerbtn').trigger('click')
expect(wrapper.vm.visible).toBe(false)
})
describe('mask related', () => {
test('should not have overlay mask when mask is false', () => {
const wrapper = _mount({
props: {
mask: false,
},
})
expect(wrapper.find('.el-overlay').exists()).toBe(false)
})
test('should close the modal when clicking on mask when `closeOnClickModal` is true', async () => {
const wrapper = _mount({})
expect(wrapper.find('.el-overlay').exists()).toBe(true)
await wrapper.find('.el-overlay').trigger('click')
expect(wrapper.vm.visible).toBe(false)
})
})
describe('life cycles', () => {
test('should call before close', async () => {
const beforeClose = jest.fn()
const wrapper = _mount({
props: {
beforeClose,
},
})
wrapper.vm.handleClose()
expect(beforeClose).toHaveBeenCalled()
})
test('should not close dialog when user cancelled', () => {
const beforeClose = jest.fn().mockImplementation((hide: (cancel: boolean) => void) => hide(true))
const wrapper = _mount({
props: {
beforeClose,
modelValue: true,
},
})
wrapper.vm.handleClose()
expect(beforeClose).toHaveBeenCalled()
expect(wrapper.vm.visible).toBe(true)
})
test('should open and close with delay', async () => {
const wrapper = _mount({
props: {
openDelay: 200,
closeDelay: 200,
modelValue: false,
},
})
expect(wrapper.vm.visible).toBe(false)
await wrapper.setProps({
modelValue: true,
})
expect(wrapper.vm.visible).toBe(false)
jest.runOnlyPendingTimers()
expect(wrapper.vm.visible).toBe(true)
})
test('should destroy on close', async () => {
const wrapper = _mount({
props: {
modelValue: true,
destroyOnClose: true,
},
})
expect(wrapper.vm.visible).toBe(true)
wrapper.vm.handleClose()
await wrapper.setProps({
// manually setting this prop because that Transition is not available in testing,
// updating model value event was emitted via transition hooks.
modelValue: false,
})
await nextTick()
expect(wrapper.html()).toBe('<!---->')
})
})
})

View File

@ -0,0 +1,5 @@
export default {
title: 'Dialog',
}

5
packages/dialog/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import Dialog from './src/index.vue'
export default (app: App): void => {
app.component(Dialog.name, Dialog)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/dialog",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.7"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

14
packages/dialog/src/dialog.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
export interface UseDialogProps {
beforeClose?: (close: (shouldCancel: boolean) => void) => void
closeOnClickModal: boolean
closeOnPressEscape: boolean
closeDelay: number
destroyOnClose: boolean
fullscreen: boolean
lockScroll: boolean
modelValue: boolean
openDelay: number
top: string
width: string
zIndex?: number
}

View File

@ -0,0 +1,215 @@
// TODO: Replace this file with single .ts file after styles merged
import {
defineComponent,
Transition,
Teleport,
h,
withDirectives,
vShow,
} from 'vue'
import { TrapFocus } from '@element-plus/directives'
import { isValidWidthUnit } from '@element-plus/utils/validators'
import ElOverlay from '@element-plus/overlay'
import {
default as useDialog,
CLOSE_EVENT,
CLOSED_EVENT,
OPEN_EVENT,
OPENED_EVENT,
UPDATE_MODEL_EVENT,
} from './useDialog'
import type { PropType, SetupContext } from 'vue'
export default defineComponent({
name: 'ElDialog',
props: {
appendToBody: {
type: Boolean,
default: false,
},
beforeClose: {
type: Function as PropType<(...args: any[]) => unknown>,
},
destroyOnClose: {
type: Boolean,
default: false,
},
center: {
type: Boolean,
default: false,
},
customClass: {
type: String,
default: '',
},
closeOnClickModal: {
type: Boolean,
default: true,
},
closeOnPressEscape: {
type: Boolean,
default: true,
},
fullscreen: {
type: Boolean,
default: false,
},
lockScroll: {
type: Boolean,
default: true,
},
modal: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
title: {
type: String,
default: '',
},
openDelay: {
type: Number,
default: 0,
},
closeDelay: {
type: Number,
default: 0,
},
top: {
type: String,
default: '15vh',
},
modelValue: {
type: Boolean,
required: true,
},
width: {
type: String,
default: '50%',
validator: isValidWidthUnit,
},
zIndex: {
type: Number,
},
},
emits: [
OPEN_EVENT,
OPENED_EVENT,
CLOSE_EVENT,
CLOSED_EVENT,
UPDATE_MODEL_EVENT,
],
setup(props, ctx) {
// init here
return useDialog(props, ctx as SetupContext)
},
render() {
if (this.destroyOnClose && !this.modelValue) {
return null
}
const { $slots } = this
const closeBtn = this.showClose
? h(
'button',
{
type: 'button',
class: 'el-dialog__headerbtn',
ariaLabel: 'close',
onClick: this.handleClose,
},
h('i', { class: 'el-dialog__close el-icon el-icon-close' }),
)
: null
const header = h(
'div',
{
class: 'el-dialog__header',
},
[
$slots.header
? $slots.header()
: h('span', { class: 'el-dialog__title' }, this.title),
closeBtn,
],
)
const body = h(
'div',
{
class: 'el-dialog__body',
},
$slots.default?.(),
)
const footer = $slots.footer
? h('div', { class: 'el-dialog__footer' }, $slots.footer())
: null
const dialog = h(
'div',
{
ariaModal: true,
ariaLabel: this.title || 'dialog',
class: [
'el-dialog',
{
'is-fullscreen': this.fullscreen,
'el-dialog--center': this.center,
},
this.customClass,
],
ref: 'dialogRef',
role: 'dialog',
style: this.style,
onClick: (e: MouseEvent) => e.stopPropagation(),
},
[header, body, footer],
)
const trappedDialog = withDirectives(dialog, [[TrapFocus]])
const overlay = withDirectives(
h(
ElOverlay,
{
mask: this.modal,
onClick: this.onModalClick,
zIndex: this.zIndex,
},
{
default: () => trappedDialog,
},
),
[[vShow, this.visible]],
)
const renderer = h(
Transition,
{
name: 'dialog-fade',
onAfterEnter: this.afterEnter,
onAfterLeave: this.afterLeave,
},
{
default: () => overlay,
},
)
if (this.appendToBody) {
return h(
Teleport,
{
to: 'body',
},
renderer,
)
}
return renderer
},
})

View File

@ -0,0 +1,160 @@
import { computed, ref, watch, nextTick, onMounted } from 'vue'
import isServer from '@element-plus/utils/isServer'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import PopupManager from '@element-plus/utils/popup-manager'
import { clearTimer } from '@element-plus/utils/util'
import { useLockScreen, useRestoreActive, useModal } from '@element-plus/hooks'
import type { UseDialogProps } from './dialog'
import type { SetupContext } from '@vue/runtime-core'
export const CLOSE_EVENT = 'close'
export const OPEN_EVENT = 'open'
export const CLOSED_EVENT = 'closed'
export const OPENED_EVENT = 'opened'
export { UPDATE_MODEL_EVENT }
export default function(props: UseDialogProps, ctx: SetupContext) {
const visible = ref(false)
const closed = ref(false)
const dialogRef = ref(null)
const openTimer = ref<TimeoutHandle>(null)
const closeTimer = ref<TimeoutHandle>(null)
const zIndex = ref(props.zIndex || PopupManager.nextZIndex())
const modalRef = ref<HTMLElement>(null)
const style = computed(() => {
const style = {} as CSSStyleDeclaration
if (!props.fullscreen) {
style.marginTop = props.top
if (props.width) {
style.width = props.width
}
}
style.zIndex = String(zIndex.value + 1)
return style
})
function afterEnter() {
ctx.emit(OPENED_EVENT)
}
function afterLeave() {
ctx.emit(CLOSED_EVENT)
ctx.emit(UPDATE_MODEL_EVENT, false)
}
function open() {
clearTimer(closeTimer)
clearTimer(openTimer)
if (props.openDelay && props.openDelay > 0) {
openTimer.value = window.setTimeout(() => {
openTimer.value = null
doOpen()
}, props.openDelay)
} else {
doOpen()
}
}
function close() {
// if (this.willClose && !this.willClose()) return;
clearTimer(openTimer)
clearTimer(closeTimer)
if (props.closeDelay && props.closeDelay > 0) {
closeTimer.value = window.setTimeout(() => {
closeTimer.value = null
doClose()
}, props.closeDelay)
} else {
doClose()
}
}
function hide(shouldCancel: boolean) {
if (shouldCancel) return
closed.value = true
visible.value = false
}
function handleClose() {
if (props.beforeClose) {
props.beforeClose(hide)
} else {
hide(false)
}
}
function onModalClick() {
if (props.closeOnClickModal) {
handleClose()
}
}
function doOpen() {
if (isServer) {
return
}
// if (props.willOpen?.()) {
// return
// }
visible.value = true
}
function doClose() {
visible.value = false
}
if (props.lockScroll) {
useLockScreen(visible)
}
if (props.closeOnPressEscape) {
useModal({
handleClose,
}, visible)
}
useRestoreActive(visible)
watch(() => props.modelValue, val => {
if (val) {
closed.value = false
open()
ctx.emit(OPEN_EVENT)
// this.$el.addEventListener('scroll', this.updatePopper)
nextTick(() => {
dialogRef.value.scrollTop = 0
})
} else {
// this.$el.removeEventListener('scroll', this.updatePopper
close()
if (!closed.value) {
ctx.emit(CLOSE_EVENT)
}
}
})
onMounted(() => {
if (props.modelValue) {
visible.value = true
open()
}
})
return {
afterEnter,
afterLeave,
handleClose,
onModalClick,
closed,
dialogRef,
style,
modalRef,
visible,
zIndex,
}
}

View File

@ -1,3 +1,4 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import * as Aria from '@element-plus/utils/aria'
@ -108,7 +109,7 @@ describe('v-trap-focus', () => {
expect(document.activeElement).toBe(wrapper.find('.button-1').element)
})
test('should prevent tab event when there is only one element', async () => {
test('should focus on the only focusable element', async () => {
wrapper = _mount(`
<div v-trap-focus>
<button />
@ -118,7 +119,7 @@ describe('v-trap-focus', () => {
await wrapper.find('button').trigger('keydown', {
code: 'Tab',
})
expect(document.activeElement).toBe(document.body)
expect(document.activeElement).toBe(wrapper.find('button').element)
})
test('should update focusable list when children changes', async () => {
@ -152,6 +153,8 @@ describe('v-trap-focus', () => {
show: true,
})
await nextTick()
expect(wrapper.element[FOCUSABLE_CHILDREN].length).toBe(2)
})
})

View File

@ -1,2 +1,2 @@
export { default as ClickOutside } from './click-outside'
export { default as TrapFocus } from './trap-focus'

View File

@ -1,3 +1,4 @@
import { nextTick } from 'vue'
import { on, off } from '@element-plus/utils/dom'
import { obtainAllFocusableElements, EVENT_CODE } from '@element-plus/utils/aria'
@ -20,6 +21,9 @@ const TrapFocus: ObjectDirective = {
if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
if (focusableElement.length === 1) {
e.preventDefault()
if (document.activeElement !== focusableElement[0]) {
focusableElement[0].focus()
}
return
}
const goingBackward = e.shiftKey
@ -48,7 +52,9 @@ const TrapFocus: ObjectDirective = {
on(document, 'keydown', el[TRAP_FOCUS_HANDLER])
},
updated(el: ITrapFocusElement) {
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
nextTick(() => {
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
})
},
unmounted(el: ITrapFocusElement) {
off(document, 'keydown', el[TRAP_FOCUS_HANDLER])

View File

@ -31,6 +31,7 @@ import ElTooltip from '@element-plus/tooltip'
import ElSlider from '@element-plus/slider'
import ElInput from '@element-plus/input'
import ElTransfer from '@element-plus/transfer'
import ElDialog from '@element-plus/dialog'
export {
ElAlert,
@ -64,6 +65,7 @@ export {
ElSlider,
ElInput,
ElTransfer,
ElDialog,
}
export default function install(app: App): void {
@ -99,4 +101,5 @@ export default function install(app: App): void {
ElSlider(app)
ElInput(app)
ElTransfer(app)
ElDialog(app)
}

View File

@ -0,0 +1,15 @@
import { ref, nextTick } from 'vue'
import { hasClass } from '@element-plus/utils/dom'
import useLockScreen from '../use-lockscreen'
describe('useLockScreen', () => {
test('should lock screen when trigger is true', async () => {
const _ref = ref(false)
const cls = 'el-popup-parent--hidden'
useLockScreen(_ref)
expect(hasClass(document.body, cls)).toBe(false)
_ref.value = true
await nextTick()
expect(hasClass(document.body, cls)).toBe(true)
})
})

View File

@ -0,0 +1,33 @@
import { ref, nextTick } from 'vue'
import { EVENT_CODE } from '@element-plus/utils/aria'
import useModal from '../use-modal'
describe('useModal', () => {
test('should work when ref value changed', async () => {
const visible = ref(false)
const handleClose = jest.fn()
useModal(
{
handleClose,
},
visible,
)
expect(handleClose).not.toHaveBeenCalled()
visible.value = true
await nextTick()
const event = new KeyboardEvent('keydown', {
code: EVENT_CODE.esc,
})
document.dispatchEvent(event)
expect(handleClose).toHaveBeenCalledTimes(1)
visible.value = false
await nextTick()
document.dispatchEvent(event)
expect(handleClose).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,25 @@
import { ref, nextTick } from 'vue'
import useRestoreActive from '../use-restore-active'
describe('useRestoreActive', () => {
it('should restore active element', async () => {
const visible = ref(false)
useRestoreActive(visible)
const btn1 = document.createElement('button')
const btn2 = document.createElement('button')
document.body.appendChild(btn1)
document.body.appendChild(btn2)
btn1.focus()
expect(document.activeElement).toBe(btn1)
visible.value = true
await nextTick()
btn2.focus()
expect(document.activeElement).toBe(btn2)
visible.value = false
await nextTick()
expect(document.activeElement).toBe(btn1)
})
})

View File

@ -1 +1,4 @@
export { default as useEvents } from './use-events'
export { default as useLockScreen } from './use-lockscreen'
export { default as useRestoreActive } from './use-restore-active'
export { default as useModal } from './use-modal'

View File

@ -0,0 +1,61 @@
import { ref, watch, isRef } from 'vue'
import getScrollBarWidth from '@element-plus/utils/scrollbar-width'
import throwError from '@element-plus/utils/error'
import {
addClass,
removeClass,
hasClass,
getStyle,
} from '@element-plus/utils/dom'
import type { Ref } from 'vue'
/**
* Hook that monitoring the ref value to lock or unlock the screen.
* When the trigger became true, it assumes modal is now opened and vice versa.
* @param trigger {Ref<boolean>}
*/
export default (trigger: Ref<boolean>) => {
if (!isRef(trigger)) {
throwError(
'[useLockScreen]',
'You need to pass a ref param to this function',
)
}
let scrollBarWidth = 0
let withoutHiddenClass = false
let bodyPaddingRight = '0'
let computedBodyPaddingRight = 0
watch(trigger, val => {
if (val) {
withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden')
if (withoutHiddenClass) {
bodyPaddingRight = document.body.style.paddingRight
computedBodyPaddingRight = parseInt(
getStyle(document.body, 'paddingRight'),
10,
)
}
scrollBarWidth = getScrollBarWidth()
const bodyHasOverflow =
document.documentElement.clientHeight < document.body.scrollHeight
const bodyOverflowY = getStyle(document.body, 'overflowY')
if (
scrollBarWidth > 0 &&
(bodyHasOverflow || bodyOverflowY === 'scroll') &&
withoutHiddenClass
) {
document.body.style.paddingRight =
computedBodyPaddingRight + scrollBarWidth + 'px'
}
addClass(document.body, 'el-popup-parent--hidden')
} else {
if (withoutHiddenClass) {
document.body.style.paddingRight = bodyPaddingRight
removeClass(document.body, 'el-popup-parent--hidden')
}
withoutHiddenClass = true
}
})
}

View File

@ -0,0 +1,43 @@
import { watch } from 'vue'
import { on } from '@element-plus/utils/dom'
import { EVENT_CODE } from '@element-plus/utils/aria'
import isServer from '@element-plus/utils/isServer'
import type { Ref, ComputedRef } from 'vue'
type ModalInstance = {
handleClose: () => void
};
const modalStack: ModalInstance[] = []
const closeModal = (e: KeyboardEvent) => {
if (modalStack.length === 0) return
if (e.code === EVENT_CODE.esc) {
const topModal = modalStack[modalStack.length - 1]
topModal.handleClose()
}
}
export default (
instance: ModalInstance,
visibleRef: Ref<boolean> | ComputedRef,
) => {
watch(
() => visibleRef.value,
val => {
if (val) {
modalStack.push(instance)
} else {
modalStack.splice(
modalStack.findIndex(modal => modal === instance),
1,
)
}
},
)
}
if (!isServer) {
on(document, 'keydown', closeModal)
}

View File

@ -0,0 +1,24 @@
import { isRef, watch } from 'vue'
import type { Ref } from 'vue'
/**
* This method provides dialogable components the ability to restore previously activated element before
* the dialog gets opened
*/
export default (toggle: Ref<boolean>, initialFocus?: Ref<HTMLElement>) => {
let previousActive: HTMLElement
watch(() => toggle.value, val => {
if (val) {
previousActive = document.activeElement as HTMLElement
if (isRef(initialFocus)) {
initialFocus.value.focus?.()
}
} else {
if (process.env.NODE_ENV === 'testing') {
previousActive.focus.call(previousActive)
} else {
previousActive.focus()
}
}
})
}

View File

@ -0,0 +1,46 @@
import { mount } from '@vue/test-utils'
import Overlay from '../src/index.vue'
const AXIOM = 'Rem is the best girl'
describe('Overlay.vue', () => {
test('render test', async () => {
const wrapper = mount(Overlay, {
slots: {
default: AXIOM,
},
})
expect(wrapper.text()).toEqual(AXIOM)
const testClass = 'test-class'
await wrapper.setProps({
overlayClass: testClass,
})
expect(wrapper.find(`.${testClass}`)).toBeTruthy()
})
test('should emit click event', async () => {
const wrapper = mount(Overlay, {
slots: {
default: AXIOM,
},
})
await wrapper.find('.el-overlay').trigger('click')
expect(wrapper.emitted()).toBeTruthy()
})
test('no mask', async () => {
const wrapper = mount(Overlay, {
slots: {
default: AXIOM,
},
})
const selector = '.el-overlay'
expect(wrapper.find(selector).exists()).toBe(true)
await wrapper.setProps({
mask: false,
})
expect(wrapper.find(selector).exists()).toBe(false)
})
})

View File

@ -0,0 +1,3 @@
import ElOverlay from './src/index.vue'
export default ElOverlay

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/overlay",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.7"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,57 @@
<script lang='ts'>
import { defineComponent, h } from 'vue'
export default defineComponent({
name: 'ElOverlay',
props: {
mask: {
type: Boolean,
default: true,
},
overlayClass: {
type: String,
},
zIndex: {
type: Number,
},
},
emits: ['click'],
setup(props, { slots, emit }) {
const onMaskClick = () => {
emit('click')
}
// init here
return () => {
return props.mask
? h(
'div',
{
class: ['el-overlay', props.overlayClass],
style: {
zIndex: props.zIndex,
},
onClick: onMaskClick,
},
slots.default?.(),
)
: slots.default?.()
}
},
})
</script>
<style>
.el-overlay-root {
height: 0;
}
.el-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2000;
height: 100%;
background-color: rgba(0,0,0,.5);
}
</style>

View File

@ -2,7 +2,7 @@ import { computed, Fragment, getCurrentInstance, ref, onMounted, onBeforeUnmount
import { debounce } from 'lodash'
import { createPopper } from '@popperjs/core'
import { generateId } from '@element-plus/utils/util'
import { generateId, clearTimer } from '@element-plus/utils/util'
import { addClass } from '@element-plus/utils/dom'
import throwError from '@element-plus/utils/error'
@ -10,19 +10,11 @@ import { default as useEvents } from '@element-plus/hooks/use-events'
import useModifier from './useModifier'
import type { Ref } from 'vue'
import type { IPopperOptions, RefElement, PopperInstance } from './popper'
export const DEFAULT_TRIGGER = ['hover']
export const UPDATE_VALUE_EVENT = 'updateValue'
const clearTimer = (timer: Ref<Nullable<NodeJS.Timeout>>) => {
if (timer.value) {
clearTimeout(timer.value)
}
timer.value = null
}
const getTrigger = () => {
const { subTree: { children } } = getCurrentInstance()
// SubTree is formed by <slot name="trigger"/><popper />

View File

@ -1,7 +1,7 @@
@import "mixins/mixins";
@import "mixins/utils";
@import "common/var";
@import "common/popup";
@import 'mixins/mixins';
@import 'mixins/utils';
@import 'common/var';
@import 'common/popup';
@include b(dialog) {
position: relative;
@ -50,7 +50,8 @@
color: $--color-info;
}
&:focus, &:hover {
&:focus,
&:hover {
.el-dialog__close {
color: $--color-primary;
}
@ -93,13 +94,22 @@
}
.dialog-fade-enter-active {
animation: dialog-fade-in .3s;
animation: modal-fade-in 0.3s !important;
.el-dialog {
animation: dialog-fade-in 0.3s;
}
}
.dialog-fade-leave-active {
animation: dialog-fade-out .3s;
animation: modal-fade-out 0.3s;
.el-dialog {
animation: dialog-fade-out 0.3s;
}
}
@keyframes dialog-fade-in {
0% {
transform: translate3d(0, -20px, 0);
@ -121,3 +131,22 @@
opacity: 0;
}
}
@keyframes modal-fade-in {
0% {
opacity: 0;
}
100% {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@keyframes modal-fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -21,7 +21,7 @@ export const EVENT_CODE = {
right: 'ArrowRight',
top: 'ArrowTop',
down: 'ArrowDown',
esc: 'Esc',
esc: 'Escape',
delete: 'Delete',
backspace: 'Backspace',
}

View File

@ -1,8 +1,10 @@
import isServer from './isServer'
import { isObject, capitalize, hyphenate, looseEqual, extend, camelize } from '@vue/shared'
import { isEmpty, castArray, isEqual } from 'lodash'
import type { AnyFunction } from './types'
import type { Ref } from 'vue'
const { hasOwnProperty } = Object.prototype
@ -127,6 +129,11 @@ export function rafThrottle<T extends AnyFunction<any>>(fn: T): AnyFunction<void
export const objToArray = castArray
export const clearTimer = (timer: Ref<TimeoutHandle>) => {
clearTimeout(timer.value)
timer.value = null
}
/**
* Generating a random int in range (0, max - 1)
* @param max {number}

View File

@ -0,0 +1,4 @@
export const isValidWidthUnit = (val: string) =>
['px', 'rem', 'em', 'vw', '%', 'vmin', 'vmax'].some(unit =>
val.endsWith(unit),
)

View File

@ -10,7 +10,7 @@
"target": "es6",
"sourceMap": true,
"lib": [
"es2020", "dom"
"es2020", "DOM"
],
"allowSyntheticDefaultImports": true
},

View File

@ -9,8 +9,12 @@ declare module '*.vue' {
declare type Nullable<T> = T | null;
declare type CustomizedHTMLElement<T> = HTMLElement & T;
declare type CustomizedHTMLElement<T> = HTMLElement & T
declare type Indexable<T> = {
[key: string]: T
};
}
declare type Hash<T> = Indexable<T>
declare type TimeoutHandle = ReturnType<typeof global.setTimeout>

View File

@ -1,6 +1,15 @@
<template>
<div>
<el-button>{{ value1 }}</el-button>
<el-button @click="visible = true">{{ value1 }}</el-button>
<el-dialog v-model="visible" :destroy-on-close="false">
dialog content
<el-button @click="innerVisible = true">
inner button
</el-button>
<el-dialog v-model="innerVisible" destroy-on-close>
I am inner dialog
</el-dialog>
</el-dialog>
</div>
</template>
@ -9,6 +18,8 @@ export default {
data() {
return {
value1: 'TEST',
visible: false,
innerVisible: false,
}
},
}