@crisp-ui-kit/crisp 0.23.0 → 0.23.2

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.
@@ -45,6 +45,7 @@ const accordion_recipe_1 = require("./accordion.recipe");
45
45
  */
46
46
  const toSet = (v) => new Set(v == null ? [] : Array.isArray(v) ? v : [v]);
47
47
  function Accordion({ items, multiple, value, defaultValue, onValueChange, className, ...rest }) {
48
+ const uid = React.useId();
48
49
  const [internal, setInternal] = React.useState(() => toSet(defaultValue));
49
50
  // `value` present means the caller owns the open set; our own state is then
50
51
  // never read, so a click can only change what is shown by going through
@@ -62,8 +63,13 @@ function Accordion({ items, multiple, value, defaultValue, onValueChange, classN
62
63
  };
63
64
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, accordion_recipe_1.accordionRecipe)({ className }), ...rest, children: items.map((item) => {
64
65
  const isOpen = open.has(item.value);
65
- const panelId = `accordion-panel-${item.value}`;
66
- return ((0, jsx_runtime_1.jsxs)("div", { className: "accordion-item", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "accordion-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggle(item.value), children: [(0, jsx_runtime_1.jsx)("span", { className: "accordion-title", children: item.title }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "accordion-chevron", size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), (0, jsx_runtime_1.jsx)("div", { id: panelId, className: "accordion-panel", role: "region", "data-open": isOpen ? "" : undefined, hidden: !isOpen, children: (0, jsx_runtime_1.jsx)("div", { className: "accordion-panel-inner", children: (0, jsx_runtime_1.jsx)("div", { className: "accordion-content", children: item.content }) }) })] }, item.value));
66
+ // Scoped to this instance. `accordion-panel-${value}` is global, so two
67
+ // accordions with an item of the same value on one page mint the same
68
+ // id twice — and getElementById returns the first, so the second
69
+ // accordion's triggers silently control the first one's panels (#227).
70
+ const panelId = `${uid}-panel-${item.value}`;
71
+ const triggerId = `${uid}-trigger-${item.value}`;
72
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "accordion-item", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", id: triggerId, className: "accordion-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggle(item.value), children: [(0, jsx_runtime_1.jsx)("span", { className: "accordion-title", children: item.title }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "accordion-chevron", size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), (0, jsx_runtime_1.jsx)("div", { id: panelId, className: "accordion-panel", role: "region", "aria-labelledby": triggerId, "data-open": isOpen ? "" : undefined, hidden: !isOpen, children: (0, jsx_runtime_1.jsx)("div", { className: "accordion-panel-inner", children: (0, jsx_runtime_1.jsx)("div", { className: "accordion-content", children: item.content }) }) })] }, item.value));
67
73
  }) }));
68
74
  }
69
75
  Accordion.displayName = "Accordion";
@@ -60,7 +60,9 @@ function Header({ api }) {
60
60
  // in RTL the chevrons swap: "previous" is on the right
61
61
  const previousDirection = api.isRtl ? "right" : "left";
62
62
  const nextDirection = api.isRtl ? "left" : "right";
63
- return ((0, jsx_runtime_1.jsxs)("div", { className: "datepicker-header", children: [(0, jsx_runtime_1.jsxs)("button", { ...api.monthYearButtonProps, type: "button", id: `${api.rootProps.id}-header-label`, className: "datepicker-header-label", children: [(0, jsx_runtime_1.jsx)("span", { "data-date-picker-header-label": "", children: api.headerLabel }), (0, jsx_runtime_1.jsx)("span", { className: "datepicker-header-chevron", "data-open": api.isWheelOpen ? "" : undefined, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "datepicker-nav", children: [(0, jsx_runtime_1.jsx)(NavigationButton, { ...api.previousButtonProps, direction: previousDirection, className: "datepicker-nav-button" }), (0, jsx_runtime_1.jsx)(NavigationButton, { ...api.nextButtonProps, direction: nextDirection, className: "datepicker-nav-button" })] })] }));
63
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "datepicker-header", children: [(0, jsx_runtime_1.jsxs)("button", { ...api.monthYearButtonProps, "aria-controls": api.isWheelOpen
64
+ ? api.monthYearButtonProps["aria-controls"]
65
+ : undefined, type: "button", id: `${api.rootProps.id}-header-label`, className: "datepicker-header-label", children: [(0, jsx_runtime_1.jsx)("span", { "data-date-picker-header-label": "", children: api.headerLabel }), (0, jsx_runtime_1.jsx)("span", { className: "datepicker-header-chevron", "data-open": api.isWheelOpen ? "" : undefined, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "datepicker-nav", children: [(0, jsx_runtime_1.jsx)(NavigationButton, { ...api.previousButtonProps, direction: previousDirection, className: "datepicker-nav-button" }), (0, jsx_runtime_1.jsx)(NavigationButton, { ...api.nextButtonProps, direction: nextDirection, className: "datepicker-nav-button" })] })] }));
64
66
  }
65
67
  function TwoMonthHeader({ api, label, labelId, position, }) {
66
68
  const isFirst = position === "first";
@@ -83,7 +85,7 @@ function MonthView({ api, month, renderDateCellContent, renderDateCellSupplement
83
85
  : labelId, className: "datepicker-grid", children: [showWeekdays && (0, jsx_runtime_1.jsx)(WeekdayRow, { api: api }), month.weeks.map((week) => ((0, jsx_runtime_1.jsx)("div", { role: "row", className: "datepicker-week-row", children: week.cells.map((cell, index) => ((0, jsx_runtime_1.jsx)(DateCellView, { cell: cell, renderDateCellContent: renderDateCellContent, renderDateCellSupplement: renderDateCellSupplement }, cell?.key ?? `${week.key}-empty-${index}`))) }, week.key)))] })] }));
84
86
  }
85
87
  function WheelView({ api }) {
86
- return ((0, jsx_runtime_1.jsx)("div", { className: "datepicker-wheel-container", children: (0, jsx_runtime_1.jsx)(wheelpicker_1.WheelPicker, { className: "datepicker-wheel", "aria-label": api.headerLabel, columns: [
88
+ return ((0, jsx_runtime_1.jsx)("div", { ...api.wheelProps, className: "datepicker-wheel-container", children: (0, jsx_runtime_1.jsx)(wheelpicker_1.WheelPicker, { className: "datepicker-wheel", "aria-label": api.headerLabel, columns: [
87
89
  {
88
90
  key: "year",
89
91
  label: api.ariaLabels.yearWheel,
@@ -53,6 +53,13 @@ const MenuContext = React.createContext(null);
53
53
  * closes on outside click. Flips up / right-aligns when space is tight.
54
54
  */
55
55
  function Menu({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, className, "aria-label": ariaLabel, }) {
56
+ // The surface and the button that opens it were two unconnected nodes: the
57
+ // menu/dialog had no id, so nothing could point at it, and no accessible
58
+ // name, so a screen reader announced a bare "menu"/"dialog". Labelling the
59
+ // surface by its own trigger is the WAI menu-button recommendation, and it
60
+ // needs no invented copy (#225).
61
+ const surfaceId = React.useId();
62
+ const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
56
63
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
57
64
  const open = openProp ?? internalOpen;
58
65
  const setOpen = React.useCallback((next) => {
@@ -110,14 +117,18 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
110
117
  });
111
118
  const triggerEl = React.cloneElement(trigger, {
112
119
  ref: triggerRef,
120
+ id: triggerId,
113
121
  "aria-haspopup": "menu",
114
122
  "aria-expanded": open,
123
+ // only while the surface exists — a reference to a node that is not
124
+ // there is not a relationship
125
+ "aria-controls": open ? surfaceId : undefined,
115
126
  onClick: (e) => {
116
127
  trigger.props.onClick?.(e);
117
128
  setOpen(!open);
118
129
  },
119
130
  });
120
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "menu-content", role: "menu", "aria-label": ariaLabel, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
131
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
121
132
  }
122
133
  Menu.displayName = "Menu";
123
134
  function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
@@ -44,6 +44,13 @@ const popover_recipe_1 = require("./popover.recipe");
44
44
  * closes on outside pointer-down and Escape. Controlled or self-managed.
45
45
  */
46
46
  function Popover({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, "aria-label": ariaLabel, className, }) {
47
+ // The surface and the button that opens it were two unconnected nodes: the
48
+ // menu/dialog had no id, so nothing could point at it, and no accessible
49
+ // name, so a screen reader announced a bare "menu"/"dialog". Labelling the
50
+ // surface by its own trigger is the WAI menu-button recommendation, and it
51
+ // needs no invented copy (#225).
52
+ const surfaceId = React.useId();
53
+ const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
47
54
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
48
55
  const open = openProp ?? internalOpen;
49
56
  const rootRef = React.useRef(null);
@@ -87,14 +94,18 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
87
94
  }, [open, setOpen]);
88
95
  const triggerEl = React.cloneElement(trigger, {
89
96
  ref: triggerRef,
97
+ id: triggerId,
90
98
  "aria-haspopup": "dialog",
91
99
  "aria-expanded": open,
100
+ // only while the surface exists — a reference to a node that is not
101
+ // there is not a relationship
102
+ "aria-controls": open ? surfaceId : undefined,
92
103
  onClick: (e) => {
93
104
  trigger.props.onClick?.(e);
94
105
  setOpen(!open);
95
106
  },
96
107
  });
97
- return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, popover_recipe_1.popoverRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)("div", { className: "popover-content", role: "dialog", "aria-label": ariaLabel, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
108
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, popover_recipe_1.popoverRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)("div", { className: "popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
98
109
  if (e.key === "Escape") {
99
110
  e.preventDefault();
100
111
  setOpen(false);
@@ -45,6 +45,12 @@ const tabs_keyboard_1 = require("./tabs-keyboard");
45
45
  * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
46
46
  */
47
47
  function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, "aria-label": ariaLabel, ...rest }) {
48
+ // Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
49
+ // so two tab bars sharing a value mint the same id twice and every
50
+ // aria-labelledby resolves to the first one (#227).
51
+ const uid = React.useId();
52
+ const tabId = (v) => `${uid}-tab-${v}`;
53
+ const panelId = (v) => `${uid}-panel-${v}`;
48
54
  const [internal, setInternal] = React.useState(defaultValue ?? tabs[0]?.value);
49
55
  const active = value ?? internal;
50
56
  const listRef = React.useRef(null);
@@ -79,11 +85,11 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, cla
79
85
  });
80
86
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "tabs-leading", children: leading }), tabs.map((t) => {
81
87
  const selected = t.value === active;
82
- return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
88
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
83
89
  }), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "tabs-indicator", style: {
84
90
  width: indicator.width,
85
91
  transform: `translateX(${indicator.left}px)`,
86
- } }))] }), children && ((0, jsx_runtime_1.jsx)("div", { className: "tabs-panel", role: "tabpanel", id: `panel-${active}`, "aria-labelledby": `tab-${active}`,
92
+ } }))] }), children && ((0, jsx_runtime_1.jsx)("div", { className: "tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
87
93
  // biome-ignore lint/a11y/noNoninteractiveTabindex: a tabpanel is focusable per the WAI-ARIA tabs pattern
88
94
  tabIndex: 0, children: children(active) }))] }));
89
95
  }
@@ -45,6 +45,7 @@ const tooltip_recipe_1 = require("./tooltip.recipe");
45
45
  * above the trigger (flipping below near the top edge).
46
46
  */
47
47
  function Tooltip({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, }) {
48
+ const tipId = React.useId();
48
49
  const triggerRef = React.useRef(null);
49
50
  const timer = React.useRef(undefined);
50
51
  const [pos, setPos] = React.useState(null);
@@ -82,9 +83,26 @@ function Tooltip({ content, children, side = "top", delay = 350, open: openProp,
82
83
  setOpen(false);
83
84
  };
84
85
  React.useEffect(() => () => clearTimeout(timer.current), []);
85
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: children }), open &&
86
+ // The whole point of a tooltip is the description it gives its trigger, and
87
+ // the only way a screen reader learns of one is `aria-describedby` on the
88
+ // element that gets focus. It cannot go on the wrapper: a description on a
89
+ // non-focusable span around a button is never announced. So the trigger is
90
+ // cloned, keeping any aria-describedby it already had, and the reference is
91
+ // only present while the bubble is (a reference to a removed node is dead
92
+ // wiring, #225).
93
+ const described = React.isValidElement(children) && open
94
+ ? React.cloneElement(children, {
95
+ "aria-describedby": [
96
+ children.props["aria-describedby"],
97
+ tipId,
98
+ ]
99
+ .filter(Boolean)
100
+ .join(" "),
101
+ })
102
+ : children;
103
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: described }), open &&
86
104
  pos != null &&
87
- (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)(), style: {
105
+ (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)(), style: {
88
106
  left: pos.left,
89
107
  top: pos.top,
90
108
  transform: pos.below
@@ -231,7 +231,7 @@ function Command({ open, onOpenChange, items = exports.defaultCommandItems, plac
231
231
  };
232
232
  if (!dlg.open)
233
233
  return null;
234
- return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, command_recipe_1.commandRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(dlg.getContentProps(), describedBy), className: (0, core_1.cx)("command-content", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "command-card", children: [(0, jsx_runtime_1.jsxs)("div", { className: "command-control", children: [(0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), (0, jsx_runtime_1.jsx)("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
234
+ return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, command_recipe_1.commandRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(dlg.getContentProps(), describedBy), className: (0, core_1.cx)("command-content", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "command-card", children: [(0, jsx_runtime_1.jsxs)("div", { className: "command-control", children: [(0, jsx_runtime_1.jsx)("label", { ...combo.getLabelProps(), className: "command-srlabel", children: ariaLabel }), (0, jsx_runtime_1.jsx)(IconSearch, {}), (0, jsx_runtime_1.jsx)("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), (0, jsx_runtime_1.jsx)("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
235
235
  const Icon = item.icon;
236
236
  const showGroup = item.group && item.group !== filteredItems[i - 1]?.group;
237
237
  return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [showGroup && ((0, jsx_runtime_1.jsx)("div", { className: "command-group-label", "aria-hidden": "true", children: item.group })), (0, jsx_runtime_1.jsxs)("div", { ...combo.getItemProps({ item }), className: "command-item", children: [(0, jsx_runtime_1.jsx)(Icon, {}), (0, jsx_runtime_1.jsx)("span", { className: "command-item-label", children: item.label }), item.shortcut && ((0, jsx_runtime_1.jsx)("span", { className: "command-item-shortcut", children: item.shortcut.map((key) => ((0, jsx_runtime_1.jsx)(kbd_1.Kbd, { children: key }, key))) }))] })] }, item.id));
@@ -13,6 +13,12 @@ const config = (0, core_1.defineRecipe)({
13
13
  & .command-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); overflow: hidden; }
14
14
  & .command-control { display: flex; align-items: center; gap: ${tokens_1.contract.space["2.5"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["4"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgMuted}; }
15
15
  & .command-control svg { flex-shrink: 0; }
16
+ /* The palette's label element. It exists for the assistive tree only: zag's
17
+ combobox names the input through it and points the listbox's
18
+ aria-labelledby at it, and without it both were unnamed and the reference
19
+ resolved to nothing (#225). Clipped rather than display:none, which would
20
+ take it out of the tree it exists for. */
21
+ & .command-srlabel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
16
22
  & .command-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.xl}; color: ${tokens_1.contract.fgPrimary}; }
17
23
  & .command-input::placeholder { color: ${tokens_1.contract.fgMuted}; }
18
24
  & .command-list { flex: 1; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2.5"]}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; }
package/dist/command.css CHANGED
@@ -7,6 +7,12 @@
7
7
  & .command-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--radius-xl); background: var(--bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg-primary) 8%, transparent); overflow: hidden; }
8
8
  & .command-control { display: flex; align-items: center; gap: var(--space-2-5); height: 52px; flex-shrink: 0; padding: 0 var(--space-4); border-bottom: 1px solid var(--border-subtle); color: var(--fg-muted); }
9
9
  & .command-control svg { flex-shrink: 0; }
10
+ /* The palette's label element. It exists for the assistive tree only: zag's
11
+ combobox names the input through it and points the listbox's
12
+ aria-labelledby at it, and without it both were unnamed and the reference
13
+ resolved to nothing (#225). Clipped rather than display:none, which would
14
+ take it out of the tree it exists for. */
15
+ & .command-srlabel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
10
16
  & .command-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: var(--font-sans); font-size: var(--text-xl); color: var(--fg-primary); }
11
17
  & .command-input::placeholder { color: var(--fg-muted); }
12
18
  & .command-list { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-1) var(--space-2-5); display: flex; flex-direction: column; gap: var(--space-px); }
@@ -9,6 +9,7 @@ import { accordionRecipe } from "./accordion.recipe.js";
9
9
  */
10
10
  const toSet = (v) => new Set(v == null ? [] : Array.isArray(v) ? v : [v]);
11
11
  export function Accordion({ items, multiple, value, defaultValue, onValueChange, className, ...rest }) {
12
+ const uid = React.useId();
12
13
  const [internal, setInternal] = React.useState(() => toSet(defaultValue));
13
14
  // `value` present means the caller owns the open set; our own state is then
14
15
  // never read, so a click can only change what is shown by going through
@@ -26,8 +27,13 @@ export function Accordion({ items, multiple, value, defaultValue, onValueChange,
26
27
  };
27
28
  return (_jsx("div", { className: accordionRecipe({ className }), ...rest, children: items.map((item) => {
28
29
  const isOpen = open.has(item.value);
29
- const panelId = `accordion-panel-${item.value}`;
30
- return (_jsxs("div", { className: "accordion-item", children: [_jsxs("button", { type: "button", className: "accordion-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggle(item.value), children: [_jsx("span", { className: "accordion-title", children: item.title }), _jsx(ChevronDown, { className: "accordion-chevron", size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), _jsx("div", { id: panelId, className: "accordion-panel", role: "region", "data-open": isOpen ? "" : undefined, hidden: !isOpen, children: _jsx("div", { className: "accordion-panel-inner", children: _jsx("div", { className: "accordion-content", children: item.content }) }) })] }, item.value));
30
+ // Scoped to this instance. `accordion-panel-${value}` is global, so two
31
+ // accordions with an item of the same value on one page mint the same
32
+ // id twice — and getElementById returns the first, so the second
33
+ // accordion's triggers silently control the first one's panels (#227).
34
+ const panelId = `${uid}-panel-${item.value}`;
35
+ const triggerId = `${uid}-trigger-${item.value}`;
36
+ return (_jsxs("div", { className: "accordion-item", children: [_jsxs("button", { type: "button", id: triggerId, className: "accordion-trigger", "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggle(item.value), children: [_jsx("span", { className: "accordion-title", children: item.title }), _jsx(ChevronDown, { className: "accordion-chevron", size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), _jsx("div", { id: panelId, className: "accordion-panel", role: "region", "aria-labelledby": triggerId, "data-open": isOpen ? "" : undefined, hidden: !isOpen, children: _jsx("div", { className: "accordion-panel-inner", children: _jsx("div", { className: "accordion-content", children: item.content }) }) })] }, item.value));
31
37
  }) }));
32
38
  }
33
39
  Accordion.displayName = "Accordion";
@@ -24,7 +24,9 @@ function Header({ api }) {
24
24
  // in RTL the chevrons swap: "previous" is on the right
25
25
  const previousDirection = api.isRtl ? "right" : "left";
26
26
  const nextDirection = api.isRtl ? "left" : "right";
27
- return (_jsxs("div", { className: "datepicker-header", children: [_jsxs("button", { ...api.monthYearButtonProps, type: "button", id: `${api.rootProps.id}-header-label`, className: "datepicker-header-label", children: [_jsx("span", { "data-date-picker-header-label": "", children: api.headerLabel }), _jsx("span", { className: "datepicker-header-chevron", "data-open": api.isWheelOpen ? "" : undefined, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsxs("div", { className: "datepicker-nav", children: [_jsx(NavigationButton, { ...api.previousButtonProps, direction: previousDirection, className: "datepicker-nav-button" }), _jsx(NavigationButton, { ...api.nextButtonProps, direction: nextDirection, className: "datepicker-nav-button" })] })] }));
27
+ return (_jsxs("div", { className: "datepicker-header", children: [_jsxs("button", { ...api.monthYearButtonProps, "aria-controls": api.isWheelOpen
28
+ ? api.monthYearButtonProps["aria-controls"]
29
+ : undefined, type: "button", id: `${api.rootProps.id}-header-label`, className: "datepicker-header-label", children: [_jsx("span", { "data-date-picker-header-label": "", children: api.headerLabel }), _jsx("span", { className: "datepicker-header-chevron", "data-open": api.isWheelOpen ? "" : undefined, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })] }), _jsxs("div", { className: "datepicker-nav", children: [_jsx(NavigationButton, { ...api.previousButtonProps, direction: previousDirection, className: "datepicker-nav-button" }), _jsx(NavigationButton, { ...api.nextButtonProps, direction: nextDirection, className: "datepicker-nav-button" })] })] }));
28
30
  }
29
31
  function TwoMonthHeader({ api, label, labelId, position, }) {
30
32
  const isFirst = position === "first";
@@ -47,7 +49,7 @@ function MonthView({ api, month, renderDateCellContent, renderDateCellSupplement
47
49
  : labelId, className: "datepicker-grid", children: [showWeekdays && _jsx(WeekdayRow, { api: api }), month.weeks.map((week) => (_jsx("div", { role: "row", className: "datepicker-week-row", children: week.cells.map((cell, index) => (_jsx(DateCellView, { cell: cell, renderDateCellContent: renderDateCellContent, renderDateCellSupplement: renderDateCellSupplement }, cell?.key ?? `${week.key}-empty-${index}`))) }, week.key)))] })] }));
48
50
  }
49
51
  function WheelView({ api }) {
50
- return (_jsx("div", { className: "datepicker-wheel-container", children: _jsx(WheelPicker, { className: "datepicker-wheel", "aria-label": api.headerLabel, columns: [
52
+ return (_jsx("div", { ...api.wheelProps, className: "datepicker-wheel-container", children: _jsx(WheelPicker, { className: "datepicker-wheel", "aria-label": api.headerLabel, columns: [
51
53
  {
52
54
  key: "year",
53
55
  label: api.ariaLabels.yearWheel,
@@ -13,6 +13,13 @@ const MenuContext = React.createContext(null);
13
13
  * closes on outside click. Flips up / right-aligns when space is tight.
14
14
  */
15
15
  export function Menu({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, className, "aria-label": ariaLabel, }) {
16
+ // The surface and the button that opens it were two unconnected nodes: the
17
+ // menu/dialog had no id, so nothing could point at it, and no accessible
18
+ // name, so a screen reader announced a bare "menu"/"dialog". Labelling the
19
+ // surface by its own trigger is the WAI menu-button recommendation, and it
20
+ // needs no invented copy (#225).
21
+ const surfaceId = React.useId();
22
+ const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
16
23
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
17
24
  const open = openProp ?? internalOpen;
18
25
  const setOpen = React.useCallback((next) => {
@@ -70,14 +77,18 @@ export function Menu({ trigger, children, align = "start", open: openProp, defau
70
77
  });
71
78
  const triggerEl = React.cloneElement(trigger, {
72
79
  ref: triggerRef,
80
+ id: triggerId,
73
81
  "aria-haspopup": "menu",
74
82
  "aria-expanded": open,
83
+ // only while the surface exists — a reference to a node that is not
84
+ // there is not a relationship
85
+ "aria-controls": open ? surfaceId : undefined,
75
86
  onClick: (e) => {
76
87
  trigger.props.onClick?.(e);
77
88
  setOpen(!open);
78
89
  },
79
90
  });
80
- return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(MenuContext.Provider, { value: { close }, children: _jsx("div", { ref: contentRef, className: "menu-content", role: "menu", "aria-label": ariaLabel, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
91
+ return (_jsxs("div", { ref: rootRef, className: menuRecipe({ className }), children: [triggerEl, open && (_jsx(MenuContext.Provider, { value: { close }, children: _jsx("div", { ref: contentRef, className: "menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
81
92
  }
82
93
  Menu.displayName = "Menu";
83
94
  export function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
@@ -8,6 +8,13 @@ import { popoverRecipe } from "./popover.recipe.js";
8
8
  * closes on outside pointer-down and Escape. Controlled or self-managed.
9
9
  */
10
10
  export function Popover({ trigger, children, align = "start", open: openProp, defaultOpen, onOpenChange, "aria-label": ariaLabel, className, }) {
11
+ // The surface and the button that opens it were two unconnected nodes: the
12
+ // menu/dialog had no id, so nothing could point at it, and no accessible
13
+ // name, so a screen reader announced a bare "menu"/"dialog". Labelling the
14
+ // surface by its own trigger is the WAI menu-button recommendation, and it
15
+ // needs no invented copy (#225).
16
+ const surfaceId = React.useId();
17
+ const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
11
18
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
12
19
  const open = openProp ?? internalOpen;
13
20
  const rootRef = React.useRef(null);
@@ -51,14 +58,18 @@ export function Popover({ trigger, children, align = "start", open: openProp, de
51
58
  }, [open, setOpen]);
52
59
  const triggerEl = React.cloneElement(trigger, {
53
60
  ref: triggerRef,
61
+ id: triggerId,
54
62
  "aria-haspopup": "dialog",
55
63
  "aria-expanded": open,
64
+ // only while the surface exists — a reference to a node that is not
65
+ // there is not a relationship
66
+ "aria-controls": open ? surfaceId : undefined,
56
67
  onClick: (e) => {
57
68
  trigger.props.onClick?.(e);
58
69
  setOpen(!open);
59
70
  },
60
71
  });
61
- return (_jsxs("div", { ref: rootRef, className: popoverRecipe({ className }), children: [triggerEl, open && (_jsx("div", { className: "popover-content", role: "dialog", "aria-label": ariaLabel, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
72
+ return (_jsxs("div", { ref: rootRef, className: popoverRecipe({ className }), children: [triggerEl, open && (_jsx("div", { className: "popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
62
73
  if (e.key === "Escape") {
63
74
  e.preventDefault();
64
75
  setOpen(false);
@@ -9,6 +9,12 @@ import { useTablistKeyboard } from "./tabs-keyboard.js";
9
9
  * `value`/`onValueChange`, or uncontrolled from `defaultValue`.
10
10
  */
11
11
  export function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, className, "aria-label": ariaLabel, ...rest }) {
12
+ // Scoped to this instance: `tab-${value}` and `panel-${value}` are global,
13
+ // so two tab bars sharing a value mint the same id twice and every
14
+ // aria-labelledby resolves to the first one (#227).
15
+ const uid = React.useId();
16
+ const tabId = (v) => `${uid}-tab-${v}`;
17
+ const panelId = (v) => `${uid}-panel-${v}`;
12
18
  const [internal, setInternal] = React.useState(defaultValue ?? tabs[0]?.value);
13
19
  const active = value ?? internal;
14
20
  const listRef = React.useRef(null);
@@ -43,11 +49,11 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
43
49
  });
44
50
  return (_jsxs("div", { className: tabsRecipe({ className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "tabs-leading", children: leading }), tabs.map((t) => {
45
51
  const selected = t.value === active;
46
- return (_jsxs("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
52
+ return (_jsxs("button", { type: "button", role: "tab", id: tabId(t.value), "aria-selected": selected, "aria-controls": children && selected ? panelId(t.value) : undefined, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
47
53
  }), indicator && (_jsx("span", { className: "tabs-indicator", style: {
48
54
  width: indicator.width,
49
55
  transform: `translateX(${indicator.left}px)`,
50
- } }))] }), children && (_jsx("div", { className: "tabs-panel", role: "tabpanel", id: `panel-${active}`, "aria-labelledby": `tab-${active}`,
56
+ } }))] }), children && (_jsx("div", { className: "tabs-panel", role: "tabpanel", id: panelId(active), "aria-labelledby": tabId(active),
51
57
  // biome-ignore lint/a11y/noNoninteractiveTabindex: a tabpanel is focusable per the WAI-ARIA tabs pattern
52
58
  tabIndex: 0, children: children(active) }))] }));
53
59
  }
@@ -9,6 +9,7 @@ import { tooltipRecipe } from "./tooltip.recipe.js";
9
9
  * above the trigger (flipping below near the top edge).
10
10
  */
11
11
  export function Tooltip({ content, children, side = "top", delay = 350, open: openProp, defaultOpen, onOpenChange, }) {
12
+ const tipId = React.useId();
12
13
  const triggerRef = React.useRef(null);
13
14
  const timer = React.useRef(undefined);
14
15
  const [pos, setPos] = React.useState(null);
@@ -46,9 +47,26 @@ export function Tooltip({ content, children, side = "top", delay = 350, open: op
46
47
  setOpen(false);
47
48
  };
48
49
  React.useEffect(() => () => clearTimeout(timer.current), []);
49
- return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: children }), open &&
50
+ // The whole point of a tooltip is the description it gives its trigger, and
51
+ // the only way a screen reader learns of one is `aria-describedby` on the
52
+ // element that gets focus. It cannot go on the wrapper: a description on a
53
+ // non-focusable span around a button is never announced. So the trigger is
54
+ // cloned, keeping any aria-describedby it already had, and the reference is
55
+ // only present while the bubble is (a reference to a removed node is dead
56
+ // wiring, #225).
57
+ const described = React.isValidElement(children) && open
58
+ ? React.cloneElement(children, {
59
+ "aria-describedby": [
60
+ children.props["aria-describedby"],
61
+ tipId,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(" "),
65
+ })
66
+ : children;
67
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ref: triggerRef, style: { display: "inline-flex" }, onPointerEnter: openAfterDelay, onPointerLeave: close, onFocusCapture: openAfterDelay, onBlurCapture: close, onKeyDown: (e) => e.key === "Escape" && close(), children: described }), open &&
50
68
  pos != null &&
51
- createPortal(_jsx("div", { role: "tooltip", className: tooltipRecipe(), style: {
69
+ createPortal(_jsx("div", { id: tipId, role: "tooltip", className: tooltipRecipe(), style: {
52
70
  left: pos.left,
53
71
  top: pos.top,
54
72
  transform: pos.below
@@ -194,7 +194,7 @@ export function Command({ open, onOpenChange, items = defaultCommandItems, place
194
194
  };
195
195
  if (!dlg.open)
196
196
  return null;
197
- return (_jsx(Portal, { children: _jsxs("div", { className: commandRecipe(), children: [_jsx("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), _jsx("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(dlg.getContentProps(), describedBy), className: cx("command-content", className), children: _jsxs("div", { className: "command-card", children: [_jsxs("div", { className: "command-control", children: [_jsx(IconSearch, {}), _jsx("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), _jsx("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? (_jsx("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
197
+ return (_jsx(Portal, { children: _jsxs("div", { className: commandRecipe(), children: [_jsx("div", { ...dlg.getBackdropProps(), className: "command-backdrop" }), _jsx("div", { ...dlg.getPositionerProps(), className: "command-positioner", children: _jsx("div", { ...rest, ...withDescribedBy(dlg.getContentProps(), describedBy), className: cx("command-content", className), children: _jsxs("div", { className: "command-card", children: [_jsxs("div", { className: "command-control", children: [_jsx("label", { ...combo.getLabelProps(), className: "command-srlabel", children: ariaLabel }), _jsx(IconSearch, {}), _jsx("input", { ref: inputRef, ...combo.getInputProps(), className: "command-input", placeholder: placeholder })] }), _jsx("div", { ...combo.getContentProps(), className: "command-list", children: filteredItems.length === 0 ? (_jsx("div", { className: "command-empty", children: emptyLabel })) : (filteredItems.map((item, i) => {
198
198
  const Icon = item.icon;
199
199
  const showGroup = item.group && item.group !== filteredItems[i - 1]?.group;
200
200
  return (_jsxs(React.Fragment, { children: [showGroup && (_jsx("div", { className: "command-group-label", "aria-hidden": "true", children: item.group })), _jsxs("div", { ...combo.getItemProps({ item }), className: "command-item", children: [_jsx(Icon, {}), _jsx("span", { className: "command-item-label", children: item.label }), item.shortcut && (_jsx("span", { className: "command-item-shortcut", children: item.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) }))] })] }, item.id));
@@ -10,6 +10,12 @@ const config = defineRecipe({
10
10
  & .command-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden; }
11
11
  & .command-control { display: flex; align-items: center; gap: ${c.space["2.5"]}; height: 52px; flex-shrink: 0; padding: 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
12
12
  & .command-control svg { flex-shrink: 0; }
13
+ /* The palette's label element. It exists for the assistive tree only: zag's
14
+ combobox names the input through it and points the listbox's
15
+ aria-labelledby at it, and without it both were unnamed and the reference
16
+ resolved to nothing (#225). Clipped rather than display:none, which would
17
+ take it out of the tree it exists for. */
18
+ & .command-srlabel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
13
19
  & .command-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: ${c.font.sans}; font-size: ${c.text.xl}; color: ${c.fgPrimary}; }
14
20
  & .command-input::placeholder { color: ${c.fgMuted}; }
15
21
  & .command-list { flex: 1; min-height: 0; overflow-y: auto; padding: ${c.space["1"]} ${c.space["2.5"]}; display: flex; flex-direction: column; gap: ${c.space.px}; }
package/dist/styles.css CHANGED
@@ -1976,6 +1976,12 @@
1976
1976
  & .command-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: var(--radius-xl); background: var(--bg-surface); box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg-primary) 8%, transparent); overflow: hidden; }
1977
1977
  & .command-control { display: flex; align-items: center; gap: var(--space-2-5); height: 52px; flex-shrink: 0; padding: 0 var(--space-4); border-bottom: 1px solid var(--border-subtle); color: var(--fg-muted); }
1978
1978
  & .command-control svg { flex-shrink: 0; }
1979
+ /* The palette's label element. It exists for the assistive tree only: zag's
1980
+ combobox names the input through it and points the listbox's
1981
+ aria-labelledby at it, and without it both were unnamed and the reference
1982
+ resolved to nothing (#225). Clipped rather than display:none, which would
1983
+ take it out of the tree it exists for. */
1984
+ & .command-srlabel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
1979
1985
  & .command-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: var(--font-sans); font-size: var(--text-xl); color: var(--fg-primary); }
1980
1986
  & .command-input::placeholder { color: var(--fg-muted); }
1981
1987
  & .command-list { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-1) var(--space-2-5); display: flex; flex-direction: column; gap: var(--space-px); }
@@ -2,7 +2,12 @@ import * as React from "react";
2
2
  export type TooltipProps = {
3
3
  /** The hint text/content shown on hover or focus. */
4
4
  content: React.ReactNode;
5
- /** The trigger element. */
5
+ /**
6
+ * The trigger element. A SINGLE element: it is cloned to carry the
7
+ * `aria-describedby` that makes the hint reachable, so anything else — a
8
+ * string, a fragment, several children — renders and hovers correctly but
9
+ * cannot be described to a screen reader.
10
+ */
6
11
  children: React.ReactNode;
7
12
  /** Preferred side; flips to the other axis end when there's no room. */
8
13
  side?: "top" | "bottom";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",