Skip to content

Commit

Permalink
Tooltip v2: Allow external id to be passed down in tooltip so that th…
Browse files Browse the repository at this point in the history
…e trigger can be labelled/described by the that id (#4200)

* Allow external id to be passed to label the trigger

* add changeset

* deconstruct id from props and add unit tests
  • Loading branch information
broccolinisoup committed Feb 6, 2024
1 parent 0bc9ee0 commit dc98814
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-kings-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Tooltip v2: Allow external id to be passed down in tooltip so that the trigger can be labelled/described by the that id
7 changes: 7 additions & 0 deletions src/drafts/Tooltip/Tooltip.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SmileyIcon,
EyeIcon,
CommentIcon,
XIcon,
} from '@primer/octicons-react'
import {default as VisuallyHidden} from '../../_VisuallyHidden'

Expand All @@ -19,6 +20,12 @@ export default {
component: Tooltip,
}

export const CustomId = () => (
<Tooltip id="tooltip-custom-id" text="Close feedback form" direction="nw" type="label">
<IconButton aria-labelledby="tooltip-custom-id" icon={XIcon} variant="invisible" onClick={() => {}} />
</Tooltip>
)

export const FilesPage = () => (
<PageHeader>
<PageHeader.ContextArea>
Expand Down
10 changes: 5 additions & 5 deletions src/drafts/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ const isInteractive = (element: HTMLElement) => {
export const TooltipContext = React.createContext<{tooltipId?: string}>({})

export const Tooltip = React.forwardRef(
({direction = 's', text, type = 'description', children, ...rest}: TooltipProps, forwardedRef) => {
const tooltipId = useId()
({direction = 's', text, type = 'description', children, id, ...rest}: TooltipProps, forwardedRef) => {
const tooltipId = useId(id)
const child = Children.only(children)
const triggerRef = useProvidedRefOrCreate(forwardedRef as React.RefObject<HTMLElement>)
const tooltipElRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -273,9 +273,9 @@ export const Tooltip = React.forwardRef(
React.cloneElement(child as React.ReactElement<TriggerPropsType>, {
ref: triggerRef,
// If it is a type description, we use tooltip to describe the trigger
'aria-describedby': type === 'description' ? `tooltip-${tooltipId}` : child.props['aria-describedby'],
'aria-describedby': type === 'description' ? tooltipId : child.props['aria-describedby'],
// If it is a label type, we use tooltip to label the trigger
'aria-labelledby': type === 'label' ? `tooltip-${tooltipId}` : child.props['aria-labelledby'],
'aria-labelledby': type === 'label' ? tooltipId : child.props['aria-labelledby'],
onBlur: (event: React.FocusEvent) => {
closeTooltip()
child.props.onBlur?.(event)
Expand All @@ -301,7 +301,7 @@ export const Tooltip = React.forwardRef(
role={type === 'description' ? 'tooltip' : undefined}
// stop AT from announcing the tooltip twice when it is a label type because it will be announced with "aria-labelledby"
aria-hidden={type === 'label' ? true : undefined}
id={`tooltip-${tooltipId}`}
id={tooltipId}
// mouse leave and enter on the tooltip itself is needed to keep the tooltip open when the mouse is over the tooltip
onMouseEnter={openTooltip}
onMouseLeave={closeTooltip}
Expand Down
21 changes: 20 additions & 1 deletion src/drafts/Tooltip/__tests__/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {Tooltip, TooltipProps} from '../Tooltip'
import {checkStoriesForAxeViolations} from '../../../utils/testing'
import {render as HTMLRender} from '@testing-library/react'
import theme from '../../../theme'
import {Button, ActionMenu, ActionList, ThemeProvider, SSRProvider, BaseStyles} from '../../../'
import {Button, IconButton, ActionMenu, ActionList, ThemeProvider, SSRProvider, BaseStyles} from '../../../'
import {XIcon} from '@primer/octicons-react'

const TooltipComponent = (props: Omit<TooltipProps, 'text'> & {text?: string}) => (
<Tooltip text="Tooltip text" {...props}>
Expand Down Expand Up @@ -91,4 +92,22 @@ describe('Tooltip', () => {
expect(menuButton).toHaveAttribute('aria-describedby', tooltip.id)
expect(menuButton).toHaveAttribute('aria-haspopup', 'true')
})
it('should use the custom tooltip id (if present) to label the trigger element', () => {
const {getByRole} = HTMLRender(
<Tooltip id="custom-tooltip-id" text="Close feedback form" direction="nw" type="label">
<IconButton aria-labelledby="custom-tooltip-id" icon={XIcon} variant="invisible" onClick={() => {}} />
</Tooltip>,
)
const triggerEL = getByRole('button')
expect(triggerEL).toHaveAttribute('aria-labelledby', 'custom-tooltip-id')
})
it('should use the custom tooltip id (if present) to described the trigger element', () => {
const {getByRole} = HTMLRender(
<Tooltip text="This operation cannot be reverted" id="custom-tooltip-id">
<Button>Delete</Button>
</Tooltip>,
)
const triggerEL = getByRole('button')
expect(triggerEL).toHaveAttribute('aria-describedby', 'custom-tooltip-id')
})
})

0 comments on commit dc98814

Please sign in to comment.