From 56f2a253247d4a8e0c7fc5614ef0a2f0646e6522 Mon Sep 17 00:00:00 2001 From: sarayourfriend Date: Thu, 11 Feb 2021 07:07:52 -0800 Subject: [PATCH] accessible-focus: Extract into package, re-write in TypeScript, test, and handle deprecated APIs (#49351) * accessible-focus: Extract from lib * accessible-focus: Use extracted package * Clean up package --- client/boot/common.js | 2 +- client/landing/gutenboarding/index.tsx | 2 +- client/lib/accessible-focus/index.js | 23 ----- client/package.json | 1 + docs/accessibility.md | 2 +- packages/accessible-focus/CHANGELOG.md | 3 + .../accessible-focus/README.md | 0 packages/accessible-focus/jest.config.js | 3 + packages/accessible-focus/package.json | 34 ++++++++ packages/accessible-focus/src/index.ts | 86 +++++++++++++++++++ packages/accessible-focus/src/test/index.js | 69 +++++++++++++++ packages/accessible-focus/tsconfig-cjs.json | 11 +++ packages/accessible-focus/tsconfig.json | 32 +++++++ packages/tsconfig.json | 1 + 14 files changed, 243 insertions(+), 26 deletions(-) delete mode 100644 client/lib/accessible-focus/index.js create mode 100644 packages/accessible-focus/CHANGELOG.md rename {client/lib => packages}/accessible-focus/README.md (100%) create mode 100644 packages/accessible-focus/jest.config.js create mode 100644 packages/accessible-focus/package.json create mode 100644 packages/accessible-focus/src/index.ts create mode 100644 packages/accessible-focus/src/test/index.js create mode 100644 packages/accessible-focus/tsconfig-cjs.json create mode 100644 packages/accessible-focus/tsconfig.json diff --git a/client/boot/common.js b/client/boot/common.js index a46f9e88e7221..c041439b4f2de 100644 --- a/client/boot/common.js +++ b/client/boot/common.js @@ -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 @@ -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'; diff --git a/client/landing/gutenboarding/index.tsx b/client/landing/gutenboarding/index.tsx index de83a3b3fad25..bae0a99938f2b 100644 --- a/client/landing/gutenboarding/index.tsx +++ b/client/landing/gutenboarding/index.tsx @@ -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'; /** @@ -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'; diff --git a/client/lib/accessible-focus/index.js b/client/lib/accessible-focus/index.js deleted file mode 100644 index 02f6241ade582..0000000000000 --- a/client/lib/accessible-focus/index.js +++ /dev/null @@ -1,23 +0,0 @@ -let keyboardNavigation = false; -const keyboardNavigationKeycodes = [ 9, 32, 37, 38, 39, 40 ]; // keyCodes for tab, space, left, up, right, down respectively - -function accessibleFocus() { - document.addEventListener( 'keydown', function ( event ) { - if ( keyboardNavigation ) { - return; - } - if ( keyboardNavigationKeycodes.indexOf( event.keyCode ) !== -1 ) { - keyboardNavigation = true; - document.documentElement.classList.add( 'accessible-focus' ); - } - } ); - document.addEventListener( 'mouseup', function () { - if ( ! keyboardNavigation ) { - return; - } - keyboardNavigation = false; - document.documentElement.classList.remove( 'accessible-focus' ); - } ); -} - -export default accessibleFocus; diff --git a/client/package.json b/client/package.json index a7ee4656e6df6..e5d653852ddb0 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/docs/accessibility.md b/docs/accessibility.md index 536ad90395a65..8473ee391a15f 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -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. diff --git a/packages/accessible-focus/CHANGELOG.md b/packages/accessible-focus/CHANGELOG.md new file mode 100644 index 0000000000000..bb6b0d15824db --- /dev/null +++ b/packages/accessible-focus/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0-alpha.0 + +Extracted from `accessible-focus` and transformed to TypeScript and tests added. diff --git a/client/lib/accessible-focus/README.md b/packages/accessible-focus/README.md similarity index 100% rename from client/lib/accessible-focus/README.md rename to packages/accessible-focus/README.md diff --git a/packages/accessible-focus/jest.config.js b/packages/accessible-focus/jest.config.js new file mode 100644 index 0000000000000..5e78d0b775e82 --- /dev/null +++ b/packages/accessible-focus/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: '../../test/packages/jest-preset.js', +}; diff --git a/packages/accessible-focus/package.json b/packages/accessible-focus/package.json new file mode 100644 index 0000000000000..4c5bbe1bf312f --- /dev/null +++ b/packages/accessible-focus/package.json @@ -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" + } +} diff --git a/packages/accessible-focus/src/index.ts b/packages/accessible-focus/src/index.ts new file mode 100644 index 0000000000000..a1469cdbacb96 --- /dev/null +++ b/packages/accessible-focus/src/index.ts @@ -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' ); + } ); +} diff --git a/packages/accessible-focus/src/test/index.js b/packages/accessible-focus/src/test/index.js new file mode 100644 index 0000000000000..d6c6f2e7884ad --- /dev/null +++ b/packages/accessible-focus/src/test/index.js @@ -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(); + } ); + } ); +} ); diff --git a/packages/accessible-focus/tsconfig-cjs.json b/packages/accessible-focus/tsconfig-cjs.json new file mode 100644 index 0000000000000..7e6a3ac57b8d8 --- /dev/null +++ b/packages/accessible-focus/tsconfig-cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "declarationDir": null, + "outDir": "dist/cjs", + "composite": false, + "incremental": true + } +} diff --git a/packages/accessible-focus/tsconfig.json b/packages/accessible-focus/tsconfig.json new file mode 100644 index 0000000000000..611197f695e7d --- /dev/null +++ b/packages/accessible-focus/tsconfig.json @@ -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/*" ] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 5a6853125af06..441e4cd948ff3 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -3,6 +3,7 @@ // Reference all TS packages "references": [ + { "path": "./accessible-focus" }, { "path": "./browser-data-collector" }, { "path": "./calypso-analytics" }, { "path": "./calypso-config" },