diff --git a/CHANGELOG.md b/CHANGELOG.md index cccd59be75..48d9dd4a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Add basic `Radio` component @alinais ([#100](https://github.com/stardust-ui/react/pull/100)) - Add `descriptionColor` to Header @kuzhelov ([#78](https://github.com/stardust-ui/react/pull/78)) +- Add accessibility behavior description @kolaps33 ([#74](https://github.com/stardust-ui/react/pull/74)) ## [v0.3.0](https://github.com/stardust-ui/react/tree/v0.3.0) (2018-08-22) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 5608045c27..1c68efc101 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -8,8 +8,14 @@ import { Accessibility } from '../../lib/accessibility/interfaces' /** * A button. - * @accessibility This is example usage of the accessibility tag. - * This should be replaced with the actual description after the PR is merged + * @accessibility + * Default behavior: ButtonBehavior + * - adds role='button' if element type is other than 'button' + * + * + * Other considerations: + * - for disabled buttons, add 'disabled' attribute so that the state is properly recognized by the screen reader + * - if button includes icon only, textual representation needs to be provided by using 'title', 'aria-label', or 'aria-labelledby' attributes */ class Button extends UIComponent { public static displayName = 'Button' diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 25c1fb6c4f..3f63913e00 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -9,6 +9,11 @@ import HeaderDescription from './HeaderDescription' * @accessibility * Headings communicate the organization of the content on the page. Web browsers, plug-ins, and assistive technologies can use them to provide in-page navigation. * Nest headings by their rank (or level). The most important heading has the rank 1 (

), the least important heading rank 6 (

). Headings with an equal or higher rank start a new section, headings with a lower rank start new subsections that are part of the higher ranked section. + * + * + * Other considerations: + * - when the description property is used in header, readers will narrate both header content and description within the element. + * In addition to that, both will be displayed in the list of headings. */ class Header extends UIComponent { static className = 'ui-header' diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index bf94142b62..1dc173c3dc 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -1,11 +1,18 @@ import * as React from 'react' import * as PropTypes from 'prop-types' import { customPropTypes, UIComponent, createShorthandFactory } from '../../lib' +import { IconBehavior } from '../../lib/accessibility/' import svgIcons from './svgIcons' export type IconXSpacing = 'none' | 'before' | 'after' | 'both' +/** + * @accessibility + * Default behavior: IconBehavior + * - attribute "aria-hidden='true'" is applied on icon + */ + class Icon extends UIComponent { static create: Function @@ -59,9 +66,13 @@ class Icon extends UIComponent { /** Adds space to the before, after or on both sides of the icon, or removes the default space around the icon ('none' value) */ xSpacing: PropTypes.oneOf(['none', 'before', 'after', 'both']), + + /** Accessibility behavior if overriden by the user. */ + accessibility: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), } static handledProps = [ + 'accessibility', 'as', 'bordered', 'circular', @@ -79,17 +90,18 @@ class Icon extends UIComponent { static defaultProps = { as: 'span', size: 'normal', + accessibility: IconBehavior, } - renderFontIcon(ElementType, classes, rest): React.ReactNode { - return + renderFontIcon(ElementType, classes, rest, accessibility): React.ReactNode { + return } - renderSvgIcon(ElementType, classes, rest): React.ReactNode { + renderSvgIcon(ElementType, classes, rest, accessibility): React.ReactNode { const icon = svgIcons[this.props.name] return ( - + {icon && icon.element} @@ -97,10 +109,10 @@ class Icon extends UIComponent { ) } - renderComponent({ ElementType, classes, rest }) { + renderComponent({ ElementType, classes, rest, accessibility }) { return this.props.svg - ? this.renderSvgIcon(ElementType, classes, rest) - : this.renderFontIcon(ElementType, classes, rest) + ? this.renderSvgIcon(ElementType, classes, rest, accessibility) + : this.renderFontIcon(ElementType, classes, rest, accessibility) } } diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx index f2da00d42c..2fa1e97067 100644 --- a/src/components/Image/Image.tsx +++ b/src/components/Image/Image.tsx @@ -7,6 +7,16 @@ import { Accessibility } from '../../lib/accessibility/interfaces' /** * An image is a graphic representation of something. + * @accessibility + * Default behavior: ImageBehavior + * - attribute "aria-hidden='true'" is applied on img element, if there is no 'alt' property provided + * + * If image should be visible to screen readers, textual representation needs to be provided in 'alt' property. + * + * Other considerations: + * - when alt property is empty, then Narrator in scan mode navigates to image and narrates it as empty paragraph + * - when image has role='presentation' then screen readers navigate to the element in scan/virtual mode. To avoid this, the attribute "aria-hidden='true'" is applied by the default image behavior + * - when alt property is used in combination with aria-label, arialabbeledby or title, additional screen readers verification is needed as each screen reader handles this combination differently. */ class Image extends UIComponent { static className = 'ui-image' diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 7fe84c8758..c848ebdd7e 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -15,7 +15,13 @@ import Icon from '../Icon' /** * An Input - * @accessibility This is example usage of the accessibility tag. + * @accessibility + * For good screen reader experience set aria-label or aria-labelledby attribute for input. + * + * + * Other considerations: + * - if input is search, then use "role='search'" + * */ class Input extends AutoControlledComponent { static className = 'ui-input' diff --git a/src/lib/accessibility/Behaviors/Button/ButtonBehavior.ts b/src/lib/accessibility/Behaviors/Button/ButtonBehavior.ts index a8f94bf72a..0ec0d6fcf7 100644 --- a/src/lib/accessibility/Behaviors/Button/ButtonBehavior.ts +++ b/src/lib/accessibility/Behaviors/Button/ButtonBehavior.ts @@ -3,8 +3,7 @@ import { Accessibility } from '../../interfaces' const ButtonBehavior: Accessibility = (props: any) => ({ attributes: { root: { - role: 'button', - 'aria-hidden': false, + role: props.as === 'button' ? undefined : 'button', 'aria-disabled': !!props['disabled'], }, }, diff --git a/src/lib/accessibility/Behaviors/Button/ToggleButtonBehavior.ts b/src/lib/accessibility/Behaviors/Button/ToggleButtonBehavior.ts index eca6533062..cc6982c452 100644 --- a/src/lib/accessibility/Behaviors/Button/ToggleButtonBehavior.ts +++ b/src/lib/accessibility/Behaviors/Button/ToggleButtonBehavior.ts @@ -3,7 +3,7 @@ import { Accessibility } from '../../interfaces' const ToggleButtonBehavior: Accessibility = (props: any) => ({ attributes: { root: { - role: 'button', + role: props.as === 'button' ? undefined : 'button', 'aria-pressed': !!props['active'], 'aria-disabled': !!props['disabled'], }, diff --git a/src/lib/accessibility/Behaviors/Icon/IconBehavior.ts b/src/lib/accessibility/Behaviors/Icon/IconBehavior.ts new file mode 100644 index 0000000000..b24f0c51ba --- /dev/null +++ b/src/lib/accessibility/Behaviors/Icon/IconBehavior.ts @@ -0,0 +1,11 @@ +import { Accessibility } from '../../interfaces' + +const IconBehavior: Accessibility = (props: any) => ({ + attributes: { + root: { + 'aria-hidden': 'true', + }, + }, +}) + +export default IconBehavior diff --git a/src/lib/accessibility/Behaviors/Image/ImageBehavior.ts b/src/lib/accessibility/Behaviors/Image/ImageBehavior.ts index af90948b62..9879b7b367 100644 --- a/src/lib/accessibility/Behaviors/Image/ImageBehavior.ts +++ b/src/lib/accessibility/Behaviors/Image/ImageBehavior.ts @@ -3,7 +3,7 @@ import { Accessibility } from '../../interfaces' const ImageBehavior: Accessibility = (props: any) => ({ attributes: { root: { - role: props['alt'] ? undefined : 'presentation', + 'aria-hidden': props['alt'] ? undefined : 'true', }, }, }) diff --git a/src/lib/accessibility/index.ts b/src/lib/accessibility/index.ts index 111378aad6..25dd9d6168 100644 --- a/src/lib/accessibility/index.ts +++ b/src/lib/accessibility/index.ts @@ -12,3 +12,4 @@ export { default as InputBehavior } from './Behaviors/Input/InputBehavior' export { default as TreeBehavior } from './Behaviors/Tree/TreeBehavior' export { default as TreeItemBehavior } from './Behaviors/Tree/TreeItemBehavior' export { default as GroupBehavior } from './Behaviors/Tree/GroupBehavior' +export { default as IconBehavior } from './Behaviors/Icon/IconBehavior' diff --git a/test/specs/commonTests/handlesAccessibility.tsx b/test/specs/commonTests/handlesAccessibility.tsx index b6450d37a6..1360a0d00d 100644 --- a/test/specs/commonTests/handlesAccessibility.tsx +++ b/test/specs/commonTests/handlesAccessibility.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { getTestingRenderedComponent } from 'test/utils' import { ButtonBehavior, DefaultBehavior } from 'src/lib/accessibility' -const getProp = (renderedComponent, propName, partSelector) => { +export const getRenderedAttribute = (renderedComponent, propName, partSelector) => { const target = partSelector ? renderedComponent.render().find(partSelector) : renderedComponent.render() @@ -24,7 +24,7 @@ const getProp = (renderedComponent, propName, partSelector) => { export default (Component, options: any = {}) => { const { requiredProps = {}, - defaultRootRole = ButtonBehavior, + defaultRootRole = undefined, accessibilityOverride = ButtonBehavior, overriddenRootRole = 'button', partSelector = '', @@ -32,7 +32,7 @@ export default (Component, options: any = {}) => { test('gets default accessibility when no override used', () => { const rendered = getTestingRenderedComponent(Component, ) - const role = getProp(rendered, 'role', partSelector) + const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(defaultRootRole) }) @@ -41,7 +41,7 @@ export default (Component, options: any = {}) => { Component, , ) - const role = getProp(rendered, 'role', partSelector) + const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBeFalsy() }) @@ -52,7 +52,7 @@ export default (Component, options: any = {}) => { Component, , ) - const role = getProp(rendered, 'role', partSelector) + const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(overriddenRootRole) }) @@ -62,7 +62,7 @@ export default (Component, options: any = {}) => { Component, , ) - const role = getProp(rendered, 'role', partSelector) + const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(testRole) }) @@ -72,7 +72,7 @@ export default (Component, options: any = {}) => { Component, , ) - const role = getProp(rendered, 'role', partSelector) + const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(testRole) }) } diff --git a/test/specs/commonTests/index.ts b/test/specs/commonTests/index.ts index 3e0836de89..3a6eea5369 100644 --- a/test/specs/commonTests/index.ts +++ b/test/specs/commonTests/index.ts @@ -5,6 +5,7 @@ export * from './implementsClassNameProps' export { default as implementsCreateMethod } from './implementsCreateMethod' export { default as implementsShorthandProp } from './implementsShorthandProp' -export { default as handlesAccessibility } from './handlesAccessibility' +export { default as handlesAccessibility, getRenderedAttribute } from './handlesAccessibility' + export { default as isConformant } from './isConformant' export { default as rendersChildren } from './rendersChildren' diff --git a/test/specs/components/Button/Button-test.tsx b/test/specs/components/Button/Button-test.tsx index 4d9c9f21e7..4abea8fb2a 100644 --- a/test/specs/components/Button/Button-test.tsx +++ b/test/specs/components/Button/Button-test.tsx @@ -1,7 +1,13 @@ import * as React from 'react' -import { isConformant, handlesAccessibility, implementsShorthandProp } from 'test/specs/commonTests' +import { + isConformant, + handlesAccessibility, + implementsShorthandProp, + getRenderedAttribute, +} from 'test/specs/commonTests' import { getTestingRenderedComponent, mountWithProvider } from 'test/utils' +import { ToggleButtonBehavior } from '../../../../src/lib/accessibility' import Button from 'src/components/Button/Button' import Icon from 'src/components/Icon/Icon' @@ -14,28 +20,109 @@ describe('Button', () => { isConformant(Button) buttonImplementsShorthandProp('icon', Icon, { mapsValueToProp: 'name' }) - handlesAccessibility(Button, { - defaultRootRole: 'button', - accessibilityOverride: MenuBehavior, - overriddenRootRole: 'menu', + describe('accessibility', () => { + describe('button', () => { + handlesAccessibility(Button, { + defaultRootRole: undefined, + accessibilityOverride: MenuBehavior, + overriddenRootRole: 'menu', + }) + }) + + describe('div Button', () => { + handlesAccessibility(Button, { + requiredProps: { as: 'div' }, + defaultRootRole: 'button', + accessibilityOverride: MenuBehavior, + overriddenRootRole: 'menu', + }) + }) + + describe('aria-disabled', () => { + test('is set to true, if disabled attribute is provided', () => { + const renderedComponent = getTestingRenderedComponent(Button,