Skip to content
Merged
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
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')
})
})