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

feat(bindings): add useAccessibility hook #1980

Merged
merged 29 commits into from
Jan 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
70f3229
feat(bindings): add useAccessibility()
layershifter Sep 24, 2019
2562d9c
add behaviors to project tests
layershifter Oct 4, 2019
7256331
revert props naming
layershifter Oct 7, 2019
bfcd28a
remove/restore comments
layershifter Oct 7, 2019
ccdddf7
improve naming, use `Object.keys()`
layershifter Oct 7, 2019
30204f7
fix typings issues
layershifter Oct 7, 2019
216f612
fix wrapInFZ
layershifter Oct 7, 2019
bf75789
add tests, fix existing
layershifter Oct 7, 2019
0bf26ef
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Oct 7, 2019
503c398
fix typings and broken UT
layershifter Oct 7, 2019
aad6c30
fix typings and broken UTs
layershifter Oct 7, 2019
5ab164d
add docs
layershifter Oct 7, 2019
5b16291
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Oct 15, 2019
cee5273
Merge branch 'master' into feat/use-accessibility
layershifter Oct 17, 2019
6ec395b
Update packages/react-bindings/README.md
layershifter Oct 18, 2019
041d029
Merge branch 'master' into feat/use-accessibility
layershifter Oct 18, 2019
304c798
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Oct 21, 2019
7271dce
merge with master
layershifter Oct 21, 2019
0d7b870
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Dec 9, 2019
61f2417
fix prettier issues
layershifter Dec 9, 2019
6bac269
fix prettier issues
layershifter Dec 9, 2019
9c5b695
fix handler
layershifter Dec 9, 2019
f36d101
fix imports
layershifter Dec 9, 2019
54af40a
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Dec 12, 2019
054a9d1
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Jan 3, 2020
b334875
fix import
layershifter Jan 3, 2020
ddcc938
Merge branches 'feat/use-accessibility' and 'master' of https://githu…
layershifter Jan 7, 2020
29a7611
fix duplicate constant
layershifter Jan 8, 2020
7f82e1b
Merge branch 'master' of https://github.com/stardust-ui/react into fe…
layershifter Jan 8, 2020
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
4 changes: 4 additions & 0 deletions packages/accessibility/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export interface AccessibilityAttributes
extends AriaWidgetAttributes,
AriaRelationshipAttributes,
ElementStateAttributes {
// Is used in @fluentui/ability-attributes for accessibility validations.
// Do not set it manually and do not rely on it in production
'data-aa-class'?: string

role?: AriaRole
tabIndex?: number
id?: string
Expand Down
63 changes: 61 additions & 2 deletions packages/react-bindings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ A set of reusable components and hooks to build component libraries and UI kits.

- [Installation](#installation)
- [Hooks](#hooks)
- [`useAccesibility()`](#useaccesibility)
- [Usage](#usage)
- [`useStateManager()`](#usestatemanager)
- [Usage](#usage)
- [Usage](#usage-1)
- [Reference](#reference)

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

# Hooks

## `useAccesibility()`

A React hook that provides bindings for accessibility behaviors.

#### Usage

The example below assumes a component called `<Image>` will be used this way:

```tsx
const imageBehavior: Accessibility<{ disabled: boolean }> = props => ({
attributes: {
root: {
"aria-disabled": props.disabled,
tabIndex: -1
},
img: {
role: "presentation"
}
},
keyActions: {
root: {
click: {
keyCombinations: [{ keyCode: 13 /* equals Enter */ }]
}
}
}
});

type ImageProps = {
disabled?: boolean;
onClick?: (
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>
) => void;
src: string;
};

const Image: React.FC<ImageProps> = props => {
const { disabled, onClick, src, ...rest } = props;
const getA11Props = useAccessibility(imageBehavior, {
mapPropsToBehavior: () => ({
disabled
}),
actionHandlers: {
click: (e: React.KeyboardEvent<HTMLDivElement>) => {
if (onClick) onClick(e);
}
}
});

return (
<div {...getA11Props("root", { onClick, ...rest })}>
<img {...getA11Props("img", { src })} />
</div>
);
};
```

## `useStateManager()`

A React hook that provides bindings for state managers.

### Usage

The examples below assume a component called `<Input>` will be used this way:
The example below assumes a component called `<Input>` will be used this way:

```tsx
type InputProps = {
Expand Down
61 changes: 61 additions & 0 deletions packages/react-bindings/src/accessibility/getAccessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
Accessibility,
AccessibilityAttributes,
AccessibilityAttributesBySlot,
AccessibilityDefinition,
} from '@fluentui/accessibility'

import getKeyDownHandlers from './getKeyDownHandlers'
import { AccessibilityActionHandlers, ReactAccessibilityBehavior } from './types'

const emptyBehavior: ReactAccessibilityBehavior = {
attributes: {},
keyHandlers: {},
}

const getAccessibility = <Props extends Record<string, any>>(
displayName: string,
behavior: Accessibility<Props>,
behaviorProps: Props,
isRtlEnabled: boolean,
actionHandlers?: AccessibilityActionHandlers,
): ReactAccessibilityBehavior => {
if (behavior === null || behavior === undefined) {
return emptyBehavior
}

const definition: AccessibilityDefinition = behavior(behaviorProps)
const keyHandlers =
actionHandlers && definition.keyActions
? getKeyDownHandlers(actionHandlers, definition.keyActions, isRtlEnabled)
: {}

if (process.env.NODE_ENV !== 'production') {
// For the non-production builds we enable the runtime accessibility attributes validator.
// We're adding the data-aa-class attribute which is being consumed by the validator, the
// schema is located in @stardust-ui/ability-attributes package.
if (definition.attributes) {
Object.keys(definition.attributes).forEach(slotName => {
const validatorName = `${displayName}${slotName === 'root' ? '' : `__${slotName}`}`

if (!(definition.attributes as AccessibilityAttributesBySlot)[slotName]) {
;(definition.attributes as AccessibilityAttributesBySlot)[
slotName
] = {} as AccessibilityAttributes
}

;(definition.attributes as AccessibilityAttributesBySlot)[slotName][
'data-aa-class'
] = validatorName
})
}
}

return {
...emptyBehavior,
...definition,
keyHandlers,
}
}

export default getAccessibility
76 changes: 76 additions & 0 deletions packages/react-bindings/src/accessibility/getKeyDownHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { KeyActions } from '@fluentui/accessibility'
// @ts-ignore
import * as keyboardKey from 'keyboard-key'
import * as React from 'react'

import shouldHandleOnKeys from './shouldHandleOnKeys'
import { AccessibilityActionHandlers, AccessibilityKeyHandlers } from './types'

const rtlKeyMap = {
[keyboardKey.ArrowRight]: keyboardKey.ArrowLeft,
[keyboardKey.ArrowLeft]: keyboardKey.ArrowRight,
}

/**
* Assigns onKeyDown handler to the slot element, based on Component's actions
* and keys mappings defined in Accessibility behavior
* @param {AccessibilityActionHandlers} componentActionHandlers Actions handlers defined in a component.
* @param {KeyActions} behaviorActions Mappings of actions and keys defined in Accessibility behavior.
* @param {boolean} isRtlEnabled Indicates if Left and Right arrow keys should be swapped in RTL mode.
*/
const getKeyDownHandlers = (
componentActionHandlers: AccessibilityActionHandlers,
behaviorActions: KeyActions,
isRtlEnabled?: boolean,
): AccessibilityKeyHandlers => {
const slotKeyHandlers: AccessibilityKeyHandlers = {}

if (!componentActionHandlers || !behaviorActions) {
return slotKeyHandlers
}

const componentHandlerNames = Object.keys(componentActionHandlers)

Object.keys(behaviorActions).forEach(slotName => {
const behaviorSlotActions = behaviorActions[slotName]
const handledActions = Object.keys(behaviorSlotActions).filter(actionName => {
const slotAction = behaviorSlotActions[actionName]

const actionHasKeyCombinations =
Array.isArray(slotAction.keyCombinations) && slotAction.keyCombinations.length > 0
const actionHandledByComponent = componentHandlerNames.indexOf(actionName) !== -1

Copy link
Member Author

@layershifter layershifter Oct 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored this place to avoid Lodash usage in our core package, tests are passing.

I also renamed variables to improve their meaning.

return actionHasKeyCombinations && actionHandledByComponent
})

if (handledActions.length > 0) {
slotKeyHandlers[slotName] = {
onKeyDown: (event: React.KeyboardEvent) => {
handledActions.forEach(actionName => {
let keyCombinations = behaviorSlotActions[actionName].keyCombinations

if (keyCombinations) {
if (isRtlEnabled) {
keyCombinations = keyCombinations.map(keyCombination => {
const keyToRtlKey = rtlKeyMap[keyCombination.keyCode]
if (keyToRtlKey) {
keyCombination.keyCode = keyToRtlKey
}
return keyCombination
})
}

if (shouldHandleOnKeys(event, keyCombinations)) {
componentActionHandlers[actionName](event)
}
}
})
},
}
}
})

return slotKeyHandlers
}

export default getKeyDownHandlers
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyCombinations } from '@fluentui/accessibility'
// @ts-ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no typings for keyboard-key 😭

import * as keyboardKey from 'keyboard-key'
import * as _ from 'lodash'
import * as React from 'react'

const isKeyModifiersMatch = (modifierValue: boolean, combinationValue?: boolean) => {
Expand All @@ -15,9 +15,8 @@ const shouldHandleOnKeys = (
event: React.KeyboardEvent,
keysCombinations: KeyCombinations[],
): boolean =>
_.some(
keysCombinations,
(keysCombination: KeyCombinations) =>
keysCombinations.some(
keysCombination =>
keysCombination.keyCode === keyboardKey.getCode(event) &&
isKeyModifiersMatch(event.altKey, keysCombination.altKey) &&
isKeyModifiersMatch(event.shiftKey, keysCombination.shiftKey) &&
Expand Down
75 changes: 75 additions & 0 deletions packages/react-bindings/src/hooks/useAccessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Accessibility, AccessibilityAttributesBySlot } from '@fluentui/accessibility'
import * as React from 'react'

import getAccessibility from '../accessibility/getAccessibility'
import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from '../accessibility/types'

type UseAccessibilityOptions<Props> = {
actionHandlers?: AccessibilityActionHandlers
debugName?: string
mapPropsToBehavior?: () => Props
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be Props | () => Props for API simplification? looking at the unit tests I think passing an object there should be sufficient for most use cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you mean, but I want to keep it obvious for now and in sync with useStateManager(). We can improve this part once we will receive feedback for this thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that Props | () => Props maybe make sense, in most of the cases I see this as a simple props mapping.. Anyways, not a blocker :)

rtl?: boolean
}

type MergedProps<SlotProps extends Record<string, any>> = SlotProps &
Partial<AccessibilityAttributesBySlot> & {
onKeyDown?: (e: React.KeyboardEvent, ...args: any[]) => void
}

const mergeProps = <SlotProps extends Record<string, any>>(
slotName: string,
slotProps: SlotProps,
definition: ReactAccessibilityBehavior,
): MergedProps<SlotProps> => {
const finalProps: MergedProps<SlotProps> = {
...definition.attributes[slotName],
...slotProps,
}
const slotHandlers = definition.keyHandlers[slotName]

if (slotHandlers) {
const onKeyDown = (e: React.KeyboardEvent, ...args: any[]) => {
if (slotHandlers && slotHandlers.onKeyDown) {
slotHandlers.onKeyDown(e)
}

if (slotProps.onKeyDown) {
slotProps.onKeyDown(e, ...args)
}
}

finalProps.onKeyDown = onKeyDown
}

return finalProps
}

const useAccessibility = <Props>(
behavior: Accessibility<Props>,
options: UseAccessibilityOptions<Props> = {},
) => {
const {
actionHandlers,
debugName = 'Undefined',
mapPropsToBehavior = () => ({}),
rtl = false,
} = options
const definition = getAccessibility(
debugName,
behavior,
mapPropsToBehavior(),
rtl,
actionHandlers,
)

const latestDefinition = React.useRef<ReactAccessibilityBehavior>(definition)
latestDefinition.current = definition

return React.useCallback(
<SlotProps extends Record<string, any>>(slotName: string, slotProps: SlotProps) =>
mergeProps(slotName, slotProps, latestDefinition.current),
[],
)
}

export default useAccessibility
8 changes: 6 additions & 2 deletions packages/react-bindings/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { default as unstable_useDispatchEffect } from './hooks/useDispatchEffect'
export { default as useStateManager } from './hooks/useStateManager'
export { default as unstable_getAccessibility } from './accessibility/getAccessibility'
export * from './accessibility/types'

export { default as AutoFocusZone } from './FocusZone/AutoFocusZone'
export * from './FocusZone/AutoFocusZone.types'
Expand All @@ -9,6 +9,10 @@ export { default as FocusZone } from './FocusZone/FocusZone'
export * from './FocusZone/FocusZone.types'
export * from './FocusZone/focusUtilities'

export { default as useAccessibility } from './hooks/useAccessibility'
export { default as unstable_useDispatchEffect } from './hooks/useDispatchEffect'
export { default as useStateManager } from './hooks/useStateManager'

export { default as callable } from './utils/callable'
export { default as getElementType } from './utils/getElementType'
export { default as getUnhandledProps } from './utils/getUnhandledProps'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getKeyDownHandlers from 'src/utils/getKeyDownHandlers'
import getKeyDownHandlers from '../../src/accessibility/getKeyDownHandlers'
import * as keyboardKey from 'keyboard-key'

const testKeyCode = keyboardKey.ArrowRight
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import shouldHandleOnKeys from 'src/utils/shouldHandleOnKeys'
import shouldHandleOnKeys from '../../src/accessibility/shouldHandleOnKeys'

const getEventArg = (
keyCode: number,
Expand Down
Loading