Skip to content

Commit 210cc7b

Browse files
dfreedmcopybara-github
authored andcommitted
chore(focus): Add Strong Focus manager
StrongFocus allows components to show an accessible "strong" focus visual when focused via keyboard, and to not show that when focused via a pointing device. PiperOrigin-RevId: 403230844
1 parent ef0f2f7 commit 210cc7b

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

components/focus/strong-focus.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
interface StrongFocus {
8+
visible: boolean;
9+
setVisible(visible: boolean): void;
10+
}
11+
12+
class FocusGlobal implements StrongFocus {
13+
visible = false;
14+
setVisible(visible: boolean) {
15+
this.visible = visible;
16+
}
17+
}
18+
19+
/**
20+
* This object can be overwritten by the `setup()` function to use a different
21+
* focus coordination object.
22+
*/
23+
let focusObject: StrongFocus = new FocusGlobal();
24+
25+
/**
26+
* Set of keyboard event codes that correspond to keyboard navigation
27+
*/
28+
const KEYBOARD_NAVIGATION_CODES =
29+
new Set(['Tab', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
30+
31+
const KEYDOWN_HANDLER = (e: KeyboardEvent) => {
32+
if (KEYBOARD_NAVIGATION_CODES.has(e.code)) {
33+
focusObject.setVisible(true);
34+
}
35+
};
36+
37+
/**
38+
* Set up integration with alternate global focus tracking object
39+
*
40+
* @param focusGlobal A global focus object to coordinate between multiple
41+
* systems
42+
* @param enableKeydownHandler Set to true to let StrongFocusService listen for
43+
* keyboard navigation
44+
*/
45+
export function setup(focusGlobal: StrongFocus, enableKeydownHandler = false) {
46+
focusObject = focusGlobal;
47+
if (enableKeydownHandler) {
48+
window.addEventListener('keydown', KEYDOWN_HANDLER);
49+
} else {
50+
window.removeEventListener('keydown', KEYDOWN_HANDLER);
51+
}
52+
}
53+
54+
/**
55+
* Setting for always showing strong focus
56+
*
57+
* Defaults to false, controlled by `setForceStrongFocus`
58+
*/
59+
let alwaysStrong = false;
60+
61+
/**
62+
* Returns `true` if the component should show strong focus.
63+
*
64+
* By default, strong focus is shown only on keyboard navigation, and not on
65+
* pointer interaction.
66+
*/
67+
export function shouldShowStrongFocus() {
68+
return alwaysStrong || focusObject.visible;
69+
}
70+
71+
/**
72+
* Control if strong focus should always be shown on component focus
73+
*
74+
* Defaults to `false`
75+
*/
76+
export function setForceStrongFocus(force: boolean) {
77+
alwaysStrong = force;
78+
}
79+
80+
/**
81+
* If `true`, strong focus is always shown
82+
*/
83+
export function isStrongFocusForced() {
84+
return alwaysStrong;
85+
}
86+
87+
/**
88+
* Components should call this when a user interacts with a component with a
89+
* pointing device.
90+
*
91+
* By default, this will prevent the strong focus from being shown.
92+
*/
93+
export function pointerPress() {
94+
focusObject.setVisible(false);
95+
}
96+
97+
setup(focusObject, true);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import 'jasmine';
8+
9+
import * as strongFocus from '../strong-focus';
10+
11+
class MockFocus {
12+
constructor(public visible = false) {}
13+
setVisible(visible: boolean) {
14+
this.visible = visible;
15+
}
16+
}
17+
18+
function simulateKeydown(code: string) {
19+
const ev = new KeyboardEvent('keydown', {code, bubbles: true});
20+
window.dispatchEvent(ev);
21+
}
22+
23+
describe('Strong Focus', () => {
24+
describe('standalone operation', () => {
25+
beforeEach(() => {
26+
strongFocus.setup(new MockFocus(), true);
27+
});
28+
29+
it('does not show strong focus by default', () => {
30+
expect(strongFocus.shouldShowStrongFocus()).toBeFalse();
31+
});
32+
33+
it('does not force strong focus by default', () => {
34+
expect(strongFocus.isStrongFocusForced()).toBeFalse();
35+
});
36+
37+
describe('keyboard navigation', () => {
38+
it('shows strong focus on Tab', () => {
39+
simulateKeydown('Tab');
40+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
41+
});
42+
it('shows strong focus on ArrowLeft', () => {
43+
simulateKeydown('ArrowLeft');
44+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
45+
});
46+
it('shows strong focus on ArrowLeft', () => {
47+
simulateKeydown('ArrowLeft');
48+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
49+
});
50+
it('shows strong focus on ArrowRight', () => {
51+
simulateKeydown('ArrowRight');
52+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
53+
});
54+
it('shows strong focus on ArrowUp', () => {
55+
simulateKeydown('ArrowUp');
56+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
57+
});
58+
it('shows strong focus on ArrowDown', () => {
59+
simulateKeydown('ArrowDown');
60+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
61+
});
62+
});
63+
64+
describe('pointer interaction', () => {
65+
it('does not show strong focus', () => {
66+
simulateKeydown('Tab');
67+
strongFocus.pointerPress();
68+
expect(strongFocus.shouldShowStrongFocus()).toBeFalse();
69+
});
70+
});
71+
});
72+
73+
describe('force strong focus', () => {
74+
beforeAll(() => {
75+
strongFocus.setForceStrongFocus(true);
76+
});
77+
afterAll(() => {
78+
strongFocus.setForceStrongFocus(false);
79+
});
80+
81+
beforeEach(() => {
82+
strongFocus.setup(new MockFocus(), true);
83+
});
84+
85+
it('shows strong focus when forced', () => {
86+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
87+
});
88+
89+
it('reports that strong focus is forced', () => {
90+
expect(strongFocus.isStrongFocusForced()).toBeTrue();
91+
});
92+
93+
it('shows strong focus after pointer interaction', () => {
94+
strongFocus.pointerPress();
95+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
96+
});
97+
});
98+
99+
describe('shared focus state', () => {
100+
let focus!: MockFocus;
101+
102+
beforeEach(() => {
103+
focus = new MockFocus();
104+
strongFocus.setup(focus);
105+
});
106+
107+
it('reads from shared state', () => {
108+
focus.visible = true;
109+
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
110+
});
111+
112+
it('writes to shared state', () => {
113+
focus.visible = true;
114+
strongFocus.pointerPress();
115+
expect(focus.visible).toBeFalse();
116+
});
117+
});
118+
119+
describe('setup function', () => {
120+
it('removes keydown listener when not wanted', () => {
121+
const focus = new MockFocus();
122+
strongFocus.setup(focus);
123+
simulateKeydown('Tab');
124+
expect(focus.visible).toBeFalse();
125+
});
126+
});
127+
});

0 commit comments

Comments
 (0)