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.
- package/dist/bits/index.d.ts +1 -0
- package/dist/bits/index.js +1 -0
- package/dist/bits/rating-group/components/rating-group-input.svelte +10 -0
- package/dist/bits/rating-group/components/rating-group-input.svelte.d.ts +18 -0
- package/dist/bits/rating-group/components/rating-group-item.svelte +38 -0
- package/dist/bits/rating-group/components/rating-group-item.svelte.d.ts +4 -0
- package/dist/bits/rating-group/components/rating-group.svelte +80 -0
- package/dist/bits/rating-group/components/rating-group.svelte.d.ts +4 -0
- package/dist/bits/rating-group/exports.d.ts +3 -0
- package/dist/bits/rating-group/exports.js +2 -0
- package/dist/bits/rating-group/index.d.ts +1 -0
- package/dist/bits/rating-group/index.js +1 -0
- package/dist/bits/rating-group/rating-group.svelte.d.ts +114 -0
- package/dist/bits/rating-group/rating-group.svelte.js +295 -0
- package/dist/bits/rating-group/types.d.ts +111 -0
- package/dist/bits/rating-group/types.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/bits/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/bits/index.js
CHANGED
|
@@ -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,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 @@
|
|
|
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";
|