From 703af3bd6fd427eb98f892e469af458dd3555c53 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 10 Nov 2026 16:27:29 -0300 Subject: [PATCH 1/6] fix(BBButton): close the tooltip with Escape A BBButton tooltip shown on hover or keyboard focus can only be closed by moving the pointer away or moving focus elsewhere. It can cover nearby content, and WCAG 1.4.13 (Content on Hover or Focus) requires such content to be dismissible without moving the pointer or focus. Add a shared Tippy plugin, `hideOnEsc`, that hides the tooltip when Escape is pressed, and pass it to the button's Tippy. The `keydown` listener is added to the document only while the tooltip is shown and removed when it hides or is destroyed. The tooltip shows again on the next hover or focus. The plugin lives in `src/utils/tooltip.ts` so every component with a Tippy tooltip can use it. The listener runs in the capture phase, so the tooltip also hides when an Escape is stopped before it reaches the document, as react-modal does for BBBModal with `shouldCloseOnEsc`, or by a focused input that handles it. The event itself is not stopped: stopping it would hide it from the consumer's own handlers while a tooltip is shown. --- src/components/Button/component.tsx | 2 ++ src/utils/tooltip.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/utils/tooltip.ts diff --git a/src/components/Button/component.tsx b/src/components/Button/component.tsx index acf13a8..b3b1d01 100644 --- a/src/components/Button/component.tsx +++ b/src/components/Button/component.tsx @@ -15,6 +15,7 @@ import { import * as Styled from './styles'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; +import { TOOLTIP_PLUGINS } from '../../utils/tooltip'; /** * A versatile Button component. @@ -218,6 +219,7 @@ function Button(props: ButtonProps): JSX.Element { {buttonElement} diff --git a/src/utils/tooltip.ts b/src/utils/tooltip.ts new file mode 100644 index 0000000..15f6a97 --- /dev/null +++ b/src/utils/tooltip.ts @@ -0,0 +1,23 @@ +import type { Plugin } from 'tippy.js'; + +// WCAG 1.4.13 requires a tooltip to be dismissible without moving the pointer or focus. +export const hideOnEsc: Plugin = { + name: 'hideOnEsc', + defaultValue: true, + fn({ hide }) { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') hide(); + }; + const stopListening = () => document.removeEventListener('keydown', onKeyDown, true); + + return { + onShow() { + document.addEventListener('keydown', onKeyDown, true); + }, + onHide: stopListening, + onDestroy: stopListening, + }; + }, +}; + +export const TOOLTIP_PLUGINS = [hideOnEsc]; From a70322d1b8462decac7a7ecc587ef86c4634dd55 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 10 Nov 2026 16:27:29 -0300 Subject: [PATCH 2/6] fix(BBBAccordion): close the tooltip with Escape The header tooltip of BBBAccordion can only be closed by moving the pointer away or moving focus elsewhere, which falls short of WCAG 1.4.13 (Content on Hover or Focus). Pass the shared `hideOnEsc` plugin to its Tippy, as BBButton does, so Escape hides the tooltip. --- src/components/Accordion/component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Accordion/component.tsx b/src/components/Accordion/component.tsx index 9524240..2400c19 100644 --- a/src/components/Accordion/component.tsx +++ b/src/components/Accordion/component.tsx @@ -4,6 +4,7 @@ import * as Styled from './styles'; import { MdExpandMore } from 'react-icons/md'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; +import { TOOLTIP_PLUGINS } from '../../utils/tooltip'; import { DEFAULT_TOOLTIP_PLACEMENT, DEFAULT_BUTTON_HEADER_POSITION } from './constants'; /** @@ -49,7 +50,7 @@ function Accordion({ return ( <> {tooltipLabel ? ( - + {expandButton} ) : ( From 649be26d12da8be975d333cbb5ce2a639d7dbcc9 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 10 Nov 2026 16:27:29 -0300 Subject: [PATCH 3/6] fix(BBBAvatar): close the tooltip with Escape The name tooltip of BBBAvatar can only be closed by moving the pointer away, which falls short of WCAG 1.4.13 (Content on Hover or Focus). Pass the shared `hideOnEsc` plugin to its Tippy, as BBButton does, so Escape hides the tooltip. --- src/components/Avatar/component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Avatar/component.tsx b/src/components/Avatar/component.tsx index fe2e161..d77b7f0 100644 --- a/src/components/Avatar/component.tsx +++ b/src/components/Avatar/component.tsx @@ -1,6 +1,7 @@ import React, { JSX, useEffect, useState } from 'react'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; +import { TOOLTIP_PLUGINS } from '../../utils/tooltip'; import * as Styled from './styles'; import { AvatarProps } from './types'; import { DEFAULT_AVATAR_SIZE, AVATAR_FALLBACK_COLORS } from './constants'; @@ -63,7 +64,7 @@ function Avatar({ } return ( - + {avatarElement} ); From 131e7eac8d9416b83e1bf7929d1b91853c7f5179 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 13 Nov 2026 20:41:40 -0300 Subject: [PATCH 4/6] fix(BBBCheckbox): close the tooltip with Escape The tooltip of BBBCheckbox can only be closed by moving the pointer away or moving focus elsewhere, which falls short of WCAG 1.4.13 (Content on Hover or Focus). Pass the shared `hideOnEsc` plugin to its Tippy, as BBButton does, so Escape hides the tooltip. --- src/components/Checkbox/component.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Checkbox/component.tsx b/src/components/Checkbox/component.tsx index 751efcc..8368265 100644 --- a/src/components/Checkbox/component.tsx +++ b/src/components/Checkbox/component.tsx @@ -6,6 +6,7 @@ import { DEFAULT_LAYOUT, DEFAULT_TOOLTIP_PLACEMENT, LAYOUTS } from './constants' import { RadioButtonUnchecked, RadioButtonChecked } from '@mui/icons-material'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; +import { TOOLTIP_PLUGINS } from '../../utils/tooltip'; // Cancelling the click (mouse, label or Space) keeps the box unchanged, and MUI skips // `onChange` for a prevented click. It sits on the root because MUI drops event handlers @@ -128,6 +129,7 @@ function Checkbox({ placement={tooltipPlacement} trigger="mouseenter focusin" aria={{ content: null }} + plugins={TOOLTIP_PLUGINS} /> )} From bac3f0e036ae8e7da3d4705a585e6d340e50b9bb Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 13 Nov 2026 20:41:40 -0300 Subject: [PATCH 5/6] fix(BBBToggle): close the tooltip with Escape The tooltip of BBBToggle can only be closed by moving the pointer away or moving focus elsewhere, which falls short of WCAG 1.4.13 (Content on Hover or Focus). Pass the shared `hideOnEsc` plugin to its Tippy, as BBButton does, so Escape hides the tooltip. --- src/components/Toggle/component.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Toggle/component.tsx b/src/components/Toggle/component.tsx index 71bb040..f151b9a 100644 --- a/src/components/Toggle/component.tsx +++ b/src/components/Toggle/component.tsx @@ -7,6 +7,7 @@ import * as Styled from './styles'; import { DEFAULT_TEXT_POSITION, DEFAULT_TOOLTIP_PLACEMENT, TEXT_POSITIONS } from './constants'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; +import { TOOLTIP_PLUGINS } from '../../utils/tooltip'; // Cancelling the click (mouse, label or Space) keeps the switch unchanged, and MUI skips // `onChange` for a prevented click. It sits on the switch because MUI drops event handlers @@ -168,6 +169,7 @@ function Toggle ({ getReferenceClientRect={getTooltipAnchorRect} delay={TOOLTIP_DELAY} aria={{ content: null }} + plugins={TOOLTIP_PLUGINS} /> )} From c6fd2f90972ff76fe4086251a798fc0bb8561e5d Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 13 Nov 2026 20:49:23 -0300 Subject: [PATCH 6/6] fix(BBBSlider): close the value label with Escape The value label of BBBSlider, shown on hover, focus or drag when `valueLabelDisplay` is 'auto', can only be closed by moving the pointer away or moving focus elsewhere, which falls short of WCAG 1.4.13 (Content on Hover or Focus). MUI's Slider renders the label and keeps its open state internal, with no Escape handling, so the shared `hideOnEsc` Tippy plugin cannot be used. Render the label through a `valueLabel` slot that closes it on Escape and opens it again on the next hover or focus. As in `hideOnEsc`, the listener is added to the document in the capture phase only while the label is open, and the event is not stopped. A label shown at all times ('on') is not hover content and stays. A consumer's own `slots.valueLabel` still takes precedence. --- src/components/Slider/component.tsx | 3 +++ src/components/Slider/valueLabel.tsx | 33 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/components/Slider/valueLabel.tsx diff --git a/src/components/Slider/component.tsx b/src/components/Slider/component.tsx index 6f21302..1cb642a 100644 --- a/src/components/Slider/component.tsx +++ b/src/components/Slider/component.tsx @@ -3,6 +3,7 @@ import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles'; import { SliderProps } from './types'; import * as Styled from './styles'; import { DEFAULT_ANIMATE, DEFAULT_SHOW_MARK_TICKS, DEFAULT_VALUE_LABEL_DISPLAY } from './constants'; +import DismissibleValueLabel from './valueLabel'; /** * A horizontal, single-thumb slider for picking a value inside a range. @@ -26,6 +27,7 @@ function Slider ({ ariaLabel, ariaLabelledBy, ariaDescribedBy, + slots, slotProps, ...rest }: SliderProps): JSX.Element { @@ -90,6 +92,7 @@ function Slider ({ aria-label={ariaLabel} aria-labelledby={ariaLabel ? undefined : ariaLabelledBy || labelId} data-test={dataTest} + slots={{ valueLabel: DismissibleValueLabel, ...slots }} slotProps={{ ...slotProps, input: { diff --git a/src/components/Slider/valueLabel.tsx b/src/components/Slider/valueLabel.tsx new file mode 100644 index 0000000..168315b --- /dev/null +++ b/src/components/Slider/valueLabel.tsx @@ -0,0 +1,33 @@ +import React, { JSX, useEffect, useState } from 'react'; +import { SliderValueLabel, SliderValueLabelProps } from '@mui/material'; +import { ValueLabelDisplay } from './types'; +import { VALUE_LABEL_DISPLAYS } from './constants'; + +// WCAG 1.4.13 requires a tooltip to be dismissible without moving the pointer or focus, and +// MUI keeps the label's open state internal with no Escape handling. A label that is always +// shown ('on') is not hover content, so it stays. +function DismissibleValueLabel({ + open, + ...props +}: SliderValueLabelProps & { valueLabelDisplay?: ValueLabelDisplay }): JSX.Element { + const [dismissed, setDismissed] = useState(false); + const dismissible = open && props.valueLabelDisplay !== VALUE_LABEL_DISPLAYS.ON; + + useEffect(() => { + if (!dismissible) { + setDismissed(false); + return undefined; + } + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setDismissed(true); + }; + document.addEventListener('keydown', onKeyDown, true); + + return () => document.removeEventListener('keydown', onKeyDown, true); + }, [dismissible]); + + return ; +} + +export default DismissibleValueLabel;