Skip to content

Commit 35716f8

Browse files
authored
feat(dropdown): [dropdown] Add visible attribute, support user-defined panel display and hide (#2827)
* feat(dropdown): [dropdown] 增加visible属性,支持用户自定义面板显隐 * feat(dropdown): [dropdown] 增加visible属性,支持用户自定义面板显隐 * feat(dropdown): [dropdown] 补充api版本号
1 parent 7d1fbeb commit 35716f8

File tree

12 files changed

+235
-35
lines changed

12 files changed

+235
-35
lines changed

examples/sites/demos/apis/dropdown.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,20 @@ export default {
218218
pcDemo: 'split-button',
219219
mfDemo: ''
220220
},
221+
{
222+
name: 'v-model:visible',
223+
type: 'boolean',
224+
defaultValue: 'false',
225+
meta: {
226+
stable: '3.21.1'
227+
},
228+
desc: {
229+
'zh-CN': '手动控制下拉弹框显隐,优先级高于trigger',
230+
'en-US': 'Manually control the display and hide of the dropdown menu, with priority higher than the trigger'
231+
},
232+
mode: ['pc'],
233+
pcDemo: 'visible'
234+
},
221235
{
222236
name: 'visible-arrow',
223237
type: 'boolean',
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<tiny-dropdown v-model:visible="visible" :show-icon="false" @item-click="itemClick" :hideOnClick="false">
3+
<tiny-button @click="handleClick">点击{{ visible ? '隐藏' : '显示' }}</tiny-button>
4+
<template #dropdown>
5+
<tiny-dropdown-menu>
6+
<tiny-dropdown-item
7+
v-for="(item, index) in options"
8+
:key="index"
9+
:label="item.label"
10+
:disabled="item.disabled"
11+
:item-data="item"
12+
></tiny-dropdown-item>
13+
</tiny-dropdown-menu>
14+
</template>
15+
</tiny-dropdown>
16+
</template>
17+
18+
<script setup>
19+
import { ref } from 'vue'
20+
import { TinyDropdown, TinyDropdownMenu, TinyDropdownItem, TinyButton } from '@opentiny/vue'
21+
22+
const visible = ref(false)
23+
const options = [
24+
{
25+
label: '黄金糕'
26+
},
27+
{
28+
label: '点击我隐藏'
29+
},
30+
{
31+
label: '螺蛳粉'
32+
},
33+
{
34+
label: '双皮奶'
35+
},
36+
{
37+
label: '蚵仔煎'
38+
}
39+
]
40+
const itemClick = (e) => {
41+
if (e.itemData.label === '点击我隐藏') {
42+
visible.value = false
43+
}
44+
}
45+
const handleClick = () => {
46+
visible.value = !visible.value
47+
}
48+
</script>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test('手动控制显隐', async ({ page }) => {
4+
page.on('pageerror', (exception) => expect(exception).toBeNull())
5+
await page.goto('dropdown#visible')
6+
7+
const wrap = page.locator('#visible')
8+
const dropDownMenu = page.locator('body > .tiny-dropdown-menu').locator('visible=true')
9+
10+
await wrap.getByText('点击显示').click()
11+
await expect(dropDownMenu).toHaveCount(1)
12+
13+
await page.locator('#all-demos-container').click()
14+
await expect(dropDownMenu).toHaveCount(1)
15+
16+
await dropDownMenu.locator('div').filter({ hasText: '黄金糕' }).nth(1).click()
17+
await expect(dropDownMenu).toHaveCount(1)
18+
19+
await wrap.getByText('点击隐藏').click()
20+
await expect(dropDownMenu).toHaveCount(0)
21+
22+
await wrap.getByText('点击显示').click()
23+
await expect(dropDownMenu).toHaveCount(1)
24+
25+
await dropDownMenu.locator('div').filter({ hasText: '点击我隐藏' }).nth(1).click()
26+
await expect(dropDownMenu).toHaveCount(0)
27+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<tiny-dropdown v-model:visible="visible" :show-icon="false" @item-click="itemClick" :hideOnClick="false">
3+
<tiny-button @click="handleClick">点击{{ visible ? '隐藏' : '显示' }}</tiny-button>
4+
<template #dropdown>
5+
<tiny-dropdown-menu>
6+
<tiny-dropdown-item
7+
v-for="(item, index) in options"
8+
:key="index"
9+
:label="item.label"
10+
:disabled="item.disabled"
11+
:item-data="item"
12+
></tiny-dropdown-item>
13+
</tiny-dropdown-menu>
14+
</template>
15+
</tiny-dropdown>
16+
</template>
17+
18+
<script>
19+
import { TinyDropdown, TinyDropdownMenu, TinyDropdownItem, TinyButton } from '@opentiny/vue'
20+
21+
export default {
22+
components: {
23+
TinyDropdown,
24+
TinyDropdownMenu,
25+
TinyDropdownItem,
26+
TinyButton
27+
},
28+
data() {
29+
return {
30+
visible: false,
31+
options: [
32+
{
33+
label: '黄金糕'
34+
},
35+
{
36+
label: '点击我隐藏'
37+
},
38+
{
39+
label: '螺蛳粉'
40+
},
41+
{
42+
label: '双皮奶'
43+
},
44+
{
45+
label: '蚵仔煎'
46+
}
47+
]
48+
}
49+
},
50+
methods: {
51+
itemClick(e) {
52+
if (e.itemData.label === '点击我隐藏') {
53+
this.visible = false
54+
}
55+
},
56+
handleClick() {
57+
this.visible = !this.visible
58+
}
59+
}
60+
}
61+
</script>

examples/sites/demos/pc/app/dropdown/webdoc/dropdown.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ export default {
111111
},
112112
codeFiles: ['trigger.vue']
113113
},
114+
{
115+
demoId: 'visible',
116+
name: {
117+
'zh-CN': '手动控制显隐',
118+
'en-US': 'Manual control of display and concealment'
119+
},
120+
desc: {
121+
'zh-CN': '<p>通过 <code>visible</code> 属性手动控制下拉菜单显隐,优先级高于trigger。</p>\n',
122+
'en-US':
123+
'<p>Manually control the visibility of the dropdown menu through the<code>visible</code>attribute, with priority over trigger.</p>\n'
124+
},
125+
codeFiles: ['visible.vue']
126+
},
114127
{
115128
demoId: 'tip',
116129
name: {

packages/renderless/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opentiny/vue-renderless",
3-
"version": "3.21.0",
3+
"version": "3.21.1",
44
"private": true,
55
"description": "An enterprise-class UI component library, support both Vue.js 2 and Vue.js 3, as well as PC and mobile.",
66
"author": "OpenTiny Team",
@@ -43,4 +43,4 @@
4343
"esno": "^4.7.0",
4444
"tsup": "7.2.0"
4545
}
46-
}
46+
}

packages/renderless/src/dropdown/index.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,28 @@ export const watchFocusing = (parent: IDropdownRenderlessParams['parent']) => (v
3939
}
4040

4141
export const show =
42-
({ props, state }: Pick<IDropdownRenderlessParams, 'props' | 'state'>) =>
42+
({ props, state, emit }: Pick<IDropdownRenderlessParams, 'props' | 'state' | 'emit'>) =>
4343
() => {
4444
if (props.disabled) {
4545
return
4646
}
4747

48-
clearTimeout(Number(state.timeout))
49-
50-
state.timeout = setTimeout(
51-
() => {
52-
state.visible = true
53-
},
54-
state.trigger === 'click' ? 0 : props.showTimeout
55-
)
48+
if (state.visibleIsBoolean) {
49+
emit('update:visible', true)
50+
} else {
51+
clearTimeout(Number(state.timeout))
52+
53+
state.timeout = setTimeout(
54+
() => {
55+
state.visible = true
56+
},
57+
state.trigger === 'click' ? 0 : props.showTimeout
58+
)
59+
}
5660
}
5761

5862
export const hide =
59-
({ api, props, state }: Pick<IDropdownRenderlessParams, 'api' | 'props' | 'state'>) =>
63+
({ api, props, state, emit }: Pick<IDropdownRenderlessParams, 'api' | 'props' | 'state' | 'emit'>) =>
6064
() => {
6165
if (props.disabled) {
6266
return
@@ -68,14 +72,18 @@ export const hide =
6872
api.resetTabindex(state.triggerElm)
6973
}
7074

71-
clearTimeout(Number(state.timeout))
72-
73-
state.timeout = setTimeout(
74-
() => {
75-
state.visible = false
76-
},
77-
state.trigger === 'click' ? 0 : props.hideTimeout
78-
)
75+
if (state.visibleIsBoolean) {
76+
emit('update:visible', false)
77+
} else {
78+
clearTimeout(Number(state.timeout))
79+
80+
state.timeout = setTimeout(
81+
() => {
82+
state.visible = false
83+
},
84+
state.trigger === 'click' ? 0 : props.hideTimeout
85+
)
86+
}
7987
}
8088

8189
export const handleClick =
@@ -85,9 +93,12 @@ export const handleClick =
8593
return
8694
}
8795

88-
emit('handle-click', state.visible)
89-
90-
state.visible ? api.hide() : api.show()
96+
if (state.visibleIsBoolean) {
97+
emit('handle-click', props.visible)
98+
} else {
99+
emit('handle-click', state.visible)
100+
state.visible ? api.hide() : api.show()
101+
}
91102
}
92103

93104
export const handleTriggerKeyDown =
@@ -112,7 +123,7 @@ export const handleTriggerKeyDown =
112123
}
113124

114125
export const handleItemKeyDown =
115-
({ api, props, state }: Pick<IDropdownRenderlessParams, 'api' | 'props' | 'state'>) =>
126+
({ api, props, state, emit }: Pick<IDropdownRenderlessParams, 'api' | 'props' | 'state' | 'emit'>) =>
116127
(event: KeyboardEvent) => {
117128
const keyCode = event.keyCode
118129
const target = event.target
@@ -199,6 +210,10 @@ export const initEvent =
199210
on(state.triggerElm, 'click', api.toggleFocusOnFalse)
200211
}
201212

213+
if (state.visibleIsBoolean) {
214+
return
215+
}
216+
202217
if (state.trigger === 'hover') {
203218
on(state.triggerElm, 'mouseenter', api.show)
204219
on(state.triggerElm, 'mouseleave', api.hide)
@@ -262,7 +277,9 @@ export const mounted =
262277
vm.$on('selected-index', (selectedIndex) => {
263278
broadcast('TinyDropdownMenu', 'menu-selected-index', selectedIndex)
264279
})
265-
vm.$on('is-disabled', api.clickOutside)
280+
if (!state.visibleIsBoolean) {
281+
vm.$on('is-disabled', api.clickOutside)
282+
}
266283
}
267284

268285
export const beforeDistory =

packages/renderless/src/dropdown/vue.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export const renderless = (
6262
designConfig,
6363
trigger: computed(() => {
6464
return props.trigger || designConfig?.props?.trigger || 'hover'
65-
})
65+
}),
66+
visibleIsBoolean: computed(() => typeof props.visible === 'boolean')
6667
})
6768

6869
provide('dropdownVm', vm)
@@ -71,12 +72,12 @@ export const renderless = (
7172
state,
7273
watchVisible: watchVisible({ broadcast, emit, nextTick }),
7374
watchFocusing: watchFocusing(parent),
74-
show: show({ props, state }),
75-
hide: hide({ api, props, state }),
75+
show: show({ props, state, emit }),
76+
hide: hide({ api, props, state, emit }),
7677
mounted: mounted({ api, vm, state, broadcast }),
7778
handleClick: handleClick({ api, props, state, emit }),
7879
handleTriggerKeyDown: handleTriggerKeyDown({ api, state }),
79-
handleItemKeyDown: handleItemKeyDown({ api, props, state }),
80+
handleItemKeyDown: handleItemKeyDown({ api, props, state, emit }),
8081
resetTabindex: resetTabindex(api),
8182
removeTabindex: removeTabindex(state),
8283
initAria: initAria({ state, props }),
@@ -91,7 +92,11 @@ export const renderless = (
9192
toggleFocusOnFalse: toggleFocus({ state, value: false })
9293
})
9394

94-
watch(() => state.visible, api.watchVisible)
95+
if (typeof props.visible === 'boolean') {
96+
watch(() => props.visible, api.watchVisible)
97+
} else {
98+
watch(() => state.visible, api.watchVisible)
99+
}
95100
watch(() => state.focusing, api.watchFocusing)
96101

97102
onMounted(api.mounted)

packages/renderless/types/dropdown.type.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ export interface IDropdownState {
2020
visible: boolean
2121
timeout: null | NodeJS.Timeout
2222
focusing: false
23-
menuItems: NodeListOf<HTMLElement> | undefined | null
23+
menuItems: NodeListOf<HTMLElement> | undefined | null | []
2424
menuItemsArray: HTMLElement[] | null
2525
triggerElm: HTMLElement | null
2626
dropdownElm: HTMLElement | null
2727
listId: string
2828
showIcon: boolean
2929
showSelfIcon: boolean
3030
designConfig: IDropdownRenderlessParamUtils['designConfig']
31+
trigger: 'click' | 'hover'
32+
visibleIsBoolean: boolean
3133
}
3234

3335
export interface IDropdownApi {

packages/vue/src/dropdown/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opentiny/vue-dropdown",
3-
"version": "3.21.0",
3+
"version": "3.21.1",
44
"description": "",
55
"main": "lib/index.js",
66
"module": "index.ts",

0 commit comments

Comments
 (0)