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 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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": "rupertdavid@microsoft.com",
"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',
};
davatron5000 marked this conversation as resolved.
Show resolved Hide resolved

/**
* The TooltipPositioning type
* @public
*/
export type TooltipPositioning = ValuesOf<typeof TooltipPositioning>;
146 changes: 146 additions & 0 deletions packages/web-components/src/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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');
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);
});
});
Loading
Loading