diff --git a/packages/grafana-ui/src/components/IconButton/IconButton.story.tsx b/packages/grafana-ui/src/components/IconButton/IconButton.story.tsx index db562ecb73b..0e410f60195 100644 --- a/packages/grafana-ui/src/components/IconButton/IconButton.story.tsx +++ b/packages/grafana-ui/src/components/IconButton/IconButton.story.tsx @@ -32,8 +32,6 @@ const meta: Meta = { tooltip: 'sample tooltip message', tooltipPlacement: 'top', variant: 'secondary', - ariaLabel: 'this property is deprecated', - ['aria-label']: 'sample aria-label content', }, argTypes: { tooltip: { diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx index 12e7ece46d0..009b7e749f3 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx @@ -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( @@ -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( @@ -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( @@ -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( + + + + ); + + // 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(); + }); }); diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index b3c78762360..0b881601f84 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -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( ({ 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( 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( [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 && (