Skip to content

Commit 58ce101

Browse files
Merge pull request #2189 from matuzalemsteles/issue-1987
feat(@clayui/shared): add experimental useFocusManagement hook
2 parents 226455f + 3c85953 commit 58ce101

File tree

8 files changed

+362
-59
lines changed

8 files changed

+362
-59
lines changed

packages/clay-autocomplete/src/Item.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import React from 'react';
1111
interface IProps extends React.ComponentProps<typeof ClayDropDown.Item> {
1212
forwardRef?: React.Ref<HTMLLIElement>;
1313

14+
innerRef?: React.Ref<HTMLAnchorElement>;
15+
1416
/**
1517
* Match is the string that will be compared with value.
1618
*/
@@ -27,14 +29,20 @@ const optionsFuzzy = {post: '</strong>', pre: '<strong>'};
2729

2830
const ClayAutocompleteItem: React.FunctionComponent<IProps> = ({
2931
forwardRef,
32+
innerRef,
3033
match = '',
3134
value,
3235
...otherProps
3336
}: IProps) => {
3437
const fuzzyMatch = fuzzy.match(match, value, optionsFuzzy);
3538

3639
return (
37-
<ClayDropDown.Item {...otherProps} ref={forwardRef}>
40+
<ClayDropDown.Item
41+
{...otherProps}
42+
innerRef={innerRef}
43+
ref={forwardRef}
44+
tabIndex={-1}
45+
>
3846
{match && fuzzyMatch ? (
3947
<div
4048
dangerouslySetInnerHTML={{

packages/clay-autocomplete/src/__tests__/__snapshots__/index.tsx.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ exports[`ClayAutocomplete renders Item with matches values 1`] = `
5151
>
5252
<span
5353
class="dropdown-item"
54+
tabindex="-1"
5455
>
5556
Bar
5657
</span>

packages/clay-autocomplete/stories/index.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import ClayDropDown from '@clayui/drop-down';
99
import React, {useEffect, useRef, useState} from 'react';
1010
import {FetchPolicy, NetworkStatus} from '@clayui/data-provider/src/types';
1111
import {storiesOf} from '@storybook/react';
12-
import {useDebounce, useKeyHandlerForList} from '@clayui/shared';
12+
import {useDebounce, useFocusManagement} from '@clayui/shared';
1313
import {useResource} from '@clayui/data-provider';
1414

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

17+
const TAB_KEY_CODE = 9;
18+
const ARROW_UP_KEY_CODE = 38;
19+
const ARROW_DOWN_KEY_CODE = 40;
20+
1721
const LoadingWithDebounce = ({
1822
loading,
1923
networkStatus,
@@ -65,56 +69,58 @@ const AutocompleteBasic = () => {
6569
};
6670

6771
const AutocompleteWithKeyboardFunctionality = () => {
68-
const activeListItemRef = useRef<HTMLLIElement>(null);
69-
const inputRef = useRef<HTMLInputElement>(null);
72+
const inputRef = useRef<HTMLInputElement | null>(null);
7073
const [value, setValue] = useState('');
71-
const [activeIndex, setActiveIndex] = useState(0);
72-
7374
const [active, setActive] = useState(!!value);
75+
const focusManager = useFocusManagement();
7476

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

79-
const keyHandler = useKeyHandlerForList({
80-
activeListItemRef,
81-
index: activeIndex,
82-
inputRef,
83-
onIndexChange: setActiveIndex,
84-
onIndexSelect: (index: number) => setValue(filteredItems[index]),
85-
totalItems: filteredItems.length,
86-
});
81+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
82+
const {keyCode, shiftKey} = event;
83+
84+
if (
85+
keyCode === ARROW_DOWN_KEY_CODE ||
86+
(keyCode === TAB_KEY_CODE && !shiftKey)
87+
) {
88+
event.preventDefault();
89+
focusManager.focusNext();
90+
} else if (
91+
keyCode === ARROW_UP_KEY_CODE ||
92+
(keyCode === TAB_KEY_CODE && shiftKey)
93+
) {
94+
event.preventDefault();
95+
focusManager.focusPrevious();
96+
}
97+
};
8798

8899
useEffect(() => {
89100
setActive(!!value);
90101
}, [value]);
91102

92-
useEffect(() => {
93-
setActiveIndex(0);
94-
}, [active]);
95-
96103
return (
97-
<ClayAutocomplete>
104+
<ClayAutocomplete onKeyDown={onKeyDown}>
98105
<ClayAutocomplete.Input
99106
onChange={(event: any) => setValue(event.target.value)}
100-
onKeyDown={keyHandler}
101-
ref={inputRef}
107+
ref={ref => {
108+
focusManager.createScope(ref, 'input');
109+
inputRef.current = ref;
110+
}}
102111
value={value}
103112
/>
104113

105114
<ClayAutocomplete.DropDown active={active} onSetActive={setActive}>
106115
<ClayDropDown.ItemList>
107116
{filteredItems.map((item, i) => (
108117
<ClayAutocomplete.Item
109-
active={i === activeIndex}
118+
innerRef={ref =>
119+
focusManager.createScope(ref, `item${i}`, true)
120+
}
110121
key={item}
111122
match={value}
112123
onClick={() => setValue(item)}
113-
ref={
114-
activeIndex === i
115-
? activeListItemRef
116-
: undefined
117-
}
118124
value={item}
119125
/>
120126
))}

packages/clay-button/src/Button.tsx

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,38 +42,44 @@ interface IProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4242
small?: boolean;
4343
}
4444

45-
interface IClayButton extends React.FunctionComponent<IProps> {
46-
Group: typeof ButtonGroup;
47-
}
45+
type Button = React.ForwardRefExoticComponent<
46+
IProps & React.RefAttributes<HTMLButtonElement>
47+
> & {Group: typeof ButtonGroup};
4848

49-
const ClayButton: IClayButton = ({
50-
block,
51-
borderless,
52-
children,
53-
className,
54-
displayType = 'primary',
55-
monospaced,
56-
outline,
57-
small,
58-
type = 'button',
59-
...otherProps
60-
}: IProps) => (
61-
<button
62-
className={classNames(className, 'btn', {
63-
'btn-block': block,
64-
'btn-monospaced': monospaced,
65-
'btn-outline-borderless': borderless,
66-
'btn-sm': small,
67-
[`btn-${displayType}`]: displayType && !outline && !borderless,
68-
[`btn-outline-${displayType}`]:
69-
displayType && (outline || borderless),
70-
})}
71-
type={type}
72-
{...otherProps}
73-
>
74-
{children}
75-
</button>
76-
);
49+
const ClayButton = React.forwardRef(
50+
(
51+
{
52+
block,
53+
borderless,
54+
children,
55+
className,
56+
displayType = 'primary',
57+
monospaced,
58+
outline,
59+
small,
60+
type = 'button',
61+
...otherProps
62+
}: IProps,
63+
ref
64+
) => (
65+
<button
66+
className={classNames(className, 'btn', {
67+
'btn-block': block,
68+
'btn-monospaced': monospaced,
69+
'btn-outline-borderless': borderless,
70+
'btn-sm': small,
71+
[`btn-${displayType}`]: displayType && !outline && !borderless,
72+
[`btn-outline-${displayType}`]:
73+
displayType && (outline || borderless),
74+
})}
75+
ref={ref}
76+
type={type}
77+
{...otherProps}
78+
>
79+
{children}
80+
</button>
81+
)
82+
) as Button;
7783

7884
ClayButton.Group = ButtonGroup;
7985

packages/clay-drop-down/src/DropDownWithBasicItems.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import ClayDropDown from './DropDown';
88
import React, {useState} from 'react';
9+
import {useFocusManagement} from '@clayui/shared';
910

1011
interface IItem {
1112
active?: boolean;
@@ -31,23 +32,49 @@ interface IProps {
3132
spritemap?: string;
3233
}
3334

35+
const TAB_KEY_CODE = 9;
36+
const ARROW_UP_KEY_CODE = 38;
37+
const ARROW_DOWN_KEY_CODE = 40;
38+
3439
export const ClayDropDownWithBasicItems: React.FunctionComponent<IProps> = ({
3540
items,
3641
spritemap,
3742
trigger,
3843
}: IProps) => {
3944
const [active, setActive] = useState(false);
45+
const focusManager = useFocusManagement();
4046

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

50+
const onKeyDown = (event: React.KeyboardEvent<any>) => {
51+
const {keyCode, shiftKey} = event;
52+
53+
if (
54+
keyCode === ARROW_DOWN_KEY_CODE ||
55+
(keyCode === TAB_KEY_CODE && !shiftKey)
56+
) {
57+
event.preventDefault();
58+
focusManager.focusNext();
59+
} else if (
60+
keyCode === ARROW_UP_KEY_CODE ||
61+
(keyCode === TAB_KEY_CODE && shiftKey)
62+
) {
63+
event.preventDefault();
64+
focusManager.focusPrevious();
65+
}
66+
};
67+
4468
return (
4569
<ClayDropDown
4670
active={active}
4771
hasLeftSymbols={hasLeftSymbols}
4872
hasRightSymbols={hasRightSymbols}
4973
onActiveChange={(newVal: boolean) => setActive(newVal)}
50-
trigger={trigger}
74+
onKeyDown={onKeyDown}
75+
trigger={React.cloneElement(trigger, {
76+
ref: (ref: any) => focusManager.createScope(ref, 'trigger'),
77+
})}
5178
>
5279
<ClayDropDown.ItemList>
5380
{items.map((item: IItem, i: number) => {
@@ -57,6 +84,9 @@ export const ClayDropDownWithBasicItems: React.FunctionComponent<IProps> = ({
5784

5885
return (
5986
<ClayDropDown.Item
87+
innerRef={(ref: HTMLLinkElement) =>
88+
focusManager.createScope(ref, `item${i}`, true)
89+
}
6090
key={i}
6191
spritemap={spritemap}
6292
{...item}

packages/clay-drop-down/src/Item.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ interface IProps
2727
*/
2828
href?: string;
2929

30+
innerRef?: React.Ref<any>;
31+
3032
/**
3133
* Path to icon spritemap from clay-css.
3234
*/
@@ -50,6 +52,7 @@ const ClayDropDownItem: React.FunctionComponent<IProps> = ({
5052
disabled,
5153
forwardRef,
5254
href,
55+
innerRef,
5356
onClick,
5457
spritemap,
5558
symbolLeft,
@@ -69,6 +72,7 @@ const ClayDropDownItem: React.FunctionComponent<IProps> = ({
6972
})}
7073
href={href}
7174
onClick={onClick}
75+
ref={innerRef}
7276
>
7377
{symbolLeft && (
7478
<span className="dropdown-item-indicator-start">

packages/clay-shared/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export {ClayPortal} from './Portal';
88
export {useKeyHandlerForList} from './useKeyHandlerForList';
99
export {useDebounce} from './useDebounce';
1010
export {useTransitionHeight} from './useTransitionHeight';
11+
export {useFocusManagement} from './useFocusManagement';

0 commit comments

Comments
 (0)