feat: add tooltips for better UX (#1554)

This commit is contained in:
Phan An 2022-10-25 19:29:56 +02:00 committed by GitHub
parent 64601411d8
commit b96e072c02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 200 additions and 27 deletions

View file

@ -40,6 +40,7 @@
"@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.9.6",
"@faker-js/faker": "^6.2.0",
"@floating-ui/dom": "^1.0.3",
"@testing-library/cypress": "^8.0.2",
"@testing-library/vue": "^6.5.1",
"@types/axios": "^0.14.0",

View file

@ -2,7 +2,6 @@ import isMobile from 'ismobilejs'
import { isObject, mergeWith } from 'lodash'
import { cleanup, render, RenderOptions } from '@testing-library/vue'
import { afterEach, beforeEach, vi } from 'vitest'
import { clickaway, focus } from '@/directives'
import { defineComponent, nextTick } from 'vue'
import { commonStore, userStore } from '@/stores'
import { http } from '@/services'
@ -87,8 +86,9 @@ export default abstract class UnitTestCase {
return render(component, deepMerge({
global: {
directives: {
'koel-clickaway': clickaway,
'koel-focus': focus
'koel-clickaway': {},
'koel-focus': {},
'koel-tooltip': {}
},
components: {
icon: this.stub('icon')

View file

@ -1,5 +1,5 @@
import { createApp } from 'vue'
import { clickaway, focus } from '@/directives'
import { clickaway, focus, tooltip } from '@/directives'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { RouterKey } from '@/symbols'
import { routes } from '@/config'
@ -11,6 +11,7 @@ createApp(App)
.component('icon', FontAwesomeIcon)
.directive('koel-focus', focus)
.directive('koel-clickaway', clickaway)
.directive('koel-tooltip', tooltip)
/**
* For Ancelot, the ancient cross of war
* for the holy town of Gods

View file

@ -5,9 +5,10 @@
<button
v-if="song?.playback_state === 'Playing'"
v-koel-tooltip.top
class="visualizer-btn"
data-testid="toggle-visualizer-btn"
title="Show/hide the visualizer"
title="Toggle the visualizer"
type="button"
@click.prevent="toggleVisualizer"
>
@ -16,6 +17,7 @@
<button
v-if="useEqualizer"
v-koel-tooltip.top
:class="{ active: showEqualizer }"
:title="`${ showEqualizer ? 'Hide' : 'Show'} equalizer`"
class="equalizer"

View file

@ -3,7 +3,7 @@
exports[`renders 1`] = `
<div class="extra-controls" data-testid="other-controls" data-v-8bf5fe81="">
<div class="wrapper" data-v-8bf5fe81="">
<!--v-if--><button class="visualizer-btn" data-testid="toggle-visualizer-btn" title="Show/hide the visualizer" type="button" data-v-8bf5fe81=""><br data-testid="icon" icon="[object Object]" data-v-8bf5fe81=""></button>
<!--v-if--><button class="visualizer-btn" data-testid="toggle-visualizer-btn" title="Toggle the visualizer" type="button" data-v-8bf5fe81=""><br data-testid="icon" icon="[object Object]" data-v-8bf5fe81=""></button>
<!--v-if--><br data-testid="Volume" data-v-8bf5fe81="">
</div>
</div>

View file

@ -2,7 +2,7 @@
exports[`renders with a current song 1`] = `
<div class="playback-controls" data-testid="footer-middle-pane" data-v-2e8b419d="">
<div class="buttons" data-v-2e8b419d=""><button title="Unlike Fahrstuhl to Heaven by Led Zeppelin" data-testid="like-btn" type="button" class="like-btn" data-v-2e8b419d=""><br data-testid="btn-like-liked" icon="[object Object]"></button><!-- a placeholder to maintain the flex layout --><button type="button" title="Play previous song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><br data-testid="PlayButton" data-v-2e8b419d=""><button type="button" title="Play next song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><button class="repeat-mode-btn" title="Change repeat mode (current mode: No Repeat)" data-testid="repeat-mode-switch" type="button" data-v-cab48a7c="" data-v-2e8b419d="">
<div class="buttons" data-v-2e8b419d=""><button title="Unlike Fahrstuhl to Heaven by Led Zeppelin" data-testid="like-btn" type="button" class="like-btn" data-v-2e8b419d=""><br data-testid="btn-like-liked" icon="[object Object]"></button><!-- a placeholder to maintain the flex layout --><button type="button" title="Play previous song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><br data-testid="PlayButton" data-v-2e8b419d=""><button type="button" title="Play next song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><button class="repeat-mode-btn" title="Change repeat mode (current: No Repeat)" data-testid="repeat-mode-switch" type="button" data-v-cab48a7c="" data-v-2e8b419d="">
<div class="fa-layers" data-v-cab48a7c=""><br data-testid="icon" icon="[object Object]" data-v-cab48a7c="">
<!--v-if-->
</div>
@ -12,7 +12,7 @@ exports[`renders with a current song 1`] = `
exports[`renders without a current song 1`] = `
<div class="playback-controls" data-testid="footer-middle-pane" data-v-2e8b419d="">
<div class="buttons" data-v-2e8b419d=""><button type="button" data-v-2e8b419d=""></button><!-- a placeholder to maintain the flex layout --><button type="button" title="Play previous song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><br data-testid="PlayButton" data-v-2e8b419d=""><button type="button" title="Play next song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><button class="repeat-mode-btn" title="Change repeat mode (current mode: No Repeat)" data-testid="repeat-mode-switch" type="button" data-v-cab48a7c="" data-v-2e8b419d="">
<div class="buttons" data-v-2e8b419d=""><button type="button" data-v-2e8b419d=""></button><!-- a placeholder to maintain the flex layout --><button type="button" title="Play previous song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><br data-testid="PlayButton" data-v-2e8b419d=""><button type="button" title="Play next song" data-v-2e8b419d=""><br data-testid="icon" icon="[object Object]" data-v-2e8b419d=""></button><button class="repeat-mode-btn" title="Change repeat mode (current: No Repeat)" data-testid="repeat-mode-switch" type="button" data-v-cab48a7c="" data-v-2e8b419d="">
<div class="fa-layers" data-v-cab48a7c=""><br data-testid="icon" icon="[object Object]" data-v-cab48a7c="">
<!--v-if-->
</div>

View file

@ -7,11 +7,11 @@
</div>
<div class="bottom">
<button title="About Koel" type="button" @click.prevent="openAboutKoelModal">
<button v-koel-tooltip.left title="About Koel" type="button" @click.prevent="openAboutKoelModal">
<icon :icon="faInfoCircle"/>
</button>
<button title="Log out" type="button" @click.prevent="logout">
<button v-koel-tooltip.left title="Log out" type="button" @click.prevent="logout">
<icon :icon="faArrowRightFromBracket"/>
</button>

View file

@ -20,7 +20,7 @@
:isFirstGroup="index === 0"
@input="onGroupChanged"
/>
<Btn class="btn-add-group" green small uppercase @click.prevent="addGroup">
<Btn class="btn-add-group" green small title="Add a new group" uppercase @click.prevent="addGroup">
<icon :icon="faPlus"/>
Group
</Btn>

View file

@ -26,7 +26,7 @@
:isFirstGroup="index === 0"
@input="onGroupChanged"
/>
<Btn class="btn-add-group" green small uppercase @click.prevent="addGroup">
<Btn class="btn-add-group" green small title="Add a new group" uppercase @click.prevent="addGroup">
<icon :icon="faPlus"/>
</Btn>
</div>

View file

@ -1,6 +1,6 @@
<template>
<div class="row" data-testid="smart-playlist-rule-row">
<Btn class="remove-rule" red @click.prevent="removeRule">
<Btn class="remove-rule" red title="Remove this rule" @click.prevent="removeRule">
<icon :icon="faTrashCan"/>
</Btn>

View file

@ -68,6 +68,7 @@
<Btn
v-if="showDeletePlaylistButton"
v-koel-tooltip
class="del btn-delete-playlist"
red
title="Delete this playlist"

View file

@ -1,6 +1,7 @@
<template>
<button
id="extraTabLyrics"
v-koel-tooltip.left
:class="{ active: value === 'Lyrics' }"
title="Lyrics"
type="button"
@ -10,6 +11,7 @@
</button>
<button
id="extraTabArtist"
v-koel-tooltip.left
:class="{ active: value === 'Artist' }"
title="Artist information"
type="button"
@ -19,6 +21,7 @@
</button>
<button
id="extraTabAlbum"
v-koel-tooltip.left
:class="{ active: value === 'Album' }"
title="Album information"
type="button"
@ -28,6 +31,7 @@
</button>
<button
v-if="useYouTube"
v-koel-tooltip.left
id="extraTabYouTube"
:class="{ active: value === 'YouTube' }"
title="Related YouTube videos"

View file

@ -1,5 +1,11 @@
<template>
<a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile">
<a
v-koel-tooltip.left
class="view-profile"
data-testid="view-profile-link"
href="/#/profile"
title="Profile and preferences"
>
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar"/>
</a>
</template>

View file

@ -1,7 +1,8 @@
<template>
<button
v-koel-tooltip.top
:class="{ active: mode !== 'NO_REPEAT' }"
:title="`Change repeat mode (current mode: ${readableMode})`"
:title="`Change repeat mode (current: ${readableMode})`"
data-testid="repeat-mode-switch"
type="button"
@click.prevent="changeMode"

View file

@ -1,6 +1,7 @@
<template>
<span class="view-modes">
<label
v-koel-tooltip
:class="{ active: value === 'thumbnails' }"
class="thumbnails"
data-testid="view-mode-thumbnail"
@ -12,6 +13,7 @@
</label>
<label
v-koel-tooltip
:class="{ active: value === 'list' }"
class="list"
data-testid="view-mode-list"

View file

@ -1,23 +1,26 @@
<template>
<span id="volume" class="volume" :class="level">
<icon
v-if="level === 'muted'"
:icon="faVolumeMute"
fixed-width
<span
v-show="level === 'muted'"
v-koel-tooltip.top
role="button"
tabindex="0"
title="Unmute"
@click="unmute"
/>
<icon
v-else
:icon="level === 'discreet' ? faVolumeLow : faVolumeHigh"
fixed-width
>
<icon :icon="faVolumeMute" fixed-width/>
</span>
<span
v-show="level !== 'muted'"
v-koel-tooltip.top
role="button"
tabindex="0"
title="Mute"
@click="mute"
/>
>
<icon :icon="level === 'discreet' ? faVolumeLow : faVolumeHigh" fixed-width/>
</span>
<input
id="volumeInput"
@ -75,7 +78,6 @@ eventBus.on('KOEL_READY', () => setLevel(preferences.volume))
position: relative;
display: flex;
align-items: center;
justify-content: center;
[type=range] {
margin: 0 0 0 8px;

View file

@ -1,3 +1,3 @@
// Vitest Snapshot v1
exports[`renders 1`] = `<a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="View/edit user profile" data-v-663f2e50=""><img alt="Avatar of John Doe" src="https://example.com/avatar.jpg" data-v-663f2e50=""></a>`;
exports[`renders 1`] = `<a class="view-profile" data-testid="view-profile-link" href="/#/profile" title="Profile and preferences" data-v-663f2e50=""><img alt="Avatar of John Doe" src="https://example.com/avatar.jpg" data-v-663f2e50=""></a>`;

View file

@ -1,2 +1,3 @@
export * from './clickaway'
export * from './focus'
export * from './tooltip'

View file

@ -0,0 +1,110 @@
import { arrow, autoUpdate, computePosition, offset, Placement } from '@floating-ui/dom'
import { Directive, DirectiveBinding } from 'vue'
type ElementWithTooltip = HTMLElement & {
$tooltip?: HTMLDivElement,
$cleanup?: Closure
}
const getOrCreateTooltip = (el: ElementWithTooltip): HTMLElement => {
if (el.$tooltip) return el.$tooltip
el.$tooltip = document.createElement('div')
el.$tooltip.classList.add('tooltip')
const arrow = document.createElement('div')
arrow.classList.add('tooltip-arrow')
const content = document.createElement('div')
content.classList.add('tooltip-content')
el.$tooltip.appendChild(content)
el.$tooltip.appendChild(arrow)
document.body.appendChild(el.$tooltip)
return el.$tooltip
}
const init = (el: ElementWithTooltip, binding: DirectiveBinding) => {
const $tooltip = getOrCreateTooltip(el)
// make sure the actual title is removed from the element, but keep a backup for the updated() hook calls
$tooltip.querySelector<HTMLDivElement>('.tooltip-content')!.innerText = binding.value
|| el.title
|| el.getAttribute('data-title')
|| el.innerText
if (el.title && !el.getAttribute('data-title')) {
el.setAttribute('data-title', el.title)
el.removeAttribute('title')
}
const $arrow = $tooltip.querySelector<HTMLDivElement>('.tooltip-arrow')!
let placement: Placement = 'bottom'
;(['left', 'right', 'top', 'bottom'] as Placement[]).forEach(p => {
if (binding.modifiers[p]) {
placement = p
}
})
const update = async () => {
const { x, y, middlewareData } = await computePosition(el, $tooltip, {
placement,
middleware: [
arrow({ element: $arrow }),
offset(8)
]
})
Object.assign($tooltip.style, {
top: `${y}px`,
left: `${x}px`
})
// @ts-ignore
const { x: arrowX, y: arrowY } = middlewareData.arrow
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement.split('-')[0]]
Object.assign($arrow.style, {
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
// @ts-ignore
[staticSide]: '-4px'
})
}
el.$cleanup = el.$cleanup || autoUpdate(el, $tooltip, update)
const showTooltip = async () => {
$tooltip.classList.add('show')
await update()
}
const hideTooltip = () => $tooltip.classList.remove('show')
el.addEventListener('mouseenter', showTooltip)
el.addEventListener('focus', showTooltip)
el.addEventListener('mouseleave', hideTooltip)
el.addEventListener('blur', hideTooltip)
}
export const tooltip: Directive = {
mounted: init,
updated: init,
unmounted: (el: ElementWithTooltip, binding) => {
el.$cleanup && el.$cleanup()
el.$tooltip && document.removeChild(el.$tooltip)
}
}

View file

@ -11,4 +11,5 @@
@import "#/vendor/_nprogress.scss";
@import "#/partials/_skeleton.scss";
@import '#/partials/_tooltip.scss';
@import "#/partials/_shared.scss";

View file

@ -0,0 +1,29 @@
.tooltip {
opacity: 0;
color: rgba(255, 255, 255, .8);
width: max-content;
position: absolute;
top: 0;
left: 0;
background: #111;
padding: 5px 12px;
border-radius: 4px;
pointer-events: none;
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, .3));
z-index: 9999;
&.show {
opacity: 1;
transition: opacity .5s ease-in-out;
transition-delay: .3s;
}
&-arrow {
position: absolute;
background: #111;
width: 8px;
height: 8px;
transform: rotate(45deg);
}
}

View file

@ -978,6 +978,18 @@
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-6.3.1.tgz#1ae963dd40405450a2945408cba553e1afa3e0fb"
integrity sha512-8YXBE2ZcU/pImVOHX7MWrSR/X5up7t6rPWZlk34RwZEcdr3ua6X+32pSd6XuOQRN+vbuvYNfA6iey8NbrjuMFQ==
"@floating-ui/core@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.1.tgz#00e64d74e911602c8533957af0cce5af6b2e93c8"
integrity sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==
"@floating-ui/dom@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.3.tgz#b439c8a66436c2cae8d97e889f0b76cce757a6ec"
integrity sha512-6H1kwjkOZKabApNtXRiYHvMmYJToJ1DV7rQ3xc/WJpOABhQIOJJOdz2AOejj8X+gcybaFmBpisVTZxBZAM3V0w==
dependencies:
"@floating-ui/core" "^1.0.1"
"@fortawesome/fontawesome-common-types@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz#76467a94aa888aeb22aafa43eb6ff889df3a5a7f"