Skip to content

Commit

Permalink
feat(focusvisible): migrate focusvisible to TypeScript (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
hzhu authored Nov 14, 2019
1 parent 84c0daf commit 335357a
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 90 deletions.
1 change: 1 addition & 0 deletions .yarnclean
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@types/react-native
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
3 changes: 2 additions & 1 deletion packages/focusvisible/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -33,5 +34,5 @@
"publishConfig": {
"access": "public"
},
"zendeskgarden:src": "src/index.js"
"zendeskgarden:src": "src/index.ts"
}
27 changes: 0 additions & 27 deletions packages/focusvisible/src/FocusVisibleContainer.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('FocusVisibleContainer', () => {
<FocusVisibleContainer>
{({ ref }) => (
<div ref={ref} data-test-id="wrapper">
<button data-test-id="button" tabIndex="0"></button>
<button data-test-id="button" tabIndex={0}></button>
<input data-test-id="input" />
<textarea data-test-id="textarea"></textarea>
</div>
Expand All @@ -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 <div>test</div>;
};
Expand Down Expand Up @@ -152,7 +157,7 @@ describe('FocusVisibleContainer', () => {
});

describe('Elements with keyboard modality', () => {
const KeyboardModalityExample = props => (
const KeyboardModalityExample = (props: React.HTMLProps<HTMLDivElement>) => (
<FocusVisibleContainer>
{({ ref }) => <div ref={ref} data-test-id="wrapper" {...props} />}
</FocusVisibleContainer>
Expand Down
36 changes: 36 additions & 0 deletions packages/focusvisible/src/FocusVisibleContainer.ts
Original file line number Diff line number Diff line change
@@ -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<IUseFocusVisibleProps, 'scope'> {
render?: (options: { ref: React.RefObject<HTMLDivElement> }) => React.ReactNode;
children?: (options: { ref: React.RefObject<HTMLDivElement> }) => React.ReactNode;
}

export const FocusVisibleContainer: React.FunctionComponent<IFocusVisibleContainerProps> = ({
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
};
17 changes: 0 additions & 17 deletions packages/focusvisible/src/index.spec.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { useRef, useEffect } from 'react';

const INPUT_TYPES_WHITE_LIST = {
const INPUT_TYPES_WHITE_LIST: Record<string, boolean> = {
text: true,
search: true,
url: true,
Expand All @@ -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<HTMLElement | null>;
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<number | undefined>();

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 &&
Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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);
};
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
};

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {
isSelected?: boolean;
}

const StyledCustomFocus = styled.div<IStyledCustomFocus>`
:focus {
outline: none;
}
Expand All @@ -34,7 +38,7 @@ storiesOf('FocusVisible Container', module)
.addDecorator(withKnobs)
.add('useFocusVisible', () => {
const Example = () => {
const ref = useRef();
const ref = useRef<HTMLDivElement>(null);

useFocusVisible({ scope: ref });

Expand All @@ -51,7 +55,7 @@ storiesOf('FocusVisible Container', module)
/>
</div>
<div>
<StyledCustomFocus tabIndex="0">
<StyledCustomFocus tabIndex={0}>
<p>Focusable div content only shows focus with keyboard interaction</p>
</StyledCustomFocus>
</div>
Expand Down Expand Up @@ -95,12 +99,12 @@ storiesOf('FocusVisible Container', module)
const { selectedItem, getContainerProps, getItemProps } = useSelection({
defaultSelectedIndex: 0
});
const ref = useRef();
const ref = useRef<HTMLUListElement>(null);

useFocusVisible({ scope: ref });

return (
<StyledExampleContainer {...getContainerProps({ ref })}>
<StyledExampleContainer {...(getContainerProps({ ref }) as any)}>
{items.map(item => {
const itemRef = React.createRef();
const isSelected = selectedItem === item;
Expand Down
Loading

0 comments on commit 335357a

Please sign in to comment.