Skip to content

Commit 9a20347

Browse files
committed
(kb) allow classic keyboard nav when not viewing a document
1) Disable the clipboard catch-all element when there is no need to enable it (we conclude we don't need it when we see no copy/put/paste command is set). This means it is disabled on most pages except the document view. 2) Make sure interactive elements are highlighted when focusing them with the keyboard. Lots of elements have `outline: none` forced in the codebase to prevent showing unwanted outlines when clicking with the mouse. But it prevents keyboard users to see where they are. Have a tiny JS script checking for keyboard interactions to force showing the outline on actual kb focus.
1 parent bc95375 commit 9a20347

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

app/client/components/Clipboard.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ var dom = require('../lib/dom');
4949
var Base = require('./Base');
5050
var tableUtil = require('../lib/tableUtil');
5151

52+
var _ = require('underscore');
53+
5254
const t = makeT('Clipboard');
5355

5456
function Clipboard(app) {
@@ -71,7 +73,14 @@ function Clipboard(app) {
7173

7274
FocusLayer.create(this, {
7375
defaultFocusElem: this.copypasteField,
74-
allowFocus: allowFocus,
76+
allowFocus: (element) => {
77+
// We always allow focus if current screen doesn't have any clipboard events registered.
78+
// This basically means the focus grab is disabled if no clipboard command is active.
79+
const { copy, cut, paste } = commands.allCommands;
80+
return (copy._activeFunc === _.noop && cut._activeFunc === _.noop && paste._activeFunc === _.noop)
81+
? true
82+
: allowFocus(element);
83+
},
7584
onDefaultFocus: () => {
7685
this.copypasteField.value = ' ';
7786
this.copypasteField.select();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* KeyboardFocusHighlighter helps kb users view what interactive elements they have focus on.
3+
*
4+
* This is done as a quick way to make sure focus rings are correctly visible when using a kb,
5+
* without impacting touch/mouse users, and without having to change the whole codebase.
6+
*
7+
* Note: this doesn't trigger when Tab is catched by the `nextField` command.
8+
*/
9+
import { Disposable, dom, styled } from "grainjs";
10+
11+
export class KeyboardFocusHighlighter extends Disposable {
12+
constructor() {
13+
super();
14+
this.autoDispose(dom.onElem(window, 'keydown', this._onKeyDown));
15+
this.autoDispose(dom.onElem(window, 'touchstart', this._clear));
16+
this.autoDispose(dom.onElem(window, 'mousedown', this._clear));
17+
}
18+
19+
public isKeyboardUser = () => {
20+
return document.documentElement.classList.contains(cssKeyboardUser.className);
21+
};
22+
23+
private _onKeyDown = (event: KeyboardEvent) => {
24+
if (event.key === 'Tab') {
25+
document.documentElement.classList.add(cssKeyboardUser.className);
26+
}
27+
};
28+
29+
private _clear = () => {
30+
document.documentElement.classList.remove(cssKeyboardUser.className);
31+
};
32+
}
33+
34+
const cssKeyboardUser = styled('div', `
35+
& :is(a, input, textarea, select, button, [tabindex="0"]):focus-visible {
36+
/* simulate default browser focus ring */
37+
outline: 1px auto Highlight !important;
38+
outline: 1px auto -webkit-focus-ring-color !important;
39+
}
40+
`);

app/client/ui/App.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {ISupportedFeatures} from 'app/common/UserConfig';
2323
import {dom} from 'grainjs';
2424
import * as ko from 'knockout';
2525
import {makeT} from 'app/client/lib/localization';
26+
import {KeyboardFocusHighlighter} from 'app/client/components/KeyboardFocusHighlighter';
2627

2728
const t = makeT('App');
2829

@@ -65,6 +66,8 @@ export class App extends DisposableWithEvents {
6566
this._settings = ko.observable({});
6667
this.features = ko.computed(() => this._settings().features || {});
6768

69+
this.autoDispose(new KeyboardFocusHighlighter());
70+
6871
if (isDesktop()) {
6972
this.autoDispose(Clipboard.create(this));
7073
} else {

0 commit comments

Comments
 (0)