Skip to content
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
8 changes: 4 additions & 4 deletions 1st-gen/packages/tooltip/test/tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ describe('Tooltip', () => {
consoleWarnStub.restore();
});

it('warns when incorrectly using `self-managed`', async () => {
it('warns when using the deprecated `self-managed` attribute', async () => {
const el = await fixture<Tooltip>(html`
<sp-tooltip variant="negative" self-managed>Help text.</sp-tooltip>
`);
Expand All @@ -281,8 +281,8 @@ describe('Tooltip', () => {
expect(consoleWarnStub.called).to.be.true;
const spyCall = consoleWarnStub.getCall(0);
expect(
(spyCall.args[0] as string).includes('Self-managed'),
'confirm dev warning message includes `Self-managed`'
(spyCall.args[0] as string).includes('self-managed'),
'confirm dev warning message includes `self-managed`'
).to.be.true;
expect(
spyCall.args[spyCall.args.length - 1],
Expand All @@ -291,7 +291,7 @@ describe('Tooltip', () => {
data: {
localName: 'sp-tooltip',
type: 'api',
level: 'high',
level: 'deprecation',
},
});
});
Expand Down
9 changes: 9 additions & 0 deletions 2nd-gen/packages/core/components/tooltip/Tooltip.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ export abstract class TooltipBase extends SpectrumElement {
this.dispatchAfterEvent(this.open);
};

// Allows Escape behavior to be testable, does not interfere with native popover dismissal
private readonly handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape' && this.open) {
this.open = false;
}
};

protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has('open')) {
Expand All @@ -269,12 +276,14 @@ export abstract class TooltipBase extends SpectrumElement {
this.addEventListener('beforetoggle', this.handleBeforeToggle);
this.addEventListener('toggle', this.handleToggle);
this.addEventListener('transitionend', this.handleTransitionEnd);
document.addEventListener('keydown', this.handleKeyDown);
}

public override disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener('beforetoggle', this.handleBeforeToggle);
this.removeEventListener('toggle', this.handleToggle);
this.removeEventListener('transitionend', this.handleTransitionEnd);
document.removeEventListener('keydown', this.handleKeyDown);
}
}
7 changes: 7 additions & 0 deletions 2nd-gen/packages/swc/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ const preview = {
'Tools vs packages',
'Writing migration guides',
'Focus management',
'Changelog strategy',
],
'Style guide',
[
Expand Down Expand Up @@ -390,6 +391,11 @@ const preview = {
['Rendering and styling migration analysis'],
'Field label',
['Rendering and styling migration analysis'],
'Grid',
[
'Accessibility migration analysis',
'Rendering and styling migration analysis',
],
'Help text',
['Rendering and styling migration analysis'],
'Illustrated message',
Expand Down Expand Up @@ -436,6 +442,7 @@ const preview = {
'Popover',
[
'Accessibility migration analysis',
'Migration plan',
'Rendering and styling migration analysis',
],
'Progress bar',
Expand Down
5 changes: 5 additions & 0 deletions 2nd-gen/packages/swc/components/tooltip/Tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import styles from './tooltip.css';
* @slot - Text label displayed in the tooltip.
*
* @cssprop --swc-tooltip-background-color - Background color of the tooltip bubble. Defaults to the neutral background color token.
*
* @fires swc-open - Dispatched when the tooltip begins to open, before the transition plays.
* @fires swc-close - Dispatched when the tooltip begins to close, before the transition plays.
* @fires swc-after-open - Dispatched after the tooltip finishes opening, once the transition completes.
* @fires swc-after-close - Dispatched after the tooltip finishes closing, once the transition completes.
*/
export class Tooltip extends TooltipBase {
// ──────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import {
import '@adobe/spectrum-wc/components/button/swc-button.js';
import '@adobe/spectrum-wc/components/tooltip/swc-tooltip.js';

// ────────────────────
// METADATA SETUP
// ────────────────────
// ────────────────
// METADATA
// ────────────────

const { args, argTypes, template } = getStorybookHelpers('swc-tooltip');

Expand Down Expand Up @@ -126,7 +126,6 @@ const makeToggle = (id: string) => (event: MouseEvent) => {
return;
}

setupEventLogger(tooltip);
tooltip.open = !tooltip.open;

if (tooltip.open) {
Expand All @@ -143,28 +142,6 @@ const makeToggle = (id: string) => (event: MouseEvent) => {
}
};

// Temporary: logs tooltip lifecycle events to the console to verify event wiring.
// Replace with proper assertions in migration-testing (Phase 6).
// Storybook's Actions addon doesn't work well for this since the events are re-dispatched
// from the popover in the top layer, so we log directly from the component instance instead.
const loggedTooltips = new WeakSet<Element>();
const setupEventLogger = (tooltip: Element): void => {
if (loggedTooltips.has(tooltip)) {
return;
}
loggedTooltips.add(tooltip);
for (const name of [
'swc-open',
'swc-close',
'swc-after-open',
'swc-after-close',
]) {
tooltip.addEventListener(name, () => {
console.log(`[swc-tooltip] ${name}`);
});
}
};

// Renders a button+tooltip pair linked via the `for` attribute.
// Each pair needs a unique `id` so multiple instances can coexist in the same story.
const triggered = (
Expand All @@ -184,6 +161,9 @@ const meta: Meta = {
title: 'Tooltip',
component: 'swc-tooltip',
parameters: {
actions: {
handles: ['swc-open', 'swc-close', 'swc-after-open', 'swc-after-close'],
},
docs: {
subtitle: `Brief contextual message that appears near a trigger element.`,
},
Expand Down
Empty file.
158 changes: 158 additions & 0 deletions 2nd-gen/packages/swc/components/tooltip/test/tooltip.a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* Copyright 2026 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { expect, test } from '@playwright/test';

import { gotoStory } from '../../../utils/a11y-helpers.js';

/**
* Accessibility tests for Tooltip component (2nd Generation)
*
* ARIA snapshot tests validate the accessibility tree structure.
* aXe WCAG compliance and color contrast validation are run via
* test-storybook (see .storybook/test-runner.ts). Both are included
* in the `test:a11y` command.
*/

test.describe('Tooltip - ARIA Snapshots', () => {
test('closed tooltip is hidden from accessibility tree', async ({ page }) => {
const root = await gotoStory(
page,
'components-tooltip--overview',
'swc-button'
);
// The trigger button is accessible; the closed popover is hidden from the tree.
await expect(root).toMatchAriaSnapshot(`
- button "Open"
`);
});

test('open tooltip exposes role="tooltip" in accessibility tree', async ({
page,
}) => {
await gotoStory(page, 'components-tooltip--overview', 'swc-button');

// Open the tooltip programmatically (HoverController not yet wired).
await page.evaluate(() => {
const tooltip = document.querySelector('swc-tooltip') as HTMLElement & {
open: boolean;
};
if (tooltip) {
tooltip.open = true;
}
});

// Wait for the popover to appear in the top layer.
await page.waitForFunction(() =>
document.querySelector('swc-tooltip')?.matches(':popover-open')
);

const root = page.locator('#storybook-root');
await expect(root).toMatchAriaSnapshot(`
- button "Open"
- tooltip "Save your changes"
`);
});

test('tooltip is removed from accessibility tree when closed', async ({
page,
}) => {
await gotoStory(page, 'components-tooltip--overview', 'swc-button');

// Open then close via the property.
await page.evaluate(() => {
const tooltip = document.querySelector('swc-tooltip') as HTMLElement & {
open: boolean;
};
if (tooltip) {
tooltip.open = true;
}
});
await page.waitForFunction(() =>
document.querySelector('swc-tooltip')?.matches(':popover-open')
);

await page.evaluate(() => {
const tooltip = document.querySelector('swc-tooltip') as HTMLElement & {
open: boolean;
};
if (tooltip) {
tooltip.open = false;
}
});
await page.waitForFunction(
() => !document.querySelector('swc-tooltip')?.matches(':popover-open')
);

const root = page.locator('#storybook-root');
await expect(root).toMatchAriaSnapshot(`
- button "Open"
`);
});

test('Escape closes an open tooltip', async ({ page }) => {
await gotoStory(page, 'components-tooltip--overview', 'swc-button');

await page.evaluate(() => {
const tooltip = document.querySelector('swc-tooltip') as HTMLElement & {
open: boolean;
};
if (tooltip) {
tooltip.open = true;
}
});
await page.waitForFunction(() =>
document.querySelector('swc-tooltip')?.matches(':popover-open')
);

await page.keyboard.press('Escape');

await page.waitForFunction(
() => !document.querySelector('swc-tooltip')?.matches(':popover-open')
);

const open = await page.evaluate(
() => (document.querySelector('swc-tooltip') as { open?: boolean })?.open
);
expect(open, 'tooltip.open is false after Escape').toBe(false);
});

test('all variant triggers are accessible', async ({ page }) => {
const root = await gotoStory(
page,
'components-tooltip--variants',
'swc-button'
);
await expect(root).toMatchAriaSnapshot(`
- button "Save"
- button "Upload"
- button "Delete"
`);
});

test('all placement triggers are accessible', async ({ page }) => {
const root = await gotoStory(
page,
'components-tooltip--placements',
'swc-button'
);
// Each placement renders a separate trigger button.
await expect(root).toMatchAriaSnapshot(`
- button "top"
- button "right"
- button "end"
- button "bottom"
- button "left"
- button "start"
`);
});
});
Loading
Loading