Skip to content

Commit b3ad726

Browse files
feat(b-tooltip): add noninteractive prop (closes #4556) (#4563)
* feat(b-tooltip): add `interactive` prop * Update package.json * Update README.md * Update README.md * Replace `interactive` prop/modifier by `noninteractive` * Update _tooltip.scss * Update README.md * Update _tooltip.scss * Update _popover.scss * Update package.json Co-authored-by: Troy Morehouse <troymore@nbnet.nb.ca>
1 parent f5a2fba commit b3ad726

File tree

13 files changed

+157
-34
lines changed

13 files changed

+157
-34
lines changed

src/components/popover/_popover.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
.popover.b-popover {
33
display: block;
44
opacity: 1;
5+
// Needed due to Bootstrap v4.4 reboot.css changes
6+
outline: 0;
57

68
&.fade:not(.show) {
79
opacity: 0;

src/components/tooltip/README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,20 +170,42 @@ override the `pointer-events` on the disabled element.
170170

171171
| Prop | Default | Description | Supported values |
172172
| -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
173-
| `target` | `null` | Element String ID, or a reference to an element or component, or a function returning either of them, that you want to trigger the tooltip. **Required** | Any valid, in-document unique element ID, element reference or component reference or a function returning any such ID / reference |
173+
| `target` | `null` | Element String ID, or a reference to an element or component, or a function returning either of them, that you want to trigger the tooltip **Required** | Any valid, in-document unique element ID, element reference or component reference or a function returning any such ID / reference |
174174
| `title` | `null` | Tooltip content (text only, no HTML). if HTML is required, place it in the default slot | Plain text |
175-
| `placement` | `'top'` | Tooltip position, relative to the trigger element. | `top`, `bottom`, `left`, `right`, `auto`, `topleft`, `topright`, `bottomleft`, `bottomright`, `lefttop`, `leftbottom`, `righttop`, `rightbottom` |
176-
| `fallback-placement` | `'flip'` | Auto-flip placement behaviour of the tooltip, relative to the trigger element. | `flip`, `clockwise`, `counterclockwise`, or an array of valid placements evaluated from left to right |
175+
| `placement` | `'top'` | Tooltip position, relative to the trigger element | `top`, `bottom`, `left`, `right`, `auto`, `topleft`, `topright`, `bottomleft`, `bottomright`, `lefttop`, `leftbottom`, `righttop`, `rightbottom` |
176+
| `fallback-placement` | `'flip'` | Auto-flip placement behaviour of the tooltip, relative to the trigger element | `flip`, `clockwise`, `counterclockwise`, or an array of valid placements evaluated from left to right |
177177
| `triggers` | `'hover focus'` | Space separated list of event(s), which will trigger open/close of tooltip | `hover`, `focus`, `click`. Note `blur` is a special use case to close tooltip on next click, usually used in conjunction with `click`. |
178178
| `no-fade` | `false` | Disable fade animation when set to `true` | `true` or `false` |
179179
| `delay` | `50` | 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. |
180180
| `offset` | `0` | Shift the center of the tooltip by specified number of pixels | Any negative or positive integer |
181181
| `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. |
182182
| `boundary` | `'scrollParent'` | The container that the tooltip will be constrained visually. The default should suffice in most cases, but you may need to change this if your target element is in a small container with overflow scroll | `'scrollParent'` (default), `'viewport'`, `'window'`, or a reference to an HTML element. |
183-
| `boundary-padding` | `5` | Amount of pixel used to define a minimum distance between the boundaries and the tooltip. This makes sure the tooltip always has a little padding between the edges of its container. | Any positive number |
183+
| `boundary-padding` | `5` | Amount of pixel used to define a minimum distance between the boundaries and the tooltip. This makes sure the tooltip always has a little padding between the edges of its container | Any positive number |
184+
| `noninteractive` | `false` | Wether the tooltip should not be user-interactive | `true` or `false` |
184185
| `variant` | `null` | Contextual color variant for the tooltip | Any contextual theme color variant name |
185186
| `custom-class` | `null` | A custom classname to apply to the tooltip outer wrapper element | A string |
186-
| `id` | `null` | An ID to use on the tooltip root element. If none is provided, one will automatically be generated. If you do provide an ID, it _must_ be guaranteed to be unique on the rendered page. | A valid unique element ID string |
187+
| `id` | `null` | An ID to use on the tooltip root element. If none is provided, one will automatically be generated. If you do provide an ID, it _must_ be guaranteed to be unique on the rendered page | A valid unique element ID string |
188+
189+
### Noninteractive tooltips
190+
191+
BootstrapVue's tooltips are user-interactive by default for accessability reasons. To restore
192+
Bootstraps default behavior apply the `noninteractive` prop:
193+
194+
```html
195+
<div class="text-center">
196+
<div>
197+
<b-button id="tooltip-button-interactive">My tooltip is interactive</b-button>
198+
<b-tooltip target="tooltip-button-interactive">I will stay open when hovered</b-tooltip>
199+
</div>
200+
201+
<div class="mt-3">
202+
<b-button id="tooltip-button-not-interactive">Mine is not...</b-button>
203+
<b-tooltip target="tooltip-button-not-interactive" noninteractive>Catch me if you can!</b-tooltip>
204+
</div>
205+
</div>
206+
207+
<!-- b-tooltip-interactive.vue -->
208+
```
187209

188210
### Variants and custom class
189211

src/components/tooltip/_tooltip.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
.tooltip.b-tooltip {
33
display: block;
44
opacity: $tooltip-opacity;
5+
// Needed due to Bootstrap v4.4 reboot.css changes
6+
outline: 0;
57

68
&.fade:not(.show) {
79
opacity: 0;
@@ -10,6 +12,12 @@
1012
&.show {
1113
opacity: $tooltip-opacity;
1214
}
15+
16+
// Disabled pointer events when in 'noninteractive' mode to hide
17+
// the tooltip when the user hovers over its content
18+
&.noninteractive {
19+
pointer-events: none;
20+
}
1321
}
1422

1523
// Create custom variants for tooltips

src/components/tooltip/helpers/bv-tooltip-template.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export const BVTooltipTemplate = /*#__PURE__*/ Vue.extend({
2929
title: '',
3030
content: '',
3131
variant: null,
32-
customClass: null
32+
customClass: null,
33+
interactive: true
3334
}
3435
},
3536
computed: {
@@ -39,6 +40,9 @@ export const BVTooltipTemplate = /*#__PURE__*/ Vue.extend({
3940
templateClasses() {
4041
return [
4142
{
43+
// Disables pointer events to hide the tooltip when the user
44+
// hovers over its content
45+
noninteractive: !this.interactive,
4246
[`b-${this.templateType}-${this.variant}`]: this.variant,
4347
// `attachment` will come from BVToolpop
4448
[`bs-${this.templateType}-${this.attachment}`]: this.attachment

src/components/tooltip/helpers/bv-tooltip.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ const templateData = {
8888
// Arrow of Tooltip/popover will try and stay away from
8989
// the edge of tooltip/popover edge by this many pixels
9090
arrowPadding: 6,
91+
// Interactive state (Boolean)
92+
interactive: true,
9193
// Disabled state (Boolean)
9294
disabled: false,
9395
// ID to use for tooltip/popover
@@ -162,7 +164,8 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
162164
content: this.content,
163165
variant: this.variant,
164166
customClass: this.customClass,
165-
noFade: this.noFade
167+
noFade: this.noFade,
168+
interactive: this.interactive
166169
}
167170
}
168171
},
@@ -346,7 +349,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
346349
// So that the template updates accordingly
347350
const $tip = this.$_tip
348351
if ($tip) {
349-
const props = ['title', 'content', 'variant', 'customClass', 'noFade']
352+
const props = ['title', 'content', 'variant', 'customClass', 'noFade', 'interactive']
350353
// Only update the values if they have changed
351354
props.forEach(prop => {
352355
if ($tip[prop] !== this[prop]) {

src/components/tooltip/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
{
5959
"prop": "show",
6060
"description": "When set will show the tooltip"
61+
},
62+
{
63+
"prop": "noninteractive",
64+
"version": "2.2.0",
65+
"description": "Wether the tooltip should not be user-interactive"
6166
}
6267
],
6368
"events": [

src/components/tooltip/tooltip.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({
8888
type: Boolean,
8989
default: false
9090
},
91+
noninteractive: {
92+
type: Boolean,
93+
default: false
94+
},
9195
disabled: {
9296
type: Boolean,
9397
default: false
@@ -126,6 +130,7 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({
126130
delay: this.delay,
127131
offset: this.offset,
128132
noFade: this.noFade,
133+
interactive: !this.noninteractive,
129134
disabled: this.disabled,
130135
id: this.id
131136
}

src/components/tooltip/tooltip.spec.js

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const appDef = {
99
props: [
1010
'triggers',
1111
'show',
12+
'noninteractive',
1213
'disabled',
1314
'noFade',
1415
'title',
@@ -23,6 +24,7 @@ const appDef = {
2324
target: 'foo',
2425
triggers: this.triggers,
2526
show: this.show,
27+
noninteractive: this.noninteractive || false,
2628
disabled: this.disabled,
2729
noFade: this.noFade || false,
2830
title: this.title || null,
@@ -173,6 +175,7 @@ describe('b-tooltip', () => {
173175
expect(tip.tagName).toEqual('DIV')
174176
expect(tip.classList.contains('tooltip')).toBe(true)
175177
expect(tip.classList.contains('b-tooltip')).toBe(true)
178+
expect(tip.classList.contains('interactive')).toBe(false)
176179

177180
// Hide the tooltip
178181
wrapper.setProps({
@@ -694,9 +697,9 @@ describe('b-tooltip', () => {
694697
expect(tip.classList.contains('b-tooltip')).toBe(true)
695698

696699
// Hide the tooltip by emitting event on instance
697-
const btooltip = wrapper.find(BTooltip)
698-
expect(btooltip.exists()).toBe(true)
699-
btooltip.vm.$emit('close')
700+
const bTooltip = wrapper.find(BTooltip)
701+
expect(bTooltip.exists()).toBe(true)
702+
bTooltip.vm.$emit('close')
700703
await waitNT(wrapper.vm)
701704
await waitRAF()
702705
await waitNT(wrapper.vm)
@@ -712,7 +715,7 @@ describe('b-tooltip', () => {
712715
expect(document.getElementById(adb)).toBe(null)
713716

714717
// Show the tooltip by emitting event on instance
715-
btooltip.vm.$emit('open')
718+
bTooltip.vm.$emit('open')
716719

717720
await waitNT(wrapper.vm)
718721
await waitRAF()
@@ -1068,7 +1071,66 @@ describe('b-tooltip', () => {
10681071
wrapper.destroy()
10691072
})
10701073

1071-
it('Applies variant class', async () => {
1074+
it('applies noninteractive class based on noninteractive prop', async () => {
1075+
jest.useFakeTimers()
1076+
const App = localVue.extend(appDef)
1077+
const wrapper = mount(App, {
1078+
attachToDocument: true,
1079+
localVue: localVue,
1080+
propsData: {
1081+
show: true
1082+
},
1083+
slots: {
1084+
default: 'title'
1085+
}
1086+
})
1087+
1088+
expect(wrapper.isVueInstance()).toBe(true)
1089+
await waitNT(wrapper.vm)
1090+
await waitRAF()
1091+
await waitNT(wrapper.vm)
1092+
await waitRAF()
1093+
jest.runOnlyPendingTimers()
1094+
await waitNT(wrapper.vm)
1095+
await waitRAF()
1096+
1097+
expect(wrapper.is('article')).toBe(true)
1098+
expect(wrapper.attributes('id')).toBeDefined()
1099+
expect(wrapper.attributes('id')).toEqual('wrapper')
1100+
1101+
// The trigger button
1102+
const $button = wrapper.find('button')
1103+
expect($button.exists()).toBe(true)
1104+
1105+
// ID of the tooltip that will be in the body
1106+
const adb = $button.attributes('aria-describedby')
1107+
expect(adb).toBeDefined()
1108+
expect(adb).not.toBe('')
1109+
expect(adb).not.toBe(null)
1110+
1111+
// Find the tooltip element in the document
1112+
const tip = document.getElementById(adb)
1113+
expect(tip).not.toBe(null)
1114+
expect(tip).toBeInstanceOf(HTMLElement)
1115+
expect(tip.tagName).toEqual('DIV')
1116+
expect(tip.classList.contains('tooltip')).toBe(true)
1117+
expect(tip.classList.contains('b-tooltip')).toBe(true)
1118+
expect(tip.classList.contains('noninteractive')).toBe(false)
1119+
1120+
// Enable 'noninteractive'. Should be reactive
1121+
wrapper.setProps({
1122+
noninteractive: true
1123+
})
1124+
await waitNT(wrapper.vm)
1125+
await waitRAF()
1126+
expect(tip.classList.contains('tooltip')).toBe(true)
1127+
expect(tip.classList.contains('b-tooltip')).toBe(true)
1128+
expect(tip.classList.contains('noninteractive')).toBe(true)
1129+
1130+
wrapper.destroy()
1131+
})
1132+
1133+
it('applies variant class', async () => {
10721134
jest.useFakeTimers()
10731135
const App = localVue.extend(appDef)
10741136
const wrapper = mount(App, {
@@ -1125,7 +1187,7 @@ describe('b-tooltip', () => {
11251187
wrapper.destroy()
11261188
})
11271189

1128-
it('Applies custom class', async () => {
1190+
it('applies custom class', async () => {
11291191
jest.useFakeTimers()
11301192
const App = localVue.extend(appDef)
11311193
const wrapper = mount(App, {

0 commit comments

Comments
 (0)