Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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