Feat/dropdown (#199)

This commit is contained in:
hangzou 2020-08-28 11:44:04 +08:00 committed by GitHub
parent dec5d665a3
commit 7931f2a2fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 983 additions and 13 deletions

View File

@ -3,7 +3,7 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"cz": "git add -A && npx git-cz", "cz": "npx git-cz",
"test": "jest", "test": "jest",
"gen": "bash ./scripts/gc.sh", "gen": "bash ./scripts/gc.sh",
"storybook": "start-storybook", "storybook": "start-storybook",

View File

@ -5,7 +5,8 @@
<el-checkbox v-model="checked2" disabled>{{ checked2 }}</el-checkbox> <el-checkbox v-model="checked2" disabled>{{ checked2 }}</el-checkbox>
<el-checkbox v-model="checked1" label="A" border /> <el-checkbox v-model="checked1" label="A" border />
<el-checkbox v-model="checked2" border /> <el-checkbox v-model="checked2" border />
<el-checkbox v-model="checked4" true-label="a" :false-label="3" />
{{ checked4 }}
<el-checkbox-group v-model="checkList"> <el-checkbox-group v-model="checkList">
<el-checkbox label="A" /> <el-checkbox label="A" />
<el-checkbox label="B" /> <el-checkbox label="B" />
@ -25,6 +26,7 @@ export default defineComponent({
checked1: false, checked1: false,
checked2: true, checked2: true,
checkList: ['Ha','A'], checkList: ['Ha','A'],
checked4: 3,
} }
}, },
methods: { methods: {

View File

@ -52,7 +52,7 @@ import {
defineComponent, defineComponent,
ref, ref,
computed, computed,
// nextTick, PropType,
watch, watch,
} from 'vue' } from 'vue'
import { useCheckbox } from './useCheckbox' import { useCheckbox } from './useCheckbox'
@ -61,12 +61,11 @@ export default defineComponent({
name: 'ElCheckboxButton', name: 'ElCheckboxButton',
props: { props: {
modelValue: { modelValue: {
type: [Object, Boolean], type: [Object, Boolean, String, Number] as PropType<Record<string, unknown> | boolean | number>,
default: () => undefined, default: () => undefined,
}, },
label: { label: {
type: [Object, Boolean, String], type: [Object, Boolean, String] as PropType<Record<string, unknown> | boolean | string>,
default: () => ({}),
}, },
indeterminate: Boolean, indeterminate: Boolean,
disabled: Boolean, disabled: Boolean,

View File

@ -64,7 +64,7 @@ import {
getCurrentInstance, getCurrentInstance,
watch, watch,
onMounted, onMounted,
// nextTick, PropType,
} from 'vue' } from 'vue'
import { useCheckbox } from './useCheckbox' import { useCheckbox } from './useCheckbox'
@ -72,12 +72,11 @@ export default defineComponent({
name: 'ElCheckbox', name: 'ElCheckbox',
props: { props: {
modelValue: { modelValue: {
type: [Object, Boolean], type: [Object, Boolean, String, Number] as PropType<Record<string, unknown> | boolean | number>,
default: () => undefined, default: () => undefined,
}, },
label: { label: {
type: [Object, Boolean, String], type: [Object, Boolean, String] as PropType<Record<string, unknown> | boolean | string>,
default: ' ',
}, },
indeterminate: Boolean, indeterminate: Boolean,
disabled: Boolean, disabled: Boolean,

View File

@ -9,7 +9,6 @@ export const useCheckbox = () => {
const focus = ref(false) const focus = ref(false)
const isGroup = computed(() => _checkboxGroup && _checkboxGroup.name === 'ElCheckboxGroup') const isGroup = computed(() => _checkboxGroup && _checkboxGroup.name === 'ElCheckboxGroup')
const _elFormItemSize = computed(() => { const _elFormItemSize = computed(() => {
return (elFormItem || {} as any).elFormItemSize return (elFormItem || {} as any).elFormItemSize
}) })
return { return {

View File

@ -0,0 +1,273 @@
import { mount } from '@vue/test-utils'
import { eventKeys } from '@element-plus/utils/aria'
import Dropdown from '../src/dropdown.vue'
import DropdownItem from '../src/dropdown-item.vue'
import DropdownMenu from '../src/dropdown-menu.vue'
const MOUSE_ENTER_EVENT = 'mouseenter'
const MOUSE_LEAVE_EVENT = 'mouseleave'
const CLICK = 'click'
const _mount = (template: string, data, otherObj?) => mount({
components: {
[Dropdown.name]: Dropdown,
[DropdownItem.name]: DropdownItem,
[DropdownMenu.name]: DropdownMenu,
},
template,
data,
...otherObj,
})
const sleep = (time = 250) => new Promise(resolve => setTimeout(resolve, time))
export const timeout = async (fn, time = 250) => {
await sleep(time)
fn()
}
describe('Dropdown', () => {
test('create', async () => {
const wrapper = _mount(
`
<el-dropdown ref="b" placement="right">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({}),
)
const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown-link')
expect(content.value).toBe(false)
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
expect(content.value).toBe(true)
await triggerElm.trigger(MOUSE_LEAVE_EVENT)
await sleep()
expect(content.value).toBe(false)
})
test('menu click', async () => {
const wrapper = _mount(
`
<el-dropdown ref="b" @command="commandHandler" placement="right">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">Apple</el-dropdown-item>
<el-dropdown-item command="b">Orange</el-dropdown-item>
<el-dropdown-item ref="c" :command="myCommandObject">Cherry</el-dropdown-item>
<el-dropdown-item command="d">Peach</el-dropdown-item>
<el-dropdown-item command="e">Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({
myCommandObject: { name: 'CommandC' },
name: '',
}),
{
methods: {
commandHandler(command) {
this.name = command.name
},
},
},
)
// const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown-link')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
await wrapper.findComponent({ ref: 'c' }).trigger('click')
await sleep()
expect((wrapper.vm as any).name).toBe('CommandC')
})
test('trigger', async () => {
const wrapper = _mount(
`
<el-dropdown trigger="click" ref="b" placement="right">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">Apple</el-dropdown-item>
<el-dropdown-item command="b">Orange</el-dropdown-item>
<el-dropdown-item ref="c" :command="myCommandObject">Cherry</el-dropdown-item>
<el-dropdown-item command="d">Peach</el-dropdown-item>
<el-dropdown-item command="e">Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({
myCommandObject: { name: 'CommandC' },
name: '',
}),
)
const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown-link')
expect(content.value).toBe(false)
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
expect(content.value).toBe(false)
await triggerElm.trigger(CLICK)
await sleep()
expect(content.value).toBe(true)
})
test('split button', async () => {
const wrapper = _mount(
`
<el-dropdown @click="handleClick" split-button type="primary" ref="b" placement="right">
dropdown
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">Apple</el-dropdown-item>
<el-dropdown-item command="b">Orange</el-dropdown-item>
<el-dropdown-item ref="c" :command="myCommandObject">Cherry</el-dropdown-item>
<el-dropdown-item command="d">Peach</el-dropdown-item>
<el-dropdown-item command="e">Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({
myCommandObject: { name: 'CommandC' },
name: '',
}),
{
methods: {
handleClick() {
this.name = 'click'
},
},
},
)
const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown__caret-button')
const button = wrapper.find('.el-button')
expect(content.value).toBe(false)
await button.trigger('click')
expect((wrapper.vm as any).name).toBe('click')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
expect(content.value).toBe(true)
})
test('hide on click', async () => {
const wrapper = _mount(
`
<el-dropdown ref="b" placement="right" :hide-on-click="false">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item ref="c">Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({}),
)
const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown-link')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
await wrapper.findComponent({ ref: 'c' }).trigger('click')
await sleep()
expect(content.value).toBe(true)
})
test('triggerElm keydown', async () => {
const wrapper = _mount(
`
<el-dropdown ref="b" placement="right" :hide-on-click="false">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item ref="c">Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({}),
)
const content = wrapper.findComponent({ ref: 'b' }).vm.$refs.popper as any
const triggerElm = wrapper.find('.el-dropdown-link')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
await triggerElm.trigger('keydown', {
keyCode: eventKeys.enter,
})
await sleep()
expect(content.value).toBe(false)
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
await triggerElm.trigger('keydown', {
keyCode: eventKeys.tab,
})
await sleep()
expect(content.value).toBe(false)
})
test('dropdown menu keydown', async () => {
const wrapper = _mount(
`
<el-dropdown ref="b" placement="right" :hide-on-click="false">
<span class="el-dropdown-link" ref="a">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu ref="a">
<el-dropdown-item ref="d">Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item ref="c">Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
`,
() => ({}),
)
const content = wrapper.findComponent({ ref: 'a' })
const triggerElm = wrapper.find('.el-dropdown-link')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await sleep()
await content.trigger('keydown', {
keyCode: eventKeys.down,
})
await sleep()
expect(wrapper.findComponent({ ref: 'd' }).attributes('tabindex')).toBe('0')
})
})

View File

@ -0,0 +1,164 @@
<template>
<div class="block">
<el-dropdown ref="c" placement="right" :hide-on-click="false">
<span ref="a" class="el-dropdown-link">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu ref="b">
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown trigger="click" :hide-on-click="false">
<span class="el-dropdown-link">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown>
<el-button type="primary">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown @command="handleCommand">
<el-button type="primary">
dropdown<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="a">Apple</el-dropdown-item>
<el-dropdown-item command="b">Orange</el-dropdown-item>
<el-dropdown-item command="c">Cherry</el-dropdown-item>
<el-dropdown-item command="d" disabled>Peach</el-dropdown-item>
<el-dropdown-item command="e" divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown split-button type="primary" @click="handleClick">
dropdown
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown
split-button
size="medium"
type="primary"
@click="handleClick"
>
dropdown
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown
split-button
size="small"
type="primary"
@click="handleClick"
>
dropdown
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<br>
<br>
<el-dropdown
split-button
size="mini"
type="primary"
@click="handleClick"
>
dropdown
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Apple</el-dropdown-item>
<el-dropdown-item>Orange</el-dropdown-item>
<el-dropdown-item>Cherry</el-dropdown-item>
<el-dropdown-item disabled>Peach</el-dropdown-item>
<el-dropdown-item divided>Pear</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleClick() {
alert('button click')
},
handleCommand(command) {
alert('click on item ' + command)
},
},
})
</script>
<style>
.el-dropdown-link {
cursor: pointer;
color: #409EFF;
}
.el-icon-arrow-down {
font-size: 12px;
}
.el-dropdown-menu__item {
word-break: keep-all;
}
</style>

View File

@ -0,0 +1,6 @@
export { default as BasicUsage } from './basic.vue'
export default {
title: 'Dropdown',
}

View File

@ -0,0 +1,10 @@
import { App } from 'vue'
import Dropdown from './src/dropdown.vue'
import DropdownItem from './src/dropdown-item.vue'
import DropdownMenu from './src/dropdown-menu.vue'
export default (app: App): void => {
app.component(Dropdown.name, Dropdown)
app.component(DropdownItem.name, DropdownItem)
app.component(DropdownMenu.name, DropdownMenu)
}

View File

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

View File

@ -0,0 +1,49 @@
<template>
<li
class="el-dropdown-menu__item"
:class="{
'is-disabled': disabled,
'el-dropdown-menu__item--divided': divided
}"
:aria-disabled="disabled"
:tabindex="disabled ? null : -1"
@click="handleClick"
>
<i v-if="icon" :class="icon"></i>
<slot></slot>
</li>
</template>
<script lang='ts'>
import { defineComponent, getCurrentInstance } from 'vue'
import { useDropdown } from './useDropdown'
export default defineComponent({
name: 'ElDropdownItem',
props: {
command: {
type: [Object, String, Number],
defautl: () => ({}),
},
disabled: Boolean,
divided: Boolean,
icon: String,
},
setup(props) {
const { elDropdown } = useDropdown()
const _instance = getCurrentInstance()
function handleClick(e: UIEvent) {
if (elDropdown.hideOnClick.value) {
elDropdown.handleClick?.()
}
elDropdown.commandHandler?.(props.command, _instance, e)
}
return {
handleClick,
}
},
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<ul
v-clickOutside:[_trigger]="_hide"
:class="[size && `el-dropdown-menu--${size}`]"
class="el-dropdown-menu"
@mouseenter.stop="show"
@mouseleave.stop="hide"
>
<slot></slot>
</ul>
</template>
<script lang='ts'>
import { defineComponent, getCurrentInstance, onMounted, ref } from 'vue'
import ClickOutside from '@element-plus/directives/click-outside'
import { useDropdown, initDropdownDomEvent } from './useDropdown'
export default defineComponent({
name: 'ElDropdownMenu',
directives: {
ClickOutside,
},
setup() {
const { _elDropdownSize, elDropdown } = useDropdown()
const size = _elDropdownSize.value
const _trigger = ref(null)
function show() {
elDropdown.show?.()
}
function hide() {
if (elDropdown.trigger.value === 'click') return
_hide()
}
function _hide() {
elDropdown.hide?.()
}
onMounted(() => {
const dropdownMenu = getCurrentInstance()
_trigger.value = elDropdown.triggerElm.value
initDropdownDomEvent(dropdownMenu, _trigger.value, elDropdown.instance)
})
return {
size,
show,
hide,
_trigger,
_hide,
}
},
})
</script>
<style>
.el-dropdown-menu {
position: relative;
margin: 0;
box-shadow: none;
}
</style>

18
packages/dropdown/src/dropdown.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import {
ComponentInternalInstance,
ComputedRef,
Ref,
} from 'vue'
export interface IElDropdownInstance {
instance?: ComponentInternalInstance
dropdownSize?: ComputedRef<string>
visible?: Ref<boolean>
handleClick?: () => void
commandHandler?: (...arg) => void
show?: () => void
hide?: () => void
trigger?: ComputedRef<string>
hideOnClick?: ComputedRef<boolean>
triggerElm?: ComputedRef<Nullable<HTMLButtonElement>>
}

View File

@ -0,0 +1,261 @@
<script lang='ts'>
import {
defineComponent,
h,
provide,
getCurrentInstance,
ref,
computed,
watch,
onMounted,
VNode,
} from 'vue'
import { on, addClass, removeClass } from '@element-plus/utils/dom'
import ClickOutside from '@element-plus/directives/click-outside'
import ElButton from '@element-plus/button/src/button.vue'
import ElButtonGroup from '@element-plus/button/src/button-group.vue'
import ELPopper from '@element-plus/popper/src/index.vue'
import { useDropdown } from './useDropdown'
export default defineComponent({
name: 'ElDropdown',
directives: {
ClickOutside,
},
components: {
ElButton,
ElButtonGroup,
ELPopper,
},
props: {
trigger: {
type: String,
default: 'hover',
},
type: String,
size: {
type: String,
default: '',
},
splitButton: Boolean,
hideOnClick: {
type: Boolean,
default: true,
},
placement: {
type: String,
default: 'bottom',
},
showTimeout: {
type: Number,
default: 250,
},
hideTimeout: {
type: Number,
default: 150,
},
tabindex: {
type: Number,
default: 0,
},
effect: {
type: String,
default: 'light',
},
},
emits: ['visible-change', 'click', 'command'],
setup(props, { emit, slots }) {
const _instance = getCurrentInstance()
const { ELEMENT } = useDropdown()
const timeout = ref<Nullable<number>>(null)
const visible = ref(false)
watch(
() => visible.value,
val => {
if(val) triggerElmFocus()
if(!val) triggerElmBlur()
emit('visible-change', val)
},
)
const focusing = ref(false)
watch(
() => focusing.value,
val => {
const selfDefine = triggerElm.value
if (selfDefine) {
if (val) {
addClass(selfDefine, 'focusing')
} else {
removeClass(selfDefine, 'focusing')
}
}
},
)
const triggerVnode = ref<Nullable<VNode>>(null)
const triggerElm = computed<Nullable<HTMLButtonElement>>(() =>
!props.splitButton
? triggerVnode.value?.el
: triggerVnode.value?.el.querySelector('.el-dropdown__caret-button'),
)
function handleClick() {
if (triggerElm.value?.disabled) return
if (visible.value) {
hide()
} else {
show()
}
}
function show() {
if (triggerElm.value?.disabled) return
timeout.value && clearTimeout(timeout.value)
timeout.value = window.setTimeout(() => {
visible.value = true
}, props.trigger === 'click' ? 0 : props.showTimeout)
}
function hide() {
if (triggerElm.value?.disabled) return
removeTabindex()
if (props.tabindex >=0) {
resetTabindex(triggerElm.value)
}
clearTimeout(timeout.value)
timeout.value = window.setTimeout(() => {
visible.value = false
}, props.trigger === 'click' ? 0 : props.hideTimeout)
}
function removeTabindex() {
triggerElm.value?.setAttribute('tabindex', '-1')
}
function resetTabindex(ele) {
removeTabindex()
ele?.setAttribute('tabindex', '0')
}
function triggerElmFocus() {
triggerElm.value?.focus?.()
}
function triggerElmBlur() {
triggerElm.value?.blur?.()
}
// for dom
Object.assign(_instance, {
handleClick,
hide,
resetTabindex,
})
const dropdownSize = computed(() => props.size || (ELEMENT || {}).size)
function commandHandler (...args) {
emit('command', ...args)
}
provide('elDropdown', {
instance: _instance,
dropdownSize,
visible,
handleClick,
commandHandler,
show,
hide,
trigger: computed(() => props.trigger),
hideOnClick: computed(() => props.hideOnClick),
triggerElm,
})
onMounted(() => {
if (!props.splitButton) {
on(triggerElm.value, 'focus', () => {
focusing.value = true
})
on(triggerElm.value, 'blur', () => {
focusing.value = false
})
on(triggerElm.value, 'click', () => {
focusing.value = false
})
}
if (props.trigger === 'hover') {
on(triggerElm.value, 'mouseenter', show)
on(triggerElm.value, 'mouseleave', hide)
} else if (props.trigger === 'click') {
on(triggerElm.value, 'click', handleClick)
}
})
const handlerMainButtonClick = event => {
emit('click', event)
hide()
}
triggerVnode.value = !props.splitButton
? slots.default?.()[0] // trigger must be a single root element
: h(ElButtonGroup, {}, {
default: () => (
[
h(ElButton, {
type: props.type,
size: dropdownSize.value,
onClick: handlerMainButtonClick,
}, {
default: () => slots.default?.()[0],
}),
h(ElButton, {
type: props.type,
size: dropdownSize.value,
class: 'el-dropdown__caret-button',
}, {
default: () => h('i', { class: 'el-dropdown__icon el-icon-arrow-down' }),
}),
]
),
})
slots.default?.().length > 1 && console.warn('trigger must be a single root element')
const dropdownVnode = h('div', {
class: 'el-dropdown',
}, [triggerVnode.value])
return () => h(ELPopper, {
ref: 'popper',
placement: props.placement,
effect: props.effect,
value: visible.value,
manualMode: true,
popperClass: 'el-dropdown-popper',
trigger: props.trigger,
}, {
default: () => slots.dropdown?.(),
trigger: () => dropdownVnode,
})
},
})
</script>
<style>
.el-dropdown-popper {
padding: 0px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.el-dropdown-popper.is-light {
border: 1px solid #EBEEF5;
}
.el-dropdown-popper.is-light .el-popper__arrow::before {
border: 1px solid #EBEEF5;
background: #FFF;
}
.el-dropdown-popper .el-dropdown-menu {
border: none;
}
.el-dropdown-selfdefine {
outline: none;
}
</style>

View File

@ -0,0 +1,115 @@
import { inject, computed, ref } from 'vue'
import { generateId } from '@element-plus/utils/util'
import { eventKeys } from '@element-plus/utils/aria'
import { on, addClass } from '@element-plus/utils/dom'
import { IElDropdownInstance } from './dropdown'
export const useDropdown = () => {
const ELEMENT = null
const elDropdown = inject<IElDropdownInstance>('elDropdown', {})
const _elDropdownSize = computed(() => elDropdown?.dropdownSize)
return {
ELEMENT,
elDropdown,
_elDropdownSize,
}
}
export const initDropdownDomEvent = (dropdownChildren, triggerElm, _instance) => {
const menuItems = ref<Nullable<HTMLButtonElement[]>>(null)
const menuItemsArray = ref<Nullable<HTMLElement[]>>(null)
const dropdownElm = ref<Nullable<HTMLElement>>(null)
const listId = ref(`dropdown-menu-${generateId()}`)
dropdownElm.value = dropdownChildren?.subTree.el
function removeTabindex() {
triggerElm.setAttribute('tabindex', '-1')
menuItemsArray.value?.forEach(item => {
item.setAttribute('tabindex', '-1')
})
}
function resetTabindex(ele) {
removeTabindex()
ele?.setAttribute('tabindex', '0')
}
function handleTriggerKeyDown(ev: KeyboardEvent) {
/**
* https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/keyCode
* keyCode is deprecated, we should replace the api with event.key
* */
const keyCode = ev.keyCode
if ([eventKeys.up, eventKeys.down].includes(keyCode)) {
removeTabindex()
resetTabindex(menuItems.value[0])
menuItems.value[0].focus()
ev.preventDefault()
ev.stopPropagation()
} else if (keyCode === eventKeys.enter) {
_instance.handleClick()
} else if ([eventKeys.tab, eventKeys.esc].includes(keyCode)) {
_instance.hide()
}
}
function handleItemKeyDown(ev) {
const keyCode = ev.keyCode
const target = ev.target
const currentIndex = menuItemsArray.value.indexOf(target)
const max = menuItemsArray.value.length - 1
let nextIndex
if ([eventKeys.up, eventKeys.down].includes(keyCode)) {
if (keyCode === eventKeys.up) {
nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
} else {
nextIndex = currentIndex < max ? currentIndex + 1 : max
}
removeTabindex()
resetTabindex(menuItems.value[nextIndex])
menuItems.value[nextIndex].focus()
ev.preventDefault()
ev.stopPropagation()
} else if (keyCode === eventKeys.enter) {
triggerElmFocus()
target.click()
if (_instance.props.hideOnClick) {
_instance.hide()
}
} else if ([eventKeys.tab, eventKeys.esc].includes(keyCode)) {
_instance.hide()
triggerElmFocus()
}
}
function initAria() {
dropdownElm.value.setAttribute('id', listId.value)
triggerElm.setAttribute('aria-haspopup', 'list')
triggerElm.setAttribute('aria-controls', listId.value)
if (!_instance.props.splitButton) {
triggerElm.setAttribute('role', 'button')
triggerElm.setAttribute('tabindex', _instance.tabindex)
addClass(triggerElm, 'el-dropdown-selfdefine')
}
}
function initEvent() {
on(triggerElm, 'keydown', handleTriggerKeyDown)
on(dropdownElm.value, 'keydown', handleItemKeyDown, true)
}
function initDomOperation() {
menuItems.value = dropdownElm.value.querySelectorAll("[tabindex='-1']") as unknown as HTMLButtonElement[]
menuItemsArray.value = [].slice.call(menuItems.value)
initEvent()
initAria()
}
function triggerElmFocus() {
triggerElm?.focus?.()
}
initDomOperation()
}

View File

@ -6,6 +6,7 @@ import ElButton from '@element-plus/button'
import ElBadge from '@element-plus/badge' import ElBadge from '@element-plus/badge'
import ElCard from '@element-plus/card' import ElCard from '@element-plus/card'
import ElCheckbox from '@element-plus/checkbox' import ElCheckbox from '@element-plus/checkbox'
import ElDropdown from '@element-plus/dropdown'
import ElTag from '@element-plus/tag' import ElTag from '@element-plus/tag'
import ElLayout from '@element-plus/layout' import ElLayout from '@element-plus/layout'
import ElDivider from '@element-plus/divider' import ElDivider from '@element-plus/divider'
@ -38,6 +39,7 @@ export {
ElCard, ElCard,
ElCheckbox, ElCheckbox,
ElDivider, ElDivider,
ElDropdown,
ElTag, ElTag,
ElTimeline, ElTimeline,
ElProgress, ElProgress,
@ -66,6 +68,7 @@ export default function install(app: App): void {
ElBadge(app) ElBadge(app)
ElCard(app) ElCard(app)
ElCheckbox(app) ElCheckbox(app)
ElDropdown(app)
ElTag(app) ElTag(app)
ElLayout(app) ElLayout(app)
ElDivider(app) ElDivider(app)

View File

@ -11,9 +11,10 @@ export const on = function(
element: HTMLElement | Document | Window, element: HTMLElement | Document | Window,
event: string, event: string,
handler: EventListenerOrEventListenerObject, handler: EventListenerOrEventListenerObject,
useCapture = false,
): void { ): void {
if (element && event && handler) { if (element && event && handler) {
element.addEventListener(event, handler, false) element.addEventListener(event, handler, useCapture)
} }
} }

View File

@ -43,7 +43,7 @@ class MenuItem {
case keys.space: { case keys.space: {
prevDef = true prevDef = true
(event.currentTarget as HTMLElement).click() ;(event.currentTarget as HTMLElement).click()
break break
} }
} }