Skip to content

Commit 7ec2205

Browse files
authored
fix(b-form-datepicker/b-form-timepicker/b-nav-item-dropdown): dropdown positioning handling (closes #5700, #5630) (#5765)
* fix(b-form-datepicker/b-form-timepicker/b-nav-item-dropdown): dropdown positioning handling * Update events.js * Update bv-form-btn-label-control.js * Update bv-form-btn-label-control.js * Update bv-form-btn-label-control.js
1 parent 949ecf7 commit 7ec2205

File tree

11 files changed

+121
-25
lines changed

11 files changed

+121
-25
lines changed

src/components/dropdown/README.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Like to move your menu away from the toggle buttons a bit? Then use the `offset`
133133
number of pixels to push right (or left when negative) from the toggle button:
134134

135135
- Specified as a number of pixels: positive for right shift, negative for left shift.
136-
- Specify the distance in CSS units (i.e. `0.3rem`, `4px`, `1.2em`, etc) passed as a string.
136+
- Specify the distance in CSS units (i.e. `0.3rem`, `4px`, `1.2em`, etc.) passed as a string.
137137

138138
```html
139139
<div>
@@ -156,12 +156,23 @@ specify a boundary element via the `boundary` prop. Supported values are `'scrol
156156
default), `'viewport'`, `'window'` or a reference to an HTML element. The boundary value is passed
157157
directly to Popper.js's `boundariesElement` configuration option.
158158

159-
**Note:** when `boundary` is any value other than the default of `'scrollParent'`, the style
159+
**Note:** When `boundary` is any value other than the default of `'scrollParent'`, the style
160160
`position: static` is applied to to the dropdown component's root element in order to allow the menu
161161
to "break-out" of its scroll container. In some situations this may affect your layout or
162162
positioning of the dropdown trigger button. In these cases you may need to wrap your dropdown inside
163163
another element.
164164

165+
### Advanced Popper.js configuration
166+
167+
If you need some advanced Popper.js configuration to make dropdowns behave to your needs, you can
168+
use the `popper-opts` prop to pass down a custom configuration object which will be deeply merged
169+
with the BootstrapVue defaults.
170+
171+
Head to the [Popper.js docs](https://popper.js.org/docs/v1/) to see all the configuration options.
172+
173+
**Note**: The props `offset`, `boundary` and `no-flip` may loose their effect when you overwrite the
174+
Popper.js configuration.
175+
165176
## Split button support
166177

167178
Create a split dropdown button, where the left button provides standard `click` event and link

src/components/dropdown/dropdown.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({
100100
props,
101101
computed: {
102102
dropdownClasses() {
103-
const { block, split, boundary } = this
103+
const { block, split } = this
104104
return [
105105
this.directionClass,
106+
this.boundaryClass,
106107
{
107108
show: this.visible,
108109
// The 'btn-group' class is required in `split` mode for button alignment
@@ -111,11 +112,7 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({
111112
'btn-group': split || !block,
112113
// When `block` is enabled and we are in `split` mode the 'd-flex' class
113114
// needs to be applied to allow the buttons to stretch to full width
114-
'd-flex': block && split,
115-
// Position `static` is needed to allow menu to "breakout" of the `scrollParent`
116-
// boundaries when boundary is anything other than `scrollParent`
117-
// See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
118-
'position-static': boundary !== 'scrollParent' || !boundary
115+
'd-flex': block && split
119116
}
120117
]
121118
},

src/components/form-datepicker/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,8 @@ either `min` or `max` (depending on which is closes to today's date).
300300
Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to
301301
control the positioning of the popup calendar.
302302

303-
Refer to the [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
304-
and usage of these props.
303+
Refer to the [`<b-dropdown>` positioning section](/docs/components/dropdown#positioning) for details
304+
on the effects and usage of these props.
305305

306306
### Initial open calendar date
307307

src/components/form-timepicker/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,8 @@ keep these labels short.
205205
Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to
206206
control the positioning of the popup calendar.
207207

208-
Refer to the [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
209-
and usage of these props.
208+
Refer to the [`<b-dropdown>` positioning section](/docs/components/dropdown#positioning) for details
209+
on the effects and usage of these props.
210210

211211
### Button only mode
212212

src/components/nav/README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ Use `<b-nav-item-dropdown>` to place dropdown items within your nav.
185185
</b-nav>
186186
</div>
187187

188-
<!-- b-nav-dropdown.vue -->
188+
<!-- b-nav-item-dropdown.vue -->
189189
```
190190

191191
Sometimes you want to add your own class names to the generated dropdown toggle button, that by
@@ -223,6 +223,14 @@ shown. When there are a large number of dropdowns rendered on the same page, per
223223
impacted due to larger overall memory utilization. You can instruct `<b-nav-item-dropdown>` to
224224
render the menu contents only when it is shown by setting the `lazy` prop to true.
225225

226+
### Dropdown placement
227+
228+
Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to
229+
control the positioning of `<b-nav-item-dropdown>`.
230+
231+
Refer to the [`<b-dropdown>` positioning section](/docs/components/dropdown#positioning) for details
232+
on the effects and usage of these props.
233+
226234
### Dropdown implementation note
227235

228236
Note that the toggle button is actually rendered as a link `<a>` tag with `role="button"` for
@@ -438,7 +446,7 @@ add the role to the `<b-nav>` itself, as this would prevent it from being announ
438446
list by assistive technologies.
439447

440448
When using a `<b-nav-item-dropdown>` in your `<b-nav>`, be sure to assign a unique `id` prop value
441-
to the `<b-nav-dropdown>` so that the appropriate `aria-*` attributes can be automatically
449+
to the `<b-nav-item-dropdown>` so that the appropriate `aria-*` attributes can be automatically
442450
generated.
443451

444452
### Tabbed interface accessibility

src/components/nav/nav-item-dropdown.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const BNavItemDropdown = /*#__PURE__*/ Vue.extend({
2828
return true
2929
},
3030
dropdownClasses() {
31-
return [this.directionClass, { show: this.visible }]
31+
return [this.directionClass, this.boundaryClass, { show: this.visible }]
3232
},
3333
menuClasses() {
3434
return [

src/mixins/dropdown.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BvEvent } from '../utils/bv-event.class'
44
import { attemptFocus, closest, contains, isVisible, requestAF, selectAll } from '../utils/dom'
55
import { stopEvent } from '../utils/events'
66
import { isNull } from '../utils/inspect'
7+
import { mergeDeep } from '../utils/object'
78
import { HTMLElement } from '../utils/safe-types'
89
import { warn } from '../utils/warn'
910
import clickOutMixin from './click-out'
@@ -68,17 +69,17 @@ export const commonProps = {
6869
default: false
6970
},
7071
offset: {
71-
// Number of pixels to offset menu, or a CSS unit value (i.e. 1px, 1rem, etc)
72+
// Number of pixels to offset menu, or a CSS unit value (i.e. `1px`, `1rem`, etc.)
7273
type: [Number, String],
7374
default: 0
7475
},
7576
noFlip: {
76-
// Disable auto-flipping of menu from bottom<=>top
77+
// Disable auto-flipping of menu from bottom <=> top
7778
type: Boolean,
7879
default: false
7980
},
8081
popperOpts: {
81-
// type: Object,
82+
type: Object,
8283
default: () => {}
8384
},
8485
boundary: {
@@ -128,6 +129,13 @@ export default {
128129
return 'dropleft'
129130
}
130131
return ''
132+
},
133+
boundaryClass() {
134+
// Position `static` is needed to allow menu to "breakout" of the `scrollParent`
135+
// boundaries when boundary is anything other than `scrollParent`
136+
// See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
137+
const { boundary } = this
138+
return boundary !== 'scrollParent' || !boundary ? 'position-static' : ''
131139
}
132140
},
133141
watch: {
@@ -267,10 +275,11 @@ export default {
267275
flip: { enabled: !this.noFlip }
268276
}
269277
}
270-
if (this.boundary) {
271-
popperConfig.modifiers.preventOverflow = { boundariesElement: this.boundary }
278+
const boundariesElement = this.boundary
279+
if (boundariesElement) {
280+
popperConfig.modifiers.preventOverflow = { boundariesElement }
272281
}
273-
return { ...popperConfig, ...(this.popperOpts || {}) }
282+
return mergeDeep(popperConfig, this.popperOpts || {})
274283
},
275284
// Turn listeners on/off while open
276285
whileOpenListen(isOpen) {

src/utils/bv-form-btn-label-control.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,10 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
264264
on: {
265265
// Disable bubbling of the click event to
266266
// prevent menu from closing and re-opening
267-
'!click': stopEvent
267+
268+
'!click': /* istanbul ignore next */ evt => {
269+
stopEvent(evt, { preventDefault: false })
270+
}
268271
}
269272
},
270273
[
@@ -281,6 +284,7 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({
281284
staticClass: 'b-form-btn-label-control dropdown',
282285
class: [
283286
this.directionClass,
287+
this.boundaryClass,
284288
{
285289
'btn-group': buttonOnly,
286290
'form-control': !buttonOnly,

src/utils/events.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ export const eventOnOff = (on, ...args) => {
4242
}
4343

4444
// Utility method to prevent the default event handling and propagation
45-
export const stopEvent = (evt, { propagation = true, immediatePropagation = false } = {}) => {
46-
evt.preventDefault()
45+
export const stopEvent = (
46+
evt,
47+
{ preventDefault = true, propagation = true, immediatePropagation = false } = {}
48+
) => {
49+
if (preventDefault) {
50+
evt.preventDefault()
51+
}
4752
if (propagation) {
4853
evt.stopPropagation()
4954
}

src/utils/object.js

+20
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ export const omit = (obj, props) =>
6161
.filter(key => props.indexOf(key) === -1)
6262
.reduce((result, key) => ({ ...result, [key]: obj[key] }), {})
6363

64+
/**
65+
* Merges two object deeply together
66+
* @link https://gist.github.com/Salakar/1d7137de9cb8b704e48a
67+
*/
68+
export const mergeDeep = (target, source) => {
69+
if (isObject(target) && isObject(source)) {
70+
keys(source).forEach(key => {
71+
if (isObject(source[key])) {
72+
if (!target[key] || !isObject(target[key])) {
73+
target[key] = source[key]
74+
}
75+
mergeDeep(target[key], source[key])
76+
} else {
77+
assign(target, { [key]: source[key] })
78+
}
79+
})
80+
}
81+
return target
82+
}
83+
6484
/**
6585
* Convenience method to create a read-only descriptor
6686
*/

src/utils/object.spec.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { pick, omit } from './object'
1+
import { pick, omit, mergeDeep } from './object'
22

33
describe('utils/object', () => {
44
it('pick() works', async () => {
@@ -16,4 +16,46 @@ describe('utils/object', () => {
1616
expect(omit(obj, Object.keys(obj))).toEqual({})
1717
expect(omit(obj, [])).toEqual(obj)
1818
})
19+
20+
it('mergeDeep() works', async () => {
21+
const A = {
22+
a: {
23+
loc: 'Earth',
24+
title: 'Hello World',
25+
type: 'Planet',
26+
deeper: {
27+
map: new Map([['a', 'AAA'], ['b', 'BBB']]),
28+
mapId: 15473
29+
}
30+
}
31+
}
32+
const B = {
33+
a: {
34+
type: 'Star',
35+
deeper: {
36+
mapId: 9999,
37+
alt_map: new Map([['x', 'XXXX'], ['y', 'YYYY']])
38+
}
39+
}
40+
}
41+
42+
const C = mergeDeep(A, B)
43+
const D = mergeDeep({ a: 1 }, { b: { c: { d: { e: 12345 } } } })
44+
const E = mergeDeep({ b: { c: 'hallo' } }, { b: { c: { d: { e: 12345 } } } })
45+
const F = mergeDeep(
46+
{ b: { c: { d: { e: 12345 } }, d: 'dag', f: 'one' } },
47+
{ b: { c: 'hallo', e: 'ok', f: 'two' } }
48+
)
49+
50+
expect(C.a.type).toEqual('Star')
51+
expect(C.a.deeper.alt_map.get('x')).toEqual('XXXX')
52+
expect(C.a.deeper.map.get('b')).toEqual('BBB')
53+
expect(D.a).toEqual(1)
54+
expect(D.b.c.d.e).toEqual(12345)
55+
expect(E.b.c.d.e).toEqual(12345)
56+
expect(F.b.c).toEqual('hallo')
57+
expect(F.b.d).toEqual('dag')
58+
expect(F.b.e).toEqual('ok')
59+
expect(F.b.f).toEqual('two')
60+
})
1961
})

0 commit comments

Comments
 (0)