Skip to content

Commit 197e3cc

Browse files
committed
feat(CDropdown): add custom toggler, add autoClose options
1 parent 1872675 commit 197e3cc

File tree

3 files changed

+133
-80
lines changed

3 files changed

+133
-80
lines changed

packages/coreui-vue/src/components/dropdown/CDropdown.ts

+44-50
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
1-
import {
2-
defineComponent,
3-
h,
4-
ref,
5-
onUnmounted,
6-
onMounted,
7-
provide,
8-
reactive,
9-
toRefs,
10-
watch,
11-
PropType,
12-
} from 'vue'
1+
import { defineComponent, h, ref, provide, watch, PropType } from 'vue'
132
import { createPopper, Placement } from '@popperjs/core'
143

154
const CDropdown = defineComponent({
@@ -51,6 +40,20 @@ const CDropdown = defineComponent({
5140
}
5241
},
5342
},
43+
/**
44+
* Configure the auto close behavior of the dropdown:
45+
* - `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.
46+
* - `false` - the dropdown will be closed by clicking the toggle button and manually calling hide or toggle method. (Also will not be closed by pressing esc key)
47+
* - `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.
48+
* - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
49+
*/
50+
autoClose: {
51+
type: [Boolean, String],
52+
default: true,
53+
validator: (value: boolean | string) => {
54+
return typeof value === 'boolean' || ['inside', 'outside'].includes(value)
55+
},
56+
},
5457
/**
5558
* Sets a darker color scheme to match a dark navbar.
5659
*/
@@ -136,25 +139,29 @@ const CDropdown = defineComponent({
136139
'show',
137140
],
138141
setup(props, { slots, emit }) {
139-
const dropdownRef = ref()
142+
const dropdownToggleRef = ref()
140143
const dropdownMenuRef = ref()
141144
const placement = ref(props.placement)
142145
const popper = ref()
146+
const visible = ref(props.visible)
147+
148+
watch(
149+
() => props.visible,
150+
() => {
151+
visible.value = props.visible
152+
},
153+
)
143154

144-
const config = reactive({
155+
provide('config', {
156+
autoClose: props.autoClose,
145157
alignment: props.alignment,
146158
dark: props.dark,
147159
popper: props.popper,
148-
visible: props.visible,
149160
})
150161

151-
const { visible } = toRefs(config)
152-
153-
provide('config', config)
154-
155162
provide('variant', props.variant)
156163
provide('visible', visible)
157-
provide('dropdownRef', dropdownRef)
164+
provide('dropdownToggleRef', dropdownToggleRef)
158165
provide('dropdownMenuRef', dropdownMenuRef)
159166

160167
if (props.direction === 'dropup') {
@@ -176,8 +183,8 @@ const CDropdown = defineComponent({
176183
return
177184
}
178185

179-
if (dropdownRef.value) {
180-
popper.value = createPopper(dropdownRef.value, dropdownMenuRef.value, {
186+
if (dropdownToggleRef.value) {
187+
popper.value = createPopper(dropdownToggleRef.value, dropdownMenuRef.value, {
181188
placement: placement.value,
182189
})
183190
}
@@ -190,43 +197,30 @@ const CDropdown = defineComponent({
190197
popper.value = undefined
191198
}
192199

193-
const toggleMenu = function () {
194-
if (props.disabled === false) {
195-
if (visible.value === true) {
196-
visible.value = false
197-
} else {
198-
visible.value = true
199-
}
200+
const toggleMenu = () => {
201+
if (props.disabled) {
202+
return
200203
}
201-
}
202204

203-
provide('toggleMenu', toggleMenu)
204-
205-
const hideMenu = function () {
206-
if (props.disabled === false) {
205+
if (visible.value === true) {
207206
visible.value = false
207+
return
208208
}
209-
}
210209

211-
const handleKeyup = (event: Event) => {
212-
if (dropdownRef.value && !dropdownRef.value.contains(event.target as HTMLElement)) {
213-
hideMenu()
214-
}
210+
visible.value = true
215211
}
216-
const handleClickOutside = (event: Event) => {
217-
if (dropdownRef.value && !dropdownRef.value.contains(event.target as HTMLElement)) {
218-
hideMenu()
212+
213+
provide('toggleMenu', toggleMenu)
214+
215+
const hideMenu = () => {
216+
if (props.disabled) {
217+
return
219218
}
219+
220+
visible.value = false
220221
}
221222

222-
onMounted(() => {
223-
window.addEventListener('click', handleClickOutside)
224-
window.addEventListener('keyup', handleKeyup)
225-
})
226-
onUnmounted(() => {
227-
window.removeEventListener('click', handleClickOutside)
228-
window.removeEventListener('keyup', handleKeyup)
229-
})
223+
provide('hideMenu', hideMenu)
230224

231225
watch(visible, () => {
232226
props.popper && (visible.value ? initPopper() : destroyPopper())

packages/coreui-vue/src/components/dropdown/CDropdownMenu.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineComponent, h, inject, toRefs, Ref } from 'vue'
1+
import { defineComponent, h, inject, onUnmounted, onUpdated, Ref } from 'vue'
22

33
const CDropdownMenu = defineComponent({
44
name: 'CDropdownMenu',
@@ -15,11 +15,13 @@ const CDropdownMenu = defineComponent({
1515
},
1616
},
1717
setup(props, { slots }) {
18+
const dropdownToggleRef = inject('dropdownToggleRef') as Ref<HTMLElement>
1819
const dropdownMenuRef = inject('dropdownMenuRef') as Ref<HTMLElement>
19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
const config = inject('config') as any
20+
const config = inject('config') as any // eslint-disable-line @typescript-eslint/no-explicit-any
21+
const hideMenu = inject('hideMenu') as () => void
22+
const visible = inject('visible') as Ref<boolean>
2123

22-
const { alignment, dark, popper, visible } = toRefs(config)
24+
const { autoClose, alignment, dark, popper } = config
2325

2426
// eslint-disable-next-line @typescript-eslint/ban-types
2527
const alignmentClassNames = (alignment: object | string) => {
@@ -35,16 +37,57 @@ const CDropdownMenu = defineComponent({
3537
return classNames
3638
}
3739

40+
const handleKeyup = (event: Event) => {
41+
if (autoClose === false) {
42+
return
43+
}
44+
if (!dropdownMenuRef.value?.contains(event.target as HTMLElement)) {
45+
hideMenu()
46+
}
47+
}
48+
const handleMouseUp = (event: Event) => {
49+
if (dropdownToggleRef.value?.contains(event.target as HTMLElement)) {
50+
return
51+
}
52+
53+
if (autoClose === true) {
54+
hideMenu()
55+
return
56+
}
57+
58+
if (autoClose === 'inside' && dropdownMenuRef.value?.contains(event.target as HTMLElement)) {
59+
hideMenu()
60+
return
61+
}
62+
63+
if (
64+
autoClose === 'outside' &&
65+
!dropdownMenuRef.value?.contains(event.target as HTMLElement)
66+
) {
67+
hideMenu()
68+
}
69+
}
70+
71+
onUpdated(() => {
72+
visible.value && window.addEventListener('mouseup', handleMouseUp)
73+
visible.value && window.addEventListener('keyup', handleKeyup)
74+
})
75+
76+
onUnmounted(() => {
77+
window.removeEventListener('mouseup', handleMouseUp)
78+
window.removeEventListener('keyup', handleKeyup)
79+
})
80+
3881
return () =>
3982
h(
4083
props.component,
4184
{
4285
class: [
4386
'dropdown-menu',
44-
{ 'dropdown-menu-dark': dark.value, show: visible.value },
45-
alignmentClassNames(alignment.value),
87+
{ 'dropdown-menu-dark': dark, show: visible.value },
88+
alignmentClassNames(alignment),
4689
],
47-
...((typeof alignment.value === 'object' || !popper.value) && {
90+
...((typeof alignment === 'object' || !popper) && {
4891
'data-coreui-popper': 'static',
4992
}),
5093
ref: dropdownMenuRef,

packages/coreui-vue/src/components/dropdown/CDropdownToggle.ts

+39-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { defineComponent, h, inject, Ref } from 'vue'
2-
1+
import { cloneVNode, defineComponent, h, inject, onMounted, Ref, ref } from 'vue'
2+
import { CButton } from '../button'
33
import { Color, Shape } from '../props'
44

55
const CDropdownToggle = defineComponent({
@@ -35,6 +35,10 @@ const CDropdownToggle = defineComponent({
3535
default: 'button',
3636
require: false,
3737
},
38+
/**
39+
* Create a custom toggler which accepts any content.
40+
*/
41+
custom: Boolean,
3842
/**
3943
* Toggle the disabled state for the component.
4044
*/
@@ -81,31 +85,37 @@ const CDropdownToggle = defineComponent({
8185
},
8286
},
8387
setup(props, { slots }) {
84-
const dropdownRef = inject('dropdownRef') as Ref<HTMLElement>
88+
const buttonRef = ref()
89+
const dropdownToggleRef = inject('dropdownToggleRef') as Ref<HTMLElement>
8590
const dropdownVariant = inject('variant') as string
86-
const visible = inject('visible') as boolean
91+
const visible = inject('visible') as Ref<boolean>
8792
const toggleMenu = inject('toggleMenu') as () => void
8893
const className = [
8994
{
9095
'dropdown-toggle': props.caret,
9196
'dropdown-toggle-split': props.split,
92-
show: visible,
97+
show: visible.value,
9398
active: props.active,
9499
disabled: props.disabled,
95100
},
96101
]
97102

98-
const buttonClassName = [
99-
'btn',
100-
props.variant ? `btn-${props.variant}-${props.color}` : `btn-${props.color}`,
101-
{
102-
[`btn-${props.size}`]: props.size,
103-
},
104-
props.shape,
105-
]
103+
onMounted(() => {
104+
if (buttonRef.value) {
105+
dropdownToggleRef.value = buttonRef.value.$el
106+
}
107+
})
106108

107109
return () =>
108-
dropdownVariant === 'nav-item'
110+
props.custom
111+
? slots.default &&
112+
slots.default().map((slot) =>
113+
cloneVNode(slot, {
114+
onClick: () => toggleMenu(),
115+
ref: dropdownToggleRef,
116+
}),
117+
)
118+
: dropdownVariant === 'nav-item'
109119
? h(
110120
'a',
111121
{
@@ -115,26 +125,32 @@ const CDropdownToggle = defineComponent({
115125
href: '#',
116126
onClick: (event: Event) => {
117127
event.preventDefault()
118-
return toggleMenu()
128+
toggleMenu()
119129
},
120-
ref: dropdownRef,
130+
ref: dropdownToggleRef,
121131
},
122132
{ default: () => slots.default && slots.default() },
123133
)
124134
: h(
125-
// TODO: check how to use CButton component
126-
props.component,
135+
CButton,
127136
{
128-
class: [...className, ...buttonClassName],
137+
class: className,
129138
active: props.active,
139+
color: props.color,
130140
disabled: props.disabled,
131141
onClick: () => toggleMenu(),
132142
...(props.component === 'button' && { type: 'button' }),
133-
ref: dropdownRef,
143+
ref: (el) => {
144+
buttonRef.value = el
145+
},
146+
shape: props.shape,
147+
size: props.size,
148+
variant: props.variant,
134149
},
135-
props.split
136-
? h('span', { class: 'visually-hidden' }, 'Toggle Dropdown')
137-
: slots.default && slots.default(),
150+
() =>
151+
props.split
152+
? h('span', { class: 'visually-hidden' }, 'Toggle Dropdown')
153+
: slots.default && slots.default(),
138154
)
139155
},
140156
})

0 commit comments

Comments
 (0)