Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eba40c8
fix(bulma-ui): let a Tooltip be hovered and dismissed with Escape
allxsmith Oct 11, 2026
8f15314
chore: regenerate
allxsmith Oct 11, 2026
126561e
docs: say a HoverCard's Escape needs nothing moved, and how a control…
allxsmith Oct 11, 2026
40d44c6
chore: regenerate
allxsmith Oct 11, 2026
c1cf058
fix(bulma-ui): open HoverCard and Tooltip when the pointer reaches th…
allxsmith Oct 12, 2026
b64cb70
fix(bulma-ui): let Escape on a Tooltip go on to what has focus
allxsmith Oct 12, 2026
ce810cf
fix(bulma-ui): keep a Tooltip's arrow from taking clicks on its trigger
allxsmith Oct 12, 2026
3c2ef2f
fix(bulma-ui): keep a Tooltip shown under the pointer when active is …
allxsmith Oct 12, 2026
d2d8ef7
docs: say a Tooltip's content is to read, with links and buttons in a…
allxsmith Oct 12, 2026
de6128f
feat(bulma-ui): let a tap show a Tooltip with openOnTap
allxsmith Oct 12, 2026
4c343a1
fix(bulma-ui): bridge a Tooltip's gap only as wide as the trigger, an…
allxsmith Oct 12, 2026
718ef0c
chore: regenerate
allxsmith Oct 12, 2026
6d1ddb9
Merge remote-tracking branch 'origin/main' into gate-fix-tooltip-wcag…
allxsmith Oct 12, 2026
f2e35ed
Merge remote-tracking branch 'origin/main' into gate-fix-tooltip-wcag…
allxsmith Oct 12, 2026
c33a894
chore: regenerate after merging main
allxsmith Oct 12, 2026
62e5281
docs: say a Tooltip Escape hid shows again on the next tap with openO…
allxsmith Oct 12, 2026
41975da
docs: say a theme that widens a Tooltip's gap needs a closeDelay to c…
allxsmith Oct 12, 2026
502c462
chore: regenerate
allxsmith Oct 12, 2026
f940611
Merge remote-tracking branch 'origin/main' into gate-fix-tooltip-wcag…
allxsmith Oct 12, 2026
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
4 changes: 2 additions & 2 deletions bestax-mcp/data/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1104,8 +1104,8 @@
"slug": "components/tooltip",
"import": "import { Tooltip } from '@allxsmith/bestax-bulma';",
"compound": false,
"propCount": 13,
"exampleCount": 10
"propCount": 14,
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
"exampleCount": 11
},
{
"name": "UnorderedList",
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/HoverCard.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"code": "function example() {\n const [open, setOpen] = useState(false);\n const [following, setFollowing] = useState(false);\n\n return (\n <>\n <Paragraph mb=\"3\">\n The card is {open ? 'open' : 'closed'}\n {following ? ', and you follow Ada' : ''}.\n </Paragraph>\n <HoverCard\n open={open}\n onOpenChange={setOpen}\n trigger={<Link href=\"#ada\">Ada Lovelace</Link>}\n >\n <Paragraph mb=\"3\">Analytical Engine team</Paragraph>\n <Button\n size=\"small\"\n color=\"primary\"\n onClick={() => {\n setFollowing(true);\n setOpen(false);\n }}\n >\n Follow\n </Button>\n </HoverCard>\n </>\n );\n}"
}
],
"accessibility": "`HoverCard` is built to\n[WCAG's Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)\ncriterion, condition by condition:\n\n- **Dismissible:** <kbd>Escape</kbd> closes the card wherever focus is, without moving the pointer\n or focus. The keypress belongs to whatever has focus. When that's the card, its trigger or\n nothing, the card takes it, so a `Popover` or `Modal` around it stays open. When it's something\n else, such as a field elsewhere on the page or a `Modal` opened from the card, the card still\n closes and the key goes on to it. With one card inside another, the inner one closes first.\n Focus that was in the card goes back to the trigger, and the card stays closed until the pointer\n or focus leaves and comes back.\n- **Hoverable:** the pointer can move from the trigger onto the card. `closeDelay` covers the gap\n between them, and the card takes pointer events, unlike a tooltip.\n- **Persistent:** no timer closes a card that's hovered or focused. Focus inside the card holds it\n however it got there. Whether a click focuses what it lands on is up to the browser, though:\n Safari and Firefox on macOS don't focus a button on click, so there a clicked button doesn't keep\n the card open once the pointer moves away.\n\nAnd around it:\n\n- Keyboard focus on the trigger opens the card, as hover does. Focus that a press brings doesn't,\n and neither does a touch pointer, so tapping the trigger on a phone just does what the trigger\n does. Clicking the trigger never toggles the card.\n- By default, then, a phone or tablet never opens the card, so whatever it shows has to be\n reachable from the trigger as well. `openOnTap` opens it with a tap, for a trigger with no tap\n action of its own. While a tap holds it open, a tap outside the trigger and the card closes it.\n- The trigger gets `aria-expanded` and `aria-controls`, which say it's expanded and name the card\n only while the card is on the page. It gets no role, no tab stop and no `aria-haspopup`, so the trigger element itself has to\n take focus, such as a link or a button. A link inside a `<span>` doesn't do, since focus comes\n back to the trigger and its ARIA is on it. A development build warns when the trigger can't take\n focus.\n- The card is a plain element with an `id` and no role. It isn't a dialog, since focus doesn't move\n into it, and it isn't a tooltip, since it can hold links. In place, a screen reader meets it\n right after the trigger, Tab goes from the trigger into its links and buttons, and tabbing past\n them closes it.\n- With `appendToBody` the card sits in the Tab order where its container is. By default that's\n the end of `<body>`, so it comes last: Tab reaches it only after the rest of the page, and focus\n leaving the trigger on the way closes it. Keyboard users can open it, read it and close it, so\n whatever it links to should be reachable from the page as well.\n- Opening the card moves no focus, and nothing traps it. Links and buttons belong in a card; a form\n belongs in a `Popover`, which opens on purpose and holds focus.\n- The fade-in is turned off under `prefers-reduced-motion: reduce`.",
"accessibility": "`HoverCard` is built to\n[WCAG's Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)\ncriterion, condition by condition:\n\n- **Dismissible:** <kbd>Escape</kbd> closes the card wherever focus is, without you moving the\n pointer or focus. In controlled use it asks to close through `onOpenChange`, so the card closes\n when you set `open` to `false`. The keypress belongs to whatever has focus. When that's the card,\n its trigger or nothing, the card takes it, so a `Popover` or `Modal` around it stays open. When\n it's something else, such as a field elsewhere on the page or a `Modal` opened from the card, the\n card still closes and the key goes on to it. With one card inside another, the inner one closes\n first. Focus that was in the card goes back to the trigger, and the card stays closed until the\n pointer or focus leaves and comes back, or, with `openOnTap`, until the next tap on the trigger.\n- **Hoverable:** the pointer can move from the trigger onto the card. `closeDelay` covers the gap\n between them, and the card takes pointer events.\n- **Persistent:** no timer closes a card that's hovered or focused. Focus inside the card holds it\n however it got there. Whether a click focuses what it lands on is up to the browser, though:\n Safari and Firefox on macOS don't focus a button on click, so there a clicked button doesn't keep\n the card open once the pointer moves away.\n\nAnd around it:\n\n- Keyboard focus on the trigger opens the card, as hover does. Focus that a press brings doesn't,\n and neither does a touch pointer, so tapping the trigger on a phone just does what the trigger\n does. Clicking the trigger never toggles the card.\n- By default, then, a phone or tablet never opens the card, so whatever it shows has to be\n reachable from the trigger as well. `openOnTap` opens it with a tap, for a trigger with no tap\n action of its own. While a tap holds it open, a tap outside the trigger and the card closes it.\n- The trigger gets `aria-expanded` and `aria-controls`, which say it's expanded and name the card\n only while the card is on the page. It gets no role, no tab stop and no `aria-haspopup`, so the trigger element itself has to\n take focus, such as a link or a button. A link inside a `<span>` doesn't do, since focus comes\n back to the trigger and its ARIA is on it. A development build warns when the trigger can't take\n focus.\n- The card is a plain element with an `id` and no role. It isn't a dialog, since focus doesn't move\n into it, and it isn't a tooltip, since it can hold links. In place, a screen reader meets it\n right after the trigger, Tab goes from the trigger into its links and buttons, and tabbing past\n them closes it.\n- With `appendToBody` the card sits in the Tab order where its container is. By default that's\n the end of `<body>`, so it comes last: Tab reaches it only after the rest of the page, and focus\n leaving the trigger on the way closes it. Keyboard users can open it, read it and close it, so\n whatever it links to should be reachable from the page as well.\n- Opening the card moves no focus, and nothing traps it. Links and buttons belong in a card; a form\n belongs in a `Popover`, which opens on purpose and holds focus.\n- The fade-in is turned off under `prefers-reduced-motion: reduce`.",
"related": ["Media", "Popover", "Portal", "Tooltip", "useBulmaClasses"],
"storybook": "https://bestax.io/storybook/?path=/story/components-hovercard--default",
"summary": "The `HoverCard` component shows a card previewing what's behind a link or a button while the pointer rests on it or keyboard focus is on it. Nothing moves focus when the card opens, and nothing traps it. In place, the card comes right after the trigger, so Tab goes from the trigger into its links and tabbing past them closes it. The pointer can move onto the card, Escape closes it from anywhere, and by default touch doesn't open it, so tapping the trigger just does what the trigger does. `openOnTap` opens it with a tap, for a trigger with no tap action of its own. For hints with nothing to click use `Tooltip`, and for content opened on purpose, such as a form, use `Popover`.",
Expand Down
27 changes: 20 additions & 7 deletions bestax-mcp/data/components/Tooltip.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"title": "With Delay",
"code": "function example() {\n return (\n <Buttons>\n <Tooltip label=\"No delay\" delay={0}>\n <Button>No delay</Button>\n </Tooltip>\n <Tooltip label=\"500ms delay\" delay={500}>\n <Button>500ms delay</Button>\n </Tooltip>\n <Tooltip label=\"1 second delay\" delay={1000}>\n <Button>1s delay</Button>\n </Tooltip>\n </Buttons>\n );\n}"
},
{
"title": "On Touch Screens",
"code": "<Tooltip label=\"Orders over $50 ship free\" openOnTap>\n <Button>Shipping</Button>\n</Tooltip>"
},
{
"title": "On Icons",
"code": "function example() {\n return (\n <Block display=\"flex\" p=\"6\">\n <Tooltip label=\"Delete item\" color=\"danger\" position=\"bottom\">\n <Span cursor=\"pointer\">\n <Icon name=\"fas fa-trash\" />\n </Span>\n </Tooltip>\n <Tooltip label=\"Edit item\" color=\"info\" position=\"bottom\">\n <Span cursor=\"pointer\">\n <Icon name=\"fas fa-edit\" />\n </Span>\n </Tooltip>\n <Tooltip label=\"Download\" color=\"success\" position=\"bottom\">\n <Span cursor=\"pointer\">\n <Icon name=\"fas fa-download\" />\n </Span>\n </Tooltip>\n </Block>\n );\n}"
Expand All @@ -46,17 +50,17 @@
"code": "function example() {\n return (\n <Paragraph style={{ lineHeight: 1.8 }}>\n Hover over{' '}\n <Tooltip label=\"Cascading Style Sheets\" color=\"info\" dashed>\n <Span>CSS</Span>\n </Tooltip>{' '}\n or{' '}\n <Tooltip label=\"HyperText Markup Language\" color=\"info\" dashed>\n <Span>HTML</Span>\n </Tooltip>{' '}\n to see their full names.\n </Paragraph>\n );\n}"
}
],
"accessibility": "- Tooltip content has `role=\"tooltip\"` for screen reader announcement\n- Has `aria-hidden` that toggles based on visibility\n- Tooltip shows on both hover and focus for keyboard users\n- The `dashed` style provides a visual cue that more information is available",
"accessibility": "`Tooltip` is built to\n[WCAG's Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)\ncriterion:\n\n- **Dismissible:** <kbd>Escape</kbd> hides a shown tooltip wherever focus is, without you moving the\n pointer or focus, and it stays hidden until the pointer or focus leaves the trigger and comes\n back, or, with `openOnTap`, until the next tap on the trigger. The key goes on as well, to\n whatever has focus, so a control in the trigger, such as a `Dropdown`, still gets its own Escape.\n Inside a `Popover` or `Modal`, one Escape hides the tooltip and closes that too, since a tooltip\n only adds to what's there.\n- **Hoverable:** the pointer can move from the trigger onto a shown tooltip, straight across the gap\n between them, and it stays. A hidden tooltip, or one only `active` shows, lets the pointer through\n to whatever is under it.\n- **Persistent:** no timer hides a tooltip that's hovered or focused.\n\nAnd around it:\n\n- Keyboard focus on the trigger shows the tooltip, as hover does. Focus that a click or a tap brings\n doesn't, and neither does a touch pointer, so tapping the trigger just does what the trigger does.\n `openOnTap` shows it with a tap, for a trigger with no tap action of its own.\n- An `active` tooltip is always shown, whatever the pointer, focus and Escape do.\n- Tooltip content has `role=\"tooltip\"`, and `aria-hidden` while it's hidden.\n- The trigger isn't linked to the tooltip with `aria-describedby`, so a screen reader doesn't\n announce the tooltip with it. Give an icon-only trigger its own name, such as an `aria-label`.\n- The `dashed` style provides a visual cue that more information is available.",
"related": ["Button", "HoverCard", "Icon", "Popover"],
"storybook": null,
"summary": "The `Tooltip` component displays helpful information when users hover over or focus on an element.",
"summary": "The `Tooltip` component displays helpful information when users hover over or focus on an element. Hovering the trigger or giving it keyboard focus shows it. Touch doesn't, unless `openOnTap` is on, and neither does focus a click or a tap brings, so tapping the trigger just does what the trigger does. The pointer can move onto a shown tooltip. Escape hides it wherever focus is, without moving the pointer or focus, and it stays hidden until one of them leaves the trigger and comes back, or, with `openOnTap`, until the next tap on the trigger. The key always goes on too, to whatever has focus or a `Modal` or `Popover` around it, since a tooltip only adds to what's there.",
"import": "import { Tooltip } from '@allxsmith/bestax-bulma';",
"sourceFile": "bulma-ui/src/components/Tooltip.tsx",
"rootClass": "tooltip",
"parts": [
{
"path": "Tooltip",
"summary": "The `Tooltip` component displays helpful information when users hover over or focus on an element.",
"summary": "The `Tooltip` component displays helpful information when users hover over or focus on an element. Hovering the trigger or giving it keyboard focus shows it. Touch doesn't, unless `openOnTap` is on, and neither does focus a click or a tap brings, so tapping the trigger just does what the trigger does. The pointer can move onto a shown tooltip. Escape hides it wherever focus is, without moving the pointer or focus, and it stays hidden until one of them leaves the trigger and comes back, or, with `openOnTap`, until the next tap on the trigger. The key always goes on too, to whatever has focus or a `Modal` or `Popover` around it, since a tooltip only adds to what's there.",
"component": null,
"props": [
{
Expand All @@ -72,7 +76,7 @@
"name": "content",
"type": "React.ReactNode",
"default": null,
"description": "Rich tooltip content (alternative to `label`).",
"description": "Rich tooltip content (alternative to `label`), to read rather than use. Nothing in it should take focus or a click: it isn't in the Tab order while hidden, focus there isn't counted as being in the tooltip, and Escape hiding it would drop that focus. For links or buttons, use `HoverCard` or `Popover`.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down Expand Up @@ -108,7 +112,7 @@
"name": "active",
"type": "boolean",
"default": "false",
"description": "Force tooltip to be always visible.",
"description": "Force tooltip to be always visible. Hover, focus and Escape leave it shown, so Escape goes on to whatever has focus, or to a `Modal` or `Popover` around it.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down Expand Up @@ -153,7 +157,7 @@
"name": "delay",
"type": "number",
"default": "0",
"description": "Delay before showing tooltip (ms).",
"description": "How long the pointer or keyboard focus waits on the trigger before the tooltip shows, in milliseconds.",
"required": false,
"inherited": false,
"deprecated": false
Expand All @@ -162,7 +166,16 @@
"name": "closeDelay",
"type": "number",
"default": "0",
"description": "Delay in ms before hiding the tooltip.",
"description": "How long the tooltip stays after the pointer and focus have left the trigger and the tooltip, in milliseconds. The pointer can move from the trigger onto a shown tooltip without one, across the default gap. A theme that raises `--bulma-tooltip-arrow-size` or `--bulma-tooltip-arrow-margin` widens the gap past what the pointer can cross on its own, so give it a `closeDelay` long enough to cross the rest.",
"required": false,
"inherited": false,
"deprecated": false
},
{
"name": "openOnTap",
"type": "boolean",
"default": "false",
"description": "Show the tooltip with a tap on a touch screen. Touch has no hover, so without this a phone or tablet never shows it. With it, a tap on the trigger shows it at once, and it stays until another tap on the trigger or the tooltip, a tap anywhere else, or Escape. The tap still does whatever the trigger does, so turn this on only for a trigger with no tap action of its own. Mouse and pen work the same either way.",
"required": false,
"inherited": false,
"deprecated": false
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
{
"id": "css-variables",
"file": "references/css-variables.md",
"bytes": 32153
"bytes": 32380
},
{
"id": "themeable-components",
Expand Down
15 changes: 3 additions & 12 deletions bulma-ui/src/components/HoverCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';
import { Portal } from '../helpers/portal';
import { useIsHydrated } from '../helpers/useIsHydrated';
import { marginGap, useAnchoredPosition } from '../helpers/useAnchoredPosition';
import { useHoverOpen } from '../helpers/useHoverOpen';
import { chain, useHoverOpen } from '../helpers/useHoverOpen';
import { isTabbableKind } from '../helpers/useFocusTrap';
import { isDev, warnOnce } from '../helpers/devWarnings';
import type { PickerPosition } from '../form/_pickerInternals/pickerTypes';
Expand Down Expand Up @@ -133,17 +133,6 @@ export interface HoverCardProps
contentClassName?: string;
}

/** Runs the caller's handler on the wrapper, then the hover card's. */
function chain<E>(
own: ((event: E) => void) | undefined,
ours: (event: E) => void
): (event: E) => void {
return event => {
own?.(event);
ours(event);
};
}

/**
* The `HoverCard` component shows a card previewing what's behind a link or a button while the pointer rests on it or keyboard focus is on it.
*
Expand Down Expand Up @@ -184,6 +173,7 @@ export const HoverCard: React.FC<HoverCardProps> = ({
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
const {
onPointerEnter,
onPointerOver,
onPointerLeave,
onPointerDown,
onPointerUp,
Expand Down Expand Up @@ -298,6 +288,7 @@ export const HoverCard: React.FC<HoverCardProps> = ({
className={rootClasses}
{...attrs}
onPointerEnter={chain(onPointerEnter, hover.onPointerEnter)}
onPointerOver={chain(onPointerOver, hover.onPointerOver)}
onPointerLeave={chain(onPointerLeave, hover.onPointerLeave)}
onFocus={chain(onFocus, hover.onFocus)}
onBlur={chain(onBlur, hover.onBlur)}
Expand Down
24 changes: 23 additions & 1 deletion bulma-ui/src/components/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ const meta: Meta<typeof Tooltip> = {
},
closeDelay: {
control: 'number',
description: 'Delay before hiding tooltip after mouse leave (ms)',
description:
'Delay before hiding tooltip once the pointer and focus have left (ms)',
},
openOnTap: {
control: 'boolean',
description:
'Show the tooltip with a tap on a touch screen; only for a trigger with no tap action of its own',
},
},
};
Expand Down Expand Up @@ -279,6 +285,22 @@ export const WithCloseDelay: Story = {
),
};

/**
* `openOnTap`, for a touch screen, which has no hover. A tap on the trigger
* shows the tooltip at once, and another tap, a tap anywhere else or Escape
* hides it. The trigger is a button with no click action of its own, since
* the tap still does whatever the trigger does.
*/
export const TouchOpen: Story = {
render: () => (
<Box p="6">
<Tooltip label="Orders over $50 ship free" openOnTap>
<Button>Shipping</Button>
</Tooltip>
</Box>
),
};

/**
* Tooltip without animation.
*/
Expand Down
Loading
Loading