Skip to content

Commit 08fd7ce

Browse files
authored
feat(tooltip+popover): add boundary element config option (positioning constraint) (bootstrap-vue#1439)
* [toolpop.class]: add boundariesElement config defaults to 'scrollParent' (poppoer default) Useful values: 'window' or 'viewport', or an element reference * Update toolpop.js * [v-b-tooltip] add boundary modifiers * [v-b-popover] add boundary modifiers * Update tooltip.class.js * Update toolpop.js * Update tooltip.js * Update popover.js * Update README.md * Update README.md * Update README.md * Update README.md * Update tooltip.html * Update tooltip.class.js * Update popover.js * Update tooltip.js * chore: minor scrollspy update * Update popover.js * Update tooltip.js * Update toolpop.js * Update tooltip.html * Update popover.html * wrong branch :p
1 parent c85a804 commit 08fd7ce

File tree

10 files changed

+70
-6
lines changed

10 files changed

+70
-6
lines changed

src/components/popover/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export default {
237237
| `delay` | `0` | Delay showing and hiding of popover by specified number of milliseconds. Can also be defined as an object in the form of `{ show: 100, hide: 400 }` allowing different show and hide delays | `0` and up, integers only.
238238
| `offset` | `0` | Shift the center of the popover by specified number of pixels. Also affects the position of the popover arrow. | Any negative or positive integer
239239
| `container` | `null` | Element string ID to append rendered popover into. If `null` or element not found, popover is appended to `<body>` (default) | Any valid in-document unique element ID.
240+
| `boundary` | `'scrollParent'` | The container that the popover will be constrained visually. The default should suffice in most cases, but you may need to chagne this if your target element is in a small container with overflow scroll | `'scrollParent'` (default), `'viewport'`, `'window'`, or a reference to an HTML element.
240241

241242

242243
### Programmatically show and hide popover

src/components/popover/fixtures/popover.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ <h4 class="mt-sm-4 ms-sm-4 text-muted">Placement</h4>
33
<div class="row">
44
<div v-for="placement in ['top', 'left', 'right', 'bottom']" :key="placement">
55
<b-btn :id="placement" variant="primary">{{ placement }}</b-btn>
6-
<b-popover :target="placement" :placement="placement" :title="placement">
6+
<b-popover :target="placement" triggers="click" :placement="placement" :title="placement">
77
{{ placement }}
88
</b-popover>
99
</div>
1010
</div>
11+
<h4 class="mt-sm-4 ms-sm-4 text-muted">Directive</h4>
12+
<div class="row">
13+
<b-btn id="directive1" v-b-popover.top.click.viewport="'content'" title="popover" variant="primary">Directive 1</b-btn>
14+
</div>
1115
</div>

src/components/tooltip/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ by `focus`, and the user then clicks the trigger element, they must click it aga
167167
| `delay` | `0` | Delay showing and hiding of tooltip by specified number of milliseconds. Can also be specified as an object in the form of `{ show: 100, hide: 400 }` allowing different show and hide delays | `0` and up, integers only.
168168
| `offset` | `0` | Shift the center of the tooltip by specified number of pixels | Any negative or positive integer
169169
| `container` | `null` | Element string ID to append rendered tooltip into. If `null` or element not found, tooltip is appended to `<body>` (default) | Any valid in-document unique element ID.
170+
| `boundary` | `'scrollParent'` | The container that the tooltip will be constrained visually. The default should suffice in most cases, but you may need to chagne this if your target element is in a small container with overflow scroll | `'scrollParent'` (default), `'viewport'`, `'window'`, or a reference to an HTML element.
170171

171172

172173
### Programmatically show and hide tooltip
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
<div id="app">
2-
<b-btn id="button" variant="outline-success">Live chat</b-btn>
3-
<b-tooltip target="button">Online</b-tooltip>
2+
<b-btn id="button1">Button 1</b-btn>
3+
<b-tooltip target="button1">Tooltip 1</b-tooltip>
4+
<br>
5+
<b-btn id="button2">Button 2</b-btn>
6+
<b-tooltip target="button2" title="Tooltip 2"></b-tooltip>
7+
<br>
8+
<b-btn id="button3">Button 3</b-btn>
9+
<b-tooltip target="button3" boundary="viewport">Tooltip 3</b-tooltip>
10+
<br>
11+
<b-btn id="button4">Button 4</b-btn>
12+
<b-tooltip target="button4" placement="bottom">Tooltip 4</b-tooltip>
13+
<br>
14+
<b-btn id="button5">Button 5</b-btn>
15+
<b-tooltip target="button5" trigger="click">Tooltip 5</b-tooltip>
16+
<br>
17+
<h5>Directive</h5>
18+
<br>
19+
<b-btn id="button6" v-b-tooltip.top.click title="Tooltip 6">Button 6</b-btn>
20+
<br>
21+
<b-btn id="button7" v-b-tooltip.right.focus="'Tooltip 7'">Button 7</b-btn>
422
</div>

src/directives/popover/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ Where `[mod]` can be (all optional):
334334
- `html` to enable rendering raw HTML. by default HTML is escaped and converted to text.
335335
- A delay value in the format of `d###` (where `###` is in ms, defaults to 0).
336336
- An offset value in pixels in the format of `o###` (where `###` is the number of pixels, defaults to 0. Negative values are allowed). Note if an offset is supplied, then the alignment positions will fallback to one of `top`, `bottom`, `left`, or `right`.
337+
- A boundary setting of `window` or `viewport`. The element to constrain the visual placement of the popover. If not specified, the boundary defaults to the trigger element's scroll parent (in most cases this will suffice).
337338

338339
Where `[container]` can be (optional):
339340
- An element ID (minus the #) to place the popover markup in when visible

src/directives/popover/popover.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const inBrowser = typeof window !== 'undefined' && typeof document !== 'undefine
88
// Key which we use to store tooltip object on element
99
const BVPO = '__BV_PopOver__'
1010

11-
// Vlid event triggers
11+
// Valid event triggers
1212
const validTriggers = {
1313
'focus': true,
1414
'hover': true,
@@ -18,6 +18,7 @@ const validTriggers = {
1818

1919
// Build a PopOver config based on bindings (if any)
2020
// Arguments and modifiers take precedence over pased value config object
21+
/* istanbul ignore next: not easy to test */
2122
function parseBindings (bindings) {
2223
// We start out with a blank config
2324
let config = {}
@@ -51,6 +52,9 @@ function parseBindings (bindings) {
5152
} else if (/^(auto|top(left|right)?|bottom(left|right)?|left(top|bottom)?|right(top|bottom)?)$/.test(mod)) {
5253
// placement of popover
5354
config.placement = mod
55+
} else if (/^(window|viewport)$/.test(mod)) {
56+
// bounday of popover
57+
config.boundary = mod
5458
} else if (/^d\d+$/.test(mod)) {
5559
// delay value
5660
const delay = parseInt(mod.slice(1), 10) || 0
@@ -101,6 +105,7 @@ function parseBindings (bindings) {
101105
//
102106
// Add or Update popover on our element
103107
//
108+
/* istanbul ignore next: not easy to test */
104109
function applyBVPO (el, bindings, vnode) {
105110
if (!inBrowser) {
106111
return
@@ -120,6 +125,7 @@ function applyBVPO (el, bindings, vnode) {
120125
//
121126
// Remove popover on our element
122127
//
128+
/* istanbul ignore next */
123129
function removeBVPO (el) {
124130
if (!inBrowser) {
125131
return
@@ -134,6 +140,7 @@ function removeBVPO (el) {
134140
/*
135141
* Export our directive
136142
*/
143+
/* istanbul ignore next: not easy to test */
137144
export default {
138145
bind (el, bindings, vnode) {
139146
applyBVPO(el, bindings, vnode)

src/directives/tooltip/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ Where [modX] can be (all optional):
247247
- `html` to enable rendering raw HTML. by default HTML is escaped and converted to text
248248
- A delay value in the format of `d###` (where `###` is in ms, defaults to 0);
249249
- An offset value in pixels in the format of `o###` (where `###` is the number of pixels, defaults to 0. Negative values allowed)
250+
- A boundary setting of `window` or `viewport`. The element to constrain the visual placement of the tooltip. If not specified, the boundary defaults to the trigger element's scroll parent (in most cases this will suffice).
250251

251252
Where `<value>` can be (optional):
252253
- A string containing the title of the tooltip
@@ -268,6 +269,7 @@ Where `<value>` can be (optional):
268269
| `trigger` | String | `'hover focus'` | How tooltip is triggered: `click`, `hover`, `focus`. You may pass multiple triggers; separate them with a space.
269270
| `offset` | Number or String | `0` | Offset of the tooltip relative to its target. For more information refer to Popper.js's offset docs.
270271
| `fallbackPlacement` | String or Array | `'flip'` | Allow to specify which position Popper will use on fallback. For more information refer to Popper.js's behavior docs
272+
| `boundary` | String or HTMLElement reference |`'scrollParent'` | The container that the tooltip will be constrained visually. The default should suffice in most cases, but you may need to chagne this if your target element is in a small container with overflow scroll. Supported values: `'scrollParent'` (default), `'viewport'`, `'window'`, or a reference to an HTML element.
271273

272274

273275
### Usage

src/directives/tooltip/tooltip.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const validTriggers = {
1818

1919
// Build a ToolTip config based on bindings (if any)
2020
// Arguments and modifiers take precedence over passed value config object
21+
/* istanbul ignore next: not easy to test */
2122
function parseBindings (bindings) {
2223
// We start out with a blank config
2324
let config = {}
@@ -51,6 +52,9 @@ function parseBindings (bindings) {
5152
} else if (/^(auto|top(left|right)?|bottom(left|right)?|left(top|bottom)?|right(top|bottom)?)$/.test(mod)) {
5253
// placement of tooltip
5354
config.placement = mod
55+
} else if (/^(window|viewport)$/.test(mod)) {
56+
// bounday of tooltip
57+
config.boundary = mod
5458
} else if (/^d\d+$/.test(mod)) {
5559
// delay value
5660
const delay = parseInt(mod.slice(1), 10) || 0
@@ -101,6 +105,7 @@ function parseBindings (bindings) {
101105
//
102106
// Add or Update tooltip on our element
103107
//
108+
/* istanbul ignore next: not easy to test */
104109
function applyBVTT (el, bindings, vnode) {
105110
if (!inBrowser) {
106111
return
@@ -120,6 +125,7 @@ function applyBVTT (el, bindings, vnode) {
120125
//
121126
// Remove tooltip on our element
122127
//
128+
/* istanbul ignore next: not easy to test */
123129
function removeBVTT (el) {
124130
if (!inBrowser) {
125131
return
@@ -134,6 +140,7 @@ function removeBVTT (el) {
134140
/*
135141
* Export our directive
136142
*/
143+
/* istanbul ignore next: not easy to test */
137144
export default {
138145
bind (el, bindings, vnode) {
139146
applyBVTT(el, bindings, vnode)

src/mixins/toolpop.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export default {
5454
type: String,
5555
default: null
5656
},
57+
boundary: {
58+
// String: scrollParent, window, or viewport
59+
// Element: element reference
60+
type: [String, Object],
61+
default: 'scrollParent'
62+
},
5763
show: {
5864
type: Boolean,
5965
default: false
@@ -116,17 +122,20 @@ export default {
116122
this._toolpop.updateConfig(this.getConfig())
117123
}
118124
},
125+
/* istanbul ignore next: not easy to test */
119126
activated () {
120127
// Called when component is inside a <keep-alive> and component brought offline
121128
this.setObservers(true)
122129
},
130+
/* istanbul ignore next: not easy to test */
123131
deactivated () {
124132
// Called when component is inside a <keep-alive> and component taken offline
125133
if (this._toolpop) {
126134
this.setObservers(false)
127135
this._toolpop.hide()
128136
}
129137
},
138+
/* istanbul ignore next: not easy to test */
130139
beforeDestroy () {
131140
// Shutdown our local event listeners
132141
this.$off('open', this.onOpen)
@@ -154,6 +163,8 @@ export default {
154163
placement: PLACEMENTS[this.placement] || 'auto',
155164
// Container curently needs to be an ID with '#' prepended, if null then body is used
156165
container: cont ? (/^#/.test(cont) ? cont : `#${cont}`) : false,
166+
// boundariesElement passed to popper
167+
boundary: this.boundary,
157168
// Show/Hide delay
158169
delay: delay || 0,
159170
// Offset can be css distance. if no units, pixels are assumed
@@ -277,6 +288,7 @@ export default {
277288
this.$el.appendChild(this.$refs.content)
278289
}
279290
},
291+
/* istanbul ignore next: not easy to test */
280292
setObservers (on) {
281293
if (on) {
282294
if (this.$refs.title) {

src/utils/tooltip.class.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ const Defaults = {
8686
arrowPadding: 6,
8787
container: false,
8888
fallbackPlacement: 'flip',
89-
callbacks: {}
89+
callbacks: {},
90+
boundary: 'scrollParent'
9091
}
9192

9293
// Transition Event names
@@ -408,6 +409,7 @@ class ToolTip {
408409
}
409410

410411
// Transitionend Callback
412+
/* istanbul ignore next */
411413
const complete = () => {
412414
if (this.$hoverState !== HoverState.SHOW && tip.parentNode) {
413415
// Remove tip from dom, and force recompile on next show
@@ -493,6 +495,7 @@ class ToolTip {
493495
this.$popper = null
494496
}
495497

498+
/* istanbul ignore next */
496499
transitionOnce (tip, complete) {
497500
const transEvents = this.getTransitionEndEvents()
498501
let called = false
@@ -710,6 +713,7 @@ class ToolTip {
710713
}
711714
}
712715

716+
/* istanbul ignore next */
713717
setRouteWatcher (on) {
714718
if (on) {
715719
this.setRouteWatcher(false)
@@ -731,6 +735,7 @@ class ToolTip {
731735
}
732736
}
733737

738+
/* istanbul ignore next */
734739
setModalListener (on) {
735740
const modal = closest(MODAL_CLASS, this.$element)
736741
if (!modal) {
@@ -743,6 +748,7 @@ class ToolTip {
743748
}
744749
}
745750

751+
/* istanbul ignore next */
746752
setRootListener (on) {
747753
// Listen for global 'bv::{hide|show}::{tooltip|popover}' hide request event
748754
if (this.$root) {
@@ -797,6 +803,7 @@ class ToolTip {
797803
}
798804
}
799805

806+
/* istanbul ignore next */
800807
setOnTouchStartListener (on) {
801808
// if this is a touch-enabled device we add extra
802809
// empty mouseover listeners to the body's immediate children;
@@ -813,6 +820,7 @@ class ToolTip {
813820
}
814821
}
815822

823+
/* istanbul ignore next */
816824
_noop () {
817825
// Empty noop handler for ontouchstart devices
818826
}
@@ -827,6 +835,7 @@ class ToolTip {
827835
}
828836

829837
// Enter handler
838+
/* istanbul ignore next */
830839
enter (e) {
831840
if (e) {
832841
this.$activeTrigger[e.type === 'focusin' ? 'focus' : 'hover'] = true
@@ -849,6 +858,7 @@ class ToolTip {
849858
}
850859

851860
// Leave handler
861+
/* istanbul ignore next */
852862
leave (e) {
853863
if (e) {
854864
this.$activeTrigger[e.type === 'focusout' ? 'focus' : 'hover'] = false
@@ -880,7 +890,8 @@ class ToolTip {
880890
modifiers: {
881891
offset: { offset: this.getOffset(placement, tip) },
882892
flip: { behavior: this.$config.fallbackPlacement },
883-
arrow: { element: '.arrow' }
893+
arrow: { element: '.arrow' },
894+
preventOverflow: { boundariesElement: this.$config.boundary }
884895
},
885896
onCreate: data => {
886897
// Handle flipping arrow classes

0 commit comments

Comments
 (0)