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

fix(tooltip): announce content to assistive technology #2069

Merged
merged 29 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
75fea5d
feat(tooltip): add ESC to close functionality
adamjohnson Nov 25, 2024
ee1ffe3
refactor(tooltip): rename CSS class
adamjohnson Nov 25, 2024
5a52b82
fix(tooltip): add `aria-describedby`/`id` pair
adamjohnson Nov 26, 2024
34e2a63
chore(tooltip): add changeset
adamjohnson Nov 26, 2024
5ae835d
fix(tooltip): remove slotchange handler functions
adamjohnson Dec 2, 2024
d9a66f9
fix(tooltip): use native `<button>` with `aria-describedby`/ IDREF pair
adamjohnson Dec 2, 2024
dfe4fdb
docs(tooltip): add a11y markup guidance around `<button>`/`<rh-button>`
adamjohnson Dec 2, 2024
3e12c51
fix(tooltip): attach event listener to `globalThis` for improved perf…
adamjohnson Dec 3, 2024
a46cac0
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Dec 3, 2024
776ba7a
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Dec 6, 2024
9bf1a1c
fix(tooltip): announce content
bennypowers Dec 8, 2024
ab9017e
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Dec 9, 2024
cbc5dc7
Merge branch 'main' into fix/tooltip/a11y-announce-content
bennypowers Dec 10, 2024
2fa8d8e
fix(tooltip): global announcer
bennypowers Dec 10, 2024
91d1e55
test(tooltip): ax queries
bennypowers Dec 10, 2024
9720690
fix(tooltip): surrender to compilers
bennypowers Dec 10, 2024
e684697
fix(tooltip): crossbrowser aria
bennypowers Dec 10, 2024
be61a06
docs(tooltip): remove test icon
bennypowers Dec 10, 2024
b33e509
fix(tooltip): revert regression
bennypowers Dec 10, 2024
5dfe772
perf(tooltip): shave bytes
bennypowers Dec 10, 2024
4e9dd1f
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Dec 16, 2024
357d4a1
fix(tooltip): use logical properties for announcer
adamjohnson Dec 17, 2024
e24478e
docs(tooltip): remove outdated a11y docs
adamjohnson Dec 17, 2024
a9ce72a
fix(tooltip): re-add `<rh-button>` to tooltip demos
adamjohnson Dec 17, 2024
d2bda3f
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Dec 18, 2024
ee2bdea
fix(tooltip): prune unused class
adamjohnson Dec 18, 2024
cea398a
fix(tooltip): change `aria-live` to `role: status`
adamjohnson Dec 18, 2024
12b660b
Merge branch 'main' into fix/tooltip/a11y-announce-content
adamjohnson Jan 3, 2025
8fe9330
Merge branch 'main' into fix/tooltip/a11y-announce-content
bennypowers Jan 5, 2025
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
5 changes: 5 additions & 0 deletions .changeset/brown-fans-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rhds/elements": patch
---

`<rh-tooltip>`: make tooltip content available to assistive technology
2 changes: 1 addition & 1 deletion elements/rh-tooltip/rh-tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
var(--rh-tooltip__content--BackgroundColor, var(--rh-color-surface-darkest, #151515)));
}

.c {
.display-c {
display: contents;
}

Expand Down
81 changes: 77 additions & 4 deletions elements/rh-tooltip/rh-tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { styleMap } from 'lit/directives/style-map.js';

import { colorContextConsumer, type ColorTheme } from '../../lib/context/color/consumer.js';

import { getRandomId } from '@patternfly/pfe-core/functions/random.js';
import {
FloatingDOMController,
type Placement,
Expand Down Expand Up @@ -57,10 +58,13 @@ export class RhTooltip extends LitElement {

#initialized = false;

#tooltipContentID = getRandomId('tooltip-content');

override connectedCallback(): void {
super.connectedCallback();
ENTER_EVENTS.forEach(evt => this.addEventListener(evt, this.show));
EXIT_EVENTS.forEach(evt => this.addEventListener(evt, this.hide));
document.addEventListener('keydown', this.#onKeydown);
adamjohnson marked this conversation as resolved.
Show resolved Hide resolved
}

override render() {
Expand All @@ -75,11 +79,11 @@ export class RhTooltip extends LitElement {
[on]: !!on,
[anchor]: !!anchor,
[alignment]: !!alignment })}">
<div class="c" role="tooltip" aria-labelledby="tooltip">
<slot id="invoker"></slot>
<div class="display-c">
<slot id="invoker" @slotchange=${this.#handleInvokerSlotChange}></slot>
</div>
<div class="c" aria-hidden="${String(!open) as 'true' | 'false'}">
<slot id="tooltip" name="content">${this.content}</slot>
<div class="display-c" aria-hidden="${String(!open) as 'true' | 'false'}">
<slot id="tooltip" name="content" @slotchange=${this.#handleTooltipSlotChange}>${this.content}</slot>
</div>
</div>
`;
Expand All @@ -100,6 +104,75 @@ export class RhTooltip extends LitElement {
async hide() {
await this.#float.hide();
}

#onKeydown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
this.hide();
}
};

#handleInvokerSlotChange(): void {
const invokerSlot = this.shadowRoot?.querySelector<HTMLSlotElement>('#invoker');
if (!invokerSlot) {
return;
}

const assignedNodes = invokerSlot.assignedElements({ flatten: true });

const focusableSelector = `
a[href]:not([inert]):not([inert] *):not([tabindex^="-"]),
input:not([type="hidden"]):not([type="radio"]):not([inert]):not([inert] *):not([tabindex^="-"]):not(:disabled),
input[type="radio"]:not([inert]):not([inert] *):not([tabindex^="-"]):not(:disabled),
select:not([inert]):not([inert] *):not([tabindex^="-"]):not(:disabled),
textarea:not([inert]):not([inert] *):not([tabindex^="-"]):not(:disabled),
button:not([inert]):not([inert] *):not([tabindex^="-"]):not(:disabled)
`;

const findFocusableElement = (node: Element): HTMLElement | null => {
if (node.matches(focusableSelector)) {
return node as HTMLElement;
}

if (node.tagName.toLowerCase().startsWith('rh-')) {
setTimeout(() => {
const shadowFocusable = node.shadowRoot?.querySelector(focusableSelector);
shadowFocusable?.setAttribute('aria-describedby', this.#tooltipContentID);
adamjohnson marked this conversation as resolved.
Show resolved Hide resolved
return;
}, 50);
}

return null;
};

let invokerElement: HTMLElement | undefined;

for (const node of assignedNodes) {
const focusableElement = findFocusableElement(node);
if (focusableElement) {
invokerElement = focusableElement;
break;
}
}

if (invokerElement) {
invokerElement.setAttribute('aria-describedby', this.#tooltipContentID);
}
}

#handleTooltipSlotChange(): void {
const tooltipSlot = this.shadowRoot?.querySelector<HTMLSlotElement>('#tooltip');
if (!tooltipSlot) {
return;
}

const assignedNodes = tooltipSlot.assignedElements({ flatten: true });
const tooltipContent = assignedNodes[0] as HTMLElement | undefined;

if (tooltipContent) {
tooltipContent.setAttribute('id', this.#tooltipContentID);
tooltipContent.setAttribute('role', 'tooltip');
}
}
}

declare global {
Expand Down