address PR comments

This commit is contained in:
JeremyWuuuuu
2020-08-06 00:09:09 +08:00
committed by zazzaz
parent d296cfef05
commit 0f04895881
5 changed files with 104 additions and 158 deletions

View File

@@ -33,7 +33,7 @@ describe('Notification.vue', () => {
expect(wrapper.vm.typeClass).toBe('')
expect(wrapper.vm.horizontalClass).toBe('right')
expect(wrapper.vm.verticalProperty).toBe('top')
expect(wrapper.vm.positionStyle).toEqual({ top: 0 })
expect(wrapper.vm.positionStyle).toEqual({ top: '0px' })
})
test('should be able to render VNode', () => {
@@ -86,21 +86,6 @@ describe('Notification.vue', () => {
offMock.mockRestore()
})
test('should call init function when it\'s provided', () => {
const _init = jest.fn()
const wrapper = _mount({
slots: {
default: AXIOM,
},
props: {
_init,
_idx: 0,
},
})
expect(_init).toHaveBeenCalled()
wrapper.unmount()
})
test('should add event listener to target element when init', () => {
jest.spyOn(domExports, 'on')
@@ -243,10 +228,12 @@ describe('Notification.vue', () => {
const wrapper = _mount({
props: {
duration: 0,
onClick: jest.fn(),
},
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
expect(wrapper.vm.onClick).toHaveBeenCalledTimes(1)
})
test('should be able to delete timer when press delete', async () => {
@@ -282,10 +269,12 @@ describe('Notification.vue', () => {
keyCode: eventKeys.esc,
// eslint-disable-next-line
} as any)
const oldClose = wrapper.vm.close
wrapper.vm.close = jest.fn(() => oldClose())
document.dispatchEvent(event)
jest.runAllTimers()
expect(wrapper.vm.closed).toBe(true)
expect(wrapper.emitted('close')).toHaveLength(1)
expect(wrapper.vm.close).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -1,4 +1,6 @@
import { isVNode } from 'vue'
import Notification, { close, closeAll } from '../src/notify'
import type { NotificationVM } from '../src/notification.constants'
jest.useFakeTimers()
@@ -10,15 +12,15 @@ describe('Notification on command', () => {
})
test('it should get component instance when calling notification constructor', async () => {
const vm = Notification({})
expect(vm).toBeNull()
const vm = Notification()
expect(isVNode(vm)).toBe(true)
expect(document.querySelector(selector)).toBeDefined()
jest.runAllTicks()
})
test('it should be able to close notification by manually close', () => {
Notification({})
Notification()
const element = document.querySelector(selector)
expect(element).toBeDefined()
close(element.id)
@@ -26,11 +28,18 @@ describe('Notification on command', () => {
})
test('it should close all notifications', () => {
const notifications: NotificationVM[] = []
const onClose = jest.fn()
for (let i = 0; i < 4; i++) {
Notification({})
notifications.push(Notification({
onClose,
}))
}
expect(document.querySelectorAll(selector).length).toBe(4)
closeAll()
for (let i = 0; i < notifications.length; i++) {
expect(onClose).toHaveBeenCalledTimes(4)
}
expect(document.querySelectorAll(selector).length).toBe(0)
})

View File

@@ -1,7 +1,7 @@
<template>
<transition name="el-notification-fade">
<div
v-show="visible"
v-if="visible"
:id="id"
:class="['el-notification', customClass, horizontalClass]"
:style="positionStyle"
@@ -10,15 +10,8 @@
@mouseleave="startTimer()"
@click="click"
>
<i
v-if="type || iconClass"
class="el-notification__icon"
:class="[typeClass, iconClass]"
></i>
<div
class="el-notification__group"
:class="{ 'is-with-icon': typeClass || iconClass }"
>
<i v-if="type || iconClass" class="el-notification__icon" :class="[typeClass, iconClass]"></i>
<div class="el-notification__group" :class="{ 'is-with-icon': typeClass || iconClass }">
<h2 class="el-notification__title" v-text="title"></h2>
<div v-show="message" class="el-notification__content">
<slot>
@@ -28,18 +21,14 @@
<p v-else v-html="message"></p>
</slot>
</div>
<div
v-if="showClose"
class="el-notification__closeBtn el-icon-close"
@click.stop="close"
></div>
<div v-if="showClose" class="el-notification__closeBtn el-icon-close" @click.stop="close"></div>
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent, reactive, computed, ref, PropType } from 'vue'
import { defaultProps } from './notification.constants'
import { defineComponent, computed, ref, PropType } from 'vue'
// notificationVM is an alias of vue.VNode
import type { NotificationVM } from './notification.constants'
import { eventKeys } from '../../utils/aria'
import { on, off } from '../../utils/dom'
@@ -55,53 +44,67 @@ export default defineComponent({
name: 'ElNotification',
props: {
customClass: { type: String, default: '' },
dangerouslyUseHTMLString: { type: Boolean, default: false }, // default false
duration: { type: Number, default: 4500 }, // default 4500
dangerouslyUseHTMLString: { type: Boolean, default: false },
duration: { type: Number, default: 4500 },
iconClass: { type: String, default: '' },
id: { type: String, default: '' },
message: { type: [String, Object] as PropType<>, default: '' },
offset: { type: Number, default: 0 }, // defaults 0
onClick: { type: Function, default: () => void 0 },
onClose: { type: Function, required: true },
position: { type: String, default: 'top-right' }, // default top-right
message: {
type: [String, Object] as PropType<string | NotificationVM>,
default: '',
},
offset: { type: Number, default: 0 },
onClick: {
type: Function as PropType<() => void>,
default: () => void 0,
},
onClose: {
type: Function as PropType<() => void>,
required: true,
},
position: {
type: String as PropType<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'>,
default: 'top-right',
},
showClose: { type: Boolean, default: true },
title: { type: String, default: '' },
type: { type: String, default: '' },
// disabling linter next two lines due to this is a internal prop which should not be accessed by end user
// eslint-disable-next-line
_idx: { type: Number, required: false, default: null },
// eslint-disable-next-line
_init: { type: Function as (idx: number, vm: NotificationVM) => void, required: false, default: null }
zIndex: { type: Number, default: 0 },
},
emits: ['close', 'click'],
setup(props) {
const data = reactive({
...defaultProps,
...props,
})
data.typeClass = computed(() => {
const type = data.type
const typeClass = computed(() => {
const type = props.type
return type && TypeMap[type] ? `el-icon-${TypeMap[type]}` : ''
})
data.horizontalClass = computed(() => {
return data.position.indexOf('right') > 1 ? 'right' : 'left'
const horizontalClass = computed(() => {
return props.position.indexOf('right') > 1 ? 'right' : 'left'
})
data.verticalProperty = computed(() => {
return data.position.startsWith('top') ? 'top' : 'bottom'
const verticalProperty = computed(() => {
return props.position.startsWith('top') ? 'top' : 'bottom'
})
data.positionStyle = ref({
[data.verticalProperty]: props.offset,
const positionStyle = computed(() => {
return {
[verticalProperty.value]: `${props.offset}px`,
}
})
console.log(data.positionStyle)
const visible = ref(true)
const closed = ref(false)
const timer = ref(null)
data.visible = ref(true)
return data
return {
horizontalClass,
typeClass,
positionStyle,
verticalProperty,
visible,
closed,
timer,
}
},
watch: {
closed(newVal: boolean) {
@@ -110,11 +113,6 @@ export default defineComponent({
on(this.$el, 'transitionend', this.destroyElement)
}
},
offset(newVal: number) {
this.positionStyle = {
[this.verticalProperty]: `${newVal}px`,
}
},
},
mounted() {
if (this.duration > 0) {
@@ -124,11 +122,6 @@ export default defineComponent({
}
}, this.duration)
}
// When using notification programmably, this is line of code is critical
// to obtain the public component instance
if (typeof this._init === 'function') {
// this._init(this._idx, this)
}
on(document, 'keydown', this.keydown)
},
beforeUnmount() {
@@ -157,13 +150,11 @@ export default defineComponent({
// Event handlers
click() {
this?.onClick()
this.$emit('click')
},
close() {
this.closed = true
this.timer = null
this.onClose()
this.$emit('close')
},
keydown({ keyCode }: KeyboardEvent) {
if (keyCode === eventKeys.delete || keyCode === eventKeys.backspace) {

View File

@@ -1,12 +1,7 @@
import type { VNode } from 'vue'
export interface INotification<P = (options: INotificationOptions) => void> {
(options: INotificationOptions): void
success?: P
warning?: P
info?: P
error?: P
}
export type INotification = (options?: INotificationOptions) => NotificationVM
export type INotificationOptions = {
customClass?: string
dangerouslyUseHTMLString?: boolean // default false
@@ -14,15 +9,14 @@ export type INotificationOptions = {
iconClass?: string
id?: string
message?: string | VNode
zIndex?: number
onClose?: () => unknown
onClick?: () => unknown
offset?: number // defaults 0
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' // default top-right
showClose?: boolean
type?: 'success' | 'warning' | 'info' | 'error' | ''
title?: string
type?: 'success' | 'warning' | 'info' | 'error'
_idx?: number
_init?: (idx: number, vm: NotificationVM) => void
}
export type NotificationVM = VNode
@@ -33,23 +27,3 @@ type NotificationQueueItem = {
}
export type NotificationQueue = Array<NotificationQueueItem>
export const defaultProps = {
visible: false,
title: '',
message: '',
duration: 4500,
type: '',
showClose: true,
customClass: '',
iconClass: '',
onClose: null,
onClick: null,
closed: false,
verticalOffset: 0,
timer: null,
dangerouslyUseHTMLString: false,
position: 'top-right',
}

View File

@@ -1,29 +1,16 @@
import { reactive, createVNode, render } from 'vue'
import { createVNode, render } from 'vue'
import NotificationConstructor from './index.vue'
import type { INotificationOptions, INotification, NotificationQueue, NotificationVM } from './notification.constants'
import isServer from '../../utils/isServer'
import PopupManager from '../../utils/popup-manager'
import { isVNode } from '../../utils/util'
// const NotificationRenderer = (props: Record<string, unknown>) => {
// if (isVNode(props.message)) {
// return h(NotificationConstructor, props, { default: () => h(props.message) })
// }
// return h(NotificationConstructor, props)
// }
let vm: NotificationVM
const notifications: NotificationQueue = []
let seed = 1
const Notification: INotification = function(options: INotificationOptions): NotificationVM {
const Notification: INotification = function(options = {}) {
if (isServer) return
const id = 'notification_' + seed++
const userOnClose = options.onClose
options.onClose = function() {
close(id, userOnClose)
}
const position = options.position || 'top-right'
let verticalOffset = options.offset || 0
@@ -34,33 +21,31 @@ const Notification: INotification = function(options: INotificationOptions): Not
})
verticalOffset += 16
const defaultOptions: INotificationOptions = {
const id = 'notification_' + seed++
const userOnClose = options.onClose
options = {
dangerouslyUseHTMLString: false,
duration: 4500,
position: 'top-right',
showClose: true,
offset: 0,
_idx: notifications.length,
// _init: function(idx: number, vm: NotificationVM): void {
// obtainInstance(idx, vm)
// },
}
options = {
...defaultOptions,
// default options end
...options,
onClose: () => {
close(id, userOnClose)
},
offset: verticalOffset,
id,
zIndex: PopupManager.nextZIndex(),
}
options = reactive(options)
const container = document.createElement('div')
container.className = `container_${id}`
container.style.zIndex = String(PopupManager.nextZIndex())
// notifications.push({ vm: null, container })
vm = createVNode(NotificationConstructor, options)
container.style.zIndex = String()
vm = createVNode(NotificationConstructor, options, isVNode(options.message) ? {
default: () => options.message,
}: null)
render(vm, container)
notifications.push({ vm, $el: container })
document.body.appendChild(container.firstElementChild)
@@ -68,23 +53,25 @@ const Notification: INotification = function(options: INotificationOptions): Not
return vm
};
['success', 'warning', 'info', 'error'].forEach(type => {
Notification[type] = options => {
if (typeof options === 'string' || isVNode(options)) {
options = {
message: options,
(['success', 'warning', 'info', 'error'] as const).forEach(type => {
Object.assign(Notification, {
type: options => {
if (typeof options === 'string' || isVNode(options)) {
options = {
message: options,
}
}
}
options.type = type
return Notification(options)
}
options.type = type
return Notification(options)
},
})
})
export function close(
id: string,
userOnClose?: (vm: NotificationVM) => void,
): void {
const idx = notifications.findIndex( ({ vm }) => {
const idx = notifications.findIndex(({ vm }) => {
const { id: _id } = vm.component.props
return id === _id
})
@@ -96,18 +83,18 @@ export function close(
if (!vm) return
userOnClose?.(vm)
const removedHeight = vm.el.offsetHeight
// document.body.removeChild(notification.container)
render(null, $el)
notifications.splice(idx, 1)
const len = notifications.length
if (len < 1) return
const position = vm.component.props.position
const position = vm.props.position
for (let i = idx; i < len; i++) {
if (notifications[i].vm.component.props.position === position) {
notifications[i].vm.el.style[vm.component.ctx.verticalProperty] =
const verticalPos = vm.props.position.split('-')[0]
notifications[i].vm.el.style[verticalPos] =
parseInt(
notifications[i].vm.el.style[vm.component.ctx.verticalProperty],
notifications[i].vm.el.style[verticalPos],
10,
) -
removedHeight -
@@ -119,12 +106,8 @@ export function close(
export function closeAll(): void {
for (let i = notifications.length - 1; i >= 0; i--) {
notifications[i].vm.component.ctx.onClose()
(notifications[i].vm.component.props as INotificationOptions).onClose()
}
}
// function obtainInstance(idx: number, vm: NotificationVM): void {
// notifications[idx] = vm
// }
export default Notification