-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Summary
Tooltips in Blueprint cannot be dismissed using the Escape key, which violates the WCAG 2.1 Success Criterion 1.4.13 (Content on Hover or Focus). This makes it impossible for users to dismiss tooltip content without moving their pointer or keyboard focus, which is particularly problematic for users with low vision who use screen magnification, users with low pointer accuracy, and keyboard-only users.
WCAG 2.1 Success Criterion 1.4.13
Success Criterion 1.4.13: Content on Hover or Focus states:
Where receiving and then removing pointer hover or keyboard focus triggers additional content to become visible and then hidden, the following are true:
Dismissible: A mechanism is available to dismiss the additional content without moving pointer hover or keyboard focus, unless the additional content communicates an input error or does not obscure or replace other content.
The Escape key is the standard mechanism for dismissing such content.
ARIA Authoring Practices Guide (APG)
The WAI-ARIA Tooltip Pattern explicitly specifies keyboard interaction requirements:
Keyboard Interaction
Escape: Dismisses the Tooltip.
Note:
- Focus stays on the triggering element while the tooltip is displayed.
- If the tooltip is invoked when the trigger element receives focus, then it is dismissed when it no longer has focus (onBlur).
- If the tooltip is invoked when a pointing cursor moves over the trigger element, then it remains open as long as the cursor is over the trigger or the tooltip.
Current Behavior
- Hover over a tooltip target to display the tooltip
- Press the Escape key
- Expected: Tooltip should close
- Actual: Tooltip remains open and does not respond to Escape key
Reproduction
Demo of the issue: https://codesandbox.io/p/sandbox/j5cyp6
Root Cause Analysis
Tooltip has the following component hierarchy: <Tooltip> → <Popover> → <Overlay2>
blueprint/packages/core/src/components/tooltip/tooltip.tsx
Lines 137 to 138 in a93381f
| autoFocus={false} | |
| canEscapeKeyClose={false} |
Both the autoFocus and canEscapeKeyClose props are hardcoded to false in the Popover component rendered by Tooltip.
This contradicts with the inherited prop interface of Tooltip which states that these props should default to true:
- The
TooltipPropsinterface inherits fromPopoverSharedProps:blueprint/packages/core/src/components/tooltip/tooltip.tsx
Lines 28 to 29 in a93381f
export interface TooltipProps<TProps extends DefaultPopoverTargetHTMLProps = DefaultPopoverTargetHTMLProps> extends Omit<PopoverSharedProps<TProps>, "shouldReturnFocusOnClose">, PopoverSharedPropsextendsOverlayableProps:export interface PopoverSharedProps<TProps extends DefaultPopoverTargetHTMLProps> extends OverlayableProps, Props { OverlayablePropsdefines bothautoFocusandcanEscapeKeyClosewith@default true:blueprint/packages/core/src/components/overlay/overlayProps.ts
Lines 20 to 32 in a93381f
/** * Whether the overlay should acquire application focus when it first opens. * * @default true */ autoFocus?: boolean; /** * Whether pressing the `esc` key should invoke `onClose`. * * @default true */ canEscapeKeyClose?: boolean;
If we change canEscapeKeyClose={true}, the escape key still won't work because Overlay2's escape key handler is attached to the container element (onKeyDown={handleContainerKeyDown}). Without autoFocus={true}, escape key events will fire on the document body, not on the Overlay2 container.
blueprint/packages/core/src/components/overlay2/overlay2.tsx
Lines 236 to 247 in a93381f
| const handleContainerKeyDown = useCallback( | |
| (e: React.KeyboardEvent<HTMLElement>) => { | |
| if (e.key === "Escape" && canEscapeKeyClose) { | |
| onClose?.(e); | |
| // prevent other overlays from closing | |
| e.stopPropagation(); | |
| // prevent browser-specific escape key behavior (Safari exits fullscreen) | |
| e.preventDefault(); | |
| } | |
| }, | |
| [canEscapeKeyClose, onClose], | |
| ); |