Skip to content
Permalink
Browse files
fix(b-form-datepicker/b-form-timepicker/b-nav-item-dropdown): dropdow…
…n 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
  • Loading branch information
jacobmllr95 committed Sep 13, 2020
1 parent 949ecf7 commit 7ec2205a96e0d14772f1ed6c047a9808a32fbf82
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 25 deletions.
@@ -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
<div>
@@ -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
@@ -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
}
]
},
@@ -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 [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
and usage of these props.
Refer to the [`<b-dropdown>` positioning section](/docs/components/dropdown#positioning) for details
on the effects and usage of these props.

### Initial open calendar date

@@ -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 [`<b-dropdown>` documentation](/docs/components/dropdown) for details on the effects
and usage of these props.
Refer to the [`<b-dropdown>` positioning section](/docs/components/dropdown#positioning) for details
on the effects and usage of these props.

### Button only mode

@@ -185,7 +185,7 @@ Use `<b-nav-item-dropdown>` to place dropdown items within your nav.
</b-nav>
</div>

<!-- b-nav-dropdown.vue -->
<!-- b-nav-item-dropdown.vue -->
```

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 `<b-nav-item-dropdown>` 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 `<b-nav-item-dropdown>`.

Refer to the [`<b-dropdown>` 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 `<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
list by assistive technologies.

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

### Tabbed interface accessibility
@@ -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 [
@@ -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) {
@@ -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,
@@ -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()
}
@@ -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
*/
@@ -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')
})
})

0 comments on commit 7ec2205

Please sign in to comment.