Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const ToolbarExampleShorthand = () => {
items={[
{
key: 'bold',
kind: 'toggle',
active: isBold,
icon: { name: 'bold', outline: true },
onClick: () => {
Expand All @@ -62,6 +63,7 @@ const ToolbarExampleShorthand = () => {
},
{
key: 'italic',
kind: 'toggle',
active: isItalic,
icon: { name: 'italic', outline: true },
onClick: () => {
Expand All @@ -70,6 +72,7 @@ const ToolbarExampleShorthand = () => {
},
{
key: 'underline',
kind: 'toggle',
active: isUnderline,
icon: { name: 'underline', outline: true },
onClick: () => {
Expand All @@ -78,6 +81,7 @@ const ToolbarExampleShorthand = () => {
},
{
key: 'strike',
kind: 'toggle',
active: isStrike,
disabled: true,
icon: { name: 'strike', outline: true },
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ChildrenComponentProps,
commonPropTypes,
rtlTextContainer,
applyAccessibilityKeyHandlers,
} from '../../lib'
import Icon from '../Icon/Icon'
import Box from '../Box/Box'
Expand Down Expand Up @@ -114,6 +115,13 @@ class Button extends UIComponent<WithAsProp<ButtonProps>, ButtonState> {
isFromKeyboard: false,
}

actionHandlers = {
performClick: event => {
event.preventDefault()
this.handleClick(event)
},
}

renderComponent({
ElementType,
classes,
Expand All @@ -132,6 +140,7 @@ class Button extends UIComponent<WithAsProp<ButtonProps>, ButtonState> {
onClick={this.handleClick}
onFocus={this.handleFocus}
{...accessibility.attributes.root}
{...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
{...rtlTextContainer.getAttributes({ forElements: [children] })}
{...unhandledProps}
>
Expand Down
15 changes: 10 additions & 5 deletions packages/react/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,7 +31,7 @@ export interface ToolbarProps
ColorComponentProps {
/**
* Accessibility behavior if overridden by the user.
* @default defaultBehavior
* @default toolbarBehavior
*/
accessibility?: Accessibility

Expand All @@ -48,11 +48,11 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>, 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
Expand All @@ -73,6 +73,11 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>, 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 })
}
Expand Down
14 changes: 12 additions & 2 deletions packages/react/src/components/Toolbar/ToolbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
commonPropTypes,
childrenExist,
isFromKeyboard,
applyAccessibilityKeyHandlers,
} from '../../lib'
import {
ComponentEventHandler,
Expand All @@ -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'
Expand All @@ -32,6 +33,7 @@ export interface ToolbarItemProps
ContentComponentProps {
/**
* Accessibility behavior if overridden by the user.
* @default buttonBehavior
*/
accessibility?: Accessibility

Expand Down Expand Up @@ -106,14 +108,22 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>, 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 }) {
const { icon, children, disabled, popup } = this.props
const renderedItem = (
<ElementType
{...accessibility.attributes.root}
{...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
{...unhandledProps}
disabled={disabled}
className={classes.root}
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ export { default as TooltipContent } from './components/Tooltip/TooltipContent'
//
// Accessibility
//
export {
default as toggleButtonBehavior,
} from './lib/accessibility/Behaviors/Button/toggleButtonBehavior'
export { default as menuBehavior } from './lib/accessibility/Behaviors/Menu/menuBehavior'
export { default as menuItemBehavior } from './lib/accessibility/Behaviors/Menu/menuItemBehavior'
export {
Expand All @@ -189,6 +192,7 @@ export {
export {
default as menuItemAsToolbarButtonBehavior,
} from './lib/accessibility/Behaviors/Toolbar/menuItemAsToolbarButtonBehavior'
export { default as toolbarBehavior } from './lib/accessibility/Behaviors/Toolbar/toolbarBehavior'
export {
default as radioGroupBehavior,
} from './lib/accessibility/Behaviors/Radio/radioGroupBehavior'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { Accessibility } from '../../types'
import * as keyboardKey from 'keyboard-key'

/**
* @specification
* 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-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 buttonBehavior: Accessibility<ButtonBehaviorProps> = 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ToggleButtonBehaviorProps> = props => ({
attributes: {
root: {
role: props.as === 'button' ? undefined : 'button',
'aria-disabled': props.disabled,
'aria-pressed': !!props.active,
},
},
})

const toggleButtonBehavior: Accessibility<ToggleButtonBehaviorProps> = props => {
const behaviorData = buttonBehavior(props)
behaviorData.attributes.root = {
...behaviorData.attributes.root,
'aria-pressed': !!props['active'],
}

return behaviorData
}

export default toggleButtonBehavior

Expand Down
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions packages/react/src/lib/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions packages/react/test/specs/behaviors/behavior-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
accordionContentBehavior,
chatBehavior,
chatMessageBehavior,
toolbarBehavior,
} from 'src/lib/accessibility'
import { TestHelper } from './testHelper'
import definitions from './testDefinitions'
Expand Down Expand Up @@ -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)
21 changes: 21 additions & 0 deletions packages/react/test/specs/behaviors/testDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 **********************
*/
Expand Down