Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web-components): add Tooltip component #32852

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: add Tooltip component",
"packageName": "@fluentui/web-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
49 changes: 49 additions & 0 deletions packages/web-components/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -4029,6 +4029,55 @@ export const ToggleButtonStyles: ElementStyles;
// @public
export const ToggleButtonTemplate: ElementViewTemplate<ToggleButton>;

// @public
export class Tooltip extends FASTElement {
constructor();
anchor: string;
// (undocumented)
connectedCallback(): void;
delay?: number;
// (undocumented)
disconnectedCallback(): void;
elementInternals: ElementInternals;
handleBlur: () => void;
handleFocus: () => void;
handleMouseEnter: () => void;
handleMouseLeave: () => void;
// @internal
hideTooltip(delay?: number): void;
positioning: string;
// @internal
showTooltip(delay?: number): void;
}

// @public
export const TooltipDefinition: FASTElementDefinition<typeof Tooltip>;

// @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<typeof TooltipPositioning>;

// @public
export const TooltipStyles: ElementStyles;

// @public
export const TooltipTemplate: ViewTemplate<Tooltip, any>;

// 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)
Expand Down
1 change: 1 addition & 0 deletions packages/web-components/src/index-rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions packages/web-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/web-components/src/tooltip/define.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { FluentDesignSystem } from '../fluent-design-system.js';
import { definition } from './tooltip.definition.js';

definition.define(FluentDesignSystem.registry);
5 changes: 5 additions & 0 deletions packages/web-components/src/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -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';
17 changes: 17 additions & 0 deletions packages/web-components/src/tooltip/tooltip.definition.ts
Original file line number Diff line number Diff line change
@@ -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: `<fluent-tooltip>`
*/
export const definition = Tooltip.compose({
name: `${FluentDesignSystem.prefix}-tooltip`,
template,
styles,
});
26 changes: 26 additions & 0 deletions packages/web-components/src/tooltip/tooltip.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ValuesOf } from '../utils/typings.js';

/**
* The TooltipPositioning options and their corresponding CSS values
* @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',
} as const;

/**
* The TooltipPositioning type
* @public
*/
export type TooltipPositioning = ValuesOf<typeof TooltipPositioning>;
155 changes: 155 additions & 0 deletions packages/web-components/src/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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 */ `
<button id="target">Target</button>
<fluent-tooltip anchor="target">This is a tooltip</fluent-tooltip>
`);
});

/**
* 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');
});

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');
const button = page.locator('button');
await expect(element).not.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('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);
});
});
Loading
Loading