Skip to content

Commit 335357a

Browse files
authored
feat(focusvisible): migrate focusvisible to TypeScript (#137)
1 parent 84c0daf commit 335357a

12 files changed

+143
-90
lines changed

.yarnclean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@types/react-native

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@types/react": "16.9.11",
4646
"@types/react-color": "3.0.1",
4747
"@types/react-dom": "16.9.4",
48+
"@types/styled-components": "4.4.0",
4849
"@types/webpack": "4.39.8",
4950
"@types/webpack-env": "1.14.1",
5051
"@typescript-eslint/eslint-plugin": "2.7.0",
@@ -94,5 +95,8 @@
9495
"webpack-bundle-analyzer": "3.6.0",
9596
"webpack-cli": "3.3.10",
9697
"webpack-node-externals": "1.7.2"
98+
},
99+
"resolutions": {
100+
"@types/react": "16.9.11"
97101
}
98102
}

packages/focusvisible/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"scripts": {
1717
"build": "../../utils/scripts/build.sh"
1818
},
19+
"types": "dist/typings/index.d.ts",
1920
"peerDependencies": {
2021
"prop-types": "^15.6.1",
2122
"react": "^16.8.0",
@@ -33,5 +34,5 @@
3334
"publishConfig": {
3435
"access": "public"
3536
},
36-
"zendeskgarden:src": "src/index.js"
37+
"zendeskgarden:src": "src/index.ts"
3738
}

packages/focusvisible/src/FocusVisibleContainer.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

packages/focusvisible/src/FocusVisibleContainer.spec.js renamed to packages/focusvisible/src/FocusVisibleContainer.spec.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('FocusVisibleContainer', () => {
1818
<FocusVisibleContainer>
1919
{({ ref }) => (
2020
<div ref={ref} data-test-id="wrapper">
21-
<button data-test-id="button" tabIndex="0"></button>
21+
<button data-test-id="button" tabIndex={0}></button>
2222
<input data-test-id="input" />
2323
<textarea data-test-id="textarea"></textarea>
2424
</div>
@@ -35,7 +35,12 @@ describe('FocusVisibleContainer', () => {
3535

3636
expect(() => {
3737
const ErrorExample = () => {
38+
/* eslint-disable @typescript-eslint/ban-ts-ignore */
39+
// @ts-ignore
40+
// Ignoring to test JS runtime usage - should throw error
41+
// when consumers do not pass a scope value into `useFocusVisible`.
3842
useFocusVisible();
43+
/* eslint-enable @typescript-eslint/ban-ts-ignore */
3944

4045
return <div>test</div>;
4146
};
@@ -152,7 +157,7 @@ describe('FocusVisibleContainer', () => {
152157
});
153158

154159
describe('Elements with keyboard modality', () => {
155-
const KeyboardModalityExample = props => (
160+
const KeyboardModalityExample = (props: React.HTMLProps<HTMLDivElement>) => (
156161
<FocusVisibleContainer>
157162
{({ ref }) => <div ref={ref} data-test-id="wrapper" {...props} />}
158163
</FocusVisibleContainer>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import { useRef } from 'react';
9+
import PropTypes from 'prop-types';
10+
11+
import { useFocusVisible, IUseFocusVisibleProps } from './useFocusVisible';
12+
13+
export interface IFocusVisibleContainerProps extends Omit<IUseFocusVisibleProps, 'scope'> {
14+
render?: (options: { ref: React.RefObject<HTMLDivElement> }) => React.ReactNode;
15+
children?: (options: { ref: React.RefObject<HTMLDivElement> }) => React.ReactNode;
16+
}
17+
18+
export const FocusVisibleContainer: React.FunctionComponent<IFocusVisibleContainerProps> = ({
19+
children,
20+
render = children,
21+
...options
22+
}) => {
23+
const scopeRef = useRef(null);
24+
25+
useFocusVisible({ scope: scopeRef, ...options });
26+
27+
return render!({ ref: scopeRef }) as React.ReactElement;
28+
};
29+
30+
FocusVisibleContainer.propTypes = {
31+
children: PropTypes.func,
32+
render: PropTypes.func,
33+
relativeDocument: PropTypes.object,
34+
className: PropTypes.string,
35+
dataAttribute: PropTypes.string
36+
};

packages/focusvisible/src/index.spec.js

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/focusvisible/src/index.js renamed to packages/focusvisible/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
/* Hooks */
9-
export { useFocusVisible } from './useFocusVisible';
9+
export { useFocusVisible, IUseFocusVisibleProps } from './useFocusVisible';
1010

1111
/* Render-props */
12-
export { FocusVisibleContainer } from './FocusVisibleContainer';
12+
export { FocusVisibleContainer, IFocusVisibleContainerProps } from './FocusVisibleContainer';

packages/focusvisible/src/useFocusVisible.js renamed to packages/focusvisible/src/useFocusVisible.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import { useRef, useEffect } from 'react';
1414

15-
const INPUT_TYPES_WHITE_LIST = {
15+
const INPUT_TYPES_WHITE_LIST: Record<string, boolean> = {
1616
text: true,
1717
search: true,
1818
url: true,
@@ -28,27 +28,35 @@ const INPUT_TYPES_WHITE_LIST = {
2828
'datetime-local': true
2929
};
3030

31-
export function useFocusVisible({
32-
scope,
33-
relativeDocument = document,
34-
className = 'garden-focus-visible',
35-
dataAttribute = 'data-garden-focus-visible'
36-
} = {}) {
37-
// console.log(scope.current)
31+
export interface IUseFocusVisibleProps {
32+
scope: React.RefObject<HTMLElement | null>;
33+
relativeDocument?: any;
34+
className?: string;
35+
dataAttribute?: string;
36+
}
37+
38+
export function useFocusVisible(
39+
{
40+
scope,
41+
relativeDocument = document,
42+
className = 'garden-focus-visible',
43+
dataAttribute = 'data-garden-focus-visible'
44+
}: IUseFocusVisibleProps = {} as any
45+
): void {
3846
if (!scope) {
3947
throw new Error('Error: the useFocusVisible() hook requires a "scope" property');
4048
}
4149

4250
const hadKeyboardEvent = useRef(false);
4351
const hadFocusVisibleRecently = useRef(false);
44-
const hadFocusVisibleRecentlyTimeout = useRef(null);
52+
const hadFocusVisibleRecentlyTimeout = useRef<number | undefined>();
4553

4654
useEffect(() => {
4755
/**
4856
* Helper function for legacy browsers and iframes which sometimes focus
4957
* elements like document, body, and non-interactive SVG.
5058
*/
51-
const isValidFocusTarget = el => {
59+
const isValidFocusTarget = (el: Element) => {
5260
if (
5361
el &&
5462
el !== scope.current &&
@@ -68,15 +76,19 @@ export function useFocusVisible({
6876
* `garden-focus-visible` class being added, i.e. whether it should always match
6977
* `:focus-visible` when focused.
7078
*/
71-
const focusTriggersKeyboardModality = el => {
72-
const type = el.type;
79+
const focusTriggersKeyboardModality = (el: HTMLElement) => {
80+
const type = (el as HTMLInputElement).type;
7381
const tagName = el.tagName;
7482

75-
if (tagName === 'INPUT' && INPUT_TYPES_WHITE_LIST[type] && !el.readOnly) {
83+
if (
84+
tagName === 'INPUT' &&
85+
INPUT_TYPES_WHITE_LIST[type] &&
86+
!(el as HTMLInputElement).readOnly
87+
) {
7688
return true;
7789
}
7890

79-
if (tagName === 'TEXTAREA' && !el.readOnly) {
91+
if (tagName === 'TEXTAREA' && !(el as HTMLTextAreaElement).readOnly) {
8092
return true;
8193
}
8294

@@ -92,7 +104,7 @@ export function useFocusVisible({
92104
/**
93105
* Whether the given element is currently :focus-visible
94106
*/
95-
const isFocused = el => {
107+
const isFocused = (el: HTMLElement) => {
96108
if (el && (el.classList.contains(className) || el.hasAttribute(dataAttribute))) {
97109
return true;
98110
}
@@ -104,19 +116,19 @@ export function useFocusVisible({
104116
* Add the `:focus-visible` class to the given element if it was not added by
105117
* the consumer.
106118
*/
107-
const addFocusVisibleClass = el => {
119+
const addFocusVisibleClass = (el: HTMLElement) => {
108120
if (isFocused(el)) {
109121
return;
110122
}
111123

112124
el.classList.add(className);
113-
el.setAttribute(dataAttribute, true);
125+
el.setAttribute(dataAttribute, 'true');
114126
};
115127

116128
/**
117129
* Remove the `:focus-visible` class from the given element.
118130
*/
119-
const removeFocusVisibleClass = el => {
131+
const removeFocusVisibleClass = (el: HTMLElement) => {
120132
el.classList.remove(className);
121133
el.removeAttribute(dataAttribute);
122134
};
@@ -128,7 +140,7 @@ export function useFocusVisible({
128140
* Apply `:focus-visible` to any current active element and keep track
129141
* of our keyboard modality state with `hadKeyboardEvent`.
130142
*/
131-
const onKeyDown = e => {
143+
const onKeyDown = (e: KeyboardEvent) => {
132144
if (e.metaKey || e.altKey || e.ctrlKey) {
133145
return;
134146
}
@@ -158,26 +170,26 @@ export function useFocusVisible({
158170
* via the keyboard (e.g. a text box)
159171
* @param {Event} e
160172
*/
161-
const onFocus = e => {
173+
const onFocus = (e: FocusEvent) => {
162174
// Prevent IE from focusing the document or HTML element.
163-
if (!isValidFocusTarget(e.target)) {
175+
if (!isValidFocusTarget(e.target as HTMLElement)) {
164176
return;
165177
}
166178

167-
if (hadKeyboardEvent.current || focusTriggersKeyboardModality(e.target)) {
168-
addFocusVisibleClass(e.target);
179+
if (hadKeyboardEvent.current || focusTriggersKeyboardModality(e.target as HTMLElement)) {
180+
addFocusVisibleClass(e.target as HTMLElement);
169181
}
170182
};
171183

172184
/**
173185
* On `blur`, remove the `:focus-visible` styling from the target.
174186
*/
175-
const onBlur = e => {
176-
if (!isValidFocusTarget(e.target)) {
187+
const onBlur = (e: FocusEvent) => {
188+
if (!isValidFocusTarget(e.target as HTMLElement)) {
177189
return;
178190
}
179191

180-
if (isFocused(e.target)) {
192+
if (isFocused(e.target as HTMLElement)) {
181193
/**
182194
* To detect a tab/window switch, we look for a blur event
183195
* followed rapidly by a visibility change. If we don't see
@@ -186,12 +198,15 @@ export function useFocusVisible({
186198
hadFocusVisibleRecently.current = true;
187199

188200
clearTimeout(hadFocusVisibleRecentlyTimeout.current);
189-
hadFocusVisibleRecentlyTimeout.current = setTimeout(() => {
201+
202+
const timeoutId = setTimeout(() => {
190203
hadFocusVisibleRecently.current = false;
191204
clearTimeout(hadFocusVisibleRecentlyTimeout.current);
192205
}, 100);
193206

194-
removeFocusVisibleClass(e.target);
207+
hadFocusVisibleRecentlyTimeout.current = Number(timeoutId);
208+
209+
removeFocusVisibleClass(e.target as HTMLElement);
195210
}
196211
};
197212

@@ -202,8 +217,10 @@ export function useFocusVisible({
202217
*
203218
* This accounts for situations where focus enters the page from the URL bar.
204219
*/
205-
const onInitialPointerMove = e => {
206-
if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {
220+
const onInitialPointerMove = (e: MouseEvent | TouchEvent) => {
221+
const nodeName = (e.target as HTMLDocument).nodeName;
222+
223+
if (nodeName && nodeName.toLowerCase() === 'html') {
207224
return;
208225
}
209226

packages/focusvisible/stories.js renamed to packages/focusvisible/stories.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { withKnobs } from '@storybook/addon-knobs';
1414
import { useFocusVisible, FocusVisibleContainer } from './src';
1515
import { useSelection } from '../selection/src';
1616

17-
const StyledCustomFocus = styled.div`
17+
interface IStyledCustomFocus extends React.HTMLProps<HTMLDivElement> {
18+
isSelected?: boolean;
19+
}
20+
21+
const StyledCustomFocus = styled.div<IStyledCustomFocus>`
1822
:focus {
1923
outline: none;
2024
}
@@ -34,7 +38,7 @@ storiesOf('FocusVisible Container', module)
3438
.addDecorator(withKnobs)
3539
.add('useFocusVisible', () => {
3640
const Example = () => {
37-
const ref = useRef();
41+
const ref = useRef<HTMLDivElement>(null);
3842

3943
useFocusVisible({ scope: ref });
4044

@@ -51,7 +55,7 @@ storiesOf('FocusVisible Container', module)
5155
/>
5256
</div>
5357
<div>
54-
<StyledCustomFocus tabIndex="0">
58+
<StyledCustomFocus tabIndex={0}>
5559
<p>Focusable div content only shows focus with keyboard interaction</p>
5660
</StyledCustomFocus>
5761
</div>
@@ -95,12 +99,12 @@ storiesOf('FocusVisible Container', module)
9599
const { selectedItem, getContainerProps, getItemProps } = useSelection({
96100
defaultSelectedIndex: 0
97101
});
98-
const ref = useRef();
102+
const ref = useRef<HTMLUListElement>(null);
99103

100104
useFocusVisible({ scope: ref });
101105

102106
return (
103-
<StyledExampleContainer {...getContainerProps({ ref })}>
107+
<StyledExampleContainer {...(getContainerProps({ ref }) as any)}>
104108
{items.map(item => {
105109
const itemRef = React.createRef();
106110
const isSelected = selectedItem === item;

0 commit comments

Comments
 (0)