mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +08:00
Feat/dropdown (#199)
This commit is contained in:
parent
dec5d665a3
commit
7931f2a2fb
@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"cz": "git add -A && npx git-cz",
|
||||
"cz": "npx git-cz",
|
||||
"test": "jest",
|
||||
"gen": "bash ./scripts/gc.sh",
|
||||
"storybook": "start-storybook",
|
||||
|
@ -5,7 +5,8 @@
|
||||
<el-checkbox v-model="checked2" disabled>{{ checked2 }}</el-checkbox>
|
||||
<el-checkbox v-model="checked1" label="A" 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 label="A" />
|
||||
<el-checkbox label="B" />
|
||||
@ -25,6 +26,7 @@ export default defineComponent({
|
||||
checked1: false,
|
||||
checked2: true,
|
||||
checkList: ['Ha','A'],
|
||||
checked4: 3,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -52,7 +52,7 @@ import {
|
||||
defineComponent,
|
||||
ref,
|
||||
computed,
|
||||
// nextTick,
|
||||
PropType,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useCheckbox } from './useCheckbox'
|
||||
@ -61,12 +61,11 @@ export default defineComponent({
|
||||
name: 'ElCheckboxButton',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Object, Boolean],
|
||||
type: [Object, Boolean, String, Number] as PropType<Record<string, unknown> | boolean | number>,
|
||||
default: () => undefined,
|
||||
},
|
||||
label: {
|
||||
type: [Object, Boolean, String],
|
||||
default: () => ({}),
|
||||
type: [Object, Boolean, String] as PropType<Record<string, unknown> | boolean | string>,
|
||||
},
|
||||
indeterminate: Boolean,
|
||||
disabled: Boolean,
|
||||
|
@ -64,7 +64,7 @@ import {
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
onMounted,
|
||||
// nextTick,
|
||||
PropType,
|
||||
} from 'vue'
|
||||
import { useCheckbox } from './useCheckbox'
|
||||
|
||||
@ -72,12 +72,11 @@ export default defineComponent({
|
||||
name: 'ElCheckbox',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Object, Boolean],
|
||||
type: [Object, Boolean, String, Number] as PropType<Record<string, unknown> | boolean | number>,
|
||||
default: () => undefined,
|
||||
},
|
||||
label: {
|
||||
type: [Object, Boolean, String],
|
||||
default: ' ',
|
||||
type: [Object, Boolean, String] as PropType<Record<string, unknown> | boolean | string>,
|
||||
},
|
||||
indeterminate: Boolean,
|
||||
disabled: Boolean,
|
||||
|
@ -9,7 +9,6 @@ export const useCheckbox = () => {
|
||||
const focus = ref(false)
|
||||
const isGroup = computed(() => _checkboxGroup && _checkboxGroup.name === 'ElCheckboxGroup')
|
||||
const _elFormItemSize = computed(() => {
|
||||
|
||||
return (elFormItem || {} as any).elFormItemSize
|
||||
})
|
||||
return {
|
||||
|
273
packages/dropdown/__tests__/dropdown.spec.ts
Normal file
273
packages/dropdown/__tests__/dropdown.spec.ts
Normal 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')
|
||||
|
||||
})
|
||||
})
|
164
packages/dropdown/doc/basic.vue
Normal file
164
packages/dropdown/doc/basic.vue
Normal 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>
|
6
packages/dropdown/doc/index.stories.ts
Normal file
6
packages/dropdown/doc/index.stories.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as BasicUsage } from './basic.vue'
|
||||
|
||||
export default {
|
||||
title: 'Dropdown',
|
||||
}
|
||||
|
10
packages/dropdown/index.ts
Normal file
10
packages/dropdown/index.ts
Normal 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)
|
||||
}
|
12
packages/dropdown/package.json
Normal file
12
packages/dropdown/package.json
Normal 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"
|
||||
}
|
||||
}
|
49
packages/dropdown/src/dropdown-item.vue
Normal file
49
packages/dropdown/src/dropdown-item.vue
Normal 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>
|
59
packages/dropdown/src/dropdown-menu.vue
Normal file
59
packages/dropdown/src/dropdown-menu.vue
Normal 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
18
packages/dropdown/src/dropdown.d.ts
vendored
Normal 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>>
|
||||
}
|
261
packages/dropdown/src/dropdown.vue
Normal file
261
packages/dropdown/src/dropdown.vue
Normal 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>
|
115
packages/dropdown/src/useDropdown.ts
Normal file
115
packages/dropdown/src/useDropdown.ts
Normal 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()
|
||||
}
|
@ -6,6 +6,7 @@ import ElButton from '@element-plus/button'
|
||||
import ElBadge from '@element-plus/badge'
|
||||
import ElCard from '@element-plus/card'
|
||||
import ElCheckbox from '@element-plus/checkbox'
|
||||
import ElDropdown from '@element-plus/dropdown'
|
||||
import ElTag from '@element-plus/tag'
|
||||
import ElLayout from '@element-plus/layout'
|
||||
import ElDivider from '@element-plus/divider'
|
||||
@ -38,6 +39,7 @@ export {
|
||||
ElCard,
|
||||
ElCheckbox,
|
||||
ElDivider,
|
||||
ElDropdown,
|
||||
ElTag,
|
||||
ElTimeline,
|
||||
ElProgress,
|
||||
@ -66,6 +68,7 @@ export default function install(app: App): void {
|
||||
ElBadge(app)
|
||||
ElCard(app)
|
||||
ElCheckbox(app)
|
||||
ElDropdown(app)
|
||||
ElTag(app)
|
||||
ElLayout(app)
|
||||
ElDivider(app)
|
||||
|
@ -11,9 +11,10 @@ export const on = function(
|
||||
element: HTMLElement | Document | Window,
|
||||
event: string,
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
useCapture = false,
|
||||
): void {
|
||||
if (element && event && handler) {
|
||||
element.addEventListener(event, handler, false)
|
||||
element.addEventListener(event, handler, useCapture)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ class MenuItem {
|
||||
case keys.space: {
|
||||
prevDef = true
|
||||
|
||||
(event.currentTarget as HTMLElement).click()
|
||||
;(event.currentTarget as HTMLElement).click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user