INIT
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m11s

This commit is contained in:
eewing
2026-02-18 15:17:47 -06:00
parent e74c106f85
commit ec317eb17c
11532 changed files with 1631690 additions and 1 deletions
@@ -0,0 +1,43 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { DateRangeFieldInputProps } from "../types.js";
import { DateRangeFieldInputState } from "../date-range-field.svelte.js";
import { createId } from "../../../internal/create-id.js";
import DateFieldHiddenInput from "../../date-field/components/date-field-hidden-input.svelte";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
name = "",
child,
children,
type,
...restProps
}: DateRangeFieldInputProps = $props();
const inputState = DateRangeFieldInputState.create(
{
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
name: boxWith(() => name),
},
type
);
const mergedProps = $derived(mergeProps(restProps, inputState.props, { role: "presentation" }));
</script>
{#if child}
{@render child({ props: mergedProps, segments: inputState.root.segmentContents })}
{:else}
<div {...mergedProps}>
{@render children?.({ segments: inputState.root.segmentContents })}
</div>
{/if}
<DateFieldHiddenInput />
@@ -0,0 +1,4 @@
import type { DateRangeFieldInputProps } from "../types.js";
declare const DateRangeFieldInput: import("svelte").Component<DateRangeFieldInputProps, {}, "ref">;
type DateRangeFieldInput = ReturnType<typeof DateRangeFieldInput>;
export default DateRangeFieldInput;
@@ -0,0 +1,34 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import { DateRangeFieldLabelState } from "../date-range-field.svelte.js";
import type { DateRangeFieldLabelProps } from "../types.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
children,
child,
...restProps
}: DateRangeFieldLabelProps = $props();
const labelState = DateRangeFieldLabelState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(mergeProps(restProps, labelState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<span {...mergedProps}>
{@render children?.()}
</span>
{/if}
@@ -0,0 +1,4 @@
import type { DateRangeFieldLabelProps } from "../types.js";
declare const DateRangeFieldLabel: import("svelte").Component<DateRangeFieldLabelProps, {}, "ref">;
type DateRangeFieldLabel = ReturnType<typeof DateRangeFieldLabel>;
export default DateRangeFieldLabel;
@@ -0,0 +1,145 @@
<script lang="ts">
import { watch } from "runed";
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { DateValue } from "@internationalized/date";
import { DateRangeFieldRootState } from "../date-range-field.svelte.js";
import type { DateRangeFieldRootProps } from "../types.js";
import { createId } from "../../../internal/create-id.js";
import { noop } from "../../../internal/noop.js";
import type { DateRange } from "../../../shared/index.js";
import { getDefaultDate } from "../../../internal/date-time/utils.js";
import { resolveLocaleProp } from "../../utilities/config/prop-resolvers.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
value = $bindable(),
onValueChange = noop,
placeholder = $bindable(),
onPlaceholderChange = noop,
disabled = false,
readonly = false,
required = false,
hourCycle,
granularity,
locale,
hideTimeZone = false,
validate = noop,
onInvalid = noop,
maxValue,
minValue,
readonlySegments = [],
children,
child,
onStartValueChange = noop,
onEndValueChange = noop,
errorMessageId,
...restProps
}: DateRangeFieldRootProps = $props();
let startValue = $state<DateValue | undefined>(value?.start);
let endValue = $state<DateValue | undefined>(value?.end);
function handleDefaultPlaceholder() {
if (placeholder !== undefined) return;
const defaultPlaceholder = getDefaultDate({
granularity,
defaultValue: value?.start,
minValue,
maxValue,
});
placeholder = defaultPlaceholder;
}
// SSR
handleDefaultPlaceholder();
watch.pre(
() => placeholder,
() => {
handleDefaultPlaceholder();
}
);
function handleDefaultValue() {
if (value !== undefined) return;
const defaultValue = { start: undefined, end: undefined };
value = defaultValue;
}
// SSR
handleDefaultValue();
/**
* Covers an edge case where when a spread props object is reassigned,
* the props are reset to their default values, which would make value
* undefined which causes errors to be thrown.
*/
watch.pre(
() => value,
() => {
handleDefaultValue();
}
);
const rootState = DateRangeFieldRootState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
disabled: boxWith(() => disabled),
readonly: boxWith(() => readonly),
required: boxWith(() => required),
hourCycle: boxWith(() => hourCycle),
granularity: boxWith(() => granularity),
locale: resolveLocaleProp(() => locale),
hideTimeZone: boxWith(() => hideTimeZone),
validate: boxWith(() => validate),
maxValue: boxWith(() => maxValue),
minValue: boxWith(() => minValue),
placeholder: boxWith(
() => placeholder as DateValue,
(v) => {
placeholder = v;
onPlaceholderChange(v);
}
),
readonlySegments: boxWith(() => readonlySegments),
value: boxWith(
() => value as DateRange,
(v) => {
value = v;
onValueChange(v);
}
),
startValue: boxWith(
() => startValue,
(v) => {
startValue = v;
onStartValueChange(v);
}
),
endValue: boxWith(
() => endValue,
(v) => {
endValue = v;
onEndValueChange(v);
}
),
onInvalid: boxWith(() => onInvalid),
errorMessageId: boxWith(() => errorMessageId),
});
const mergedProps = $derived(mergeProps(restProps, rootState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
@@ -0,0 +1,4 @@
import type { DateRangeFieldRootProps } from "../types.js";
declare const DateRangeField: import("svelte").Component<DateRangeFieldRootProps, {}, "value" | "placeholder" | "ref">;
type DateRangeField = ReturnType<typeof DateRangeField>;
export default DateRangeField;
@@ -0,0 +1,91 @@
import type { DateValue } from "@internationalized/date";
import { DOMContext, type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
import { Context } from "runed";
import { DateFieldInputState, DateFieldRootState } from "../date-field/date-field.svelte.js";
import type { DateOnInvalid, DateRange, DateRangeValidator, SegmentPart } from "../../shared/index.js";
import type { RefAttachment, WithRefOpts } from "../../internal/types.js";
import type { Granularity } from "../../shared/date/types.js";
import { type Formatter } from "../../internal/date-time/formatter.js";
export declare const dateRangeFieldAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["root", "label"]>;
export declare const DateRangeFieldRootContext: Context<DateRangeFieldRootState>;
interface DateRangeFieldRootStateOpts extends WithRefOpts, WritableBoxedValues<{
value: DateRange;
placeholder: DateValue;
startValue: DateValue | undefined;
endValue: DateValue | undefined;
}>, ReadableBoxedValues<{
readonlySegments: SegmentPart[];
validate: DateRangeValidator | undefined;
onInvalid: DateOnInvalid | undefined;
minValue: DateValue | undefined;
maxValue: DateValue | undefined;
disabled: boolean;
readonly: boolean;
granularity: Granularity | undefined;
hourCycle: 12 | 24 | undefined;
locale: string;
hideTimeZone: boolean;
required: boolean;
errorMessageId: string | undefined;
}> {
}
export declare class DateRangeFieldRootState {
#private;
static create(opts: DateRangeFieldRootStateOpts): DateRangeFieldRootState;
readonly opts: DateRangeFieldRootStateOpts;
startFieldState: DateFieldRootState | undefined;
endFieldState: DateFieldRootState | undefined;
descriptionId: string;
formatter: Formatter;
fieldNode: HTMLElement | null;
labelNode: HTMLElement | null;
descriptionNode: HTMLElement | null;
readonly startValueComplete: boolean;
readonly endValueComplete: boolean;
readonly rangeComplete: boolean;
domContext: DOMContext;
readonly attachment: RefAttachment;
constructor(opts: DateRangeFieldRootStateOpts);
readonly validationStatus: false | {
readonly reason: "custom";
readonly message: string | string[];
} | {
readonly reason: "min";
readonly message?: undefined;
} | {
readonly reason: "max";
readonly message?: undefined;
};
readonly isInvalid: boolean;
readonly props: {
readonly id: string;
readonly role: "group";
readonly "data-invalid": "" | undefined;
};
}
interface DateRangeFieldLabelStateOpts extends WithRefOpts {
}
export declare class DateRangeFieldLabelState {
#private;
static create(opts: DateRangeFieldLabelStateOpts): DateRangeFieldLabelState;
readonly opts: DateRangeFieldLabelStateOpts;
readonly root: DateRangeFieldRootState;
readonly attachment: RefAttachment;
constructor(opts: DateRangeFieldLabelStateOpts, root: DateRangeFieldRootState);
readonly props: {
readonly id: string;
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly onclick: () => void;
};
}
interface DateRangeFieldInputStateOpts extends WithRefOpts, WritableBoxedValues<{
value: DateValue | undefined;
}>, ReadableBoxedValues<{
name: string;
}> {
}
export declare class DateRangeFieldInputState {
static create(opts: Omit<DateRangeFieldInputStateOpts, "value">, type: "start" | "end"): DateFieldInputState;
}
export {};
@@ -0,0 +1,202 @@
import { boxWith, onDestroyEffect, attachRef, DOMContext, } from "svelte-toolbelt";
import { Context, watch } from "runed";
import { DateFieldInputState, DateFieldRootState } from "../date-field/date-field.svelte.js";
import { useId } from "../../internal/use-id.js";
import { createBitsAttrs, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
import { createFormatter } from "../../internal/date-time/formatter.js";
import { removeDescriptionElement } from "../../internal/date-time/field/helpers.js";
import { isBefore } from "../../internal/date-time/utils.js";
import { getFirstSegment } from "../../internal/date-time/field/segments.js";
export const dateRangeFieldAttrs = createBitsAttrs({
component: "date-range-field",
parts: ["root", "label"],
});
export const DateRangeFieldRootContext = new Context("DateRangeField.Root");
export class DateRangeFieldRootState {
static create(opts) {
return DateRangeFieldRootContext.set(new DateRangeFieldRootState(opts));
}
opts;
startFieldState = undefined;
endFieldState = undefined;
descriptionId = useId();
formatter;
fieldNode = $state(null);
labelNode = $state(null);
descriptionNode = $state(null);
startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
domContext;
attachment;
constructor(opts) {
this.opts = opts;
this.formatter = createFormatter({
initialLocale: this.opts.locale.current,
monthFormat: boxWith(() => "long"),
yearFormat: boxWith(() => "numeric"),
});
this.domContext = new DOMContext(this.opts.ref);
this.attachment = attachRef(this.opts.ref, (v) => (this.fieldNode = v));
onDestroyEffect(() => {
removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
});
$effect(() => {
if (this.formatter.getLocale() === this.opts.locale.current)
return;
this.formatter.setLocale(this.opts.locale.current);
});
/**
* Synchronize the start and end values with the `value` in case
* it is updated externally.
*/
watch(() => this.opts.value.current, (value) => {
if (value.start && value.end) {
this.opts.startValue.current = value.start;
this.opts.endValue.current = value.end;
}
else if (value.start) {
this.opts.startValue.current = value.start;
this.opts.endValue.current = undefined;
}
else if (value.start === undefined && value.end === undefined) {
this.opts.startValue.current = undefined;
this.opts.endValue.current = undefined;
}
});
/**
* Synchronize the placeholder value with the current start value
*/
watch(() => this.opts.value.current, (value) => {
const startValue = value.start;
if (startValue && this.opts.placeholder.current !== startValue) {
this.opts.placeholder.current = startValue;
}
});
watch([() => this.opts.startValue.current, () => this.opts.endValue.current], ([startValue, endValue]) => {
if (this.opts.value.current &&
this.opts.value.current.start === startValue &&
this.opts.value.current.end === endValue) {
return;
}
if (startValue && endValue) {
this.#updateValue((prev) => {
if (prev.start === startValue && prev.end === endValue) {
return prev;
}
return {
start: startValue,
end: endValue,
};
});
}
else if (this.opts.value.current &&
this.opts.value.current.start &&
this.opts.value.current.end) {
this.opts.value.current.start = undefined;
this.opts.value.current.end = undefined;
}
});
}
validationStatus = $derived.by(() => {
const value = this.opts.value.current;
if (value === undefined)
return false;
if (value.start === undefined || value.end === undefined)
return false;
const msg = this.opts.validate.current?.({
start: value.start,
end: value.end,
});
if (msg) {
return {
reason: "custom",
message: msg,
};
}
const minValue = this.opts.minValue.current;
if (minValue && value.start && isBefore(value.start, minValue)) {
return {
reason: "min",
};
}
const maxValue = this.opts.maxValue.current;
if ((maxValue && value.end && isBefore(maxValue, value.end)) ||
(maxValue && value.start && isBefore(maxValue, value.start))) {
return {
reason: "max",
};
}
return false;
});
isInvalid = $derived.by(() => {
if (this.validationStatus === false)
return false;
return true;
});
#updateValue(cb) {
const value = this.opts.value.current;
const newValue = cb(value);
this.opts.value.current = newValue;
}
props = $derived.by(() => ({
id: this.opts.id.current,
role: "group",
[dateRangeFieldAttrs.root]: "",
"data-invalid": boolToEmptyStrOrUndef(this.isInvalid),
...this.attachment,
}));
}
export class DateRangeFieldLabelState {
static create(opts) {
return new DateRangeFieldLabelState(opts, DateRangeFieldRootContext.get());
}
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(this.opts.ref, (v) => (this.root.labelNode = v));
}
#onclick = () => {
if (this.root.opts.disabled.current)
return;
const firstSegment = getFirstSegment(this.root.fieldNode);
if (!firstSegment)
return;
firstSegment.focus();
};
props = $derived.by(() => ({
id: this.opts.id.current,
"data-invalid": boolToEmptyStrOrUndef(this.root.isInvalid),
"data-disabled": boolToEmptyStrOrUndef(this.root.opts.disabled.current),
[dateRangeFieldAttrs.label]: "",
onclick: this.#onclick,
...this.attachment,
}));
}
export class DateRangeFieldInputState {
static create(opts, type) {
const root = DateRangeFieldRootContext.get();
const fieldState = DateFieldRootState.create({
value: type === "start" ? root.opts.startValue : root.opts.endValue,
disabled: root.opts.disabled,
readonly: root.opts.readonly,
readonlySegments: root.opts.readonlySegments,
validate: boxWith(() => undefined),
minValue: root.opts.minValue,
maxValue: root.opts.maxValue,
hourCycle: root.opts.hourCycle,
locale: root.opts.locale,
hideTimeZone: root.opts.hideTimeZone,
required: root.opts.required,
granularity: root.opts.granularity,
placeholder: root.opts.placeholder,
onInvalid: root.opts.onInvalid,
errorMessageId: root.opts.errorMessageId,
isInvalidProp: boxWith(() => root.isInvalid),
}, root);
return new DateFieldInputState({ name: opts.name, id: opts.id, ref: opts.ref }, fieldState);
}
}
+5
View File
@@ -0,0 +1,5 @@
export { default as Root } from "./components/date-range-field.svelte";
export { default as Input } from "./components/date-range-field-input.svelte";
export { default as Label } from "./components/date-range-field-label.svelte";
export { default as Segment } from "../date-field/components/date-field-segment.svelte";
export type { DateRangeFieldRootProps as RootProps, DateRangeFieldLabelProps as LabelProps, DateRangeFieldInputProps as InputProps, DateRangeFieldSegmentProps as SegmentProps, } from "./types.js";
+4
View File
@@ -0,0 +1,4 @@
export { default as Root } from "./components/date-range-field.svelte";
export { default as Input } from "./components/date-range-field-input.svelte";
export { default as Label } from "./components/date-range-field-label.svelte";
export { default as Segment } from "../date-field/components/date-field-segment.svelte";
+1
View File
@@ -0,0 +1 @@
export * as DateRangeField from "./exports.js";
+1
View File
@@ -0,0 +1 @@
export * as DateRangeField from "./exports.js";
+154
View File
@@ -0,0 +1,154 @@
import type { DateValue } from "@internationalized/date";
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
import type { BitsPrimitiveDivAttributes, BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
import type { DateOnInvalid, DateRange, DateRangeValidator, EditableSegmentPart, SegmentPart } from "../../shared/index.js";
import type { DateFieldSegmentProps, DateFieldSegmentPropsWithoutHTML } from "../../types.js";
import type { Granularity } from "../../shared/date/types.js";
export type DateRangeFieldRootPropsWithoutHTML = WithChild<{
/**
* The value of the date range field.
*
* @bindable
*/
value?: DateRange;
/**
* A callback that is called when the value of the date range field changes.
*/
onValueChange?: OnChangeFn<DateRange | undefined>;
/**
* The placeholder value of the date field. This determines the format
* and what date the field starts at when it is empty.
*
* @bindable
*/
placeholder?: DateValue;
/**
* A callback that is called when the date field's placeholder value changes.
*/
onPlaceholderChange?: OnChangeFn<DateValue | undefined>;
/**
* A function that returns a string or array of strings as validation errors if the date is
* invalid, or nothing if the date is valid
*/
validate?: DateRangeValidator;
/**
* A callback fired when the date field's value is invalid. Use this to display an error
* message to the user.
*/
onInvalid?: DateOnInvalid;
/**
* The minimum acceptable date. When provided, the date field
* will be marked as invalid if the user enters a date before this date.
*/
minValue?: DateValue;
/**
* The maximum acceptable date. When provided, the date field
* will be marked as invalid if the user enters a date after this date.
*/
maxValue?: DateValue;
/**
* If true, the date field will be disabled and users will not be able
* to interact with it. This also disables the hidden input element if
* the date field is used in a form.
*
* @defaultValue false
*/
disabled?: boolean;
/**
* If true, the date field will be readonly, and users will not be able to
* edit the values of any of the individual segments.
*
* @defaultValue false
*/
readonly?: boolean;
/**
* If true, the date field will be required, which is useful when used within
* a form. If the date field is empty when the form is submitted, the form
* will not be valid.
*
* @defaultValue false
*/
required?: boolean;
/**
* An array of segment names that should be readonly. If provided, only the
* segments not in this array will be editable.
*/
readonlySegments?: EditableSegmentPart[];
/**
* The format to use for displaying the time in the input.
* If using a 12 hour clock, ensure you also include the `dayPeriod`
* segment in your input to ensure the user can select AM/PM.
*
* @defaultValue the locale's default time format
*/
hourCycle?: 12 | 24;
/**
* The locale to use for formatting the date field.
*
* @defaultValue 'en'
*/
locale?: string;
/**
* The granularity of the date field. This determines which
* segments will be includes in the segments array used to
* build the date field.
*
* By default, when a `CalendarDate` value is used, the granularity
* will default to `'day'`, and when a `CalendarDateTime` or `ZonedDateTime`
* value is used, the granularity will default to `'minute'`.
*
* Granularity is only used for visual purposes, and does not impact
* the value of the date field. You can have the same value synced
* between multiple date fields with different granularities and they
* will all contain the same value.
*
* @defaultValue 'day'
*/
granularity?: Granularity;
/**
* Whether or not to hide the timeZoneName segment from the date field.
*
* @defaultValue false;
*/
hideTimeZone?: boolean;
/**
* A callback function called when the start value changes. This doesn't necessarily mean
* the `value` has updated and should be used to apply cosmetic changes to the calendar when
* only part of the value is changed/completed.
*/
onStartValueChange?: OnChangeFn<DateValue | undefined>;
/**
* A callback function called when the end value changes. This doesn't necessarily mean
* the `value` has updated and should be used to apply cosmetic changes to the calendar when
* only part of the value is changed/completed.
*/
onEndValueChange?: OnChangeFn<DateValue | undefined>;
/**
* The `id` of the element which contains the error messages for the date field when the
* date is invalid.
*/
errorMessageId?: string;
}>;
export type DateRangeFieldRootProps = DateRangeFieldRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, DateRangeFieldRootPropsWithoutHTML>;
export type DateRangeFieldLabelPropsWithoutHTML = WithChild;
export type DateRangeFieldLabelProps = DateRangeFieldLabelPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, DateRangeFieldLabelPropsWithoutHTML>;
export type DateRangeFieldInputSnippetProps = {
segments: Array<{
part: SegmentPart;
value: string;
}>;
};
export type DateRangeFieldInputPropsWithoutHTML = WithChild<{
/**
* The name to use for the hidden input element associated with this input
* used for form submission.
*/
name?: string;
/**
* Whether this input represents the start or end of the date range.
*/
type: "start" | "end";
}, DateRangeFieldInputSnippetProps>;
export type DateRangeFieldInputProps = DateRangeFieldInputPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, DateRangeFieldInputPropsWithoutHTML>;
export type DateRangeFieldSegmentPropsWithoutHTML = DateFieldSegmentPropsWithoutHTML;
export type DateRangeFieldSegmentProps = DateFieldSegmentProps;
+1
View File
@@ -0,0 +1 @@
export {};