diff --git a/docs/src/examples/components/Toolbar/Types/ToolbarExampleEditor.shorthand.tsx b/docs/src/examples/components/Toolbar/Types/ToolbarExampleEditor.shorthand.tsx index 4a94fab34d..02690ae115 100644 --- a/docs/src/examples/components/Toolbar/Types/ToolbarExampleEditor.shorthand.tsx +++ b/docs/src/examples/components/Toolbar/Types/ToolbarExampleEditor.shorthand.tsx @@ -54,6 +54,7 @@ const ToolbarExampleShorthand = () => { items={[ { key: 'bold', + kind: 'toggle', active: isBold, icon: { name: 'bold', outline: true }, onClick: () => { @@ -62,6 +63,7 @@ const ToolbarExampleShorthand = () => { }, { key: 'italic', + kind: 'toggle', active: isItalic, icon: { name: 'italic', outline: true }, onClick: () => { @@ -70,6 +72,7 @@ const ToolbarExampleShorthand = () => { }, { key: 'underline', + kind: 'toggle', active: isUnderline, icon: { name: 'underline', outline: true }, onClick: () => { @@ -78,6 +81,7 @@ const ToolbarExampleShorthand = () => { }, { key: 'strike', + kind: 'toggle', active: isStrike, disabled: true, icon: { name: 'strike', outline: true }, diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx index 5cbe1b89c7..c7d09f6ca1 100644 --- a/packages/react/src/components/Button/Button.tsx +++ b/packages/react/src/components/Button/Button.tsx @@ -13,6 +13,7 @@ import { ChildrenComponentProps, commonPropTypes, rtlTextContainer, + applyAccessibilityKeyHandlers, } from '../../lib' import Icon from '../Icon/Icon' import Box from '../Box/Box' @@ -114,6 +115,13 @@ class Button extends UIComponent, ButtonState> { isFromKeyboard: false, } + actionHandlers = { + performClick: event => { + event.preventDefault() + this.handleClick(event) + }, + } + renderComponent({ ElementType, classes, @@ -132,6 +140,7 @@ class Button extends UIComponent, ButtonState> { onClick={this.handleClick} onFocus={this.handleFocus} {...accessibility.attributes.root} + {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)} {...rtlTextContainer.getAttributes({ forElements: [children] })} {...unhandledProps} > diff --git a/packages/react/src/components/Toolbar/Toolbar.tsx b/packages/react/src/components/Toolbar/Toolbar.tsx index f597e7f9e9..e30b0e1caa 100644 --- a/packages/react/src/components/Toolbar/Toolbar.tsx +++ b/packages/react/src/components/Toolbar/Toolbar.tsx @@ -15,14 +15,14 @@ import { import { mergeComponentVariables } from '../../lib/mergeThemes' import { Accessibility } from '../../lib/accessibility/types' -import { defaultBehavior } from '../../lib/accessibility' +import { toolbarBehavior, toggleButtonBehavior } from '../../lib/accessibility' import { ShorthandCollection, WithAsProp, withSafeTypeForAs } from '../../types' import ToolbarItem from './ToolbarItem' import ToolbarDivider from './ToolbarDivider' import ToolbarRadioGroup from './ToolbarRadioGroup' -export type ToolbarItemShorthandKinds = 'divider' | 'item' | 'group' +export type ToolbarItemShorthandKinds = 'divider' | 'item' | 'group' | 'toggle' export interface ToolbarProps extends UIComponentProps, @@ -31,7 +31,7 @@ export interface ToolbarProps ColorComponentProps { /** * Accessibility behavior if overridden by the user. - * @default defaultBehavior + * @default toolbarBehavior */ accessibility?: Accessibility @@ -48,11 +48,11 @@ class Toolbar extends UIComponent, any> { static propTypes = { ...commonPropTypes.createCommon(), - items: customPropTypes.collectionShorthandWithKindProp(['divider', 'item', 'group']), + items: customPropTypes.collectionShorthandWithKindProp(['divider', 'item', 'group', 'toggle']), } static defaultProps = { - accessibility: defaultBehavior, + accessibility: toolbarBehavior, } static Item = ToolbarItem @@ -73,6 +73,11 @@ class Toolbar extends UIComponent, any> { return ToolbarDivider.create(item, { overrideProps: itemOverridesFn }) case 'group': return ToolbarRadioGroup.create(item, { overrideProps: itemOverridesFn }) + case 'toggle': + return ToolbarItem.create(item, { + defaultProps: { accessibility: toggleButtonBehavior }, + overrideProps: itemOverridesFn, + }) default: return ToolbarItem.create(item, { overrideProps: itemOverridesFn }) } diff --git a/packages/react/src/components/Toolbar/ToolbarItem.tsx b/packages/react/src/components/Toolbar/ToolbarItem.tsx index 82042ba307..790a4082d2 100644 --- a/packages/react/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react/src/components/Toolbar/ToolbarItem.tsx @@ -12,6 +12,7 @@ import { commonPropTypes, childrenExist, isFromKeyboard, + applyAccessibilityKeyHandlers, } from '../../lib' import { ComponentEventHandler, @@ -21,7 +22,7 @@ import { Omit, } from '../../types' import { Accessibility } from '../../lib/accessibility/types' -import { defaultBehavior, popupFocusTrapBehavior } from '../../lib/accessibility' +import { buttonBehavior, popupFocusTrapBehavior } from '../../lib/accessibility' import Icon from '../Icon/Icon' import Popup, { PopupProps } from '../Popup/Popup' @@ -32,6 +33,7 @@ export interface ToolbarItemProps ContentComponentProps { /** * Accessibility behavior if overridden by the user. + * @default buttonBehavior */ accessibility?: Accessibility @@ -106,7 +108,14 @@ class ToolbarItem extends UIComponent, ToolbarItemS static defaultProps = { as: 'button', - accessibility: defaultBehavior as Accessibility, + accessibility: buttonBehavior as Accessibility, + } + + actionHandlers = { + performClick: event => { + event.preventDefault() + this.handleClick(event) + }, } renderComponent({ ElementType, classes, unhandledProps, accessibility }) { @@ -114,6 +123,7 @@ class ToolbarItem extends UIComponent, ToolbarItemS const renderedItem = ( = props => ({ attributes: { root: { role: props.as === 'button' ? undefined : 'button', + tabIndex: props.as === 'button' ? undefined : 0, 'aria-disabled': props.disabled, }, }, + + keyActions: { + root: { + ...(props.as !== 'button' && + props.as !== 'a' && { + performClick: { + keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], + }, + }), + }, + }, }) export default buttonBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Button/toggleButtonBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Button/toggleButtonBehavior.ts index da4d296b11..eea2ec3212 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Button/toggleButtonBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Button/toggleButtonBehavior.ts @@ -1,21 +1,24 @@ import { Accessibility } from '../../types' -import { ButtonBehaviorProps } from './buttonBehavior' +import buttonBehavior, { ButtonBehaviorProps } from './buttonBehavior' /** * @specification - * Adds role='button' if element type is other than 'button'. This allows screen readers to handle the component as a button - * Adds attribute 'aria-pressed=true' based on the property 'active'. This can be overriden by providing 'aria-presssed' property directly to the component. + * Adds role='button' if element type is other than 'button'. This allows screen readers to handle the component as a button. + * Adds attribute 'tabIndex=0' if element type is other than 'button'. + * Adds attribute 'aria-pressed=true' based on the property 'active'. * Adds attribute 'aria-disabled=true' based on the property 'disabled'. This can be overriden by providing 'aria-disabled' property directly to the component. + * Triggers 'performClick' action with 'Enter' or 'Spacebar' on 'root'. */ -const toggleButtonBehavior: Accessibility = props => ({ - attributes: { - root: { - role: props.as === 'button' ? undefined : 'button', - 'aria-disabled': props.disabled, - 'aria-pressed': !!props.active, - }, - }, -}) + +const toggleButtonBehavior: Accessibility = props => { + const behaviorData = buttonBehavior(props) + behaviorData.attributes.root = { + ...behaviorData.attributes.root, + 'aria-pressed': !!props['active'], + } + + return behaviorData +} export default toggleButtonBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Toolbar/toolbarBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Toolbar/toolbarBehavior.ts new file mode 100644 index 0000000000..afaef9fc89 --- /dev/null +++ b/packages/react/src/lib/accessibility/Behaviors/Toolbar/toolbarBehavior.ts @@ -0,0 +1,29 @@ +import { Accessibility, FocusZoneMode } from '../../types' +import { FocusZoneDirection } from '../../FocusZone' + +/** + * @description + * Implements ARIA Toolbar design pattern. + * Child item components need to have toolbarItemBehavior assigned. + * @specification + * Adds role 'toolbar' to 'root' component's part. + * Embeds component into FocusZone. + * Provides arrow key navigation in horizontal direction. + * When component's container element receives focus, focus will be set to the default focusable child element of the component. + */ +const toolbarBehavior: Accessibility = (props: any) => ({ + attributes: { + root: { + role: 'toolbar', + }, + }, + focusZone: { + mode: FocusZoneMode.Embed, + props: { + shouldFocusInnerElementWhenReceivedFocus: true, + direction: FocusZoneDirection.horizontal, + }, + }, +}) + +export default toolbarBehavior diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts index cb2e8d0267..27cb2e2616 100644 --- a/packages/react/src/lib/accessibility/index.ts +++ b/packages/react/src/lib/accessibility/index.ts @@ -24,6 +24,7 @@ export { default as menuAsToolbarBehavior } from './Behaviors/Toolbar/menuAsTool export { default as menuItemAsToolbarButtonBehavior, } from './Behaviors/Toolbar/menuItemAsToolbarButtonBehavior' +export { default as toolbarBehavior } from './Behaviors/Toolbar/toolbarBehavior' export { default as radioGroupBehavior } from './Behaviors/Radio/radioGroupBehavior' export { default as radioGroupItemBehavior } from './Behaviors/Radio/radioGroupItemBehavior' export { default as popupBehavior } from './Behaviors/Popup/popupBehavior' diff --git a/packages/react/test/specs/behaviors/behavior-test.tsx b/packages/react/test/specs/behaviors/behavior-test.tsx index dd31ff0a14..0a03d414ea 100644 --- a/packages/react/test/specs/behaviors/behavior-test.tsx +++ b/packages/react/test/specs/behaviors/behavior-test.tsx @@ -44,6 +44,7 @@ import { accordionContentBehavior, chatBehavior, chatMessageBehavior, + toolbarBehavior, } from 'src/lib/accessibility' import { TestHelper } from './testHelper' import definitions from './testDefinitions' @@ -92,5 +93,6 @@ testHelper.addBehavior('accordionTitleBehavior', accordionTitleBehavior) testHelper.addBehavior('accordionContentBehavior', accordionContentBehavior) testHelper.addBehavior('chatBehavior', chatBehavior) testHelper.addBehavior('chatMessageBehavior', chatMessageBehavior) +testHelper.addBehavior('toolbarBehavior', toolbarBehavior) testHelper.run(behaviorMenuItems) diff --git a/packages/react/test/specs/behaviors/testDefinitions.ts b/packages/react/test/specs/behaviors/testDefinitions.ts index e33253c0ef..721bfba556 100644 --- a/packages/react/test/specs/behaviors/testDefinitions.ts +++ b/packages/react/test/specs/behaviors/testDefinitions.ts @@ -339,6 +339,27 @@ definitions.push({ }, }) +// Example: Adds attribute 'tabIndex=0' if element type is other than 'button'. +definitions.push({ + regexp: /Adds attribute '([\w-]+)=([\w\d]+)' if element type is other than '(\w+)'\./g, + testMethod: (parameters: TestMethod) => { + const [attributeToBeAdded, attributeExpectedValue, as] = parameters.props + const property = {} + const expectedResult = parameters.behavior(property).attributes.root[attributeToBeAdded] + expect(testHelper.convertToMatchingTypeIfApplicable(expectedResult)).toBe( + testHelper.convertToMatchingTypeIfApplicable(attributeExpectedValue), + ) + + const propertyAsButton = { as } + const expectedResultAsButton = parameters.behavior(propertyAsButton).attributes.root[ + attributeToBeAdded + ] + expect(testHelper.convertToMatchingTypeIfApplicable(expectedResultAsButton)).toBe( + testHelper.convertToMatchingTypeIfApplicable(undefined), + ) + }, +}) + /* * ********************** FOCUS ZONE ********************** */