Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Tooltip describe variant using aria-description string instead of aria-describedby reference #2814

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions cypress/integration/Tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ describe('Tooltip', () => {
cy.checkA11y();
});

it('the "Delete" button should have an aria-describedby linking to the role="tooltip" element', () => {
it('the "Delete" button should have an accessible description equal to the tooltip text', () => {
cy.findByRole('button', {name: 'Delete'}).should(
'have.ariaDescription',
'have.attr',
'aria-description',
'The service will restart after this action'
);
});
Expand Down Expand Up @@ -354,9 +355,7 @@ describe('Tooltip', () => {
context(`when the preferred placement is set to ${io.placement}`, () => {
beforeEach(() => {
if (io.isMovedToSide) {
cy.findByTestId(`slide-${io.placement}`)
.type('500')
.trigger('change');
cy.findByTestId(`slide-${io.placement}`).type('500').trigger('change');
}
cy.findByRole('button', {name: io.placement}).click();
cy.scrollTo(io.x, io.y);
Expand Down
16 changes: 16 additions & 0 deletions modules/react/_examples/stories/Tooltips.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {ListOfUploadedFiles} from './examples/Tooltips/ListOfUploadedFiles';

<Meta title="Examples/Tooltips" component={Tooltip} />

# Accessible Tooltip Examples

## Using descriptive tooltips for repeated text buttons

In this example, the "Delete" buttons are used repeatedly to reference the multiple files that have
been uploaded to the web app. The text buttons already have an accessible name (a.k.a. label)
derived from the button's inner text. The `describe` tooltip can be useful for providing more
in-context description for both low vision sighted users and screen reader users without overriding
the button name "Delete".

<ExampleCodeBlock code={ListOfUploadedFiles} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import {DeleteButton} from '@workday/canvas-kit-react/button';
import {Flex} from '@workday/canvas-kit-react/layout';
import {Heading, Text} from '@workday/canvas-kit-react/text';
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {trashIcon} from '@workday/canvas-system-icons-web';

const files = ['Cover Letter.docx', 'Resume.docx', 'Portfolio.pptx', 'Portrait.png'];

const listStyles = {
alignItems: 'center',
width: '35rem',
};

const deleteBtnStyle = {
marginLeft: 'auto',
};

export const ListOfUploadedFiles = () => {
return (
<>
<Heading size="medium">Uploaded Files:</Heading>
<Flex as="ul" gap="1rem" flexDirection="column">
{files.map(i => (
<Flex as="li" style={listStyles}>
<Text>{i}</Text>
<Tooltip type="describe" title={i}>
<DeleteButton icon={trashIcon} style={deleteBtnStyle}>
Delete
</DeleteButton>
</Tooltip>
</Flex>
))}
</Flex>
</>
);
};
9 changes: 2 additions & 7 deletions modules/react/tooltip/lib/useTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function useTooltip<T extends Element = Element>({
* - `label`: Sets the accessible name for the wrapped element. Use for icons or if tooltip
* `title` prop is the same as the text content of the wrapped element. E.g. TertiaryButton that renders an icon or
* Ellipsis tooltips.
* - `describe`: Sets `aria-describedby` of the wrapped element. Use if the tooltip has additional
* - `describe`: Sets `aria-description` strings for the wrapped element. Use if the tooltip has additional
* information about the target.
* - `muted`: No effort is made to make the tooltip accessible to screen readers. Use if the
* tooltip contents are not useful to a screen reader or if you have handled accessibility of
Expand Down Expand Up @@ -135,7 +135,7 @@ export function useTooltip<T extends Element = Element>({

const targetProps = {
// extra description of the target element for assistive technology
'aria-describedby': type === 'describe' && visible ? id : undefined,
'aria-description': type === 'describe' ? titleText : undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is potentially breaking change, because now tooltip will require to have titleText passed, but it's optional now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Raisa. This would be breaking.

Also, shouldn't both be present since aria-describedby points to the target of the element with the id and aria-description is just describing what the element is with nothing pointing to it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is potentially breaking change, because now tooltip will require to have titleText passed, but it's optional now.

Does this mean title prop was optional? Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, shouldn't both be present since aria-describedby points to the target of the element with the id and aria-description is just describing what the element is with nothing pointing to it?

No, for similar reasons why we discourage using both aria-labelledby references and aria-label strings on the same element. Browsers follow an algorithm to define a 1) accessible name and 2) accessible description for an element.

If aria-describedby is used on an element, then an aria-description string will be ignored and discarded by the browser.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

titleText is optional in the useTooltip hook, but not the title in the Tooltip component. titleText was optional in useTooltip because type="describe" did not use the config. This change makes it so type="describe" also uses the config. There is still the type="muted" that doesn't use the titleText config.

Now, title is required by Tooltip because it is the contents of the tooltip. OverflowTooltip does not use title because the contents of the target are the contents of the tooltip. An OverflowTooltip uses the type="muted" because a screen reader doesn't understand the concept of the ellipsis - to the screen reader, the full contents are exposed already. A screen dictation user will be able to target based on what content is already visible and not cut off - from there it is part of the design to make sure the most significant part of the target text is visible when overflowing.

// This will replace the accessible name of the target element
'aria-label': type === 'label' ? titleText : undefined,
onMouseEnter: onOpenFromTarget,
Expand All @@ -145,11 +145,6 @@ export function useTooltip<T extends Element = Element>({
onBlur: onHide,
};

// remove `aria-describedby` if undefined to not override what's provided
if (targetProps['aria-describedby'] === undefined) {
delete targetProps['aria-describedby'];
}

return {
/** Mix these properties into the target element. **Must be an Element** */
targetProps,
Expand Down
5 changes: 2 additions & 3 deletions modules/react/tooltip/spec/Tooltip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ describe('Tooltip', () => {
fireEvent.mouseEnter(screen.getByText('Test Text')); // triggers the tooltip

jest.advanceTimersByTime(300); // advance the timer by the amount of delay time
expect(screen.getByText('Test Text')).toHaveAttribute('aria-describedby');
expect(screen.getByText('Test Text')).toHaveAttribute('aria-description');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to assert the contents of the attribute and not just the presence of it. The second parameter is the text that is expected.


const id = screen.getByText('Test Text').getAttribute('aria-describedby');
expect(screen.getByRole('tooltip')).toHaveAttribute('id', id);
expect(screen.getByRole('tooltip')).toHaveAttribute('id');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is important for the tooltip to have an id at this point. This line should be removed. The id was only used for the aria-describedby.

});
jest.clearAllTimers();
});
Expand Down
3 changes: 1 addition & 2 deletions modules/react/tooltip/spec/useTooltip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ describe('useTooltip with type="describe"', () => {
jest.advanceTimersByTime(300); // advance the timer by the amount of delay time

expect(tooltip).toHaveAttribute('id');
const id = tooltip.getAttribute('id');
expect(target).toHaveAttribute('aria-describedby', id);
expect(target).toHaveAttribute('aria-description');
});
jest.clearAllTimers();
});
8 changes: 4 additions & 4 deletions modules/react/tooltip/stories/Tooltip.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ and focus events.

### Describing an Element

The default mode for a tooltip is to label content via `aria-label`. If a tooltip is meant to
provide ancillary information, the `type` can be set to `describe`. This will add `aria-describedby`
to the target element. This will allow screen reader users to hear the name of the control that is
being focused and the ancillary tooltip information.
The default mode for a tooltip is to assign a name to the target element with an `aria-label`
string. If a tooltip is meant to provide ancillary information, the `type` can be set to `describe`.
This will add `aria-description` strings to the target element instead. This variant is useful on
text buttons and other components that already have a label or name.

<ExampleCodeBlock code={DescribeType} />

Expand Down
18 changes: 14 additions & 4 deletions modules/react/tooltip/stories/examples/DescribeType.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import React from 'react';

import {DeleteButton} from '@workday/canvas-kit-react/button';
import {DeleteButton, SecondaryButton, TertiaryButton} from '@workday/canvas-kit-react/button';
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {Flex} from '@workday/canvas-kit-react/layout';
import {chartConfigIcon} from '@workday/canvas-system-icons-web';

export const DescribeType = () => {
return (
<Tooltip type="describe" title="The service will restart after this action">
<DeleteButton>Delete</DeleteButton>
</Tooltip>
<Flex gap="s">
<Tooltip type="describe" title="Search using additional criteria">
<TertiaryButton icon={chartConfigIcon}>Advanced Search</TertiaryButton>
</Tooltip>
<Tooltip type="describe" title="Create saved search">
<SecondaryButton>Save</SecondaryButton>
</Tooltip>
<Tooltip type="describe" title="The service will restart after this action">
<DeleteButton>Delete</DeleteButton>
</Tooltip>
</Flex>
);
};
Loading