mirror of
https://github.com/material-components/material-web.git
synced 2024-10-27 22:17:25 +03:00
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
This commit is contained in:
parent
ef0f2f7a96
commit
210cc7b59f
97
components/focus/strong-focus.ts
Normal file
97
components/focus/strong-focus.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface StrongFocus {
|
||||
visible: boolean;
|
||||
setVisible(visible: boolean): void;
|
||||
}
|
||||
|
||||
class FocusGlobal implements StrongFocus {
|
||||
visible = false;
|
||||
setVisible(visible: boolean) {
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This object can be overwritten by the `setup()` function to use a different
|
||||
* focus coordination object.
|
||||
*/
|
||||
let focusObject: StrongFocus = new FocusGlobal();
|
||||
|
||||
/**
|
||||
* Set of keyboard event codes that correspond to keyboard navigation
|
||||
*/
|
||||
const KEYBOARD_NAVIGATION_CODES =
|
||||
new Set(['Tab', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
|
||||
|
||||
const KEYDOWN_HANDLER = (e: KeyboardEvent) => {
|
||||
if (KEYBOARD_NAVIGATION_CODES.has(e.code)) {
|
||||
focusObject.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up integration with alternate global focus tracking object
|
||||
*
|
||||
* @param focusGlobal A global focus object to coordinate between multiple
|
||||
* systems
|
||||
* @param enableKeydownHandler Set to true to let StrongFocusService listen for
|
||||
* keyboard navigation
|
||||
*/
|
||||
export function setup(focusGlobal: StrongFocus, enableKeydownHandler = false) {
|
||||
focusObject = focusGlobal;
|
||||
if (enableKeydownHandler) {
|
||||
window.addEventListener('keydown', KEYDOWN_HANDLER);
|
||||
} else {
|
||||
window.removeEventListener('keydown', KEYDOWN_HANDLER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting for always showing strong focus
|
||||
*
|
||||
* Defaults to false, controlled by `setForceStrongFocus`
|
||||
*/
|
||||
let alwaysStrong = false;
|
||||
|
||||
/**
|
||||
* Returns `true` if the component should show strong focus.
|
||||
*
|
||||
* By default, strong focus is shown only on keyboard navigation, and not on
|
||||
* pointer interaction.
|
||||
*/
|
||||
export function shouldShowStrongFocus() {
|
||||
return alwaysStrong || focusObject.visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control if strong focus should always be shown on component focus
|
||||
*
|
||||
* Defaults to `false`
|
||||
*/
|
||||
export function setForceStrongFocus(force: boolean) {
|
||||
alwaysStrong = force;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, strong focus is always shown
|
||||
*/
|
||||
export function isStrongFocusForced() {
|
||||
return alwaysStrong;
|
||||
}
|
||||
|
||||
/**
|
||||
* Components should call this when a user interacts with a component with a
|
||||
* pointing device.
|
||||
*
|
||||
* By default, this will prevent the strong focus from being shown.
|
||||
*/
|
||||
export function pointerPress() {
|
||||
focusObject.setVisible(false);
|
||||
}
|
||||
|
||||
setup(focusObject, true);
|
127
components/focus/test/strong-focus.test.ts
Normal file
127
components/focus/test/strong-focus.test.ts
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import 'jasmine';
|
||||
|
||||
import * as strongFocus from '../strong-focus';
|
||||
|
||||
class MockFocus {
|
||||
constructor(public visible = false) {}
|
||||
setVisible(visible: boolean) {
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
||||
|
||||
function simulateKeydown(code: string) {
|
||||
const ev = new KeyboardEvent('keydown', {code, bubbles: true});
|
||||
window.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
describe('Strong Focus', () => {
|
||||
describe('standalone operation', () => {
|
||||
beforeEach(() => {
|
||||
strongFocus.setup(new MockFocus(), true);
|
||||
});
|
||||
|
||||
it('does not show strong focus by default', () => {
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeFalse();
|
||||
});
|
||||
|
||||
it('does not force strong focus by default', () => {
|
||||
expect(strongFocus.isStrongFocusForced()).toBeFalse();
|
||||
});
|
||||
|
||||
describe('keyboard navigation', () => {
|
||||
it('shows strong focus on Tab', () => {
|
||||
simulateKeydown('Tab');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
it('shows strong focus on ArrowLeft', () => {
|
||||
simulateKeydown('ArrowLeft');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
it('shows strong focus on ArrowLeft', () => {
|
||||
simulateKeydown('ArrowLeft');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
it('shows strong focus on ArrowRight', () => {
|
||||
simulateKeydown('ArrowRight');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
it('shows strong focus on ArrowUp', () => {
|
||||
simulateKeydown('ArrowUp');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
it('shows strong focus on ArrowDown', () => {
|
||||
simulateKeydown('ArrowDown');
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pointer interaction', () => {
|
||||
it('does not show strong focus', () => {
|
||||
simulateKeydown('Tab');
|
||||
strongFocus.pointerPress();
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('force strong focus', () => {
|
||||
beforeAll(() => {
|
||||
strongFocus.setForceStrongFocus(true);
|
||||
});
|
||||
afterAll(() => {
|
||||
strongFocus.setForceStrongFocus(false);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
strongFocus.setup(new MockFocus(), true);
|
||||
});
|
||||
|
||||
it('shows strong focus when forced', () => {
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
|
||||
it('reports that strong focus is forced', () => {
|
||||
expect(strongFocus.isStrongFocusForced()).toBeTrue();
|
||||
});
|
||||
|
||||
it('shows strong focus after pointer interaction', () => {
|
||||
strongFocus.pointerPress();
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared focus state', () => {
|
||||
let focus!: MockFocus;
|
||||
|
||||
beforeEach(() => {
|
||||
focus = new MockFocus();
|
||||
strongFocus.setup(focus);
|
||||
});
|
||||
|
||||
it('reads from shared state', () => {
|
||||
focus.visible = true;
|
||||
expect(strongFocus.shouldShowStrongFocus()).toBeTrue();
|
||||
});
|
||||
|
||||
it('writes to shared state', () => {
|
||||
focus.visible = true;
|
||||
strongFocus.pointerPress();
|
||||
expect(focus.visible).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup function', () => {
|
||||
it('removes keydown listener when not wanted', () => {
|
||||
const focus = new MockFocus();
|
||||
strongFocus.setup(focus);
|
||||
simulateKeydown('Tab');
|
||||
expect(focus.visible).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user