Skip to content

Commit e1c7660

Browse files
committed
Add accessible name to tooltip portal element with id theme-picker-tooltip
1 parent f40e625 commit e1c7660

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

‎axe-results-incomplete.json‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[
2+
{
3+
"id": "aria-valid-attr-value",
4+
"impact": "critical",
5+
"tags": [
6+
"cat.aria",
7+
"wcag2a",
8+
"wcag412",
9+
"EN-301-549",
10+
"EN-9.4.1.2",
11+
"RGAAv4",
12+
"RGAA-7.1.1"
13+
],
14+
"description": "Ensure all ARIA attributes have valid values",
15+
"help": "ARIA attributes must conform to valid values",
16+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/aria-valid-attr-value?application=playwright",
17+
"nodes": [
18+
{
19+
"any": [],
20+
"all": [
21+
{
22+
"id": "aria-valid-attr-value",
23+
"data": {
24+
"messageKey": "controlsWithinPopup",
25+
"needsReview": "aria-controls=\"theme-picker-panel\""
26+
},
27+
"relatedNodes": [],
28+
"impact": "critical",
29+
"message": "Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"theme-picker-panel\""
30+
}
31+
],
32+
"none": [],
33+
"impact": "critical",
34+
"html": "<button class=\"theme-toggle-btn the...\" type=\"button\" aria-expanded=\"false\" aria-controls=\"theme-picker-panel\" aria-label=\"toggle theme switche...\" aria-haspopup=\"true\" data-theme-toggle=\"\">",
35+
"target": [
36+
".theme-toggle-btn"
37+
],
38+
"failureSummary": "Fix all of the following:\n Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: aria-controls=\"theme-picker-panel\""
39+
}
40+
]
41+
}
42+
]

‎axe-results-violations.json‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
[
2+
{
3+
"id": "aria-tooltip-name",
4+
"impact": "serious",
5+
"tags": [
6+
"cat.aria",
7+
"wcag2a",
8+
"wcag412",
9+
"EN-301-549",
10+
"EN-9.4.1.2"
11+
],
12+
"description": "Ensure every ARIA tooltip node has an accessible name",
13+
"help": "ARIA tooltip nodes must have an accessible name",
14+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/aria-tooltip-name?application=playwright",
15+
"nodes": [
16+
{
17+
"any": [
18+
{
19+
"id": "has-visible-text",
20+
"data": null,
21+
"relatedNodes": [],
22+
"impact": "serious",
23+
"message": "Element does not have text that is visible to screen readers"
24+
},
25+
{
26+
"id": "aria-label",
27+
"data": null,
28+
"relatedNodes": [],
29+
"impact": "serious",
30+
"message": "aria-label attribute does not exist or is empty"
31+
},
32+
{
33+
"id": "aria-labelledby",
34+
"data": null,
35+
"relatedNodes": [],
36+
"impact": "serious",
37+
"message": "aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty"
38+
},
39+
{
40+
"id": "non-empty-title",
41+
"data": {
42+
"messageKey": "noAttr"
43+
},
44+
"relatedNodes": [],
45+
"impact": "serious",
46+
"message": "Element has no title attribute"
47+
}
48+
],
49+
"all": [],
50+
"none": [],
51+
"impact": "serious",
52+
"html": "<div data-theme-tooltip-portal=\"true\" id=\"theme-picker-tooltip\" role=\"tooltip\" class=\"fixed left-0 top-0 z-(--z-theme-picker) pointer-events-none opacity-0 transition-opacity duration-150 ease-out\">",
53+
"target": [
54+
"#theme-picker-tooltip"
55+
],
56+
"failureSummary": "Fix any of the following:\n Element does not have text that is visible to screen readers\n aria-label attribute does not exist or is empty\n aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n Element has no title attribute"
57+
}
58+
]
59+
}
60+
]

‎src/components/ThemePicker/client/__tests__/index.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const getThemeButtons = (root: ThemePickerElement) =>
4949
queryElements<HTMLButtonElement>(root, '[data-theme]')
5050
const getThemeSelectButtons = (root: ThemePickerElement) =>
5151
queryElements<HTMLButtonElement>(root, '.themepicker__selectBtn')
52+
const getTooltipPortal = (doc: Document) =>
53+
queryElement<HTMLDivElement>(doc, '[data-theme-tooltip-portal]')
5254

5355
let container: AstroContainer
5456

@@ -176,6 +178,23 @@ describe('ThemePicker Component', () => {
176178
expect(header.getAttribute('role')).toBe('banner')
177179
})
178180
})
181+
182+
it('keeps the tooltip hidden from assistive tech until a theme is focused', async () => {
183+
await renderThemePickerDom(({ element, window }) => {
184+
const tooltip = getTooltipPortal(window.document)
185+
expect(tooltip.getAttribute('role')).toBe('tooltip')
186+
expect(tooltip.getAttribute('aria-hidden')).toBe('true')
187+
expect(tooltip.hasAttribute('hidden')).toBe(true)
188+
189+
const [firstThemeButton] = getThemeButtons(element)
190+
firstThemeButton.dispatchEvent(new window.FocusEvent('focusin', { bubbles: true }))
191+
192+
expect(firstThemeButton.getAttribute('aria-describedby')).toBe('theme-picker-tooltip')
193+
expect(tooltip.hasAttribute('hidden')).toBe(false)
194+
expect(tooltip.getAttribute('aria-hidden')).toBeNull()
195+
expect(tooltip.textContent?.trim().length).toBeGreaterThan(0)
196+
})
197+
})
179198
})
180199

181200
describe('CSS Classes', () => {

‎src/components/ThemePicker/client/index.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export class ThemePickerElement extends LitElement {
8888

8989
private readonly tooltipId = 'theme-picker-tooltip'
9090

91+
private readonly hiddenTooltipAttribute = 'hidden'
92+
9193
private readonly emblaUpdateHandler = () => this.updateEmblaOverflowVisibility()
9294

9395
// Track View Transitions
@@ -565,6 +567,8 @@ export class ThemePickerElement extends LitElement {
565567
portal.setAttribute('data-theme-tooltip-portal', 'true')
566568
portal.setAttribute('id', this.tooltipId)
567569
portal.setAttribute('role', 'tooltip')
570+
portal.setAttribute('aria-hidden', 'true')
571+
portal.setAttribute(this.hiddenTooltipAttribute, '')
568572
portal.className =
569573
'fixed left-0 top-0 z-(--z-theme-picker) pointer-events-none opacity-0 transition-opacity duration-150 ease-out'
570574

@@ -616,7 +620,9 @@ export class ThemePickerElement extends LitElement {
616620
this.tooltipPortalContent.textContent = tooltipText
617621
this.tooltipActiveButton = button
618622

619-
// Make it accessible when visible.
623+
// Expose the tooltip only while it has content and is referenced.
624+
this.tooltipPortal.removeAttribute('aria-hidden')
625+
this.tooltipPortal.removeAttribute(this.hiddenTooltipAttribute)
620626
button.setAttribute('aria-describedby', this.tooltipId)
621627

622628
this.updateTooltipPosition()
@@ -633,6 +639,8 @@ export class ThemePickerElement extends LitElement {
633639
}
634640

635641
this.tooltipActiveButton = null
642+
this.tooltipPortal.setAttribute('aria-hidden', 'true')
643+
this.tooltipPortal.setAttribute(this.hiddenTooltipAttribute, '')
636644
this.tooltipPortal.style.opacity = '0'
637645

638646
if (this.tooltipRafId !== null && typeof cancelAnimationFrame === 'function') {

0 commit comments

Comments
 (0)