diff --git a/packages/accessibility/src/types.ts b/packages/accessibility/src/types.ts index 6ae0ae6568..6b92678cb7 100644 --- a/packages/accessibility/src/types.ts +++ b/packages/accessibility/src/types.ts @@ -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 diff --git a/packages/react-bindings/README.md b/packages/react-bindings/README.md index ead1507a94..35d331127f 100644 --- a/packages/react-bindings/README.md +++ b/packages/react-bindings/README.md @@ -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) @@ -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 `` 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 | React.KeyboardEvent + ) => void; + src: string; +}; + +const Image: React.FC = props => { + const { disabled, onClick, src, ...rest } = props; + const getA11Props = useAccessibility(imageBehavior, { + mapPropsToBehavior: () => ({ + disabled + }), + actionHandlers: { + click: (e: React.KeyboardEvent) => { + if (onClick) onClick(e); + } + } + }); + + return ( +
+ +
+ ); +}; +``` + ## `useStateManager()` A React hook that provides bindings for state managers. ### Usage -The examples below assume a component called `` will be used this way: +The example below assumes a component called `` will be used this way: ```tsx type InputProps = { diff --git a/packages/react-bindings/src/accessibility/getAccessibility.ts b/packages/react-bindings/src/accessibility/getAccessibility.ts new file mode 100644 index 0000000000..76906a98ef --- /dev/null +++ b/packages/react-bindings/src/accessibility/getAccessibility.ts @@ -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 = >( + displayName: string, + behavior: Accessibility, + 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 diff --git a/packages/react-bindings/src/accessibility/getKeyDownHandlers.ts b/packages/react-bindings/src/accessibility/getKeyDownHandlers.ts new file mode 100644 index 0000000000..b315fc5a4e --- /dev/null +++ b/packages/react-bindings/src/accessibility/getKeyDownHandlers.ts @@ -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 diff --git a/packages/react/src/utils/shouldHandleOnKeys.ts b/packages/react-bindings/src/accessibility/shouldHandleOnKeys.ts similarity index 88% rename from packages/react/src/utils/shouldHandleOnKeys.ts rename to packages/react-bindings/src/accessibility/shouldHandleOnKeys.ts index e2bb9d714a..8bc577314d 100644 --- a/packages/react/src/utils/shouldHandleOnKeys.ts +++ b/packages/react-bindings/src/accessibility/shouldHandleOnKeys.ts @@ -1,6 +1,6 @@ import { KeyCombinations } from '@fluentui/accessibility' +// @ts-ignore 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) && diff --git a/packages/react/src/utils/accessibility/reactTypes.ts b/packages/react-bindings/src/accessibility/types.ts similarity index 100% rename from packages/react/src/utils/accessibility/reactTypes.ts rename to packages/react-bindings/src/accessibility/types.ts diff --git a/packages/react-bindings/src/hooks/useAccessibility.ts b/packages/react-bindings/src/hooks/useAccessibility.ts new file mode 100644 index 0000000000..3115d95dde --- /dev/null +++ b/packages/react-bindings/src/hooks/useAccessibility.ts @@ -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 = { + actionHandlers?: AccessibilityActionHandlers + debugName?: string + mapPropsToBehavior?: () => Props + rtl?: boolean +} + +type MergedProps> = SlotProps & + Partial & { + onKeyDown?: (e: React.KeyboardEvent, ...args: any[]) => void + } + +const mergeProps = >( + slotName: string, + slotProps: SlotProps, + definition: ReactAccessibilityBehavior, +): MergedProps => { + const finalProps: MergedProps = { + ...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 = ( + behavior: Accessibility, + options: UseAccessibilityOptions = {}, +) => { + const { + actionHandlers, + debugName = 'Undefined', + mapPropsToBehavior = () => ({}), + rtl = false, + } = options + const definition = getAccessibility( + debugName, + behavior, + mapPropsToBehavior(), + rtl, + actionHandlers, + ) + + const latestDefinition = React.useRef(definition) + latestDefinition.current = definition + + return React.useCallback( + >(slotName: string, slotProps: SlotProps) => + mergeProps(slotName, slotProps, latestDefinition.current), + [], + ) +} + +export default useAccessibility diff --git a/packages/react-bindings/src/index.ts b/packages/react-bindings/src/index.ts index 986ab73f30..ecdae50957 100644 --- a/packages/react-bindings/src/index.ts +++ b/packages/react-bindings/src/index.ts @@ -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' @@ -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' diff --git a/packages/react/test/specs/utils/getKeyDownHandlers-test.ts b/packages/react-bindings/test/accesibility/getKeyDownHandlers-test.ts similarity index 99% rename from packages/react/test/specs/utils/getKeyDownHandlers-test.ts rename to packages/react-bindings/test/accesibility/getKeyDownHandlers-test.ts index ee13c2e2da..01573daeaf 100644 --- a/packages/react/test/specs/utils/getKeyDownHandlers-test.ts +++ b/packages/react-bindings/test/accesibility/getKeyDownHandlers-test.ts @@ -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 diff --git a/packages/react/test/specs/utils/shouldHandleOnKeys-test.ts b/packages/react-bindings/test/accesibility/shouldHandleOnKeys-test.ts similarity index 96% rename from packages/react/test/specs/utils/shouldHandleOnKeys-test.ts rename to packages/react-bindings/test/accesibility/shouldHandleOnKeys-test.ts index 0b62ee8eaf..b7e5271025 100644 --- a/packages/react/test/specs/utils/shouldHandleOnKeys-test.ts +++ b/packages/react-bindings/test/accesibility/shouldHandleOnKeys-test.ts @@ -1,4 +1,4 @@ -import shouldHandleOnKeys from 'src/utils/shouldHandleOnKeys' +import shouldHandleOnKeys from '../../src/accessibility/shouldHandleOnKeys' const getEventArg = ( keyCode: number, diff --git a/packages/react-bindings/test/hooks/useAccessibility-test.tsx b/packages/react-bindings/test/hooks/useAccessibility-test.tsx new file mode 100644 index 0000000000..7fd0d34f0e --- /dev/null +++ b/packages/react-bindings/test/hooks/useAccessibility-test.tsx @@ -0,0 +1,118 @@ +import { Accessibility } from '@fluentui/accessibility' +import { useAccessibility } from '@fluentui/react-bindings' +import { shallow } from 'enzyme' +// @ts-ignore +import * as keyboardKey from 'keyboard-key' +import * as React from 'react' + +type TestBehaviorProps = { + disabled: boolean +} + +const testBehavior: Accessibility = props => ({ + attributes: { + root: { + 'aria-disabled': props.disabled, + tabIndex: 1, + }, + img: { + 'aria-label': 'Pixel', + role: 'presentation', + }, + }, + keyActions: { + root: { + click: { + keyCombinations: [{ keyCode: keyboardKey.ArrowDown }], + }, + }, + }, +}) + +type TestComponentProps = { + disabled?: boolean + onClick?: (e: React.KeyboardEvent, slotName: string) => void + onKeyDown?: React.KeyboardEventHandler +} & React.HTMLAttributes + +const TestComponent: React.FunctionComponent = props => { + const { disabled, onClick, onKeyDown, ...rest } = props + const getA11Props = useAccessibility(testBehavior, { + mapPropsToBehavior: () => ({ + disabled, + }), + actionHandlers: { + click: (e: React.KeyboardEvent) => { + if (onClick) onClick(e, 'root') + }, + }, + }) + + return ( +
+ +
+ ) +} + +describe('useAccessibility', () => { + it('sets attributes', () => { + const wrapper = shallow() + + expect(wrapper.find('div').prop('tabIndex')).toBe(1) + expect(wrapper.find('img').prop('role')).toBe('presentation') + }) + + it('attributes can be conditional', () => { + expect( + shallow() + .find('div') + .prop('aria-disabled'), + ).toBe(true) + expect( + shallow() + .find('div') + .prop('aria-disabled'), + ).toBe(false) + }) + + it('attributes can be overridden', () => { + expect( + shallow() + .find('div') + .prop('tabIndex'), + ).toBe(-1) + }) + + it('adds event handlers', () => { + const onKeyDown = jest.fn() + const onClick = jest.fn() + const wrapper = shallow() + + wrapper + .find('div') + .simulate('click') + .simulate('keydown', { + keyCode: keyboardKey.ArrowDown, + }) + + expect(onKeyDown).toBeCalledTimes(1) + expect(onKeyDown).toBeCalledWith( + expect.objectContaining({ + keyCode: keyboardKey.ArrowDown, + }), + ) + + expect(onClick).toBeCalledTimes(1) + expect(onClick).toBeCalledWith( + expect.objectContaining({ + keyCode: keyboardKey.ArrowDown, + }), + 'root', + ) + }) +}) diff --git a/packages/react/src/components/Carousel/CarouselNavigation.tsx b/packages/react/src/components/Carousel/CarouselNavigation.tsx index 3c509f0c1b..420737fd20 100644 --- a/packages/react/src/components/Carousel/CarouselNavigation.tsx +++ b/packages/react/src/components/Carousel/CarouselNavigation.tsx @@ -1,3 +1,4 @@ +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import * as PropTypes from 'prop-types' import * as React from 'react' import * as customPropTypes from '@fluentui/react-proptypes' @@ -22,7 +23,6 @@ import { } from '../../types' import CarouselNavigationItem, { CarouselNavigationItemProps } from './CarouselNavigationItem' import { ComponentVariablesObject } from '../../themes/types' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' import { mergeComponentVariables } from '../../utils/mergeThemes' export interface CarouselNavigationProps extends UIComponentProps, ChildrenComponentProps { diff --git a/packages/react/src/components/Menu/Menu.tsx b/packages/react/src/components/Menu/Menu.tsx index 91fb0aa271..3c6c896786 100644 --- a/packages/react/src/components/Menu/Menu.tsx +++ b/packages/react/src/components/Menu/Menu.tsx @@ -1,4 +1,5 @@ import { Accessibility, menuBehavior } from '@fluentui/accessibility' +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import * as _ from 'lodash' import * as PropTypes from 'prop-types' @@ -18,7 +19,6 @@ import { import { mergeComponentVariables } from '../../utils/mergeThemes' import MenuItem, { MenuItemProps } from './MenuItem' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' import { ComponentVariablesObject, ComponentSlotStylesPrepared } from '../../themes/types' import { WithAsProp, diff --git a/packages/react/src/components/Popup/Popup.tsx b/packages/react/src/components/Popup/Popup.tsx index 5173efe421..e36d55fad5 100644 --- a/packages/react/src/components/Popup/Popup.tsx +++ b/packages/react/src/components/Popup/Popup.tsx @@ -1,5 +1,9 @@ import { Accessibility, popupBehavior } from '@fluentui/accessibility' -import { AutoFocusZoneProps, FocusTrapZoneProps } from '@fluentui/react-bindings' +import { + ReactAccessibilityBehavior, + AutoFocusZoneProps, + FocusTrapZoneProps, +} from '@fluentui/react-bindings' import { EventListener } from '@fluentui/react-component-event-listener' import { NodeRef, Unstable_NestingAuto } from '@fluentui/react-component-nesting-registry' import { handleRef, toRefObject, Ref } from '@fluentui/react-component-ref' @@ -32,7 +36,6 @@ import { } from '../../utils/positioner' import PopupContent, { PopupContentProps } from './PopupContent' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' import { createShorthandFactory, ShorthandFactory } from '../../utils/factories' import createReferenceFromContextClick from './createReferenceFromContextClick' import isRightClick from '../../utils/isRightClick' diff --git a/packages/react/src/components/Portal/Portal.tsx b/packages/react/src/components/Portal/Portal.tsx index 440632a977..aea6271c87 100644 --- a/packages/react/src/components/Portal/Portal.tsx +++ b/packages/react/src/components/Portal/Portal.tsx @@ -1,5 +1,9 @@ import { AccessibilityAttributes } from '@fluentui/accessibility' -import { FocusTrapZone, FocusTrapZoneProps } from '@fluentui/react-bindings' +import { + AccessibilityHandlerProps, + FocusTrapZone, + FocusTrapZoneProps, +} from '@fluentui/react-bindings' import { EventListener } from '@fluentui/react-component-event-listener' import { handleRef, Ref, toRefObject } from '@fluentui/react-component-ref' import * as customPropTypes from '@fluentui/react-proptypes' @@ -17,7 +21,6 @@ import { rtlTextContainer, } from '../../utils' import PortalInner from './PortalInner' -import { AccessibilityHandlerProps } from '../../utils/accessibility/reactTypes' export type TriggerAccessibility = { attributes?: AccessibilityAttributes diff --git a/packages/react/src/components/Table/Table.tsx b/packages/react/src/components/Table/Table.tsx index f77eda2931..3f97ccfa40 100644 --- a/packages/react/src/components/Table/Table.tsx +++ b/packages/react/src/components/Table/Table.tsx @@ -1,3 +1,5 @@ +import { Accessibility, tableBehavior } from '@fluentui/accessibility' +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import * as PropTypes from 'prop-types' import * as _ from 'lodash' @@ -16,8 +18,6 @@ import { mergeComponentVariables } from '../../utils/mergeThemes' import TableRow, { TableRowProps } from './TableRow' import TableCell from './TableCell' import { WithAsProp, ShorthandCollection, ShorthandValue } from '../../types' -import { Accessibility, tableBehavior } from '@fluentui/accessibility' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' export interface TableSlotClassNames { header: string diff --git a/packages/react/src/components/Table/TableRow.tsx b/packages/react/src/components/Table/TableRow.tsx index b467543080..d6d2374a58 100644 --- a/packages/react/src/components/Table/TableRow.tsx +++ b/packages/react/src/components/Table/TableRow.tsx @@ -1,3 +1,4 @@ +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import { Ref } from '@fluentui/react-component-ref' import * as PropTypes from 'prop-types' @@ -18,7 +19,6 @@ import { ShorthandCollection, WithAsProp } from '../../types' import { Accessibility, tableRowBehavior } from '@fluentui/accessibility' import { ComponentVariablesObject } from '../../themes/types' import { mergeComponentVariables } from '../../utils/mergeThemes' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' export interface TableRowProps extends UIComponentProps { /** diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx index e1ecccd9bd..8adc5f7fd8 100644 --- a/packages/react/src/components/Tooltip/Tooltip.tsx +++ b/packages/react/src/components/Tooltip/Tooltip.tsx @@ -1,3 +1,5 @@ +import { Accessibility, tooltipAsLabelBehavior } from '@fluentui/accessibility' +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import { toRefObject, Ref } from '@fluentui/react-component-ref' import * as customPropTypes from '@fluentui/react-proptypes' import * as React from 'react' @@ -28,8 +30,6 @@ import { PopperChildrenProps, } from '../../utils/positioner' import TooltipContent, { TooltipContentProps } from './TooltipContent' -import { Accessibility, tooltipAsLabelBehavior } from '@fluentui/accessibility' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' import PortalInner from '../Portal/PortalInner' export interface TooltipSlotClassNames { diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index ce99f7415c..97a763aa34 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -1,5 +1,5 @@ import { Accessibility, treeBehavior } from '@fluentui/accessibility' -import { getNextElement } from '@fluentui/react-bindings' +import { ReactAccessibilityBehavior, getNextElement } from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import * as _ from 'lodash' import * as PropTypes from 'prop-types' @@ -7,6 +7,7 @@ import * as React from 'react' import { Ref } from '@fluentui/react-component-ref' import TreeItem, { TreeItemProps } from './TreeItem' +import TreeTitle, { TreeTitleProps } from './TreeTitle' import { childrenExist, commonPropTypes, @@ -26,8 +27,6 @@ import { ShorthandValue, } from '../../types' import { hasSubtree, removeItemAtIndex } from './utils' -import TreeTitle, { TreeTitleProps } from './TreeTitle' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' export interface TreeSlotClassNames { item: string diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index ee5b1847a1..6109d0b1ec 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -1,4 +1,5 @@ import { Accessibility, treeItemBehavior } from '@fluentui/accessibility' +import { ReactAccessibilityBehavior } from '@fluentui/react-bindings' import * as customPropTypes from '@fluentui/react-proptypes' import * as _ from 'lodash' import * as PropTypes from 'prop-types' @@ -26,7 +27,6 @@ import { ShorthandCollection, } from '../../types' import { hasSubtree } from './utils' -import { ReactAccessibilityBehavior } from '../../utils/accessibility/reactTypes' export interface TreeItemSlotClassNames { title: string diff --git a/packages/react/src/utils/UIComponent.tsx b/packages/react/src/utils/UIComponent.tsx index bcd5dfd014..168fc73c29 100644 --- a/packages/react/src/utils/UIComponent.tsx +++ b/packages/react/src/utils/UIComponent.tsx @@ -1,10 +1,10 @@ +import { AccessibilityActionHandlers } from '@fluentui/react-bindings' import * as React from 'react' import * as _ from 'lodash' // @ts-ignore We have this export in package, but it is not present in typings import { ThemeContext } from 'react-fela' import renderComponent, { RenderResultConfig } from './renderComponent' -import { AccessibilityActionHandlers } from './accessibility/reactTypes' // TODO @Bugaa92: deprecated by createComponent.tsx class UIComponent extends React.Component { diff --git a/packages/react/src/utils/applyAccessibilityKeyHandlers.ts b/packages/react/src/utils/applyAccessibilityKeyHandlers.ts index 4b138434ce..86129ecff2 100644 --- a/packages/react/src/utils/applyAccessibilityKeyHandlers.ts +++ b/packages/react/src/utils/applyAccessibilityKeyHandlers.ts @@ -1,8 +1,8 @@ +import { AccessibilityHandlerProps, KeyboardEventHandler } from '@fluentui/react-bindings' import * as _ from 'lodash' import * as React from 'react' import { Props, ShorthandValue } from '../types' -import { AccessibilityHandlerProps, KeyboardEventHandler } from './accessibility/reactTypes' // Makes sure that 'onKeyDown' is correctly overriden on the slots. // It should be applied after 'unhandledProps' because they can contain 'onKeyDown' from user and is handled by UTs in isConformant() diff --git a/packages/react/src/utils/createComponent.tsx b/packages/react/src/utils/createComponent.tsx index 2055a081ea..1966a2a01a 100644 --- a/packages/react/src/utils/createComponent.tsx +++ b/packages/react/src/utils/createComponent.tsx @@ -1,9 +1,9 @@ -import createComponentInternal, { CreateComponentReturnType } from './createComponentInternal' +import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from '@fluentui/react-bindings' import * as React from 'react' import * as _ from 'lodash' +import createComponentInternal, { CreateComponentReturnType } from './createComponentInternal' import { ComponentSlotClasses, ComponentSlotStylesPrepared } from '../themes/types' -import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from './accessibility/reactTypes' import { ObjectOf } from '../types' export interface CreateComponentRenderConfig { diff --git a/packages/react/src/utils/createComponentInternal.ts b/packages/react/src/utils/createComponentInternal.ts index 37017d7519..52577814f2 100644 --- a/packages/react/src/utils/createComponentInternal.ts +++ b/packages/react/src/utils/createComponentInternal.ts @@ -1,10 +1,10 @@ +import { AccessibilityActionHandlers } from '@fluentui/react-bindings' import * as React from 'react' import * as _ from 'lodash' // @ts-ignore import { ThemeContext } from 'react-fela' import renderComponent, { RenderResultConfig } from './renderComponent' -import { AccessibilityActionHandlers } from './accessibility/reactTypes' import { createShorthandFactory, ShorthandFactory } from './factories' import { ObjectOf, ProviderContextPrepared } from '../types' diff --git a/packages/react/src/utils/getKeyDownHandlers.ts b/packages/react/src/utils/getKeyDownHandlers.ts deleted file mode 100644 index 86a403d4b3..0000000000 --- a/packages/react/src/utils/getKeyDownHandlers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { KeyActions } from '@fluentui/accessibility' -import * as _ from 'lodash' -import * as keyboardKey from 'keyboard-key' -import * as React from 'react' - -import shouldHandleOnKeys from './shouldHandleOnKeys' -import { AccessibilityActionHandlers, AccessibilityKeyHandlers } from './accessibility/reactTypes' - -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 componentActionHandlers - Actions handlers defined in a component. - * @param behaviorKeyActions - Mappings of actions and keys defined in Accessibility behavior. - * @param isRtlEnabled - Indicates if Left and Right arrow keys should be swapped in RTL mode. - */ -const getKeyDownHandlers = ( - componentActionHandlers: AccessibilityActionHandlers, - behaviorKeyActions: KeyActions, - isRtlEnabled?: boolean, -): AccessibilityKeyHandlers => { - const keyHandlers = {} - - if (!componentActionHandlers || !behaviorKeyActions) return keyHandlers - - for (const componentPart in behaviorKeyActions) { - const componentPartKeyAction = _.pickBy( - behaviorKeyActions[componentPart], - actions => !_.isEmpty(actions.keyCombinations), - ) - const handledActions = _.intersection( - _.keys(componentPartKeyAction), - _.keys(componentActionHandlers), - ) - if (!handledActions.length) continue - - keyHandlers[componentPart] = { - onKeyDown: (event: React.KeyboardEvent) => { - handledActions.forEach(actionName => { - let keyCombinations = componentPartKeyAction[actionName].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 keyHandlers -} - -export default getKeyDownHandlers diff --git a/packages/react/src/utils/renderComponent.tsx b/packages/react/src/utils/renderComponent.tsx index b7bda3bbd9..518d2b8573 100644 --- a/packages/react/src/utils/renderComponent.tsx +++ b/packages/react/src/utils/renderComponent.tsx @@ -1,16 +1,14 @@ +import { FocusZoneMode } from '@fluentui/accessibility' import { - AccessibilityDefinition, - FocusZoneMode, - FocusZoneDefinition, - Accessibility, -} from '@fluentui/accessibility' -import { + AccessibilityActionHandlers, callable, FocusZone, FocusZoneProps, FOCUSZONE_WRAP_ATTRIBUTE, getElementType, getUnhandledProps, + ReactAccessibilityBehavior, + unstable_getAccessibility as getAccessibility, } from '@fluentui/react-bindings' import cx from 'classnames' import * as React from 'react' @@ -27,8 +25,6 @@ import { ThemePrepared, } from '../themes/types' import { Props, ProviderContextPrepared } from '../types' -import { ReactAccessibilityBehavior, AccessibilityActionHandlers } from './accessibility/reactTypes' -import getKeyDownHandlers from './getKeyDownHandlers' import { emptyTheme, mergeComponentStyles, mergeComponentVariables } from './mergeThemes' import createAnimationStyles from './createAnimationStyles' import { isEnabled as isDebugEnabled } from './debug/debugEnabled' @@ -61,101 +57,6 @@ export interface RenderConfig

{ saveDebug: (debug: DebugData | null) => void } -const emptyBehavior: ReactAccessibilityBehavior = { - attributes: {}, - keyHandlers: {}, -} - -const getAccessibility = ( - displayName: string, - props: State & PropsWithVarsAndStyles & { accessibility?: Accessibility }, - actionHandlers: AccessibilityActionHandlers, - isRtlEnabled: boolean, -): ReactAccessibilityBehavior => { - const { accessibility } = props - - if (_.isNil(accessibility)) { - return emptyBehavior - } - - const definition: AccessibilityDefinition = accessibility(props) - const keyHandlers = 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 @fluentui/ability-attributes package. - if (definition.attributes) { - const slotNames = Object.keys(definition.attributes) - slotNames.forEach(slotName => { - if (!definition.attributes[slotName]) { - definition.attributes[slotName] = {} - } - - const slotAttributes = definition.attributes[slotName] - if (!('data-aa-class' in slotAttributes)) { - definition.attributes[slotName]['data-aa-class'] = `${displayName}${ - slotName === 'root' ? '' : `__${slotName}` - }` - } - }) - } - } - - return { - ...emptyBehavior, - ...definition, - keyHandlers, - } -} - -/** - * This function provides compile-time type checking for the following: - * - if FocusZone implements FocusZone interface, - * - if FocusZone properties extend FocusZoneProps, and - * - if the passed properties extend FocusZoneProps. - * - * Should the FocusZone implementation change at any time, this function should provide a compile-time guarantee - * that the new implementation is backwards compatible with the old implementation. - */ -function wrapInGenericFocusZone< - COMPONENT_PROPS extends FocusZoneProps, - PROPS extends COMPONENT_PROPS, - COMPONENT extends FocusZone & React.Component ->( - FocusZone: { new (...args: any[]): COMPONENT }, - props: PROPS | undefined, - children: React.ReactNode, -) { - props[FOCUSZONE_WRAP_ATTRIBUTE] = true - return {children} -} - -const renderWithFocusZone =

( - render: RenderComponentCallback

, - focusZoneDefinition: FocusZoneDefinition, - config: RenderResultConfig

, -): any => { - if (focusZoneDefinition.mode === FocusZoneMode.Wrap) { - return wrapInGenericFocusZone( - FocusZone, - { - ...focusZoneDefinition.props, - isRtl: config.rtl, - }, - render(config), - ) - } - if (focusZoneDefinition.mode === FocusZoneMode.Embed) { - const originalElementType = config.ElementType - config.ElementType = FocusZone as any - config.unhandledProps = { ...config.unhandledProps, ...focusZoneDefinition.props } - config.unhandledProps.as = originalElementType - config.unhandledProps.isRtl = config.rtl - } - return render(config) -} - const renderComponent =

( config: RenderConfig

, context?: ProviderContextPrepared, @@ -217,9 +118,10 @@ const renderComponent =

( const accessibility: ReactAccessibilityBehavior = getAccessibility( displayName, + props.accessibility, stateAndProps, - actionHandlers, rtl, + actionHandlers, ) const unhandledProps = getUnhandledProps(handledProps, props) @@ -261,13 +163,7 @@ const renderComponent =

( rtl, theme, } - - let result - if (accessibility.focusZone) { - result = renderWithFocusZone(render, accessibility.focusZone, resolvedConfig) - } else { - result = render(resolvedConfig) - } + let wrapInFocusZone: (element: React.ReactElement) => React.ReactElement = element => element // conditionally add sources for evaluating debug information to component if (process.env.NODE_ENV !== 'production' && isDebugEnabled) { @@ -321,7 +217,32 @@ const renderComponent =

( } } - return result + if (accessibility.focusZone && accessibility.focusZone.mode === FocusZoneMode.Wrap) { + wrapInFocusZone = element => + React.createElement( + FocusZone, + { + [FOCUSZONE_WRAP_ATTRIBUTE]: true, + ...accessibility.focusZone.props, + isRtl: rtl, + } as FocusZoneProps & { [FOCUSZONE_WRAP_ATTRIBUTE]: boolean }, + element, + ) + } + + if (accessibility.focusZone && accessibility.focusZone.mode === FocusZoneMode.Embed) { + const originalElementType = resolvedConfig.ElementType + + resolvedConfig.ElementType = FocusZone as any + resolvedConfig.unhandledProps = { + ...resolvedConfig.unhandledProps, + ...accessibility.focusZone.props, + } + resolvedConfig.unhandledProps.as = originalElementType + resolvedConfig.unhandledProps.isRtl = resolvedConfig.rtl + } + + return wrapInFocusZone(render(resolvedConfig)) } export default renderComponent