diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md index 1fd609ab98a..21c364af14f 100644 --- a/src/components/dropdown/README.md +++ b/src/components/dropdown/README.md @@ -133,7 +133,7 @@ Like to move your menu away from the toggle buttons a bit? Then use the `offset` number of pixels to push right (or left when negative) from the toggle button: - Specified as a number of pixels: positive for right shift, negative for left shift. -- Specify the distance in CSS units (i.e. `0.3rem`, `4px`, `1.2em`, etc) passed as a string. +- Specify the distance in CSS units (i.e. `0.3rem`, `4px`, `1.2em`, etc.) passed as a string. ```html
@@ -156,12 +156,23 @@ specify a boundary element via the `boundary` prop. Supported values are `'scrol default), `'viewport'`, `'window'` or a reference to an HTML element. The boundary value is passed directly to Popper.js's `boundariesElement` configuration option. -**Note:** when `boundary` is any value other than the default of `'scrollParent'`, the style +**Note:** When `boundary` is any value other than the default of `'scrollParent'`, the style `position: static` is applied to to the dropdown component's root element in order to allow the menu to "break-out" of its scroll container. In some situations this may affect your layout or positioning of the dropdown trigger button. In these cases you may need to wrap your dropdown inside another element. +### Advanced Popper.js configuration + +If you need some advanced Popper.js configuration to make dropdowns behave to your needs, you can +use the `popper-opts` prop to pass down a custom configuration object which will be deeply merged +with the BootstrapVue defaults. + +Head to the [Popper.js docs](https://popper.js.org/docs/v1/) to see all the configuration options. + +**Note**: The props `offset`, `boundary` and `no-flip` may loose their effect when you overwrite the +Popper.js configuration. + ## Split button support Create a split dropdown button, where the left button provides standard `click` event and link diff --git a/src/components/dropdown/dropdown.js b/src/components/dropdown/dropdown.js index 286d5466fbe..93e5f27678d 100644 --- a/src/components/dropdown/dropdown.js +++ b/src/components/dropdown/dropdown.js @@ -100,9 +100,10 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({ props, computed: { dropdownClasses() { - const { block, split, boundary } = this + const { block, split } = this return [ this.directionClass, + this.boundaryClass, { show: this.visible, // The 'btn-group' class is required in `split` mode for button alignment @@ -111,11 +112,7 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({ 'btn-group': split || !block, // When `block` is enabled and we are in `split` mode the 'd-flex' class // needs to be applied to allow the buttons to stretch to full width - 'd-flex': block && split, - // Position `static` is needed to allow menu to "breakout" of the `scrollParent` - // boundaries when boundary is anything other than `scrollParent` - // See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786 - 'position-static': boundary !== 'scrollParent' || !boundary + 'd-flex': block && split } ] }, diff --git a/src/components/form-datepicker/README.md b/src/components/form-datepicker/README.md index 28dee0bd5fb..116de34cbc9 100644 --- a/src/components/form-datepicker/README.md +++ b/src/components/form-datepicker/README.md @@ -300,8 +300,8 @@ either `min` or `max` (depending on which is closes to today's date). Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to control the positioning of the popup calendar. -Refer to the [`` documentation](/docs/components/dropdown) for details on the effects -and usage of these props. +Refer to the [`` positioning section](/docs/components/dropdown#positioning) for details +on the effects and usage of these props. ### Initial open calendar date diff --git a/src/components/form-timepicker/README.md b/src/components/form-timepicker/README.md index f64c4ed011f..156156a1640 100644 --- a/src/components/form-timepicker/README.md +++ b/src/components/form-timepicker/README.md @@ -205,8 +205,8 @@ keep these labels short. Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to control the positioning of the popup calendar. -Refer to the [`` documentation](/docs/components/dropdown) for details on the effects -and usage of these props. +Refer to the [`` positioning section](/docs/components/dropdown#positioning) for details +on the effects and usage of these props. ### Button only mode diff --git a/src/components/nav/README.md b/src/components/nav/README.md index 924287d2b52..8b4076a586e 100644 --- a/src/components/nav/README.md +++ b/src/components/nav/README.md @@ -185,7 +185,7 @@ Use `` to place dropdown items within your nav.
- + ``` 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 impacted due to larger overall memory utilization. You can instruct `` to render the menu contents only when it is shown by setting the `lazy` prop to true. +### Dropdown placement + +Use the dropdown props `right`, `dropup`, `dropright`, `dropleft`, `no-flip`, and `offset` to +control the positioning of ``. + +Refer to the [`` positioning section](/docs/components/dropdown#positioning) for details +on the effects and usage of these props. + ### Dropdown implementation note Note that the toggle button is actually rendered as a link `` tag with `role="button"` for @@ -438,7 +446,7 @@ add the role to the `` itself, as this would prevent it from being announ list by assistive technologies. When using a `` in your ``, be sure to assign a unique `id` prop value -to the `` so that the appropriate `aria-*` attributes can be automatically +to the `` so that the appropriate `aria-*` attributes can be automatically generated. ### Tabbed interface accessibility diff --git a/src/components/nav/nav-item-dropdown.js b/src/components/nav/nav-item-dropdown.js index 3facb61e9d2..b8745b0dc76 100644 --- a/src/components/nav/nav-item-dropdown.js +++ b/src/components/nav/nav-item-dropdown.js @@ -28,7 +28,7 @@ export const BNavItemDropdown = /*#__PURE__*/ Vue.extend({ return true }, dropdownClasses() { - return [this.directionClass, { show: this.visible }] + return [this.directionClass, this.boundaryClass, { show: this.visible }] }, menuClasses() { return [ diff --git a/src/mixins/dropdown.js b/src/mixins/dropdown.js index 5147d634e3a..304ccbb612b 100644 --- a/src/mixins/dropdown.js +++ b/src/mixins/dropdown.js @@ -4,6 +4,7 @@ import { BvEvent } from '../utils/bv-event.class' import { attemptFocus, closest, contains, isVisible, requestAF, selectAll } from '../utils/dom' import { stopEvent } from '../utils/events' import { isNull } from '../utils/inspect' +import { mergeDeep } from '../utils/object' import { HTMLElement } from '../utils/safe-types' import { warn } from '../utils/warn' import clickOutMixin from './click-out' @@ -68,17 +69,17 @@ export const commonProps = { default: false }, offset: { - // Number of pixels to offset menu, or a CSS unit value (i.e. 1px, 1rem, etc) + // Number of pixels to offset menu, or a CSS unit value (i.e. `1px`, `1rem`, etc.) type: [Number, String], default: 0 }, noFlip: { - // Disable auto-flipping of menu from bottom<=>top + // Disable auto-flipping of menu from bottom <=> top type: Boolean, default: false }, popperOpts: { - // type: Object, + type: Object, default: () => {} }, boundary: { @@ -128,6 +129,13 @@ export default { return 'dropleft' } return '' + }, + boundaryClass() { + // Position `static` is needed to allow menu to "breakout" of the `scrollParent` + // boundaries when boundary is anything other than `scrollParent` + // See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786 + const { boundary } = this + return boundary !== 'scrollParent' || !boundary ? 'position-static' : '' } }, watch: { @@ -267,10 +275,11 @@ export default { flip: { enabled: !this.noFlip } } } - if (this.boundary) { - popperConfig.modifiers.preventOverflow = { boundariesElement: this.boundary } + const boundariesElement = this.boundary + if (boundariesElement) { + popperConfig.modifiers.preventOverflow = { boundariesElement } } - return { ...popperConfig, ...(this.popperOpts || {}) } + return mergeDeep(popperConfig, this.popperOpts || {}) }, // Turn listeners on/off while open whileOpenListen(isOpen) { diff --git a/src/utils/bv-form-btn-label-control.js b/src/utils/bv-form-btn-label-control.js index 334f5176cab..4a6811f6874 100644 --- a/src/utils/bv-form-btn-label-control.js +++ b/src/utils/bv-form-btn-label-control.js @@ -264,7 +264,10 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({ on: { // Disable bubbling of the click event to // prevent menu from closing and re-opening - '!click': stopEvent + + '!click': /* istanbul ignore next */ evt => { + stopEvent(evt, { preventDefault: false }) + } } }, [ @@ -281,6 +284,7 @@ export const BVFormBtnLabelControl = /*#__PURE__*/ Vue.extend({ staticClass: 'b-form-btn-label-control dropdown', class: [ this.directionClass, + this.boundaryClass, { 'btn-group': buttonOnly, 'form-control': !buttonOnly, diff --git a/src/utils/events.js b/src/utils/events.js index 91bbeb4765e..962acf94460 100644 --- a/src/utils/events.js +++ b/src/utils/events.js @@ -42,8 +42,13 @@ export const eventOnOff = (on, ...args) => { } // Utility method to prevent the default event handling and propagation -export const stopEvent = (evt, { propagation = true, immediatePropagation = false } = {}) => { - evt.preventDefault() +export const stopEvent = ( + evt, + { preventDefault = true, propagation = true, immediatePropagation = false } = {} +) => { + if (preventDefault) { + evt.preventDefault() + } if (propagation) { evt.stopPropagation() } diff --git a/src/utils/object.js b/src/utils/object.js index 72cba7268cd..55caa58bb04 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -61,6 +61,26 @@ export const omit = (obj, props) => .filter(key => props.indexOf(key) === -1) .reduce((result, key) => ({ ...result, [key]: obj[key] }), {}) +/** + * Merges two object deeply together + * @link https://gist.github.com/Salakar/1d7137de9cb8b704e48a + */ +export const mergeDeep = (target, source) => { + if (isObject(target) && isObject(source)) { + keys(source).forEach(key => { + if (isObject(source[key])) { + if (!target[key] || !isObject(target[key])) { + target[key] = source[key] + } + mergeDeep(target[key], source[key]) + } else { + assign(target, { [key]: source[key] }) + } + }) + } + return target +} + /** * Convenience method to create a read-only descriptor */ diff --git a/src/utils/object.spec.js b/src/utils/object.spec.js index 50b29841816..4ea27d07ef5 100644 --- a/src/utils/object.spec.js +++ b/src/utils/object.spec.js @@ -1,4 +1,4 @@ -import { pick, omit } from './object' +import { pick, omit, mergeDeep } from './object' describe('utils/object', () => { it('pick() works', async () => { @@ -16,4 +16,46 @@ describe('utils/object', () => { expect(omit(obj, Object.keys(obj))).toEqual({}) expect(omit(obj, [])).toEqual(obj) }) + + it('mergeDeep() works', async () => { + const A = { + a: { + loc: 'Earth', + title: 'Hello World', + type: 'Planet', + deeper: { + map: new Map([['a', 'AAA'], ['b', 'BBB']]), + mapId: 15473 + } + } + } + const B = { + a: { + type: 'Star', + deeper: { + mapId: 9999, + alt_map: new Map([['x', 'XXXX'], ['y', 'YYYY']]) + } + } + } + + const C = mergeDeep(A, B) + const D = mergeDeep({ a: 1 }, { b: { c: { d: { e: 12345 } } } }) + const E = mergeDeep({ b: { c: 'hallo' } }, { b: { c: { d: { e: 12345 } } } }) + const F = mergeDeep( + { b: { c: { d: { e: 12345 } }, d: 'dag', f: 'one' } }, + { b: { c: 'hallo', e: 'ok', f: 'two' } } + ) + + expect(C.a.type).toEqual('Star') + expect(C.a.deeper.alt_map.get('x')).toEqual('XXXX') + expect(C.a.deeper.map.get('b')).toEqual('BBB') + expect(D.a).toEqual(1) + expect(D.b.c.d.e).toEqual(12345) + expect(E.b.c.d.e).toEqual(12345) + expect(F.b.c).toEqual('hallo') + expect(F.b.d).toEqual('dag') + expect(F.b.e).toEqual('ok') + expect(F.b.f).toEqual('two') + }) })