Skip to content

Commit 5a7c37e

Browse files
authored
Merge pull request #504 from Lemoncode/fixaccessibilitybug/custom-select
Fixaccessibilitybug/custom select
2 parents 87b9668 + 31d0911 commit 5a7c37e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1116
-784
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
3+
export const useClickOutside = (
4+
isOpen: boolean,
5+
ref: React.RefObject<HTMLElement>,
6+
callback: (e: MouseEvent) => void
7+
) => {
8+
const handleClickOutside = (e: MouseEvent) => {
9+
callback(e);
10+
};
11+
12+
React.useEffect(() => {
13+
ref.current?.addEventListener('click', handleClickOutside);
14+
15+
return () => {
16+
ref.current?.removeEventListener('click', handleClickOutside);
17+
};
18+
}, [isOpen]);
19+
};

src/common/a11y/common.model.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export type BaseA11yOption<Option> = Option & {
2+
tabIndex: number;
3+
};
4+
5+
export type NestedOption<Option> = {
6+
id: string;
7+
children?: Option[];
8+
};
9+
10+
export type FlatOption<Option extends NestedOption<Option>> = Omit<
11+
Option,
12+
'children'
13+
> & {
14+
parentId?: string;
15+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const getArrowUpIndex = (currentIndex: number) => {
2+
const isFirstOption = currentIndex === 0;
3+
return isFirstOption ? currentIndex : currentIndex - 1;
4+
};
5+
6+
export const getArrowDownIndex = (currentIndex: number, options: any[]) => {
7+
const isLastOption = currentIndex === options.length - 1;
8+
return isLastOption ? currentIndex : currentIndex + 1;
9+
};
10+
11+
export const getFocusedOption = <FocusableOption extends { tabIndex: number }>(
12+
options: FocusableOption[]
13+
) => options.find(option => option.tabIndex === 0);

src/common/a11y/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export * from './select';
2+
export * from './nested-select';
3+
export * from './on-key.hook';
4+
export * from './focus.common-helpers';
5+
export * from './nested-list';
6+
export * from './common.model';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BaseA11yOption } from '../common.model';
2+
3+
export const setInitialFocus = <
4+
Option,
5+
A11yOption extends BaseA11yOption<Option>,
6+
>(
7+
options: Option[]
8+
): A11yOption[] => {
9+
const a11ySelectionOptions = options.map<A11yOption>(
10+
(option, index) =>
11+
({
12+
...option,
13+
tabIndex: index === 0 ? 0 : -1,
14+
}) as unknown as A11yOption
15+
);
16+
17+
return a11ySelectionOptions;
18+
};
19+
20+
export const onFocusOption =
21+
<Option>(option: BaseA11yOption<Option>) =>
22+
(element: any) => {
23+
if (option.tabIndex === 0) {
24+
element?.focus();
25+
}
26+
};

src/common/a11y/list/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './list.hooks';
2+
export * from './list.model';

src/common/a11y/list/list.hooks.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from 'react';
2+
import { BaseA11yOption } from '../common.model';
3+
import { getArrowDownIndex, getArrowUpIndex } from '../focus.common-helpers';
4+
import { useOnKey } from '../on-key.hook';
5+
import { onFocusOption, setInitialFocus } from './focus.helpers';
6+
import { SetInitialFocusFn } from './list.model';
7+
import { useOnTwoKeys } from '../on-two-Keys.hook';
8+
9+
export const useA11yList = <Option, A11yOption extends BaseA11yOption<Option>>(
10+
options: Option[],
11+
onSetInitialFocus: SetInitialFocusFn<Option, A11yOption> = setInitialFocus
12+
) => {
13+
const optionListRef = React.useRef<any>(null);
14+
const [internalOptions, setInternalOptions] = React.useState<A11yOption[]>(
15+
onSetInitialFocus(options)
16+
);
17+
18+
const handleFocus = (event: KeyboardEvent) => {
19+
const currentIndex = internalOptions.findIndex(
20+
option => option.tabIndex === 0
21+
);
22+
const nextIndex =
23+
event.key === 'ArrowUp'
24+
? getArrowUpIndex(currentIndex)
25+
: getArrowDownIndex(currentIndex, internalOptions);
26+
27+
if (currentIndex !== nextIndex) {
28+
setInternalOptions(prevOptions =>
29+
prevOptions.map((option, index) => {
30+
switch (index) {
31+
case currentIndex:
32+
return {
33+
...option,
34+
tabIndex: -1,
35+
};
36+
case nextIndex:
37+
return {
38+
...option,
39+
tabIndex: 0,
40+
};
41+
default:
42+
return option;
43+
}
44+
})
45+
);
46+
}
47+
};
48+
49+
const handleFirstAndLast = (value: number) => {
50+
setInternalOptions(prevOptions =>
51+
prevOptions.map((option, index) => {
52+
switch (index) {
53+
case value:
54+
return {
55+
...option,
56+
tabIndex: 0,
57+
};
58+
default:
59+
return {
60+
...option,
61+
tabIndex: -1,
62+
};
63+
}
64+
})
65+
);
66+
};
67+
68+
//Need this for Mac users
69+
useOnTwoKeys(
70+
optionListRef,
71+
['ArrowUp', 'ArrowDown'],
72+
'Meta',
73+
(event: KeyboardEvent) =>
74+
event.key === 'ArrowUp'
75+
? handleFirstAndLast(0)
76+
: handleFirstAndLast(internalOptions.length - 1)
77+
);
78+
79+
useOnKey(optionListRef, ['ArrowDown', 'ArrowUp'], (event: KeyboardEvent) => {
80+
handleFocus(event);
81+
});
82+
83+
useOnKey(optionListRef, ['Home', 'End'], (event: KeyboardEvent) =>
84+
event.key === 'Home'
85+
? handleFirstAndLast(0)
86+
: handleFirstAndLast(internalOptions.length - 1)
87+
);
88+
89+
return {
90+
optionListRef,
91+
options: internalOptions,
92+
setOptions: setInternalOptions,
93+
onFocusOption,
94+
};
95+
};

src/common/a11y/list/list.model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type SetInitialFocusFn<Option, A11yOption> = (
2+
options: Option[]
3+
) => A11yOption[];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './nested-list.hooks';
2+
export * from './nested-list.model';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
mapFlatOptionsToNestedListOptions,
3+
mapNestedListOptionsToFlatOptions,
4+
} from './nested-list.mappers';
5+
import { NestedOption } from '../common.model';
6+
import { useA11yNested } from '../nested.hooks';
7+
import { useA11yList } from '../list';
8+
9+
export const useA11yNestedList = <Option extends NestedOption<Option>>(
10+
options: Option[]
11+
) => {
12+
const flatOptions = mapNestedListOptionsToFlatOptions(options);
13+
14+
const {
15+
optionListRef,
16+
options: internalOptions,
17+
setOptions,
18+
onFocusOption,
19+
} = useA11yList(flatOptions);
20+
21+
useA11yNested(optionListRef, internalOptions, setOptions);
22+
23+
return {
24+
optionListRef,
25+
options: mapFlatOptionsToNestedListOptions(internalOptions),
26+
setOptions,
27+
onFocusOption,
28+
};
29+
};

0 commit comments

Comments
 (0)