@allxsmith/bestax-bulma 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,25 @@
1
1
  /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
- import { useState, useRef, useLayoutEffect } from 'react';
3
+ import { useState, useRef, useId, isValidElement, Fragment, useEffect, useLayoutEffect, cloneElement } from 'react';
4
4
  import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
5
5
  import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
6
6
  import { useHoverOpen, chain } from '../helpers/useHoverOpen.js';
7
+ import { isTabbableKind } from '../helpers/useFocusTrap.js';
8
+ import { isDev, warnOnce } from '../helpers/devWarnings.js';
7
9
 
10
+ /** An id list with `id` added at the end, unless it's in there already. */
11
+ const withId = (ids, id) => [...new Set([...(ids ?? '').split(/\s+/), id])].filter(Boolean).join(' ');
12
+ /**
13
+ * The first element in the trigger that Tab lands on, judged by what it is
14
+ * rather than by its styles, which can hide it for now. The trigger is all
15
+ * of the wrapper that comes before the tooltip.
16
+ */
17
+ const triggerTabStop = (wrapper, tooltip) => {
18
+ const elements = Array.from(wrapper.querySelectorAll('*'));
19
+ return elements
20
+ .slice(0, elements.indexOf(tooltip))
21
+ .find(el => isTabbableKind(el) && !el.matches(':disabled'));
22
+ };
8
23
  /**
9
24
  * The `Tooltip` component displays helpful information when users hover over or focus on an element.
10
25
  *
@@ -16,7 +31,8 @@ import { useHoverOpen, chain } from '../helpers/useHoverOpen.js';
16
31
  * the trigger and comes back, or, with `openOnTap`, until the next tap on
17
32
  * the trigger. The key always goes on too, to whatever has focus or a
18
33
  * `Modal` or `Popover` around it, since a tooltip only adds to what's
19
- * there.
34
+ * there. The tooltip is its trigger's description, so a screen reader reads
35
+ * it as the trigger takes focus, whether it's showing yet or not.
20
36
  *
21
37
  * @function
22
38
  * @param {TooltipProps} props - Props for the Tooltip component.
@@ -29,15 +45,17 @@ import { useHoverOpen, chain } from '../helpers/useHoverOpen.js';
29
45
  * </Tooltip>
30
46
  *
31
47
  * @example
32
- * // Positioned tooltip with color
33
- * <Tooltip label="Delete item" position="right" color="danger">
34
- * <Icon icon="fas fa-trash" />
48
+ * // Positioned tooltip with color, on an icon-only button with a name of its own
49
+ * <Tooltip label="Moves it to the trash" position="right" color="danger">
50
+ * <Button aria-label="Delete">
51
+ * <Icon name="fas fa-trash" />
52
+ * </Button>
35
53
  * </Tooltip>
36
54
  *
37
55
  * @example
38
56
  * // Multiline tooltip
39
57
  * <Tooltip label="This is a longer tooltip that wraps to multiple lines" multiline>
40
- * <span>?</span>
58
+ * <button>More info</button>
41
59
  * </Tooltip>
42
60
  */
43
61
  const Tooltip = ({ label, content, position = 'top', color, size, active: alwaysActive, multiline = false, animated = true, square = false, dashed = false, delay = 0, closeDelay = 0, openOnTap = false, children, className, tooltipClassName, ...props }) => {
@@ -48,6 +66,30 @@ const Tooltip = ({ label, content, position = 'top', color, size, active: always
48
66
  const [contentStyle, setContentStyle] = useState({});
49
67
  const wrapperRef = useRef(null);
50
68
  const contentRef = useRef(null);
69
+ const tooltipId = `${useId()}-tooltip`;
70
+ // Only a single element can be given the link, and a fragment takes no
71
+ // props of its own.
72
+ const linkable = isValidElement(children) &&
73
+ children.type !== Fragment;
74
+ // A screen reader reads the tooltip only when Tab lands on an element that
75
+ // carries the link. Only the page shows whether it does, since a component
76
+ // may not pass the attribute on, or what takes focus may sit inside the
77
+ // element that took it. After every commit, since the children can change,
78
+ // and only in development.
79
+ useEffect(() => {
80
+ if (!isDev())
81
+ return;
82
+ const stop = triggerTabStop(wrapperRef.current, contentRef.current);
83
+ if (stop?.getAttribute('aria-describedby')?.split(/\s+/).includes(tooltipId)) {
84
+ return;
85
+ }
86
+ warnOnce('Tooltip:unlinked-trigger', "[bestax-bulma] <Tooltip> can't describe its trigger, so a screen " +
87
+ "reader won't read the tooltip: Tab reaches nothing in the trigger, " +
88
+ "or what it reaches doesn't carry the aria-describedby that points " +
89
+ 'at the tooltip. Pass one element that takes focus and passes ' +
90
+ 'aria-describedby on to its DOM node, such as a button, a link or a ' +
91
+ 'Span with tabIndex={0}.');
92
+ });
51
93
  const hover = useHoverOpen({
52
94
  // The pointer and focus as they are. `active` shows it on top of that, so
53
95
  // dropping `active` leaves it shown while the pointer or focus holds it.
@@ -170,7 +212,15 @@ const Tooltip = ({ label, content, position = 'top', color, size, active: always
170
212
  });
171
213
  const combinedClasses = classNames(tooltipClasses, bulmaHelperClasses, className);
172
214
  const combinedTooltipClasses = usePrefixedClassNames('tooltip-content', tooltipClassName);
173
- return (jsxs("span", { ref: wrapperRef, className: combinedClasses, 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), onPointerDown: chain(onPointerDown, hover.onPointerDown), onPointerUp: chain(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [children, jsx("span", { ref: contentRef, className: combinedTooltipClasses, role: "tooltip", "aria-hidden": !isVisible && !alwaysActive, style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
215
+ return (jsxs("span", { ref: wrapperRef, className: combinedClasses, 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), onPointerDown: chain(onPointerDown, hover.onPointerDown), onPointerUp: chain(onPointerUp, hover.onPointerUp), ...(label ? { 'data-tooltip': label } : {}), ...attrs, children: [linkable
216
+ ? cloneElement(children, {
217
+ 'aria-describedby': withId(children.props['aria-describedby'], tooltipId),
218
+ })
219
+ : children, jsx("span", { ref: contentRef, id: tooltipId, className: combinedTooltipClasses,
220
+ // Hidden by `visibility: hidden` in the stylesheet, which keeps it out
221
+ // of the accessibility tree and still lets it describe the trigger
222
+ // (accname step 2A), so it takes no aria-hidden.
223
+ role: "tooltip", style: Object.keys(contentStyle).length > 0 ? contentStyle : undefined, children: content || label })] }));
174
224
  };
175
225
 
176
226
  export { Tooltip };
@@ -1 +1 @@
1
- {"version":3,"file":"Tooltip.js","sources":["../../../src/components/Tooltip.tsx"],"sourcesContent":["import React, { useState, useRef, useLayoutEffect } from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';\nimport { chain, useHoverOpen } from '../helpers/useHoverOpen';\n\n/** Screen positions for the tooltip. */\nexport type TooltipPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';\n/** Color variants for the tooltip. */\nexport type TooltipColor =\n | 'primary'\n | 'link'\n | 'info'\n | 'success'\n | 'warning'\n | 'danger'\n | 'dark'\n | 'light';\n/** Size variants for the tooltip. */\nexport type TooltipSize = 'small' | 'medium' | 'large';\n\n/**\n * Props for the Tooltip component.\n * @extraProp {React.ReactNode} [children] - The element that triggers the tooltip.\n * @extraProp {string} [className] - Additional CSS classes.\n */\nexport interface TooltipProps\n extends\n Omit<React.HTMLAttributes<HTMLSpanElement>, 'color' | 'content'>,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** The tooltip text content. */\n label?: string;\n /**\n * Rich tooltip content (alternative to `label`), to read rather than use.\n * Nothing in it should take focus or a click: it isn't in the Tab order\n * while hidden, focus there isn't counted as being in the tooltip, and\n * Escape hiding it would drop that focus. For links or buttons, use\n * `HoverCard` or `Popover`.\n */\n content?: React.ReactNode;\n /** Position of the tooltip. Default: 'top'. */\n position?: TooltipPosition;\n /** Color variant for the tooltip. */\n color?: TooltipColor;\n /** Size of the tooltip. */\n size?: TooltipSize;\n /**\n * Force tooltip to be always visible. Hover, focus and Escape leave it\n * shown, so Escape goes on to whatever has focus, or to a `Modal` or\n * `Popover` around it.\n */\n active?: boolean;\n /** Allow tooltip to wrap to multiple lines. */\n multiline?: boolean;\n /** Enable fade animation. Default: true. */\n animated?: boolean;\n /** Use square corners instead of rounded. */\n square?: boolean;\n /** Show dashed underline on trigger. */\n dashed?: boolean;\n /**\n * How long the pointer or keyboard focus waits on the trigger before the\n * tooltip shows, in milliseconds.\n */\n delay?: number;\n /**\n * How long the tooltip stays after the pointer and focus have left the\n * trigger and the tooltip, in milliseconds. The pointer can move from the\n * trigger onto a shown tooltip without one, across the default gap. A\n * theme that raises `--bulma-tooltip-arrow-size` or\n * `--bulma-tooltip-arrow-margin` widens the gap past what the pointer can\n * cross on its own, so give it a `closeDelay` long enough to cross the\n * rest.\n */\n closeDelay?: number;\n /**\n * Show the tooltip with a tap on a touch screen. Touch has no hover, so\n * without this a phone or tablet never shows it. With it, a tap on the\n * trigger shows it at once, and it stays until another tap on the trigger\n * or the tooltip, a tap anywhere else, or Escape. The tap still does\n * whatever the trigger does, so turn this on only for a trigger with no\n * tap action of its own. Mouse and pen work the same either way.\n * @defaultValue false\n */\n openOnTap?: boolean;\n /** Additional classes for the tooltip element. */\n tooltipClassName?: string;\n}\n\n/**\n * The `Tooltip` component displays helpful information when users hover over or focus on an element.\n *\n * Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,\n * unless `openOnTap` is on, and neither does focus a click or a tap brings,\n * so tapping the trigger just does what the trigger does. The pointer can\n * move onto a shown tooltip. Escape hides it wherever focus is, without\n * moving the pointer or focus, and it stays hidden until one of them leaves\n * the trigger and comes back, or, with `openOnTap`, until the next tap on\n * the trigger. The key always goes on too, to whatever has focus or a\n * `Modal` or `Popover` around it, since a tooltip only adds to what's\n * there.\n *\n * @function\n * @param {TooltipProps} props - Props for the Tooltip component.\n * @returns {JSX.Element} The rendered tooltip wrapper.\n *\n * @example\n * // Basic tooltip\n * <Tooltip label=\"This is helpful info\">\n * <button>Hover me</button>\n * </Tooltip>\n *\n * @example\n * // Positioned tooltip with color\n * <Tooltip label=\"Delete item\" position=\"right\" color=\"danger\">\n * <Icon icon=\"fas fa-trash\" />\n * </Tooltip>\n *\n * @example\n * // Multiline tooltip\n * <Tooltip label=\"This is a longer tooltip that wraps to multiple lines\" multiline>\n * <span>?</span>\n * </Tooltip>\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n label,\n content,\n position = 'top',\n color,\n size,\n active: alwaysActive,\n multiline = false,\n animated = true,\n square = false,\n dashed = false,\n delay = 0,\n closeDelay = 0,\n openOnTap = false,\n children,\n className,\n tooltipClassName,\n ...props\n}) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses(props);\n const {\n onPointerEnter,\n onPointerOver,\n onPointerLeave,\n onPointerDown,\n onPointerUp,\n onFocus,\n onBlur,\n ...attrs\n } = rest;\n const [isVisible, setIsVisible] = useState(false);\n const [resolvedPosition, setResolvedPosition] = useState<\n Exclude<TooltipPosition, 'auto'>\n >(position === 'auto' ? 'top' : position);\n const [contentStyle, setContentStyle] = useState<React.CSSProperties>({});\n const wrapperRef = useRef<HTMLSpanElement | null>(null);\n const contentRef = useRef<HTMLSpanElement | null>(null);\n\n const hover = useHoverOpen({\n // The pointer and focus as they are. `active` shows it on top of that, so\n // dropping `active` leaves it shown while the pointer or focus holds it.\n // An ask to close can come while it's already hidden, which is no change.\n open: isVisible,\n onOpenChange: setIsVisible,\n openDelay: delay,\n closeDelay,\n // The wrapper stands in for the trigger. It holds only the children and\n // the tooltip, and nothing in the tooltip takes focus, so focus anywhere\n // in it is focus on the trigger.\n triggerRef: wrapperRef,\n floatingRef: contentRef,\n // Escape hides it and carries on, so a control in the trigger, or a\n // Modal around it, still gets the key.\n consumeEscape: false,\n openOnTap,\n });\n\n // Auto-placement and overflow correction\n useLayoutEffect(() => {\n const isAuto = position === 'auto';\n\n if (!isAuto) {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement\n setResolvedPosition(position);\n }\n\n const showing = isVisible || alwaysActive;\n if (!showing) {\n setContentStyle({});\n return;\n }\n\n const wrapper = wrapperRef.current;\n const content = contentRef.current;\n if (!wrapper || !content) return;\n\n const wrapperRect = wrapper.getBoundingClientRect();\n const margin = 40;\n const edgePadding = 8; // minimum distance from viewport edge\n\n // Step 1: determine position (auto mode only)\n if (isAuto) {\n const spaceTop = wrapperRect.top;\n const spaceBottom = window.innerHeight - wrapperRect.bottom;\n\n if (spaceTop >= margin) {\n setResolvedPosition('top');\n } else if (spaceBottom >= margin) {\n setResolvedPosition('bottom');\n } else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {\n setResolvedPosition('right');\n } else {\n setResolvedPosition('left');\n }\n }\n\n // Step 2: measure tooltip content and correct overflow.\n // Use requestAnimationFrame so the resolved position class is applied first.\n requestAnimationFrame(() => {\n const contentEl = contentRef.current;\n if (!contentEl) return;\n\n const contentRect = contentEl.getBoundingClientRect();\n const style: React.CSSProperties = {};\n\n // For top/bottom positions, tooltip is centered horizontally via CSS\n // (left: 50%; transform: translateX(-50%)).\n // Check if it overflows left or right and apply a corrective shift.\n const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto\n const effectivePos = pos || 'top'; // fallback, actual is set by state\n const isVertical =\n effectivePos === 'top' ||\n effectivePos === 'bottom' ||\n resolvedPosition === 'top' ||\n resolvedPosition === 'bottom';\n\n if (isVertical) {\n if (contentRect.left < edgePadding) {\n // Overflows left — shift right\n const shift = edgePadding - contentRect.left;\n style.left = `calc(50% + ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${-shift}px`;\n } else if (contentRect.right > window.innerWidth - edgePadding) {\n // Overflows right — shift left\n const shift = contentRect.right - (window.innerWidth - edgePadding);\n style.left = `calc(50% - ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${shift}px`;\n }\n } else {\n // For left/right positions, tooltip is centered vertically via CSS\n // (top: 50%; transform: translateY(-50%)).\n if (contentRect.top < edgePadding) {\n const shift = edgePadding - contentRect.top;\n style.top = `calc(50% + ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${-shift}px`;\n } else if (contentRect.bottom > window.innerHeight - edgePadding) {\n const shift = contentRect.bottom - (window.innerHeight - edgePadding);\n style.top = `calc(50% - ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${shift}px`;\n }\n }\n\n setContentStyle(style);\n });\n }, [position, resolvedPosition, isVisible, alwaysActive, label, content]);\n\n // Generate Bulma classes\n const tooltipClasses = usePrefixedClassNames('tooltip', {\n 'is-active': alwaysActive || isVisible,\n // Open by the pointer, focus or a tap, rather than shown by `active`\n // alone: only then does the tooltip, and the gap to it, take the pointer.\n 'is-open': isVisible,\n [`is-${resolvedPosition}`]: resolvedPosition,\n [`is-${color}`]: color,\n [`is-${size}`]: size,\n 'is-multiline': multiline,\n 'is-animated': animated,\n 'is-square': square,\n 'is-dashed': dashed,\n });\n\n const combinedClasses = classNames(\n tooltipClasses,\n bulmaHelperClasses,\n className\n );\n const combinedTooltipClasses = usePrefixedClassNames(\n 'tooltip-content',\n tooltipClassName\n );\n\n return (\n // The tooltip is inside the wrapper, so the pointer on it is still on the\n // wrapper, and the stylesheet bridges the gap between them.\n <span\n ref={wrapperRef}\n className={combinedClasses}\n onPointerEnter={chain(onPointerEnter, hover.onPointerEnter)}\n onPointerOver={chain(onPointerOver, hover.onPointerOver)}\n onPointerLeave={chain(onPointerLeave, hover.onPointerLeave)}\n onFocus={chain(onFocus, hover.onFocus)}\n onBlur={chain(onBlur, hover.onBlur)}\n onPointerDown={chain(onPointerDown, hover.onPointerDown)}\n onPointerUp={chain(onPointerUp, hover.onPointerUp)}\n {...(label ? { 'data-tooltip': label } : {})}\n {...attrs}\n >\n {children}\n <span\n ref={contentRef}\n className={combinedTooltipClasses}\n role=\"tooltip\"\n aria-hidden={!isVisible && !alwaysActive}\n style={Object.keys(contentStyle).length > 0 ? contentStyle : undefined}\n >\n {content || label}\n </span>\n </span>\n );\n};\n\nexport default Tooltip;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"Tooltip.js","sources":["../../../src/components/Tooltip.tsx"],"sourcesContent":["import React, {\n Fragment,\n cloneElement,\n isValidElement,\n useEffect,\n useId,\n useState,\n useRef,\n useLayoutEffect,\n} from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';\nimport { chain, useHoverOpen } from '../helpers/useHoverOpen';\nimport { isTabbableKind } from '../helpers/useFocusTrap';\nimport { isDev, warnOnce } from '../helpers/devWarnings';\n\n/** Screen positions for the tooltip. */\nexport type TooltipPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';\n/** Color variants for the tooltip. */\nexport type TooltipColor =\n | 'primary'\n | 'link'\n | 'info'\n | 'success'\n | 'warning'\n | 'danger'\n | 'dark'\n | 'light';\n/** Size variants for the tooltip. */\nexport type TooltipSize = 'small' | 'medium' | 'large';\n\n/** An id list with `id` added at the end, unless it's in there already. */\nconst withId = (ids: string | undefined, id: string) =>\n [...new Set([...(ids ?? '').split(/\\s+/), id])].filter(Boolean).join(' ');\n\n/**\n * The first element in the trigger that Tab lands on, judged by what it is\n * rather than by its styles, which can hide it for now. The trigger is all\n * of the wrapper that comes before the tooltip.\n */\nconst triggerTabStop = (wrapper: Element, tooltip: Element) => {\n const elements = Array.from(wrapper.querySelectorAll('*'));\n return elements\n .slice(0, elements.indexOf(tooltip))\n .find(el => isTabbableKind(el) && !el.matches(':disabled'));\n};\n\n/**\n * Props for the Tooltip component.\n * @extraProp {React.ReactNode} [children] - The element that triggers the\n * tooltip. Its `aria-describedby` points at the tooltip, after any ids it\n * already had, so a screen reader reads the tooltip as its description when\n * it takes focus, even while the tooltip is hidden. That takes a single\n * element which passes `aria-describedby` on to its DOM node, as native\n * elements and bestax components do, and takes focus itself, such as a\n * button, a link or a `Span` with `tabIndex={0}`: something focusable inside\n * it doesn't get the link, and text, a fragment or several elements get none.\n * A development build warns when the first element Tab reaches in the\n * trigger doesn't carry the link, or there isn't one.\n * @extraProp {string} [className] - Additional CSS classes.\n */\nexport interface TooltipProps\n extends\n Omit<React.HTMLAttributes<HTMLSpanElement>, 'color' | 'content'>,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** The tooltip text content. */\n label?: string;\n /**\n * Rich tooltip content (alternative to `label`), to read rather than use.\n * Nothing in it should take focus or a click: it isn't in the Tab order\n * while hidden, focus there isn't counted as being in the tooltip, and\n * Escape hiding it would drop that focus. For links or buttons, use\n * `HoverCard` or `Popover`. A screen reader reads it as plain text, since\n * it's the trigger's description.\n */\n content?: React.ReactNode;\n /** Position of the tooltip. Default: 'top'. */\n position?: TooltipPosition;\n /** Color variant for the tooltip. */\n color?: TooltipColor;\n /** Size of the tooltip. */\n size?: TooltipSize;\n /**\n * Force tooltip to be always visible. Hover, focus and Escape leave it\n * shown, so Escape goes on to whatever has focus, or to a `Modal` or\n * `Popover` around it.\n */\n active?: boolean;\n /** Allow tooltip to wrap to multiple lines. */\n multiline?: boolean;\n /** Enable fade animation. Default: true. */\n animated?: boolean;\n /** Use square corners instead of rounded. */\n square?: boolean;\n /** Show dashed underline on trigger. */\n dashed?: boolean;\n /**\n * How long the pointer or keyboard focus waits on the trigger before the\n * tooltip shows, in milliseconds.\n */\n delay?: number;\n /**\n * How long the tooltip stays after the pointer and focus have left the\n * trigger and the tooltip, in milliseconds. The pointer can move from the\n * trigger onto a shown tooltip without one, across the default gap. A\n * theme that raises `--bulma-tooltip-arrow-size` or\n * `--bulma-tooltip-arrow-margin` widens the gap past what the pointer can\n * cross on its own, so give it a `closeDelay` long enough to cross the\n * rest.\n */\n closeDelay?: number;\n /**\n * Show the tooltip with a tap on a touch screen. Touch has no hover, so\n * without this a phone or tablet never shows it. With it, a tap on the\n * trigger shows it at once, and it stays until another tap on the trigger\n * or the tooltip, a tap anywhere else, or Escape. The tap still does\n * whatever the trigger does, so turn this on only for a trigger with no\n * tap action of its own. Mouse and pen work the same either way.\n * @defaultValue false\n */\n openOnTap?: boolean;\n /** Additional classes for the tooltip element. */\n tooltipClassName?: string;\n}\n\n/**\n * The `Tooltip` component displays helpful information when users hover over or focus on an element.\n *\n * Hovering the trigger or giving it keyboard focus shows it. Touch doesn't,\n * unless `openOnTap` is on, and neither does focus a click or a tap brings,\n * so tapping the trigger just does what the trigger does. The pointer can\n * move onto a shown tooltip. Escape hides it wherever focus is, without\n * moving the pointer or focus, and it stays hidden until one of them leaves\n * the trigger and comes back, or, with `openOnTap`, until the next tap on\n * the trigger. The key always goes on too, to whatever has focus or a\n * `Modal` or `Popover` around it, since a tooltip only adds to what's\n * there. The tooltip is its trigger's description, so a screen reader reads\n * it as the trigger takes focus, whether it's showing yet or not.\n *\n * @function\n * @param {TooltipProps} props - Props for the Tooltip component.\n * @returns {JSX.Element} The rendered tooltip wrapper.\n *\n * @example\n * // Basic tooltip\n * <Tooltip label=\"This is helpful info\">\n * <button>Hover me</button>\n * </Tooltip>\n *\n * @example\n * // Positioned tooltip with color, on an icon-only button with a name of its own\n * <Tooltip label=\"Moves it to the trash\" position=\"right\" color=\"danger\">\n * <Button aria-label=\"Delete\">\n * <Icon name=\"fas fa-trash\" />\n * </Button>\n * </Tooltip>\n *\n * @example\n * // Multiline tooltip\n * <Tooltip label=\"This is a longer tooltip that wraps to multiple lines\" multiline>\n * <button>More info</button>\n * </Tooltip>\n */\nexport const Tooltip: React.FC<TooltipProps> = ({\n label,\n content,\n position = 'top',\n color,\n size,\n active: alwaysActive,\n multiline = false,\n animated = true,\n square = false,\n dashed = false,\n delay = 0,\n closeDelay = 0,\n openOnTap = false,\n children,\n className,\n tooltipClassName,\n ...props\n}) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses(props);\n const {\n onPointerEnter,\n onPointerOver,\n onPointerLeave,\n onPointerDown,\n onPointerUp,\n onFocus,\n onBlur,\n ...attrs\n } = rest;\n const [isVisible, setIsVisible] = useState(false);\n const [resolvedPosition, setResolvedPosition] = useState<\n Exclude<TooltipPosition, 'auto'>\n >(position === 'auto' ? 'top' : position);\n const [contentStyle, setContentStyle] = useState<React.CSSProperties>({});\n const wrapperRef = useRef<HTMLSpanElement | null>(null);\n const contentRef = useRef<HTMLSpanElement | null>(null);\n const tooltipId = `${useId()}-tooltip`;\n\n // Only a single element can be given the link, and a fragment takes no\n // props of its own.\n const linkable =\n isValidElement<{ 'aria-describedby'?: string }>(children) &&\n children.type !== Fragment;\n\n // A screen reader reads the tooltip only when Tab lands on an element that\n // carries the link. Only the page shows whether it does, since a component\n // may not pass the attribute on, or what takes focus may sit inside the\n // element that took it. After every commit, since the children can change,\n // and only in development.\n useEffect(() => {\n if (!isDev()) return;\n const stop = triggerTabStop(\n wrapperRef.current as HTMLSpanElement,\n contentRef.current as HTMLSpanElement\n );\n if (\n stop?.getAttribute('aria-describedby')?.split(/\\s+/).includes(tooltipId)\n ) {\n return;\n }\n warnOnce(\n 'Tooltip:unlinked-trigger',\n \"[bestax-bulma] <Tooltip> can't describe its trigger, so a screen \" +\n \"reader won't read the tooltip: Tab reaches nothing in the trigger, \" +\n \"or what it reaches doesn't carry the aria-describedby that points \" +\n 'at the tooltip. Pass one element that takes focus and passes ' +\n 'aria-describedby on to its DOM node, such as a button, a link or a ' +\n 'Span with tabIndex={0}.'\n );\n });\n\n const hover = useHoverOpen({\n // The pointer and focus as they are. `active` shows it on top of that, so\n // dropping `active` leaves it shown while the pointer or focus holds it.\n // An ask to close can come while it's already hidden, which is no change.\n open: isVisible,\n onOpenChange: setIsVisible,\n openDelay: delay,\n closeDelay,\n // The wrapper stands in for the trigger. It holds only the children and\n // the tooltip, and nothing in the tooltip takes focus, so focus anywhere\n // in it is focus on the trigger.\n triggerRef: wrapperRef,\n floatingRef: contentRef,\n // Escape hides it and carries on, so a control in the trigger, or a\n // Modal around it, still gets the key.\n consumeEscape: false,\n openOnTap,\n });\n\n // Auto-placement and overflow correction\n useLayoutEffect(() => {\n const isAuto = position === 'auto';\n\n if (!isAuto) {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- layout measurement effect resolves placement\n setResolvedPosition(position);\n }\n\n const showing = isVisible || alwaysActive;\n if (!showing) {\n setContentStyle({});\n return;\n }\n\n const wrapper = wrapperRef.current;\n const content = contentRef.current;\n if (!wrapper || !content) return;\n\n const wrapperRect = wrapper.getBoundingClientRect();\n const margin = 40;\n const edgePadding = 8; // minimum distance from viewport edge\n\n // Step 1: determine position (auto mode only)\n if (isAuto) {\n const spaceTop = wrapperRect.top;\n const spaceBottom = window.innerHeight - wrapperRect.bottom;\n\n if (spaceTop >= margin) {\n setResolvedPosition('top');\n } else if (spaceBottom >= margin) {\n setResolvedPosition('bottom');\n } else if (window.innerWidth - wrapperRect.right >= wrapperRect.left) {\n setResolvedPosition('right');\n } else {\n setResolvedPosition('left');\n }\n }\n\n // Step 2: measure tooltip content and correct overflow.\n // Use requestAnimationFrame so the resolved position class is applied first.\n requestAnimationFrame(() => {\n const contentEl = contentRef.current;\n if (!contentEl) return;\n\n const contentRect = contentEl.getBoundingClientRect();\n const style: React.CSSProperties = {};\n\n // For top/bottom positions, tooltip is centered horizontally via CSS\n // (left: 50%; transform: translateX(-50%)).\n // Check if it overflows left or right and apply a corrective shift.\n const pos = isAuto ? undefined : position; // read from state won't be updated yet in auto\n const effectivePos = pos || 'top'; // fallback, actual is set by state\n const isVertical =\n effectivePos === 'top' ||\n effectivePos === 'bottom' ||\n resolvedPosition === 'top' ||\n resolvedPosition === 'bottom';\n\n if (isVertical) {\n if (contentRect.left < edgePadding) {\n // Overflows left — shift right\n const shift = edgePadding - contentRect.left;\n style.left = `calc(50% + ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${-shift}px`;\n } else if (contentRect.right > window.innerWidth - edgePadding) {\n // Overflows right — shift left\n const shift = contentRect.right - (window.innerWidth - edgePadding);\n style.left = `calc(50% - ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${shift}px`;\n }\n } else {\n // For left/right positions, tooltip is centered vertically via CSS\n // (top: 50%; transform: translateY(-50%)).\n if (contentRect.top < edgePadding) {\n const shift = edgePadding - contentRect.top;\n style.top = `calc(50% + ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${-shift}px`;\n } else if (contentRect.bottom > window.innerHeight - edgePadding) {\n const shift = contentRect.bottom - (window.innerHeight - edgePadding);\n style.top = `calc(50% - ${shift}px)`;\n (style as Record<string, string>)['--tooltip-arrow-offset'] =\n `${shift}px`;\n }\n }\n\n setContentStyle(style);\n });\n }, [position, resolvedPosition, isVisible, alwaysActive, label, content]);\n\n // Generate Bulma classes\n const tooltipClasses = usePrefixedClassNames('tooltip', {\n 'is-active': alwaysActive || isVisible,\n // Open by the pointer, focus or a tap, rather than shown by `active`\n // alone: only then does the tooltip, and the gap to it, take the pointer.\n 'is-open': isVisible,\n [`is-${resolvedPosition}`]: resolvedPosition,\n [`is-${color}`]: color,\n [`is-${size}`]: size,\n 'is-multiline': multiline,\n 'is-animated': animated,\n 'is-square': square,\n 'is-dashed': dashed,\n });\n\n const combinedClasses = classNames(\n tooltipClasses,\n bulmaHelperClasses,\n className\n );\n const combinedTooltipClasses = usePrefixedClassNames(\n 'tooltip-content',\n tooltipClassName\n );\n\n return (\n // The tooltip is inside the wrapper, so the pointer on it is still on the\n // wrapper, and the stylesheet bridges the gap between them.\n <span\n ref={wrapperRef}\n className={combinedClasses}\n onPointerEnter={chain(onPointerEnter, hover.onPointerEnter)}\n onPointerOver={chain(onPointerOver, hover.onPointerOver)}\n onPointerLeave={chain(onPointerLeave, hover.onPointerLeave)}\n onFocus={chain(onFocus, hover.onFocus)}\n onBlur={chain(onBlur, hover.onBlur)}\n onPointerDown={chain(onPointerDown, hover.onPointerDown)}\n onPointerUp={chain(onPointerUp, hover.onPointerUp)}\n {...(label ? { 'data-tooltip': label } : {})}\n {...attrs}\n >\n {linkable\n ? cloneElement(children, {\n 'aria-describedby': withId(\n children.props['aria-describedby'],\n tooltipId\n ),\n })\n : children}\n <span\n ref={contentRef}\n id={tooltipId}\n className={combinedTooltipClasses}\n // Hidden by `visibility: hidden` in the stylesheet, which keeps it out\n // of the accessibility tree and still lets it describe the trigger\n // (accname step 2A), so it takes no aria-hidden.\n role=\"tooltip\"\n style={Object.keys(contentStyle).length > 0 ? contentStyle : undefined}\n >\n {content || label}\n </span>\n </span>\n );\n};\n\nexport default Tooltip;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -4,6 +4,7 @@ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
4
4
  import { withSubComponents } from '../helpers/withSubComponents.js';
5
5
  import { Button } from './Button.js';
6
6
  import { LinkButton } from './LinkButton.js';
7
+ import { ToggleButton } from './ToggleButton.js';
7
8
  import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
8
9
 
9
10
  const validButtonsSizes = ['small', 'medium', 'large'];
@@ -36,6 +37,7 @@ const ButtonsComponent = ({ className, textColor, color, bgColor, isCentered, is
36
37
  const Buttons = withSubComponents(ButtonsComponent, {
37
38
  Button,
38
39
  LinkButton,
40
+ ToggleButton,
39
41
  }, 'Buttons');
40
42
 
41
43
  export { Buttons };
@@ -1 +1 @@
1
- {"version":3,"file":"Buttons.js","sources":["../../../src/elements/Buttons.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { withSubComponents } from '../helpers/withSubComponents';\nimport { Button } from './Button';\nimport { LinkButton } from './LinkButton';\nimport {\n useBulmaClasses,\n BulmaClassesProps,\n validColors,\n} from '../helpers/useBulmaClasses';\n\nconst validButtonsSizes = ['small', 'medium', 'large'] as const;\n/**\n * Valid size values for the Buttons component (Bulma button group sizes).\n */\nexport type ButtonsSize = (typeof validButtonsSizes)[number];\n\n/**\n * Props for the Buttons component.\n */\ninterface ButtonsProps\n extends\n React.HTMLAttributes<HTMLDivElement>,\n Omit<BulmaClassesProps, 'size'> {\n /** Additional CSS classes to apply. */\n className?: string;\n /** Text color helper for the button group. */\n textColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /**\n * Text color alias: renders `has-text-<color>`, exactly like `textColor`.\n * Not a filled variant (no `.buttons.is-<color>` CSS exists; color the\n * individual `Button`s instead). Prefer `textColor`, which takes precedence\n * when both are set; use `bgColor` for a colored surface.\n */\n color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n /** Background color helper for the button group. */\n bgColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Center the group of buttons. */\n isCentered?: boolean;\n /** Align the group of buttons to the right. */\n isRight?: boolean;\n /** Group buttons together as addons (removes spacing between them). */\n hasAddons?: boolean;\n /** Size of the button group. */\n size?: ButtonsSize;\n /** The button elements to render inside the group. */\n children: React.ReactNode;\n}\n\n/**\n * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.\n *\n * @function\n * @param {ButtonsProps} props - Props for the Buttons component.\n * @returns {JSX.Element} The rendered group of buttons.\n * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}\n */\nconst ButtonsComponent: React.FC<ButtonsProps> = ({\n className,\n textColor,\n color,\n bgColor,\n isCentered,\n isRight,\n hasAddons,\n size,\n children,\n ...props\n}) => {\n const buttonsClasses = usePrefixedClassNames('buttons', {\n 'is-centered': isCentered,\n 'is-right': isRight,\n 'has-addons': hasAddons,\n [`are-${size}`]: size && validButtonsSizes.includes(size),\n });\n\n /**\n * Generates Bulma helper classes and separates out remaining props.\n */\n const { bulmaHelperClasses, rest } = useBulmaClasses({\n color: textColor ?? color,\n backgroundColor: bgColor,\n ...props,\n });\n\n const combinedClasses = classNames(\n buttonsClasses,\n className,\n bulmaHelperClasses\n );\n\n return (\n <div className={combinedClasses} {...rest}>\n {children}\n </div>\n );\n};\n\nexport const Buttons = withSubComponents(\n ButtonsComponent,\n {\n Button,\n LinkButton,\n },\n 'Buttons'\n);\n"],"names":["_jsx"],"mappings":";;;;;;;;AAWA,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU;AAsC/D;;;;;;;AAOG;AACH,MAAM,gBAAgB,GAA2B,CAAC,EAChD,SAAS,EACT,SAAS,EACT,KAAK,EACL,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,GAAG,KAAK,EACT,KAAI;AACH,IAAA,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,EAAE;AACtD,QAAA,aAAa,EAAE,UAAU;AACzB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,CAAC,CAAA,IAAA,EAAO,IAAI,CAAA,CAAE,GAAG,IAAI,IAAI,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC;AAC1D,KAAA,CAAC;AAEF;;AAEG;AACH,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC;QACnD,KAAK,EAAE,SAAS,IAAI,KAAK;AACzB,QAAA,eAAe,EAAE,OAAO;AACxB,QAAA,GAAG,KAAK;AACT,KAAA,CAAC;IAEF,MAAM,eAAe,GAAG,UAAU,CAChC,cAAc,EACd,SAAS,EACT,kBAAkB,CACnB;IAED,QACEA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,eAAe,EAAA,GAAM,IAAI,EAAA,QAAA,EACtC,QAAQ,EAAA,CACL;AAEV,CAAC;AAEM,MAAM,OAAO,GAAG,iBAAiB,CACtC,gBAAgB,EAChB;IACE,MAAM;IACN,UAAU;CACX,EACD,SAAS;;"}
1
+ {"version":3,"file":"Buttons.js","sources":["../../../src/elements/Buttons.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { withSubComponents } from '../helpers/withSubComponents';\nimport { Button } from './Button';\nimport { LinkButton } from './LinkButton';\nimport { ToggleButton } from './ToggleButton';\nimport {\n useBulmaClasses,\n BulmaClassesProps,\n validColors,\n} from '../helpers/useBulmaClasses';\n\nconst validButtonsSizes = ['small', 'medium', 'large'] as const;\n/**\n * Valid size values for the Buttons component (Bulma button group sizes).\n */\nexport type ButtonsSize = (typeof validButtonsSizes)[number];\n\n/**\n * Props for the Buttons component.\n */\ninterface ButtonsProps\n extends\n React.HTMLAttributes<HTMLDivElement>,\n Omit<BulmaClassesProps, 'size'> {\n /** Additional CSS classes to apply. */\n className?: string;\n /** Text color helper for the button group. */\n textColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /**\n * Text color alias: renders `has-text-<color>`, exactly like `textColor`.\n * Not a filled variant (no `.buttons.is-<color>` CSS exists; color the\n * individual `Button`s instead). Prefer `textColor`, which takes precedence\n * when both are set; use `bgColor` for a colored surface.\n */\n color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n /** Background color helper for the button group. */\n bgColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Center the group of buttons. */\n isCentered?: boolean;\n /** Align the group of buttons to the right. */\n isRight?: boolean;\n /** Group buttons together as addons (removes spacing between them). */\n hasAddons?: boolean;\n /** Size of the button group. */\n size?: ButtonsSize;\n /** The button elements to render inside the group. */\n children: React.ReactNode;\n}\n\n/**\n * The `Buttons` component lets you group multiple `Button` elements together with Bulma's spacing, alignment, and add-on features.\n *\n * @function\n * @param {ButtonsProps} props - Props for the Buttons component.\n * @returns {JSX.Element} The rendered group of buttons.\n * @see {@link https://bulma.io/documentation/elements/button/#group | Bulma Button Group documentation}\n */\nconst ButtonsComponent: React.FC<ButtonsProps> = ({\n className,\n textColor,\n color,\n bgColor,\n isCentered,\n isRight,\n hasAddons,\n size,\n children,\n ...props\n}) => {\n const buttonsClasses = usePrefixedClassNames('buttons', {\n 'is-centered': isCentered,\n 'is-right': isRight,\n 'has-addons': hasAddons,\n [`are-${size}`]: size && validButtonsSizes.includes(size),\n });\n\n /**\n * Generates Bulma helper classes and separates out remaining props.\n */\n const { bulmaHelperClasses, rest } = useBulmaClasses({\n color: textColor ?? color,\n backgroundColor: bgColor,\n ...props,\n });\n\n const combinedClasses = classNames(\n buttonsClasses,\n className,\n bulmaHelperClasses\n );\n\n return (\n <div className={combinedClasses} {...rest}>\n {children}\n </div>\n );\n};\n\nexport const Buttons = withSubComponents(\n ButtonsComponent,\n {\n Button,\n LinkButton,\n ToggleButton,\n },\n 'Buttons'\n);\n"],"names":["_jsx"],"mappings":";;;;;;;;;AAYA,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU;AAsC/D;;;;;;;AAOG;AACH,MAAM,gBAAgB,GAA2B,CAAC,EAChD,SAAS,EACT,SAAS,EACT,KAAK,EACL,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,GAAG,KAAK,EACT,KAAI;AACH,IAAA,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,EAAE;AACtD,QAAA,aAAa,EAAE,UAAU;AACzB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,CAAC,CAAA,IAAA,EAAO,IAAI,CAAA,CAAE,GAAG,IAAI,IAAI,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC;AAC1D,KAAA,CAAC;AAEF;;AAEG;AACH,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC;QACnD,KAAK,EAAE,SAAS,IAAI,KAAK;AACzB,QAAA,eAAe,EAAE,OAAO;AACxB,QAAA,GAAG,KAAK;AACT,KAAA,CAAC;IAEF,MAAM,eAAe,GAAG,UAAU,CAChC,cAAc,EACd,SAAS,EACT,kBAAkB,CACnB;IAED,QACEA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,eAAe,EAAA,GAAM,IAAI,EAAA,QAAA,EACtC,QAAQ,EAAA,CACL;AAEV,CAAC;AAEM,MAAM,OAAO,GAAG,iBAAiB,CACtC,gBAAgB,EAChB;IACE,MAAM;IACN,UAAU;IACV,YAAY;CACb,EACD,SAAS;;"}
@@ -11,7 +11,8 @@ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
11
11
  * carries no form value: a setting saved with a form is a `Checkbox` or a
12
12
  * `Switch`. A set of toggles that each turn on and off on their own goes in
13
13
  * `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
14
- * one can be on is a radio group rather than a row of these: use `Radios`.
14
+ * one can be on is a radio group rather than a row of these: use
15
+ * `SegmentedControl`, or `Radios` for a choice that's sent with a form.
15
16
  *
16
17
  * @function
17
18
  * @param {ToggleButtonProps} props - Props for the ToggleButton component.
@@ -29,7 +30,7 @@ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
29
30
  * </ToggleButton>
30
31
  * </Buttons>
31
32
  */
32
- const ToggleButton = forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', className, onClick, ...props }, ref) {
33
+ const ToggleButton = forwardRef(function ToggleButton({ pressed: controlledPressed, defaultPressed = false, onPressedChange, color = 'link', isLoading, 'aria-busy': ariaBusy, 'aria-disabled': ariaDisabled, className, onClick, ...props }, ref) {
33
34
  const isControlled = controlledPressed !== undefined;
34
35
  const [internalPressed, setInternalPressed] = useState(defaultPressed);
35
36
  const pressed = controlledPressed ?? internalPressed;
@@ -40,7 +41,13 @@ const ToggleButton = forwardRef(function ToggleButton({ pressed: controlledPress
40
41
  // The caller's onClick runs first, so a `preventDefault()` there vetoes
41
42
  // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here
42
43
  // through the button's own click, and a disabled button gets no click.
44
+ //
45
+ // A loading one gets the click but drops it. Bulma's spinner only stops a
46
+ // pointer, so without this a key flipped a toggle the mouse couldn't.
47
+ // `disabled` would stop both, but it takes focus away mid-save.
43
48
  const handleClick = (event) => {
49
+ if (isLoading)
50
+ return;
44
51
  onClick?.(event);
45
52
  if (event.defaultPrevented)
46
53
  return;
@@ -49,10 +56,12 @@ const ToggleButton = forwardRef(function ToggleButton({ pressed: controlledPress
49
56
  if (!isControlled)
50
57
  setInternalPressed(next);
51
58
  };
52
- // The props are spread first, so the four things a toggle depends on win
53
- // over anything a JavaScript caller or a spread object passes along: it
54
- // stays a button, never submits, and reports the state it's in.
55
- return (jsx(Button, { ...props, ref: ref, as: "button", type: "button", "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
59
+ // The props are spread first, so what a toggle depends on wins over
60
+ // anything a JavaScript caller or a spread object passes along: it stays
61
+ // a button, never submits, reports the state it's in, and says it's busy
62
+ // and won't respond while it's loading. A caller's own `aria-busy` or
63
+ // `aria-disabled` stays when it isn't loading.
64
+ return (jsx(Button, { ...props, ref: ref, as: "button", type: "button", isLoading: isLoading, "aria-busy": isLoading || ariaBusy, "aria-disabled": isLoading || ariaDisabled, "aria-pressed": pressed, color: pressed ? color : undefined, className: classNames(selectedClass, className), onClick: handleClick }));
56
65
  });
57
66
  ToggleButton.displayName = 'ToggleButton';
58
67
 
@@ -1 +1 @@
1
- {"version":3,"file":"ToggleButton.js","sources":["../../../src/elements/ToggleButton.tsx"],"sourcesContent":["import React, { forwardRef, useState } from 'react';\nimport { Button, type ButtonOwnProps } from './Button';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\n\n// Spelled out rather than as an `Exclude` of Button's colors so the API page\n// can list the values. The tests hold it to that `Exclude`, so a color Button\n// gains fails there until it's added here or left out on purpose.\n/**\n * The fills a pressed `ToggleButton` takes: `Button`'s colors, minus `text`\n * and `ghost`, which don't fill, and `white`, `light`, `dark` and `black`.\n * Bulma's color modifier fills the button and drops its border, and those four\n * fill it with about the lightness a plain button already has in one theme or\n * the other, so pressed would look like off without its border.\n */\nexport type ToggleButtonColor =\n 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n\n/**\n * Props for the ToggleButton component. The helper props, the other button\n * attributes and the `ref` all land on the `<button>`.\n *\n * Some of `Button`'s props are left out on purpose. `as`, since only a button\n * can be pressed, and `type`, which is always `button`. `isActive`, which is\n * `pressed` here, and `isStatic`, a toggle you couldn't press. And the props\n * that change the fill (`isLight`, `isOutlined`, `isInverted`, `textColor`,\n * `bgColor` and their shades), since each would color the button in both\n * states or turn \"on\" into a faint tint. `color` takes only the fills that\n * stand out from a plain button: see `ToggleButtonColor`.\n *\n * @extraProp {React.Ref<HTMLButtonElement>} [ref] - Ref forwarded to the `<button>`.\n */\nexport interface ToggleButtonProps\n extends\n Omit<\n ButtonOwnProps,\n | 'color'\n | 'isActive'\n | 'isStatic'\n | 'isLight'\n | 'isOutlined'\n | 'isInverted'\n | 'textColor'\n | 'bgColor'\n | 'colorShade'\n | 'backgroundColorShade'\n >,\n Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ButtonOwnProps | 'type' | 'aria-pressed'\n > {\n /**\n * Whether it's pressed, when you hold the state. Setting it makes the toggle\n * controlled: a click only calls `onPressedChange`, and nothing changes until\n * you pass the new value. Pick this or `defaultPressed` for the button's\n * lifetime.\n */\n pressed?: boolean;\n /** Whether it starts pressed, when it holds its own state. */\n defaultPressed?: boolean;\n /**\n * Called with the new state each time it's toggled, by a click, Enter or\n * Space, whether or not `pressed` is set. An `onClick` that calls\n * `preventDefault()` stops the toggle, and this doesn't fire.\n */\n onPressedChange?: (pressed: boolean) => void;\n /**\n * The fill while it's pressed. Not pressed, it's a plain button, so the fill\n * is the difference people see. `white`, `light`, `dark` and `black` aren't\n * offered, since in one theme or the other each fills about as light or dark\n * as a plain button already is. Of the rest, a pale one such as `warning`\n * changes less than the default `link`.\n */\n color?: ToggleButtonColor;\n /**\n * The label, which stays the same in both states: a screen reader announces\n * whether it's pressed, so \"Mute\" stays \"Mute\". An icon can swap, but an\n * icon-only toggle needs an `aria-label` that doesn't. A label that changes\n * (\"Play\", then \"Pause\") belongs on a plain `Button`.\n */\n children?: React.ReactNode;\n /**\n * Not accepted: `aria-pressed` follows the state. Declared because JSX lets a\n * hyphenated attribute through when the props don't name it, so leaving it\n * out of the type alone would not reject `aria-pressed=\"mixed\"`.\n * @internal\n */\n 'aria-pressed'?: never;\n}\n\n/**\n * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.\n *\n * It's always a `<button type=\"button\">`, so it never submits a form and\n * carries no form value: a setting saved with a form is a `Checkbox` or a\n * `Switch`. A set of toggles that each turn on and off on their own goes in\n * `Buttons hasAddons` with `role=\"group\"` and an `aria-label`. A set where only\n * one can be on is a radio group rather than a row of these: use `Radios`.\n *\n * @function\n * @param {ToggleButtonProps} props - Props for the ToggleButton component.\n * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.\n * @returns {JSX.Element} The rendered toggle button.\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}\n *\n * @example\n * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>\n *\n * @example\n * <Buttons hasAddons role=\"group\" aria-label=\"Text style\">\n * <ToggleButton aria-label=\"Bold\" pressed={bold} onPressedChange={setBold}>\n * <Icon name=\"bold\" />\n * </ToggleButton>\n * </Buttons>\n */\nexport const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(\n function ToggleButton(\n {\n pressed: controlledPressed,\n defaultPressed = false,\n onPressedChange,\n color = 'link',\n className,\n onClick,\n ...props\n },\n ref\n ) {\n const isControlled = controlledPressed !== undefined;\n const [internalPressed, setInternalPressed] = useState(defaultPressed);\n const pressed = controlledPressed ?? internalPressed;\n\n // Prefixed like Button's own classes, which a hand-written\n // `className=\"is-selected\"` could not be under ConfigProvider classPrefix.\n // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.\n const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });\n\n // The caller's onClick runs first, so a `preventDefault()` there vetoes\n // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here\n // through the button's own click, and a disabled button gets no click.\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n const next = !pressed;\n onPressedChange?.(next);\n if (!isControlled) setInternalPressed(next);\n };\n\n // The props are spread first, so the four things a toggle depends on win\n // over anything a JavaScript caller or a spread object passes along: it\n // stays a button, never submits, and reports the state it's in.\n return (\n <Button\n {...props}\n ref={ref}\n as=\"button\"\n type=\"button\"\n aria-pressed={pressed}\n color={pressed ? color : undefined}\n className={classNames(selectedClass, className)}\n onClick={handleClick}\n />\n );\n }\n);\n\nToggleButton.displayName = 'ToggleButton';\n\nexport default ToggleButton;\n"],"names":["_jsx"],"mappings":";;;;;;AAyFA;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACI,MAAM,YAAY,GAAG,UAAU,CACpC,SAAS,YAAY,CACnB,EACE,OAAO,EAAE,iBAAiB,EAC1B,cAAc,GAAG,KAAK,EACtB,eAAe,EACf,KAAK,GAAG,MAAM,EACd,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EACD,GAAG,EAAA;AAEH,IAAA,MAAM,YAAY,GAAG,iBAAiB,KAAK,SAAS;IACpD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,cAAc,CAAC;AACtE,IAAA,MAAM,OAAO,GAAG,iBAAiB,IAAI,eAAe;;;;IAKpD,MAAM,aAAa,GAAG,qBAAqB,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;;;;AAKvE,IAAA,MAAM,WAAW,GAAG,CAAC,KAA0C,KAAI;AACjE,QAAA,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,KAAK,CAAC,gBAAgB;YAAE;AAC5B,QAAA,MAAM,IAAI,GAAG,CAAC,OAAO;AACrB,QAAA,eAAe,GAAG,IAAI,CAAC;AACvB,QAAA,IAAI,CAAC,YAAY;YAAE,kBAAkB,CAAC,IAAI,CAAC;AAC7C,IAAA,CAAC;;;;IAKD,QACEA,IAAC,MAAM,EAAA,EAAA,GACD,KAAK,EACT,GAAG,EAAE,GAAG,EACR,EAAE,EAAC,QAAQ,EACX,IAAI,EAAC,QAAQ,EAAA,cAAA,EACC,OAAO,EACrB,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,EAClC,SAAS,EAAE,UAAU,CAAC,aAAa,EAAE,SAAS,CAAC,EAC/C,OAAO,EAAE,WAAW,EAAA,CACpB;AAEN,CAAC;AAGH,YAAY,CAAC,WAAW,GAAG,cAAc;;"}
1
+ {"version":3,"file":"ToggleButton.js","sources":["../../../src/elements/ToggleButton.tsx"],"sourcesContent":["import React, { forwardRef, useState } from 'react';\nimport { Button, type ButtonOwnProps } from './Button';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\n\n// Spelled out rather than as an `Exclude` of Button's colors so the API page\n// can list the values. The tests hold it to that `Exclude`, so a color Button\n// gains fails there until it's added here or left out on purpose.\n/**\n * The fills a pressed `ToggleButton` takes: `Button`'s colors, minus `text`\n * and `ghost`, which don't fill, and `white`, `light`, `dark` and `black`.\n * Bulma's color modifier fills the button and drops its border, and those four\n * fill it with about the lightness a plain button already has in one theme or\n * the other, so pressed would look like off without its border.\n */\nexport type ToggleButtonColor =\n 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n\n/**\n * Props for the ToggleButton component. The helper props, the other button\n * attributes and the `ref` all land on the `<button>`.\n *\n * Some of `Button`'s props are left out on purpose. `as`, since only a button\n * can be pressed, and `type`, which is always `button`. `isActive`, which is\n * `pressed` here, and `isStatic`, a toggle you couldn't press. And the props\n * that change the fill (`isLight`, `isOutlined`, `isInverted`, `textColor`,\n * `bgColor` and their shades), since each would color the button in both\n * states or turn \"on\" into a faint tint. `color` takes only the fills that\n * stand out from a plain button: see `ToggleButtonColor`.\n *\n * @extraProp {React.Ref<HTMLButtonElement>} [ref] - Ref forwarded to the `<button>`.\n */\nexport interface ToggleButtonProps\n extends\n Omit<\n ButtonOwnProps,\n | 'color'\n | 'isActive'\n | 'isStatic'\n | 'isLight'\n | 'isOutlined'\n | 'isInverted'\n | 'textColor'\n | 'bgColor'\n | 'colorShade'\n | 'backgroundColorShade'\n >,\n Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ButtonOwnProps | 'type' | 'aria-pressed'\n > {\n /**\n * Whether it's pressed, when you hold the state. Setting it makes the toggle\n * controlled: a click only calls `onPressedChange`, and nothing changes until\n * you pass the new value. Pick this or `defaultPressed` for the button's\n * lifetime.\n */\n pressed?: boolean;\n /** Whether it starts pressed, when it holds its own state. */\n defaultPressed?: boolean;\n /**\n * Called with the new state each time it's toggled, whether or not\n * `pressed` is set. An `onClick` that calls `preventDefault()` stops the\n * toggle, and this doesn't fire.\n */\n onPressedChange?: (pressed: boolean) => void;\n /**\n * The fill while it's pressed. Not pressed, it's a plain button, so the fill\n * is the difference people see. `white`, `light`, `dark` and `black` aren't\n * offered, since in one theme or the other each fills about as light or dark\n * as a plain button already is. Of the rest, a pale one such as `warning`\n * changes less than the default `link`.\n */\n color?: ToggleButtonColor;\n /**\n * The label, which stays the same in both states: a screen reader announces\n * whether it's pressed, so \"Mute\" stays \"Mute\". An icon can swap, but an\n * icon-only toggle needs an `aria-label` that doesn't. A label that changes\n * (\"Play\", then \"Pause\") belongs on a plain `Button`.\n */\n children?: React.ReactNode;\n /**\n * Shows Bulma's spinner in place of the label and holds the toggle where it\n * is: a click, Enter or Space does nothing, so neither `onClick` nor\n * `onPressedChange` fires, until it's cleared. Set it while a change is\n * saving. The button carries `aria-busy` and `aria-disabled` meanwhile, and\n * stays focusable, so focus isn't lost mid-save. Under\n * `prefers-reduced-motion: reduce` the spinner stops and stays drawn (with\n * bestax's CSS loaded).\n */\n isLoading?: boolean;\n /**\n * Not accepted: `aria-pressed` follows the state. Declared because JSX lets a\n * hyphenated attribute through when the props don't name it, so leaving it\n * out of the type alone would not reject `aria-pressed=\"mixed\"`.\n * @internal\n */\n 'aria-pressed'?: never;\n}\n\n/**\n * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.\n *\n * It's always a `<button type=\"button\">`, so it never submits a form and\n * carries no form value: a setting saved with a form is a `Checkbox` or a\n * `Switch`. A set of toggles that each turn on and off on their own goes in\n * `Buttons hasAddons` with `role=\"group\"` and an `aria-label`. A set where only\n * one can be on is a radio group rather than a row of these: use\n * `SegmentedControl`, or `Radios` for a choice that's sent with a form.\n *\n * @function\n * @param {ToggleButtonProps} props - Props for the ToggleButton component.\n * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.\n * @returns {JSX.Element} The rendered toggle button.\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}\n *\n * @example\n * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>\n *\n * @example\n * <Buttons hasAddons role=\"group\" aria-label=\"Text style\">\n * <ToggleButton aria-label=\"Bold\" pressed={bold} onPressedChange={setBold}>\n * <Icon name=\"bold\" />\n * </ToggleButton>\n * </Buttons>\n */\nexport const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(\n function ToggleButton(\n {\n pressed: controlledPressed,\n defaultPressed = false,\n onPressedChange,\n color = 'link',\n isLoading,\n 'aria-busy': ariaBusy,\n 'aria-disabled': ariaDisabled,\n className,\n onClick,\n ...props\n },\n ref\n ) {\n const isControlled = controlledPressed !== undefined;\n const [internalPressed, setInternalPressed] = useState(defaultPressed);\n const pressed = controlledPressed ?? internalPressed;\n\n // Prefixed like Button's own classes, which a hand-written\n // `className=\"is-selected\"` could not be under ConfigProvider classPrefix.\n // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.\n const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });\n\n // The caller's onClick runs first, so a `preventDefault()` there vetoes\n // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here\n // through the button's own click, and a disabled button gets no click.\n //\n // A loading one gets the click but drops it. Bulma's spinner only stops a\n // pointer, so without this a key flipped a toggle the mouse couldn't.\n // `disabled` would stop both, but it takes focus away mid-save.\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n if (isLoading) return;\n onClick?.(event);\n if (event.defaultPrevented) return;\n const next = !pressed;\n onPressedChange?.(next);\n if (!isControlled) setInternalPressed(next);\n };\n\n // The props are spread first, so what a toggle depends on wins over\n // anything a JavaScript caller or a spread object passes along: it stays\n // a button, never submits, reports the state it's in, and says it's busy\n // and won't respond while it's loading. A caller's own `aria-busy` or\n // `aria-disabled` stays when it isn't loading.\n return (\n <Button\n {...props}\n ref={ref}\n as=\"button\"\n type=\"button\"\n isLoading={isLoading}\n aria-busy={isLoading || ariaBusy}\n aria-disabled={isLoading || ariaDisabled}\n aria-pressed={pressed}\n color={pressed ? color : undefined}\n className={classNames(selectedClass, className)}\n onClick={handleClick}\n />\n );\n }\n);\n\nToggleButton.displayName = 'ToggleButton';\n\nexport default ToggleButton;\n"],"names":["_jsx"],"mappings":";;;;;;AAmGA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,YAAY,GAAG,UAAU,CACpC,SAAS,YAAY,CACnB,EACE,OAAO,EAAE,iBAAiB,EAC1B,cAAc,GAAG,KAAK,EACtB,eAAe,EACf,KAAK,GAAG,MAAM,EACd,SAAS,EACT,WAAW,EAAE,QAAQ,EACrB,eAAe,EAAE,YAAY,EAC7B,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EACD,GAAG,EAAA;AAEH,IAAA,MAAM,YAAY,GAAG,iBAAiB,KAAK,SAAS;IACpD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,cAAc,CAAC;AACtE,IAAA,MAAM,OAAO,GAAG,iBAAiB,IAAI,eAAe;;;;IAKpD,MAAM,aAAa,GAAG,qBAAqB,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;;;;;;;;AASvE,IAAA,MAAM,WAAW,GAAG,CAAC,KAA0C,KAAI;AACjE,QAAA,IAAI,SAAS;YAAE;AACf,QAAA,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,KAAK,CAAC,gBAAgB;YAAE;AAC5B,QAAA,MAAM,IAAI,GAAG,CAAC,OAAO;AACrB,QAAA,eAAe,GAAG,IAAI,CAAC;AACvB,QAAA,IAAI,CAAC,YAAY;YAAE,kBAAkB,CAAC,IAAI,CAAC;AAC7C,IAAA,CAAC;;;;;;IAOD,QACEA,GAAA,CAAC,MAAM,EAAA,EAAA,GACD,KAAK,EACT,GAAG,EAAE,GAAG,EACR,EAAE,EAAC,QAAQ,EACX,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EAAA,WAAA,EACT,SAAS,IAAI,QAAQ,EAAA,eAAA,EACjB,SAAS,IAAI,YAAY,kBAC1B,OAAO,EACrB,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,EAClC,SAAS,EAAE,UAAU,CAAC,aAAa,EAAE,SAAS,CAAC,EAC/C,OAAO,EAAE,WAAW,EAAA,CACpB;AAEN,CAAC;AAGH,YAAY,CAAC,WAAW,GAAG,cAAc;;"}