Skip to content

Commit

Permalink
accessible-focus: Extract into package, re-write in TypeScript, test,…
Browse files Browse the repository at this point in the history
… and handle deprecated APIs (Automattic#49351)

* accessible-focus: Extract from lib

* accessible-focus: Use extracted package

* Clean up package
  • Loading branch information
sarayourfriend authored Feb 11, 2021
1 parent 2d340e0 commit 56f2a25
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 26 deletions.
2 changes: 1 addition & 1 deletion client/boot/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React from 'react';
import ReactDom from 'react-dom';
import Modal from 'react-modal';
import store from 'store';
import accessibleFocus from '@automattic/accessible-focus';

/**
* Internal dependencies
Expand All @@ -18,7 +19,6 @@ import { ProviderWrappedLayout } from 'calypso/controller';
import { getToken } from 'calypso/lib/oauth-token';
import emailVerification from 'calypso/components/email-verification';
import { getSavedVariations } from 'calypso/lib/abtest'; // used by error logger
import accessibleFocus from 'calypso/lib/accessible-focus';
import Logger from 'calypso/lib/catch-js-errors';
import { hasTouch } from 'calypso/lib/touch-detect';
import { installPerfmonPageHandlers } from 'calypso/lib/perfmon';
Expand Down
2 changes: 1 addition & 1 deletion client/landing/gutenboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import config from '@automattic/calypso-config';
import { subscribe, select, dispatch } from '@wordpress/data';
import { initializeAnalytics } from '@automattic/calypso-analytics';
import type { Site as SiteStore } from '@automattic/data-stores';
import accessibleFocus from '@automattic/accessible-focus';
import { xorWith, isEqual, isEmpty, shuffle } from 'lodash';

/**
Expand All @@ -17,7 +18,6 @@ import { xorWith, isEqual, isEmpty, shuffle } from 'lodash';
import Gutenboard from './gutenboard';
import { LocaleContext } from './components/locale-context';
import { setupWpDataDebug } from './devtools';
import accessibleFocus from 'calypso/lib/accessible-focus';
import availableDesigns from './available-designs';
import { Step, path } from './path';
import { SITE_STORE } from './stores/site';
Expand Down
23 changes: 0 additions & 23 deletions client/lib/accessible-focus/index.js

This file was deleted.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"main": "server/index.js",
"dependencies": {
"@automattic/accessible-focus": "^1.0.0-alpha.0",
"@automattic/browser-data-collector": "^0.0.1",
"@automattic/calypso-analytics": "^1.0.0-alpha.1",
"@automattic/calypso-build": "^7.0.0",
Expand Down
2 changes: 1 addition & 1 deletion docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ Find tools that will help you bring accessibility into your workflow.

As we work to make Calypso more accessible, we'll probably add more things here.

- [accessible-focus](https://github.com/Automattic/wp-calypso/tree/HEAD/client/lib/accessible-focus/README.md): A small module which is run at client startup and adds an `accessible-focus` class to the document's html element when keyboard navigation is detected, so that obvious focus styles can be added without being distracting for non-keyboard users.
- [accessible-focus](https://github.com/Automattic/wp-calypso/tree/HEAD/packages/accessible-focus/README.md): A small module which is run at client startup and adds an `accessible-focus` class to the document's html element when keyboard navigation is detected, so that obvious focus styles can be added without being distracting for non-keyboard users.
- [Focusable](https://github.com/Automattic/wp-calypso/tree/HEAD/client/components/focusable/README.md): A component that lets you wrap complex content in an accessible, clickable wrapper. It adds the "button" ARIA role, for screen reader support, and enables keyboard support for keyboard-only accessibility.
- [ScreenReaderText](https://github.com/Automattic/wp-calypso/tree/HEAD/client/components/screen-reader-text): A component that adds text which is invisible on normal displays, but "visible" to screen readers.
3 changes: 3 additions & 0 deletions packages/accessible-focus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0-alpha.0

Extracted from `accessible-focus` and transformed to TypeScript and tests added.
File renamed without changes.
3 changes: 3 additions & 0 deletions packages/accessible-focus/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
preset: '../../test/packages/jest-preset.js',
};
34 changes: 34 additions & 0 deletions packages/accessible-focus/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@automattic/accessible-focus",
"version": "1.0.0-alpha.0",
"description": "A package for detecting keyboard navigation.",
"homepage": "https://github.com/Automattic/wp-calypso",
"license": "GPL-2.0-or-later",
"author": "Automattic Inc.",
"sideEffects": false,
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"calypso:src": "src/index.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/Automattic/wp-calypso.git",
"directory": "packages/accessible-focus"
},
"files": [
"dist",
"src"
],
"types": "dist/types",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/Automattic/wp-calypso/issues"
},
"scripts": {
"clean": "tsc --build ./tsconfig.json ./tsconfig-cjs.json --clean && npx rimraf dist",
"build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json",
"prepack": "yarn run clean && yarn run build",
"watch": "tsc --build ./tsconfig.json --watch"
}
}
86 changes: 86 additions & 0 deletions packages/accessible-focus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#non-printable_keys_function_keys
const keyboardNavigationKeycodes = [ 9, 32, 37, 38, 39, 40 ]; // keyCodes for tab, space, left, up, right, down respectively

// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
const keyboardNavigationKeyValues = [
'Tab',
' ',
// Some browsers returned `'Spacebar'` rather than `' '` for the Space key
'Spacebar',
'ArrowDown',
'ArrowUp',
'ArrowLeft',
'ArrowRight',
];

declare global {
interface KeyboardEvent {
/**
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyIdentifier
* Provides Safari support
*
* @deprecated
*/
keyIdentifier: string;
}
}

/**
* `event.keyCode` is [deprecated](https://stackoverflow.com/a/35395154), so we must check each of the following:
* 1. `event.key`
* 2. `event.keyIdentifier`
* 3. `event.keyCode` (for posterity)
*
* However, [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
* and [`event.keyIdentifier`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyIdentifier)
* both return a string rather than a numerical key code as the old `keyCode` API did.
*
* Therefore, we have two arrays of keycodes, one for the string APIs and one for the
* older number based API.
*
* @param event The keyboard event to detect keyboard navigation against.
*/
export function detectKeyboardNavigation( event: KeyboardEvent ): boolean {
let code: number | string | undefined;

if ( event.key !== undefined ) {
code = event.key;
} else if ( event.keyIdentifier !== undefined ) {
code = event.keyIdentifier;
} else if ( event.keyCode !== undefined ) {
code = event.keyCode;
}

// This shouldn't ever happen but we do it to appease TypeScript
if ( code === undefined ) {
return false;
}

if ( typeof code === 'string' ) {
return keyboardNavigationKeyValues.indexOf( code ) !== -1;
}

return keyboardNavigationKeycodes.indexOf( code ) !== -1;
}

let keyboardNavigation = false;

export default function accessibleFocus(): void {
document.addEventListener( 'keydown', function ( event ) {
if ( keyboardNavigation ) {
return;
}

if ( detectKeyboardNavigation( event ) ) {
keyboardNavigation = true;
document.documentElement.classList.add( 'accessible-focus' );
}
} );
document.addEventListener( 'mouseup', function () {
if ( ! keyboardNavigation ) {
return;
}
keyboardNavigation = false;
document.documentElement.classList.remove( 'accessible-focus' );
} );
}
69 changes: 69 additions & 0 deletions packages/accessible-focus/src/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @jest-environment jsdom
*/

/**
* Internal dependencies
*/
import { detectKeyboardNavigation } from '..';

describe( 'detectKeyboardNavigation', () => {
describe( 'keyCode', () => {
it.each( [ 9, 32, 37, 38, 39, 40 ] )(
'should return true when the keyCode is %s',
( keyCode ) => {
const event = {
keyCode,
};

expect( detectKeyboardNavigation( event ) ).toBeTruthy();
}
);

it( 'should be false when keyCode does not indicate keyboard navigation', () => {
const event = {
keyCode: 46, // delete
};

expect( detectKeyboardNavigation( event ) ).toBeFalsy();
} );
} );

describe( 'keyIdentifier', () => {
it.each( [ 'Tab', ' ', 'Spacebar', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight' ] )(
'should return true when keyIdenitifer is "%s"',
( keyIdentifier ) => {
const event = {
keyIdentifier,
};

expect( detectKeyboardNavigation( event ) ).toBeTruthy();
}
);

it( 'should be false when keyIdentifier does not indicate keyboard navigation', () => {
const event = {
keyIdenitifer: 'Delete',
};

expect( detectKeyboardNavigation( event ) ).toBeFalsy();
} );
} );

describe( 'key', () => {
it.each( [ 'Tab', ' ', 'Spacebar', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight' ] )(
'should return true when key is "%s"',
( key ) => {
const event = { key };

expect( detectKeyboardNavigation( event ) ).toBeTruthy();
}
);

it( 'should be false when key does not indicate keyboard navigation', () => {
const event = { key: 'Delete' };

expect( detectKeyboardNavigation( event ) ).toBeFalsy();
} );
} );
} );
11 changes: 11 additions & 0 deletions packages/accessible-focus/tsconfig-cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"declarationDir": null,
"outDir": "dist/cjs",
"composite": false,
"incremental": true
}
}
32 changes: 32 additions & 0 deletions packages/accessible-focus/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES5",
"baseUrl": ".",
"module": "esnext",
"allowJs": false,
"declaration": true,
"declarationDir": "dist/types",
"outDir": "dist/esm",
"rootDir": "src",

"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,

"moduleResolution": "node",
"esModuleInterop": true,

"forceConsistentCasingInFileNames": true,

"typeRoots": [ "../../node_modules/@types" ],
"types": [ "node" ],

"noEmitHelpers": true,
"importHelpers": true,

"composite": true
},
"include": [ "src" ],
"exclude": [ "**/test/*" ]
}
1 change: 1 addition & 0 deletions packages/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

// Reference all TS packages
"references": [
{ "path": "./accessible-focus" },
{ "path": "./browser-data-collector" },
{ "path": "./calypso-analytics" },
{ "path": "./calypso-config" },
Expand Down

0 comments on commit 56f2a25

Please sign in to comment.