Skip to content
Merged
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
47 changes: 39 additions & 8 deletions 2nd-gen/packages/core/components/tooltip/Tooltip.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ export abstract class TooltipBase extends SpectrumElement {

/**
* When set, wires `ariaLabelledByElements` instead of `ariaDescribedByElements` on the trigger's
* inner interactive element. For icon-only triggers where the tooltip text is the sole accessible name.
*
* Additive/deferred: active in the additive phase.
* inner interactive element. Use for icon-only triggers where the tooltip text is the sole accessible
* name and adding an accessible label directly to the trigger host is not possible.
* Prefer `accessible-label` on the trigger when feasible — it works without this attribute.
*
* @default false
*/
Expand Down Expand Up @@ -195,11 +195,28 @@ export abstract class TooltipBase extends SpectrumElement {
const target = (trigger.shadowRoot?.querySelector('button') ??
trigger) as Element & {
ariaDescribedByElements: Element[] | null;
ariaLabelledByElements: Element[] | null;
};
const current = target.ariaDescribedByElements ?? [];
target.ariaDescribedByElements = this.open
? [...current.filter((el) => el !== this), this]
: current.filter((el) => el !== this);

if (this.labeling) {
// Remove any stale describedby reference (e.g. if labeling changed while open).
const described = target.ariaDescribedByElements ?? [];
target.ariaDescribedByElements = described.filter((el) => el !== this);

const labelled = target.ariaLabelledByElements ?? [];
target.ariaLabelledByElements = this.open
? [...labelled.filter((el) => el !== this), this]
: labelled.filter((el) => el !== this);
} else {
// Remove any stale labelledby reference (e.g. if labeling changed while open).
const labelled = target.ariaLabelledByElements ?? [];
target.ariaLabelledByElements = labelled.filter((el) => el !== this);

const described = target.ariaDescribedByElements ?? [];
target.ariaDescribedByElements = this.open
? [...described.filter((el) => el !== this), this]
: described.filter((el) => el !== this);
}
}

private dispatchAfterEvent(isOpen: boolean): void {
Expand Down Expand Up @@ -234,7 +251,10 @@ export abstract class TooltipBase extends SpectrumElement {
this.open = isOpen;
}
// When no CSS transition is active, dispatch after-* immediately since transitionend will not fire.
if (getComputedStyle(this).transitionDuration === '0s') {
// transitionDuration is comma-separated when multiple properties transition ("0s, 0s, …"),
// so check that every value in the list is zero rather than comparing the full string.
const durations = getComputedStyle(this).transitionDuration.split(',');
if (durations.every((d) => d.trim() === '0s')) {
this.afterEventPending = false;
this.dispatchAfterEvent(isOpen);
}
Expand All @@ -248,6 +268,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 @@ -258,6 +285,8 @@ export abstract class TooltipBase extends SpectrumElement {
this.hidePopover();
}
}
}
if (changedProperties.has('open') || changedProperties.has('labeling')) {
this.syncAriaRelationship();
}
}
Expand All @@ -269,12 +298,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);
}
}
9 changes: 9 additions & 0 deletions 2nd-gen/packages/core/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ type SWCWarningOptions = {
};
type BrandedSWCWarningID = `${ElementLocalName}:${WarningType}:${WarningLevel}`;

/**
* ARIA element-reflection properties (IDRef-free associations). Not yet in the
* TypeScript DOM lib.
*/
interface ARIAMixin {
ariaDescribedByElements: readonly Element[] | null;
ariaLabelledByElements: readonly Element[] | null;
}

interface Window {
__swc: {
DEBUG: boolean;
Expand Down
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,38 +142,42 @@ 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 = (
tooltipArgs: Record<string, unknown>,
id: string,
buttonLabel: string
) => html`
<swc-button id=${id} @click=${makeToggle(id)}>${buttonLabel}</swc-button>
${template({ ...tooltipArgs, for: id })}
`;
buttonLabel?: string,
iconOnly: boolean = false
) => {
if (!iconOnly) {
return html`
<swc-button id=${id} @click=${makeToggle(id)}>${buttonLabel}</swc-button>
${template({ ...tooltipArgs, for: id })}
`;
} else {
return html`
<swc-button
id=${id}
@click=${makeToggle(id)}
accessible-label=${String(tooltipArgs['default-slot'] ?? '')}
>
<svg
slot="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 36 36"
aria-hidden="true"
focusable="false"
>
<path
d="M31.5 17H19V4.5a1 1 0 0 0-2 0V17H4.5a1 1 0 0 0 0 2H17v12.5a1 1 0 0 0 2 0V19h12.5a1 1 0 0 0 0-2z"
/>
</svg>
</swc-button>
${template({ ...tooltipArgs, for: id })}
`;
}
};

/**
* Each story renders one or more buttons that trigger associated tooltips when clicked.
Expand All @@ -184,6 +187,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 Expand Up @@ -344,6 +350,37 @@ export const Placements: Story = {

// TODO: will complete in separate documentation pass of phase 7

// ──────────────────────────────
// BEHAVIORS STORIES
// ──────────────────────────────

/**
* When a trigger has no visible text label and the tooltip text is its sole accessible name,
* set the `labeling` attribute on the tooltip. This switches the ARIA wiring from
* `ariaDescribedByElements` (supplementary description) to `ariaLabelledByElements` (accessible name)
* on the trigger's inner interactive element.
*/
export const Labeling: Story = {
render: (args) => html`
${triggered(
{
...args,
labeling: true,
'default-slot': 'Save changes',
},
'tooltip-labeling-trigger',
undefined,
true
)}
`,
args: {
placement: 'top',
variant: 'neutral',
},
tags: ['behaviors'],
parameters: { 'section-order': 1 },
};

// ────────────────────────────────
// ACCESSIBILITY STORIES
// ────────────────────────────────
Expand Down
Empty file.
Loading
Loading