INIT
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { RatingGroupHiddenInputState } from "../rating-group.svelte.js";
|
||||
import HiddenInput from "../../utilities/hidden-input.svelte";
|
||||
|
||||
const inputState = RatingGroupHiddenInputState.create();
|
||||
</script>
|
||||
|
||||
{#if inputState.shouldRender}
|
||||
<HiddenInput {...inputState.props} />
|
||||
{/if}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
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> {
|
||||
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
||||
$$bindings?: Bindings;
|
||||
} & Exports;
|
||||
(internal: unknown, props: {
|
||||
$$events?: Events;
|
||||
$$slots?: Slots;
|
||||
}): Exports & {
|
||||
$set?: any;
|
||||
$on?: any;
|
||||
};
|
||||
z_$$bindings?: Bindings;
|
||||
}
|
||||
declare const RatingGroupInput: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
||||
[evt: string]: CustomEvent<any>;
|
||||
}, {}, {}, string>;
|
||||
type RatingGroupInput = InstanceType<typeof RatingGroupInput>;
|
||||
export default RatingGroupInput;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { RatingGroupItemProps } from "../types.js";
|
||||
import { RatingGroupItemState } from "../rating-group.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
disabled = false,
|
||||
index,
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
...restProps
|
||||
}: RatingGroupItemProps = $props();
|
||||
|
||||
const itemState = RatingGroupItemState.create({
|
||||
disabled: boxWith(() => Boolean(disabled)),
|
||||
index: boxWith(() => index),
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, ...itemState.snippetProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.(itemState.snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { RatingGroupItemProps } from "../types.js";
|
||||
declare const RatingGroupItem: import("svelte").Component<RatingGroupItemProps, {}, "ref">;
|
||||
type RatingGroupItem = ReturnType<typeof RatingGroupItem>;
|
||||
export default RatingGroupItem;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { RatingGroupRootProps } from "../types.js";
|
||||
import { RatingGroupRootState } from "../rating-group.svelte.js";
|
||||
import RatingGroupInput from "./rating-group-input.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
disabled = false,
|
||||
children,
|
||||
child,
|
||||
value = $bindable(0),
|
||||
ref = $bindable(null),
|
||||
orientation = "horizontal",
|
||||
name = undefined,
|
||||
required = false,
|
||||
min = 0,
|
||||
max = 5,
|
||||
allowHalf = false,
|
||||
readonly = false,
|
||||
id = createId(uid),
|
||||
onValueChange = noop,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-valuetext": ariaValuetextProp,
|
||||
hoverPreview = true,
|
||||
...restProps
|
||||
}: RatingGroupRootProps = $props();
|
||||
|
||||
if (value < min || value > max) {
|
||||
value = Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
const ariaValuetext: NonNullable<RatingGroupRootProps["aria-valuetext"]> = $derived.by(() => {
|
||||
if (ariaValuetextProp) return ariaValuetextProp;
|
||||
return (value: number, max: number) => `${value} out of ${max}`;
|
||||
});
|
||||
|
||||
const rootState = RatingGroupRootState.create({
|
||||
orientation: boxWith(() => orientation),
|
||||
disabled: boxWith(() => disabled),
|
||||
name: boxWith(() => name),
|
||||
required: boxWith(() => required),
|
||||
min: boxWith(() => min),
|
||||
max: boxWith(() => max),
|
||||
allowHalf: boxWith(() => allowHalf),
|
||||
readonly: boxWith(() => readonly),
|
||||
id: boxWith(() => id),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
if (v === value) return;
|
||||
value = v;
|
||||
onValueChange?.(v);
|
||||
}
|
||||
),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
ariaValuetext: boxWith(() => ariaValuetext),
|
||||
hoverPreview: boxWith(() => hoverPreview),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, rootState.props, { "aria-label": ariaLabel })
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, ...rootState.snippetProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.(rootState.snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RatingGroupInput />
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { RatingGroupRootProps } from "../types.js";
|
||||
declare const RatingGroup: import("svelte").Component<RatingGroupRootProps, {}, "value" | "ref">;
|
||||
type RatingGroup = ReturnType<typeof RatingGroup>;
|
||||
export default RatingGroup;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { default as Root } from "./components/rating-group.svelte";
|
||||
export { default as Item } from "./components/rating-group-item.svelte";
|
||||
export type { RatingGroupRootProps as RootProps, RatingGroupItemProps as ItemProps, } from "./types.js";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { default as Root } from "./components/rating-group.svelte";
|
||||
export { default as Item } from "./components/rating-group-item.svelte";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as RatingGroup from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as RatingGroup from "./exports.js";
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { DOMContext, type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
|
||||
import type { BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
||||
import type { RatingGroupAriaValuetext, RatingGroupItemState as RatingGroupItemStateType } from "./types.js";
|
||||
import type { Orientation } from "../../shared/index.js";
|
||||
interface RatingGroupRootStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
required: boolean;
|
||||
orientation: Orientation;
|
||||
name: string | undefined;
|
||||
min: number;
|
||||
max: number;
|
||||
allowHalf: boolean;
|
||||
readonly: boolean;
|
||||
hoverPreview: boolean;
|
||||
ariaValuetext: NonNullable<RatingGroupAriaValuetext>;
|
||||
}>, WritableBoxedValues<{
|
||||
value: number;
|
||||
}> {
|
||||
}
|
||||
export declare class RatingGroupRootState {
|
||||
#private;
|
||||
static create(opts: RatingGroupRootStateOpts): RatingGroupRootState;
|
||||
readonly opts: RatingGroupRootStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
domContext: DOMContext;
|
||||
readonly hasValue: boolean;
|
||||
readonly valueToUse: number;
|
||||
readonly isRTL: boolean;
|
||||
readonly ariaValuetext: string;
|
||||
readonly items: {
|
||||
index: number;
|
||||
state: RatingGroupItemStateType;
|
||||
}[];
|
||||
constructor(opts: RatingGroupRootStateOpts);
|
||||
isActive(itemIndex: number): boolean;
|
||||
isPartial(itemIndex: number): boolean;
|
||||
setHoverValue(value: number | null): void;
|
||||
setValue(value: number): void;
|
||||
calculateRatingFromPointer(itemIndex: number, event: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
currentTarget: HTMLElement;
|
||||
}): number;
|
||||
onpointerleave(): void;
|
||||
readonly handlers: Record<string, () => void>;
|
||||
onkeydown(e: BitsKeyboardEvent): void;
|
||||
readonly snippetProps: {
|
||||
items: {
|
||||
index: number;
|
||||
state: RatingGroupItemStateType;
|
||||
}[];
|
||||
value: number;
|
||||
max: number;
|
||||
};
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "slider";
|
||||
readonly "aria-valuenow": number;
|
||||
readonly "aria-valuemin": number;
|
||||
readonly "aria-valuemax": number;
|
||||
readonly "aria-valuetext": string;
|
||||
readonly "aria-orientation": Orientation;
|
||||
readonly "aria-required": "true" | "false";
|
||||
readonly "aria-disabled": "true" | undefined;
|
||||
readonly "aria-label": "Rating";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-readonly": "" | undefined;
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly tabindex: 0 | -1;
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
readonly onpointerleave: () => void;
|
||||
};
|
||||
}
|
||||
interface RatingGroupItemStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
index: number;
|
||||
}> {
|
||||
}
|
||||
export declare class RatingGroupItemState {
|
||||
#private;
|
||||
static create(opts: RatingGroupItemStateOpts): RatingGroupItemState;
|
||||
readonly opts: RatingGroupItemStateOpts;
|
||||
readonly root: RatingGroupRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: RatingGroupItemStateOpts, root: RatingGroupRootState);
|
||||
onclick(e: BitsMouseEvent): void;
|
||||
onpointermove(e: BitsPointerEvent): void;
|
||||
readonly snippetProps: {
|
||||
readonly state: RatingGroupItemStateType;
|
||||
};
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "presentation";
|
||||
readonly "data-value": number;
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-readonly": "" | undefined;
|
||||
readonly "data-state": RatingGroupItemStateType;
|
||||
readonly onclick: (e: BitsMouseEvent) => void;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
};
|
||||
}
|
||||
export declare class RatingGroupHiddenInputState {
|
||||
static create(): RatingGroupHiddenInputState;
|
||||
readonly root: RatingGroupRootState;
|
||||
readonly shouldRender: boolean;
|
||||
readonly props: {
|
||||
readonly name: string | undefined;
|
||||
readonly value: number;
|
||||
readonly required: boolean;
|
||||
readonly disabled: boolean;
|
||||
};
|
||||
constructor(root: RatingGroupRootState);
|
||||
}
|
||||
export {};
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
import { attachRef, DOMContext, } from "svelte-toolbelt";
|
||||
import { Context } from "runed";
|
||||
import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
const ratingGroupAttrs = createBitsAttrs({
|
||||
component: "rating-group",
|
||||
parts: ["root", "item"],
|
||||
});
|
||||
const RatingGroupRootContext = new Context("RatingGroup.Root");
|
||||
export class RatingGroupRootState {
|
||||
static create(opts) {
|
||||
return RatingGroupRootContext.set(new RatingGroupRootState(opts));
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
#hoverValue = $state(null);
|
||||
#keySequence = $state("");
|
||||
#keySequenceTimeout = null;
|
||||
domContext;
|
||||
hasValue = $derived.by(() => this.opts.value.current > 0);
|
||||
valueToUse = $derived.by(() => this.#hoverValue ?? this.opts.value.current);
|
||||
isRTL = $derived.by(() => {
|
||||
const element = this.opts.ref.current;
|
||||
if (!element)
|
||||
return false;
|
||||
const style = getComputedStyle(element);
|
||||
return style.direction === "rtl";
|
||||
});
|
||||
ariaValuetext = $derived.by(() => {
|
||||
return typeof this.opts.ariaValuetext.current === "function"
|
||||
? this.opts.ariaValuetext.current(this.opts.value.current, this.opts.max.current)
|
||||
: this.opts.ariaValuetext.current;
|
||||
});
|
||||
items = $derived.by(() => {
|
||||
const value = this.valueToUse;
|
||||
return Array.from({ length: this.opts.max.current }, (_, i) => {
|
||||
const itemValue = i + 1;
|
||||
const halfValue = itemValue - 0.5;
|
||||
const state = value >= itemValue
|
||||
? "active"
|
||||
: this.opts.allowHalf.current && value >= halfValue
|
||||
? "partial"
|
||||
: "inactive";
|
||||
return { index: i, state };
|
||||
});
|
||||
});
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.onpointerleave = this.onpointerleave.bind(this);
|
||||
this.domContext = new DOMContext(this.opts.ref);
|
||||
}
|
||||
isActive(itemIndex) {
|
||||
return this.valueToUse >= itemIndex + 1;
|
||||
}
|
||||
isPartial(itemIndex) {
|
||||
if (!this.opts.allowHalf.current)
|
||||
return false;
|
||||
const itemValue = itemIndex + 1;
|
||||
return this.valueToUse >= itemValue - 0.5 && this.valueToUse < itemValue;
|
||||
}
|
||||
setHoverValue(value) {
|
||||
if (this.opts.readonly.current ||
|
||||
this.opts.disabled.current ||
|
||||
!this.opts.hoverPreview.current)
|
||||
return;
|
||||
this.#hoverValue =
|
||||
value === null
|
||||
? null
|
||||
: Math.max(this.opts.min.current, Math.min(this.opts.max.current, value));
|
||||
}
|
||||
setValue(value) {
|
||||
if (this.opts.readonly.current || this.opts.disabled.current)
|
||||
return;
|
||||
this.opts.value.current = Math.max(this.opts.min.current, Math.min(this.opts.max.current, value));
|
||||
}
|
||||
calculateRatingFromPointer(itemIndex, event) {
|
||||
const ratingValue = itemIndex + 1;
|
||||
if (!this.opts.allowHalf.current)
|
||||
return ratingValue;
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const style = getComputedStyle(event.currentTarget);
|
||||
const isHorizontal = this.opts.orientation.current === "horizontal";
|
||||
const position = isHorizontal
|
||||
? (event.clientX - rect.left) / rect.width
|
||||
: (event.clientY - rect.top) / rect.height;
|
||||
const normalizedPosition = style.direction === "rtl" ? 1 - position : position;
|
||||
return normalizedPosition < 0.5 ? ratingValue - 0.5 : ratingValue;
|
||||
}
|
||||
onpointerleave() {
|
||||
this.setHoverValue(null);
|
||||
}
|
||||
handlers = {
|
||||
[kbd.ARROW_UP]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.#adjustValue(this.opts.allowHalf.current ? 0.5 : 1);
|
||||
},
|
||||
[kbd.ARROW_RIGHT]: () => {
|
||||
this.setHoverValue(null);
|
||||
const increment = this.opts.allowHalf.current ? 0.5 : 1;
|
||||
// in RTL mode, right arrow should decrement
|
||||
this.#adjustValue(this.isRTL ? -increment : increment);
|
||||
},
|
||||
[kbd.ARROW_DOWN]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.#adjustValue(this.opts.allowHalf.current ? -0.5 : -1);
|
||||
},
|
||||
[kbd.ARROW_LEFT]: () => {
|
||||
this.setHoverValue(null);
|
||||
const increment = this.opts.allowHalf.current ? 0.5 : 1;
|
||||
// in RTL mode, left arrow should increment
|
||||
this.#adjustValue(this.isRTL ? increment : -increment);
|
||||
},
|
||||
[kbd.HOME]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.setValue(this.opts.min.current);
|
||||
},
|
||||
[kbd.END]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.setValue(this.opts.max.current);
|
||||
},
|
||||
[kbd.PAGE_UP]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.#adjustValue(1);
|
||||
},
|
||||
[kbd.PAGE_DOWN]: () => {
|
||||
this.setHoverValue(null);
|
||||
this.#adjustValue(-1);
|
||||
},
|
||||
};
|
||||
onkeydown(e) {
|
||||
if (this.opts.disabled.current || this.opts.readonly.current)
|
||||
return;
|
||||
if (this.handlers[e.key]) {
|
||||
e.preventDefault();
|
||||
this.#clearKeySequence();
|
||||
this.handlers[e.key]?.();
|
||||
return;
|
||||
}
|
||||
if (this.opts.allowHalf.current && this.#handleDecimalInput(e))
|
||||
return;
|
||||
// handle direct number input
|
||||
const num = parseInt(e.key || "");
|
||||
if (!isNaN(num) && e.key) {
|
||||
e.preventDefault();
|
||||
if (num >= this.opts.min.current && num <= this.opts.max.current) {
|
||||
this.setValue(num);
|
||||
if (this.opts.allowHalf.current) {
|
||||
this.#startDecimalListening(num);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.#clearKeySequence();
|
||||
}
|
||||
#adjustValue(delta) {
|
||||
this.setValue(this.opts.value.current + delta);
|
||||
}
|
||||
#handleDecimalInput(e) {
|
||||
if (!e.key)
|
||||
return false;
|
||||
if (e.key === ".") {
|
||||
e.preventDefault();
|
||||
this.#keySequence += e.key;
|
||||
return true;
|
||||
}
|
||||
if (e.key === "5" && this.#keySequence.match(/^\d+\.$/)) {
|
||||
e.preventDefault();
|
||||
this.#keySequence += e.key;
|
||||
const match = this.#keySequence.match(/^(\d+)\.5$/);
|
||||
if (match?.[1]) {
|
||||
const value = parseFloat(this.#keySequence);
|
||||
if (value >= this.opts.min.current && value <= this.opts.max.current) {
|
||||
this.setValue(value);
|
||||
this.#clearKeySequence();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#startDecimalListening(baseValue) {
|
||||
this.#keySequence = baseValue.toString();
|
||||
if (this.#keySequenceTimeout) {
|
||||
this.domContext.clearTimeout(this.#keySequenceTimeout);
|
||||
}
|
||||
this.#keySequenceTimeout = this.domContext.setTimeout(() => this.#clearKeySequence(), 1000);
|
||||
}
|
||||
#clearKeySequence() {
|
||||
this.#keySequence = "";
|
||||
if (this.#keySequenceTimeout) {
|
||||
this.domContext.clearTimeout(this.#keySequenceTimeout);
|
||||
this.#keySequenceTimeout = null;
|
||||
}
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
items: this.items,
|
||||
value: this.opts.value.current,
|
||||
max: this.opts.max.current,
|
||||
}));
|
||||
props = $derived.by(() => {
|
||||
return {
|
||||
id: this.opts.id.current,
|
||||
role: "slider",
|
||||
"aria-valuenow": this.opts.value.current,
|
||||
"aria-valuemin": this.opts.min.current,
|
||||
"aria-valuemax": this.opts.max.current,
|
||||
"aria-valuetext": this.ariaValuetext,
|
||||
"aria-orientation": this.opts.orientation.current,
|
||||
"aria-required": boolToStr(this.opts.required.current),
|
||||
"aria-disabled": this.opts.disabled.current ? "true" : undefined,
|
||||
"aria-label": "Rating",
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-readonly": this.opts.readonly.current ? "" : undefined,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
tabindex: this.opts.disabled.current ? -1 : 0,
|
||||
[ratingGroupAttrs.root]: "",
|
||||
onkeydown: this.onkeydown,
|
||||
onpointerleave: this.onpointerleave,
|
||||
...this.attachment,
|
||||
};
|
||||
});
|
||||
}
|
||||
export class RatingGroupItemState {
|
||||
static create(opts) {
|
||||
return new RatingGroupItemState(opts, RatingGroupRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
#isDisabled = $derived.by(() => this.opts.disabled.current || this.root.opts.disabled.current);
|
||||
#isActive = $derived.by(() => this.root.isActive(this.opts.index.current));
|
||||
#isPartial = $derived.by(() => this.root.isPartial(this.opts.index.current));
|
||||
#state = $derived.by(() => {
|
||||
if (this.#isActive)
|
||||
return "active";
|
||||
if (this.#isPartial)
|
||||
return "partial";
|
||||
return "inactive";
|
||||
});
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.onpointermove = this.onpointermove.bind(this);
|
||||
}
|
||||
onclick(e) {
|
||||
if (this.#isDisabled || this.root.opts.readonly.current)
|
||||
return;
|
||||
// handle clearing when clicking on first item (index 0) that's already
|
||||
// active and min is 0
|
||||
if (this.opts.index.current === 0 &&
|
||||
this.root.opts.min.current === 0 &&
|
||||
this.root.opts.value.current > 0) {
|
||||
const newValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
|
||||
const currentValue = this.root.opts.value.current;
|
||||
// only clear if the calculated rating exactly matches current value
|
||||
if (newValue === currentValue) {
|
||||
this.root.setValue(0);
|
||||
if (this.root.opts.ref.current) {
|
||||
this.root.opts.ref.current.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const newValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
|
||||
this.root.setValue(newValue);
|
||||
if (this.root.opts.ref.current) {
|
||||
this.root.opts.ref.current.focus();
|
||||
}
|
||||
}
|
||||
onpointermove(e) {
|
||||
if (this.#isDisabled ||
|
||||
this.root.opts.readonly.current ||
|
||||
!this.root.opts.hoverPreview.current)
|
||||
return;
|
||||
// skip hover preview for touch devices
|
||||
if (e.pointerType === "touch")
|
||||
return;
|
||||
const hoverValue = this.root.calculateRatingFromPointer(this.opts.index.current, e);
|
||||
this.root.setHoverValue(hoverValue);
|
||||
}
|
||||
snippetProps = $derived.by(() => {
|
||||
return {
|
||||
state: this.#state,
|
||||
};
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "presentation",
|
||||
"data-value": this.opts.index.current + 1,
|
||||
"data-orientation": this.root.opts.orientation.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.#isDisabled),
|
||||
"data-readonly": this.root.opts.readonly.current ? "" : undefined,
|
||||
"data-state": this.#state,
|
||||
[ratingGroupAttrs.item]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onpointermove: this.onpointermove,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class RatingGroupHiddenInputState {
|
||||
static create() {
|
||||
return new RatingGroupHiddenInputState(RatingGroupRootContext.get());
|
||||
}
|
||||
root;
|
||||
shouldRender = $derived.by(() => this.root.opts.name.current !== undefined);
|
||||
props = $derived.by(() => ({
|
||||
name: this.root.opts.name.current,
|
||||
value: this.root.opts.value.current,
|
||||
required: this.root.opts.required.current,
|
||||
disabled: this.root.opts.disabled.current,
|
||||
}));
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
|
||||
import type { Orientation } from "../../index.js";
|
||||
import type { BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
|
||||
export type RatingGroupItemState = "active" | "partial" | "inactive";
|
||||
export type RatingGroupItemData = {
|
||||
index: number;
|
||||
state: RatingGroupItemState;
|
||||
};
|
||||
export type RatingGroupRootSnippetProps = {
|
||||
items: RatingGroupItemData[];
|
||||
value: number;
|
||||
max: number;
|
||||
};
|
||||
export type RatingGroupAriaValuetext = BitsPrimitiveDivAttributes["aria-valuetext"] | ((value: number, max: number) => string);
|
||||
export type RatingGroupRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The orientation of the rating group. Used to determine
|
||||
* how keyboard interactions work.
|
||||
*
|
||||
* @default "horizontal"
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
/**
|
||||
* The current rating value.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
value?: number;
|
||||
/**
|
||||
* The callback to call when the rating value changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<number>;
|
||||
/**
|
||||
* The name to apply to the rating group's input element for
|
||||
* form submission. If not provided, a hidden input will not
|
||||
* be rendered and the rating group will not be part of a form.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Whether the rating group is disabled.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Whether the rating group is required for form submission.
|
||||
* If `true`, ensure you provide a `name` prop so the hidden
|
||||
* input is rendered.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* The minimum rating value.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* The maximum rating value (number of items).
|
||||
*
|
||||
* @default 5
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* Whether to allow half-star ratings.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
allowHalf?: boolean;
|
||||
/**
|
||||
* Whether the rating group is readonly.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
readonly?: boolean;
|
||||
/**
|
||||
* Whether to show a preview when hovering over rating items.
|
||||
* Touch events are ignored to prevent accidental previews.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
hoverPreview?: boolean;
|
||||
/**
|
||||
* An extended `aria-valuetext` property to use for the rating group.
|
||||
* Can either be a string, or a function that receives the current value
|
||||
* and max value and returns a string.
|
||||
*
|
||||
* @default ((value: number, max: number) => `${value} out of ${max}`)
|
||||
*/
|
||||
"aria-valuetext"?: RatingGroupAriaValuetext;
|
||||
}, RatingGroupRootSnippetProps>;
|
||||
export type RatingGroupRootProps = RatingGroupRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, RatingGroupRootPropsWithoutHTML>;
|
||||
export type RatingGroupItemSnippetProps = {
|
||||
state: RatingGroupItemState;
|
||||
};
|
||||
export type RatingGroupItemPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The index of the rating item (0-based index).
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* Whether the rating item is disabled.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean | null | undefined;
|
||||
}, RatingGroupItemSnippetProps>;
|
||||
export type RatingGroupItemProps = RatingGroupItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, RatingGroupItemPropsWithoutHTML>;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user