Skip to content

Commit d4e4781

Browse files
authored
refactor: make it possible to customize overlay modality root (#9749)
1 parent f8fd25e commit d4e4781

File tree

2 files changed

+126
-2
lines changed

2 files changed

+126
-2
lines changed

packages/overlay/src/vaadin-overlay-focus-mixin.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const OverlayFocusMixin = (superClass) =>
4848
constructor() {
4949
super();
5050

51-
this.__ariaModalController = new AriaModalController(this);
51+
this.__ariaModalController = new AriaModalController(this, () => this._modalRoot);
5252
this.__focusTrapController = new FocusTrapController(this);
5353
this.__focusRestorationController = new FocusRestorationController();
5454
}
@@ -71,6 +71,15 @@ export const OverlayFocusMixin = (superClass) =>
7171
this.addController(this.__focusRestorationController);
7272
}
7373

74+
/**
75+
* Override to specify another element used as a modality root,
76+
* e.g. the overlay's owner element, rather than overlay itself.
77+
* @protected
78+
*/
79+
get _modalRoot() {
80+
return this;
81+
}
82+
7483
/**
7584
* Release focus and restore focus after the overlay is closed.
7685
*

packages/overlay/test/focus-trap.test.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { fixtureSync, nextRender, oneEvent, tabKeyDown } from '@vaadin/testing-helpers';
3-
import '../src/vaadin-overlay.js';
43
import { getFocusableElements, isElementFocused } from '@vaadin/a11y-base/src/focus-utils.js';
4+
import { Overlay } from '../src/vaadin-overlay.js';
55

66
describe('focus-trap', () => {
77
let overlay, overlayPart, focusableElements;
@@ -195,4 +195,119 @@ describe('focus-trap', () => {
195195
expect(outer.hasAttribute('aria-hidden')).to.be.false;
196196
});
197197
});
198+
199+
describe('aria-hidden modal root', () => {
200+
customElements.define(
201+
'custom-overlay-wrapper',
202+
class extends HTMLElement {
203+
constructor() {
204+
super();
205+
206+
this.attachShadow({ mode: 'open' });
207+
208+
const overlay = document.createElement('custom-overlay');
209+
210+
const owner = document.createElement('div');
211+
overlay.owner = owner;
212+
213+
// Forward the slotted content from wrapper to overlay
214+
const slot = document.createElement('slot');
215+
overlay.appendChild(slot);
216+
217+
overlay.focusTrap = true;
218+
overlay.renderer = (root) => {
219+
root.innerHTML = '<input placeholder="Input">';
220+
};
221+
222+
this.shadowRoot.append(overlay);
223+
this.append(owner);
224+
}
225+
},
226+
);
227+
228+
customElements.define(
229+
'custom-overlay',
230+
class extends Overlay {
231+
get _contentRoot() {
232+
return this.owner;
233+
}
234+
235+
get _modalRoot() {
236+
return this.owner;
237+
}
238+
239+
_attachOverlay() {
240+
this.setAttribute('popover', 'manual');
241+
this.showPopover();
242+
}
243+
244+
_detachOverlay() {
245+
this.hidePopover();
246+
}
247+
},
248+
);
249+
250+
let outer, inner, wrapper, overlay;
251+
252+
beforeEach(() => {
253+
// Create outer element and pass it explicitly.
254+
outer = document.createElement('main');
255+
256+
// Our `fixtureSync()` requires a single parent.
257+
inner = fixtureSync(
258+
`
259+
<div>
260+
<aside>
261+
<button>Foo</button>
262+
</aside>
263+
<div>
264+
<button>Bar</button>
265+
<custom-overlay-wrapper></custom-overlay-wrapper>
266+
<button>Baz</button>
267+
</div>
268+
</div>
269+
`,
270+
outer,
271+
);
272+
273+
wrapper = inner.querySelector('custom-overlay-wrapper');
274+
overlay = wrapper.shadowRoot.querySelector('custom-overlay');
275+
});
276+
277+
afterEach(() => {
278+
overlay.opened = false;
279+
});
280+
281+
it('should not set aria-hidden on wrapping elements on overlay open', async () => {
282+
overlay.opened = true;
283+
await oneEvent(overlay, 'vaadin-overlay-open');
284+
285+
expect(outer.hasAttribute('aria-hidden')).to.be.false;
286+
expect(inner.hasAttribute('aria-hidden')).to.be.false;
287+
expect(wrapper.hasAttribute('aria-hidden')).to.be.false;
288+
});
289+
290+
it('should not set aria-hidden on content root element on overlay open', async () => {
291+
overlay.opened = true;
292+
await oneEvent(overlay, 'vaadin-overlay-open');
293+
294+
const root = wrapper.querySelector('div');
295+
const input = root.firstElementChild;
296+
297+
expect(root.hasAttribute('aria-hidden')).to.be.false;
298+
expect(input.hasAttribute('aria-hidden')).to.be.false;
299+
});
300+
301+
it('should set aria-hidden on sibling elements on overlay open', async () => {
302+
overlay.opened = true;
303+
await oneEvent(overlay, 'vaadin-overlay-open');
304+
305+
const buttons = wrapper.parentElement.querySelectorAll('button');
306+
expect(buttons[0].getAttribute('aria-hidden')).to.equal('true');
307+
expect(buttons[1].getAttribute('aria-hidden')).to.equal('true');
308+
309+
const aside = outer.querySelector('aside');
310+
expect(aside.getAttribute('aria-hidden')).to.equal('true');
311+
});
312+
});
198313
});

0 commit comments

Comments
 (0)