bits-ui 2.2.1 → 2.3.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.
@@ -26,6 +26,7 @@ export { Popover } from "./popover/index.js";
26
26
  export { Progress } from "./progress/index.js";
27
27
  export { RadioGroup } from "./radio-group/index.js";
28
28
  export { RangeCalendar } from "./range-calendar/index.js";
29
+ export { RatingGroup } from "./rating-group/index.js";
29
30
  export { ScrollArea } from "./scroll-area/index.js";
30
31
  export { Select } from "./select/index.js";
31
32
  export { Separator } from "./separator/index.js";
@@ -26,6 +26,7 @@ export { Popover } from "./popover/index.js";
26
26
  export { Progress } from "./progress/index.js";
27
27
  export { RadioGroup } from "./radio-group/index.js";
28
28
  export { RangeCalendar } from "./range-calendar/index.js";
29
+ export { RatingGroup } from "./rating-group/index.js";
29
30
  export { ScrollArea } from "./scroll-area/index.js";
30
31
  export { Select } from "./select/index.js";
31
32
  export { Separator } from "./separator/index.js";
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import { useRatingGroupHiddenInput } from "../rating-group.svelte.js";
3
+ import HiddenInput from "../../utilities/hidden-input.svelte";
4
+
5
+ const inputState = useRatingGroupHiddenInput();
6
+ </script>
7
+
8
+ {#if inputState.shouldRender}
9
+ <HiddenInput {...inputState.props} />
10
+ {/if}
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const RatingGroupInput: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type RatingGroupInput = InstanceType<typeof RatingGroupInput>;
18
+ export default RatingGroupInput;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { box, mergeProps } from "svelte-toolbelt";
3
+ import type { RatingGroupItemProps } from "../types.js";
4
+ import { useRatingGroupItem } from "../rating-group.svelte.js";
5
+ import { createId } from "../../../internal/create-id.js";
6
+
7
+ const uid = $props.id();
8
+
9
+ let {
10
+ disabled = false,
11
+ index,
12
+ children,
13
+ child,
14
+ ref = $bindable(null),
15
+ id = createId(uid),
16
+ ...restProps
17
+ }: RatingGroupItemProps = $props();
18
+
19
+ const itemState = useRatingGroupItem({
20
+ disabled: box.with(() => Boolean(disabled)),
21
+ index: box.with(() => index),
22
+ id: box.with(() => id),
23
+ ref: box.with(
24
+ () => ref,
25
+ (v) => (ref = v)
26
+ ),
27
+ });
28
+
29
+ const mergedProps = $derived(mergeProps(restProps, itemState.props));
30
+ </script>
31
+
32
+ {#if child}
33
+ {@render child({ props: mergedProps, ...itemState.snippetProps })}
34
+ {:else}
35
+ <div {...mergedProps}>
36
+ {@render children?.(itemState.snippetProps)}
37
+ </div>
38
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { RatingGroupItemProps } from "../types.js";
2
+ declare const RatingGroupItem: import("svelte").Component<RatingGroupItemProps, {}, "ref">;
3
+ type RatingGroupItem = ReturnType<typeof RatingGroupItem>;
4
+ export default RatingGroupItem;
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import { box, mergeProps } from "svelte-toolbelt";
3
+ import type { RatingGroupRootProps } from "../types.js";
4
+ import { useRatingGroupRoot } from "../rating-group.svelte.js";
5
+ import RatingGroupInput from "./rating-group-input.svelte";
6
+ import { createId } from "../../../internal/create-id.js";
7
+ import { noop } from "../../../internal/noop.js";
8
+
9
+ const uid = $props.id();
10
+
11
+ let {
12
+ disabled = false,
13
+ children,
14
+ child,
15
+ value = $bindable(0),
16
+ ref = $bindable(null),
17
+ orientation = "horizontal",
18
+ name = undefined,
19
+ required = false,
20
+ min = 0,
21
+ max = 5,
22
+ allowHalf = false,
23
+ readonly = false,
24
+ id = createId(uid),
25
+ onValueChange = noop,
26
+ "aria-label": ariaLabel,
27
+ "aria-valuetext": ariaValuetextProp,
28
+ hoverPreview = true,
29
+ ...restProps
30
+ }: RatingGroupRootProps = $props();
31
+
32
+ if (value < min || value > max) {
33
+ value = Math.max(min, Math.min(max, value));
34
+ }
35
+
36
+ const ariaValuetext: NonNullable<RatingGroupRootProps["aria-valuetext"]> = $derived.by(() => {
37
+ if (ariaValuetextProp) return ariaValuetextProp;
38
+ return (value: number, max: number) => `${value} out of ${max}`;
39
+ });
40
+
41
+ const rootState = useRatingGroupRoot({
42
+ orientation: box.with(() => orientation),
43
+ disabled: box.with(() => disabled),
44
+ name: box.with(() => name),
45
+ required: box.with(() => required),
46
+ min: box.with(() => min),
47
+ max: box.with(() => max),
48
+ allowHalf: box.with(() => allowHalf),
49
+ readonly: box.with(() => readonly),
50
+ id: box.with(() => id),
51
+ value: box.with(
52
+ () => value,
53
+ (v) => {
54
+ if (v === value) return;
55
+ value = v;
56
+ onValueChange?.(v);
57
+ }
58
+ ),
59
+ ref: box.with(
60
+ () => ref,
61
+ (v) => (ref = v)
62
+ ),
63
+ ariaValuetext: box.with(() => ariaValuetext),
64
+ hoverPreview: box.with(() => hoverPreview),
65
+ });
66
+
67
+ const mergedProps = $derived(
68
+ mergeProps(restProps, rootState.props, { "aria-label": ariaLabel })
69
+ );
70
+ </script>
71
+
72
+ {#if child}
73
+ {@render child({ props: mergedProps, ...rootState.snippetProps })}
74
+ {:else}
75
+ <div {...mergedProps}>
76
+ {@render children?.(rootState.snippetProps)}
77
+ </div>
78
+ {/if}
79
+
80
+ <RatingGroupInput />
@@ -0,0 +1,4 @@
1
+ import type { RatingGroupRootProps } from "../types.js";
2
+ declare const RatingGroup: import("svelte").Component<RatingGroupRootProps, {}, "value" | "ref">;
3
+ type RatingGroup = ReturnType<typeof RatingGroup>;
4
+ export default RatingGroup;
@@ -0,0 +1,3 @@
1
+ export { default as Root } from "./components/rating-group.svelte";
2
+ export { default as Item } from "./components/rating-group-item.svelte";
3
+ export type { RatingGroupRootProps as RootProps, RatingGroupItemProps as ItemProps, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { default as Root } from "./components/rating-group.svelte";
2
+ export { default as Item } from "./components/rating-group-item.svelte";
@@ -0,0 +1 @@
1
+ export * as RatingGroup from "./exports.js";
@@ -0,0 +1 @@
1
+ export * as RatingGroup from "./exports.js";
@@ -0,0 +1,114 @@
1
+ import { DOMContext } from "svelte-toolbelt";
2
+ import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
+ import type { BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
4
+ import type { RatingGroupAriaValuetext, RatingGroupItemState as RatingGroupItemStateType } from "./types.js";
5
+ import type { Orientation } from "../../shared/index.js";
6
+ type RatingGroupRootStateProps = WithRefProps<ReadableBoxedValues<{
7
+ disabled: boolean;
8
+ required: boolean;
9
+ orientation: Orientation;
10
+ name: string | undefined;
11
+ min: number;
12
+ max: number;
13
+ allowHalf: boolean;
14
+ readonly: boolean;
15
+ hoverPreview: boolean;
16
+ ariaValuetext: NonNullable<RatingGroupAriaValuetext>;
17
+ }> & WritableBoxedValues<{
18
+ value: number;
19
+ }>>;
20
+ declare class RatingGroupRootState {
21
+ #private;
22
+ readonly opts: RatingGroupRootStateProps;
23
+ domContext: DOMContext;
24
+ readonly hasValue: boolean;
25
+ readonly valueToUse: number;
26
+ readonly isRTL: boolean;
27
+ readonly ariaValuetext: string;
28
+ readonly items: {
29
+ index: number;
30
+ state: RatingGroupItemStateType;
31
+ }[];
32
+ constructor(opts: RatingGroupRootStateProps);
33
+ isActive(itemIndex: number): boolean;
34
+ isPartial(itemIndex: number): boolean;
35
+ setHoverValue(value: number | null): void;
36
+ setValue(value: number): void;
37
+ calculateRatingFromPointer(itemIndex: number, event: {
38
+ clientX: number;
39
+ clientY: number;
40
+ currentTarget: HTMLElement;
41
+ }): number;
42
+ onpointerleave(): void;
43
+ readonly handlers: Record<string, () => void>;
44
+ onkeydown(e: BitsKeyboardEvent): void;
45
+ readonly snippetProps: {
46
+ items: {
47
+ index: number;
48
+ state: RatingGroupItemStateType;
49
+ }[];
50
+ value: number;
51
+ max: number;
52
+ };
53
+ readonly props: {
54
+ readonly id: string;
55
+ readonly role: "slider";
56
+ readonly "aria-valuenow": number;
57
+ readonly "aria-valuemin": number;
58
+ readonly "aria-valuemax": number;
59
+ readonly "aria-valuetext": string;
60
+ readonly "aria-orientation": Orientation;
61
+ readonly "aria-required": "true" | "false";
62
+ readonly "aria-disabled": "true" | undefined;
63
+ readonly "aria-label": "Rating";
64
+ readonly "data-disabled": "" | undefined;
65
+ readonly "data-readonly": "" | undefined;
66
+ readonly "data-orientation": Orientation;
67
+ readonly tabindex: 0 | -1;
68
+ readonly "data-rating-group-root": "";
69
+ readonly onkeydown: (e: BitsKeyboardEvent) => void;
70
+ readonly onpointerleave: () => void;
71
+ };
72
+ }
73
+ type RatingGroupItemStateProps = WithRefProps<ReadableBoxedValues<{
74
+ disabled: boolean;
75
+ index: number;
76
+ }>>;
77
+ declare class RatingGroupItemState {
78
+ #private;
79
+ readonly opts: RatingGroupItemStateProps;
80
+ readonly root: RatingGroupRootState;
81
+ constructor(opts: RatingGroupItemStateProps, root: RatingGroupRootState);
82
+ onclick(e: BitsMouseEvent): void;
83
+ onpointermove(e: BitsPointerEvent): void;
84
+ readonly snippetProps: {
85
+ readonly state: RatingGroupItemStateType;
86
+ };
87
+ readonly props: {
88
+ readonly id: string;
89
+ readonly role: "presentation";
90
+ readonly "data-value": number;
91
+ readonly "data-orientation": Orientation;
92
+ readonly "data-disabled": "" | undefined;
93
+ readonly "data-readonly": "" | undefined;
94
+ readonly "data-state": RatingGroupItemStateType;
95
+ readonly "data-rating-group-item": "";
96
+ readonly onclick: (e: BitsMouseEvent) => void;
97
+ readonly onpointermove: (e: BitsPointerEvent) => void;
98
+ };
99
+ }
100
+ declare class RatingGroupHiddenInputState {
101
+ readonly root: RatingGroupRootState;
102
+ readonly shouldRender: boolean;
103
+ readonly props: {
104
+ readonly name: string | undefined;
105
+ readonly value: number;
106
+ readonly required: boolean;
107
+ readonly disabled: boolean;
108
+ };
109
+ constructor(root: RatingGroupRootState);
110
+ }
111
+ export declare function useRatingGroupRoot(props: RatingGroupRootStateProps): RatingGroupRootState;
112
+ export declare function useRatingGroupItem(props: RatingGroupItemStateProps): RatingGroupItemState;
113
+ export declare function useRatingGroupHiddenInput(): RatingGroupHiddenInputState;
114
+ export {};
@@ -0,0 +1,295 @@
1
+ import { attachRef, DOMContext } from "svelte-toolbelt";
2
+ import { Context } from "runed";
3
+ import { getAriaRequired, getDataDisabled } from "../../internal/attrs.js";
4
+ import { kbd } from "../../internal/kbd.js";
5
+ const RATING_GROUP_ROOT_ATTR = "data-rating-group-root";
6
+ const RATING_GROUP_ITEM_ATTR = "data-rating-group-item";
7
+ class RatingGroupRootState {
8
+ opts;
9
+ #hoverValue = $state(null);
10
+ #keySequence = $state("");
11
+ #keySequenceTimeout = null;
12
+ domContext;
13
+ hasValue = $derived.by(() => this.opts.value.current > 0);
14
+ valueToUse = $derived.by(() => this.#hoverValue ?? this.opts.value.current);
15
+ isRTL = $derived.by(() => {
16
+ const element = this.opts.ref.current;
17
+ if (!element)
18
+ return false;
19
+ const style = getComputedStyle(element);
20
+ return style.direction === "rtl";
21
+ });
22
+ ariaValuetext = $derived.by(() => {
23
+ return typeof this.opts.ariaValuetext.current === "function"
24
+ ? this.opts.ariaValuetext.current(this.opts.value.current, this.opts.max.current)
25
+ : this.opts.ariaValuetext.current;
26
+ });
27
+ items = $derived.by(() => {
28
+ const { max, allowHalf } = this.opts;
29
+ const value = this.valueToUse;
30
+ return Array.from({ length: max.current }, (_, i) => {
31
+ const itemValue = i + 1;
32
+ const halfValue = itemValue - 0.5;
33
+ const state = value >= itemValue
34
+ ? "active"
35
+ : allowHalf.current && value >= halfValue
36
+ ? "partial"
37
+ : "inactive";
38
+ return { index: i, state };
39
+ });
40
+ });
41
+ constructor(opts) {
42
+ this.opts = opts;
43
+ this.onkeydown = this.onkeydown.bind(this);
44
+ this.onpointerleave = this.onpointerleave.bind(this);
45
+ this.domContext = new DOMContext(this.opts.ref);
46
+ }
47
+ isActive(itemIndex) {
48
+ return this.valueToUse >= itemIndex + 1;
49
+ }
50
+ isPartial(itemIndex) {
51
+ if (!this.opts.allowHalf.current)
52
+ return false;
53
+ const itemValue = itemIndex + 1;
54
+ return this.valueToUse >= itemValue - 0.5 && this.valueToUse < itemValue;
55
+ }
56
+ setHoverValue(value) {
57
+ if (this.opts.readonly.current ||
58
+ this.opts.disabled.current ||
59
+ !this.opts.hoverPreview.current)
60
+ return;
61
+ this.#hoverValue =
62
+ value === null
63
+ ? null
64
+ : Math.max(this.opts.min.current, Math.min(this.opts.max.current, value));
65
+ }
66
+ setValue(value) {
67
+ if (this.opts.readonly.current || this.opts.disabled.current)
68
+ return;
69
+ this.opts.value.current = Math.max(this.opts.min.current, Math.min(this.opts.max.current, value));
70
+ }
71
+ calculateRatingFromPointer(itemIndex, event) {
72
+ const ratingValue = itemIndex + 1;
73
+ if (!this.opts.allowHalf.current)
74
+ return ratingValue;
75
+ const rect = event.currentTarget.getBoundingClientRect();
76
+ const style = getComputedStyle(event.currentTarget);
77
+ const isHorizontal = this.opts.orientation.current === "horizontal";
78
+ const position = isHorizontal
79
+ ? (event.clientX - rect.left) / rect.width
80
+ : (event.clientY - rect.top) / rect.height;
81
+ const normalizedPosition = style.direction === "rtl" ? 1 - position : position;
82
+ return normalizedPosition < 0.5 ? ratingValue - 0.5 : ratingValue;
83
+ }
84
+ onpointerleave() {
85
+ this.setHoverValue(null);
86
+ }
87
+ handlers = {
88
+ [kbd.ARROW_UP]: () => this.#adjustValue(this.opts.allowHalf.current ? 0.5 : 1),
89
+ [kbd.ARROW_RIGHT]: () => {
90
+ const increment = this.opts.allowHalf.current ? 0.5 : 1;
91
+ // in RTL mode, right arrow should decrement
92
+ this.#adjustValue(this.isRTL ? -increment : increment);
93
+ },
94
+ [kbd.ARROW_DOWN]: () => this.#adjustValue(this.opts.allowHalf.current ? -0.5 : -1),
95
+ [kbd.ARROW_LEFT]: () => {
96
+ const increment = this.opts.allowHalf.current ? 0.5 : 1;
97
+ // in RTL mode, left arrow should increment
98
+ this.#adjustValue(this.isRTL ? increment : -increment);
99
+ },
100
+ [kbd.HOME]: () => this.setValue(this.opts.min.current),
101
+ [kbd.END]: () => this.setValue(this.opts.max.current),
102
+ [kbd.PAGE_UP]: () => this.#adjustValue(1),
103
+ [kbd.PAGE_DOWN]: () => this.#adjustValue(-1),
104
+ };
105
+ onkeydown(e) {
106
+ if (this.opts.disabled.current || this.opts.readonly.current)
107
+ return;
108
+ if (this.handlers[e.key]) {
109
+ e.preventDefault();
110
+ this.#clearKeySequence();
111
+ this.handlers[e.key]?.();
112
+ return;
113
+ }
114
+ if (this.opts.allowHalf.current && this.#handleDecimalInput(e))
115
+ return;
116
+ // handle direct number input
117
+ const num = parseInt(e.key || "");
118
+ if (!isNaN(num) && e.key) {
119
+ e.preventDefault();
120
+ if (num >= this.opts.min.current && num <= this.opts.max.current) {
121
+ this.setValue(num);
122
+ if (this.opts.allowHalf.current) {
123
+ this.#startDecimalListening(num);
124
+ }
125
+ }
126
+ return;
127
+ }
128
+ this.#clearKeySequence();
129
+ }
130
+ #adjustValue(delta) {
131
+ this.setValue(this.opts.value.current + delta);
132
+ }
133
+ #handleDecimalInput(e) {
134
+ if (!e.key)
135
+ return false;
136
+ if (e.key === ".") {
137
+ e.preventDefault();
138
+ this.#keySequence += e.key;
139
+ return true;
140
+ }
141
+ if (e.key === "5" && this.#keySequence.match(/^\d+\.$/)) {
142
+ e.preventDefault();
143
+ this.#keySequence += e.key;
144
+ const match = this.#keySequence.match(/^(\d+)\.5$/);
145
+ if (match?.[1]) {
146
+ const value = parseFloat(this.#keySequence);
147
+ if (value >= this.opts.min.current && value <= this.opts.max.current) {
148
+ this.setValue(value);
149
+ this.#clearKeySequence();
150
+ }
151
+ }
152
+ return true;
153
+ }
154
+ return false;
155
+ }
156
+ #startDecimalListening(baseValue) {
157
+ this.#keySequence = baseValue.toString();
158
+ if (this.#keySequenceTimeout) {
159
+ this.domContext.clearTimeout(this.#keySequenceTimeout);
160
+ }
161
+ this.#keySequenceTimeout = this.domContext.setTimeout(() => this.#clearKeySequence(), 1000);
162
+ }
163
+ #clearKeySequence() {
164
+ this.#keySequence = "";
165
+ if (this.#keySequenceTimeout) {
166
+ this.domContext.clearTimeout(this.#keySequenceTimeout);
167
+ this.#keySequenceTimeout = null;
168
+ }
169
+ }
170
+ snippetProps = $derived.by(() => ({
171
+ items: this.items,
172
+ value: this.opts.value.current,
173
+ max: this.opts.max.current,
174
+ }));
175
+ props = $derived.by(() => {
176
+ return {
177
+ id: this.opts.id.current,
178
+ role: "slider",
179
+ "aria-valuenow": this.opts.value.current,
180
+ "aria-valuemin": this.opts.min.current,
181
+ "aria-valuemax": this.opts.max.current,
182
+ "aria-valuetext": this.ariaValuetext,
183
+ "aria-orientation": this.opts.orientation.current,
184
+ "aria-required": getAriaRequired(this.opts.required.current),
185
+ "aria-disabled": this.opts.disabled.current ? "true" : undefined,
186
+ "aria-label": "Rating",
187
+ "data-disabled": getDataDisabled(this.opts.disabled.current),
188
+ "data-readonly": this.opts.readonly.current ? "" : undefined,
189
+ "data-orientation": this.opts.orientation.current,
190
+ tabindex: this.opts.disabled.current ? -1 : 0,
191
+ [RATING_GROUP_ROOT_ATTR]: "",
192
+ onkeydown: this.onkeydown,
193
+ onpointerleave: this.onpointerleave,
194
+ ...attachRef(this.opts.ref),
195
+ };
196
+ });
197
+ }
198
+ class RatingGroupItemState {
199
+ opts;
200
+ root;
201
+ #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current);
202
+ #isActive = $derived.by(() => this.root.isActive(this.opts.index.current));
203
+ #isPartial = $derived.by(() => this.root.isPartial(this.opts.index.current));
204
+ #state = $derived.by(() => {
205
+ if (this.#isActive)
206
+ return "active";
207
+ if (this.#isPartial)
208
+ return "partial";
209
+ return "inactive";
210
+ });
211
+ constructor(opts, root) {
212
+ this.opts = opts;
213
+ this.root = root;
214
+ this.onclick = this.onclick.bind(this);
215
+ this.onpointermove = this.onpointermove.bind(this);
216
+ }
217
+ onclick(e) {
218
+ if (this.#isDisabled || this.root.opts.readonly.current)
219
+ return;
220
+ // handle clearing when clicking on first item (index 0) that's already
221
+ // active and min is 0
222
+ if (this.opts.index.current === 0 &&
223
+ this.root.opts.min.current === 0 &&
224
+ this.root.opts.value.current > 0) {
225
+ const newValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
226
+ const currentValue = this.root.opts.value.current;
227
+ // only clear if the calculated rating exactly matches current value
228
+ if (newValue === currentValue) {
229
+ this.root.setValue(0);
230
+ if (this.root.opts.ref.current) {
231
+ this.root.opts.ref.current.focus();
232
+ }
233
+ return;
234
+ }
235
+ }
236
+ const newValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
237
+ this.root.setValue(newValue);
238
+ if (this.root.opts.ref.current) {
239
+ this.root.opts.ref.current.focus();
240
+ }
241
+ }
242
+ onpointermove(e) {
243
+ if (this.#isDisabled ||
244
+ this.root.opts.readonly.current ||
245
+ !this.root.opts.hoverPreview.current)
246
+ return;
247
+ // skip hover preview for touch devices
248
+ if (e.pointerType === "touch")
249
+ return;
250
+ const hoverValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
251
+ this.root.setHoverValue(hoverValue);
252
+ }
253
+ snippetProps = $derived.by(() => {
254
+ return {
255
+ state: this.#state,
256
+ };
257
+ });
258
+ props = $derived.by(() => ({
259
+ id: this.opts.id.current,
260
+ role: "presentation",
261
+ "data-value": this.opts.index.current + 1,
262
+ "data-orientation": this.root.opts.orientation.current,
263
+ "data-disabled": getDataDisabled(this.#isDisabled),
264
+ "data-readonly": this.root.opts.readonly.current ? "" : undefined,
265
+ "data-state": this.#state,
266
+ [RATING_GROUP_ITEM_ATTR]: "",
267
+ //
268
+ onclick: this.onclick,
269
+ onpointermove: this.onpointermove,
270
+ ...attachRef(this.opts.ref),
271
+ }));
272
+ }
273
+ class RatingGroupHiddenInputState {
274
+ root;
275
+ shouldRender = $derived.by(() => this.root.opts.name.current !== undefined);
276
+ props = $derived.by(() => ({
277
+ name: this.root.opts.name.current,
278
+ value: this.root.opts.value.current,
279
+ required: this.root.opts.required.current,
280
+ disabled: this.root.opts.disabled.current,
281
+ }));
282
+ constructor(root) {
283
+ this.root = root;
284
+ }
285
+ }
286
+ const RatingGroupRootContext = new Context("RatingGroup.Root");
287
+ export function useRatingGroupRoot(props) {
288
+ return RatingGroupRootContext.set(new RatingGroupRootState(props));
289
+ }
290
+ export function useRatingGroupItem(props) {
291
+ return new RatingGroupItemState(props, RatingGroupRootContext.get());
292
+ }
293
+ export function useRatingGroupHiddenInput() {
294
+ return new RatingGroupHiddenInputState(RatingGroupRootContext.get());
295
+ }
@@ -0,0 +1,111 @@
1
+ import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
2
+ import type { Orientation } from "../../index.js";
3
+ import type { BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
4
+ export type RatingGroupItemState = "active" | "partial" | "inactive";
5
+ export type RatingGroupItemData = {
6
+ index: number;
7
+ state: RatingGroupItemState;
8
+ };
9
+ export type RatingGroupRootSnippetProps = {
10
+ items: RatingGroupItemData[];
11
+ value: number;
12
+ max: number;
13
+ };
14
+ export type RatingGroupAriaValuetext = BitsPrimitiveDivAttributes["aria-valuetext"] | ((value: number, max: number) => string);
15
+ export type RatingGroupRootPropsWithoutHTML = WithChild<{
16
+ /**
17
+ * The orientation of the rating group. Used to determine
18
+ * how keyboard interactions work.
19
+ *
20
+ * @default "horizontal"
21
+ */
22
+ orientation?: Orientation;
23
+ /**
24
+ * The current rating value.
25
+ *
26
+ * @default 0
27
+ */
28
+ value?: number;
29
+ /**
30
+ * The callback to call when the rating value changes.
31
+ */
32
+ onValueChange?: OnChangeFn<number>;
33
+ /**
34
+ * The name to apply to the rating group's input element for
35
+ * form submission. If not provided, a hidden input will not
36
+ * be rendered and the rating group will not be part of a form.
37
+ *
38
+ * @default undefined
39
+ */
40
+ name?: string;
41
+ /**
42
+ * Whether the rating group is disabled.
43
+ *
44
+ * @default false
45
+ */
46
+ disabled?: boolean;
47
+ /**
48
+ * Whether the rating group is required for form submission.
49
+ * If `true`, ensure you provide a `name` prop so the hidden
50
+ * input is rendered.
51
+ *
52
+ * @default false
53
+ */
54
+ required?: boolean;
55
+ /**
56
+ * The minimum rating value.
57
+ *
58
+ * @default 0
59
+ */
60
+ min?: number;
61
+ /**
62
+ * The maximum rating value (number of items).
63
+ *
64
+ * @default 5
65
+ */
66
+ max?: number;
67
+ /**
68
+ * Whether to allow half-star ratings.
69
+ *
70
+ * @default false
71
+ */
72
+ allowHalf?: boolean;
73
+ /**
74
+ * Whether the rating group is readonly.
75
+ *
76
+ * @default false
77
+ */
78
+ readonly?: boolean;
79
+ /**
80
+ * Whether to show a preview when hovering over rating items.
81
+ * Touch events are ignored to prevent accidental previews.
82
+ *
83
+ * @default true
84
+ */
85
+ hoverPreview?: boolean;
86
+ /**
87
+ * An extended `aria-valuetext` property to use for the rating group.
88
+ * Can either be a string, or a function that receives the current value
89
+ * and max value and returns a string.
90
+ *
91
+ * @default ((value: number, max: number) => `${value} out of ${max}`)
92
+ */
93
+ "aria-valuetext"?: RatingGroupAriaValuetext;
94
+ }, RatingGroupRootSnippetProps>;
95
+ export type RatingGroupRootProps = RatingGroupRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, RatingGroupRootPropsWithoutHTML>;
96
+ export type RatingGroupItemSnippetProps = {
97
+ state: RatingGroupItemState;
98
+ };
99
+ export type RatingGroupItemPropsWithoutHTML = WithChild<{
100
+ /**
101
+ * The index of the rating item (0-based index).
102
+ */
103
+ index: number;
104
+ /**
105
+ * Whether the rating item is disabled.
106
+ *
107
+ * @default false
108
+ */
109
+ disabled?: boolean | null | undefined;
110
+ }, RatingGroupItemSnippetProps>;
111
+ export type RatingGroupItemProps = RatingGroupItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, RatingGroupItemPropsWithoutHTML>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, Meter, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, TimeField, TimeRangeField, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
1
+ export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, Meter, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, RatingGroup as unstable_RatingGroup, ScrollArea, Select, Separator, Slider, Switch, Tabs, TimeField, TimeRangeField, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
2
2
  export * from "./shared/index.js";
3
3
  export type * from "./shared/index.js";
4
4
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, Meter, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, TimeField, TimeRangeField, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
1
+ export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, Meter, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, RatingGroup as unstable_RatingGroup, ScrollArea, Select, Separator, Slider, Switch, Tabs, TimeField, TimeRangeField, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
2
2
  export * from "./shared/index.js";
3
3
  export * from "./types.js";
package/dist/types.d.ts CHANGED
@@ -27,6 +27,7 @@ export type * from "./bits/popover/types.js";
27
27
  export type * from "./bits/progress/types.js";
28
28
  export type * from "./bits/radio-group/types.js";
29
29
  export type * from "./bits/range-calendar/types.js";
30
+ export type * from "./bits/rating-group/types.js";
30
31
  export type * from "./bits/scroll-area/types.js";
31
32
  export type * from "./bits/select/types.js";
32
33
  export type * from "./bits/separator/types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",