Skip to content

Commit 2456ff9

Browse files
authored
comp(MdTooltip): add tooltips (#20)
* comp(MdTooltip): crate initial styles for tooltip * test(MdTooltio): fix test * comp(MdTooltip): add tooltips
1 parent 4d3c1f1 commit 2456ff9

File tree

17 files changed

+508
-9
lines changed

17 files changed

+508
-9
lines changed

docs/app/i18n/en-US.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ export default {
9191
toolbar: {
9292
title: 'Toolbar'
9393
},
94+
tooltip: {
95+
title: 'Tooltip'
96+
},
9497
layout: {
9598
title: 'Layout'
9699
},
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<example src="./examples/Direction.vue" />
2+
<example src="./examples/Delay.vue" />
3+
<example src="./examples/Dynamically.vue" />
4+
5+
<template>
6+
<page-container centered :title="$t('pages.tooltip.title')">
7+
<div class="page-container-section">
8+
<p>Tooltips identify an element when they are activated. They may contain brief helper text about its function. For example, they may contain text information about actionable icons.</p>
9+
<p>You can setup a tooltip using optional direction and delay:</p>
10+
</div>
11+
12+
<div class="page-container-section">
13+
<h2>Direction</h2>
14+
15+
<p>You can set tooltip direction using the four available values - <code>top</code>, <code>right</code>, <code>bottom</code> and <code>left</code>:</p>
16+
<code-example title="Text position" :component="examples['direction']" />
17+
</div>
18+
19+
<div class="page-container-section">
20+
<h2>Delay</h2>
21+
<p>Sometimes you don't want to pop the tooltip right away. To achieve that you can use a custom delay in milliseconds to postpone the action:</p>
22+
<code-example title="Delay" :component="examples['delay']" />
23+
</div>
24+
25+
<div class="page-container-section">
26+
<h2>Dynamically show a tooltip</h2>
27+
28+
<p>In some cases we may want to trigger the tooltip manually, to make sure that your user will understand and action. You can do it:</p>
29+
<code-example title="Trigger" :component="examples['dynamically']" />
30+
31+
<api-item title="API - md-tooltip">
32+
<p>The following options can be applied to all tooltips:</p>
33+
34+
<api-table :headings="props.headings" :props="props.props" slot="props" />
35+
</api-item>
36+
</div>
37+
</page-container>
38+
</template>
39+
40+
<script>
41+
import examples from 'docs-mixins/docsExample'
42+
43+
export default {
44+
name: 'Tooltip',
45+
mixins: [examples],
46+
data: () => ({
47+
props: {
48+
headings: ['Name', 'Description', 'Default'],
49+
props: [
50+
{
51+
name: 'md-direction',
52+
type: 'String',
53+
description: 'Specify where the tooltip will appear based on the parent element.',
54+
defaults: 'bottom'
55+
},
56+
{
57+
offset: true,
58+
name: 'md-direction="top"',
59+
type: 'String',
60+
description: 'Show the tooltip above the parent element.',
61+
defaults: '-'
62+
},
63+
{
64+
offset: true,
65+
name: 'md-direction="right"',
66+
type: 'String',
67+
description: 'Show the tooltip after the parent element.',
68+
defaults: '-'
69+
},
70+
{
71+
offset: true,
72+
name: 'md-direction="bottom"',
73+
type: 'String',
74+
description: 'Show the tooltip below the parent element.',
75+
defaults: '-'
76+
},
77+
{
78+
offset: true,
79+
name: 'md-direction="left"',
80+
type: 'String',
81+
description: 'Show the tooltip before the parent element.',
82+
defaults: '-'
83+
},
84+
{
85+
name: 'md-delay',
86+
type: 'Number',
87+
description: 'Postpone the exhibition of a tooltip. In milliseconds.',
88+
defaults: '0'
89+
},
90+
{
91+
name: 'md-active',
92+
type: 'Boolean',
93+
description: 'Used to trigger the visibility of a tooltip. Should be used with the <code>.sync</code> modifier.',
94+
defaults: 'false'
95+
}
96+
]
97+
}
98+
})
99+
}
100+
</script>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<template>
2+
<div>
3+
<span>
4+
No delay
5+
6+
<md-tooltip>Bottom</md-tooltip>
7+
</span>
8+
9+
<span>
10+
300ms
11+
12+
<md-tooltip md-delay="300">Bottom</md-tooltip>
13+
</span>
14+
15+
<span>
16+
1s
17+
18+
<md-tooltip md-delay="1000">Bottom</md-tooltip>
19+
</span>
20+
</div>
21+
</template>
22+
23+
<script>
24+
export default {
25+
name: 'Delay'
26+
}
27+
</script>
28+
29+
<style lang="scss" scoped>
30+
span {
31+
min-width: 60px;
32+
margin: 36px;
33+
display: inline-block;
34+
text-align: center;
35+
}
36+
</style>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<div>
3+
<md-avatar>
4+
<img src="assets/examples/avatar.png" alt="Avatar">
5+
<md-tooltip md-direction="top">Top</md-tooltip>
6+
</md-avatar>
7+
8+
<md-avatar>
9+
<img src="assets/examples/avatar.png" alt="Avatar">
10+
<md-tooltip md-direction="right">Right</md-tooltip>
11+
</md-avatar>
12+
13+
<md-avatar>
14+
<img src="assets/examples/avatar.png" alt="Avatar">
15+
<md-tooltip md-direction="bottom">Bottom</md-tooltip>
16+
</md-avatar>
17+
18+
<md-avatar>
19+
<img src="assets/examples/avatar.png" alt="Avatar">
20+
<md-tooltip md-direction="left">Left</md-tooltip>
21+
</md-avatar>
22+
</div>
23+
</template>
24+
25+
<script>
26+
export default {
27+
name: 'Direction'
28+
}
29+
</script>
30+
31+
<style lang="scss" scoped>
32+
.md-avatar {
33+
margin: 36px;
34+
}
35+
</style>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<div>
3+
<md-avatar>
4+
<img src="assets/examples/avatar.png" alt="Avatar">
5+
<md-tooltip :md-active.sync="tooltipActive">Top</md-tooltip>
6+
</md-avatar>
7+
8+
<md-button class="md-raised md-primary" @click="tooltipActive = !tooltipActive">Toggle Tooltip</md-button>
9+
</div>
10+
</template>
11+
12+
<script>
13+
export default {
14+
name: 'Dynamically',
15+
data: () => ({
16+
tooltipActive: false
17+
})
18+
}
19+
</script>
20+
21+
<style lang="scss" scoped>
22+
.md-avatar,
23+
.md-button {
24+
margin: 36px;
25+
}
26+
</style>

docs/app/routes.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import VueRouter from 'vue-router'
44
Vue.use(VueRouter)
55

66
export const routes = [
7+
{
8+
path: '/components/tooltip',
9+
name: 'components/tooltip',
10+
component: () => import(/* webpackChunkName: "tooltip" */ './pages/Components/Tooltip/Tooltip.vue')
11+
},
712
{
813
path: '/components/dialog',
914
name: 'components/dialog',

docs/app/template/MainNavContent.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<router-link to="/components/subheader">{{ $t('pages.subheader.title') }}</router-link>
3434
<router-link to="/components/tabs">{{ $t('pages.tabs.title') }}</router-link>
3535
<router-link to="/components/toolbar">{{ $t('pages.toolbar.title') }}</router-link>
36+
<router-link to="/components/tooltip">{{ $t('pages.tooltip.title') }}</router-link>
3637
</div>
3738

3839
<router-link to="/ui-elements">{{ $t('pages.uiElements.title') }}</router-link>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import mountTemplate from 'test/utils/mountTemplate'
2+
import MdPopover from './MdPopover.vue'
3+
4+
test('should render the popover', async () => {
5+
const template = '<md-popover></md-popover>'
6+
const wrapper = await mountTemplate(MdPopover, template)
7+
8+
expect(wrapper.hasClass('md-popover')).toBe(true)
9+
})
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<template>
2+
<md-portal class="md-popover" v-bind="$attrs" :class="popoverClasses" :md-if="shouldRender" @md-original-parent="setOriginalParent" @md-destroy="killPopper">
3+
<slot />
4+
</md-portal>
5+
</template>
6+
7+
<script>
8+
import Popper from 'popper.js'
9+
import deepmerge from 'deepmerge'
10+
import MdPortal from 'components/MdPortal/MdPortal'
11+
12+
export default {
13+
name: 'MdPopover',
14+
components: {
15+
MdPortal
16+
},
17+
props: {
18+
mdIf: Boolean,
19+
mdSettings: {
20+
type: Object,
21+
default: () => ({})
22+
}
23+
},
24+
data: () => ({
25+
popperInstance: null,
26+
originalParentEl: null,
27+
shouldRender: false,
28+
shouldActivate: false
29+
}),
30+
computed: {
31+
popoverClasses () {
32+
if (this.shouldActivate) {
33+
return 'md-active'
34+
} else if (this.shouldRender) {
35+
return 'md-rendering'
36+
}
37+
}
38+
},
39+
watch: {
40+
mdIf (shouldRender) {
41+
this.shouldRender = shouldRender
42+
43+
if (shouldRender) {
44+
this.bindPopper()
45+
} else {
46+
this.shouldActivate = false
47+
}
48+
},
49+
mdSettings (settings) {
50+
if (this.popperInstance) {
51+
this.popperInstance.options = deepmerge(this.getPopperOptions(), settings)
52+
this.popperInstance.scheduleUpdate()
53+
}
54+
}
55+
},
56+
methods: {
57+
getPopperOptions () {
58+
return {
59+
placement: 'bottom',
60+
modifiers: {
61+
preventOverflow: {
62+
padding: 8
63+
},
64+
computeStyle: {
65+
gpuAcceleration: false
66+
}
67+
},
68+
onCreate: () => {
69+
this.shouldActivate = true
70+
}
71+
}
72+
},
73+
setOriginalParent (el) {
74+
this.originalParentEl = el
75+
},
76+
killPopper () {
77+
if (this.popperInstance) {
78+
this.popperInstance.destroy()
79+
this.popperInstance = null
80+
}
81+
},
82+
async bindPopper () {
83+
await this.$nextTick()
84+
85+
const el = this.$children[0].$el
86+
87+
if (this.originalParentEl) {
88+
const options = deepmerge(this.getPopperOptions(), this.mdSettings)
89+
90+
this.popperInstance = new Popper(this.originalParentEl, el, options)
91+
}
92+
}
93+
},
94+
beforeDestroy () {
95+
this.killPopper()
96+
}
97+
}
98+
</script>
99+
100+
<style lang="scss">
101+
.md-popover {
102+
&.md-rendering {
103+
opacity: 0;
104+
}
105+
}
106+
</style>

src/components/MdPortal/MdPortal.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@
2424
props: {
2525
mdIf: {
2626
type: null,
27-
validator: (value) => validator('md-if', this, value === null || typeof value === 'boolean', 'You should pass a Boolean value')
27+
validator: value => validator('md-if', this, value === null || typeof value === 'boolean', 'You should pass a Boolean value')
2828
},
2929
mdTransitionName: String,
3030
mdTransitionAppear: Boolean,
31-
mdFollowEl: HTMLElement,
3231
mdTargetEl: {
3332
type: HTMLElement,
34-
validator: (value) => validator('md-target-el', this, value && value instanceof HTMLElement, 'You should pass a valid HTMLElement')
33+
validator: value => validator('md-target-el', this, value && value instanceof HTMLElement, 'You should pass a valid HTMLElement')
3534
}
3635
},
3736
data: () => ({
3837
leaveTimeout: null,
38+
originalParentEl: null,
3939
targetEl: null
4040
}),
4141
computed: {
@@ -114,13 +114,18 @@
114114
this.$el.classList.remove(this.leaveClass)
115115
this.$el.classList.remove(this.leaveActiveClass)
116116
this.$el.classList.remove(this.leaveToClass)
117+
this.$emit('md-destroy')
117118
this.killGhostElement()
118119
})
119120
}
120121
},
121122
created () {
123+
this.originalParentEl = this.originalParentEl || this.$options._parentElm
122124
this.changeParentEl()
123125
},
126+
mounted () {
127+
this.$emit('md-original-parent', this.originalParentEl)
128+
},
124129
async beforeDestroy () {
125130
if (this.$el.classList) {
126131
this.$el.classList.add(this.leaveClass)

0 commit comments

Comments
 (0)