Skip to content

Commit 96789d8

Browse files
committed
chore(v0): add useAccessibilityBehavior() & useAccessibilitySlotProps() hooks
1 parent fad45ed commit 96789d8

File tree

10 files changed

+529
-33
lines changed

10 files changed

+529
-33
lines changed
Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import {
2-
Accessibility,
3-
AccessibilityAttributes,
4-
AccessibilityAttributesBySlot,
5-
AccessibilityDefinition,
6-
} from '@fluentui/accessibility';
1+
import type { Accessibility, AccessibilityDefinition } from '@fluentui/accessibility';
72

83
import { getKeyDownHandlers } from './getKeyDownHandlers';
9-
import { AccessibilityActionHandlers, ReactAccessibilityBehavior } from './types';
4+
import type { AccessibilityActionHandlers, ReactAccessibilityBehavior } from './types';
105

11-
const emptyBehavior: ReactAccessibilityBehavior = {
6+
export const emptyBehavior: ReactAccessibilityBehavior = {
127
attributes: {},
138
keyHandlers: {},
9+
rtl: false,
1410
};
1511

1612
export const getAccessibility = <Props extends Record<string, any>>(
17-
displayName: string,
1813
behavior: Accessibility<Props>,
1914
behaviorProps: Props,
2015
isRtlEnabled: boolean,
@@ -38,28 +33,10 @@ export const getAccessibility = <Props extends Record<string, any>>(
3833
};
3934
}
4035

41-
if (process.env.NODE_ENV !== 'production') {
42-
// For the non-production builds we enable the runtime accessibility attributes validator.
43-
// We're adding the data-aa-class attribute which is being consumed by the validator, the
44-
// schema is located in @fluentui/ability-attributes package.
45-
if (definition.attributes) {
46-
Object.keys(definition.attributes).forEach(slotName => {
47-
const validatorName =
48-
(definition.attributes as AccessibilityAttributesBySlot)[slotName]['data-aa-class'] ||
49-
`${displayName}${slotName === 'root' ? '' : `__${slotName}`}`;
50-
51-
if (!(definition.attributes as AccessibilityAttributesBySlot)[slotName]) {
52-
(definition.attributes as AccessibilityAttributesBySlot)[slotName] = {} as AccessibilityAttributes;
53-
}
54-
55-
(definition.attributes as AccessibilityAttributesBySlot)[slotName]['data-aa-class'] = validatorName;
56-
});
57-
}
58-
}
59-
6036
return {
6137
...emptyBehavior,
6238
...definition,
6339
keyHandlers,
40+
rtl: isRtlEnabled,
6441
};
6542
};

packages/fluentui/react-bindings/src/accessibility/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import * as React from 'react';
55
* Accessibility types for React implementation.
66
*/
77

8-
export interface ReactAccessibilityBehavior extends AccessibilityDefinition {
8+
export interface ReactAccessibilityBehavior extends Pick<AccessibilityDefinition, 'focusZone' | 'childBehaviors'> {
99
attributes: AccessibilityAttributesBySlot;
1010
keyHandlers: AccessibilityKeyHandlers;
11+
rtl: boolean;
1112
}
1213

1314
export type AccessibilityKeyHandlers = {
@@ -22,4 +23,4 @@ export type AccessibilityActionHandlers = {
2223
[actionName: string]: KeyboardEventHandler;
2324
};
2425

25-
export type KeyboardEventHandler = (event: React.KeyboardEvent) => void;
26+
export type KeyboardEventHandler = (event: React.KeyboardEvent, ...args: unknown[]) => void;

packages/fluentui/react-bindings/src/hooks/useAccessibility.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ export const useAccessibility = <Props extends {}>(
3333
behavior: Accessibility<Props>,
3434
options: UseAccessibilityOptions<Props> = {},
3535
) => {
36-
const { actionHandlers, debugName = 'Undefined', mapPropsToBehavior = () => ({}), rtl = false } = options;
37-
38-
const definition = getAccessibility(debugName, behavior, mapPropsToBehavior(), rtl, actionHandlers);
36+
const { actionHandlers, mapPropsToBehavior = () => ({}), rtl = false } = options;
37+
const definition = getAccessibility(behavior, mapPropsToBehavior(), rtl, actionHandlers);
3938

4039
const latestDefinition = React.useRef<ReactAccessibilityBehavior>();
4140
const slotHandlers = React.useRef<Record<string, KeyboardEventHandler>>({});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Accessibility } from '@fluentui/accessibility';
2+
3+
import { getAccessibility } from '../accessibility/getAccessibility';
4+
import type { AccessibilityActionHandlers, ReactAccessibilityBehavior } from '../accessibility/types';
5+
6+
type UseAccessibilityOptions<Props> = {
7+
actionHandlers?: AccessibilityActionHandlers;
8+
behaviorProps?: Props;
9+
rtl: boolean;
10+
};
11+
12+
export const useAccessibilityBehavior = <Props extends {}>(
13+
behavior: Accessibility<Props>,
14+
options: UseAccessibilityOptions<Props>,
15+
): ReactAccessibilityBehavior => {
16+
const { actionHandlers, behaviorProps, rtl = false } = options;
17+
18+
// No need to memoize this as behaviors return:
19+
// - flat props per slots - references don't matter there
20+
// - action handlers - useAccessibilitySlotProps() uses useEventCallback() that does not "care" about callback references
21+
return getAccessibility(behavior, behaviorProps ?? {}, rtl, actionHandlers);
22+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { AccessibilityAttributes } from '@fluentui/accessibility';
2+
import * as React from 'react';
3+
4+
import type { KeyboardEventHandler, ReactAccessibilityBehavior } from '../accessibility/types';
5+
import { useEventCallback } from './useEventCallback';
6+
7+
type UserProps = {
8+
onKeyDown?: KeyboardEventHandler;
9+
};
10+
11+
type MergedProps<SlotProps extends Record<string, unknown>> = SlotProps & Partial<AccessibilityAttributes> & UserProps;
12+
13+
export const useAccessibilitySlotProps = <SlotProps extends Record<string, unknown> & UserProps>(
14+
definition: ReactAccessibilityBehavior,
15+
slotName: string,
16+
slotProps: SlotProps,
17+
): MergedProps<SlotProps> => {
18+
const childBehavior = definition.childBehaviors ? definition.childBehaviors[slotName] : undefined;
19+
20+
const handleKeyDown = useEventCallback((e: React.KeyboardEvent, ...args: unknown[]) => {
21+
const accessibilityHandler = definition.keyHandlers[slotName]?.onKeyDown;
22+
const userHandler = slotProps.onKeyDown;
23+
24+
if (accessibilityHandler) accessibilityHandler(e);
25+
if (userHandler) userHandler(e, ...args);
26+
});
27+
28+
return {
29+
...(childBehavior && { accessibility: childBehavior }),
30+
...definition.attributes[slotName],
31+
...slotProps,
32+
onKeyDown: handleKeyDown,
33+
};
34+
};

packages/fluentui/react-bindings/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export * from './FocusZone/FocusZone.types';
1212
export * from './FocusZone/focusUtilities';
1313

1414
export { useAccessibility } from './hooks/useAccessibility';
15+
export { useAccessibilityBehavior } from './hooks/useAccessibilityBehavior';
16+
export { useAccessibilitySlotProps } from './hooks/useAccessibilitySlotProps';
1517
export { useCallbackRef } from './hooks/useCallbackRef';
1618
export { useControllableState } from './hooks/useControllableState';
1719
export { useDeepMemo } from './hooks/useDeepMemo';
@@ -36,6 +38,7 @@ export { childrenExist } from './utils/childrenExist';
3638
export { getElementType } from './utils/getElementType';
3739
export { getUnhandledProps } from './utils/getUnhandledProps';
3840
export { mergeVariablesOverrides } from './utils/mergeVariablesOverrides';
41+
export { wrapWithFocusZone } from './utils/wrapWithFocusZone';
3942

4043
export * from './context';
4144

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
3+
import type { ReactAccessibilityBehavior } from '../accessibility/types';
4+
import { FocusZone } from '../FocusZone/FocusZone';
5+
6+
export function wrapWithFocusZone(
7+
definition: ReactAccessibilityBehavior,
8+
element: React.ReactElement & React.RefAttributes<any>,
9+
): React.ReactElement {
10+
if (definition.focusZone) {
11+
let child: React.ReactElement & React.RefAttributes<any> = element;
12+
13+
if (process.env.NODE_ENV !== 'production') {
14+
child = React.Children.only(element);
15+
}
16+
17+
return (
18+
<FocusZone
19+
{...definition.focusZone.props}
20+
{...child.props}
21+
as={child.type}
22+
innerRef={child.ref}
23+
isRtl={definition.rtl}
24+
/>
25+
);
26+
}
27+
28+
return element;
29+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Accessibility, keyboardKey } from '@fluentui/accessibility';
2+
import { renderHook } from '@testing-library/react';
3+
import * as React from 'react';
4+
5+
import { useAccessibilityBehavior } from '../../src/hooks/useAccessibilityBehavior';
6+
7+
type TestBehaviorProps = {
8+
disabled: boolean;
9+
};
10+
11+
const testBehavior: Accessibility<TestBehaviorProps> = props => ({
12+
attributes: {
13+
root: {
14+
'aria-disabled': props.disabled,
15+
tabIndex: 1,
16+
},
17+
},
18+
keyActions: {
19+
root: {
20+
click: {
21+
keyCombinations: [{ keyCode: keyboardKey.ArrowDown }],
22+
},
23+
},
24+
},
25+
});
26+
27+
describe('useAccessibilityBehavior', () => {
28+
it('sets attributes', () => {
29+
const onClick = jest.fn();
30+
31+
const { result } = renderHook(() =>
32+
useAccessibilityBehavior(testBehavior, {
33+
behaviorProps: { disabled: true },
34+
actionHandlers: {
35+
click: ev => {
36+
onClick(ev);
37+
},
38+
},
39+
rtl: false,
40+
}),
41+
);
42+
43+
expect(result.current.attributes).toEqual({
44+
root: {
45+
'aria-disabled': true,
46+
tabIndex: 1,
47+
},
48+
});
49+
expect(result.current.childBehaviors).toBeUndefined();
50+
expect(result.current.focusZone).toBeUndefined();
51+
expect(result.current.keyHandlers).toEqual({
52+
root: {
53+
onKeyDown: expect.any(Function),
54+
},
55+
});
56+
57+
// Calls on `onClick`
58+
result.current.keyHandlers.root?.onKeyDown?.({ keyCode: keyboardKey.ArrowDown } as unknown as React.KeyboardEvent);
59+
// Does nothing
60+
result.current.keyHandlers.root?.onKeyDown?.({ keyCode: keyboardKey.ArrowUp } as unknown as React.KeyboardEvent);
61+
62+
expect(onClick).toHaveBeenCalledWith({ keyCode: keyboardKey.ArrowDown });
63+
expect(onClick).toHaveBeenCalledTimes(1);
64+
});
65+
});

0 commit comments

Comments
 (0)