Skip to content

Commit 3d1d777

Browse files
authored
feat(b-dropdown & b-nav-item-dropdown): pass optional scope to default slot & fixes keyboard nav with dropdown forms (bootstrap-vue#3242)
1 parent 4cf623c commit 3d1d777

10 files changed

+167
-97
lines changed

src/components/dropdown/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,16 @@ export default {
572572
Refer to the [Events](/docs/components/dropdown#component-reference) section of documentation for
573573
the full list of events.
574574

575+
## Optionally scoped default slot
576+
577+
<span class="badge badge-info small">NEW in 2.0.0-rc.20</span>
578+
579+
The default slot is optionally scoped with the following scope available:
580+
581+
| Property or Method | Description |
582+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
583+
| `hide()` | Can be used to close the dropdown menu. Accepts an optional boolean argument, which if `true` returns focus to the toggle button |
584+
575585
## Accessibility
576586

577587
Providing a unique `id` prop ensures ARIA compliance by automatically adding the appropriate

src/components/dropdown/_dropdown-form.scss

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,35 @@ $bv-dropdown-form-defined: false !default;
66

77
// Custom styles for <b-dropdown-form>
88
// Based on class `.dropdown-item`
9-
.b-dropdown-form {
10-
display: inline-block;
11-
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
12-
width: 100%;
13-
clear: both;
14-
font-weight: $font-weight-normal;
9+
.dropdown.b-dropdown {
10+
.b-dropdown-form {
11+
display: inline-block;
12+
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
13+
width: 100%;
14+
clear: both;
15+
font-weight: $font-weight-normal;
1516

16-
&:first-child {
17-
@include border-top-radius($dropdown-inner-border-radius);
18-
}
17+
&:focus {
18+
// From https://github.com/twbs/bootstrap/blob/master/scss/_reboot.scss
19+
// mimicking button:focus styling.
20+
// We add important here as anything with tabindex `-1` and focused will not
21+
// have a focus ring due to reboot.scss and it's `!important` override.
22+
// Needed for keyboard navigation high-lighting
23+
outline: 1px dotted !important;
24+
outline: 5px auto -webkit-focus-ring-color !important;
25+
}
1926

20-
&:last-child {
21-
@include border-bottom-radius($dropdown-inner-border-radius);
27+
&.disabled,
28+
&:disabled {
29+
outline: 0 !important;
30+
color: $dropdown-link-disabled-color;
31+
pointer-events: none;
32+
// background-color: transparent;
33+
// Remove CSS gradients if they're enabled
34+
// @if $enable-gradients {
35+
// background-image: none;
36+
// }
37+
}
2238
}
2339
}
2440
}

src/components/dropdown/_dropdown-text.scss

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,5 @@ $bv-dropdown-text-defined: false !default;
1313
width: 100%;
1414
clear: both;
1515
font-weight: $font-weight-lighter;
16-
17-
&:first-child {
18-
@include border-top-radius($dropdown-inner-border-radius);
19-
}
20-
21-
&:last-child {
22-
@include border-bottom-radius($dropdown-inner-border-radius);
23-
}
2416
}
2517
}

src/components/dropdown/_dropdown.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,28 @@ $bv-dropdown-defined: false !default;
2424
}
2525
}
2626
}
27+
28+
// Prevent dropdown background overflow if there's no padding
29+
// See https://github.com/twbs/bootstrap/pull/27703
30+
// Added here to address <li> wrapping of items
31+
@if $dropdown-padding-y == 0 {
32+
.dropdown-menu {
33+
> :first-child {
34+
.dropdown-item,
35+
.dropdown-form,
36+
.dropdown-text {
37+
@include border-top-radius($dropdown-inner-border-radius);
38+
}
39+
}
40+
41+
> :last-child {
42+
.dropdown-item,
43+
.dropdown-form,
44+
.dropdown-text {
45+
@include border-bottom-radius($dropdown-inner-border-radius);
46+
}
47+
}
48+
}
49+
}
2750
}
2851
}

src/components/dropdown/dropdown-form.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,27 @@ export default Vue.extend({
66
name: 'BDropdownForm',
77
functional: true,
88
inheritAttrs: false,
9-
props: { ...formProps },
9+
props: {
10+
...formProps,
11+
disabled: {
12+
type: Boolean,
13+
default: false
14+
}
15+
},
1016
render(h, { props, data, children }) {
1117
return h('li', [
1218
h(
1319
BForm,
1420
mergeData(data, {
21+
ref: 'form',
1522
staticClass: 'b-dropdown-form',
23+
class: { disabled: props.disabled },
1624
props,
17-
ref: 'form'
25+
attrs: {
26+
disabled: props.disabled,
27+
// Tab index of -1 for keyboard navigation
28+
tabindex: props.disabled ? null : '-1'
29+
}
1830
}),
1931
children
2032
)

src/components/dropdown/dropdown-form.spec.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,47 @@ describe('dropdown-form', () => {
1010
expect(form.is('form')).toBe(true)
1111
})
1212

13-
it('has custom class "b-dropdown-form"', async () => {
13+
it('default has expected classes', async () => {
1414
const wrapper = mount(BDropdownForm)
1515
expect(wrapper.is('li')).toBe(true)
1616

1717
const form = wrapper.find('form')
1818
expect(form.classes()).toContain('b-dropdown-form')
1919
expect(form.classes()).not.toContain('was-validated')
20+
expect(form.classes()).not.toContain('disabled')
2021
})
2122

22-
it('has class "was-validated" when validated=true', async () => {
23+
it('has tabindex on form', async () => {
24+
const wrapper = mount(BDropdownForm)
25+
expect(wrapper.is('li')).toBe(true)
26+
27+
const form = wrapper.find('form')
28+
expect(form.is('form')).toBe(true)
29+
expect(form.attributes('tabindex')).toBeDefined()
30+
expect(form.attributes('tabindex')).toEqual('-1')
31+
})
32+
33+
it('does not have tabindex on form when disabled', async () => {
2334
const wrapper = mount(BDropdownForm, {
24-
context: {
25-
props: { validated: true }
35+
propsData: {
36+
disabled: true
2637
}
2738
})
2839
expect(wrapper.is('li')).toBe(true)
2940

41+
const form = wrapper.find('form')
42+
expect(form.is('form')).toBe(true)
43+
expect(form.attributes('tabindex')).not.toBeDefined()
44+
expect(form.attributes('disabled')).toBeDefined()
45+
expect(form.classes()).toContain('disabled')
46+
})
47+
48+
it('has class "was-validated" when validated=true', async () => {
49+
const wrapper = mount(BDropdownForm, {
50+
propsData: { validated: true }
51+
})
52+
expect(wrapper.is('li')).toBe(true)
53+
3054
const form = wrapper.find('form')
3155
expect(form.classes()).toContain('was-validated')
3256
expect(form.classes()).toContain('b-dropdown-form')
@@ -42,9 +66,7 @@ describe('dropdown-form', () => {
4266

4367
it('has attribute novalidate when novalidate=true', async () => {
4468
const wrapper = mount(BDropdownForm, {
45-
context: {
46-
props: { novalidate: true }
47-
}
69+
propsData: { novalidate: true }
4870
})
4971
expect(wrapper.is('li')).toBe(true)
5072

src/components/dropdown/dropdown.js

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Vue from '../../utils/vue'
22
import { stripTags } from '../../utils/html'
33
import { getComponentConfig } from '../../utils/config'
4+
import { HTMLElement } from '../../utils/safe-types'
45
import idMixin from '../../mixins/id'
56
import dropdownMixin from '../../mixins/dropdown'
67
import normalizeSlotMixin from '../../mixins/normalize-slot'
@@ -60,8 +61,8 @@ export const props = {
6061
},
6162
boundary: {
6263
// String: `scrollParent`, `window` or `viewport`
63-
// Object: HTML Element reference
64-
type: [String, Object],
64+
// HTMLElement: HTML Element reference
65+
type: [String, HTMLElement],
6566
default: 'scrollParent'
6667
}
6768
}
@@ -73,40 +74,33 @@ export default Vue.extend({
7374
props,
7475
computed: {
7576
dropdownClasses() {
76-
// Position `static` is needed to allow menu to "breakout" of the scrollParent boundaries
77-
// when boundary is anything other than `scrollParent`
78-
// See https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
79-
const positionStatic = this.boundary !== 'scrollParent' || !this.boundary
80-
8177
return [
82-
'btn-group',
83-
'b-dropdown',
84-
'dropdown',
8578
this.directionClass,
8679
{
8780
show: this.visible,
88-
'position-static': positionStatic
81+
// Position `static` is needed to allow menu to "breakout" of the scrollParent boundaries
82+
// when boundary is anything other than `scrollParent`
83+
// See https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
84+
'position-static': this.boundary !== 'scrollParent' || !this.boundary
8985
}
9086
]
9187
},
9288
menuClasses() {
9389
return [
94-
'dropdown-menu',
90+
this.menuClass,
9591
{
9692
'dropdown-menu-right': this.right,
9793
show: this.visible
98-
},
99-
this.menuClass
94+
}
10095
]
10196
},
10297
toggleClasses() {
10398
return [
104-
'dropdown-toggle',
99+
this.toggleClass,
105100
{
106101
'dropdown-toggle-split': this.split,
107102
'dropdown-toggle-no-caret': this.noCaret && !this.split
108-
},
109-
this.toggleClass
103+
}
110104
]
111105
}
112106
},
@@ -149,6 +143,7 @@ export default Vue.extend({
149143
BButton,
150144
{
151145
ref: 'toggle',
146+
staticClass: 'dropdown-toggle',
152147
class: this.toggleClasses,
153148
props: {
154149
variant: this.variant,
@@ -172,23 +167,27 @@ export default Vue.extend({
172167
'ul',
173168
{
174169
ref: 'menu',
170+
staticClass: 'dropdown-menu',
175171
class: this.menuClasses,
176172
attrs: {
177173
role: this.role,
178174
tabindex: '-1',
179175
'aria-labelledby': this.safeId(this.split ? '_BV_button_' : '_BV_toggle_')
180176
},
181177
on: {
182-
mouseover: this.onMouseOver,
183-
keydown: this.onKeydown // tab, up, down, esc
178+
keydown: this.onKeydown // up, down, esc
184179
}
185180
},
186-
this.normalizeSlot('default')
181+
this.normalizeSlot('default', { hide: this.hide })
182+
)
183+
return h(
184+
'div',
185+
{
186+
staticClass: 'dropdown btn-group b-dropdown',
187+
class: this.dropdownClasses,
188+
attrs: { id: this.safeId() }
189+
},
190+
[split, toggle, menu]
187191
)
188-
return h('div', { attrs: { id: this.safeId() }, class: this.dropdownClasses }, [
189-
split,
190-
toggle,
191-
menu
192-
])
193192
}
194193
})

src/components/nav/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ add them (like above) which will produce something like:
204204

205205
Refer to [`<b-dropdown>`](/docs/components/dropdown) for a list of supported sub-components.
206206

207+
### Optionally scoped default slot
208+
209+
<span class="badge badge-info small">NEW in 2.0.0-rc.20</span>
210+
211+
The dropdown default slot is optionally scoped with the following scope available:
212+
213+
| Property or Method | Description |
214+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
215+
| `hide()` | Can be used to close the dropdown menu. Accepts an optional boolean argument, which if `true` returns focus to the toggle button |
216+
207217
## Using in navbar
208218

209219
Prop `is-nav-bar` has been deprecated and will be removed in a future release.

0 commit comments

Comments
 (0)