parent
e192a2f815
commit
cf186d3712
@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { watch } from "runed";
|
||||
import { boxWith, mergeProps } from "../../utilities/index";
|
||||
import type { DateValue } from "$lib/glob/lib";
|
||||
import { DateRangeFieldRootState } from "../date-range-field.svelte.ts";
|
||||
import type { DateRangeFieldRootProps } from "../types.js";
|
||||
import { createId, noop } from "../../utilities/index";
|
||||
import type { DateRange } from "$lib/glob/lib";
|
||||
import {
|
||||
DEFAULT_DATI_DATE_ORDER,
|
||||
DEFAULT_DATI_TIME_FORMAT,
|
||||
datiTimeFormatToHourCycle,
|
||||
getDefaultDate,
|
||||
} from "$lib/glob/lib";
|
||||
import { resolveLocaleProp } from "../../comn/config/prop-resolvers.ts";
|
||||
|
||||
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,
|
||||
timeFormat,
|
||||
granularity,
|
||||
locale,
|
||||
dateOrder,
|
||||
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(() => timeFormat ? datiTimeFormatToHourCycle(timeFormat) : (hourCycle ?? datiTimeFormatToHourCycle(DEFAULT_DATI_TIME_FORMAT))),
|
||||
granularity: boxWith(() => granularity),
|
||||
locale: resolveLocaleProp(() => locale),
|
||||
dateOrder: boxWith(() => dateOrder ?? DEFAULT_DATI_DATE_ORDER),
|
||||
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,309 @@
|
||||
import { Context, watch } from "runed";
|
||||
import type { DateValue, DatiDateOrder } from "$lib/glob/lib";
|
||||
|
||||
import type {
|
||||
ReadableBoxedValues,
|
||||
WritableBoxedValues,
|
||||
RefAttachment,
|
||||
WithRefOpts,
|
||||
} from "../utilities";
|
||||
|
||||
import {
|
||||
boxWith,
|
||||
onDestroyEffect,
|
||||
attachRef,
|
||||
DOMContext,
|
||||
useId,
|
||||
createAcelAttrs,
|
||||
boolToEmptyStrOrUndef
|
||||
} from "../utilities";
|
||||
|
||||
|
||||
import { DateFieldInputState, DateFieldRootState } from "../date-field/date-field.svelte.ts";
|
||||
|
||||
import type {
|
||||
DateOnInvalid,
|
||||
DateRange,
|
||||
DateRangeValidator,
|
||||
Granularity,
|
||||
} from "$lib/glob/lib";
|
||||
import { isBefore } from "$lib/glob/lib";
|
||||
import type { SegmentPart } from "../utilities/datetime/types.ts";
|
||||
import { type Formatter, createFormatter } from "../utilities/datetime/formatter.js";
|
||||
import { removeDescriptionElement } from "../utilities/datetime/field/helpers.js";
|
||||
import { getFirstSegment } from "../utilities/datetime/field/segments.ts";
|
||||
|
||||
export const dateRangeFieldAttrs = createAcelAttrs({
|
||||
component: "date-range-field",
|
||||
parts: ["root", "label"],
|
||||
});
|
||||
|
||||
export const DateRangeFieldRootContext = new Context<DateRangeFieldRootState>(
|
||||
"DateRangeField.Root"
|
||||
);
|
||||
|
||||
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;
|
||||
dateOrder: DatiDateOrder;
|
||||
hideTimeZone: boolean;
|
||||
required: boolean;
|
||||
errorMessageId: string | undefined;
|
||||
}> {}
|
||||
|
||||
export class DateRangeFieldRootState {
|
||||
static create(opts: DateRangeFieldRootStateOpts) {
|
||||
return DateRangeFieldRootContext.set(new DateRangeFieldRootState(opts));
|
||||
}
|
||||
|
||||
readonly opts: DateRangeFieldRootStateOpts;
|
||||
startFieldState: DateFieldRootState | undefined = undefined;
|
||||
endFieldState: DateFieldRootState | undefined = undefined;
|
||||
descriptionId = useId();
|
||||
formatter: Formatter;
|
||||
fieldNode = $state<HTMLElement | null>(null);
|
||||
labelNode = $state<HTMLElement | null>(null);
|
||||
descriptionNode = $state<HTMLElement | null>(null);
|
||||
readonly startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
|
||||
readonly endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
|
||||
readonly rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
|
||||
domContext: DOMContext;
|
||||
readonly attachment: RefAttachment;
|
||||
|
||||
constructor(opts: DateRangeFieldRootStateOpts) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
readonly validationStatus = $derived.by(() => {
|
||||
const value = this.opts.value.current;
|
||||
if (value === undefined) return false as const;
|
||||
if (value.start === undefined || value.end === undefined) return false as const;
|
||||
|
||||
const msg = this.opts.validate.current?.({
|
||||
start: value.start,
|
||||
end: value.end,
|
||||
});
|
||||
|
||||
if (msg) {
|
||||
return {
|
||||
reason: "custom",
|
||||
message: msg,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const minValue = this.opts.minValue.current;
|
||||
if (minValue && value.start && isBefore(value.start, minValue)) {
|
||||
return {
|
||||
reason: "min",
|
||||
} as const;
|
||||
}
|
||||
|
||||
const maxValue = this.opts.maxValue.current;
|
||||
|
||||
if (
|
||||
(maxValue && value.end && isBefore(maxValue, value.end)) ||
|
||||
(maxValue && value.start && isBefore(maxValue, value.start))
|
||||
) {
|
||||
return {
|
||||
reason: "max",
|
||||
} as const;
|
||||
}
|
||||
|
||||
return false as const;
|
||||
});
|
||||
|
||||
readonly isInvalid = $derived.by(() => {
|
||||
if (this.validationStatus === false) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
#updateValue(cb: (value: DateRange) => DateRange) {
|
||||
const value = this.opts.value.current;
|
||||
const newValue = cb(value);
|
||||
this.opts.value.current = newValue;
|
||||
}
|
||||
|
||||
readonly props = $derived.by(
|
||||
() =>
|
||||
({
|
||||
id: this.opts.id.current,
|
||||
role: "group",
|
||||
[dateRangeFieldAttrs.root]: "",
|
||||
"data-invalid": boolToEmptyStrOrUndef(this.isInvalid),
|
||||
...this.attachment,
|
||||
}) as const
|
||||
);
|
||||
}
|
||||
|
||||
interface DateRangeFieldLabelStateOpts extends WithRefOpts {}
|
||||
|
||||
export class DateRangeFieldLabelState {
|
||||
static create(opts: DateRangeFieldLabelStateOpts) {
|
||||
return new DateRangeFieldLabelState(opts, DateRangeFieldRootContext.get());
|
||||
}
|
||||
|
||||
readonly opts: DateRangeFieldLabelStateOpts;
|
||||
readonly root: DateRangeFieldRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
|
||||
constructor(opts: DateRangeFieldLabelStateOpts, root: DateRangeFieldRootState) {
|
||||
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();
|
||||
};
|
||||
|
||||
readonly 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,
|
||||
}) as const
|
||||
);
|
||||
}
|
||||
|
||||
interface DateRangeFieldInputStateOpts
|
||||
extends WithRefOpts,
|
||||
WritableBoxedValues<{
|
||||
value: DateValue | undefined;
|
||||
}>,
|
||||
ReadableBoxedValues<{
|
||||
name: string;
|
||||
}> {}
|
||||
|
||||
export class DateRangeFieldInputState {
|
||||
static create(opts: Omit<DateRangeFieldInputStateOpts, "value">, type: "start" | "end") {
|
||||
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,
|
||||
dateOrder: root.opts.dateOrder,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,213 @@
|
||||
import type { DateValue } from "$lib/glob/lib";
|
||||
import type { OnChangeFn, WithChild, Without } from "../utilities";
|
||||
import type {
|
||||
AcelPrimitiveDivAttributes,
|
||||
AcelPrimitiveSpanAttributes,
|
||||
} from "../utilities";
|
||||
import type {
|
||||
DateOnInvalid,
|
||||
DateRange,
|
||||
DateRangeValidator,
|
||||
Granularity,
|
||||
DatiDateOrder,
|
||||
DatiTimeFormat,
|
||||
} from "$lib/glob/lib";
|
||||
import type { EditableSegmentPart, SegmentPart } from "../utilities/datetime/types.ts";
|
||||
import type {
|
||||
DateFieldSegmentProps,
|
||||
DateFieldSegmentPropsWithoutHTML
|
||||
} from '../date-field/types';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Preferred order for date segments in the input.
|
||||
*
|
||||
* @defaultValue 'DMY'
|
||||
*/
|
||||
dateOrder?: DatiDateOrder;
|
||||
|
||||
/**
|
||||
* Preferred time format for time segments in the input.
|
||||
* When provided, it overrides `hourCycle`.
|
||||
*
|
||||
* @defaultValue '24h'
|
||||
*/
|
||||
timeFormat?: DatiTimeFormat;
|
||||
|
||||
/**
|
||||
* 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<AcelPrimitiveDivAttributes, DateRangeFieldRootPropsWithoutHTML>;
|
||||
|
||||
export type DateRangeFieldLabelPropsWithoutHTML = WithChild;
|
||||
|
||||
export type DateRangeFieldLabelProps = DateRangeFieldLabelPropsWithoutHTML &
|
||||
Without<AcelPrimitiveSpanAttributes, 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<AcelPrimitiveDivAttributes, DateRangeFieldInputPropsWithoutHTML>;
|
||||
|
||||
export type DateRangeFieldSegmentPropsWithoutHTML = DateFieldSegmentPropsWithoutHTML;
|
||||
|
||||
export type DateRangeFieldSegmentProps = DateFieldSegmentProps;
|
||||
@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps, createId } from "../../utilities/index";
|
||||
import type { DateRangePickerCalendarProps } from "../types.ts";
|
||||
import { DateRangePickerRootContext } from "../date-range-picker.svelte.ts";
|
||||
import { RangeCalendarRootState } from "../../range-calendar/range-calendar.svelte.ts";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: DateRangePickerCalendarProps = $props();
|
||||
|
||||
const dateRangePickerRootState = DateRangePickerRootContext.get();
|
||||
|
||||
const rangeCalendarState = RangeCalendarRootState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
calendarLabel: dateRangePickerRootState.opts.calendarLabel,
|
||||
fixedWeeks: dateRangePickerRootState.opts.fixedWeeks,
|
||||
isDateDisabled: dateRangePickerRootState.opts.isDateDisabled,
|
||||
isDateUnavailable: dateRangePickerRootState.opts.isDateUnavailable,
|
||||
locale: dateRangePickerRootState.opts.locale,
|
||||
dateOrder: dateRangePickerRootState.opts.dateOrder,
|
||||
numberOfMonths: dateRangePickerRootState.opts.numberOfMonths,
|
||||
pagedNavigation: dateRangePickerRootState.opts.pagedNavigation,
|
||||
preventDeselect: dateRangePickerRootState.opts.preventDeselect,
|
||||
readonly: dateRangePickerRootState.opts.readonly,
|
||||
weekStartsOn: dateRangePickerRootState.opts.weekStartsOn,
|
||||
weekdayFormat: dateRangePickerRootState.opts.weekdayFormat,
|
||||
disabled: dateRangePickerRootState.opts.disabled,
|
||||
disableDaysOutsideMonth: dateRangePickerRootState.opts.disableDaysOutsideMonth,
|
||||
maxValue: dateRangePickerRootState.opts.maxValue,
|
||||
minValue: dateRangePickerRootState.opts.minValue,
|
||||
placeholder: dateRangePickerRootState.opts.placeholder,
|
||||
value: dateRangePickerRootState.opts.value,
|
||||
excludeDisabled: dateRangePickerRootState.opts.excludeDisabled,
|
||||
onRangeSelect: dateRangePickerRootState.opts.onRangeSelect,
|
||||
startValue: dateRangePickerRootState.opts.startValue,
|
||||
endValue: dateRangePickerRootState.opts.endValue,
|
||||
defaultPlaceholder: dateRangePickerRootState.opts.defaultPlaceholder,
|
||||
minDays: dateRangePickerRootState.opts.minDays,
|
||||
maxDays: dateRangePickerRootState.opts.maxDays,
|
||||
monthFormat: dateRangePickerRootState.opts.monthFormat,
|
||||
yearFormat: dateRangePickerRootState.opts.yearFormat,
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rangeCalendarState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, ...rangeCalendarState.snippetProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.(rangeCalendarState.snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,67 @@
|
||||
import type { DateValue } from "$lib/glob/lib";
|
||||
import { Context } from "runed";
|
||||
import { type ReadableBoxedValues, type WritableBoxedValues } from "../utilities";
|
||||
import type {
|
||||
DateMatcher,
|
||||
DateRange,
|
||||
Granularity,
|
||||
HourCycle,
|
||||
WeekStartsOn,
|
||||
DatiDateOrder,
|
||||
} from "$lib/glob/lib";
|
||||
import type { SegmentPart } from "../utilities/datetime/types";
|
||||
|
||||
export const DateRangePickerRootContext = new Context<DateRangePickerRootState>(
|
||||
"DateRangePicker.Root"
|
||||
);
|
||||
|
||||
interface DateRangePickerRootStateOpts
|
||||
extends WritableBoxedValues<{
|
||||
value: DateRange;
|
||||
startValue: DateValue | undefined;
|
||||
endValue: DateValue | undefined;
|
||||
open: boolean;
|
||||
placeholder: DateValue;
|
||||
}>,
|
||||
ReadableBoxedValues<{
|
||||
readonlySegments: SegmentPart[];
|
||||
isDateUnavailable: DateMatcher;
|
||||
isDateDisabled: DateMatcher;
|
||||
minValue: DateValue | undefined;
|
||||
maxValue: DateValue | undefined;
|
||||
minDays: number | undefined;
|
||||
maxDays: number | undefined;
|
||||
disabled: boolean;
|
||||
readonly: boolean;
|
||||
granularity: Granularity | undefined;
|
||||
hourCycle: HourCycle | undefined;
|
||||
locale: string;
|
||||
dateOrder: DatiDateOrder;
|
||||
hideTimeZone: boolean;
|
||||
required: boolean;
|
||||
preventDeselect: boolean;
|
||||
pagedNavigation: boolean;
|
||||
weekStartsOn: WeekStartsOn | undefined;
|
||||
weekdayFormat: Intl.DateTimeFormatOptions["weekday"];
|
||||
fixedWeeks: boolean;
|
||||
numberOfMonths: number;
|
||||
calendarLabel: string;
|
||||
disableDaysOutsideMonth: boolean;
|
||||
excludeDisabled: boolean;
|
||||
onRangeSelect?: () => void;
|
||||
monthFormat: Intl.DateTimeFormatOptions["month"] | ((month: number) => string);
|
||||
yearFormat: Intl.DateTimeFormatOptions["year"] | ((year: number) => string);
|
||||
}> {
|
||||
defaultPlaceholder: DateValue;
|
||||
}
|
||||
|
||||
export class DateRangePickerRootState {
|
||||
static create(opts: DateRangePickerRootStateOpts) {
|
||||
return DateRangePickerRootContext.set(new DateRangePickerRootState(opts));
|
||||
}
|
||||
readonly opts: DateRangePickerRootStateOpts;
|
||||
|
||||
constructor(opts: DateRangePickerRootStateOpts) {
|
||||
this.opts = opts;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,391 @@
|
||||
import type { DateValue } from "$lib/glob/lib";
|
||||
import type { OnChangeFn, WithChild, Without } from "../utilities/types.ts";
|
||||
import type { AcelPrimitiveDivAttributes } from "../utilities/attributes";
|
||||
import type {
|
||||
DateMatcher,
|
||||
DateOnInvalid,
|
||||
DateRange,
|
||||
DateRangeValidator,
|
||||
Granularity,
|
||||
WeekStartsOn,
|
||||
DatiDateOrder,
|
||||
DatiTimeFormat,
|
||||
} from "$lib/glob/lib";
|
||||
import type { EditableSegmentPart } from "../utilities/datetime/types";
|
||||
import type { CalendarRootSnippetProps } from "../calendar/types.ts";
|
||||
|
||||
export type DateRangePickerRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The value of the date picker.
|
||||
*
|
||||
* @bindable
|
||||
*/
|
||||
value?: DateRange;
|
||||
|
||||
/**
|
||||
* A callback function called when the value changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<DateRange>;
|
||||
|
||||
/**
|
||||
* 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 function called when the placeholder value changes.
|
||||
*/
|
||||
onPlaceholderChange?: OnChangeFn<DateValue>;
|
||||
|
||||
/**
|
||||
* The open state of the date picker popover.
|
||||
*
|
||||
* @bindable
|
||||
*/
|
||||
open?: boolean;
|
||||
|
||||
/**
|
||||
* A callback function called when the open state changes.
|
||||
*/
|
||||
onOpenChange?: OnChangeFn<boolean>;
|
||||
|
||||
/**
|
||||
* A callback function called when the open state changes complete.
|
||||
*/
|
||||
onOpenChangeComplete?: OnChangeFn<boolean>;
|
||||
|
||||
/**
|
||||
* A function that returns true if the given date is unavailable,
|
||||
* where if selected, the date field will be marked as invalid.
|
||||
*/
|
||||
isDateUnavailable?: DateMatcher;
|
||||
|
||||
/**
|
||||
* A function that returns true if the given date is disabled,
|
||||
* which makes the date unable to be selected in the calendar.
|
||||
*/
|
||||
isDateDisabled?: DateMatcher;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Preferred order for date segments in the input.
|
||||
*
|
||||
* @defaultValue 'DMY'
|
||||
*/
|
||||
dateOrder?: DatiDateOrder;
|
||||
|
||||
/**
|
||||
* Preferred time format for time segments in the input.
|
||||
* When provided, it overrides `hourCycle`.
|
||||
*
|
||||
* @defaultValue '24h'
|
||||
*/
|
||||
timeFormat?: DatiTimeFormat;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Whether or not the hidden input of the date field requires a value
|
||||
* to be submitted.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
required?: boolean;
|
||||
|
||||
/**
|
||||
* Applicable only when `numberOfMonths` is greater than 1.
|
||||
*
|
||||
* Controls whether to use paged navigation for the next and previous buttons in the
|
||||
* date picker. With paged navigation set to `true`, clicking the next/prev buttons
|
||||
* changes all months in view. When set to `false`, it shifts the view by a single month.
|
||||
*
|
||||
* For example, with `pagedNavigation` set to `true` and 2 months displayed (January and
|
||||
* February), clicking the next button changes the view to March and April. If `pagedNavigation`
|
||||
* is `false`, the view shifts to February and March.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
pagedNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* The day of the week to start the calendar on, which must
|
||||
* be a number between 0 and 6, where 0 is Sunday and 6 is
|
||||
* Saturday.
|
||||
*
|
||||
* @defaultValue 0 (Sunday)
|
||||
*/
|
||||
weekStartsOn?: WeekStartsOn;
|
||||
|
||||
/**
|
||||
* How the string representation of the weekdays provided via the `weekdays` state store
|
||||
* should be formatted.
|
||||
*
|
||||
* ```md
|
||||
* - "long": "Sunday", "Monday", "Tuesday", etc.
|
||||
* - "short": "Sun", "Mon", "Tue", etc.
|
||||
* - "narrow": "S", "M", "T", etc.
|
||||
*```
|
||||
*
|
||||
* @defaultValue "narrow"
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#weekday
|
||||
*/
|
||||
weekdayFormat?: Intl.DateTimeFormatOptions["weekday"];
|
||||
|
||||
/**
|
||||
* Display 6 weeks per month, regardless the month's number of weeks.
|
||||
* This is useful for displaying a consistent calendar, where the size
|
||||
* of the calendar doesn't change month to month.
|
||||
*
|
||||
* To display 6 weeks per month, you will need to render out the previous
|
||||
* and next month's dates in the calendar as well.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
fixedWeeks?: boolean;
|
||||
|
||||
/**
|
||||
* Determines the number of months to display on the calendar simultaneously.
|
||||
* For navigation between months, refer to the `pagedNavigation` prop.
|
||||
*
|
||||
* @defaultValue 1
|
||||
*/
|
||||
numberOfMonths?: number;
|
||||
|
||||
/**
|
||||
* This label is exclusively used for accessibility, remaining hidden from the page.
|
||||
* It's read by screen readers when the calendar is opened. The current month and year
|
||||
* are automatically appended to the label, so you only need to provide the base label.
|
||||
*
|
||||
* For instance:
|
||||
* - 'Date of birth' will be read as 'Date of birth, January 2021' if the current month is January 2021.
|
||||
* - 'Appointment date' will be read as 'Appointment date, January 2021' if the current month is January 2021.
|
||||
* - 'Booking date' will be read as 'Booking date, January 2021' if the current month is January 2021.
|
||||
*/
|
||||
calendarLabel?: string;
|
||||
|
||||
/**
|
||||
* Whether to disable the selection of days outside the current month. By default,
|
||||
* days outside the current month are rendered to fill the calendar grid, but they
|
||||
* are not selectable. Setting this prop to `true` will disable this behavior.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disableDaysOutsideMonth?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not users can deselect a date once selected
|
||||
* without selecting another date.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
preventDeselect?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to close the popover when a date range is selected.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
closeOnRangeSelect?: 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;
|
||||
|
||||
/**
|
||||
* The minimum number of days that can be selected in a range.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
minDays?: number;
|
||||
|
||||
/**
|
||||
* The maximum number of days that can be selected in a range.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
maxDays?: number;
|
||||
|
||||
/**
|
||||
* Whether to automatically reset the range if any date within the selected range
|
||||
* becomes disabled. When true, the entire range will be cleared if a disabled
|
||||
* date is found between the start and end dates.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
excludeDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* The format of the month names in the calendar.
|
||||
*
|
||||
* @default "long"
|
||||
*/
|
||||
monthFormat?: Intl.DateTimeFormatOptions["month"] | ((month: number) => string);
|
||||
|
||||
/**
|
||||
* The format of the year names in the calendar.
|
||||
*
|
||||
* @default "numeric"
|
||||
*/
|
||||
yearFormat?: Intl.DateTimeFormatOptions["year"] | ((year: number) => string);
|
||||
}>;
|
||||
|
||||
export type DateRangePickerRootProps = DateRangePickerRootPropsWithoutHTML &
|
||||
Without<AcelPrimitiveDivAttributes, DateRangePickerRootPropsWithoutHTML>;
|
||||
|
||||
export type {
|
||||
PopoverTriggerPropsWithoutHTML as DateRangePickerTriggerPropsWithoutHTML,
|
||||
PopoverTriggerProps as DateRangePickerTriggerProps,
|
||||
PopoverContentPropsWithoutHTML as DateRangePickerContentPropsWithoutHTML,
|
||||
PopoverContentProps as DateRangePickerContentProps,
|
||||
PopoverArrowPropsWithoutHTML as DateRangePickerArrowPropsWithoutHTML,
|
||||
PopoverArrowProps as DateRangePickerArrowProps,
|
||||
PopoverClosePropsWithoutHTML as DateRangePickerClosePropsWithoutHTML,
|
||||
PopoverCloseProps as DateRangePickerCloseProps,
|
||||
} from "../popover/types.ts";
|
||||
|
||||
export type {
|
||||
DateRangeFieldInputPropsWithoutHTML as DateRangePickerInputPropsWithoutHTML,
|
||||
DateRangeFieldInputProps as DateRangePickerInputProps,
|
||||
DateRangeFieldLabelPropsWithoutHTML as DateRangePickerLabelPropsWithoutHTML,
|
||||
DateRangeFieldLabelProps as DateRangePickerLabelProps,
|
||||
DateRangeFieldSegmentPropsWithoutHTML as DateRangePickerSegmentPropsWithoutHTML,
|
||||
DateRangeFieldSegmentProps as DateRangePickerSegmentProps,
|
||||
} from "../date-range-field/types.ts";
|
||||
|
||||
export type DateRangePickerCalendarPropsWithoutHTML = WithChild<{}, CalendarRootSnippetProps>;
|
||||
|
||||
export type DateRangePickerCalendarProps = DateRangePickerCalendarPropsWithoutHTML &
|
||||
Without<AcelPrimitiveDivAttributes, DateRangePickerCalendarPropsWithoutHTML>;
|
||||
|
||||
export type {
|
||||
RangeCalendarCellPropsWithoutHTML as DateRangePickerCellPropsWithoutHTML,
|
||||
RangeCalendarCellProps as DateRangePickerCellProps,
|
||||
RangeCalendarDayPropsWithoutHTML as DateRangePickerDayPropsWithoutHTML,
|
||||
RangeCalendarDayProps as DateRangePickerDayProps,
|
||||
RangeCalendarGridPropsWithoutHTML as DateRangePickerGridPropsWithoutHTML,
|
||||
RangeCalendarGridProps as DateRangePickerGridProps,
|
||||
RangeCalendarGridBodyPropsWithoutHTML as DateRangePickerGridBodyPropsWithoutHTML,
|
||||
RangeCalendarGridBodyProps as DateRangePickerGridBodyProps,
|
||||
RangeCalendarGridHeadPropsWithoutHTML as DateRangePickerGridHeadPropsWithoutHTML,
|
||||
RangeCalendarGridHeadProps as DateRangePickerGridHeadProps,
|
||||
RangeCalendarGridRowPropsWithoutHTML as DateRangePickerGridRowPropsWithoutHTML,
|
||||
RangeCalendarGridRowProps as DateRangePickerGridRowProps,
|
||||
RangeCalendarHeadCellPropsWithoutHTML as DateRangePickerHeadCellPropsWithoutHTML,
|
||||
RangeCalendarHeadCellProps as DateRangePickerHeadCellProps,
|
||||
RangeCalendarHeaderPropsWithoutHTML as DateRangePickerHeaderPropsWithoutHTML,
|
||||
RangeCalendarHeaderProps as DateRangePickerHeaderProps,
|
||||
RangeCalendarHeadingPropsWithoutHTML as DateRangePickerHeadingPropsWithoutHTML,
|
||||
RangeCalendarHeadingProps as DateRangePickerHeadingProps,
|
||||
RangeCalendarNextButtonPropsWithoutHTML as DateRangePickerNextButtonPropsWithoutHTML,
|
||||
RangeCalendarNextButtonProps as DateRangePickerNextButtonProps,
|
||||
RangeCalendarPrevButtonPropsWithoutHTML as DateRangePickerPrevButtonPropsWithoutHTML,
|
||||
RangeCalendarPrevButtonProps as DateRangePickerPrevButtonProps,
|
||||
RangeCalendarMonthSelectProps as DateRangePickerMonthSelectProps,
|
||||
RangeCalendarMonthSelectPropsWithoutHTML as DateRangePickerMonthSelectPropsWithoutHTML,
|
||||
RangeCalendarYearSelectProps as DateRangePickerYearSelectProps,
|
||||
RangeCalendarYearSelectPropsWithoutHTML as DateRangePickerYearSelectPropsWithoutHTML,
|
||||
} from "../range-calendar/types";
|
||||
@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
import { watch } from "runed";
|
||||
import { boxWith, mergeProps } from "../../utilities/index";
|
||||
import { type DateValue, DEFAULT_DATI_DATE_ORDER } from "$lib/glob/lib";
|
||||
import type { RangeCalendarRootProps } from "../types.js";
|
||||
import { RangeCalendarRootState } from "../range-calendar.svelte.ts";
|
||||
import { noop } from "../../utilities/noop";
|
||||
import { createId } from "../../utilities/create-id";
|
||||
import { getDefaultDate } from "$lib/glob/lib";
|
||||
import { resolveLocaleProp } from "../../comn/config/prop-resolvers";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
onValueChange = noop,
|
||||
placeholder = $bindable(),
|
||||
onPlaceholderChange = noop,
|
||||
weekdayFormat = "narrow",
|
||||
weekStartsOn,
|
||||
pagedNavigation = false,
|
||||
isDateDisabled = () => false,
|
||||
isDateUnavailable = () => false,
|
||||
fixedWeeks = false,
|
||||
numberOfMonths = 1,
|
||||
locale,
|
||||
dateOrder = DEFAULT_DATI_DATE_ORDER,
|
||||
calendarLabel = "Event",
|
||||
disabled = false,
|
||||
readonly = false,
|
||||
minValue = undefined,
|
||||
maxValue = undefined,
|
||||
preventDeselect = false,
|
||||
disableDaysOutsideMonth = true,
|
||||
minDays,
|
||||
maxDays,
|
||||
onStartValueChange = noop,
|
||||
onEndValueChange = noop,
|
||||
excludeDisabled = false,
|
||||
monthFormat = "long",
|
||||
yearFormat = "numeric",
|
||||
...restProps
|
||||
}: RangeCalendarRootProps = $props();
|
||||
|
||||
let startValue = $state<DateValue | undefined>(value?.start);
|
||||
let endValue = $state<DateValue | undefined>(value?.end);
|
||||
|
||||
function getDefaultPlaceholder() {
|
||||
return getDefaultDate({
|
||||
defaultValue: value?.start,
|
||||
minValue,
|
||||
maxValue,
|
||||
});
|
||||
}
|
||||
|
||||
function handleDefaultPlaceholder() {
|
||||
if (placeholder !== undefined) return;
|
||||
placeholder = getDefaultPlaceholder();
|
||||
}
|
||||
|
||||
// SSR
|
||||
handleDefaultPlaceholder();
|
||||
|
||||
watch.pre(
|
||||
() => placeholder,
|
||||
() => {
|
||||
handleDefaultPlaceholder();
|
||||
}
|
||||
);
|
||||
|
||||
function handleDefaultValue() {
|
||||
if (value !== undefined) return;
|
||||
value = { start: undefined, end: undefined };
|
||||
}
|
||||
|
||||
// SSR
|
||||
handleDefaultValue();
|
||||
|
||||
watch.pre(
|
||||
() => value,
|
||||
() => {
|
||||
handleDefaultValue();
|
||||
}
|
||||
);
|
||||
|
||||
const rootState = RangeCalendarRootState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(
|
||||
() => value!,
|
||||
(v) => {
|
||||
value = v;
|
||||
onValueChange(v);
|
||||
}
|
||||
),
|
||||
placeholder: boxWith(
|
||||
() => placeholder!,
|
||||
(v) => {
|
||||
placeholder = v;
|
||||
onPlaceholderChange(v);
|
||||
}
|
||||
),
|
||||
disabled: boxWith(() => disabled),
|
||||
readonly: boxWith(() => readonly),
|
||||
preventDeselect: boxWith(() => preventDeselect),
|
||||
minValue: boxWith(() => minValue),
|
||||
maxValue: boxWith(() => maxValue),
|
||||
isDateUnavailable: boxWith(() => isDateUnavailable),
|
||||
isDateDisabled: boxWith(() => isDateDisabled),
|
||||
pagedNavigation: boxWith(() => pagedNavigation),
|
||||
weekStartsOn: boxWith(() => weekStartsOn),
|
||||
weekdayFormat: boxWith(() => weekdayFormat),
|
||||
numberOfMonths: boxWith(() => numberOfMonths),
|
||||
locale: resolveLocaleProp(() => locale),
|
||||
dateOrder: boxWith(() => dateOrder),
|
||||
calendarLabel: boxWith(() => calendarLabel),
|
||||
fixedWeeks: boxWith(() => fixedWeeks),
|
||||
disableDaysOutsideMonth: boxWith(() => disableDaysOutsideMonth),
|
||||
minDays: boxWith(() => minDays),
|
||||
maxDays: boxWith(() => maxDays),
|
||||
excludeDisabled: boxWith(() => excludeDisabled),
|
||||
startValue: boxWith(
|
||||
() => startValue,
|
||||
(v) => {
|
||||
startValue = v;
|
||||
onStartValueChange(v);
|
||||
}
|
||||
),
|
||||
endValue: boxWith(
|
||||
() => endValue,
|
||||
(v) => {
|
||||
endValue = v;
|
||||
onEndValueChange(v);
|
||||
}
|
||||
),
|
||||
monthFormat: boxWith(() => monthFormat),
|
||||
yearFormat: boxWith(() => yearFormat),
|
||||
defaultPlaceholder: getDefaultPlaceholder(),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, ...rootState.snippetProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.(rootState.snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,925 @@
|
||||
import {
|
||||
type DateValue,
|
||||
type DateRange,
|
||||
type Month,
|
||||
type WeekStartsOn,
|
||||
type DatiDateOrder,
|
||||
getLocalTimeZone,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isToday,
|
||||
areAllDaysBetweenValid,
|
||||
getDateValueType,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isBetweenInclusive,
|
||||
dateValueToDate as toDate,
|
||||
} from "$lib/glob/lib";
|
||||
import {
|
||||
attachRef,
|
||||
DOMContext,
|
||||
type ReadableBoxedValues,
|
||||
type WritableBoxedValues,
|
||||
} from "../utilities/index";
|
||||
import { Context, watch } from "runed";
|
||||
import { CalendarRootContext } from "../calendar/calendar.svelte.ts";
|
||||
import type {
|
||||
AcelFocusEvent,
|
||||
AcelKeyboardEvent,
|
||||
AcelMouseEvent,
|
||||
RefAttachment,
|
||||
WithRefOpts,
|
||||
} from "../utilities/types";
|
||||
import { useId } from "../utilities/use-id";
|
||||
import { boolToStr, boolToEmptyStrOrUndef } from "../utilities/attrs";
|
||||
import { type Announcer, getAnnouncer } from "../utilities/datetime/announcer";
|
||||
import { type Formatter, createFormatter } from "../utilities/datetime/formatter";
|
||||
import {
|
||||
calendarAttrs,
|
||||
createMonths,
|
||||
getCalendarElementProps,
|
||||
getCalendarHeadingValue,
|
||||
getDefaultYears,
|
||||
getIsNextButtonDisabled,
|
||||
getIsPrevButtonDisabled,
|
||||
getWeekdays,
|
||||
handleCalendarKeydown,
|
||||
handleCalendarNextPage,
|
||||
handleCalendarPrevPage,
|
||||
shiftCalendarFocus,
|
||||
useEnsureNonDisabledPlaceholder,
|
||||
useMonthViewOptionsSync,
|
||||
useMonthViewPlaceholderSync,
|
||||
} from "../utilities/datetime/calendar-helpers.svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
|
||||
const RangeCalendarCellContext = new Context<RangeCalendarCellState>("RangeCalendar.Cell");
|
||||
|
||||
interface RangeCalendarRootStateOpts
|
||||
extends WithRefOpts,
|
||||
WritableBoxedValues<{
|
||||
value: DateRange;
|
||||
placeholder: DateValue;
|
||||
startValue: DateValue | undefined;
|
||||
endValue: DateValue | undefined;
|
||||
}>,
|
||||
ReadableBoxedValues<{
|
||||
preventDeselect: boolean;
|
||||
minValue: DateValue | undefined;
|
||||
maxValue: DateValue | undefined;
|
||||
disabled: boolean;
|
||||
pagedNavigation: boolean;
|
||||
weekStartsOn: WeekStartsOn | undefined;
|
||||
weekdayFormat: Intl.DateTimeFormatOptions["weekday"];
|
||||
isDateDisabled: (date: DateValue) => boolean;
|
||||
isDateUnavailable: (date: DateValue) => boolean;
|
||||
fixedWeeks: boolean;
|
||||
numberOfMonths: number;
|
||||
locale: string;
|
||||
dateOrder: DatiDateOrder;
|
||||
calendarLabel: string;
|
||||
readonly: boolean;
|
||||
disableDaysOutsideMonth: boolean;
|
||||
excludeDisabled: boolean;
|
||||
minDays: number | undefined;
|
||||
maxDays: number | undefined;
|
||||
/**
|
||||
* This is strictly used by the `DateRangePicker` component to close the popover when a date range
|
||||
* is selected. It is not intended to be used by the user.
|
||||
*/
|
||||
onRangeSelect?: () => void;
|
||||
monthFormat: Intl.DateTimeFormatOptions["month"] | ((month: number) => string);
|
||||
yearFormat: Intl.DateTimeFormatOptions["year"] | ((year: number) => string);
|
||||
}> {
|
||||
defaultPlaceholder: DateValue;
|
||||
}
|
||||
|
||||
export class RangeCalendarRootState {
|
||||
static create(opts: RangeCalendarRootStateOpts) {
|
||||
const state = new RangeCalendarRootState(opts);
|
||||
CalendarRootContext.set(state as never);
|
||||
return state;
|
||||
}
|
||||
|
||||
readonly opts: RangeCalendarRootStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly visibleMonths = $derived.by(() => this.months.map((month) => month.value));
|
||||
months: Month<DateValue>[] = $state([]);
|
||||
announcer: Announcer;
|
||||
formatter: Formatter;
|
||||
accessibleHeadingId = useId();
|
||||
focusedValue = $state<DateValue | undefined>(undefined);
|
||||
lastPressedDateValue: DateValue | undefined = undefined;
|
||||
domContext: DOMContext;
|
||||
|
||||
/**
|
||||
* This derived state holds an array of localized day names for the current
|
||||
* locale and calendar view. It dynamically syncs with the 'weekStartsOn' option,
|
||||
* updating its content when the option changes. Using this state to render the
|
||||
* calendar's days of the week is strongly recommended, as it guarantees that
|
||||
* the days are correctly formatted for the current locale and calendar view.
|
||||
*/
|
||||
readonly weekdays = $derived.by(() => {
|
||||
return getWeekdays({
|
||||
months: this.months,
|
||||
formatter: this.formatter,
|
||||
weekdayFormat: this.opts.weekdayFormat.current,
|
||||
});
|
||||
});
|
||||
|
||||
readonly isStartInvalid = $derived.by(() => {
|
||||
if (!this.opts.startValue.current) return false;
|
||||
return (
|
||||
this.isDateUnavailable(this.opts.startValue.current) ||
|
||||
this.isDateDisabled(this.opts.startValue.current)
|
||||
);
|
||||
});
|
||||
|
||||
readonly isEndInvalid = $derived.by(() => {
|
||||
if (!this.opts.endValue.current) return false;
|
||||
return (
|
||||
this.isDateUnavailable(this.opts.endValue.current) ||
|
||||
this.isDateDisabled(this.opts.endValue.current)
|
||||
);
|
||||
});
|
||||
|
||||
readonly isInvalid = $derived.by(() => {
|
||||
if (this.isStartInvalid || this.isEndInvalid) return true;
|
||||
|
||||
if (
|
||||
this.opts.endValue.current &&
|
||||
this.opts.startValue.current &&
|
||||
isBefore(this.opts.endValue.current, this.opts.startValue.current)
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
readonly isNextButtonDisabled = $derived.by(() => {
|
||||
return getIsNextButtonDisabled({
|
||||
maxValue: this.opts.maxValue.current,
|
||||
months: this.months,
|
||||
disabled: this.opts.disabled.current,
|
||||
});
|
||||
});
|
||||
|
||||
readonly isPrevButtonDisabled = $derived.by(() => {
|
||||
return getIsPrevButtonDisabled({
|
||||
minValue: this.opts.minValue.current,
|
||||
months: this.months,
|
||||
disabled: this.opts.disabled.current,
|
||||
});
|
||||
});
|
||||
|
||||
readonly headingValue = $derived.by(() => {
|
||||
this.opts.monthFormat.current;
|
||||
this.opts.yearFormat.current;
|
||||
return getCalendarHeadingValue({
|
||||
months: this.months,
|
||||
formatter: this.formatter,
|
||||
locale: this.opts.locale.current,
|
||||
});
|
||||
});
|
||||
|
||||
readonly fullCalendarLabel = $derived.by(
|
||||
() => `${this.opts.calendarLabel.current} ${this.headingValue}`
|
||||
);
|
||||
|
||||
readonly highlightedRange = $derived.by(() => {
|
||||
if (this.opts.startValue.current && this.opts.endValue.current) return null;
|
||||
if (!this.opts.startValue.current || !this.focusedValue) return null;
|
||||
|
||||
const isStartBeforeFocused = isBefore(this.opts.startValue.current, this.focusedValue);
|
||||
const start = isStartBeforeFocused ? this.opts.startValue.current : this.focusedValue;
|
||||
const end = isStartBeforeFocused ? this.focusedValue : this.opts.startValue.current;
|
||||
const range = { start, end };
|
||||
|
||||
if (isSameDay(start.add({ days: 1 }), end) || isSameDay(start, end)) {
|
||||
return range;
|
||||
}
|
||||
|
||||
const isValid = areAllDaysBetweenValid(
|
||||
start,
|
||||
end,
|
||||
this.isDateUnavailable,
|
||||
this.isDateDisabled
|
||||
);
|
||||
|
||||
if (isValid) return range;
|
||||
return null;
|
||||
});
|
||||
|
||||
readonly initialPlaceholderYear = $derived.by(() =>
|
||||
untrack(() => this.opts.placeholder.current.year)
|
||||
);
|
||||
|
||||
readonly defaultYears = $derived.by(() => {
|
||||
return getDefaultYears({
|
||||
minValue: this.opts.minValue.current,
|
||||
maxValue: this.opts.maxValue.current,
|
||||
placeholderYear: this.initialPlaceholderYear,
|
||||
});
|
||||
});
|
||||
|
||||
constructor(opts: RangeCalendarRootStateOpts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
this.domContext = new DOMContext(opts.ref);
|
||||
this.announcer = getAnnouncer(null);
|
||||
this.formatter = createFormatter({
|
||||
initialLocale: this.opts.locale.current,
|
||||
monthFormat: this.opts.monthFormat,
|
||||
yearFormat: this.opts.yearFormat,
|
||||
});
|
||||
|
||||
this.months = createMonths({
|
||||
dateObj: this.opts.placeholder.current,
|
||||
weekStartsOn: this.opts.weekStartsOn.current,
|
||||
locale: this.opts.locale.current,
|
||||
fixedWeeks: this.opts.fixedWeeks.current,
|
||||
numberOfMonths: this.opts.numberOfMonths.current,
|
||||
});
|
||||
|
||||
$effect.pre(() => {
|
||||
if (this.formatter.getLocale() === this.opts.locale.current) return;
|
||||
this.formatter.setLocale(this.opts.locale.current);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
this.announcer = getAnnouncer(this.domContext.getDocument());
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the displayed months based on changes in the placeholder values,
|
||||
* which determines the month to show in the calendar.
|
||||
*/
|
||||
useMonthViewPlaceholderSync({
|
||||
placeholder: this.opts.placeholder,
|
||||
getVisibleMonths: () => this.visibleMonths,
|
||||
weekStartsOn: this.opts.weekStartsOn,
|
||||
locale: this.opts.locale,
|
||||
fixedWeeks: this.opts.fixedWeeks,
|
||||
numberOfMonths: this.opts.numberOfMonths,
|
||||
setMonths: this.setMonths,
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the displayed months based on changes in the options values,
|
||||
* which determines the month to show in the calendar.
|
||||
*/
|
||||
useMonthViewOptionsSync({
|
||||
fixedWeeks: this.opts.fixedWeeks,
|
||||
locale: this.opts.locale,
|
||||
numberOfMonths: this.opts.numberOfMonths,
|
||||
placeholder: this.opts.placeholder,
|
||||
setMonths: this.setMonths,
|
||||
weekStartsOn: this.opts.weekStartsOn,
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the accessible heading's text content when the `fullCalendarLabel`
|
||||
* changes.
|
||||
*/
|
||||
$effect(() => {
|
||||
const node = this.domContext.getElementById(this.accessibleHeadingId);
|
||||
if (!node) return;
|
||||
node.textContent = this.fullCalendarLabel;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Check for disabled dates in the selected range when excludeDisabled is enabled
|
||||
*/
|
||||
watch(
|
||||
[
|
||||
() => this.opts.startValue.current,
|
||||
() => this.opts.endValue.current,
|
||||
() => this.opts.excludeDisabled.current,
|
||||
],
|
||||
([startValue, endValue, excludeDisabled]) => {
|
||||
if (!excludeDisabled || !startValue || !endValue) return;
|
||||
|
||||
if (this.#hasDisabledDatesInRange(startValue, endValue)) {
|
||||
this.#setStartValue(undefined);
|
||||
this.#setEndValue(undefined);
|
||||
this.#announceEmpty();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
if (isBefore(endValue, startValue)) {
|
||||
const start = startValue;
|
||||
const end = endValue;
|
||||
this.#setStartValue(end);
|
||||
this.#setEndValue(start);
|
||||
if (!this.#isRangeValid(endValue, startValue)) {
|
||||
this.#setStartValue(startValue);
|
||||
this.#setEndValue(undefined);
|
||||
return { start: startValue, end: undefined };
|
||||
}
|
||||
return { start: endValue, end: startValue };
|
||||
} else {
|
||||
if (!this.#isRangeValid(startValue, endValue)) {
|
||||
this.#setStartValue(endValue);
|
||||
this.#setEndValue(undefined);
|
||||
return { start: endValue, end: undefined };
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.shiftFocus = this.shiftFocus.bind(this);
|
||||
this.handleCellClick = this.handleCellClick.bind(this);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.prevPage = this.prevPage.bind(this);
|
||||
this.nextYear = this.nextYear.bind(this);
|
||||
this.prevYear = this.prevYear.bind(this);
|
||||
this.setYear = this.setYear.bind(this);
|
||||
this.setMonth = this.setMonth.bind(this);
|
||||
this.isDateDisabled = this.isDateDisabled.bind(this);
|
||||
this.isDateUnavailable = this.isDateUnavailable.bind(this);
|
||||
this.isOutsideVisibleMonths = this.isOutsideVisibleMonths.bind(this);
|
||||
this.isSelected = this.isSelected.bind(this);
|
||||
this.getAcelAttr = this.getAcelAttr.bind(this);
|
||||
|
||||
useEnsureNonDisabledPlaceholder({
|
||||
placeholder: opts.placeholder,
|
||||
defaultPlaceholder: opts.defaultPlaceholder,
|
||||
isDateDisabled: opts.isDateDisabled,
|
||||
maxValue: opts.maxValue,
|
||||
minValue: opts.minValue,
|
||||
ref: opts.ref,
|
||||
});
|
||||
}
|
||||
|
||||
getAcelAttr: (typeof calendarAttrs)["getAttr"] = (part) => {
|
||||
return calendarAttrs.getAttr(part);
|
||||
};
|
||||
|
||||
#updateValue(cb: (value: DateRange) => DateRange) {
|
||||
const value = this.opts.value.current;
|
||||
const newValue = cb(value);
|
||||
this.opts.value.current = newValue;
|
||||
if (newValue.start && newValue.end) {
|
||||
this.opts.onRangeSelect?.current?.();
|
||||
}
|
||||
}
|
||||
|
||||
#setStartValue(value: DateValue | undefined) {
|
||||
this.opts.startValue.current = value;
|
||||
// update the main value prop immediately for external consumers
|
||||
this.#updateValue((prev) => ({
|
||||
...prev,
|
||||
start: value,
|
||||
}));
|
||||
}
|
||||
|
||||
#setEndValue(value: DateValue | undefined) {
|
||||
this.opts.endValue.current = value;
|
||||
// update the main value prop immediately for external consumers
|
||||
this.#updateValue((prev) => ({
|
||||
...prev,
|
||||
end: value,
|
||||
}));
|
||||
}
|
||||
|
||||
setMonths = (months: Month<DateValue>[]) => {
|
||||
this.months = months;
|
||||
};
|
||||
|
||||
isOutsideVisibleMonths(date: DateValue) {
|
||||
return !this.visibleMonths.some((month) => isSameMonth(date, month));
|
||||
}
|
||||
|
||||
isDateDisabled(date: DateValue) {
|
||||
if (this.opts.isDateDisabled.current(date) || this.opts.disabled.current) return true;
|
||||
const minValue = this.opts.minValue.current;
|
||||
const maxValue = this.opts.maxValue.current;
|
||||
if (minValue && isBefore(date, minValue)) return true;
|
||||
if (maxValue && isAfter(date, maxValue)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
isDateUnavailable(date: DateValue) {
|
||||
if (this.opts.isDateUnavailable.current(date)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
isSelectionStart(date: DateValue) {
|
||||
if (!this.opts.startValue.current) return false;
|
||||
return isSameDay(date, this.opts.startValue.current);
|
||||
}
|
||||
|
||||
isSelectionEnd(date: DateValue) {
|
||||
if (!this.opts.endValue.current) return false;
|
||||
return isSameDay(date, this.opts.endValue.current);
|
||||
}
|
||||
|
||||
isSelected(date: DateValue) {
|
||||
if (this.opts.startValue.current && isSameDay(this.opts.startValue.current, date))
|
||||
return true;
|
||||
if (this.opts.endValue.current && isSameDay(this.opts.endValue.current, date)) return true;
|
||||
if (this.opts.startValue.current && this.opts.endValue.current) {
|
||||
return isBetweenInclusive(
|
||||
date,
|
||||
this.opts.startValue.current,
|
||||
this.opts.endValue.current
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#isRangeValid(start: DateValue, end: DateValue): boolean {
|
||||
// ensure we always use the correct order for calculation
|
||||
const orderedStart = isBefore(end, start) ? end : start;
|
||||
const orderedEnd = isBefore(end, start) ? start : end;
|
||||
|
||||
const startDate = orderedStart.toDate(getLocalTimeZone());
|
||||
const endDate = orderedEnd.toDate(getLocalTimeZone());
|
||||
|
||||
const timeDifference = endDate.getTime() - startDate.getTime();
|
||||
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||
const daysInRange = daysDifference + 1; // +1 to include both start and end days
|
||||
|
||||
if (this.opts.minDays.current && daysInRange < this.opts.minDays.current) return false;
|
||||
if (this.opts.maxDays.current && daysInRange > this.opts.maxDays.current) return false;
|
||||
|
||||
// check for disabled dates in range if excludeDisabled is enabled
|
||||
if (
|
||||
this.opts.excludeDisabled.current &&
|
||||
this.#hasDisabledDatesInRange(orderedStart, orderedEnd)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
shiftFocus(node: HTMLElement, add: number) {
|
||||
return shiftCalendarFocus({
|
||||
node,
|
||||
add,
|
||||
placeholder: this.opts.placeholder,
|
||||
calendarNode: this.opts.ref.current,
|
||||
isPrevButtonDisabled: this.isPrevButtonDisabled,
|
||||
isNextButtonDisabled: this.isNextButtonDisabled,
|
||||
months: this.months,
|
||||
numberOfMonths: this.opts.numberOfMonths.current,
|
||||
});
|
||||
}
|
||||
|
||||
#announceEmpty() {
|
||||
this.announcer.announce("Selected date is now empty.", "polite");
|
||||
}
|
||||
|
||||
#announceSelectedDate(date: DateValue) {
|
||||
this.announcer.announce(
|
||||
`Selected Date: ${this.formatter.selectedDate(date, false)}`,
|
||||
"polite"
|
||||
);
|
||||
}
|
||||
|
||||
#announceSelectedRange(start: DateValue, end: DateValue) {
|
||||
this.announcer.announce(
|
||||
`Selected Dates: ${this.formatter.selectedDate(start, false)} to ${this.formatter.selectedDate(end, false)}`,
|
||||
"polite"
|
||||
);
|
||||
}
|
||||
|
||||
handleCellClick(e: Event, date: DateValue) {
|
||||
if (this.isDateDisabled(date) || this.isDateUnavailable(date)) return;
|
||||
const prevLastPressedDate = this.lastPressedDateValue;
|
||||
this.lastPressedDateValue = date;
|
||||
|
||||
if (this.opts.startValue.current && this.highlightedRange === null) {
|
||||
if (
|
||||
isSameDay(this.opts.startValue.current, date) &&
|
||||
!this.opts.preventDeselect.current &&
|
||||
!this.opts.endValue.current
|
||||
) {
|
||||
this.#setStartValue(undefined);
|
||||
this.opts.placeholder.current = date;
|
||||
this.#announceEmpty();
|
||||
return;
|
||||
} else if (!this.opts.endValue.current) {
|
||||
e.preventDefault();
|
||||
if (prevLastPressedDate && isSameDay(prevLastPressedDate, date)) {
|
||||
this.#setStartValue(date);
|
||||
this.#announceSelectedDate(date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.opts.startValue.current &&
|
||||
this.opts.endValue.current &&
|
||||
isSameDay(this.opts.endValue.current, date) &&
|
||||
!this.opts.preventDeselect.current
|
||||
) {
|
||||
this.#setStartValue(undefined);
|
||||
this.#setEndValue(undefined);
|
||||
this.opts.placeholder.current = date;
|
||||
this.#announceEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.opts.startValue.current) {
|
||||
this.#announceSelectedDate(date);
|
||||
this.#setStartValue(date);
|
||||
} else if (!this.opts.endValue.current) {
|
||||
// determine the start and end dates for validation
|
||||
const startDate = this.opts.startValue.current;
|
||||
const endDate = date;
|
||||
const orderedStart = isBefore(endDate, startDate) ? endDate : startDate;
|
||||
const orderedEnd = isBefore(endDate, startDate) ? startDate : endDate;
|
||||
|
||||
// check if the range violates constraints
|
||||
if (!this.#isRangeValid(orderedStart, orderedEnd)) {
|
||||
// reset to just the clicked date
|
||||
this.#setStartValue(date);
|
||||
this.#setEndValue(undefined);
|
||||
this.#announceSelectedDate(date);
|
||||
} else {
|
||||
// ensure start and end are properly ordered
|
||||
if (isBefore(endDate, startDate)) {
|
||||
// backward selection - reorder the values
|
||||
this.#setStartValue(endDate);
|
||||
this.#setEndValue(startDate);
|
||||
this.#announceSelectedRange(endDate, startDate);
|
||||
} else {
|
||||
// forward selection - keep original order
|
||||
this.#setEndValue(date);
|
||||
this.#announceSelectedRange(this.opts.startValue.current, date);
|
||||
}
|
||||
}
|
||||
} else if (this.opts.endValue.current && this.opts.startValue.current) {
|
||||
this.#setEndValue(undefined);
|
||||
this.#announceSelectedDate(date);
|
||||
this.#setStartValue(date);
|
||||
}
|
||||
}
|
||||
|
||||
onkeydown(event: AcelKeyboardEvent) {
|
||||
return handleCalendarKeydown({
|
||||
event,
|
||||
handleCellClick: this.handleCellClick,
|
||||
placeholderValue: this.opts.placeholder.current,
|
||||
shiftFocus: this.shiftFocus,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the next page of the calendar.
|
||||
*/
|
||||
nextPage() {
|
||||
handleCalendarNextPage({
|
||||
fixedWeeks: this.opts.fixedWeeks.current,
|
||||
locale: this.opts.locale.current,
|
||||
numberOfMonths: this.opts.numberOfMonths.current,
|
||||
pagedNavigation: this.opts.pagedNavigation.current,
|
||||
setMonths: this.setMonths,
|
||||
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
|
||||
weekStartsOn: this.opts.weekStartsOn.current,
|
||||
months: this.months,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the previous page of the calendar.
|
||||
*/
|
||||
prevPage() {
|
||||
handleCalendarPrevPage({
|
||||
fixedWeeks: this.opts.fixedWeeks.current,
|
||||
locale: this.opts.locale.current,
|
||||
numberOfMonths: this.opts.numberOfMonths.current,
|
||||
pagedNavigation: this.opts.pagedNavigation.current,
|
||||
setMonths: this.setMonths,
|
||||
setPlaceholder: (date: DateValue) => (this.opts.placeholder.current = date),
|
||||
weekStartsOn: this.opts.weekStartsOn.current,
|
||||
months: this.months,
|
||||
});
|
||||
}
|
||||
|
||||
nextYear() {
|
||||
this.opts.placeholder.current = this.opts.placeholder.current.add({ years: 1 });
|
||||
}
|
||||
|
||||
prevYear() {
|
||||
this.opts.placeholder.current = this.opts.placeholder.current.subtract({ years: 1 });
|
||||
}
|
||||
|
||||
setYear(year: number) {
|
||||
this.opts.placeholder.current = this.opts.placeholder.current.set({ year });
|
||||
}
|
||||
|
||||
setMonth(month: number) {
|
||||
this.opts.placeholder.current = this.opts.placeholder.current.set({ month });
|
||||
}
|
||||
|
||||
getBitsAttr: (typeof calendarAttrs)["getAttr"] = (part) => {
|
||||
return calendarAttrs.getAttr(part, "range-calendar");
|
||||
};
|
||||
|
||||
readonly snippetProps = $derived.by(() => ({
|
||||
months: this.months,
|
||||
weekdays: this.weekdays,
|
||||
dateOrder: this.opts.dateOrder.current,
|
||||
}));
|
||||
|
||||
readonly props = $derived.by(
|
||||
() =>
|
||||
({
|
||||
...getCalendarElementProps({
|
||||
fullCalendarLabel: this.fullCalendarLabel,
|
||||
id: this.opts.id.current,
|
||||
isInvalid: this.isInvalid,
|
||||
disabled: this.opts.disabled.current,
|
||||
readonly: this.opts.readonly.current,
|
||||
}),
|
||||
[this.getBitsAttr("root")]: "",
|
||||
//
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}) as const
|
||||
);
|
||||
|
||||
#hasDisabledDatesInRange(start: DateValue, end: DateValue): boolean {
|
||||
for (
|
||||
let date = start;
|
||||
isBefore(date, end) || isSameDay(date, end);
|
||||
date = date.add({ days: 1 })
|
||||
) {
|
||||
if (this.isDateDisabled(date)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface RangeCalendarCellStateOpts
|
||||
extends WithRefOpts,
|
||||
ReadableBoxedValues<{
|
||||
date: DateValue;
|
||||
month: DateValue;
|
||||
}> {}
|
||||
|
||||
export class RangeCalendarCellState {
|
||||
static create(opts: RangeCalendarCellStateOpts) {
|
||||
return RangeCalendarCellContext.set(
|
||||
new RangeCalendarCellState(opts, CalendarRootContext.get() as unknown as RangeCalendarRootState)
|
||||
);
|
||||
}
|
||||
readonly opts: RangeCalendarCellStateOpts;
|
||||
readonly root: RangeCalendarRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly cellDate = $derived.by(() => toDate(this.opts.date.current));
|
||||
readonly isOutsideMonth = $derived.by(
|
||||
() => !isSameMonth(this.opts.date.current, this.opts.month.current)
|
||||
);
|
||||
readonly isDisabled = $derived.by(
|
||||
() =>
|
||||
this.root.isDateDisabled(this.opts.date.current) ||
|
||||
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current)
|
||||
);
|
||||
readonly isUnavailable = $derived.by(() =>
|
||||
this.root.opts.isDateUnavailable.current(this.opts.date.current)
|
||||
);
|
||||
readonly isDateToday = $derived.by(() => isToday(this.opts.date.current, getLocalTimeZone()));
|
||||
|
||||
readonly isOutsideVisibleMonths = $derived.by(() =>
|
||||
this.root.isOutsideVisibleMonths(this.opts.date.current)
|
||||
);
|
||||
readonly isFocusedDate = $derived.by(() =>
|
||||
isSameDay(this.opts.date.current, this.root.opts.placeholder.current)
|
||||
);
|
||||
readonly isSelectedDate = $derived.by(() => this.root.isSelected(this.opts.date.current));
|
||||
readonly isSelectionStart = $derived.by(() =>
|
||||
this.root.isSelectionStart(this.opts.date.current)
|
||||
);
|
||||
|
||||
readonly isRangeStart = $derived.by(() => this.root.isSelectionStart(this.opts.date.current));
|
||||
|
||||
readonly isRangeEnd = $derived.by(() => {
|
||||
if (!this.root.opts.endValue.current)
|
||||
return this.root.isSelectionStart(this.opts.date.current);
|
||||
return this.root.isSelectionEnd(this.opts.date.current);
|
||||
});
|
||||
|
||||
readonly isRangeMiddle = $derived.by(() => this.isSelectionMiddle);
|
||||
|
||||
readonly isSelectionMiddle = $derived.by(() => {
|
||||
return this.isSelectedDate && !this.isSelectionStart && !this.isSelectionEnd;
|
||||
});
|
||||
|
||||
readonly isSelectionEnd = $derived.by(() => this.root.isSelectionEnd(this.opts.date.current));
|
||||
readonly isHighlighted = $derived.by(() =>
|
||||
this.root.highlightedRange
|
||||
? isBetweenInclusive(
|
||||
this.opts.date.current,
|
||||
this.root.highlightedRange.start,
|
||||
this.root.highlightedRange.end
|
||||
)
|
||||
: false
|
||||
);
|
||||
|
||||
readonly labelText = $derived.by(() =>
|
||||
this.root.formatter.custom(this.cellDate, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
constructor(opts: RangeCalendarCellStateOpts, root: RangeCalendarRootState) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
}
|
||||
|
||||
readonly snippetProps = $derived.by(() => ({
|
||||
disabled: this.isDisabled,
|
||||
unavailable: this.isUnavailable,
|
||||
selected: this.isSelectedDate,
|
||||
}));
|
||||
|
||||
readonly ariaDisabled = $derived.by(() => {
|
||||
return (
|
||||
this.isDisabled ||
|
||||
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current) ||
|
||||
this.isUnavailable
|
||||
);
|
||||
});
|
||||
|
||||
readonly sharedDataAttrs = $derived.by(
|
||||
() =>
|
||||
({
|
||||
"data-unavailable": boolToEmptyStrOrUndef(this.isUnavailable),
|
||||
"data-today": this.isDateToday ? "" : undefined,
|
||||
"data-outside-month": this.isOutsideMonth ? "" : undefined,
|
||||
"data-outside-visible-months": this.isOutsideVisibleMonths ? "" : undefined,
|
||||
"data-focused": this.isFocusedDate ? "" : undefined,
|
||||
"data-selection-start": this.isSelectionStart ? "" : undefined,
|
||||
"data-selection-end": this.isSelectionEnd ? "" : undefined,
|
||||
"data-range-start": this.isRangeStart ? "" : undefined,
|
||||
"data-range-end": this.isRangeEnd ? "" : undefined,
|
||||
"data-range-middle": this.isRangeMiddle ? "" : undefined,
|
||||
"data-highlighted": this.isHighlighted ? "" : undefined,
|
||||
"data-selected": boolToEmptyStrOrUndef(this.isSelectedDate),
|
||||
"data-value": this.opts.date.current.toString(),
|
||||
"data-type": getDateValueType(this.opts.date.current),
|
||||
"data-disabled": boolToEmptyStrOrUndef(
|
||||
this.isDisabled ||
|
||||
(this.isOutsideMonth && this.root.opts.disableDaysOutsideMonth.current)
|
||||
),
|
||||
}) as const
|
||||
);
|
||||
|
||||
readonly props = $derived.by(
|
||||
() =>
|
||||
({
|
||||
id: this.opts.id.current,
|
||||
role: "gridcell",
|
||||
"aria-selected": boolToStr(this.isSelectedDate),
|
||||
"aria-disabled": boolToStr(this.ariaDisabled),
|
||||
...this.sharedDataAttrs,
|
||||
[this.root.getBitsAttr("cell")]: "",
|
||||
...this.attachment,
|
||||
}) as const
|
||||
);
|
||||
}
|
||||
|
||||
interface RangeCalendarDayStateOpts extends WithRefOpts {}
|
||||
|
||||
export class RangeCalendarDayState {
|
||||
static create(opts: RangeCalendarDayStateOpts) {
|
||||
return new RangeCalendarDayState(opts, RangeCalendarCellContext.get());
|
||||
}
|
||||
|
||||
readonly opts: RangeCalendarDayStateOpts;
|
||||
readonly cell: RangeCalendarCellState;
|
||||
readonly attachment: RefAttachment;
|
||||
|
||||
constructor(opts: RangeCalendarDayStateOpts, cell: RangeCalendarCellState) {
|
||||
this.opts = opts;
|
||||
this.cell = cell;
|
||||
this.attachment = attachRef(opts.ref);
|
||||
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.onmouseenter = this.onmouseenter.bind(this);
|
||||
this.onfocusin = this.onfocusin.bind(this);
|
||||
}
|
||||
|
||||
readonly #tabindex = $derived.by(() =>
|
||||
(this.cell.isOutsideMonth && this.cell.root.opts.disableDaysOutsideMonth.current) ||
|
||||
this.cell.isDisabled
|
||||
? undefined
|
||||
: this.cell.isFocusedDate
|
||||
? 0
|
||||
: -1
|
||||
);
|
||||
|
||||
onclick(e: AcelMouseEvent) {
|
||||
if (this.cell.isDisabled) return;
|
||||
this.cell.root.handleCellClick(e, this.cell.opts.date.current);
|
||||
}
|
||||
|
||||
onmouseenter(_: AcelMouseEvent) {
|
||||
if (this.cell.isDisabled) return;
|
||||
this.cell.root.focusedValue = this.cell.opts.date.current;
|
||||
}
|
||||
|
||||
onfocusin(_: AcelFocusEvent) {
|
||||
if (this.cell.isDisabled) return;
|
||||
this.cell.root.focusedValue = this.cell.opts.date.current;
|
||||
}
|
||||
|
||||
readonly snippetProps = $derived.by(() => ({
|
||||
disabled: this.cell.isDisabled,
|
||||
unavailable: this.cell.isUnavailable,
|
||||
selected: this.cell.isSelectedDate,
|
||||
day: `${this.cell.opts.date.current.day}`,
|
||||
}));
|
||||
|
||||
readonly props = $derived.by(
|
||||
() =>
|
||||
({
|
||||
id: this.opts.id.current,
|
||||
role: "button",
|
||||
"aria-label": this.cell.labelText,
|
||||
"aria-disabled": boolToStr(this.cell.ariaDisabled),
|
||||
...this.cell.sharedDataAttrs,
|
||||
tabindex: this.#tabindex,
|
||||
[this.cell.root.getBitsAttr("day")]: "",
|
||||
// Shared logic for range calendar and calendar
|
||||
"data-bits-day": "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onmouseenter: this.onmouseenter,
|
||||
onfocusin: this.onfocusin,
|
||||
...this.attachment,
|
||||
}) as const
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
import type { DateValue, DatiDateOrder } from "$lib/glob/lib";
|
||||
import type { OnChangeFn, WithChild, Without } from "../utilities/types";
|
||||
import type { DateMatcher, DateRange, Month } from "$lib/glob/lib";
|
||||
import type { AcelPrimitiveDivAttributes } from "../utilities/attributes";
|
||||
|
||||
export type RangeCalendarRootSnippetProps = {
|
||||
months: Month<DateValue>[];
|
||||
weekdays: string[];
|
||||
dateOrder: DatiDateOrder;
|
||||
};
|
||||
|
||||
export type RangeCalendarRootPropsWithoutHTML = WithChild<
|
||||
{
|
||||
/**
|
||||
* The value of the selected date range.
|
||||
* @bindable
|
||||
*/
|
||||
value?: DateRange;
|
||||
|
||||
/**
|
||||
* A callback function called when the value changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<DateRange>;
|
||||
|
||||
/**
|
||||
* The placeholder date, used to control the view of the
|
||||
* calendar when no value is present.
|
||||
*
|
||||
* @default the current date
|
||||
*/
|
||||
placeholder?: DateValue;
|
||||
|
||||
/**
|
||||
* A callback function called when the placeholder value
|
||||
* changes.
|
||||
*/
|
||||
onPlaceholderChange?: OnChangeFn<DateValue>;
|
||||
|
||||
/**
|
||||
* The minimum number of days that can be selected in a range.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
minDays?: number;
|
||||
|
||||
/**
|
||||
* The maximum number of days that can be selected in a range.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
maxDays?: number;
|
||||
|
||||
/**
|
||||
* Whether or not users can deselect a date once selected
|
||||
* without selecting another date.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
preventDeselect?: boolean;
|
||||
|
||||
/**
|
||||
* The minimum date that can be selected in the calendar.
|
||||
*/
|
||||
minValue?: DateValue;
|
||||
|
||||
/**
|
||||
* The maximum date that can be selected in the calendar.
|
||||
*/
|
||||
maxValue?: DateValue;
|
||||
|
||||
/**
|
||||
* Whether or not the calendar is disabled.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* Applicable only when `numberOfMonths` is greater than 1.
|
||||
*
|
||||
* Controls whether to use paged navigation for the next and previous buttons in the
|
||||
* date picker. With paged navigation set to `true`, clicking the next/prev buttons
|
||||
* changes all months in view. When set to `false`, it shifts the view by a single month.
|
||||
*
|
||||
* For example, with `pagedNavigation` set to `true` and 2 months displayed (January and
|
||||
* February), clicking the next button changes the view to March and April. If `pagedNavigation`
|
||||
* is `false`, the view shifts to February and March.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
pagedNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* The day of the week to start the calendar on, which must
|
||||
* be a number between 0 and 6, where 0 is Sunday and 6 is
|
||||
* Saturday.
|
||||
*
|
||||
* @default 0 (Sunday)
|
||||
*/
|
||||
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
/**
|
||||
* How the string representation of the weekdays provided via the `weekdays` state store
|
||||
* should be formatted.
|
||||
*
|
||||
* ```md
|
||||
* - "long": "Sunday", "Monday", "Tuesday", etc.
|
||||
* - "short": "Sun", "Mon", "Tue", etc.
|
||||
* - "narrow": "S", "M", "T", etc.
|
||||
*```
|
||||
*
|
||||
* @default "narrow"
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#weekday
|
||||
*/
|
||||
weekdayFormat?: Intl.DateTimeFormatOptions["weekday"];
|
||||
|
||||
/**
|
||||
* A function that receives a date and returns `true` or `false` to indicate whether
|
||||
* the date is disabled.
|
||||
*
|
||||
* @remarks
|
||||
* Disabled dates cannot be focused or selected. Additionally, they are tagged
|
||||
* with a data attribute to enable custom styling.
|
||||
*
|
||||
* `[data-disabled]` - applied to disabled dates
|
||||
*
|
||||
*/
|
||||
isDateDisabled?: DateMatcher;
|
||||
|
||||
/**
|
||||
* Dates matching the provided matchers are marked as "unavailable." Unlike disabled dates,
|
||||
* users can still focus and select unavailable dates. However, selecting an unavailable date
|
||||
* renders the date picker as invalid.
|
||||
*
|
||||
* For example, in a calendar for booking appointments, you might mark already booked dates as
|
||||
* unavailable. These dates could become available again before the appointment date, allowing
|
||||
* users to select them to learn more about the appointment.
|
||||
*
|
||||
* `[data-unavailable]` - applied to unavailable dates
|
||||
*
|
||||
*/
|
||||
isDateUnavailable?: DateMatcher;
|
||||
|
||||
/**
|
||||
* Display 6 weeks per month, regardless the month's number of weeks.
|
||||
* This is useful for displaying a consistent calendar, where the size
|
||||
* of the calendar doesn't change month to month.
|
||||
*
|
||||
* To display 6 weeks per month, you will need to render out the previous
|
||||
* and next month's dates in the calendar as well.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
fixedWeeks?: boolean;
|
||||
|
||||
/**
|
||||
* Determines the number of months to display on the calendar simultaneously.
|
||||
* For navigation between months, refer to the `pagedNavigation` prop.
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
numberOfMonths?: number;
|
||||
|
||||
/**
|
||||
* This label is exclusively used for accessibility, remaining hidden from the page.
|
||||
* It's read by screen readers when the calendar is opened. The current month and year
|
||||
* are automatically appended to the label, so you only need to provide the base label.
|
||||
*
|
||||
* For instance:
|
||||
* - 'Date of birth' will be read as 'Date of birth, January 2021' if the current month is January 2021.
|
||||
* - 'Appointment date' will be read as 'Appointment date, January 2021' if the current month is January 2021.
|
||||
* - 'Booking date' will be read as 'Booking date, January 2021' if the current month is January 2021.
|
||||
*/
|
||||
calendarLabel?: string;
|
||||
|
||||
/**
|
||||
* The default locale setting.
|
||||
*
|
||||
* @default 'en'
|
||||
*/
|
||||
locale?: string;
|
||||
dateOrder?: DatiDateOrder;
|
||||
|
||||
/**
|
||||
* Whether the calendar is readonly. When true, the user will be able
|
||||
* to focus and navigate the calendar, but will not be able to select
|
||||
* dates. @see disabled for a similar prop that prevents focusing
|
||||
* and selecting dates.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
readonly?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to disable the selection of days outside the current month. By default,
|
||||
* days outside the current month are rendered to fill the calendar grid, but they
|
||||
* are not selectable. Setting this prop to `true` will disable this behavior.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableDaysOutsideMonth?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to automatically reset the range if any date within the selected range
|
||||
* becomes disabled. When true, the entire range will be cleared if a disabled
|
||||
* date is found between the start and end dates.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
excludeDisabled?: 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 format of the month names in the calendar.
|
||||
*
|
||||
* @default "long"
|
||||
*/
|
||||
monthFormat?: Intl.DateTimeFormatOptions["month"] | ((month: number) => string);
|
||||
|
||||
/**
|
||||
* The format of the year names in the calendar.
|
||||
*
|
||||
* @default "numeric"
|
||||
*/
|
||||
yearFormat?: Intl.DateTimeFormatOptions["year"] | ((year: number) => string);
|
||||
},
|
||||
RangeCalendarRootSnippetProps
|
||||
>;
|
||||
|
||||
export type RangeCalendarRootProps = RangeCalendarRootPropsWithoutHTML &
|
||||
Without<AcelPrimitiveDivAttributes, RangeCalendarRootPropsWithoutHTML>;
|
||||
|
||||
export type {
|
||||
CalendarPrevButtonProps as RangeCalendarPrevButtonProps,
|
||||
CalendarPrevButtonPropsWithoutHTML as RangeCalendarPrevButtonPropsWithoutHTML,
|
||||
CalendarNextButtonProps as RangeCalendarNextButtonProps,
|
||||
CalendarNextButtonPropsWithoutHTML as RangeCalendarNextButtonPropsWithoutHTML,
|
||||
CalendarHeadingProps as RangeCalendarHeadingProps,
|
||||
CalendarHeadingPropsWithoutHTML as RangeCalendarHeadingPropsWithoutHTML,
|
||||
CalendarGridProps as RangeCalendarGridProps,
|
||||
CalendarGridPropsWithoutHTML as RangeCalendarGridPropsWithoutHTML,
|
||||
CalendarCellProps as RangeCalendarCellProps,
|
||||
CalendarCellPropsWithoutHTML as RangeCalendarCellPropsWithoutHTML,
|
||||
CalendarDayProps as RangeCalendarDayProps,
|
||||
CalendarDayPropsWithoutHTML as RangeCalendarDayPropsWithoutHTML,
|
||||
CalendarGridBodyProps as RangeCalendarGridBodyProps,
|
||||
CalendarGridBodyPropsWithoutHTML as RangeCalendarGridBodyPropsWithoutHTML,
|
||||
CalendarGridHeadProps as RangeCalendarGridHeadProps,
|
||||
CalendarGridHeadPropsWithoutHTML as RangeCalendarGridHeadPropsWithoutHTML,
|
||||
CalendarGridRowProps as RangeCalendarGridRowProps,
|
||||
CalendarGridRowPropsWithoutHTML as RangeCalendarGridRowPropsWithoutHTML,
|
||||
CalendarHeadCellProps as RangeCalendarHeadCellProps,
|
||||
CalendarHeadCellPropsWithoutHTML as RangeCalendarHeadCellPropsWithoutHTML,
|
||||
CalendarHeaderProps as RangeCalendarHeaderProps,
|
||||
CalendarHeaderPropsWithoutHTML as RangeCalendarHeaderPropsWithoutHTML,
|
||||
CalendarMonthSelectProps as RangeCalendarMonthSelectProps,
|
||||
CalendarMonthSelectPropsWithoutHTML as RangeCalendarMonthSelectPropsWithoutHTML,
|
||||
CalendarYearSelectProps as RangeCalendarYearSelectProps,
|
||||
CalendarYearSelectPropsWithoutHTML as RangeCalendarYearSelectPropsWithoutHTML,
|
||||
} from "../calendar/types.js";
|
||||
Loading…
Reference in new issue