diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 012648018..b924dcb6f 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -16,10 +16,13 @@ The most elegant solution I found was to use the [Web Animations API](https://de - 🚨 BREAKING: changed `left` and `right` placements to `start` and `end` in `sl-drawer` - 🚨 BREAKING: changed `left` and `right` placements to `start` and `end` in `sl-tab-group` +- 🚨 BREAKING: removed `--hide-duration`, `--hide-timing-function`, `--show-duration`, and `--show-timing-function` custom properties from `sl-tooltip` (use the Animation Registry instead) - Added the Animation Registry - Fixed a bug where removing `sl-dropdown` from the DOM and adding it back destroyed the popover reference [#443](https://github.com/shoelace-style/shoelace/issues/443) -- Updated animations for `sl-alert`, `sl-dialog`, `sl-drawer`, and `sl-dropdown` to use the Animation Registry instead of CSS transitions +- Updated animations for `sl-alert`, `sl-dialog`, `sl-drawer`, `sl-dropdown`, and `sl-tooltip` to use the Animation Registry instead of CSS transitions - Improved a11y by respecting `prefers-reduced-motion` for all show/hide animations +- Improved `--show-delay` and `--hide-delay` behavior in `sl-tooltip` so they only apply on hover +- Removed the internal popover utility ## 2.0.0-beta.40 diff --git a/src/components/tooltip/tooltip.scss b/src/components/tooltip/tooltip.scss index cbc8b91fa..05ce9870d 100644 --- a/src/components/tooltip/tooltip.scss +++ b/src/components/tooltip/tooltip.scss @@ -2,12 +2,8 @@ :host { --max-width: 20rem; - --hide-delay: 0s; - --hide-duration: 0.125s; - --hide-timing-function: ease; - --show-delay: 0.125s; - --show-duration: 0.125s; - --show-timing-function: ease; + --hide-delay: 0ms; + --show-delay: 150ms; display: contents; } @@ -27,14 +23,7 @@ font-weight: var(--sl-tooltip-font-weight); line-height: var(--sl-tooltip-line-height); color: var(--sl-tooltip-color); - opacity: 0; padding: var(--sl-tooltip-padding); - transform: scale(0.8); - transform-origin: bottom; - transition-property: opacity, transform; - transition-delay: var(--hide-delay); - transition-duration: var(--hide-duration); - transition-timing-function: var(--hide-timing-function); &:after { content: ''; @@ -60,14 +49,6 @@ &[data-popper-placement^='right'] .tooltip { transform-origin: left; } - - &.popover-visible .tooltip { - opacity: 1; - transform: none; - transition-delay: var(--show-delay); - transition-duration: var(--show-duration); - transition-timing-function: var(--show-timing-function); - } } // Arrow + bottom diff --git a/src/components/tooltip/tooltip.ts b/src/components/tooltip/tooltip.ts index 92688480e..e7cd83e58 100644 --- a/src/components/tooltip/tooltip.ts +++ b/src/components/tooltip/tooltip.ts @@ -1,8 +1,10 @@ import { LitElement, html, unsafeCSS } from 'lit'; import { customElement, property, query } from 'lit/decorators'; import { classMap } from 'lit-html/directives/class-map'; +import { animateTo, parseDuration, stopAnimations } from '../../internal/animate'; +import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm'; import { event, EventEmitter, watch } from '../../internal/decorators'; -import Popover from '../../internal/popover'; +import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry'; import styles from 'sass:./tooltip.scss'; let id = 0; @@ -16,13 +18,12 @@ let id = 0; * * @part base - The component's base wrapper. * - * @customProperty --hide-delay - The amount of time to wait before hiding the tooltip. - * @customProperty --hide-duration - The amount of time the hide transition takes to complete. - * @customProperty --hide-timing-function - The timing function (easing) to use for the hide transition. * @customProperty --max-width - The maximum width of the tooltip. - * @customProperty --show-delay - The amount of time to wait before showing the tooltip. - * @customProperty --show-duration - The amount of time the show transition takes to complete. - * @customProperty --show-timing-function - The timing function (easing) to use for the show transition. + * @customProperty --hide-delay - The amount of time to wait before hiding the tooltip (hover only). + * @customProperty --show-delay - The amount of time to wait before showing the tooltip (hover only). + * + * @animation tooltip.show - The animation to use when showing the tooltip. + * @animation tooltip.hide - The animation to use when hiding the tooltip. */ @customElement('sl-tooltip') export default class SlTooltip extends LitElement { @@ -32,10 +33,10 @@ export default class SlTooltip extends LitElement { @query('.tooltip') tooltip: HTMLElement; private componentId = `tooltip-${++id}`; + private hasInitialized = false; private target: HTMLElement; - private popover: Popover; - - private isVisible = false; + private popover: PopperInstance; + private hoverTimeout: any; /** The tooltip's content. Alternatively, you can use the content slot. */ @property() content = ''; @@ -100,9 +101,8 @@ export default class SlTooltip extends LitElement { this.handleMouseOut = this.handleMouseOut.bind(this); } - firstUpdated() { + async firstUpdated() { this.target = this.getTarget(); - this.popover = new Popover(this.target, this.positioner); this.syncOptions(); this.addEventListener('blur', this.handleBlur, true); @@ -112,16 +112,20 @@ export default class SlTooltip extends LitElement { this.addEventListener('mouseover', this.handleMouseOver); this.addEventListener('mouseout', this.handleMouseOut); - // Show on init if open - this.positioner.hidden = !this.open; - if (this.open) { - this.show(); - } + // Set initial visibility + this.tooltip.hidden = !this.open; + + // Set the initialized flag after the first update is complete + await this.updateComplete; + this.hasInitialized = true; } disconnectedCallback() { super.disconnectedCallback(); - this.popover.destroy(); + + if (this.popover) { + this.popover.destroy(); + } this.removeEventListener('blur', this.handleBlur, true); this.removeEventListener('focus', this.handleFocus, true); @@ -132,27 +136,56 @@ export default class SlTooltip extends LitElement { } /** Shows the tooltip. */ - show() { + async show() { // Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher - if (this.isVisible || this.disabled) { + if (!this.hasInitialized || this.open) { return; } + if (this.popover) { + this.popover.destroy(); + } + const slShow = this.slShow.emit(); if (slShow.defaultPrevented) { this.open = false; return; } - this.isVisible = true; this.open = true; - this.popover.show(); + + await stopAnimations(this.tooltip); + + this.popover = createPopper(this.target, this.positioner, { + placement: this.placement, + strategy: 'absolute', + modifiers: [ + { + name: 'flip', + options: { + boundary: 'viewport' + } + }, + { + name: 'offset', + options: { + offset: [this.skidding, this.distance] + } + } + ] + }); + + this.tooltip.hidden = false; + const { keyframes, options } = getAnimation(this, 'tooltip.show'); + await animateTo(this.tooltip, keyframes, options); + + this.slAfterShow.emit(); } /** Shows the tooltip. */ - hide() { + async hide() { // Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher - if (!this.isVisible) { + if (!this.hasInitialized || !this.open) { return; } @@ -162,9 +195,14 @@ export default class SlTooltip extends LitElement { return; } - this.isVisible = false; this.open = false; - this.popover.hide(); + + await stopAnimations(this.tooltip); + const { keyframes, options } = getAnimation(this, 'tooltip.hide'); + await animateTo(this.tooltip, keyframes, options); + this.tooltip.hidden = true; + + this.slAfterHide.emit(); } getTarget() { @@ -208,13 +246,17 @@ export default class SlTooltip extends LitElement { handleMouseOver() { if (this.hasTrigger('hover')) { - this.show(); + const delay = parseDuration(getComputedStyle(this).getPropertyValue('--show-delay')); + clearTimeout(this.hoverTimeout); + this.hoverTimeout = setTimeout(() => this.show(), delay); } } handleMouseOut() { if (this.hasTrigger('hover')) { - this.hide(); + const delay = parseDuration(getComputedStyle(this).getPropertyValue('--hide-delay')); + clearTimeout(this.hoverTimeout); + this.hoverTimeout = setTimeout(() => this.hide(), delay); } } @@ -258,11 +300,21 @@ export default class SlTooltip extends LitElement { if (this.popover) { this.popover.setOptions({ placement: this.placement, - distance: this.distance, - skidding: this.skidding, - transitionElement: this.tooltip, - onAfterHide: () => this.slAfterHide.emit(), - onAfterShow: () => this.slAfterShow.emit() + strategy: 'absolute', + modifiers: [ + { + name: 'flip', + options: { + boundary: 'viewport' + } + }, + { + name: 'offset', + options: { + offset: [this.skidding, this.distance] + } + } + ] }); } } @@ -289,6 +341,19 @@ export default class SlTooltip extends LitElement { } } +setDefaultAnimation('tooltip.show', { + keyframes: [{ opacity: 0 }, { opacity: 0, transform: 'scale(0.8)' }, { opacity: 1, transform: 'scale(1)' }], + options: { duration: 150, easing: 'ease' } +}); + +setDefaultAnimation('tooltip.hide', { + keyframes: [ + { opacity: 1, transform: 'scale(1)' }, + { opacity: 0, transform: 'scale(0.8)' } + ], + options: { duration: 150, easing: 'ease' } +}); + declare global { interface HTMLElementTagNameMap { 'sl-tooltip': SlTooltip;