Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit b407e90

Browse files
feat(bindings): add useAccessibility hook (#1980)
* feat(bindings): add useAccessibility() * add behaviors to project tests * revert props naming * remove/restore comments * improve naming, use `Object.keys()` * fix typings issues * fix wrapInFZ * add tests, fix existing * fix typings and broken UT * fix typings and broken UTs * add docs * Update packages/react-bindings/README.md Co-Authored-By: Marija Najdova <mnajdova@gmail.com> * merge with master * fix prettier issues * fix prettier issues * fix handler * fix imports * fix import * fix duplicate constant Co-authored-by: Marija Najdova <mnajdova@gmail.com>
1 parent 1b4b6b6 commit b407e90

26 files changed

+464
-209
lines changed

packages/accessibility/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ export interface AccessibilityAttributes
141141
extends AriaWidgetAttributes,
142142
AriaRelationshipAttributes,
143143
ElementStateAttributes {
144+
// Is used in @fluentui/ability-attributes for accessibility validations.
145+
// Do not set it manually and do not rely on it in production
146+
'data-aa-class'?: string
147+
144148
role?: AriaRole
145149
tabIndex?: number
146150
id?: string

packages/react-bindings/README.md

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ A set of reusable components and hooks to build component libraries and UI kits.
99

1010
- [Installation](#installation)
1111
- [Hooks](#hooks)
12+
- [`useAccesibility()`](#useaccesibility)
13+
- [Usage](#usage)
1214
- [`useStateManager()`](#usestatemanager)
13-
- [Usage](#usage)
15+
- [Usage](#usage-1)
1416
- [Reference](#reference)
1517

1618
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -29,13 +31,70 @@ yarn add @fluentui/react-bindings
2931

3032
# Hooks
3133

34+
## `useAccesibility()`
35+
36+
A React hook that provides bindings for accessibility behaviors.
37+
38+
#### Usage
39+
40+
The example below assumes a component called `<Image>` will be used this way:
41+
42+
```tsx
43+
const imageBehavior: Accessibility<{ disabled: boolean }> = props => ({
44+
attributes: {
45+
root: {
46+
"aria-disabled": props.disabled,
47+
tabIndex: -1
48+
},
49+
img: {
50+
role: "presentation"
51+
}
52+
},
53+
keyActions: {
54+
root: {
55+
click: {
56+
keyCombinations: [{ keyCode: 13 /* equals Enter */ }]
57+
}
58+
}
59+
}
60+
});
61+
62+
type ImageProps = {
63+
disabled?: boolean;
64+
onClick?: (
65+
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>
66+
) => void;
67+
src: string;
68+
};
69+
70+
const Image: React.FC<ImageProps> = props => {
71+
const { disabled, onClick, src, ...rest } = props;
72+
const getA11Props = useAccessibility(imageBehavior, {
73+
mapPropsToBehavior: () => ({
74+
disabled
75+
}),
76+
actionHandlers: {
77+
click: (e: React.KeyboardEvent<HTMLDivElement>) => {
78+
if (onClick) onClick(e);
79+
}
80+
}
81+
});
82+
83+
return (
84+
<div {...getA11Props("root", { onClick, ...rest })}>
85+
<img {...getA11Props("img", { src })} />
86+
</div>
87+
);
88+
};
89+
```
90+
3291
## `useStateManager()`
3392

3493
A React hook that provides bindings for state managers.
3594

3695
### Usage
3796

38-
The examples below assume a component called `<Input>` will be used this way:
97+
The example below assumes a component called `<Input>` will be used this way:
3998

4099
```tsx
41100
type InputProps = {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
Accessibility,
3+
AccessibilityAttributes,
4+
AccessibilityAttributesBySlot,
5+
AccessibilityDefinition,
6+
} from '@fluentui/accessibility'
7+
8+
import getKeyDownHandlers from './getKeyDownHandlers'
9+
import { AccessibilityActionHandlers, ReactAccessibilityBehavior } from './types'
10+
11+
const emptyBehavior: ReactAccessibilityBehavior = {
12+
attributes: {},
13+
keyHandlers: {},
14+
}
15+
16+
const getAccessibility = <Props extends Record<string, any>>(
17+
displayName: string,
18+
behavior: Accessibility<Props>,
19+
behaviorProps: Props,
20+
isRtlEnabled: boolean,
21+
actionHandlers?: AccessibilityActionHandlers,
22+
): ReactAccessibilityBehavior => {
23+
if (behavior === null || behavior === undefined) {
24+
return emptyBehavior
25+
}
26+
27+
const definition: AccessibilityDefinition = behavior(behaviorProps)
28+
const keyHandlers =
29+
actionHandlers && definition.keyActions
30+
? getKeyDownHandlers(actionHandlers, definition.keyActions, isRtlEnabled)
31+
: {}
32+
33+
if (process.env.NODE_ENV !== 'production') {
34+
// For the non-production builds we enable the runtime accessibility attributes validator.
35+
// We're adding the data-aa-class attribute which is being consumed by the validator, the
36+
// schema is located in @stardust-ui/ability-attributes package.
37+
if (definition.attributes) {
38+
Object.keys(definition.attributes).forEach(slotName => {
39+
const validatorName = `${displayName}${slotName === 'root' ? '' : `__${slotName}`}`
40+
41+
if (!(definition.attributes as AccessibilityAttributesBySlot)[slotName]) {
42+
;(definition.attributes as AccessibilityAttributesBySlot)[
43+
slotName
44+
] = {} as AccessibilityAttributes
45+
}
46+
47+
;(definition.attributes as AccessibilityAttributesBySlot)[slotName][
48+
'data-aa-class'
49+
] = validatorName
50+
})
51+
}
52+
}
53+
54+
return {
55+
...emptyBehavior,
56+
...definition,
57+
keyHandlers,
58+
}
59+
}
60+
61+
export default getAccessibility
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { KeyActions } from '@fluentui/accessibility'
2+
// @ts-ignore
3+
import * as keyboardKey from 'keyboard-key'
4+
import * as React from 'react'
5+
6+
import shouldHandleOnKeys from './shouldHandleOnKeys'
7+
import { AccessibilityActionHandlers, AccessibilityKeyHandlers } from './types'
8+
9+
const rtlKeyMap = {
10+
[keyboardKey.ArrowRight]: keyboardKey.ArrowLeft,
11+
[keyboardKey.ArrowLeft]: keyboardKey.ArrowRight,
12+
}
13+
14+
/**
15+
* Assigns onKeyDown handler to the slot element, based on Component's actions
16+
* and keys mappings defined in Accessibility behavior
17+
* @param {AccessibilityActionHandlers} componentActionHandlers Actions handlers defined in a component.
18+
* @param {KeyActions} behaviorActions Mappings of actions and keys defined in Accessibility behavior.
19+
* @param {boolean} isRtlEnabled Indicates if Left and Right arrow keys should be swapped in RTL mode.
20+
*/
21+
const getKeyDownHandlers = (
22+
componentActionHandlers: AccessibilityActionHandlers,
23+
behaviorActions: KeyActions,
24+
isRtlEnabled?: boolean,
25+
): AccessibilityKeyHandlers => {
26+
const slotKeyHandlers: AccessibilityKeyHandlers = {}
27+
28+
if (!componentActionHandlers || !behaviorActions) {
29+
return slotKeyHandlers
30+
}
31+
32+
const componentHandlerNames = Object.keys(componentActionHandlers)
33+
34+
Object.keys(behaviorActions).forEach(slotName => {
35+
const behaviorSlotActions = behaviorActions[slotName]
36+
const handledActions = Object.keys(behaviorSlotActions).filter(actionName => {
37+
const slotAction = behaviorSlotActions[actionName]
38+
39+
const actionHasKeyCombinations =
40+
Array.isArray(slotAction.keyCombinations) && slotAction.keyCombinations.length > 0
41+
const actionHandledByComponent = componentHandlerNames.indexOf(actionName) !== -1
42+
43+
return actionHasKeyCombinations && actionHandledByComponent
44+
})
45+
46+
if (handledActions.length > 0) {
47+
slotKeyHandlers[slotName] = {
48+
onKeyDown: (event: React.KeyboardEvent) => {
49+
handledActions.forEach(actionName => {
50+
let keyCombinations = behaviorSlotActions[actionName].keyCombinations
51+
52+
if (keyCombinations) {
53+
if (isRtlEnabled) {
54+
keyCombinations = keyCombinations.map(keyCombination => {
55+
const keyToRtlKey = rtlKeyMap[keyCombination.keyCode]
56+
if (keyToRtlKey) {
57+
keyCombination.keyCode = keyToRtlKey
58+
}
59+
return keyCombination
60+
})
61+
}
62+
63+
if (shouldHandleOnKeys(event, keyCombinations)) {
64+
componentActionHandlers[actionName](event)
65+
}
66+
}
67+
})
68+
},
69+
}
70+
}
71+
})
72+
73+
return slotKeyHandlers
74+
}
75+
76+
export default getKeyDownHandlers

packages/react/src/utils/shouldHandleOnKeys.ts renamed to packages/react-bindings/src/accessibility/shouldHandleOnKeys.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { KeyCombinations } from '@fluentui/accessibility'
2+
// @ts-ignore
23
import * as keyboardKey from 'keyboard-key'
3-
import * as _ from 'lodash'
44
import * as React from 'react'
55

66
const isKeyModifiersMatch = (modifierValue: boolean, combinationValue?: boolean) => {
@@ -15,9 +15,8 @@ const shouldHandleOnKeys = (
1515
event: React.KeyboardEvent,
1616
keysCombinations: KeyCombinations[],
1717
): boolean =>
18-
_.some(
19-
keysCombinations,
20-
(keysCombination: KeyCombinations) =>
18+
keysCombinations.some(
19+
keysCombination =>
2120
keysCombination.keyCode === keyboardKey.getCode(event) &&
2221
isKeyModifiersMatch(event.altKey, keysCombination.altKey) &&
2322
isKeyModifiersMatch(event.shiftKey, keysCombination.shiftKey) &&
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Accessibility, AccessibilityAttributesBySlot } from '@fluentui/accessibility'
2+
import * as React from 'react'
3+
4+
import getAccessibility from '../accessibility/getAccessibility'
5+
import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from '../accessibility/types'
6+
7+
type UseAccessibilityOptions<Props> = {
8+
actionHandlers?: AccessibilityActionHandlers
9+
debugName?: string
10+
mapPropsToBehavior?: () => Props
11+
rtl?: boolean
12+
}
13+
14+
type MergedProps<SlotProps extends Record<string, any>> = SlotProps &
15+
Partial<AccessibilityAttributesBySlot> & {
16+
onKeyDown?: (e: React.KeyboardEvent, ...args: any[]) => void
17+
}
18+
19+
const mergeProps = <SlotProps extends Record<string, any>>(
20+
slotName: string,
21+
slotProps: SlotProps,
22+
definition: ReactAccessibilityBehavior,
23+
): MergedProps<SlotProps> => {
24+
const finalProps: MergedProps<SlotProps> = {
25+
...definition.attributes[slotName],
26+
...slotProps,
27+
}
28+
const slotHandlers = definition.keyHandlers[slotName]
29+
30+
if (slotHandlers) {
31+
const onKeyDown = (e: React.KeyboardEvent, ...args: any[]) => {
32+
if (slotHandlers && slotHandlers.onKeyDown) {
33+
slotHandlers.onKeyDown(e)
34+
}
35+
36+
if (slotProps.onKeyDown) {
37+
slotProps.onKeyDown(e, ...args)
38+
}
39+
}
40+
41+
finalProps.onKeyDown = onKeyDown
42+
}
43+
44+
return finalProps
45+
}
46+
47+
const useAccessibility = <Props>(
48+
behavior: Accessibility<Props>,
49+
options: UseAccessibilityOptions<Props> = {},
50+
) => {
51+
const {
52+
actionHandlers,
53+
debugName = 'Undefined',
54+
mapPropsToBehavior = () => ({}),
55+
rtl = false,
56+
} = options
57+
const definition = getAccessibility(
58+
debugName,
59+
behavior,
60+
mapPropsToBehavior(),
61+
rtl,
62+
actionHandlers,
63+
)
64+
65+
const latestDefinition = React.useRef<ReactAccessibilityBehavior>(definition)
66+
latestDefinition.current = definition
67+
68+
return React.useCallback(
69+
<SlotProps extends Record<string, any>>(slotName: string, slotProps: SlotProps) =>
70+
mergeProps(slotName, slotProps, latestDefinition.current),
71+
[],
72+
)
73+
}
74+
75+
export default useAccessibility

packages/react-bindings/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export { default as unstable_useDispatchEffect } from './hooks/useDispatchEffect'
2-
export { default as useStateManager } from './hooks/useStateManager'
1+
export { default as unstable_getAccessibility } from './accessibility/getAccessibility'
2+
export * from './accessibility/types'
33

44
export { default as AutoFocusZone } from './FocusZone/AutoFocusZone'
55
export * from './FocusZone/AutoFocusZone.types'
@@ -9,6 +9,10 @@ export { default as FocusZone } from './FocusZone/FocusZone'
99
export * from './FocusZone/FocusZone.types'
1010
export * from './FocusZone/focusUtilities'
1111

12+
export { default as useAccessibility } from './hooks/useAccessibility'
13+
export { default as unstable_useDispatchEffect } from './hooks/useDispatchEffect'
14+
export { default as useStateManager } from './hooks/useStateManager'
15+
1216
export { default as callable } from './utils/callable'
1317
export { default as getElementType } from './utils/getElementType'
1418
export { default as getUnhandledProps } from './utils/getUnhandledProps'

packages/react/test/specs/utils/getKeyDownHandlers-test.ts renamed to packages/react-bindings/test/accesibility/getKeyDownHandlers-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import getKeyDownHandlers from 'src/utils/getKeyDownHandlers'
1+
import getKeyDownHandlers from '../../src/accessibility/getKeyDownHandlers'
22
import * as keyboardKey from 'keyboard-key'
33

44
const testKeyCode = keyboardKey.ArrowRight

packages/react/test/specs/utils/shouldHandleOnKeys-test.ts renamed to packages/react-bindings/test/accesibility/shouldHandleOnKeys-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import shouldHandleOnKeys from 'src/utils/shouldHandleOnKeys'
1+
import shouldHandleOnKeys from '../../src/accessibility/shouldHandleOnKeys'
22

33
const getEventArg = (
44
keyCode: number,

0 commit comments

Comments
 (0)