-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #504 from Lemoncode/fixaccessibilitybug/custom-select
Fixaccessibilitybug/custom select
- Loading branch information
Showing
45 changed files
with
1,116 additions
and
784 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import React from 'react'; | ||
|
||
export const useClickOutside = ( | ||
isOpen: boolean, | ||
ref: React.RefObject<HTMLElement>, | ||
callback: (e: MouseEvent) => void | ||
) => { | ||
const handleClickOutside = (e: MouseEvent) => { | ||
callback(e); | ||
}; | ||
|
||
React.useEffect(() => { | ||
ref.current?.addEventListener('click', handleClickOutside); | ||
|
||
return () => { | ||
ref.current?.removeEventListener('click', handleClickOutside); | ||
}; | ||
}, [isOpen]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export type BaseA11yOption<Option> = Option & { | ||
tabIndex: number; | ||
}; | ||
|
||
export type NestedOption<Option> = { | ||
id: string; | ||
children?: Option[]; | ||
}; | ||
|
||
export type FlatOption<Option extends NestedOption<Option>> = Omit< | ||
Option, | ||
'children' | ||
> & { | ||
parentId?: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const getArrowUpIndex = (currentIndex: number) => { | ||
const isFirstOption = currentIndex === 0; | ||
return isFirstOption ? currentIndex : currentIndex - 1; | ||
}; | ||
|
||
export const getArrowDownIndex = (currentIndex: number, options: any[]) => { | ||
const isLastOption = currentIndex === options.length - 1; | ||
return isLastOption ? currentIndex : currentIndex + 1; | ||
}; | ||
|
||
export const getFocusedOption = <FocusableOption extends { tabIndex: number }>( | ||
options: FocusableOption[] | ||
) => options.find(option => option.tabIndex === 0); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export * from './select'; | ||
export * from './nested-select'; | ||
export * from './on-key.hook'; | ||
export * from './focus.common-helpers'; | ||
export * from './nested-list'; | ||
export * from './common.model'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { BaseA11yOption } from '../common.model'; | ||
|
||
export const setInitialFocus = < | ||
Option, | ||
A11yOption extends BaseA11yOption<Option>, | ||
>( | ||
options: Option[] | ||
): A11yOption[] => { | ||
const a11ySelectionOptions = options.map<A11yOption>( | ||
(option, index) => | ||
({ | ||
...option, | ||
tabIndex: index === 0 ? 0 : -1, | ||
}) as unknown as A11yOption | ||
); | ||
|
||
return a11ySelectionOptions; | ||
}; | ||
|
||
export const onFocusOption = | ||
<Option>(option: BaseA11yOption<Option>) => | ||
(element: any) => { | ||
if (option.tabIndex === 0) { | ||
element?.focus(); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './list.hooks'; | ||
export * from './list.model'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import React from 'react'; | ||
import { BaseA11yOption } from '../common.model'; | ||
import { getArrowDownIndex, getArrowUpIndex } from '../focus.common-helpers'; | ||
import { useOnKey } from '../on-key.hook'; | ||
import { onFocusOption, setInitialFocus } from './focus.helpers'; | ||
import { SetInitialFocusFn } from './list.model'; | ||
import { useOnTwoKeys } from '../on-two-Keys.hook'; | ||
|
||
export const useA11yList = <Option, A11yOption extends BaseA11yOption<Option>>( | ||
options: Option[], | ||
onSetInitialFocus: SetInitialFocusFn<Option, A11yOption> = setInitialFocus | ||
) => { | ||
const optionListRef = React.useRef<any>(null); | ||
const [internalOptions, setInternalOptions] = React.useState<A11yOption[]>( | ||
onSetInitialFocus(options) | ||
); | ||
|
||
const handleFocus = (event: KeyboardEvent) => { | ||
const currentIndex = internalOptions.findIndex( | ||
option => option.tabIndex === 0 | ||
); | ||
const nextIndex = | ||
event.key === 'ArrowUp' | ||
? getArrowUpIndex(currentIndex) | ||
: getArrowDownIndex(currentIndex, internalOptions); | ||
|
||
if (currentIndex !== nextIndex) { | ||
setInternalOptions(prevOptions => | ||
prevOptions.map((option, index) => { | ||
switch (index) { | ||
case currentIndex: | ||
return { | ||
...option, | ||
tabIndex: -1, | ||
}; | ||
case nextIndex: | ||
return { | ||
...option, | ||
tabIndex: 0, | ||
}; | ||
default: | ||
return option; | ||
} | ||
}) | ||
); | ||
} | ||
}; | ||
|
||
const handleFirstAndLast = (value: number) => { | ||
setInternalOptions(prevOptions => | ||
prevOptions.map((option, index) => { | ||
switch (index) { | ||
case value: | ||
return { | ||
...option, | ||
tabIndex: 0, | ||
}; | ||
default: | ||
return { | ||
...option, | ||
tabIndex: -1, | ||
}; | ||
} | ||
}) | ||
); | ||
}; | ||
|
||
//Need this for Mac users | ||
useOnTwoKeys( | ||
optionListRef, | ||
['ArrowUp', 'ArrowDown'], | ||
'Meta', | ||
(event: KeyboardEvent) => | ||
event.key === 'ArrowUp' | ||
? handleFirstAndLast(0) | ||
: handleFirstAndLast(internalOptions.length - 1) | ||
); | ||
|
||
useOnKey(optionListRef, ['ArrowDown', 'ArrowUp'], (event: KeyboardEvent) => { | ||
handleFocus(event); | ||
}); | ||
|
||
useOnKey(optionListRef, ['Home', 'End'], (event: KeyboardEvent) => | ||
event.key === 'Home' | ||
? handleFirstAndLast(0) | ||
: handleFirstAndLast(internalOptions.length - 1) | ||
); | ||
|
||
return { | ||
optionListRef, | ||
options: internalOptions, | ||
setOptions: setInternalOptions, | ||
onFocusOption, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export type SetInitialFocusFn<Option, A11yOption> = ( | ||
options: Option[] | ||
) => A11yOption[]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './nested-list.hooks'; | ||
export * from './nested-list.model'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { | ||
mapFlatOptionsToNestedListOptions, | ||
mapNestedListOptionsToFlatOptions, | ||
} from './nested-list.mappers'; | ||
import { NestedOption } from '../common.model'; | ||
import { useA11yNested } from '../nested.hooks'; | ||
import { useA11yList } from '../list'; | ||
|
||
export const useA11yNestedList = <Option extends NestedOption<Option>>( | ||
options: Option[] | ||
) => { | ||
const flatOptions = mapNestedListOptionsToFlatOptions(options); | ||
|
||
const { | ||
optionListRef, | ||
options: internalOptions, | ||
setOptions, | ||
onFocusOption, | ||
} = useA11yList(flatOptions); | ||
|
||
useA11yNested(optionListRef, internalOptions, setOptions); | ||
|
||
return { | ||
optionListRef, | ||
options: mapFlatOptionsToNestedListOptions(internalOptions), | ||
setOptions, | ||
onFocusOption, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { FlatOption, NestedOption } from '../common.model'; | ||
import { A11yNestedListOption } from './nested-list.model'; | ||
|
||
export const mapNestedListOptionsToFlatOptions = < | ||
Option extends NestedOption<Option>, | ||
>( | ||
options: Option[], | ||
parentId?: string | ||
): FlatOption<Option>[] => { | ||
return options.reduce<FlatOption<Option>[]>((acc, o) => { | ||
const { children, ...option } = o; | ||
const flatOption: FlatOption<Option> = { | ||
...option, | ||
parentId, | ||
}; | ||
return [ | ||
...acc, | ||
flatOption, | ||
...(children | ||
? mapNestedListOptionsToFlatOptions(children, option.id) | ||
: []), | ||
]; | ||
}, []); | ||
}; | ||
|
||
export const mapFlatOptionsToNestedListOptions = < | ||
Option extends NestedOption<Option>, | ||
>( | ||
flatOptions: A11yNestedListOption<FlatOption<Option>>[] | ||
): A11yNestedListOption<Option>[] => { | ||
const map = new Map<string, any>(); | ||
flatOptions.forEach(flatOption => { | ||
const { parentId, tabIndex, id, ...option } = flatOption; | ||
map.set(id, { ...option, id, tabIndex, children: undefined }); | ||
}); | ||
|
||
const rootIds = new Set(map.keys()); | ||
|
||
flatOptions.forEach(flatOption => { | ||
const { parentId, id } = flatOption; | ||
const parent = map.get(parentId!); | ||
const child = map.get(id); | ||
if (parent && child) { | ||
if (parent.children === undefined) { | ||
parent.children = []; | ||
} | ||
parent.children.push(child); | ||
rootIds.delete(id); | ||
} | ||
}); | ||
|
||
return Array.from(rootIds).map(id => map.get(id)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { NestedOption } from '../common.model'; | ||
|
||
export type A11yNestedListOption<Option extends NestedOption<Option>> = Omit< | ||
Option, | ||
'children' | ||
> & { | ||
tabIndex: number; | ||
children?: A11yNestedListOption<Option>[]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './nested-select.hooks'; | ||
export * from './nested-select.model'; | ||
export * from './nested-select.mappers'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { useA11ySelect } from '../select'; | ||
import { useA11yNested } from '../nested.hooks'; | ||
import { NestedOption, FlatOption } from '../common.model'; | ||
import { | ||
mapNestedSelectOptionsToFlatOptions, | ||
mapFlatOptionsToNestedSelectOptions, | ||
} from './nested-select.mappers'; | ||
|
||
export const useA11yNestedSelect = <Option extends NestedOption<Option>>( | ||
options: Option[], | ||
getOptionId: <Key extends keyof FlatOption<Option>>( | ||
option: FlatOption<Option> | ||
) => FlatOption<Option>[Key], | ||
initialOption?: Option, | ||
onChangeOption?: (option: FlatOption<Option> | undefined) => void | ||
) => { | ||
const flatOptions = mapNestedSelectOptionsToFlatOptions(options); | ||
|
||
const { | ||
optionListRef, | ||
buttonRef, | ||
veilRef, | ||
isOpen, | ||
setIsOpen, | ||
options: internalOptions, | ||
setOptions, | ||
selectedOption, | ||
setSelectedOption, | ||
onFocusOption, | ||
} = useA11ySelect(flatOptions, getOptionId, initialOption, onChangeOption); | ||
|
||
useA11yNested(optionListRef, internalOptions, setOptions); | ||
|
||
return { | ||
optionListRef, | ||
buttonRef, | ||
veilRef, | ||
options: mapFlatOptionsToNestedSelectOptions(internalOptions), | ||
setOptions, | ||
isOpen, | ||
setIsOpen, | ||
selectedOption, | ||
setSelectedOption, | ||
onFocusOption, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { NestedOption, FlatOption } from '../common.model'; | ||
import { A11yNestedSelectOption } from './nested-select.model'; | ||
import { A11ySelectOption } from '../select'; | ||
|
||
export const mapNestedSelectOptionsToFlatOptions = < | ||
Option extends NestedOption<Option>, | ||
>( | ||
options: Option[], | ||
parentId?: string | ||
): FlatOption<Option>[] => { | ||
return options.reduce<FlatOption<Option>[]>((acc, o) => { | ||
const { children, ...option } = o; | ||
const flatOption: FlatOption<Option> = { | ||
...option, | ||
parentId, | ||
isSelectable: !Array.isArray(children) || children.length === 0, | ||
}; | ||
return [ | ||
...acc, | ||
flatOption, | ||
...(children | ||
? mapNestedSelectOptionsToFlatOptions(children, option.id) | ||
: []), | ||
]; | ||
}, []); | ||
}; | ||
|
||
export const mapFlatOptionsToNestedSelectOptions = < | ||
Option extends NestedOption<Option>, | ||
>( | ||
flatOptions: A11ySelectOption<FlatOption<Option>>[] | ||
): A11yNestedSelectOption<Option>[] => { | ||
const map = new Map<string, any>(); | ||
flatOptions.forEach(flatOption => { | ||
const { parentId, tabIndex, id, isSelectable, ...option } = flatOption; | ||
map.set(id, { ...option, id, tabIndex, isSelectable, children: undefined }); | ||
}); | ||
|
||
const rootIds = new Set(map.keys()); | ||
|
||
flatOptions.forEach(flatOption => { | ||
const { parentId, id } = flatOption; | ||
const parent = map.get(parentId!); | ||
const child = map.get(id); | ||
if (parent && child) { | ||
if (parent.children === undefined) { | ||
parent.children = []; | ||
} | ||
parent.children.push(child); | ||
rootIds.delete(id); | ||
} | ||
}); | ||
|
||
return Array.from(rootIds).map(id => map.get(id)); | ||
}; |
Oops, something went wrong.