diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 000000000..880989118 --- /dev/null +++ b/.yarnclean @@ -0,0 +1 @@ +@types/react-native \ No newline at end of file diff --git a/package.json b/package.json index 5b0ce7be5..13aaa8e60 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/react": "16.9.11", "@types/react-color": "3.0.1", "@types/react-dom": "16.9.4", + "@types/styled-components": "4.4.0", "@types/webpack": "4.39.8", "@types/webpack-env": "1.14.1", "@typescript-eslint/eslint-plugin": "2.7.0", @@ -94,5 +95,8 @@ "webpack-bundle-analyzer": "3.6.0", "webpack-cli": "3.3.10", "webpack-node-externals": "1.7.2" + }, + "resolutions": { + "@types/react": "16.9.11" } } diff --git a/packages/focusvisible/package.json b/packages/focusvisible/package.json index 0251c52ac..09f025e05 100644 --- a/packages/focusvisible/package.json +++ b/packages/focusvisible/package.json @@ -16,6 +16,7 @@ "scripts": { "build": "../../utils/scripts/build.sh" }, + "types": "dist/typings/index.d.ts", "peerDependencies": { "prop-types": "^15.6.1", "react": "^16.8.0", @@ -33,5 +34,5 @@ "publishConfig": { "access": "public" }, - "zendeskgarden:src": "src/index.js" + "zendeskgarden:src": "src/index.ts" } diff --git a/packages/focusvisible/src/FocusVisibleContainer.js b/packages/focusvisible/src/FocusVisibleContainer.js deleted file mode 100644 index c7976eae4..000000000 --- a/packages/focusvisible/src/FocusVisibleContainer.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { useRef } from 'react'; -import PropTypes from 'prop-types'; - -import { useFocusVisible } from './useFocusVisible'; - -export function FocusVisibleContainer({ children, render = children, ...options }) { - const scopeRef = useRef(); - - useFocusVisible({ scope: scopeRef, ...options }); - - return render({ ref: scopeRef }); -} - -FocusVisibleContainer.propTypes = { - children: PropTypes.func, - render: PropTypes.func, - relativeDocument: PropTypes.object, - className: PropTypes.string, - dataAttribute: PropTypes.string -}; diff --git a/packages/focusvisible/src/FocusVisibleContainer.spec.js b/packages/focusvisible/src/FocusVisibleContainer.spec.tsx similarity index 93% rename from packages/focusvisible/src/FocusVisibleContainer.spec.js rename to packages/focusvisible/src/FocusVisibleContainer.spec.tsx index ad85b1825..c695ccb50 100644 --- a/packages/focusvisible/src/FocusVisibleContainer.spec.js +++ b/packages/focusvisible/src/FocusVisibleContainer.spec.tsx @@ -18,7 +18,7 @@ describe('FocusVisibleContainer', () => { {({ ref }) => (
- +
@@ -35,7 +35,12 @@ describe('FocusVisibleContainer', () => { expect(() => { const ErrorExample = () => { + /* eslint-disable @typescript-eslint/ban-ts-ignore */ + // @ts-ignore + // Ignoring to test JS runtime usage - should throw error + // when consumers do not pass a scope value into `useFocusVisible`. useFocusVisible(); + /* eslint-enable @typescript-eslint/ban-ts-ignore */ return
test
; }; @@ -152,7 +157,7 @@ describe('FocusVisibleContainer', () => { }); describe('Elements with keyboard modality', () => { - const KeyboardModalityExample = props => ( + const KeyboardModalityExample = (props: React.HTMLProps) => ( {({ ref }) =>
} diff --git a/packages/focusvisible/src/FocusVisibleContainer.ts b/packages/focusvisible/src/FocusVisibleContainer.ts new file mode 100644 index 000000000..6da1c3e1f --- /dev/null +++ b/packages/focusvisible/src/FocusVisibleContainer.ts @@ -0,0 +1,36 @@ +/** + * Copyright Zendesk, Inc. + * + * Use of this source code is governed under the Apache License, Version 2.0 + * found at http://www.apache.org/licenses/LICENSE-2.0. + */ + +import { useRef } from 'react'; +import PropTypes from 'prop-types'; + +import { useFocusVisible, IUseFocusVisibleProps } from './useFocusVisible'; + +export interface IFocusVisibleContainerProps extends Omit { + render?: (options: { ref: React.RefObject }) => React.ReactNode; + children?: (options: { ref: React.RefObject }) => React.ReactNode; +} + +export const FocusVisibleContainer: React.FunctionComponent = ({ + children, + render = children, + ...options +}) => { + const scopeRef = useRef(null); + + useFocusVisible({ scope: scopeRef, ...options }); + + return render!({ ref: scopeRef }) as React.ReactElement; +}; + +FocusVisibleContainer.propTypes = { + children: PropTypes.func, + render: PropTypes.func, + relativeDocument: PropTypes.object, + className: PropTypes.string, + dataAttribute: PropTypes.string +}; diff --git a/packages/focusvisible/src/index.spec.js b/packages/focusvisible/src/index.spec.js deleted file mode 100644 index b8a4aff71..000000000 --- a/packages/focusvisible/src/index.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { getExports } from 'garden-test-utils'; -import * as rootIndex from './'; - -describe('Index', () => { - it('exports all components and utilities', async () => { - const exports = await getExports({ cwd: __dirname }); - - expect(Object.keys(rootIndex).sort()).toEqual(exports); - }); -}); diff --git a/packages/focusvisible/src/index.js b/packages/focusvisible/src/index.ts similarity index 54% rename from packages/focusvisible/src/index.js rename to packages/focusvisible/src/index.ts index 4cb92a720..b43decb9c 100644 --- a/packages/focusvisible/src/index.js +++ b/packages/focusvisible/src/index.ts @@ -6,7 +6,7 @@ */ /* Hooks */ -export { useFocusVisible } from './useFocusVisible'; +export { useFocusVisible, IUseFocusVisibleProps } from './useFocusVisible'; /* Render-props */ -export { FocusVisibleContainer } from './FocusVisibleContainer'; +export { FocusVisibleContainer, IFocusVisibleContainerProps } from './FocusVisibleContainer'; diff --git a/packages/focusvisible/src/useFocusVisible.js b/packages/focusvisible/src/useFocusVisible.ts similarity index 83% rename from packages/focusvisible/src/useFocusVisible.js rename to packages/focusvisible/src/useFocusVisible.ts index 9d8fe3fab..870171f15 100644 --- a/packages/focusvisible/src/useFocusVisible.js +++ b/packages/focusvisible/src/useFocusVisible.ts @@ -12,7 +12,7 @@ import { useRef, useEffect } from 'react'; -const INPUT_TYPES_WHITE_LIST = { +const INPUT_TYPES_WHITE_LIST: Record = { text: true, search: true, url: true, @@ -28,27 +28,35 @@ const INPUT_TYPES_WHITE_LIST = { 'datetime-local': true }; -export function useFocusVisible({ - scope, - relativeDocument = document, - className = 'garden-focus-visible', - dataAttribute = 'data-garden-focus-visible' -} = {}) { - // console.log(scope.current) +export interface IUseFocusVisibleProps { + scope: React.RefObject; + relativeDocument?: any; + className?: string; + dataAttribute?: string; +} + +export function useFocusVisible( + { + scope, + relativeDocument = document, + className = 'garden-focus-visible', + dataAttribute = 'data-garden-focus-visible' + }: IUseFocusVisibleProps = {} as any +): void { if (!scope) { throw new Error('Error: the useFocusVisible() hook requires a "scope" property'); } const hadKeyboardEvent = useRef(false); const hadFocusVisibleRecently = useRef(false); - const hadFocusVisibleRecentlyTimeout = useRef(null); + const hadFocusVisibleRecentlyTimeout = useRef(); useEffect(() => { /** * Helper function for legacy browsers and iframes which sometimes focus * elements like document, body, and non-interactive SVG. */ - const isValidFocusTarget = el => { + const isValidFocusTarget = (el: Element) => { if ( el && el !== scope.current && @@ -68,15 +76,19 @@ export function useFocusVisible({ * `garden-focus-visible` class being added, i.e. whether it should always match * `:focus-visible` when focused. */ - const focusTriggersKeyboardModality = el => { - const type = el.type; + const focusTriggersKeyboardModality = (el: HTMLElement) => { + const type = (el as HTMLInputElement).type; const tagName = el.tagName; - if (tagName === 'INPUT' && INPUT_TYPES_WHITE_LIST[type] && !el.readOnly) { + if ( + tagName === 'INPUT' && + INPUT_TYPES_WHITE_LIST[type] && + !(el as HTMLInputElement).readOnly + ) { return true; } - if (tagName === 'TEXTAREA' && !el.readOnly) { + if (tagName === 'TEXTAREA' && !(el as HTMLTextAreaElement).readOnly) { return true; } @@ -92,7 +104,7 @@ export function useFocusVisible({ /** * Whether the given element is currently :focus-visible */ - const isFocused = el => { + const isFocused = (el: HTMLElement) => { if (el && (el.classList.contains(className) || el.hasAttribute(dataAttribute))) { return true; } @@ -104,19 +116,19 @@ export function useFocusVisible({ * Add the `:focus-visible` class to the given element if it was not added by * the consumer. */ - const addFocusVisibleClass = el => { + const addFocusVisibleClass = (el: HTMLElement) => { if (isFocused(el)) { return; } el.classList.add(className); - el.setAttribute(dataAttribute, true); + el.setAttribute(dataAttribute, 'true'); }; /** * Remove the `:focus-visible` class from the given element. */ - const removeFocusVisibleClass = el => { + const removeFocusVisibleClass = (el: HTMLElement) => { el.classList.remove(className); el.removeAttribute(dataAttribute); }; @@ -128,7 +140,7 @@ export function useFocusVisible({ * Apply `:focus-visible` to any current active element and keep track * of our keyboard modality state with `hadKeyboardEvent`. */ - const onKeyDown = e => { + const onKeyDown = (e: KeyboardEvent) => { if (e.metaKey || e.altKey || e.ctrlKey) { return; } @@ -158,26 +170,26 @@ export function useFocusVisible({ * via the keyboard (e.g. a text box) * @param {Event} e */ - const onFocus = e => { + const onFocus = (e: FocusEvent) => { // Prevent IE from focusing the document or HTML element. - if (!isValidFocusTarget(e.target)) { + if (!isValidFocusTarget(e.target as HTMLElement)) { return; } - if (hadKeyboardEvent.current || focusTriggersKeyboardModality(e.target)) { - addFocusVisibleClass(e.target); + if (hadKeyboardEvent.current || focusTriggersKeyboardModality(e.target as HTMLElement)) { + addFocusVisibleClass(e.target as HTMLElement); } }; /** * On `blur`, remove the `:focus-visible` styling from the target. */ - const onBlur = e => { - if (!isValidFocusTarget(e.target)) { + const onBlur = (e: FocusEvent) => { + if (!isValidFocusTarget(e.target as HTMLElement)) { return; } - if (isFocused(e.target)) { + if (isFocused(e.target as HTMLElement)) { /** * To detect a tab/window switch, we look for a blur event * followed rapidly by a visibility change. If we don't see @@ -186,12 +198,15 @@ export function useFocusVisible({ hadFocusVisibleRecently.current = true; clearTimeout(hadFocusVisibleRecentlyTimeout.current); - hadFocusVisibleRecentlyTimeout.current = setTimeout(() => { + + const timeoutId = setTimeout(() => { hadFocusVisibleRecently.current = false; clearTimeout(hadFocusVisibleRecentlyTimeout.current); }, 100); - removeFocusVisibleClass(e.target); + hadFocusVisibleRecentlyTimeout.current = Number(timeoutId); + + removeFocusVisibleClass(e.target as HTMLElement); } }; @@ -202,8 +217,10 @@ export function useFocusVisible({ * * This accounts for situations where focus enters the page from the URL bar. */ - const onInitialPointerMove = e => { - if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') { + const onInitialPointerMove = (e: MouseEvent | TouchEvent) => { + const nodeName = (e.target as HTMLDocument).nodeName; + + if (nodeName && nodeName.toLowerCase() === 'html') { return; } diff --git a/packages/focusvisible/stories.js b/packages/focusvisible/stories.tsx similarity index 90% rename from packages/focusvisible/stories.js rename to packages/focusvisible/stories.tsx index c6c65e5ab..3490e2b6e 100644 --- a/packages/focusvisible/stories.js +++ b/packages/focusvisible/stories.tsx @@ -14,7 +14,11 @@ import { withKnobs } from '@storybook/addon-knobs'; import { useFocusVisible, FocusVisibleContainer } from './src'; import { useSelection } from '../selection/src'; -const StyledCustomFocus = styled.div` +interface IStyledCustomFocus extends React.HTMLProps { + isSelected?: boolean; +} + +const StyledCustomFocus = styled.div` :focus { outline: none; } @@ -34,7 +38,7 @@ storiesOf('FocusVisible Container', module) .addDecorator(withKnobs) .add('useFocusVisible', () => { const Example = () => { - const ref = useRef(); + const ref = useRef(null); useFocusVisible({ scope: ref }); @@ -51,7 +55,7 @@ storiesOf('FocusVisible Container', module) />
- +

Focusable div content only shows focus with keyboard interaction

@@ -95,12 +99,12 @@ storiesOf('FocusVisible Container', module) const { selectedItem, getContainerProps, getItemProps } = useSelection({ defaultSelectedIndex: 0 }); - const ref = useRef(); + const ref = useRef(null); useFocusVisible({ scope: ref }); return ( - + {items.map(item => { const itemRef = React.createRef(); const isSelected = selectedItem === item; diff --git a/packages/focusvisible/tsconfig.build.json b/packages/focusvisible/tsconfig.build.json new file mode 100644 index 000000000..048001557 --- /dev/null +++ b/packages/focusvisible/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "dist/typings", + "declaration": true, + "sourceMap": false, + "noEmit": false + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.tsx", "**/*.spec.ts"] +} diff --git a/yarn.lock b/yarn.lock index 66c8583e0..eea0cb3cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2763,6 +2763,14 @@ dependencies: "@types/react" "*" +"@types/react-native@*": + version "0.60.22" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.22.tgz#ba199a441cb0612514244ffb1d0fe6f04c878575" + integrity sha512-LTXMKEyGA+x4kadmjujX6yAgpcaZutJ01lC7zLJWCULaZg7Qw5/3iOQpwIJRUcOc+a8A2RR7rSxplehVf9IuhA== + dependencies: + "@types/prop-types" "*" + "@types/react" "*" + "@types/react-syntax-highlighter@10.1.0": version "10.1.0" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-10.1.0.tgz#9c534e29bbe05dba9beae1234f3ae944836685d4" @@ -2795,6 +2803,15 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/styled-components@4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.0.tgz#15a3d59533fd3a5bd013db4a7c4422ec542c59d2" + integrity sha512-QFl+w3hQJNHE64Or3PXMFpC3HAQDiuQLi5o9m1XPEwYWfgCZtAribO5ksjxnO8U0LG8Parh0ESCgVxo4VfxlHg== + dependencies: + "@types/react" "*" + "@types/react-native" "*" + csstype "^2.2.0" + "@types/tapable@*": version "1.0.4" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" @@ -4617,9 +4634,9 @@ can-use-dom@^0.1.0: integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30001004, caniuse-lite@^1.0.30001006: - version "1.0.30001009" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001009.tgz#69b77997b882a7aee6af24c8d7d2fa27ee41f348" - integrity sha512-M3rEqHN6SaVjgo4bIik7HsGcWXsi+lI9WA0p51RPMFx5gXfduyOXWJrc0R4xBkSK1pgNf4CNgy5M+6H+WiEP8g== + version "1.0.30001010" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001010.tgz#397a14034d384260453cc81994f494626d34b938" + integrity sha512-RA5GH9YjFNea4ZQszdWgh2SC+dpLiRAg4VDQS2b5JRI45OxmbGrYocYHTa9x0bKMQUE7uvHkNPNffUr+pCxSGw== capture-exit@^2.0.0: version "2.0.0" @@ -9494,9 +9511,9 @@ make-error@1.x: integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== make-fetch-happen@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.1.tgz#fac65400ab5f7a9c001862a3e9b0f417f0840175" - integrity sha512-b4dfaMvUDR67zxUq1+GN7Ke9rH5WvGRmoHuMH7l+gmUCR2tCXFP6mpeJ9Dp+jB6z8mShRopSf1vLRBhRs8Cu5w== + version "5.0.2" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd" + integrity sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag== dependencies: agentkeepalive "^3.4.1" cacache "^12.0.0"