From 0f43b250f81ea8208a350792363f56d14cae7716 Mon Sep 17 00:00:00 2001 From: Westbrook Johnson Date: Sun, 21 Nov 2021 21:31:52 -0500 Subject: [PATCH] fix(tooltip): ensure delayed and self-managed tooltips do not disrupt the page layout --- package.json | 2 +- packages/tooltip/src/Tooltip.ts | 49 +++++++++++++++------ packages/tooltip/src/tooltip.css | 4 -- packages/tooltip/stories/tooltip.stories.ts | 6 ++- packages/tooltip/test/tooltip.test.ts | 34 +++++++++++++- 5 files changed, 74 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index ec2783b845..4010d8f322 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "cssnano/**/postcss-calc": "7.0.0", "rollup-plugin-workbox": "6.1.1" }, - "customElements": "projects/documentation/custom-elements.json", + "customElements": ".storybook/custom-elements.json", "workspaces": [ "packages/*", "projects/*" diff --git a/packages/tooltip/src/Tooltip.ts b/packages/tooltip/src/Tooltip.ts index 2891d119d2..09f80f1255 100644 --- a/packages/tooltip/src/Tooltip.ts +++ b/packages/tooltip/src/Tooltip.ts @@ -29,6 +29,14 @@ import { openOverlay } from '@spectrum-web-components/overlay/src/loader.js'; import tooltipStyles from './tooltip.css.js'; +export class TooltipProxy extends HTMLElement { + disconnectedCallback(): void { + this.dispatchEvent(new Event('disconnected')); + } +} + +customElements.define('tooltip-proxy', TooltipProxy); + /** * @element sp-tooltip * @@ -103,7 +111,18 @@ export class Tooltip extends SpectrumElement { event.detail.overlayContentTipElement = this.tipElement; } - private _proxy?: HTMLElement; + private _proxy!: HTMLElement; + + private generateProxy(): void { + if (!this._proxy) { + this._proxy = document.createElement('tooltip-proxy'); + this._proxy.id = this._tooltipId; + this._proxy.hidden = true; + this._proxy.slot = 'hidden-tooltip-content'; + this._proxy.setAttribute('role', 'tooltip'); + this._proxy.addEventListener('disconnected', this.closeOverlay); + } + } public overlayWillOpenCallback({ trigger, @@ -111,15 +130,10 @@ export class Tooltip extends SpectrumElement { trigger: HTMLElement; }): void { this.setAttribute('aria-hidden', 'true'); - if (!this._proxy) { - this._proxy = document.createElement('span'); - this._proxy.textContent = this.textContent; - this._proxy.id = this._tooltipId; - this._proxy.hidden = true; - this._proxy.setAttribute('role', 'tooltip'); - } + this.generateProxy(); + this._proxy.textContent = this.textContent; trigger.setAttribute('aria-describedby', this._tooltipId); - trigger.insertAdjacentElement('beforebegin', this._proxy); + this.insertAdjacentElement('beforebegin', this._proxy); } public overlayOpenCancelledCallback({ @@ -137,10 +151,7 @@ export class Tooltip extends SpectrumElement { } private removeProxy(): void { - if (this._proxy) { - this._proxy.remove(); - this._proxy = undefined; - } + this._proxy.remove(); } private closeOverlayCallback?: Promise<() => void>; @@ -167,14 +178,25 @@ export class Tooltip extends SpectrumElement { delete this.closeOverlayCallback; }; + private previousSlot?: string; + private manageTooltip(): void { const parentElement = this.parentElement as HTMLElement; if (this.selfManaged) { + if (this.slot) { + this.previousSlot = this.slot; + } + this.slot = 'self-managed-tooltip'; parentElement.addEventListener('pointerenter', this.openOverlay); parentElement.addEventListener('focusin', this.openOverlay); parentElement.addEventListener('pointerleave', this.closeOverlay); parentElement.addEventListener('focusout', this.closeOverlay); } else { + if (this.previousSlot) { + this.slot = this.previousSlot; + } else if (this.slot === 'self-managed-tooltip') { + this.removeAttribute('slot'); + } parentElement.removeEventListener('pointerenter', this.openOverlay); parentElement.removeEventListener('focusin', this.openOverlay); parentElement.removeEventListener( @@ -201,6 +223,7 @@ export class Tooltip extends SpectrumElement { this.closeOverlay(); } } + this.generateProxy(); super.update(changed); } diff --git a/packages/tooltip/src/tooltip.css b/packages/tooltip/src/tooltip.css index ae92746b37..60d87b1cae 100644 --- a/packages/tooltip/src/tooltip.css +++ b/packages/tooltip/src/tooltip.css @@ -19,10 +19,6 @@ governing permissions and limitations under the License. border: none; } -:host([self-managed]:not([aria-hidden])) { - display: none; -} - :host([placement*='right']) #tip, :host([placement*='left']) #tip, :host([placement*='bottom']) #tip { diff --git a/packages/tooltip/stories/tooltip.stories.ts b/packages/tooltip/stories/tooltip.stories.ts index 10709ec29c..1254347a3e 100644 --- a/packages/tooltip/stories/tooltip.stories.ts +++ b/packages/tooltip/stories/tooltip.stories.ts @@ -259,6 +259,10 @@ const overlayStyles = html` flex: none; margin: 24px 0; } + + .self-managed:nth-child(3) { + margin-left: 50px; + } `; @@ -304,7 +308,7 @@ export const selfManaged = ({ delayed, }: Properties): TemplateResult => html` ${overlayStyles} - + This is a button. { const el = button.querySelector('sp-tooltip') as Tooltip; await elementUpdated(el); - await expect(el).to.be.accessible(); + await expect(button).to.be.accessible(); const opened = oneEvent(button, 'sp-opened'); button.focus(); @@ -56,7 +56,7 @@ describe('Tooltip', () => { await elementUpdated(el); expect(el.open).to.be.true; - await expect(el).to.be.accessible(); + await expect(button).to.be.accessible(); const closed = oneEvent(button, 'sp-closed'); button.blur(); @@ -65,6 +65,36 @@ describe('Tooltip', () => { expect(el.open).to.be.false; }); + it('cleans up when self manages', async () => { + const button = await fixture