Skip to content

Commit 3e7d144

Browse files
fix: respect focusVisible option in FocusRestorationController (#10158) (#10163)
Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com>
1 parent cfe9424 commit 3e7d144

File tree

4 files changed

+113
-41
lines changed

4 files changed

+113
-41
lines changed

packages/a11y-base/src/focus-restoration-controller.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ export class FocusRestorationController {
2929
return;
3030
}
3131

32-
const preventScroll = options ? options.preventScroll : false;
32+
const focusOptions = {
33+
preventScroll: options ? options.preventScroll : false,
34+
focusVisible: options ? options.focusVisible : false,
35+
};
3336

3437
if (getDeepActiveElement() === document.body) {
3538
// In Firefox and Safari, focusing the node synchronously
3639
// doesn't work as expected when the overlay is closing on outside click.
3740
// These browsers force focus to move to the body element and retain it
3841
// there until the next event loop iteration.
39-
setTimeout(() => focusNode.focus({ preventScroll }));
42+
setTimeout(() => focusNode.focus(focusOptions));
4043
} else {
41-
focusNode.focus({ preventScroll });
44+
focusNode.focus(focusOptions);
4245
}
4346

4447
this.focusNode = null;

packages/a11y-base/test/focus-restoration-controller.test.js

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,43 +60,87 @@ describe('focus-restoration-controller', () => {
6060
expect(getDeepActiveElement()).to.equal(button2);
6161
});
6262

63-
it('should not prevent scroll when restoring focus synchronously by default', () => {
64-
button1.focus();
65-
const spy = sinon.spy(button2, 'focus');
66-
controller.saveFocus(button2);
67-
controller.restoreFocus();
68-
expect(spy).to.be.calledOnce;
69-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: false });
70-
});
63+
describe('preventScroll', () => {
64+
it('should not prevent scroll when restoring focus synchronously by default', () => {
65+
button1.focus();
66+
const spy = sinon.spy(button2, 'focus');
67+
controller.saveFocus(button2);
68+
controller.restoreFocus();
69+
expect(spy).to.be.calledOnce;
70+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: false });
71+
});
7172

72-
it('should prevent scroll when restoring focus synchronously with preventScroll', () => {
73-
button1.focus();
74-
const spy = sinon.spy(button2, 'focus');
75-
controller.saveFocus(button2);
76-
controller.restoreFocus({ preventScroll: true });
77-
expect(spy).to.be.calledOnce;
78-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: true });
79-
});
73+
it('should prevent scroll when restoring focus synchronously with preventScroll', () => {
74+
button1.focus();
75+
const spy = sinon.spy(button2, 'focus');
76+
controller.saveFocus(button2);
77+
controller.restoreFocus({ preventScroll: true });
78+
expect(spy).to.be.calledOnce;
79+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: true });
80+
});
8081

81-
it('should not prevent scroll when restoring focus asynchronously by default', async () => {
82-
button1.focus();
83-
const spy = sinon.spy(button2, 'focus');
84-
controller.saveFocus(button2);
85-
outsideClick();
86-
controller.restoreFocus();
87-
await aTimeout(0);
88-
expect(spy).to.be.calledOnce;
89-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: false });
82+
it('should not prevent scroll when restoring focus asynchronously by default', async () => {
83+
button1.focus();
84+
const spy = sinon.spy(button2, 'focus');
85+
controller.saveFocus(button2);
86+
outsideClick();
87+
controller.restoreFocus();
88+
await aTimeout(0);
89+
expect(spy).to.be.calledOnce;
90+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: false });
91+
});
92+
93+
it('should prevent scroll when restoring focus asynchronously with preventScroll', async () => {
94+
button1.focus();
95+
const spy = sinon.spy(button2, 'focus');
96+
controller.saveFocus(button2);
97+
outsideClick();
98+
controller.restoreFocus({ preventScroll: true });
99+
await aTimeout(0);
100+
expect(spy).to.be.calledOnce;
101+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: true });
102+
});
90103
});
91104

92-
it('should prevent scroll when restoring focus asynchronously with preventScroll', async () => {
93-
button1.focus();
94-
const spy = sinon.spy(button2, 'focus');
95-
controller.saveFocus(button2);
96-
outsideClick();
97-
controller.restoreFocus({ preventScroll: true });
98-
await aTimeout(0);
99-
expect(spy).to.be.calledOnce;
100-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: true });
105+
describe('focusVisible', () => {
106+
it('should not set focusVisible when restoring focus synchronously by default', () => {
107+
button1.focus();
108+
const spy = sinon.spy(button2, 'focus');
109+
controller.saveFocus(button2);
110+
controller.restoreFocus();
111+
expect(spy).to.be.calledOnce;
112+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: false });
113+
});
114+
115+
it('should set focusVisible when restoring focus synchronously with focusVisible', () => {
116+
button1.focus();
117+
const spy = sinon.spy(button2, 'focus');
118+
controller.saveFocus(button2);
119+
controller.restoreFocus({ focusVisible: true });
120+
expect(spy).to.be.calledOnce;
121+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: true });
122+
});
123+
124+
it('should not set focusVisible when restoring focus asynchronously by default', async () => {
125+
button1.focus();
126+
const spy = sinon.spy(button2, 'focus');
127+
controller.saveFocus(button2);
128+
outsideClick();
129+
controller.restoreFocus();
130+
await aTimeout(0);
131+
expect(spy).to.be.calledOnce;
132+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: false });
133+
});
134+
135+
it('should set focusVisible when restoring focus asynchronously with focusVisible', async () => {
136+
button1.focus();
137+
const spy = sinon.spy(button2, 'focus');
138+
controller.saveFocus(button2);
139+
outsideClick();
140+
controller.restoreFocus({ focusVisible: true });
141+
await aTimeout(0);
142+
expect(spy).to.be.calledOnce;
143+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: true });
144+
});
101145
});
102146
});

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ export const OverlayFocusMixin = (superClass) =>
7676
}
7777

7878
if (this.restoreFocusOnClose && this._shouldRestoreFocus()) {
79-
const preventScroll = !isKeyboardActive();
80-
this.__focusRestorationController.restoreFocus({ preventScroll });
79+
const focusVisible = isKeyboardActive();
80+
const preventScroll = !focusVisible;
81+
this.__focusRestorationController.restoreFocus({ preventScroll, focusVisible });
8182
}
8283
}
8384

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ describe('restore focus', () => {
143143
mousedown(document.body);
144144
overlay.opened = false;
145145
expect(spy).to.be.calledOnce;
146-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: true });
146+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: true });
147147
});
148148

149149
it('should not prevent scroll when restoring focus on close after keydown', async () => {
@@ -154,7 +154,31 @@ describe('restore focus', () => {
154154
escKeyDown(document.body);
155155
overlay.opened = false;
156156
expect(spy).to.be.calledOnce;
157-
expect(spy.firstCall.args[0]).to.eql({ preventScroll: false });
157+
expect(spy.firstCall.args[0]).to.deep.include({ preventScroll: false });
158+
});
159+
});
160+
161+
describe('focusVisible', () => {
162+
it('should set focusVisible: false when restoring focus on close after mousedown', async () => {
163+
focusable.focus();
164+
overlay.opened = true;
165+
await oneEvent(overlay, 'vaadin-overlay-open');
166+
const spy = sinon.spy(focusable, 'focus');
167+
mousedown(document.body);
168+
overlay.opened = false;
169+
expect(spy).to.be.calledOnce;
170+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: false });
171+
});
172+
173+
it('should set focusVisible: true when restoring focus on close after keydown', async () => {
174+
focusable.focus();
175+
overlay.opened = true;
176+
await oneEvent(overlay, 'vaadin-overlay-open');
177+
const spy = sinon.spy(focusable, 'focus');
178+
escKeyDown(document.body);
179+
overlay.opened = false;
180+
expect(spy).to.be.calledOnce;
181+
expect(spy.firstCall.args[0]).to.deep.include({ focusVisible: true });
158182
});
159183
});
160184
});

0 commit comments

Comments
 (0)