From a715e0c1b61d1ed712fd996aa49bab5d01ac69ac Mon Sep 17 00:00:00 2001 From: konnorrogers Date: Tue, 11 Jun 2024 11:54:58 -0400 Subject: [PATCH] working on tooltip --- docs/docs/components/tooltip.md | 107 ++++++++-------------- src/components/tooltip/tooltip.styles.ts | 2 +- src/components/tooltip/tooltip.ts | 109 ++++++++++++++--------- 3 files changed, 106 insertions(+), 112 deletions(-) diff --git a/docs/docs/components/tooltip.md b/docs/docs/components/tooltip.md index 7d1a7b9f9..a3321ab77 100644 --- a/docs/docs/components/tooltip.md +++ b/docs/docs/components/tooltip.md @@ -9,14 +9,7 @@ A tooltip's target is its _first child element_, so you should only wrap one ele Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout. ```html {.example} - - Hover Me - - -

- - - +This is a tooltip Hover Me ``` @@ -26,9 +19,8 @@ import WaButton from '@shoelace-style/shoelace/dist/react/button'; import WaTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; const App = () => ( - - Hover Me - + This is a tooltip + Hover Me ); ``` {% endraw %} @@ -42,93 +34,68 @@ Use the `placement` attribute to set the preferred placement of the tooltip. ```html {.example}
- - - - - - - - - - - + + +
- - - - - - - + +
- - - - - - - + +
- - - - - - - + +
- - - - - - - - - - - + + +
+top-start +top +top-end +left-start +right-start +left +right +left-end +right-end +bottom-start +bottom +bottom-end + ``` diff --git a/src/components/tooltip/tooltip.styles.ts b/src/components/tooltip/tooltip.styles.ts index da7a72650..ddf0a9a14 100644 --- a/src/components/tooltip/tooltip.styles.ts +++ b/src/components/tooltip/tooltip.styles.ts @@ -4,7 +4,7 @@ export default css` :host { --max-width: 20rem; - display: contents; + display: inline-block; } .tooltip { diff --git a/src/components/tooltip/tooltip.ts b/src/components/tooltip/tooltip.ts index 90f5e5022..7dc7f108c 100644 --- a/src/components/tooltip/tooltip.ts +++ b/src/components/tooltip/tooltip.ts @@ -2,6 +2,7 @@ import { animateWithClass, stopAnimations } from '../../internal/animate.js'; import { classMap } from 'lit/directives/class-map.js'; import { customElement, property, query, state } from 'lit/decorators.js'; import { html } from 'lit'; +import { uniqueId } from '../../internal/math.js'; import { waitForEvent } from '../../internal/event.js'; import { watch } from '../../internal/watch.js'; import componentStyles from '../../styles/component.styles.js'; @@ -9,7 +10,6 @@ import styles from './tooltip.styles.js'; import WaPopup from '../popup/popup.js'; import WebAwesomeElement from '../../internal/webawesome-element.js'; import type { CSSResultGroup } from 'lit'; -import { ifDefined } from 'lit/directives/if-defined.js'; /** * @summary Tooltips display additional information based on a specific action. @@ -101,39 +101,18 @@ export default class WaTooltip extends WebAwesomeElement { */ @property({ type: Boolean }) hoist = false; - @property() selector: null | string = null + @property() for: null | string = null - @state() anchor: Element + @state() anchor: null | Element = null - rootNode: null | Document | ShadowRoot = null - - constructor() { - super(); - this.addEventListener('blur', this.handleBlur, true); - this.addEventListener('focus', this.handleFocus, true); - this.addEventListener('click', this.handleClick); - this.addEventListener('mouseover', this.handleMouseOver); - this.addEventListener('mouseout', this.handleMouseOut); - } + private eventController = new AbortController() connectedCallback () { super.connectedCallback() - this.rootNode = this.getRootNode() as Document | ShadowRoot | null - } - @watch("selector") - handleSelectorChange () { - if (this.rootNode && this.selector) { - const elements = this.rootNode.querySelectorAll(this.selector) - - for (const element of elements) { - element.setAttribute("aria-labelledby", this.id) - element.addEventListener('blur', this.handleBlur, true); - element.addEventListener('focus', this.handleFocus, true); - element.addEventListener('click', this.handleClick); - element.addEventListener('mouseover', this.handleMouseOver); - element.addEventListener('mouseout', this.handleMouseOut); - } + // If the user doesn't give us an id, generate one. + if (!this.id) { + this.id = uniqueId("wa-tooltip-") } } @@ -141,10 +120,17 @@ export default class WaTooltip extends WebAwesomeElement { // Cleanup this event in case the tooltip is removed while open this.closeWatcher?.destroy(); document.removeEventListener('keydown', this.handleDocumentKeyDown); + this.eventController.abort() + + if (this.anchor) { + const label = (this.anchor.getAttribute("aria-labelledby") || "") + this.anchor.setAttribute("aria-labelledby", label.replace(this.id, "")) + } } + firstUpdated() { - // this.body.hidden = !this.open; + this.body.hidden = !this.open; // If the tooltip is visible on init, update its position if (this.open) { @@ -232,10 +218,10 @@ export default class WaTooltip extends WebAwesomeElement { this.hide(); }; } else { - document.addEventListener('keydown', this.handleDocumentKeyDown); + document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal }); } - // this.body.hidden = false; + this.body.hidden = false; this.popup.active = true; await stopAnimations(this.popup.popup); await animateWithClass(this.popup.popup, 'show-with-scale'); @@ -251,13 +237,58 @@ export default class WaTooltip extends WebAwesomeElement { await stopAnimations(this.popup.popup); await animateWithClass(this.popup.popup, 'hide-with-scale'); this.popup.active = false; - // this.body.hidden = true; + this.body.hidden = true; this.emit('wa-after-hide'); } } - @watch(['content', 'distance', 'hoist', 'placement', 'skidding']) + @watch("for") + handleForChange () { + const rootNode = this.getRootNode() as Document | ShadowRoot | null + + if (!rootNode) { return } + + const newAnchor = this.for ? rootNode.querySelector(`#${this.for}`) : null + const oldAnchor = this.anchor + + if (newAnchor === oldAnchor) { + return + } + + const { signal } = this.eventController + + // "\\b" is a space boundary, used for making sure we dont add the tooltip to aria-labelledby twice. + const labelRegex = new RegExp(`\\b${this.id}\\b`) + + if (newAnchor) { + const currentLabel = (newAnchor.getAttribute("aria-labelledby") || "") + if (!currentLabel.match(labelRegex)) { + newAnchor.setAttribute("aria-labelledby", currentLabel + " " + this.id) + } + + newAnchor.addEventListener('blur', this.handleBlur, { capture: true, signal }); + newAnchor.addEventListener('focus', this.handleFocus, { capture: true, signal }); + newAnchor.addEventListener('click', this.handleClick, { signal }); + newAnchor.addEventListener('mouseover', this.handleMouseOver, { signal }); + newAnchor.addEventListener('mouseout', this.handleMouseOut, { signal }); + } + + if (oldAnchor) { + const label = (oldAnchor.getAttribute("aria-labelledby") || "") + oldAnchor.setAttribute("aria-labelledby", label.replace(labelRegex, "")) + oldAnchor.removeEventListener('blur', this.handleBlur, { capture: true }); + oldAnchor.removeEventListener('focus', this.handleFocus, { capture: true }); + oldAnchor.removeEventListener('click', this.handleClick); + oldAnchor.removeEventListener('mouseover', this.handleMouseOver); + oldAnchor.removeEventListener('mouseout', this.handleMouseOut); + } + + this.anchor = newAnchor + } + + + @watch(['distance', 'hoist', 'placement', 'skidding']) async handleOptionsChange() { if (this.hasUpdated) { await this.updateComplete; @@ -289,11 +320,12 @@ export default class WaTooltip extends WebAwesomeElement { /** Hides the tooltip */ async hide() { if (!this.open) { + this.anchor = null return undefined; } - this.anchor = null this.open = false; + this.anchor = null return waitForEvent(this, 'wa-after-hide'); } @@ -323,16 +355,11 @@ export default class WaTooltip extends WebAwesomeElement { shift arrow hover-bridge + .anchor=${this.anchor} > - ${'' /* eslint-disable-next-line lit-a11y/no-aria-slot */} -
+
- - ${'' /* eslint-disable-next-line lit-a11y/accessible-name */} -
- ${this.content} -
`; }