Skip to content

Commit

Permalink
feat(tooltip): Add accessibility enhancements
Browse files Browse the repository at this point in the history
Use aria-labelledby, aria-haspopup to connect the trigger and bubble content for screen readers
  • Loading branch information
lzcabrera committed Oct 3, 2017
1 parent 39893cb commit a69c73e
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 78 deletions.
17 changes: 8 additions & 9 deletions src/components/Input/Input.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { childrenOfType } from 'airbnb-prop-types'
import {childrenOfType} from 'airbnb-prop-types'

import StandaloneIcon from '../Icons/StandaloneIcon/StandaloneIcon'
import Text from '../Typography/Text/Text'
Expand Down Expand Up @@ -58,7 +58,7 @@ class Input extends React.Component {
}

onChange = event => {
const { onChange } = this.props
const {onChange} = this.props

this.setState({
value: event.target.value,
Expand All @@ -70,19 +70,19 @@ class Input extends React.Component {
}

onFocus = event => {
const { onFocus } = this.props
const {onFocus} = this.props

this.setState({ focus: true })
this.setState({focus: true})

if (onFocus) {
onFocus(event)
}
}

onBlur = event => {
const { onBlur } = this.props
const {onBlur} = this.props

this.setState({ focus: false })
this.setState({focus: false})

if (onBlur) {
onBlur(event)
Expand Down Expand Up @@ -136,12 +136,11 @@ class Input extends React.Component {
}

render() {
const { type, label, feedback, error, helper, tooltip, ...rest } = this.props
const {type, label, feedback, error, helper, tooltip, ...rest} = this.props

const inputId = generateId(rest.id, rest.name, label)
const helperId = helper && inputId.postfix('helper')
const errorId = error && inputId.postfix('error-message')
const tooltipId = tooltip && tooltip.props.id

const wrapperClassName = getWrapperClassName(feedback, this.state.focus, rest.disabled)
const labelClassNames = joinClassNames(styles.resetLabel, styles.label)
Expand Down Expand Up @@ -190,7 +189,7 @@ class Input extends React.Component {
onFocus={this.onFocus}
onBlur={this.onBlur}
aria-invalid={feedback === 'error' ? 'true' : 'false'}
aria-describedby={errorId || helperId || tooltipId || undefined} // TODO: merge helperId and TooltipId text if both are present
aria-describedby={errorId || helperId || undefined}
/>

<Fade timeout={100} in={showIcon} mountOnEnter={true} unmountOnExit={true}>
Expand Down
10 changes: 8 additions & 2 deletions src/components/Tooltip/Tooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ class Tooltip extends React.Component {

return (
<div {...safeRest(rest)} className={styles.wrapper}>
{this.state.open && this.renderBubble(id, direction, children)}
{this.renderBubble(id, direction, children)}

<button className={styles.trigger} onClick={this.toggleBubble}>
<button
className={styles.trigger}
onClick={this.toggleBubble}
aria-haspopup="true"
aria-expanded={this.state.open ? 'true' : 'false'}
aria-labelledby={id}
>
<DecorativeIcon symbol="questionMarkCircle" />
</button>
</div>
Expand Down
137 changes: 70 additions & 67 deletions src/components/Tooltip/__tests__/Tooltip.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,97 @@
import React from "react";
import { shallow, render } from "enzyme";
import toJson from "enzyme-to-json";

import Tooltip from "../Tooltip";
import DecorativeIcon from "../../Icons/DecorativeIcon/DecorativeIcon";
import Text from "../../Typography/Text/Text";

describe("Tooltip", () => {
const defaultChildren = "Helper text";
const randomId = "the-id";
const doShallow = (
overrides = {},
children = defaultChildren,
id = randomId
) =>
import React from 'react'
import {shallow, render} from 'enzyme'
import toJson from 'enzyme-to-json'

import Tooltip from '../Tooltip'
import DecorativeIcon from '../../Icons/DecorativeIcon/DecorativeIcon'
import Text from '../../Typography/Text/Text'

describe('Tooltip', () => {
const defaultChildren = 'Helper text'
const randomId = 'the-id'
const doShallow = (overrides = {}, children = defaultChildren, id = randomId) =>
shallow(
<Tooltip {...overrides} id={id}>
{children}
</Tooltip>
);
)

const findBubbleElement = tooltip => tooltip.find("div").at(1);
const openBubble = tooltip => tooltip.find("button").simulate("click");
const findBubbleElement = tooltip => tooltip.find('div').at(1)
const findTrigger = tooltip => tooltip.find('button')
const openBubble = tooltip => tooltip.find('button').simulate('click')

it("renders", () => {
const tooltip = render(<Tooltip id="the-id">Helper text</Tooltip>);
it('renders', () => {
const tooltip = render(<Tooltip id="the-id">Helper text</Tooltip>)

expect(toJson(tooltip)).toMatchSnapshot();
});
expect(toJson(tooltip)).toMatchSnapshot()
})

it("has an id", () => {
const tooltip = doShallow({ id: "the-bubble-id" });
openBubble(tooltip);
it('has an id', () => {
const tooltip = doShallow({}, 'Some text', 'the-bubble-id')
openBubble(tooltip)

expect(findBubbleElement(tooltip)).toHaveProp("id", "the-bubble-id");
});
expect(findBubbleElement(tooltip)).toHaveProp('id', 'the-bubble-id')
})

it("has a trigger", () => {
const tooltip = doShallow();
it('has a trigger', () => {
const tooltip = doShallow()

expect(tooltip.find("button")).toContainReact(
<DecorativeIcon symbol="questionMarkCircle" />
);
});
expect(findTrigger(tooltip)).toContainReact(<DecorativeIcon symbol="questionMarkCircle" />)
})

it("shows the bubble content", () => {
const tooltip = doShallow({}, "Some content");
openBubble(tooltip);
it('shows the bubble content', () => {
const tooltip = doShallow({}, 'Some content')
openBubble(tooltip)

expect(
findBubbleElement(tooltip)
.find(Text)
.dive()
).toHaveText("Some content");
});
).toHaveText('Some content')
})

it("has small text in the bubble", () => {
const tooltip = doShallow({}, "Some content");
openBubble(tooltip);
it('has small text in the bubble', () => {
const tooltip = doShallow({}, 'Some content')
openBubble(tooltip)

expect(findBubbleElement(tooltip)).toContainReact(
<Text size="small">Some content</Text>
);
});
expect(findBubbleElement(tooltip)).toContainReact(<Text size="small">Some content</Text>)
})

it("has a direction", () => {
let tooltip = doShallow();
openBubble(tooltip);
it('has a direction', () => {
let tooltip = doShallow()
openBubble(tooltip)

expect(findBubbleElement(tooltip)).toHaveClassName("right");
expect(findBubbleElement(tooltip)).toHaveClassName('right')

tooltip = doShallow({ direction: "left" });
openBubble(tooltip);
tooltip = doShallow({direction: 'left'})
openBubble(tooltip)

expect(findBubbleElement(tooltip)).toHaveClassName("left");
});
expect(findBubbleElement(tooltip)).toHaveClassName('left')
})

it("passes additional attributes to the element", () => {
const tooltip = doShallow({ "data-some-attr": "some value" });
describe('accessibility', () => {
it('connects the bubble message to the trigger button for screen readers', () => {
const tooltip = doShallow({}, 'Random text', 'random-id')

expect(tooltip).toHaveProp("data-some-attr", "some value");
});
expect(findBubbleElement(tooltip)).toHaveProp('id', 'random-id')
expect(findTrigger(tooltip)).toHaveProp('aria-labelledby', 'random-id')
expect(findTrigger(tooltip)).toHaveProp('aria-haspopup', 'true')
})
})

it("does not allow custom CSS", () => {
it('passes additional attributes to the element', () => {
const tooltip = doShallow({'data-some-attr': 'some value'})

expect(tooltip).toHaveProp('data-some-attr', 'some value')
})

it('does not allow custom CSS', () => {
const tooltip = doShallow({
className: "my-custom-class",
style: { color: "hotpink" }
});

expect(tooltip).not.toHaveProp("className", "my-custom-class");
expect(tooltip).not.toHaveProp("style");
});
});
className: 'my-custom-class',
style: {color: 'hotpink'},
})

expect(tooltip).not.toHaveProp('className', 'my-custom-class')
expect(tooltip).not.toHaveProp('style')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,26 @@ exports[`Tooltip renders 1`] = `
<div
class="wrapper"
>
<div
aria-hidden="true"
class="right hideBubble"
id="the-id"
role="tooltip"
>
<div
class="verticalPadding-2 horizontalPadding-3"
>
<span
class="small smallFont color"
>
Helper text
</span>
</div>
</div>
<button
aria-expanded="false"
aria-haspopup="true"
aria-labelledby="the-id"
class="trigger"
>
<i
Expand Down

0 comments on commit a69c73e

Please sign in to comment.