Skip to content

Commit ab0b14b

Browse files
authored
feat(dropdown): [dropdown] add visible attribute to support user-defined panel display. (#2774)
* feat(dropdown): [dropdown] 增加visible属性,支持用户自定义面板显隐
1 parent 3cfae44 commit ab0b14b

File tree

10 files changed

+245
-36
lines changed

10 files changed

+245
-36
lines changed

examples/sites/demos/apis/dropdown.js

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

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/src/dropdown/index.ts

Lines changed: 49 additions & 24 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)
@@ -251,7 +266,13 @@ export const handleMainButtonClick =
251266
}
252267

253268
export const mounted =
254-
({ api, vm, state, broadcast }: Pick<IDropdownRenderlessParams, 'api' | 'vm' | 'state' | 'broadcast'>) =>
269+
({
270+
api,
271+
vm,
272+
state,
273+
broadcast,
274+
props
275+
}: Pick<IDropdownRenderlessParams, 'api' | 'vm' | 'state' | 'broadcast' | 'props'>) =>
255276
() => {
256277
if (state.showSelfIcon) {
257278
state.showIcon = false
@@ -262,7 +283,11 @@ export const mounted =
262283
vm.$on('selected-index', (selectedIndex) => {
263284
broadcast('TinyDropdownMenu', 'menu-selected-index', selectedIndex)
264285
})
265-
vm.$on('is-disabled', api.clickOutside)
286+
if (!state.visibleIsBoolean) {
287+
vm.$on('is-disabled', api.clickOutside)
288+
} else if (props.visible) {
289+
broadcast('TinyDropdownMenu', 'visible', true)
290+
}
266291
}
267292

268293
export const beforeDistory =

packages/renderless/src/dropdown/vue.ts

Lines changed: 11 additions & 6 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 }),
76-
mounted: mounted({ api, vm, state, broadcast }),
75+
show: show({ props, state, emit }),
76+
hide: hide({ api, props, state, emit }),
77+
mounted: mounted({ api, vm, state, broadcast, props }),
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/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import template from 'virtual-template?pc|mobile-first'
44
export const dropdownProps = {
55
...$props,
66
modelValue: [String, Number],
7+
// tiny新增
8+
visible: {
9+
type: [Boolean, undefined],
10+
default: undefined
11+
},
712
type: String,
813
trigger: String, // 默认主题为 'hover'
914
size: {

0 commit comments

Comments
 (0)