@botiverse/raft-ui 0.76.0 → 0.77.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.
- package/CHANGELOG.md +18 -0
- package/dist/components/combobox/combobox.d.mts +7 -1
- package/dist/components/combobox/combobox.mjs +35 -8
- package/dist/components/combobox-select/combobox-select.css +5 -4
- package/dist/components/combobox-select/combobox-select.mjs +27 -3
- package/dist/components/select/select.d.mts +8 -1
- package/dist/components/select/select.mjs +44 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [0.77.0](https://github.com/botiverse/raft-design/compare/v0.76.1...v0.77.0) (2026-11-08)
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Select, Combobox and ComboboxSelect lists scroll in a ScrollArea**: each list is a `useScrollAreaContainer` container with absolute gutters, inside a frame (`data-slot="select-list-frame"`, `combobox-list-frame`) that holds the overlay scrollbar. The list is never a Tab stop: focus stays in the input or on the options. ([#1074](https://github.com/botiverse/raft-design/pull/1074))
|
|
6
|
+
- Why: they were the last native-scrolling exception for Base UI lists; on Windows they drew a classic scrollbar with arrows that took width from the options (Artea's runtimes list, #proj-raft-ui:20a421bd).
|
|
7
|
+
- Affects: the scrollbar is the thin overlay thumb on every platform. `SelectList` / `ComboboxList` render a frame `<div>` around the list element; `ref`, `className`, `style` and the other props still go to the list. CSS or tests that expected the list to be the popup's direct child, or waited on `document.getAnimations()` to finish (the thumb runs on a scroll timeline), need updating.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/scroll-area/
|
|
9
|
+
|
|
10
|
+
## [0.76.1](https://github.com/botiverse/raft-design/compare/v0.76.0...v0.76.1) (2026-11-08)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Select and Combobox lists never scroll sideways**: a Select list reaches into the popup's 4px inset and pads it back, so a `SelectSeparator` meets the popup's edge and stays inside the list, and Select and Combobox lists set `overflow-x-hidden`. ([#1070](https://github.com/botiverse/raft-design/pull/1070))
|
|
15
|
+
- Why: the separator reached 4px past each side of the list, which is the scroller, so the list scrolled sideways by 8px, and a classic scrollbar (Windows) drew a horizontal bar under the runtimes list (Artea, #proj-raft-ui:20a421bd). The list also clipped the line 4px short of the popup's edge.
|
|
16
|
+
- Affects: every Select with a separator, such as the runtime picker's Show installed only row: no sideways scroll, and the line now runs to the popup's border. Options keep their inset. A list action keeps its highlight after it adds items above it, as before.
|
|
17
|
+
- Docs: https://raft-design.botiverse.build/components/scroll-area/
|
|
18
|
+
|
|
1
19
|
## [0.76.0](https://github.com/botiverse/raft-design/compare/v0.75.0...v0.76.0) (2026-11-08)
|
|
2
20
|
|
|
3
21
|
### Changed
|
|
@@ -42,7 +42,13 @@ export type ComboboxContentProps = StyledProps<Combobox.Popup.Props> & Pick<Comb
|
|
|
42
42
|
};
|
|
43
43
|
export declare function ComboboxContent({ align, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, portalProps, className, initialFocus, finalFocus, ...props }: ComboboxContentProps): import("react").JSX.Element;
|
|
44
44
|
export type ComboboxListProps = StyledProps<Combobox.List.Props>;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The scrolling list, a ScrollArea container with absolute gutters: the
|
|
47
|
+
* overlay scrollbar lies on a frame around it, since Base UI owns the list's
|
|
48
|
+
* children (items can be a function of each item). The list is never a Tab
|
|
49
|
+
* stop: focus stays in the input, as Base UI manages it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ComboboxList({ className, ref, style, ...props }: ComboboxListProps): import("react").JSX.Element;
|
|
46
52
|
export type ComboboxItemProps = StyledProps<Combobox.Item.Props>;
|
|
47
53
|
export declare function ComboboxItem({ className, ...props }: ComboboxItemProps): import("react").JSX.Element;
|
|
48
54
|
export type ComboboxItemIndicatorProps = StyledProps<Combobox.ItemIndicator.Props>;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { cn } from "../../cn.mjs";
|
|
1
2
|
import { tv } from "../../lib/tv.mjs";
|
|
2
3
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
4
|
import { Button } from "../button/button.mjs";
|
|
5
|
+
import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
|
|
6
|
+
import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
|
|
4
7
|
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
5
8
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
6
9
|
import { chipRemoveBase } from "../../lib/chip-remove.mjs";
|
|
7
10
|
import { Check, X } from "lucide-react";
|
|
8
11
|
import { createContext, use, useCallback, useMemo, useState } from "react";
|
|
9
|
-
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
13
|
import { Combobox } from "@base-ui/react/combobox";
|
|
11
14
|
//#region src/components/combobox/combobox.tsx
|
|
12
15
|
const combobox = tv({
|
|
@@ -67,7 +70,8 @@ const combobox = tv({
|
|
|
67
70
|
],
|
|
68
71
|
header: ["flex items-center justify-between px-3", "gap-2 py-1.5 pr-1.5 pl-4"],
|
|
69
72
|
separator: ["h-px shrink-0 border-0", "bg-line-hairline"],
|
|
70
|
-
|
|
73
|
+
listFrame: ["min-h-0"],
|
|
74
|
+
list: ["max-h-64 min-h-0", "flex flex-col p-1 scroll-py-1"],
|
|
71
75
|
item: [
|
|
72
76
|
"flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
|
|
73
77
|
"text-left outline-none transition-colors",
|
|
@@ -319,12 +323,35 @@ function ComboboxContent({ align = "start", alignOffset = 0, anchor, arrowPaddin
|
|
|
319
323
|
})
|
|
320
324
|
});
|
|
321
325
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
/**
|
|
327
|
+
* The scrolling list, a ScrollArea container with absolute gutters: the
|
|
328
|
+
* overlay scrollbar lies on a frame around it, since Base UI owns the list's
|
|
329
|
+
* children (items can be a function of each item). The list is never a Tab
|
|
330
|
+
* stop: focus stays in the input, as Base UI manages it.
|
|
331
|
+
*/
|
|
332
|
+
function ComboboxList({ className, ref, style, ...props }) {
|
|
333
|
+
const { list, listFrame } = combobox();
|
|
334
|
+
const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
|
|
335
|
+
containerRef: ref,
|
|
336
|
+
guttersPosition: "absolute",
|
|
337
|
+
overscroll: "contain"
|
|
338
|
+
});
|
|
339
|
+
const ownStyle = useMemo(() => ({
|
|
340
|
+
...containerProps.style,
|
|
341
|
+
...style
|
|
342
|
+
}), [containerProps.style, style]);
|
|
343
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
344
|
+
...frameProps,
|
|
345
|
+
"data-slot": "combobox-list-frame",
|
|
346
|
+
className: listFrame({ className: frameProps.className }),
|
|
347
|
+
children: [/* @__PURE__ */ jsx(Combobox.List, {
|
|
348
|
+
"data-slot": "combobox-list",
|
|
349
|
+
...containerProps,
|
|
350
|
+
tabIndex: -1,
|
|
351
|
+
...props,
|
|
352
|
+
style: ownStyle,
|
|
353
|
+
className: list({ className: cn(containerProps.className, className) })
|
|
354
|
+
}), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
|
|
328
355
|
});
|
|
329
356
|
}
|
|
330
357
|
function ComboboxItem({ className, ...props }) {
|
|
@@ -181,13 +181,14 @@
|
|
|
181
181
|
var(--elevation-float);
|
|
182
182
|
outline: 0;
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
.r-combobox-select__list-frame {
|
|
185
|
+
min-height: 0;
|
|
186
|
+
}
|
|
187
|
+
/* A ScrollArea container (absolute gutters): useScrollAreaContainer gives it its overflow. */
|
|
186
188
|
.r-combobox-select__list {
|
|
187
189
|
box-sizing: border-box;
|
|
188
190
|
max-height: var(--combobox-select-list-max-height, 16rem);
|
|
189
|
-
|
|
190
|
-
overscroll-behavior: contain;
|
|
191
|
+
min-height: 0;
|
|
191
192
|
scroll-padding-block: 4px;
|
|
192
193
|
padding: 4px;
|
|
193
194
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useTheme } from "../../lib/theme/use-theme.mjs";
|
|
2
2
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
|
+
import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
|
|
4
|
+
import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
|
|
3
5
|
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
4
6
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
5
7
|
import { usePhoneMedia } from "../../lib/phone-media.mjs";
|
|
@@ -457,9 +459,8 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
|
|
|
457
459
|
className: popupClassName,
|
|
458
460
|
children: [
|
|
459
461
|
/* @__PURE__ */ jsx(Combobox.Status, { children: loadingMessage }),
|
|
460
|
-
/* @__PURE__ */ jsx(
|
|
461
|
-
|
|
462
|
-
"aria-busy": loading,
|
|
462
|
+
/* @__PURE__ */ jsx(ComboboxSelectList, {
|
|
463
|
+
loading,
|
|
463
464
|
children: renderGroup
|
|
464
465
|
}),
|
|
465
466
|
/* @__PURE__ */ jsx(Combobox.Empty, { children: emptyMessage })
|
|
@@ -469,5 +470,28 @@ function ComboboxSelect({ options, multiple, value, defaultValue, onValueChange,
|
|
|
469
470
|
})]
|
|
470
471
|
});
|
|
471
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* The scrolling list, a ScrollArea container with absolute gutters: the
|
|
475
|
+
* overlay scrollbar lies on a frame around it, since Base UI owns the list's
|
|
476
|
+
* children (a function of each group). The list is never a Tab stop: focus
|
|
477
|
+
* stays in the input, as Base UI manages it.
|
|
478
|
+
*/
|
|
479
|
+
function ComboboxSelectList({ children, loading }) {
|
|
480
|
+
const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
|
|
481
|
+
guttersPosition: "absolute",
|
|
482
|
+
overscroll: "contain"
|
|
483
|
+
});
|
|
484
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
485
|
+
...frameProps,
|
|
486
|
+
className: `${frameProps.className} r-combobox-select__list-frame`,
|
|
487
|
+
children: [/* @__PURE__ */ jsx(Combobox.List, {
|
|
488
|
+
...containerProps,
|
|
489
|
+
tabIndex: -1,
|
|
490
|
+
className: `${containerProps.className} r-combobox-select__list`,
|
|
491
|
+
"aria-busy": loading,
|
|
492
|
+
children
|
|
493
|
+
}), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
|
|
494
|
+
});
|
|
495
|
+
}
|
|
472
496
|
//#endregion
|
|
473
497
|
export { ComboboxSelect };
|
|
@@ -35,7 +35,14 @@ export type SelectContentProps = StyledProps<Select.Popup.Props> & Pick<Select.P
|
|
|
35
35
|
};
|
|
36
36
|
export declare function SelectContent({ align, alignItemWithTrigger, alignOffset, anchor, arrowPadding, collisionAvoidance, collisionBoundary, collisionPadding, disableAnchorTracking, positionMethod, side, sideOffset, sticky, portalProps, className, ...props }: SelectContentProps): import("react").JSX.Element;
|
|
37
37
|
export type SelectListProps = StyledProps<Select.List.Props>;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The scrolling list, a ScrollArea container with absolute gutters: its frame
|
|
40
|
+
* reaches into the popup's 4px inset and the list pads it back, so a
|
|
41
|
+
* separator meets the popup's edge, and the overlay scrollbar takes no room
|
|
42
|
+
* from the options, on every platform. The list is never a Tab stop: focus
|
|
43
|
+
* stays on the options, as Base UI manages it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function SelectList({ className, ref, style, ...props }: SelectListProps): import("react").JSX.Element;
|
|
39
46
|
export type SelectItemProps = StyledProps<Select.Item.Props>;
|
|
40
47
|
export declare function SelectItem({ className, ...props }: SelectItemProps): import("react").JSX.Element;
|
|
41
48
|
export type SelectItemLeadingProps = StyledProps<ComponentPropsWithRef<"span">>;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { cn } from "../../cn.mjs";
|
|
1
2
|
import { tv } from "../../lib/tv.mjs";
|
|
2
3
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
3
4
|
import { Button } from "../button/button.mjs";
|
|
5
|
+
import { ScrollAreaGutters } from "../scroll-area/scroll-area-gutters.mjs";
|
|
6
|
+
import { useScrollAreaContainer } from "../scroll-area/use-scroll-area-container.mjs";
|
|
4
7
|
import { usePopupCollision } from "../../lib/frame-insets.mjs";
|
|
5
8
|
import { useOverflowTitle } from "../../lib/use-overflow-title.mjs";
|
|
6
9
|
import { Check, ChevronDown } from "lucide-react";
|
|
7
10
|
import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
|
-
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
12
|
import { Select } from "@base-ui/react/select";
|
|
10
13
|
//#region src/components/select/select.tsx
|
|
11
14
|
const select = tv({
|
|
@@ -44,7 +47,8 @@ const select = tv({
|
|
|
44
47
|
"rounded-container bg-layer-popover p-1 text-foreground",
|
|
45
48
|
"shadow-float ring-1 ring-line-seam"
|
|
46
49
|
],
|
|
47
|
-
|
|
50
|
+
listFrame: ["-mx-1 min-h-0"],
|
|
51
|
+
list: ["max-h-64 min-h-0 scroll-py-6", "flex flex-col px-1"],
|
|
48
52
|
item: [
|
|
49
53
|
"flex w-full shrink-0 cursor-default select-none items-center justify-between overflow-hidden",
|
|
50
54
|
"text-left outline-none transition-colors",
|
|
@@ -151,7 +155,9 @@ function findActionValue(value) {
|
|
|
151
155
|
* row, the same position then holds another item while focus stays on the row,
|
|
152
156
|
* and the next arrow key starts from the wrong place. Once the list has
|
|
153
157
|
* re-rendered, focus the list and then the row again: the row's focus handler
|
|
154
|
-
* re-reads its position and moves the highlight back to it.
|
|
158
|
+
* re-reads its position and moves the highlight back to it. The list takes
|
|
159
|
+
* focus only for this: it has no tabindex of its own, and Chrome focuses a
|
|
160
|
+
* scroller without one only while its content overflows it.
|
|
155
161
|
*/
|
|
156
162
|
function keepActionHighlighted(event) {
|
|
157
163
|
const target = event.target;
|
|
@@ -160,8 +166,12 @@ function keepActionHighlighted(event) {
|
|
|
160
166
|
if (!row) return;
|
|
161
167
|
requestAnimationFrame(() => {
|
|
162
168
|
if (!row.isConnected || row.hasAttribute("data-highlighted")) return;
|
|
163
|
-
row.closest("[role=\"listbox\"]")
|
|
169
|
+
const list = row.closest("[role=\"listbox\"]");
|
|
170
|
+
const lent = list !== null && !list.hasAttribute("tabindex");
|
|
171
|
+
if (lent) list.tabIndex = -1;
|
|
172
|
+
list?.focus({ preventScroll: true });
|
|
164
173
|
row.focus({ preventScroll: true });
|
|
174
|
+
if (lent) list.removeAttribute("tabindex");
|
|
165
175
|
});
|
|
166
176
|
}
|
|
167
177
|
function Select$1({ chrome = "default", onValueChange, onOpenChange, isItemEqualToValue, itemToStringLabel, itemToStringValue, ...props }) {
|
|
@@ -300,12 +310,36 @@ function SelectContent({ align = "start", alignItemWithTrigger = false, alignOff
|
|
|
300
310
|
children: positionerNode
|
|
301
311
|
});
|
|
302
312
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
/**
|
|
314
|
+
* The scrolling list, a ScrollArea container with absolute gutters: its frame
|
|
315
|
+
* reaches into the popup's 4px inset and the list pads it back, so a
|
|
316
|
+
* separator meets the popup's edge, and the overlay scrollbar takes no room
|
|
317
|
+
* from the options, on every platform. The list is never a Tab stop: focus
|
|
318
|
+
* stays on the options, as Base UI manages it.
|
|
319
|
+
*/
|
|
320
|
+
function SelectList({ className, ref, style, ...props }) {
|
|
321
|
+
const { list, listFrame } = select();
|
|
322
|
+
const { containerProps, frameProps, guttersProps } = useScrollAreaContainer({
|
|
323
|
+
containerRef: ref,
|
|
324
|
+
guttersPosition: "absolute",
|
|
325
|
+
overscroll: "contain"
|
|
326
|
+
});
|
|
327
|
+
const ownStyle = useMemo(() => ({
|
|
328
|
+
...containerProps.style,
|
|
329
|
+
...style
|
|
330
|
+
}), [containerProps.style, style]);
|
|
331
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
332
|
+
...frameProps,
|
|
333
|
+
"data-slot": "select-list-frame",
|
|
334
|
+
className: listFrame({ className: frameProps.className }),
|
|
335
|
+
children: [/* @__PURE__ */ jsx(Select.List, {
|
|
336
|
+
"data-slot": "select-list",
|
|
337
|
+
...containerProps,
|
|
338
|
+
tabIndex: -1,
|
|
339
|
+
...props,
|
|
340
|
+
style: ownStyle,
|
|
341
|
+
className: list({ className: cn(containerProps.className, className) })
|
|
342
|
+
}), /* @__PURE__ */ jsx(ScrollAreaGutters, { ...guttersProps })]
|
|
309
343
|
});
|
|
310
344
|
}
|
|
311
345
|
const SelectItemPartsContext = createContext(null);
|