Skip to content

Commit 7c794fd

Browse files
deodorhunterclaudepnicolli
authored
fix: dimmer AT firefox/macos quirks (#438)
* fix(dimmer): re-apply inert via rAF in firstUpdated to fix Firefox/VoiceOver first-load reachability On Mac/Firefox/VoiceOver, background card headings were reachable via AT navigation on first page load when `active` was set declaratively. After toggling off then on, the bug disappeared — confirming a timing issue. Root cause: `_updateBackgroundInert()` applied `inert` during `slotchange`, but child custom elements (it-card) were still upgrading their shadow DOM at that point. Firefox's AT registered the heading links before the `inert` constraint propagated through the fully-rendered tree. Fix: override `firstUpdated()` to re-apply `_updateBackgroundInert()` after one rAF, matching the AT-settlement pattern in focus-trap-controller. Also documents BSI#1165 (upstream bootstrap-italia a11y issue) in dimmer.scss since it cannot be addressed from this layer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(dimmer): add aria-hidden to background elements to fix Firefox/VoiceOver The rAF firstUpdated workaround did not fix the issue. Root cause: Firefox's inert implementation does not propagate through shadow DOM slot boundaries when building its accessibility tree on first page load. inert IS set in the DOM but Firefox VoiceOver still finds slotted <a> links inside custom elements (e.g. it-card) because it does not walk the light DOM ancestor chain when creating AT nodes for shadow DOM content. aria-hidden="true" operates on the DOM tree (not the flat tree) and Firefox has mature, correct support for it: when set on a container, it explicitly excludes the element and all DOM-tree descendants from the accessibility tree, including light DOM children that happen to be slotted into child custom elements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changeset --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Piero Nicolli <piero.nicolli@redturtle.it>
1 parent 5cde271 commit 7c794fd

4 files changed

Lines changed: 22 additions & 5 deletions

File tree

.changeset/lazy-mammals-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@italia/dimmer': patch
3+
---
4+
5+
Fixed browser-specific quirks with the accessibility trees

packages/dimmer/src/dimmer.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
@use 'bootstrap-italia/src/scss/base/maps' as *;
1111

1212
// Import dimmer component styles from Bootstrap Italia
13+
// TODO: track BSI#1165 (https://github.com/italia/bootstrap-italia/issues/1165) —
14+
// upstream dimmer a11y issue; apply override here once BSI ships a fix.
1315
@use 'bootstrap-italia/src/scss/components/dimmer' as *;
1416

1517
// .dimmer.dimmer-{color} — same element, so the @each must live OUTSIDE .dimmer,

packages/dimmer/src/it-dimmer.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,24 @@ export class ItDimmer extends BaseComponent {
163163
}
164164

165165
/**
166-
* Applica/rimuove `inert` sugli elementi slottati nel background slot.
167-
* Necessario per Safari, che non rispetta `aria-hidden` sull'overlay
168-
* e lascia interagibili gli elementi sotto il dimmer.
166+
* Applica/rimuove `inert` e `aria-hidden` sugli elementi slottati nel background slot.
167+
*
168+
* - `inert`: blocca interazione da tastiera/mouse (necessario per Safari, che non rispetta
169+
* `aria-hidden` sull'overlay).
170+
* - `aria-hidden`: nasconde l'elemento dall'albero di accessibilità via DOM tree (non flat
171+
* tree). Necessario per Firefox: `inert` da solo non propaga correttamente attraverso
172+
* le slot boundary nelle custom elements al primo caricamento della pagina.
169173
*/
170174
private _updateBackgroundInert(): void {
171175
const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');
172176
if (!slot) return;
173177
for (const el of slot.assignedElements({ flatten: true })) {
174178
if (this.active) {
175179
el.setAttribute('inert', '');
180+
el.setAttribute('aria-hidden', 'true');
176181
} else {
177182
el.removeAttribute('inert');
183+
el.removeAttribute('aria-hidden');
178184
}
179185
}
180186
}

packages/dimmer/test/it-dimmer.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,22 +440,24 @@ describe('it-dimmer', () => {
440440
expect(dimmer?.getAttribute('aria-hidden')).to.be.null;
441441
});
442442

443-
it('sets inert on background slot elements when active', async () => {
443+
it('sets inert and aria-hidden on background slot elements when active', async () => {
444444
const el = await fixture<ItDimmer>(html`
445445
<it-dimmer>
446446
<div id="bg">Sfondo</div>
447447
</it-dimmer>
448448
`);
449449
const bg = el.querySelector('#bg')!;
450450
expect(bg.hasAttribute('inert')).to.be.false;
451+
expect(bg.getAttribute('aria-hidden')).to.be.null;
451452

452453
el.active = true;
453454
await el.updateComplete;
454455

455456
expect(bg.hasAttribute('inert')).to.be.true;
457+
expect(bg.getAttribute('aria-hidden')).to.equal('true');
456458
});
457459

458-
it('removes inert from background slot elements when hidden', async () => {
460+
it('removes inert and aria-hidden from background slot elements when hidden', async () => {
459461
const el = await fixture<ItDimmer>(html`
460462
<it-dimmer active>
461463
<div id="bg">Sfondo</div>
@@ -464,11 +466,13 @@ describe('it-dimmer', () => {
464466
await el.updateComplete;
465467
const bg = el.querySelector('#bg')!;
466468
expect(bg.hasAttribute('inert')).to.be.true;
469+
expect(bg.getAttribute('aria-hidden')).to.equal('true');
467470

468471
el.active = false;
469472
await el.updateComplete;
470473

471474
expect(bg.hasAttribute('inert')).to.be.false;
475+
expect(bg.getAttribute('aria-hidden')).to.be.null;
472476
});
473477

474478
it('has part="dimmable" on wrapper', async () => {

0 commit comments

Comments
 (0)