-
Notifications
You must be signed in to change notification settings - Fork 53
feat(bindings): add useAccessibility hook #1980
Changes from all commits
70f3229
2562d9c
7256331
bfcd28a
ccdddf7
30204f7
216f612
bf75789
0bf26ef
503c398
aad6c30
5ab164d
5b16291
cee5273
6ec395b
041d029
304c798
7271dce
0d7b870
61f2417
6bac269
9c5b695
f36d101
54af40a
054a9d1
b334875
ddcc938
29a7611
7f82e1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are no typings for |
||
import * as keyboardKey from 'keyboard-key' | ||
import * as _ from 'lodash' | ||
import * as React from 'react' | ||
|
||
const isKeyModifiersMatch = (modifierValue: boolean, combinationValue?: boolean) => { | ||
|
@@ -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) && | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can this be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that |
||
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 |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.