Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ main {
grid-template-columns: 420px auto;
}

summary {
:focus {
outline: var(--green3) dotted 2px;
}

[data-is-focused="true"] {
outline: none;
}

Expand Down
13 changes: 13 additions & 0 deletions src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import SEO from './seo';
import { isMobileScreen } from '../util/isScreenWithinWidth';
import { notifyWhenStickyHeadersChange } from '../util/notifyWhenStickyHeadersChange';
import { StickyChange, SentinelObserverSetupOptions } from '../types';
import {
addFocusOutlineListeners,
removeFocusOutlineListeners,
} from '../util/outlineOnKeyboardNav';

type Props = {
children: React.ReactNode;
Expand All @@ -20,12 +24,17 @@ const Layout = ({ children, title, description, img }: Props) => {
const prevOffset = useRef<number>(-1);

useEffect(() => {
if (window.document && 'documentElement' in window.document) {
addFocusOutlineListeners();
}
if ('IntersectionObserver' in window) {
setupObserver();
} else {
// Fallback for browsers without IntersectionObserver support
magicHeroNumber();
}

return cleanUp;
});

const setupObserver = (): void => {
Expand Down Expand Up @@ -77,6 +86,10 @@ const Layout = ({ children, title, description, img }: Props) => {
window.requestAnimationFrame(magicHeroNumber);
};

const cleanUp = (): void => {
return removeFocusOutlineListeners();
};

return (
<>
<SEO title={title} description={description} img={img} />
Expand Down
58 changes: 58 additions & 0 deletions src/util/outlineOnKeyboardNav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const TAB_KEYCODE = 9;
const FOCUS_ATTR = 'data-is-focused';
const BLUR_EVENT = 'blur';
const FOCUS_EVENT = 'focus';
const KEYDOWN_EVENT = 'keydown';
const MOUSEDOWN_EVENT = 'mousedown';

let IS_MOUSE_EVENT = false;

/**
* Attaches listeners for keydown, mousedown, focus, and blur to the document,
* which handle adding or removing focus outline css class for mouse events.
*/
export function addFocusOutlineListeners() {
const docEl = window.document.documentElement;
IS_MOUSE_EVENT = false;

docEl.addEventListener(KEYDOWN_EVENT, handleKeyDown, false);
docEl.addEventListener(MOUSEDOWN_EVENT, handleMouseDown, false);
docEl.addEventListener(FOCUS_EVENT, handleFocus, true);
docEl.addEventListener(BLUR_EVENT, handleBlur, true);
}

/**
* Detaches listeners
*/
export function removeFocusOutlineListeners() {
const docEl = window.document.documentElement;

if (docEl) {
docEl.removeEventListener(KEYDOWN_EVENT, handleKeyDown, false);
docEl.removeEventListener(MOUSEDOWN_EVENT, handleMouseDown, false);
docEl.removeEventListener(FOCUS_EVENT, handleFocus, true);
docEl.removeEventListener(BLUR_EVENT, handleBlur, true);
}
}

export function handleKeyDown(event: KeyboardEvent) {
if (event.keyCode === TAB_KEYCODE) {
IS_MOUSE_EVENT = false;
}
}

export function handleMouseDown(event: MouseEvent) {
IS_MOUSE_EVENT = true;
}

export function handleFocus(event: Event) {
if (IS_MOUSE_EVENT && event.target && event.target !== event.currentTarget) {
(event.target as Element).setAttribute(FOCUS_ATTR, 'true');
}
}

export function handleBlur(event: Event) {
if (event.target !== event.currentTarget) {
(event.target as Element).removeAttribute(FOCUS_ATTR);
}
}
35 changes: 35 additions & 0 deletions test/util/outlineOnKeyboardNav.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
handleMouseDown,
handleFocus,
handleKeyDown,
handleBlur,
} from '../../src/util/outlineOnKeyboardNav';
describe('Tests for focus and blur handlers', () => {
const FOCUS_ATTR = 'data-is-focused';
const TAB_KEYCODE = 9;
let event = {};
beforeEach(() => {
event = {
target: document.createElement('div'),
currentTarget: document.createElement('div'),
keyCode: TAB_KEYCODE,
};
});
it('hides outline for mouse focus', () => {
handleMouseDown(event);
handleFocus(event);
expect(event.target.getAttribute(FOCUS_ATTR)).toEqual('true');
});
it('doesnt hide outline for keyboard focus', () => {
handleKeyDown(event);
handleFocus(event);
expect(event.target.getAttribute(FOCUS_ATTR)).toBeNull();
});
it('Blurs after leaving focus', () => {
handleMouseDown(event);
handleFocus(event);
expect(event.target.getAttribute(FOCUS_ATTR)).toEqual('true');
handleBlur(event);
expect(event.target.getAttribute(FOCUS_ATTR)).toBeNull();
});
});