Add support for multiple tooltips in the same Vue component

Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/33223
This commit is contained in:
Eric Eastwood 2017-06-06 15:46:31 -05:00
parent bf0b3d8352
commit 6cb575d612
30 changed files with 251 additions and 123 deletions

View File

@ -2,6 +2,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,6 +13,10 @@ export default {
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
@ -33,8 +38,6 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
@ -53,11 +56,11 @@ export default {
class="btn-group"
role="group">
<button
v-tooltip
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">

View File

@ -1,4 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the external url link in environments table.
*/
@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Open';
@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn external-url has-tooltip"
v-tooltip
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"

View File

@ -2,6 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
monitoringUrl: {
@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn monitoring-url hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"

View File

@ -5,6 +5,7 @@
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
@ -46,8 +51,9 @@ export default {
</script>
<template>
<button
v-tooltip
type="button"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
class="btn stop-env-link hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"

View File

@ -4,6 +4,7 @@
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
@ -29,7 +34,8 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn terminal-button hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"

View File

@ -1,10 +1,10 @@
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
props: {
formState: {
type: Object,
@ -71,9 +71,9 @@
data-placeholder="Move to a different project" />
</div>
<span
v-tooltip
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
<i
class="fa fa-question-circle"
aria-hidden="true">

View File

@ -3,7 +3,7 @@
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -28,12 +28,12 @@ export default {
required: false,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
@ -58,7 +58,6 @@ export default {
makeRequest() {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
},
},
@ -67,6 +66,7 @@ export default {
<template>
<button
v-tooltip
type="button"
@click="onClick"
:class="buttonClass"
@ -74,7 +74,6 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
ref="tooltip"
:disabled="isLoading">
<i
:class="iconClass"

View File

@ -1,6 +1,6 @@
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
@ -46,12 +46,11 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i

View File

@ -1,6 +1,6 @@
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
@ -42,13 +42,12 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">

View File

@ -1,7 +1,7 @@
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
@ -34,9 +34,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
jobComponent,
@ -53,12 +53,12 @@
<template>
<div>
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
:title="tooltipText">
<job-name-component
:name="job.name"

View File

@ -2,7 +2,7 @@
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@ -54,9 +54,9 @@
jobNameComponent,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
tooltipText() {
@ -77,12 +77,11 @@
<template>
<div>
<a
v-tooltip
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
@ -93,10 +92,9 @@
<div
v-else
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component

View File

@ -1,6 +1,6 @@
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,9 +12,9 @@ export default {
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
user() {
return this.pipeline.user;
@ -45,16 +45,16 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
v-tooltip
class="js-pipeline-url-latest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span

View File

@ -4,6 +4,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -12,6 +13,9 @@
required: true,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
@ -25,8 +29,6 @@
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
@ -43,13 +45,13 @@
<template>
<div class="btn-group">
<button
v-tooltip
type="button"
class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i

View File

@ -1,5 +1,5 @@
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -8,9 +8,9 @@
required: true,
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
@ -18,12 +18,12 @@
class="btn-group"
role="group">
<button
v-tooltip
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
aria-label="Artifacts">
<i
class="fa fa-download"
aria-hidden="true">

View File

@ -16,7 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
@ -32,9 +32,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
data() {
return {
@ -132,7 +132,7 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
v-tooltip
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"

View File

@ -1,7 +1,7 @@
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
@ -16,9 +16,11 @@
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
data() {
return {
iconTimerSvg,
@ -81,7 +83,7 @@
</i>
<time
ref="tooltip"
v-tooltip
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">

View File

@ -2,7 +2,7 @@
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
@ -47,9 +47,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
ciIconBadge,
@ -90,10 +90,10 @@ export default {
<template v-if="user">
<a
v-tooltip
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
class="js-user-link commit-committer-link">
<user-avatar-image
:img-src="user.avatar_url"

View File

@ -1,17 +1,17 @@
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
directives: {
tooltip,
},
components: {
toolbarButton,
},
@ -94,13 +94,13 @@
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
type="button">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">

View File

@ -1,10 +1,7 @@
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
@ -29,6 +26,9 @@
default: false,
},
},
directives: {
tooltip,
},
computed: {
iconClass() {
return `fa-${this.icon}`;
@ -39,10 +39,10 @@
<template>
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"

View File

@ -1,5 +1,5 @@
<script>
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
@ -28,19 +28,21 @@ export default {
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
<time
v-tooltip
:class="cssClass"
class="js-vue-timeago"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
data-container="body">
{{timeFormated(time)}}
</time>
</template>

View File

@ -16,11 +16,10 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
@ -53,6 +52,9 @@ export default {
default: 'top',
},
},
directives: {
tooltip,
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
@ -72,6 +74,7 @@ export default {
<template>
<img
v-tooltip
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imageSource"
@ -81,6 +84,5 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>

View File

@ -0,0 +1,13 @@
export default {
bind(el) {
$(el).tooltip();
},
componentUpdated(el) {
$(el).tooltip('fixTitle');
},
unbind(el) {
$(el).tooltip('destroy');
},
};

View File

@ -1,13 +0,0 @@
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
};

View File

@ -463,20 +463,24 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. `destroyed`
#### Vue and Boostrap
1. Tooltips: Do not rely on `has-tooltip` class name for vue components
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
```javascript
// bad
<span class="has-tooltip">
<span
class="has-tooltip"
title="Some tooltip text">
Text
</span>
// good
<span data-toggle="tooltip">
<span
v-tooltip
title="Some tooltip text">
Text
</span>
```
1. Tooltips: When using a tooltip, include the tooltip mixin
1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js`
1. Don't change `data-original-title`.
```javascript

View File

@ -32,9 +32,16 @@ describe('Actions Component', () => {
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Deploy to...');
});
});
it('should render a dropdown button with icon and title attribute', () => {
expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...');
expect(component.$el.querySelector('.dropdown-new').getAttribute('data-original-title')).toEqual('Deploy to...');
expect(component.$el.querySelector('.dropdown-new').getAttribute('aria-label')).toEqual('Deploy to...');
});
it('should render a dropdown with the provided list of actions', () => {

View File

@ -3,21 +3,30 @@ import monitoringComp from '~/environments/components/environment_monitoring.vue
describe('Monitoring Component', () => {
let MonitoringComponent;
let component;
const monitoringUrl = 'https://gitlab.com';
beforeEach(() => {
MonitoringComponent = Vue.extend(monitoringComp);
});
it('should render a link to environment monitoring page', () => {
const monitoringUrl = 'https://gitlab.com';
const component = new MonitoringComponent({
component = new MonitoringComponent({
propsData: {
monitoringUrl,
},
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Monitoring');
});
});
it('should render a link to environment monitoring page', () => {
expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
expect(component.$el.getAttribute('title')).toEqual('Monitoring');
expect(component.$el.getAttribute('data-original-title')).toEqual('Monitoring');
expect(component.$el.getAttribute('aria-label')).toEqual('Monitoring');
});
});

View File

@ -17,8 +17,15 @@ describe('Stop Component', () => {
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Stop');
});
});
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('title')).toEqual('Stop');
expect(component.$el.getAttribute('data-original-title')).toEqual('Stop');
expect(component.$el.getAttribute('aria-label')).toEqual('Stop');
});
});

View File

@ -16,9 +16,16 @@ describe('Stop Component', () => {
}).$mount();
});
describe('computed', () => {
it('title', () => {
expect(component.title).toEqual('Terminal');
});
});
it('should render a link to open a web terminal with the provided path', () => {
expect(component.$el.tagName).toEqual('A');
expect(component.$el.getAttribute('title')).toEqual('Terminal');
expect(component.$el.getAttribute('data-original-title')).toEqual('Terminal');
expect(component.$el.getAttribute('aria-label')).toEqual('Terminal');
expect(component.$el.getAttribute('href')).toEqual(terminalPath);
});
});

View File

@ -22,7 +22,6 @@ describe('Time ago with tooltip component', () => {
}).$mount();
expect(vm.$el.tagName).toEqual('TIME');
expect(vm.$el.classList.contains('js-vue-timeago')).toEqual(true);
expect(
vm.$el.getAttribute('data-original-title'),
).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));

View File

@ -0,0 +1,63 @@
import Vue from 'vue';
import tooltip from '~/vue_shared/directives/tooltip';
describe('Tooltip directive', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('with a single tooltip', () => {
beforeEach(() => {
const SomeComponent = Vue.extend({
directives: {
tooltip,
},
template: `
<div
v-tooltip
title="foo">
</div>
`,
});
vm = new SomeComponent().$mount();
});
it('should have tooltip plugin applied', () => {
expect($(vm.$el).data('bs.tooltip')).toBeDefined();
});
});
describe('with multiple tooltips', () => {
beforeEach(() => {
const SomeComponent = Vue.extend({
directives: {
tooltip,
},
template: `
<div>
<div
v-tooltip
class="js-look-for-tooltip"
title="foo">
</div>
<div
v-tooltip
title="bar">
</div>
</div>
`,
});
vm = new SomeComponent().$mount();
});
it('should have tooltip plugin applied to all instances', () => {
expect($(vm.$el).find('.js-look-for-tooltip').data('bs.tooltip')).toBeDefined();
});
});
});