From 67b0b437005f211b5c71aba9d33e324f082699b9 Mon Sep 17 00:00:00 2001 From: charles Date: Mon, 3 Jun 2024 20:22:34 -0600 Subject: [PATCH] fix: generalize tooltip ui (#233) --- src/components/icons/icon.js | 2 +- src/models/app.js | 4 ++ src/models/heart-icon.js | 24 ++++++--- src/models/tooltip.js | 74 ++++++++++++++++++++++++++ src/models/tracklist/heart-icon.js | 2 + src/models/tracklist/track-list.js | 5 ++ src/models/tracklist/tracklist-icon.js | 5 +- src/styles.css | 40 +------------- src/utils/tooltip.js | 13 +++++ 9 files changed, 122 insertions(+), 47 deletions(-) create mode 100644 src/models/tooltip.js create mode 100644 src/utils/tooltip.js diff --git a/src/components/icons/icon.js b/src/components/icons/icon.js index e16f3f56..31d5d661 100644 --- a/src/components/icons/icon.js +++ b/src/components/icons/icon.js @@ -15,7 +15,7 @@ export const SNIP_ICON = { export const SKIP_ICON = { role: 'skip', - ariaLabel: 'Skip Song' + ariaLabel: 'Block Track' } export const NOW_PLAYING_SKIP_ICON = { diff --git a/src/models/app.js b/src/models/app.js index 317d07bc..093375be 100644 --- a/src/models/app.js +++ b/src/models/app.js @@ -4,6 +4,7 @@ import TrackList from './tracklist/track-list.js' import NowPlayingIcons from './now-playing-icons.js' import Chorus from './chorus.js' +import ToolTip from './tooltip.js' import QueueObserver from '../observers/queue.js' import SongTracker from '../observers/song-tracker.js' import TrackListObserver from '../observers/track-list.js' @@ -23,6 +24,7 @@ export default class App { } #init() { + this._toolTip = new ToolTip() this._songTracker = new SongTracker() this._chorus = new Chorus(this._songTracker) this._snip = new CurrentSnip(this._songTracker) @@ -52,6 +54,7 @@ export default class App { this._active = false this._video.reset() + this._toolTip.removeUI() this._nowPlayingIcons.clearIcons() this._queueObserver.disconnect() @@ -68,6 +71,7 @@ export default class App { this._active = true this._chorus.init() + this._toolTip.placeUI() this._nowPlayingIcons.placeIcons() this._queueObserver.observe() diff --git a/src/models/heart-icon.js b/src/models/heart-icon.js index fac0f795..d3d17c16 100644 --- a/src/models/heart-icon.js +++ b/src/models/heart-icon.js @@ -5,6 +5,7 @@ import { store } from '../stores/data.js' import Dispatcher from '../events/dispatcher.js' import { currentData } from '../data/current.js' import { createIcon, HEART_ICON } from '../components/icons/icon.js' +import { updateToolTip } from '../utils/tooltip.js' export default class HeartIcon { constructor() { @@ -13,7 +14,6 @@ export default class HeartIcon { init() { this.#placeIcon() - this.#setupListener() } removeIcon() { @@ -22,10 +22,21 @@ export default class HeartIcon { } #placeIcon() { - const heartButton = parseNodeString(this.#createHeartIcon) - const refNode = this.#nowPlayingButton - refNode.parentElement.insertBefore(heartButton, refNode) - this.#toggleNowPlayingButton(false) + this._interval = setInterval(() => { + if (!this._interval) return + + const refNode = document.getElementById('chorus') + if (!refNode) return + + const heartButton = parseNodeString(this.#createHeartIcon) + const settingsButton = document.getElementById('chorus-icon') + refNode.insertBefore(heartButton, settingsButton) + this.#toggleNowPlayingButton(false) + this.#setupListener() + + clearInterval(this._interval) + this._interval = null + }, 25) } #toggleNowPlayingButton(show) { @@ -44,7 +55,7 @@ export default class HeartIcon { } #setupListener() { - this.#heartIcon?.addEventListener('click', async () => this.#handleClick()) + this.#heartIcon?.addEventListener('click', async () => await this.#handleClick()) } async #dispatchIsInCollection(ids) { @@ -192,5 +203,6 @@ export default class HeartIcon { #updateIconLabel(highlight) { const text = `${highlight ? 'Remove from' : 'Save to'} Liked` this.#heartIcon?.setAttribute('aria-label', text) + updateToolTip(this.#heartIcon) } } diff --git a/src/models/tooltip.js b/src/models/tooltip.js new file mode 100644 index 00000000..ba47fca9 --- /dev/null +++ b/src/models/tooltip.js @@ -0,0 +1,74 @@ +import { parseNodeString } from '../utils/parser.js' +import { updateToolTip } from '../utils/tooltip.js' + +export default class ToolTip { + placeUI() { + this.#setupToolTip() + } + + get #toolTip() { + return document.getElementById('tooltip') + } + + #setupToolTip() { + if (this.#toolTip) return + + const toolTipEl = `
` + const toolTip = parseNodeString(toolTipEl) + document.body.appendChild(toolTip) + + this.#setupNowPlayingListeners() + } + + setupTrackListListeners(row) { + const buttons = row.querySelectorAll('button[role]') + buttons.forEach((button) => { + button.addEventListener('mouseenter', this.#showToolTip) + button.addEventListener('mouseleave', this.#hideToolTip) + }) + } + + #setupNowPlayingListeners() { + this._interval = setInterval(() => { + if (!this._interval) return + + const chorusButtons = document.querySelectorAll('#chorus > button[role]') + const generalControls = document.querySelectorAll( + '[data-testid="general-controls"] > div > button[id]' + ) + if (chorusButtons.length !== 3 || generalControls.length !== 3) return + ;[...chorusButtons, ...generalControls].forEach((button) => { + button.addEventListener('mouseenter', this.#showToolTip) + button.addEventListener('mouseleave', this.#hideToolTip) + }) + + clearInterval(this._interval) + this._interval = null + }, 25) + } + + #isChorusUI(target) { + const role = target.getAttribute('role') + if (!role) return false + + return ['heart', 'settings', 'skip', 'ff', 'rw', 'loop'].includes(role) + } + + #showToolTip = (event) => { + if (!this.#isChorusUI(event.target)) return + + this.#toolTip.style.display = 'inline-block' + updateToolTip(event.target) + } + + #hideToolTip = () => { + if (!this.#toolTip) return + this.#toolTip.style.display = 'none' + } + + removeUI() { + this.#toolTip?.removeEventListener('mouseenter', this.#showToolTip) + this.#toolTip?.removeEventListener('mouseleave', this.#hideToolTip) + this.#toolTip?.remove() + } +} diff --git a/src/models/tracklist/heart-icon.js b/src/models/tracklist/heart-icon.js index 8cafb067..0ef656b3 100644 --- a/src/models/tracklist/heart-icon.js +++ b/src/models/tracklist/heart-icon.js @@ -5,6 +5,7 @@ import { getTrackId, trackSongInfo } from '../../utils/song.js' import Dispatcher from '../../events/dispatcher.js' import { currentData } from '../../data/current.js' import { highlightIconTimer } from '../../utils/highlight.js' +import { updateToolTip } from '../../utils/tooltip.js' export default class HeartIcon extends TrackListIcon { constructor(store) { @@ -50,6 +51,7 @@ export default class HeartIcon extends TrackListIcon { const icon = row.querySelector(this._selector) this.animate(icon, saved) + updateToolTip(icon) const { id: songId } = trackSongInfo(row) await this.#updateCurrentTrack({ songId, highlight: saved }) diff --git a/src/models/tracklist/track-list.js b/src/models/tracklist/track-list.js index d16fcae7..cf646796 100644 --- a/src/models/tracklist/track-list.js +++ b/src/models/tracklist/track-list.js @@ -2,12 +2,14 @@ import SkipIcon from './skip-icon.js' import SnipIcon from './snip-icon.js' import HeartIcon from './heart-icon.js' +import ToolTip from '../tooltip.js' import Chorus from '../chorus.js' import TrackSnip from '../snip/track-snip.js' import Dispatcher from '../../events/dispatcher.js' import { store } from '../../stores/data.js' import { getTrackId, trackSongInfo } from '../../utils/song.js' +import { updateToolTip } from '../../utils/tooltip.js' export default class TrackList { constructor(songTracker) { @@ -17,6 +19,7 @@ export default class TrackList { this._heartIcon = new HeartIcon(store) this._snipIcon = new SnipIcon(store) this._trackSnip = new TrackSnip(store) + this._toolTip = new ToolTip() this._visibleEvents = ['mouseenter'] this._events = ['mouseenter', 'mouseleave'] @@ -200,6 +203,7 @@ export default class TrackList { const icon = row.querySelector('button[role="skip"]') await this._skipIcon._saveTrack(row) this._skipIcon._animate(icon) + updateToolTip(icon) } else { const icon = row.querySelector('button[role="heart"]') await this._heartIcon.toggleTrackLiked(row) @@ -236,6 +240,7 @@ export default class TrackList { icon.setInitialState(row) }) this.#setMouseEvents(row) + this._toolTip.setupTrackListListeners(row) }) } } diff --git a/src/models/tracklist/tracklist-icon.js b/src/models/tracklist/tracklist-icon.js index 521d9b4d..4e7fc2c0 100644 --- a/src/models/tracklist/tracklist-icon.js +++ b/src/models/tracklist/tracklist-icon.js @@ -1,5 +1,6 @@ import { parseNodeString } from '../../utils/parser.js' import { trackSongInfo, currentSongInfo } from '../../utils/song.js' +import { updateToolTip } from '../../utils/tooltip.js' import Queue from '../queue.js' @@ -105,6 +106,8 @@ export default class TrackListIcon { const display = snipInfo?.[this._key] ?? false this._burn({ icon, burn: display }) this._glow({ icon, glow: display }) + icon.setAttribute('aria-label', `${display ? 'Un' : 'B'}lock Track`) + updateToolTip(icon) } #getStyleProp(icon) { @@ -118,7 +121,7 @@ export default class TrackListIcon { } if (icon.role == 'skip') { - icon.setAttribute('aria-label', `${burn ? 'Uns' : 'S'}kip Song`) + icon.setAttribute('aria-label', `${burn ? 'Unb' : 'B'}lock Track`) } const styleProp = this.#getStyleProp(icon) diff --git a/src/styles.css b/src/styles.css index e33d135e..83a141b1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -312,35 +312,10 @@ input[type='number']::-webkit-outer-spin-button { justify-content: flex-end; } -[role=ff][aria-label], -[role=rw][aria-label], -[role=loop][aria-label], -[role=skip][aria-label], -[role=snip][aria-label], -[role=speed][aria-label], -[role=heart][aria-label], -[role=settings][aria-label], -[role=artist-disco][aria-label] { - position: relative; - display: inline-block; -} - -[role=ff][aria-label]:after, -[role=rw][aria-label]:after, -[role=loop][aria-label]:after, -[role=skip][aria-label]:after, -[role=snip][aria-label]:after, -[role=speed][aria-label]:after, -[role=heart][aria-label]:after, -[role=settings][aria-label]:after, -[role=artist-disco][aria-label]:after { - content: attr(aria-label); +.tooltip { display: none; position: absolute; - top: -1.25rem; z-index: 99999; - right: 50%; - transform: translate3d(calc(50%), -1.5rem, 10rem); pointer-events: none; padding: 8px; padding-top: 7px; @@ -348,7 +323,6 @@ input[type='number']::-webkit-outer-spin-button { white-space: nowrap; text-decoration: none; text-indent: 0; - overflow: visible; font-size: 0.9rem; font-weight: normal; color: #fff; @@ -357,18 +331,6 @@ input[type='number']::-webkit-outer-spin-button { box-shadow: 0px 4px 6px 0px rgb(0 0 0 / 35%); } -[role=ff][aria-label]:hover:after, -[role=rw][aria-label]:hover:after, -[role=loop][aria-label]:hover:after, -[role=skip][aria-label]:hover:after, -[role=snip][aria-label]:hover:after, -[role=speed][aria-label]:hover:after, -[role=heart][aria-label]:hover:after, -[role=settings][aria-label]:hover:after, -[role=artist-disco][aria-label]:hover:after { - display: inline-block; -} - hr { margin: 0; width: 100%; diff --git a/src/utils/tooltip.js b/src/utils/tooltip.js new file mode 100644 index 00000000..f035d76d --- /dev/null +++ b/src/utils/tooltip.js @@ -0,0 +1,13 @@ +export function updateToolTip(element) { + const ariaLabel = element?.getAttribute('aria-label') + const toolTip = document.getElementById('tooltip') + + if (!toolTip || !ariaLabel) return + + toolTip.textContent = ariaLabel + const rect = element.getBoundingClientRect() + const toolTipRect = toolTip.getBoundingClientRect() + + toolTip.style.left = `${rect.left + rect.width / 2 - toolTipRect.width / 2}px` + toolTip.style.top = `${rect.top - 40}px` +}