Skip to content

Commit bd6c20b

Browse files
kevjinamiller-gh
authored andcommitted
feat: Show outline on keyboard nav and style outline to match theme (nodejs#240)
1 parent de6f931 commit bd6c20b

4 files changed

Lines changed: 111 additions & 1 deletion

File tree

src/components/layout.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ main {
128128
grid-template-columns: 420px auto;
129129
}
130130

131-
summary {
131+
:focus {
132+
outline: var(--green3) dotted 2px;
133+
}
134+
135+
[data-is-focused="true"] {
132136
outline: none;
133137
}
134138

src/components/layout.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import SEO from './seo';
88
import { isMobileScreen } from '../util/isScreenWithinWidth';
99
import { notifyWhenStickyHeadersChange } from '../util/notifyWhenStickyHeadersChange';
1010
import { StickyChange, SentinelObserverSetupOptions } from '../types';
11+
import {
12+
addFocusOutlineListeners,
13+
removeFocusOutlineListeners,
14+
} from '../util/outlineOnKeyboardNav';
1115

1216
type Props = {
1317
children: React.ReactNode;
@@ -20,6 +24,9 @@ const Layout = ({ children, title, description, img }: Props) => {
2024
const prevOffset = useRef<number>(-1);
2125

2226
useEffect(() => {
27+
if (window.document && 'documentElement' in window.document) {
28+
addFocusOutlineListeners();
29+
}
2330
if ('IntersectionObserver' in window) {
2431
setupObserver();
2532
} else {
@@ -29,6 +36,8 @@ const Layout = ({ children, title, description, img }: Props) => {
2936
// Fallback for browsers without IntersectionObserver support
3037
.catch(magicHeroNumber);
3138
}
39+
40+
return cleanUp;
3241
});
3342

3443
const setupObserver = (): void => {
@@ -80,6 +89,10 @@ const Layout = ({ children, title, description, img }: Props) => {
8089
window.requestAnimationFrame(magicHeroNumber);
8190
};
8291

92+
const cleanUp = (): void => {
93+
return removeFocusOutlineListeners();
94+
};
95+
8396
return (
8497
<>
8598
<SEO title={title} description={description} img={img} />

src/util/outlineOnKeyboardNav.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const TAB_KEYCODE = 9;
2+
const FOCUS_ATTR = 'data-is-focused';
3+
const BLUR_EVENT = 'blur';
4+
const FOCUS_EVENT = 'focus';
5+
const KEYDOWN_EVENT = 'keydown';
6+
const MOUSEDOWN_EVENT = 'mousedown';
7+
8+
let IS_MOUSE_EVENT = false;
9+
10+
/**
11+
* Attaches listeners for keydown, mousedown, focus, and blur to the document,
12+
* which handle adding or removing focus outline css class for mouse events.
13+
*/
14+
export function addFocusOutlineListeners() {
15+
const docEl = window.document.documentElement;
16+
IS_MOUSE_EVENT = false;
17+
18+
docEl.addEventListener(KEYDOWN_EVENT, handleKeyDown, false);
19+
docEl.addEventListener(MOUSEDOWN_EVENT, handleMouseDown, false);
20+
docEl.addEventListener(FOCUS_EVENT, handleFocus, true);
21+
docEl.addEventListener(BLUR_EVENT, handleBlur, true);
22+
}
23+
24+
/**
25+
* Detaches listeners
26+
*/
27+
export function removeFocusOutlineListeners() {
28+
const docEl = window.document.documentElement;
29+
30+
if (docEl) {
31+
docEl.removeEventListener(KEYDOWN_EVENT, handleKeyDown, false);
32+
docEl.removeEventListener(MOUSEDOWN_EVENT, handleMouseDown, false);
33+
docEl.removeEventListener(FOCUS_EVENT, handleFocus, true);
34+
docEl.removeEventListener(BLUR_EVENT, handleBlur, true);
35+
}
36+
}
37+
38+
export function handleKeyDown(event: KeyboardEvent) {
39+
if (event.keyCode === TAB_KEYCODE) {
40+
IS_MOUSE_EVENT = false;
41+
}
42+
}
43+
44+
export function handleMouseDown(event: MouseEvent) {
45+
IS_MOUSE_EVENT = true;
46+
}
47+
48+
export function handleFocus(event: Event) {
49+
if (IS_MOUSE_EVENT && event.target && event.target !== event.currentTarget) {
50+
(event.target as Element).setAttribute(FOCUS_ATTR, 'true');
51+
}
52+
}
53+
54+
export function handleBlur(event: Event) {
55+
if (event.target !== event.currentTarget) {
56+
(event.target as Element).removeAttribute(FOCUS_ATTR);
57+
}
58+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
handleMouseDown,
3+
handleFocus,
4+
handleKeyDown,
5+
handleBlur,
6+
} from '../../src/util/outlineOnKeyboardNav';
7+
describe('Tests for focus and blur handlers', () => {
8+
const FOCUS_ATTR = 'data-is-focused';
9+
const TAB_KEYCODE = 9;
10+
let event = {};
11+
beforeEach(() => {
12+
event = {
13+
target: document.createElement('div'),
14+
currentTarget: document.createElement('div'),
15+
keyCode: TAB_KEYCODE,
16+
};
17+
});
18+
it('hides outline for mouse focus', () => {
19+
handleMouseDown(event);
20+
handleFocus(event);
21+
expect(event.target.getAttribute(FOCUS_ATTR)).toEqual('true');
22+
});
23+
it('doesnt hide outline for keyboard focus', () => {
24+
handleKeyDown(event);
25+
handleFocus(event);
26+
expect(event.target.getAttribute(FOCUS_ATTR)).toBeNull();
27+
});
28+
it('Blurs after leaving focus', () => {
29+
handleMouseDown(event);
30+
handleFocus(event);
31+
expect(event.target.getAttribute(FOCUS_ATTR)).toEqual('true');
32+
handleBlur(event);
33+
expect(event.target.getAttribute(FOCUS_ATTR)).toBeNull();
34+
});
35+
});

0 commit comments

Comments
 (0)