From 101d3f4e7ded92e6d2a618ab88923a3aabfd052a Mon Sep 17 00:00:00 2001 From: Dave Rupert Date: Tue, 17 Sep 2024 10:51:33 -0500 Subject: [PATCH 1/7] feat(web-components): add tooltip component --- packages/web-components/docs/api-report.md | 52 ++++++ packages/web-components/src/index-rollup.ts | 1 + packages/web-components/src/index.ts | 1 + packages/web-components/src/tooltip/define.ts | 4 + packages/web-components/src/tooltip/index.ts | 5 + .../src/tooltip/tooltip.definition.ts | 17 ++ .../src/tooltip/tooltip.options.ts | 26 +++ .../src/tooltip/tooltip.spec.ts | 135 ++++++++++++++++ .../src/tooltip/tooltip.stories.ts | 151 ++++++++++++++++++ .../src/tooltip/tooltip.styles.ts | 94 +++++++++++ .../src/tooltip/tooltip.template.ts | 12 ++ .../web-components/src/tooltip/tooltip.ts | 116 ++++++++++++++ 12 files changed, 614 insertions(+) create mode 100644 packages/web-components/src/tooltip/define.ts create mode 100644 packages/web-components/src/tooltip/index.ts create mode 100644 packages/web-components/src/tooltip/tooltip.definition.ts create mode 100644 packages/web-components/src/tooltip/tooltip.options.ts create mode 100644 packages/web-components/src/tooltip/tooltip.spec.ts create mode 100644 packages/web-components/src/tooltip/tooltip.stories.ts create mode 100644 packages/web-components/src/tooltip/tooltip.styles.ts create mode 100644 packages/web-components/src/tooltip/tooltip.template.ts create mode 100644 packages/web-components/src/tooltip/tooltip.ts diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 2fcf53a72c60a..8a6743d150377 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -4029,6 +4029,58 @@ export const ToggleButtonStyles: ElementStyles; // @public export const ToggleButtonTemplate: ElementViewTemplate; +// @public +export class Tooltip extends FASTElement { + constructor(); + anchor: string; + // (undocumented) + connectedCallback(): void; + delay: number; + // (undocumented) + disconnectedCallback(): void; + // (undocumented) + elementInternals: ElementInternals; + handleBlur: () => void; + handleFocus: () => void; + handleMouseEnter: () => void; + handleMouseLeave: () => void; + // @internal + hideTooltip(delay?: number): void; + positioning: string; + // @internal + showTooltip(delay?: number): void; + // @internal + tooltip: HTMLElement; +} + +// @public +export const TooltipDefinition: FASTElementDefinition; + +// @public +export const TooltipPositioning: { + 'above-start': string; + above: string; + 'above-end': string; + 'below-start': string; + below: string; + 'below-end': string; + 'before-top': string; + before: string; + 'before-bottom': string; + 'after-top': string; + after: string; + 'after-bottom': string; +}; + +// @public +export type TooltipPositioning = ValuesOf; + +// @public +export const TooltipStyles: ElementStyles; + +// @public +export const TooltipTemplate: ViewTemplate; + // Warning: (ae-missing-release-tag) "typographyBody1StrongerStyles" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/packages/web-components/src/index-rollup.ts b/packages/web-components/src/index-rollup.ts index 832770e275eec..33b12fce648d9 100644 --- a/packages/web-components/src/index-rollup.ts +++ b/packages/web-components/src/index-rollup.ts @@ -36,3 +36,4 @@ import './textarea/define.js'; import './text-input/define.js'; import './text/define.js'; import './toggle-button/define.js'; +import './tooltip/define.js'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index a9f1707983906..36ff11fa9ee84 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -295,6 +295,7 @@ export { ToggleButtonTemplate, } from './toggle-button/index.js'; export type { ToggleButtonOptions } from './toggle-button/index.js'; +export { Tooltip, TooltipDefinition, TooltipPositioning, TooltipStyles, TooltipTemplate } from './tooltip/index.js'; export { darkModeStylesheetBehavior, forcedColorsStylesheetBehavior, diff --git a/packages/web-components/src/tooltip/define.ts b/packages/web-components/src/tooltip/define.ts new file mode 100644 index 0000000000000..ca1312dd587df --- /dev/null +++ b/packages/web-components/src/tooltip/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './tooltip.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/tooltip/index.ts b/packages/web-components/src/tooltip/index.ts new file mode 100644 index 0000000000000..ed075c91df59f --- /dev/null +++ b/packages/web-components/src/tooltip/index.ts @@ -0,0 +1,5 @@ +export { definition as TooltipDefinition } from './tooltip.definition.js'; +export { Tooltip } from './tooltip.js'; +export { TooltipPositioning } from './tooltip.options.js'; +export { styles as TooltipStyles } from './tooltip.styles.js'; +export { template as TooltipTemplate } from './tooltip.template.js'; diff --git a/packages/web-components/src/tooltip/tooltip.definition.ts b/packages/web-components/src/tooltip/tooltip.definition.ts new file mode 100644 index 0000000000000..eb88d205fdd88 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.definition.ts @@ -0,0 +1,17 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { Tooltip } from './tooltip.js'; +import { styles } from './tooltip.styles.js'; +import { template } from './tooltip.template.js'; + +/** + * The {@link Tooltip } custom element definition. + * + * @public + * @remarks + * HTML Element: `` + */ +export const definition = Tooltip.compose({ + name: `${FluentDesignSystem.prefix}-tooltip`, + template, + styles, +}); diff --git a/packages/web-components/src/tooltip/tooltip.options.ts b/packages/web-components/src/tooltip/tooltip.options.ts new file mode 100644 index 0000000000000..9ce8a6fe73cb8 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.options.ts @@ -0,0 +1,26 @@ +import type { ValuesOf } from '../utils/typings.js'; + +/** + * An example Tooltip option + * @public + */ +export const TooltipPositioning = { + 'above-start': 'block-start span-inline-end', + above: 'block-start', + 'above-end': 'block-start span-inline-start', + 'below-start': 'block-end span-inline-end', + below: 'block-end', + 'below-end': 'block-end span-inline-start', + 'before-top': 'inline-start span-block-end', + before: 'inline-start', + 'before-bottom': 'inline-start span-block-start', + 'after-top': 'inline-end span-block-end', + after: 'inline-end', + 'after-bottom': 'inline-end span-block-start', +}; + +/** + * An example TooltipPosition type + * @public + */ +export type TooltipPositioning = ValuesOf; diff --git a/packages/web-components/src/tooltip/tooltip.spec.ts b/packages/web-components/src/tooltip/tooltip.spec.ts new file mode 100644 index 0000000000000..744e570f7af08 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.spec.ts @@ -0,0 +1,135 @@ +import { test } from '@playwright/test'; +import { expect, fixtureURL } from '../helpers.tests.js'; +import type { Tooltip } from './tooltip.js'; + +test.describe('Tooltip', () => { + test.beforeEach(async ({ page }) => { + await page.goto(fixtureURL('components-tooltip-tooltip--tooltip')); + await page.waitForFunction(() => customElements.whenDefined('fluent-tooltip')); + + await page.setContent(/* html */ ` + + This is a tooltip + `); + }); + + /** + * WAI ARIA rules + * The element that serves as the tooltip container has role tooltip. + * The element that triggers the tooltip references the tooltip element with aria-describedby. + */ + test('should have the role set to `tooltip`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + await expect(element).toHaveJSProperty('elementInternals.role', 'tooltip'); + }); + + test('should have the `aria-describedby` attribute set to the tooltip id', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await expect(element).toHaveAttribute('id', 'tooltip-target'); + await expect(button).toHaveAttribute('aria-describedby', 'tooltip-target'); + }); + + test('should not be visible by default', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + await expect(element).toBeHidden(); + }); + + test('default placement should be set to `above`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + await expect(element).toHaveAttribute('positioning', 'above'); + }); + + test('should show the tooltip on hover', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await expect(element).toBeHidden(); + await button.hover(); + await expect(element).toBeVisible(); + }); + + test('should show the tooltip on focus', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await expect(element).toBeHidden(); + await button.focus(); + await expect(element).toBeVisible(); + await button.blur(); + await expect(element).toBeHidden(); + }); + + test('position should be set to `above` when `positioning` is set to `above`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await element.evaluate((node: Tooltip) => { + node.positioning = 'above'; + }); + await expect(element).toHaveAttribute('positioning', 'above'); + + // show the element to get the position + await button.focus(); + + const buttonTop = await button.evaluate((node: HTMLElement) => node.getBoundingClientRect().top); + const elementBottom = await element.evaluate((node: HTMLElement) => node.getBoundingClientRect().bottom); + + await expect(buttonTop).toBeGreaterThan(elementBottom); + }); + + test('position should be set to `below` when `positioning` is set to `below`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await element.evaluate((node: Tooltip) => { + node.positioning = 'below'; + }); + await expect(element).toHaveAttribute('positioning', 'below'); + + // show the element to get the position + await button.focus(); + + const buttonBottom = await button.evaluate((node: HTMLElement) => node.getBoundingClientRect().bottom); + const elementTop = await element.evaluate((node: HTMLElement) => node.getBoundingClientRect().top); + + await expect(buttonBottom).toBeLessThan(elementTop); + }); + + test('position should be set to `before` when `positioning` is set to `before`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await element.evaluate((node: Tooltip) => { + node.positioning = 'before'; + }); + await expect(element).toHaveAttribute('positioning', 'before'); + + // show the element to get the position + await button.focus(); + + const buttonLeft = await button.evaluate((node: HTMLElement) => node.getBoundingClientRect().left); + const elementRight = await element.evaluate((node: HTMLElement) => node.getBoundingClientRect().right); + + await expect(buttonLeft).toBeGreaterThan(elementRight); + }); + + test('position should be set to `after` when `positioning` is set to `after`', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await element.evaluate((node: Tooltip) => { + node.positioning = 'after'; + }); + await expect(element).toHaveAttribute('positioning', 'after'); + + // show the element to get the position + await button.focus(); + + const buttonRight = await button.evaluate((node: HTMLElement) => node.getBoundingClientRect().right); + const elementLeft = await element.evaluate((node: HTMLElement) => node.getBoundingClientRect().left); + + await expect(buttonRight).toBeLessThan(elementLeft); + }); +}); diff --git a/packages/web-components/src/tooltip/tooltip.stories.ts b/packages/web-components/src/tooltip/tooltip.stories.ts new file mode 100644 index 0000000000000..ef906f5e73e2c --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.stories.ts @@ -0,0 +1,151 @@ +import { html, repeat } from '@microsoft/fast-element'; +import { uniqueId } from '@microsoft/fast-web-utilities'; +import { Meta, renderComponent, Story } from '../helpers.stories.js'; +import { definition } from './tooltip.definition.js'; +import { Tooltip } from './tooltip.js'; +import { TooltipPositioning } from './tooltip.options.js'; + +const storyTemplate = html` + Hover me + + ${story => story.slottedContent?.()} + +`; + +export default { + title: 'Components/Tooltip', + component: definition.name, + render: renderComponent(storyTemplate), + args: { + anchor: uniqueId('anchor'), + positioning: 'above', + delay: 250, + }, + argTypes: { + anchor: { + control: 'text', + description: 'The target element for the tooltip to anchor on', + table: { category: 'attributes', type: { summary: 'string' } }, + }, + slottedContent: { + control: false, + description: 'The default slot', + table: { category: 'slots', type: {} }, + }, + delay: { + control: 'number', + description: 'Number of milliseconds to delay the tooltip from showing/hiding', + table: { category: 'attributes', type: { summary: 'number' } }, + }, + positioning: { + control: 'select', + options: Object.keys(TooltipPositioning), + table: { category: 'attributes', type: { summary: 'Controls the positioning of the tooltip' } }, + }, + }, +} as unknown as Meta; + +export const Default: Story = renderComponent(storyTemplate).bind({}); +Default.args = { + slottedContent: () => html`Really long tooltip content goes here. lorem ipsum dolor sit amet.`, +}; + +const iconArrowRight = (rotation = 0) => html` + +`; + +const iconArrowLeft = (rotation = 0) => html` + +`; + +const iconArrowUp = (rotation = 0) => html` + +`; + +const glyphs = { + 'above-start': iconArrowRight(-90), + above: iconArrowUp(), + 'above-end': iconArrowLeft(90), + 'below-start': iconArrowLeft(-90), + below: iconArrowUp(180), + 'below-end': iconArrowRight(90), + 'before-top': iconArrowLeft(0), + before: iconArrowUp(-90), + 'before-bottom': iconArrowRight(180), + 'after-top': iconArrowRight(), + after: iconArrowUp(90), + 'after-bottom': iconArrowLeft(180), +}; + +const positionButtonTemplate = html` + + ${x => glyphs[x.id as keyof typeof glyphs]} + +`; + +const positionTooltipTemplate = html` + ${x => x.id} +`; + +export const Positioning: Story = renderComponent(html` + +
${repeat(x => x.storyItems, positionButtonTemplate)}
+ + ${repeat(x => x.storyItems, positionTooltipTemplate)} +`).bind({}); + +Positioning.args = { + storyItems: Object.keys(TooltipPositioning).map(id => ({ id })), +}; diff --git a/packages/web-components/src/tooltip/tooltip.styles.ts b/packages/web-components/src/tooltip/tooltip.styles.ts new file mode 100644 index 0000000000000..4ca1d16934519 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.styles.ts @@ -0,0 +1,94 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '../utils/display.js'; + +import { + borderRadiusMedium, + colorNeutralBackground1, + colorNeutralForeground1, + colorNeutralShadowAmbient, + colorNeutralShadowKey, + fontFamilyBase, + fontSizeBase200, + lineHeightBase200, + spacingHorizontalXS, + spacingVerticalXS, +} from '../theme/design-tokens.js'; + +/** + * Styles for the tooltip component + * @public + */ +export const styles = css` + ${display('inline-flex')} + + :host(:not(:popover-open)) { + display: none; + } + + :host { + --blockOffset: ${spacingVerticalXS}; + --inlineOffset: ${spacingHorizontalXS}; + background: ${colorNeutralBackground1}; + border-radius: ${borderRadiusMedium}; + border: 1px solid var(--colorTransparentStroke); + box-sizing: border-box; + color: ${colorNeutralForeground1}; + display: inline-flex; + filter: drop-shadow(0 0 2px ${colorNeutralShadowAmbient}) drop-shadow(0 4px 8px ${colorNeutralShadowKey}); + font-family: ${fontFamilyBase}; + font-size: ${fontSizeBase200}; + inset-area: block-start; + line-height: ${lineHeightBase200}; + margin: 0; + max-width: 240px; + padding: 4px 11px 6px; + position-try-fallbacks: flip-block flip-inline; + position: absolute; + width: max-content; + z-index: 1; + } + + :host(:is([positioning^='above'], [positioning^='below'])) { + margin-block: var(--blockOffset); + } + :host(:is([positioning^='before'], [positioning^='after'])) { + margin-inline: var(--inlineOffset); + } + + :host([positioning='above-start']) { + inset-area: block-start span-inline-end; + } + :host([positioning='above']) { + inset-area: block-start; + } + :host([positioning='above-end']) { + inset-area: block-start span-inline-start; + } + :host([positioning='below-start']) { + inset-area: block-end span-inline-end; + } + :host([positioning='below']) { + inset-area: block-end; + } + :host([positioning='below-end']) { + inset-area: block-end span-inline-start; + } + :host([positioning='before-top']) { + inset-area: inline-start span-block-end; + } + :host([positioning='before']) { + inset-area: inline-start; + } + :host([positioning='before-bottom']) { + inset-area: inline-start span-block-start; + } + :host([positioning='after-top']) { + inset-area: inline-end span-block-end; + } + :host([positioning='after']) { + inset-area: inline-end; + } + :host([positioning='after-bottom']) { + inset-area: inline-end span-block-start; + } +`; diff --git a/packages/web-components/src/tooltip/tooltip.template.ts b/packages/web-components/src/tooltip/tooltip.template.ts new file mode 100644 index 0000000000000..aef051d1237c4 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.template.ts @@ -0,0 +1,12 @@ +import { html, ref } from '@microsoft/fast-element'; +import type { Tooltip } from './tooltip.js'; + +/** + * Template for the tooltip component + * @public + */ +export const template = html` + +`; diff --git a/packages/web-components/src/tooltip/tooltip.ts b/packages/web-components/src/tooltip/tooltip.ts new file mode 100644 index 0000000000000..3aa868f903e37 --- /dev/null +++ b/packages/web-components/src/tooltip/tooltip.ts @@ -0,0 +1,116 @@ +import { attr, FASTElement } from '@microsoft/fast-element'; + +/** + * A Tooltip Custom HTML Element. + * @public + */ +export class Tooltip extends FASTElement { + public elementInternals = this.attachInternals(); + + /** + * Set the delay for the tooltip + */ + @attr + public delay: number = 250; + + /** + * Set the positioning of the tooltip + */ + @attr + public positioning: string = 'above'; + + /** + * Reference to the tooltip element + * @internal + */ + public tooltip!: HTMLElement; + + /** + * The id of the anchor element for the tooltip + * @public + */ + @attr + public anchor: string = ''; + + /** + * Reference to the anchor element + * @internal + */ + private anchorElement: HTMLElement | null = null; + + public constructor() { + super(); + this.elementInternals.role = 'tooltip'; + } + + public connectedCallback(): void { + super.connectedCallback(); + + this.anchorElement = document.getElementById(this.anchor) || null; + + if (this.anchorElement) { + this.anchorElement.setAttribute('aria-describedby', `tooltip-${this.anchor}`); + + // @ts-expect-error Baseline 2024 + this.anchorElement.style.anchorName = `--${this.anchor}`; + // @ts-expect-error Baseline 2024 + this.style.positionAnchor = `--${this.anchor}`; + + this.anchorElement.addEventListener('focus', this.handleFocus); + this.anchorElement.addEventListener('blur', this.handleBlur); + this.anchorElement.addEventListener('mouseenter', this.handleMouseEnter); + this.anchorElement.addEventListener('mouseleave', this.handleMouseLeave); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this.anchorElement?.removeEventListener('focus', this.handleFocus); + this.anchorElement?.removeEventListener('blur', this.handleBlur); + this.anchorElement?.removeEventListener('mouseenter', this.handleMouseEnter); + this.anchorElement?.removeEventListener('mouseleave', this.handleMouseLeave); + } + + /** + * Shows the tooltip + * @param delay Number of milliseconds to delay showing the tooltip + * @internal + */ + public showTooltip(delay: number = 0): void { + setTimeout(() => { + this.tooltip.setAttribute('aria-hidden', 'false'); + // @ts-expect-error Baseline 2024 + this.showPopover(); + }, delay); + } + + /** + * Hide the tooltip + * @param delay Number of milliseconds to delay hiding the tooltip + * @internal + */ + public hideTooltip(delay: number = 0): void { + setTimeout(() => { + this.tooltip.setAttribute('aria-hidden', 'true'); + // @ts-expect-error Baseline 2024 + this.hidePopover(); + }, delay); + } + + /** + * Show the tooltip on mouse enter + */ + public handleMouseEnter = () => this.showTooltip(this.delay); + /** + * Hide the tooltip on mouse leave + */ + public handleMouseLeave = () => this.hideTooltip(this.delay); + /** + * Show the tooltip on focus + */ + public handleFocus = () => this.showTooltip(); + /** + * Hide the tooltip on blur + */ + public handleBlur = () => this.hideTooltip(); +} From f3d4fc6a2f5bc4768203cf6b6ff9a3863878030c Mon Sep 17 00:00:00 2001 From: Dave Rupert Date: Tue, 17 Sep 2024 12:00:05 -0500 Subject: [PATCH 2/7] add changefile --- ...eb-components-037cbaec-4ee8-4f24-8cb7-1ae8ea8546f4.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-web-components-037cbaec-4ee8-4f24-8cb7-1ae8ea8546f4.json diff --git a/change/@fluentui-web-components-037cbaec-4ee8-4f24-8cb7-1ae8ea8546f4.json b/change/@fluentui-web-components-037cbaec-4ee8-4f24-8cb7-1ae8ea8546f4.json new file mode 100644 index 0000000000000..f82c20fa0fa23 --- /dev/null +++ b/change/@fluentui-web-components-037cbaec-4ee8-4f24-8cb7-1ae8ea8546f4.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: add Tooltip component", + "packageName": "@fluentui/web-components", + "email": "rupertdavid@microsoft.com", + "dependentChangeType": "patch" +} From 78e6bce8f4d27b8614524d08c51868f6e2e83b3f Mon Sep 17 00:00:00 2001 From: Dave Rupert Date: Tue, 17 Sep 2024 12:14:19 -0500 Subject: [PATCH 3/7] fix default story --- .../src/tooltip/tooltip.stories.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/web-components/src/tooltip/tooltip.stories.ts b/packages/web-components/src/tooltip/tooltip.stories.ts index ef906f5e73e2c..f6c5fe5b8924e 100644 --- a/packages/web-components/src/tooltip/tooltip.stories.ts +++ b/packages/web-components/src/tooltip/tooltip.stories.ts @@ -1,33 +1,31 @@ -import { html, repeat } from '@microsoft/fast-element'; +import { html, render, repeat } from '@microsoft/fast-element'; import { uniqueId } from '@microsoft/fast-web-utilities'; import { Meta, renderComponent, Story } from '../helpers.stories.js'; import { definition } from './tooltip.definition.js'; import { Tooltip } from './tooltip.js'; import { TooltipPositioning } from './tooltip.options.js'; -const storyTemplate = html` - Hover me - - ${story => story.slottedContent?.()} - -`; +const storyTemplate = () => { + const id = uniqueId('anchor'); + + return html` + Hover me + + ${story => story.slottedContent?.()} + + `; +}; export default { title: 'Components/Tooltip', component: definition.name, - render: renderComponent(storyTemplate), + render: renderComponent(storyTemplate()), args: { - anchor: uniqueId('anchor'), positioning: 'above', delay: 250, }, argTypes: { anchor: { - control: 'text', description: 'The target element for the tooltip to anchor on', table: { category: 'attributes', type: { summary: 'string' } }, }, @@ -49,7 +47,9 @@ export default { }, } as unknown as Meta; -export const Default: Story = renderComponent(storyTemplate).bind({}); +export const Default: Story = args => { + return renderComponent(html`${render(args, storyTemplate)}`)(args); +}; Default.args = { slottedContent: () => html`Really long tooltip content goes here. lorem ipsum dolor sit amet.`, }; From d22b3ead8a7bc7dc65123f229d2e766bab424846 Mon Sep 17 00:00:00 2001 From: Dave Rupert Date: Thu, 19 Sep 2024 16:11:43 -0500 Subject: [PATCH 4/7] updates per feedback --- packages/web-components/docs/api-report.md | 5 +-- .../src/tooltip/tooltip.options.ts | 4 +- .../src/tooltip/tooltip.spec.ts | 13 ++++++- .../src/tooltip/tooltip.stories.ts | 3 +- .../src/tooltip/tooltip.styles.ts | 33 +++++++++-------- .../src/tooltip/tooltip.template.ts | 4 +- .../web-components/src/tooltip/tooltip.ts | 37 ++++++++++++------- 7 files changed, 58 insertions(+), 41 deletions(-) diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 8a6743d150377..9fa00871a6efc 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -4035,10 +4035,9 @@ export class Tooltip extends FASTElement { anchor: string; // (undocumented) connectedCallback(): void; - delay: number; + delay?: number; // (undocumented) disconnectedCallback(): void; - // (undocumented) elementInternals: ElementInternals; handleBlur: () => void; handleFocus: () => void; @@ -4049,8 +4048,6 @@ export class Tooltip extends FASTElement { positioning: string; // @internal showTooltip(delay?: number): void; - // @internal - tooltip: HTMLElement; } // @public diff --git a/packages/web-components/src/tooltip/tooltip.options.ts b/packages/web-components/src/tooltip/tooltip.options.ts index 9ce8a6fe73cb8..a7157f9130f79 100644 --- a/packages/web-components/src/tooltip/tooltip.options.ts +++ b/packages/web-components/src/tooltip/tooltip.options.ts @@ -1,7 +1,7 @@ import type { ValuesOf } from '../utils/typings.js'; /** - * An example Tooltip option + * The TooltipPositioning options and their corresponding CSS values * @public */ export const TooltipPositioning = { @@ -20,7 +20,7 @@ export const TooltipPositioning = { }; /** - * An example TooltipPosition type + * The TooltipPositioning type * @public */ export type TooltipPositioning = ValuesOf; diff --git a/packages/web-components/src/tooltip/tooltip.spec.ts b/packages/web-components/src/tooltip/tooltip.spec.ts index 744e570f7af08..342eebca54997 100644 --- a/packages/web-components/src/tooltip/tooltip.spec.ts +++ b/packages/web-components/src/tooltip/tooltip.spec.ts @@ -14,10 +14,21 @@ test.describe('Tooltip', () => { }); /** - * WAI ARIA rules + * ARIA APG Tooltip Pattern {@link https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/ } + * ESC dismisses the tooltip. * The element that serves as the tooltip container has role tooltip. * The element that triggers the tooltip references the tooltip element with aria-describedby. */ + test('escape key should hide the tooltip', async ({ page }) => { + const element = page.locator('fluent-tooltip'); + const button = page.locator('button'); + + await button.focus(); + await expect(element).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(element).toBeHidden(); + }); + test('should have the role set to `tooltip`', async ({ page }) => { const element = page.locator('fluent-tooltip'); await expect(element).toHaveJSProperty('elementInternals.role', 'tooltip'); diff --git a/packages/web-components/src/tooltip/tooltip.stories.ts b/packages/web-components/src/tooltip/tooltip.stories.ts index f6c5fe5b8924e..83549ee53be2e 100644 --- a/packages/web-components/src/tooltip/tooltip.stories.ts +++ b/packages/web-components/src/tooltip/tooltip.stories.ts @@ -22,7 +22,6 @@ export default { render: renderComponent(storyTemplate()), args: { positioning: 'above', - delay: 250, }, argTypes: { anchor: { @@ -36,7 +35,7 @@ export default { }, delay: { control: 'number', - description: 'Number of milliseconds to delay the tooltip from showing/hiding', + description: 'Number of milliseconds to delay the tooltip from showing/hiding on hover. Default is 250ms', table: { category: 'attributes', type: { summary: 'number' } }, }, positioning: { diff --git a/packages/web-components/src/tooltip/tooltip.styles.ts b/packages/web-components/src/tooltip/tooltip.styles.ts index 4ca1d16934519..aeade002d652a 100644 --- a/packages/web-components/src/tooltip/tooltip.styles.ts +++ b/packages/web-components/src/tooltip/tooltip.styles.ts @@ -1,6 +1,5 @@ import { css } from '@microsoft/fast-element'; import { display } from '../utils/display.js'; - import { borderRadiusMedium, colorNeutralBackground1, @@ -13,6 +12,7 @@ import { spacingHorizontalXS, spacingVerticalXS, } from '../theme/design-tokens.js'; +import { TooltipPositioning } from './tooltip.options.js'; /** * Styles for the tooltip component @@ -42,53 +42,54 @@ export const styles = css` margin: 0; max-width: 240px; padding: 4px 11px 6px; - position-try-fallbacks: flip-block flip-inline; position: absolute; - width: max-content; + width: auto; z-index: 1; } - :host(:is([positioning^='above'], [positioning^='below'])) { + :host(:is([positioning^='above'], [positioning^='below'], :not([positioning]))) { margin-block: var(--blockOffset); + position-try-fallbacks: flip-block; } :host(:is([positioning^='before'], [positioning^='after'])) { margin-inline: var(--inlineOffset); + position-try-fallbacks: flip-inline; } :host([positioning='above-start']) { - inset-area: block-start span-inline-end; + inset-area: ${TooltipPositioning['above-start']}; } :host([positioning='above']) { - inset-area: block-start; + inset-area: ${TooltipPositioning.above}; } :host([positioning='above-end']) { - inset-area: block-start span-inline-start; + inset-area: ${TooltipPositioning['above-end']}; } :host([positioning='below-start']) { - inset-area: block-end span-inline-end; + inset-area: ${TooltipPositioning['below-start']}; } :host([positioning='below']) { - inset-area: block-end; + inset-area: ${TooltipPositioning.below}; } :host([positioning='below-end']) { - inset-area: block-end span-inline-start; + inset-area: ${TooltipPositioning['below-end']}; } :host([positioning='before-top']) { - inset-area: inline-start span-block-end; + inset-area: ${TooltipPositioning['before-top']}; } :host([positioning='before']) { - inset-area: inline-start; + inset-area: ${TooltipPositioning.before}; } :host([positioning='before-bottom']) { - inset-area: inline-start span-block-start; + inset-area: ${TooltipPositioning['before-bottom']}; } :host([positioning='after-top']) { - inset-area: inline-end span-block-end; + inset-area: ${TooltipPositioning['after-top']}; } :host([positioning='after']) { - inset-area: inline-end; + inset-area: ${TooltipPositioning.after}; } :host([positioning='after-bottom']) { - inset-area: inline-end span-block-start; + inset-area: ${TooltipPositioning['after-bottom']}; } `; diff --git a/packages/web-components/src/tooltip/tooltip.template.ts b/packages/web-components/src/tooltip/tooltip.template.ts index aef051d1237c4..b115a45b239d1 100644 --- a/packages/web-components/src/tooltip/tooltip.template.ts +++ b/packages/web-components/src/tooltip/tooltip.template.ts @@ -1,4 +1,4 @@ -import { html, ref } from '@microsoft/fast-element'; +import { html } from '@microsoft/fast-element'; import type { Tooltip } from './tooltip.js'; /** @@ -6,7 +6,7 @@ import type { Tooltip } from './tooltip.js'; * @public */ export const template = html` -