Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: rewrite the useFocusManagement hook with a new approach #2369

Merged
merged 15 commits into from
Sep 19, 2019
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
7 changes: 1 addition & 6 deletions packages/clay-autocomplete/src/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ const ClayAutocompleteItem: React.FunctionComponent<IProps> = ({
const fuzzyMatch = fuzzy.match(match, value, optionsFuzzy);

return (
<ClayDropDown.Item
{...otherProps}
innerRef={innerRef}
ref={forwardRef}
tabIndex={-1}
>
<ClayDropDown.Item {...otherProps} innerRef={innerRef} ref={forwardRef}>
{match && fuzzyMatch ? (
<div
dangerouslySetInnerHTML={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,9 @@ exports[`ClayAutocomplete renders Input with classNames when loading for true 1`

exports[`ClayAutocomplete renders Item with matches values 1`] = `
<div>
<li
tabindex="-1"
>
<li>
<span
class="dropdown-item"
tabindex="-1"
>
Bar
</span>
Expand Down
65 changes: 40 additions & 25 deletions packages/clay-autocomplete/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,53 @@ interface IProps extends React.HTMLAttributes<HTMLDivElement> {
component?: React.ForwardRefExoticComponent<any>;
}

const ClayAutocomplete: React.FunctionComponent<IProps> & {
type Autocomplete = React.ForwardRefExoticComponent<IProps> & {
DropDown: typeof DropDown;
Input: typeof Input;
Item: typeof Item;
LoadingIndicator: typeof LoadingIndicator;
} = ({
children,
className,
component: Component = AutocompleteMarkup,
...otherProps
}: IProps) => {
const containerElementRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(false);
};

const ClayAutocomplete = React.forwardRef(
(
{
children,
className,
component: Component = AutocompleteMarkup,
...otherProps
}: IProps,
ref
) => {
const containerElementRef = useRef<null | HTMLDivElement>(null);
const [loading, setLoading] = useState(false);

return (
<Component
{...otherProps}
className={className}
ref={containerElementRef}
>
<Context.Provider
value={{
containerElementRef,
loading,
onLoadingChange: (loading: boolean) => setLoading(loading),
return (
<Component
{...otherProps}
className={className}
ref={(r: any) => {
containerElementRef.current = r;
if (typeof ref === 'function') {
ref(r);
} else if (ref !== null) {
(ref.current as React.MutableRefObject<any>) = r;
}
}}
>
{children}
</Context.Provider>
</Component>
);
};
<Context.Provider
value={{
containerElementRef,
loading,
onLoadingChange: (loading: boolean) =>
setLoading(loading),
}}
>
{children}
</Context.Provider>
</Component>
);
}
) as Autocomplete;

ClayAutocomplete.DropDown = DropDown;
ClayAutocomplete.Input = Input;
Expand Down
76 changes: 26 additions & 50 deletions packages/clay-autocomplete/stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ import ClayAutocomplete from '../src';
import ClayDropDown from '@clayui/drop-down';
import React, {useEffect, useRef, useState} from 'react';
import {FetchPolicy, NetworkStatus} from '@clayui/data-provider/src/types';
import {FocusScope, useDebounce} from '@clayui/shared';
import {storiesOf} from '@storybook/react';
import {useDebounce, useFocusManagement} from '@clayui/shared';
import {useResource} from '@clayui/data-provider';

import '@clayui/css/lib/css/atlas.css';

const TAB_KEY_CODE = 9;
const ARROW_UP_KEY_CODE = 38;
const ARROW_DOWN_KEY_CODE = 40;

const LoadingWithDebounce = ({
loading,
networkStatus,
Expand Down Expand Up @@ -72,61 +68,41 @@ const AutocompleteWithKeyboardFunctionality = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState('');
const [active, setActive] = useState(!!value);
const focusManager = useFocusManagement();

const filteredItems = ['one', 'two', 'three', 'four', 'five'].filter(item =>
item.match(value)
);

const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
const {keyCode, shiftKey} = event;

if (
keyCode === ARROW_DOWN_KEY_CODE ||
(keyCode === TAB_KEY_CODE && !shiftKey)
) {
event.preventDefault();
focusManager.focusNext();
} else if (
keyCode === ARROW_UP_KEY_CODE ||
(keyCode === TAB_KEY_CODE && shiftKey)
) {
event.preventDefault();
focusManager.focusPrevious();
}
};

useEffect(() => {
setActive(!!value);
}, [value]);

return (
<ClayAutocomplete onKeyDown={onKeyDown}>
<ClayAutocomplete.Input
onChange={(event: any) => setValue(event.target.value)}
ref={ref => {
focusManager.createScope(ref, 'input');
inputRef.current = ref;
}}
value={value}
/>

<ClayAutocomplete.DropDown active={active} onSetActive={setActive}>
<ClayDropDown.ItemList>
{filteredItems.map((item, i) => (
<ClayAutocomplete.Item
innerRef={ref =>
focusManager.createScope(ref, `item${i}`, true)
}
key={item}
match={value}
onClick={() => setValue(item)}
value={item}
/>
))}
</ClayDropDown.ItemList>
</ClayAutocomplete.DropDown>
</ClayAutocomplete>
<FocusScope>
<ClayAutocomplete>
<ClayAutocomplete.Input
onChange={(event: any) => setValue(event.target.value)}
ref={inputRef}
value={value}
/>

<ClayAutocomplete.DropDown
active={active}
onSetActive={setActive}
>
<ClayDropDown.ItemList>
{filteredItems.map(item => (
<ClayAutocomplete.Item
key={item}
match={value}
onClick={() => setValue(item)}
value={item}
/>
))}
</ClayDropDown.ItemList>
</ClayAutocomplete.DropDown>
</ClayAutocomplete>
</FocusScope>
);
};

Expand Down
67 changes: 35 additions & 32 deletions packages/clay-drop-down/src/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ItemList from './ItemList';
import Menu, {Align} from './Menu';
import React, {useRef} from 'react';
import Search from './Search';
import {FocusScope} from '@clayui/shared';

interface IProps extends React.HTMLAttributes<HTMLDivElement | HTMLLIElement> {
/**
Expand Down Expand Up @@ -89,39 +90,41 @@ const ClayDropDown: React.FunctionComponent<IProps> & {
};

return (
<ContainerElement
{...otherProps}
className={classNames('dropdown', className)}
onKeyUp={handleKeyUp}
>
{React.cloneElement(trigger, {
className: classNames(
'dropdown-toggle',
trigger.props.className
),
onClick: () => onActiveChange(!active),
ref: (node: HTMLButtonElement) => {
triggerElementRef.current = node;
// Call the original ref, if any.
const {ref} = trigger;
if (typeof ref === 'function') {
ref(node);
}
},
})}

<Menu
active={active}
alignElementRef={triggerElementRef}
alignmentPosition={alignmentPosition}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
onSetActive={onActiveChange}
ref={menuElementRef}
<FocusScope>
<ContainerElement
{...otherProps}
className={classNames('dropdown', className)}
onKeyUp={handleKeyUp}
>
{children}
</Menu>
</ContainerElement>
{React.cloneElement(trigger, {
className: classNames(
'dropdown-toggle',
trigger.props.className
),
onClick: () => onActiveChange(!active),
ref: (node: HTMLButtonElement) => {
triggerElementRef.current = node;
// Call the original ref, if any.
const {ref} = trigger;
if (typeof ref === 'function') {
ref(node);
}
},
})}

<Menu
active={active}
alignElementRef={triggerElementRef}
alignmentPosition={alignmentPosition}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
onSetActive={onActiveChange}
ref={menuElementRef}
>
{children}
</Menu>
</ContainerElement>
</FocusScope>
);
};

Expand Down
25 changes: 15 additions & 10 deletions packages/clay-drop-down/src/DropDownWithItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ interface IProps extends IDropDownContentProps {
searchValue?: string;
}

const Checkbox: React.FunctionComponent<IItem> = ({
interface IInternalItem {
spritemap?: string;
}

const Checkbox: React.FunctionComponent<IItem & IInternalItem> = ({
checked = false,
onChange = () => {},
...otherProps
Expand All @@ -119,10 +123,9 @@ const Checkbox: React.FunctionComponent<IItem> = ({

const ClayDropDownContext = React.createContext({close: () => {}});

const Item: React.FunctionComponent<Omit<IItem, 'onChange'>> = ({
onClick,
...props
}) => {
const Item: React.FunctionComponent<
Omit<IItem, 'onChange'> & IInternalItem
> = ({label, onClick, ...props}) => {
const {close} = React.useContext(ClayDropDownContext);

return (
Expand All @@ -136,12 +139,12 @@ const Item: React.FunctionComponent<Omit<IItem, 'onChange'>> = ({
}}
{...props}
>
{props.label}
{label}
</ClayDropDown.Item>
);
};

const Group: React.FunctionComponent<IItem & {spritemap?: string}> = ({
const Group: React.FunctionComponent<IItem & IInternalItem> = ({
items,
label,
spritemap,
Expand All @@ -160,7 +163,10 @@ interface IRadioContext {

const RadioGroupContext = React.createContext({} as IRadioContext);

const Radio: React.FunctionComponent<IItem> = ({value = '', ...otherProps}) => {
const Radio: React.FunctionComponent<IItem & IInternalItem> = ({
value = '',
...otherProps
}) => {
const {checked, name, onChange} = useContext(RadioGroupContext);

return (
Expand All @@ -177,7 +183,7 @@ const Radio: React.FunctionComponent<IItem> = ({value = '', ...otherProps}) => {
);
};

const RadioGroup: React.FunctionComponent<IItem & {spritemap?: string}> = ({
const RadioGroup: React.FunctionComponent<IItem & IInternalItem> = ({
items,
label,
name,
Expand Down Expand Up @@ -253,7 +259,6 @@ export const ClayDropDownWithItems: React.FunctionComponent<IProps> = ({
trigger,
}: IProps) => {
const [active, setActive] = useState(false);

const hasRightSymbols = !!items.find(item => item.symbolRight);
const hasLeftSymbols = !!items.find(item => item.symbolLeft);

Expand Down
2 changes: 1 addition & 1 deletion packages/clay-drop-down/src/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const ClayDropDownItem: React.FunctionComponent<IProps> = ({
const ItemElement = href ? 'a' : clickableElement;

return (
<li aria-selected={active} ref={forwardRef} tabIndex={-1}>
<li aria-selected={active} ref={forwardRef}>
<ItemElement
{...otherProps}
className={classNames('dropdown-item', className, {
Expand Down
4 changes: 2 additions & 2 deletions packages/clay-drop-down/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import classNames from 'classnames';
import React, {useLayoutEffect, useRef} from 'react';
import React, {useEffect, useRef} from 'react';
import {Align} from 'metal-position';
import {ClayPortal} from '@clayui/shared';
import {useDropdownCloseInteractions} from './hooks';
Expand Down Expand Up @@ -80,7 +80,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>((

useDropdownCloseInteractions([alignElementRef, subPortalRef], onSetActive);

useLayoutEffect(() => {
useEffect(() => {
if (
alignElementRef.current &&
(ref as React.RefObject<HTMLDivElement>).current
Expand Down
Loading