Skip to content

Commit 01498cb

Browse files
authored
feat(dropdowns): add boundary prop for controlling placement constraint (bootstrap-vue#1440)
* feat(dropdowns): add boundary prop for controlling placement constraint New prop `boundary` which can be `scrollParent` (Popper default), `viewport`, `window` or a reference to an HTML element. Controls how the placement is constrained visually. * Update dropdown.js * Update dropdown.js * Update README.md * Update dropdown.js * Update dropdown.js * Update README.md * Update dropdown.js * Update README.md * Update README.md * Update dropdown.js
1 parent 08fd7ce commit 01498cb

File tree

3 files changed

+46
-1
lines changed

3 files changed

+46
-1
lines changed

src/components/dropdown/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ the toggle button:
171171
<!-- dropdown-offset.vue -->
172172
```
173173

174+
### Boundary constraint
175+
By default, dropdowns are visually constrained to its scroll parent, which will suffice
176+
in most situations. However, if you place a dropdown inside an element that has `overflow: scroll`
177+
(or similar) set, the drodpwon menu may - in some situations - get cut off. To get around this,
178+
you can sepcify a boundary element via the `boundary` prop. Supported values are `'scrollParent'`
179+
(the default), `'viewport'`, `'window'` or a reference to an HTML element. The boundary value
180+
is passed directly to Popper.js's `boundariesElement` configurtion option.
181+
182+
**Note:** when `boundary` is any value other than the default of `'scrollParent'`, the style
183+
`position: static` is applied to to the dropdown component's root element in order to allow the
184+
menu to "break-out" of its scroll container. In some situations this may affect your layout or
185+
positioning of the dropdown trigger button. In these cases you may need to wrap your
186+
dropdown inside another element.
174187

175188
## Split button support
176189
Create a split dropdown button, where the left button provides standard

src/components/dropdown/dropdown.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ export default {
7171
},
7272
[ this.$slots.default ]
7373
)
74-
return h('div', { attrs: { id: t.safeId() }, class: t.dropdownClasses }, [split, toggle, menu])
74+
return h(
75+
'div',
76+
{ attrs: { id: t.safeId() }, class: t.dropdownClasses, style: t.dropdownStyles },
77+
[ split, toggle, menu ]
78+
)
7579
},
7680
props: {
7781
split: {
@@ -97,6 +101,12 @@ export default {
97101
role: {
98102
type: String,
99103
default: 'menu'
104+
},
105+
boundary: {
106+
// String: `scrollParent`, `window` or `viewport`
107+
// Object: HTML Element reference
108+
type: [String, Object],
109+
default: 'scrollParent'
100110
}
101111
},
102112
computed: {
@@ -109,6 +119,16 @@ export default {
109119
this.visible ? 'show' : ''
110120
]
111121
},
122+
dropdownStyles () {
123+
// Position `static` is needed to allow menu to "breakout" of the scrollParent boundaries
124+
// See https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
125+
if (this.boundary === 'scrollParent' || !this.boundary) {
126+
return {}
127+
}
128+
// We enable this feature only when the user supplies a boundary other than `scrollParent`
129+
// to preserve default functionality
130+
return { position: 'static' }
131+
},
112132
menuClasses () {
113133
return [
114134
'dropdown-menu',

src/mixins/dropdown.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ export default {
8282
// Use new namespaced events
8383
this.listenOnRoot('bv::link::clicked', this.rootCloseListener)
8484
},
85+
/* istanbul ignore next: not easy to test */
8586
deactivated () {
8687
// In case we are inside a `<keep-alive>`
8788
this.visible = false
8889
this.setTouchStart(false)
8990
this.removePopper()
9091
},
92+
/* istanbul ignore next: not easy to test */
9193
beforeDestroy () {
9294
this.visible = false
9395
this.setTouchStart(false)
@@ -194,6 +196,11 @@ export default {
194196
}
195197
}
196198
}
199+
if (this.boundary) {
200+
popperConfig.modifiers.preventOverflow = {
201+
boundariesElement: this.boundary
202+
}
203+
}
197204
return assign(popperConfig, this.popperOpts || {})
198205
},
199206
setTouchStart (on) {
@@ -214,6 +221,7 @@ export default {
214221
})
215222
}
216223
},
224+
/* istanbul ignore next: not easy to test */
217225
_noop () {
218226
// Do nothing event handler (used in touchstart event handler)
219227
},
@@ -264,6 +272,7 @@ export default {
264272
}
265273
this.$emit('click', evt)
266274
},
275+
/* istanbul ignore next: not easy to test */
267276
onKeydown (evt) {
268277
// Called from dropdown menu context
269278
const key = evt.keyCode
@@ -281,6 +290,7 @@ export default {
281290
this.focusNext(evt, true)
282291
}
283292
},
293+
/* istanbul ignore next: not easy to test */
284294
onEsc (evt) {
285295
if (this.visible) {
286296
this.visible = false
@@ -290,6 +300,7 @@ export default {
290300
this.$nextTick(this.focusToggler)
291301
}
292302
},
303+
/* istanbul ignore next: not easy to test */
293304
onTab (evt) {
294305
if (this.visible) {
295306
// TODO: Need special handler for dealing with form inputs
@@ -304,6 +315,7 @@ export default {
304315
}
305316
this.visible = false
306317
},
318+
/* istanbul ignore next: not easy to test */
307319
onMouseOver (evt) {
308320
// Focus the item on hover
309321
// TODO: Special handling for inputs? Inputs are in a special .dropdown-form container

0 commit comments

Comments
 (0)