mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tooltip: Ensure tooltip text is correctly announced by screenreaders (#76683)
* add aria-describedby, tooltip role and unit tests Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Co-authored-by: joshhunt <josh@trtr.co> * remove `delayShow` so tooltip text is announced correctly * adjust IconButton, fix unit tests * undo tooltip aria-label change * only set aria-describedby if there's no aria-label --------- Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Co-authored-by: joshhunt <josh@trtr.co>
This commit is contained in:
parent
5cc3a3f1ed
commit
d632dd672c
@ -32,8 +32,6 @@ const meta: Meta<typeof IconButton> = {
|
||||
tooltip: 'sample tooltip message',
|
||||
tooltipPlacement: 'top',
|
||||
variant: 'secondary',
|
||||
ariaLabel: 'this property is deprecated',
|
||||
['aria-label']: 'sample aria-label content',
|
||||
},
|
||||
argTypes: {
|
||||
tooltip: {
|
||||
|
@ -39,6 +39,7 @@ describe('Tooltip', () => {
|
||||
|
||||
expect(refObj.current).not.toBeNull();
|
||||
});
|
||||
|
||||
it('to be shown on hover and be dismissable by pressing Esc key when show is undefined', async () => {
|
||||
render(
|
||||
<Tooltip content="Tooltip content">
|
||||
@ -50,6 +51,7 @@ describe('Tooltip', () => {
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is always visible when show prop is true', async () => {
|
||||
render(
|
||||
<Tooltip content="Tooltip content" show={true}>
|
||||
@ -61,6 +63,7 @@ describe('Tooltip', () => {
|
||||
await userEvent.unhover(screen.getByText('On the page'));
|
||||
expect(screen.getByText('Tooltip content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is never visible when show prop is false', async () => {
|
||||
render(
|
||||
<Tooltip content="Tooltip content" show={false}>
|
||||
@ -70,4 +73,27 @@ describe('Tooltip', () => {
|
||||
await userEvent.hover(screen.getByText('On the page'));
|
||||
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('exposes the tooltip text to screen readers', async () => {
|
||||
render(
|
||||
<Tooltip content="Tooltip content">
|
||||
<button>On the page</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
// if tooltip is not visible, description won't be set
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
description: 'Tooltip content',
|
||||
})
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// tab to button to make tooltip visible
|
||||
await userEvent.keyboard('{tab}');
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
description: 'Tooltip content',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useId, useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@ -24,7 +24,8 @@ export interface TooltipProps {
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
({ children, theme, interactive, show, placement, content }, forwardedRef) => {
|
||||
const [controlledVisible, setControlledVisible] = React.useState(show);
|
||||
const [controlledVisible, setControlledVisible] = useState(show);
|
||||
const tooltipId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledVisible !== false) {
|
||||
@ -44,10 +45,9 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible, update } = usePopperTooltip({
|
||||
visible: show ?? controlledVisible,
|
||||
placement: placement,
|
||||
interactive: interactive,
|
||||
placement,
|
||||
interactive,
|
||||
delayHide: interactive ? 100 : 0,
|
||||
delayShow: 150,
|
||||
offset: [0, 8],
|
||||
trigger: ['hover', 'focus'],
|
||||
onVisibleChange: setControlledVisible,
|
||||
@ -69,17 +69,23 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
|
||||
[forwardedRef, setTriggerRef]
|
||||
);
|
||||
|
||||
// if the child has an aria-label, this should take precedence over the tooltip content
|
||||
const childHasAriaLabel = 'aria-label' in children.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
ref: handleRef,
|
||||
tabIndex: 0, // tooltip should be keyboard focusable
|
||||
tabIndex: 0, // tooltip trigger should be keyboard focusable
|
||||
'aria-describedby': !childHasAriaLabel && visible ? tooltipId : undefined,
|
||||
})}
|
||||
{visible && (
|
||||
<Portal>
|
||||
<div
|
||||
data-testid={selectors.components.Tooltip.container}
|
||||
ref={setTooltipRef}
|
||||
id={tooltipId}
|
||||
role="tooltip"
|
||||
{...getTooltipProps({ className: style.container })}
|
||||
>
|
||||
<div {...getArrowProps({ className: style.arrow })} />
|
||||
|
@ -402,10 +402,8 @@ describe(`Traces Filters`, () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const removeLabel = screen.getAllByLabelText(`Remove`);
|
||||
await act(async () => {
|
||||
const removeLabel = screen.getAllByLabelText(/Remove/);
|
||||
await userEvent.click(removeLabel[1]);
|
||||
});
|
||||
|
||||
rerender(
|
||||
<Filters
|
||||
|
@ -269,7 +269,7 @@ describe('AnnoListPanel', () => {
|
||||
'anno-list-panel-1'
|
||||
);
|
||||
expect(screen.getByText(/filter:/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/result email/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /result email/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -86,7 +86,7 @@ const Avatar = ({ onClick, avatarUrl, login, email }: AvatarProps) => {
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltipContent} theme="info" placement="top">
|
||||
<button onClick={onAvatarClick} className={styles.avatar} aria-label={`Created by ${email}`}>
|
||||
<button onClick={onAvatarClick} className={styles.avatar}>
|
||||
<img src={avatarUrl} alt="avatar icon" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
Loading…
Reference in New Issue
Block a user