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,10 @@
<script lang="ts">
import { TimeFieldHiddenInputState } from "../time-field.svelte.js";
import HiddenInput from "../../utilities/hidden-input.svelte";
const hiddenInputState = TimeFieldHiddenInputState.create();
</script>
{#if hiddenInputState.shouldRender}
<HiddenInput {...hiddenInputState.props} />
{/if}
@@ -0,0 +1,18 @@
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
$$bindings?: Bindings;
} & Exports;
(internal: unknown, props: {
$$events?: Events;
$$slots?: Slots;
}): Exports & {
$set?: any;
$on?: any;
};
z_$$bindings?: Bindings;
}
declare const TimeFieldHiddenInput: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
[evt: string]: CustomEvent<any>;
}, {}, {}, string>;
type TimeFieldHiddenInput = InstanceType<typeof TimeFieldHiddenInput>;
export default TimeFieldHiddenInput;
@@ -0,0 +1,39 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import { TimeFieldInputState } from "../time-field.svelte.js";
import type { TimeFieldInputProps } from "../types.js";
import DateFieldHiddenInput from "./time-field-hidden-input.svelte";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
name = "",
children,
child,
...restProps
}: TimeFieldInputProps = $props();
const inputState = TimeFieldInputState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
name: boxWith(() => name),
});
const mergedProps = $derived(mergeProps(restProps, inputState.props));
</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 { TimeFieldInputProps } from "../types.js";
declare const TimeFieldInput: import("svelte").Component<TimeFieldInputProps, {}, "ref">;
type TimeFieldInput = ReturnType<typeof TimeFieldInput>;
export default TimeFieldInput;
@@ -0,0 +1,34 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import { TimeFieldLabelState } from "../time-field.svelte.js";
import type { TimeFieldLabelProps } from "../types.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
children,
child,
...restProps
}: TimeFieldLabelProps = $props();
const labelState = TimeFieldLabelState.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}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
@@ -0,0 +1,4 @@
import type { TimeFieldLabelProps } from "../types.js";
declare const TimeFieldLabel: import("svelte").Component<TimeFieldLabelProps, {}, "ref">;
type TimeFieldLabel = ReturnType<typeof TimeFieldLabel>;
export default TimeFieldLabel;
@@ -0,0 +1,37 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import { DateFieldSegmentState } from "../time-field.svelte.js";
import type { TimeFieldSegmentProps } from "../types.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
children,
child,
part,
...restProps
}: TimeFieldSegmentProps = $props();
const segmentState = DateFieldSegmentState.create(part, {
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(
mergeProps(restProps, segmentState.props as Record<string, unknown>)
);
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<span {...mergedProps}>
{@render children?.()}
</span>
{/if}
@@ -0,0 +1,4 @@
import type { TimeFieldSegmentProps } from "../types.js";
declare const TimeFieldSegment: import("svelte").Component<TimeFieldSegmentProps, {}, "ref">;
type TimeFieldSegment = ReturnType<typeof TimeFieldSegment>;
export default TimeFieldSegment;
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts" module>
import type { TimeValue } from "../../../shared/date/types.js";
import type { Time } from "@internationalized/date";
type T = unknown;
</script>
<script lang="ts" generics="T extends TimeValue = Time">
import { watch } from "runed";
import { boxWith } from "svelte-toolbelt";
import { TimeFieldRootState } from "../time-field.svelte.js";
import type { TimeFieldRootProps } from "../types.js";
import { noop } from "../../../internal/noop.js";
import { getDefaultTime } from "../../../internal/date-time/utils.js";
import { resolveLocaleProp } from "../../utilities/config/prop-resolvers.js";
let {
disabled = false,
granularity,
hideTimeZone = false,
hourCycle,
locale,
maxValue,
minValue,
onPlaceholderChange = noop,
onValueChange = noop,
validate = noop,
onInvalid = noop,
placeholder = $bindable(),
value = $bindable(),
readonly = false,
readonlySegments = [],
required = false,
errorMessageId,
children,
}: TimeFieldRootProps<T> = $props();
function handleDefaultPlaceholder() {
if (placeholder !== undefined) return;
const defaultPlaceholder = getDefaultTime({
granularity,
defaultValue: value,
});
placeholder = defaultPlaceholder;
}
// SSR
handleDefaultPlaceholder();
/**
* Covers an edge case where when a spread props object is reassigned,
* the props are reset to their default values, which would make placeholder
* undefined which causes errors to be thrown.
*/
watch.pre(
() => placeholder,
() => {
handleDefaultPlaceholder();
}
);
TimeFieldRootState.create({
value: boxWith(
() => value,
(v) => {
value = v;
onValueChange(v);
}
),
placeholder: boxWith(
() => placeholder as TimeValue,
(v) => {
placeholder = v;
onPlaceholderChange(v);
}
),
disabled: boxWith(() => disabled),
granularity: boxWith(() => granularity),
hideTimeZone: boxWith(() => hideTimeZone),
hourCycle: boxWith(() => hourCycle),
locale: resolveLocaleProp(() => locale),
maxValue: boxWith(() => maxValue),
minValue: boxWith(() => minValue),
validate: boxWith(() => validate),
readonly: boxWith(() => readonly),
readonlySegments: boxWith(() => readonlySegments),
required: boxWith(() => required),
onInvalid: boxWith(() => onInvalid),
errorMessageId: boxWith(() => errorMessageId),
isInvalidProp: boxWith(() => undefined),
});
</script>
{@render children?.()}
@@ -0,0 +1,20 @@
import type { TimeValue } from "../../../shared/date/types.js";
import type { Time } from "@internationalized/date";
import type { TimeFieldRootProps } from "../types.js";
declare class __sveltets_Render<T extends TimeValue = Time> {
props(): TimeFieldRootProps<T>;
events(): {};
slots(): {};
bindings(): "value" | "placeholder";
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends TimeValue = Time>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends TimeValue = Time>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const TimeField: $$IsomorphicComponent;
type TimeField<T extends TimeValue = Time> = InstanceType<typeof TimeField<T>>;
export default TimeField;
+5
View File
@@ -0,0 +1,5 @@
export { default as Root } from "./components/time-field.svelte";
export { default as Input } from "./components/time-field-input.svelte";
export { default as Label } from "./components/time-field-label.svelte";
export { default as Segment } from "./components/time-field-segment.svelte";
export type { TimeFieldRootProps as RootProps, TimeFieldInputProps as InputProps, TimeFieldLabelProps as LabelProps, TimeFieldSegmentProps as SegmentProps, } from "./types.js";
+4
View File
@@ -0,0 +1,4 @@
export { default as Root } from "./components/time-field.svelte";
export { default as Input } from "./components/time-field-input.svelte";
export { default as Label } from "./components/time-field-label.svelte";
export { default as Segment } from "./components/time-field-segment.svelte";
+1
View File
@@ -0,0 +1 @@
export * as TimeField from "./exports.js";
+1
View File
@@ -0,0 +1 @@
export * as TimeField from "./exports.js";
+427
View File
@@ -0,0 +1,427 @@
import type { Updater } from "svelte/store";
import { Time } from "@internationalized/date";
import { type WritableBox, DOMContext, type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
import type { TimeSegmentObj, SegmentPart, HourCycle, TimeValidator, TimeOnInvalid, EditableTimeSegmentPart } from "../../shared/date/types.js";
import { type TimeFormatter } from "../../internal/date-time/formatter.js";
import { type Announcer } from "../../internal/date-time/announcer.js";
import type { TimeValue } from "../../shared/date/types.js";
import type { TimeRangeFieldRootState } from "../time-range-field/time-range-field.svelte.js";
export declare const timeFieldAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["input", "label"]>;
interface SegmentConfig {
min: number | ((root: TimeFieldRootState) => number);
max: number | ((root: TimeFieldRootState) => number);
cycle: number;
canBeZero?: boolean;
padZero?: boolean;
}
export interface TimeFieldRootStateOpts<T extends TimeValue = Time> extends WritableBoxedValues<{
value: T | undefined;
placeholder: TimeValue;
}>, ReadableBoxedValues<{
readonlySegments: SegmentPart[];
validate: TimeValidator<T> | undefined;
onInvalid: TimeOnInvalid | undefined;
minValue: TimeValue | undefined;
maxValue: TimeValue | undefined;
disabled: boolean;
readonly: boolean;
granularity: "hour" | "minute" | "second" | undefined;
hourCycle: HourCycle | undefined;
locale: string;
hideTimeZone: boolean;
required: boolean;
errorMessageId: string | undefined;
isInvalidProp: boolean | undefined;
}> {
}
export declare class TimeFieldRootState<T extends TimeValue = Time> {
#private;
static create<T extends TimeValue = Time>(opts: TimeFieldRootStateOpts<T>, rangeRoot?: TimeRangeFieldRootState<T>): TimeFieldRootState<Time>;
value: TimeFieldRootStateOpts<T>["value"];
placeholder: WritableBox<TimeValue>;
validate: TimeFieldRootStateOpts<T>["validate"];
minValue: TimeFieldRootStateOpts<T>["minValue"];
maxValue: TimeFieldRootStateOpts<T>["maxValue"];
disabled: TimeFieldRootStateOpts<T>["disabled"];
readonly: TimeFieldRootStateOpts<T>["readonly"];
granularity: TimeFieldRootStateOpts<T>["granularity"];
readonlySegments: TimeFieldRootStateOpts<T>["readonlySegments"];
hourCycleProp: TimeFieldRootStateOpts<T>["hourCycle"];
locale: TimeFieldRootStateOpts<T>["locale"];
hideTimeZone: TimeFieldRootStateOpts<T>["hideTimeZone"];
required: TimeFieldRootStateOpts<T>["required"];
onInvalid: TimeFieldRootStateOpts<T>["onInvalid"];
errorMessageId: TimeFieldRootStateOpts<T>["errorMessageId"];
isInvalidProp: TimeFieldRootStateOpts<T>["isInvalidProp"];
descriptionId: string;
formatter: TimeFormatter;
initialSegments: TimeSegmentObj;
segmentValues: TimeSegmentObj;
announcer: Announcer;
readonly readonlySegmentsSet: Set<SegmentPart>;
segmentStates: import("../../shared/date/types.js").TimeSegmentStateMap;
descriptionNode: HTMLElement | null;
validationNode: HTMLElement | null;
states: import("../../shared/date/types.js").TimeSegmentStateMap;
dayPeriodNode: HTMLElement | null;
name: string;
readonly maxValueTime: Time | undefined;
readonly minValueTime: Time | undefined;
readonly valueTime: Time | undefined;
readonly hourCycle: HourCycle;
readonly rangeRoot: TimeRangeFieldRootState<T> | undefined;
domContext: DOMContext;
constructor(props: TimeFieldRootStateOpts<T>, rangeRoot?: TimeRangeFieldRootState<T>);
setName(name: string): void;
setFieldNode(node: HTMLElement | null): void;
/**
* Gets the correct field node for the time field regardless of whether it's being
* used in a standalone context or within a `TimeRangeField` component.
*/
getFieldNode(): HTMLElement | null;
setLabelNode(node: HTMLElement | null): void;
getLabelNode(): HTMLElement | null;
setValue(value: T | undefined): void;
syncSegmentValues(value: TimeValue): void;
validationStatus: false | {
readonly reason: "custom";
readonly message: string | string[];
} | {
readonly reason: "min";
readonly message?: undefined;
} | {
readonly reason: "max";
readonly message?: undefined;
};
isInvalid: boolean;
inferredGranularity: "hour" | "minute" | "second";
timeRef: T;
allSegmentContent: {
obj: import("../../shared/date/types.js").TimeSegmentContentObj;
arr: {
part: import("../../internal/date-time/field/types.js").TimeSegmentPart;
value: string;
}[];
};
segmentContents: {
part: import("../../internal/date-time/field/types.js").TimeSegmentPart;
value: string;
}[];
sharedSegmentAttrs: {
role: string;
contenteditable: string;
tabindex: number;
spellcheck: boolean;
inputmode: string;
autocorrect: string;
enterkeyhint: string;
style: {
caretColor: string;
};
};
updateSegment<T extends EditableTimeSegmentPart>(part: T, cb: Updater<TimeSegmentObj[T]>): void;
handleSegmentClick(e: BitsMouseEvent): void;
getBaseSegmentAttrs(part: SegmentPart, segmentId: string): {
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
} | {
"aria-labelledby": string;
contenteditable: string | undefined;
"aria-describedby": string | undefined;
tabindex: number | undefined;
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
};
}
interface TimeFieldInputStateOpts extends WithRefOpts, ReadableBoxedValues<{
name: string;
}> {
}
export declare class TimeFieldInputState {
#private;
static create(opts: TimeFieldInputStateOpts): TimeFieldInputState;
readonly opts: TimeFieldInputStateOpts;
readonly root: TimeFieldRootState;
readonly attachment: RefAttachment;
domContext: DOMContext;
constructor(opts: TimeFieldInputStateOpts, root: TimeFieldRootState);
readonly props: {
readonly id: string;
readonly role: "group";
readonly "aria-labelledby": string | undefined;
readonly "aria-describedby": string | undefined;
readonly "aria-disabled": "true" | "false";
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
};
}
export declare class TimeFieldHiddenInputState {
static create(): TimeFieldHiddenInputState;
readonly root: TimeFieldRootState;
readonly shouldRender: boolean;
readonly isoValue: string | undefined;
constructor(root: TimeFieldRootState);
readonly props: {
readonly name: string;
readonly value: string | undefined;
readonly required: boolean;
};
}
interface TimeFieldLabelStateOpts extends WithRefOpts {
}
export declare class TimeFieldLabelState {
static create(opts: TimeFieldLabelStateOpts): TimeFieldLabelState;
readonly opts: TimeFieldLabelStateOpts;
readonly root: TimeFieldRootState;
readonly attachment: RefAttachment;
constructor(opts: TimeFieldLabelStateOpts, root: TimeFieldRootState);
onclick(_: BitsMouseEvent): void;
readonly props: {
readonly id: string;
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly onclick: (_: BitsMouseEvent) => void;
};
}
declare abstract class BaseTimeSegmentState {
#private;
readonly opts: WithRefOpts;
readonly root: TimeFieldRootState;
readonly announcer: Announcer;
readonly part: string;
readonly config: SegmentConfig;
readonly attachment: RefAttachment;
constructor(opts: WithRefOpts, root: TimeFieldRootState, part: string, config: SegmentConfig);
onkeydown(e: BitsKeyboardEvent): void;
onfocusout(_: BitsFocusEvent): void;
getSegmentProps(): {
"aria-label": string;
"aria-valuemin": number;
"aria-valuemax": number;
"aria-valuenow": number;
"aria-valuetext": string;
};
props: {
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
onkeydown: (e: BitsKeyboardEvent) => void;
onfocusout: (_: BitsFocusEvent) => void;
onclick: (e: BitsMouseEvent) => void;
"aria-label": string;
"aria-valuemin": number;
"aria-valuemax": number;
"aria-valuenow": number;
"aria-valuetext": string;
id: string;
role: string;
contenteditable: string;
tabindex: number;
spellcheck: boolean;
inputmode: string;
autocorrect: string;
enterkeyhint: string;
style: {
caretColor: string;
};
} | {
"aria-labelledby": string;
contenteditable: string | undefined;
"aria-describedby": string | undefined;
tabindex: number | undefined;
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
onkeydown: (e: BitsKeyboardEvent) => void;
onfocusout: (_: BitsFocusEvent) => void;
onclick: (e: BitsMouseEvent) => void;
"aria-label": string;
"aria-valuemin": number;
"aria-valuemax": number;
"aria-valuenow": number;
"aria-valuetext": string;
id: string;
role: string;
spellcheck: boolean;
inputmode: string;
autocorrect: string;
enterkeyhint: string;
style: {
caretColor: string;
};
};
}
declare class TimeFieldHourSegmentState extends BaseTimeSegmentState {
constructor(opts: WithRefOpts, root: TimeFieldRootState);
onkeydown(e: BitsKeyboardEvent): void;
}
declare class TimeFieldMinuteSegmentState extends BaseTimeSegmentState {
constructor(opts: WithRefOpts, root: TimeFieldRootState);
}
declare class TimeFieldSecondSegmentState extends BaseTimeSegmentState {
constructor(opts: WithRefOpts, root: TimeFieldRootState);
}
interface TimeFieldDayPeriodSegmentStateOpts extends WithRefOpts {
}
declare class TimeFieldDayPeriodSegmentState {
#private;
readonly opts: TimeFieldDayPeriodSegmentStateOpts;
readonly root: TimeFieldRootState;
readonly attachment: RefAttachment;
constructor(opts: TimeFieldDayPeriodSegmentStateOpts, root: TimeFieldRootState);
onkeydown(e: BitsKeyboardEvent): void;
readonly props: {
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
id: string;
inputmode: string;
"aria-label": string;
"aria-valuemin": number;
"aria-valuemax": number;
"aria-valuenow": number;
"aria-valuetext": string;
onkeydown: (e: BitsKeyboardEvent) => void;
onclick: (e: BitsMouseEvent) => void;
role: string;
contenteditable: string;
tabindex: number;
spellcheck: boolean;
autocorrect: string;
enterkeyhint: string;
style: {
caretColor: string;
};
} | {
"aria-labelledby": string;
contenteditable: string | undefined;
"aria-describedby": string | undefined;
tabindex: number | undefined;
"aria-invalid": "true" | undefined;
"aria-disabled": "true" | "false";
"aria-readonly": "true" | "false";
"data-invalid": "" | undefined;
"data-disabled": "" | undefined;
"data-readonly": "" | undefined;
"data-segment": string;
id: string;
inputmode: string;
"aria-label": string;
"aria-valuemin": number;
"aria-valuemax": number;
"aria-valuenow": number;
"aria-valuetext": string;
onkeydown: (e: BitsKeyboardEvent) => void;
onclick: (e: BitsMouseEvent) => void;
role: string;
spellcheck: boolean;
autocorrect: string;
enterkeyhint: string;
style: {
caretColor: string;
};
} | undefined;
}
interface TimeFieldLiteralSegmentStateOpts extends WithRefOpts {
}
declare class TimeFieldLiteralSegmentState {
readonly opts: TimeFieldLiteralSegmentStateOpts;
readonly root: TimeFieldRootState;
readonly attachment: RefAttachment;
constructor(opts: TimeFieldLiteralSegmentStateOpts, root: TimeFieldRootState);
readonly props: {
readonly "aria-invalid": "true" | undefined;
readonly "aria-disabled": "true" | "false";
readonly "aria-readonly": "true" | "false";
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly "data-readonly": "" | undefined;
readonly "data-segment": string;
readonly id: string;
readonly "aria-hidden": "true" | undefined;
} | {
readonly "aria-labelledby": string;
readonly contenteditable: string | undefined;
readonly "aria-describedby": string | undefined;
readonly tabindex: number | undefined;
readonly "aria-invalid": "true" | undefined;
readonly "aria-disabled": "true" | "false";
readonly "aria-readonly": "true" | "false";
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly "data-readonly": "" | undefined;
readonly "data-segment": string;
readonly id: string;
readonly "aria-hidden": "true" | undefined;
};
}
declare class TimeFieldTimeZoneSegmentState {
readonly opts: TimeFieldLiteralSegmentStateOpts;
readonly root: TimeFieldRootState;
readonly attachment: RefAttachment;
constructor(opts: TimeFieldLiteralSegmentStateOpts, root: TimeFieldRootState);
onkeydown(e: BitsKeyboardEvent): void;
readonly props: {
readonly "data-readonly": "" | undefined;
readonly "aria-invalid": "true" | undefined;
readonly "aria-disabled": "true" | "false";
readonly "aria-readonly": "true" | "false";
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly "data-segment": string;
readonly role: "textbox";
readonly id: string;
readonly "aria-label": "timezone, ";
readonly style: {
readonly caretColor: "transparent";
};
readonly onkeydown: (e: BitsKeyboardEvent) => void;
readonly tabindex: 0;
} | {
readonly "data-readonly": "" | undefined;
readonly "aria-labelledby": string;
readonly contenteditable: string | undefined;
readonly "aria-describedby": string | undefined;
readonly tabindex: number | undefined;
readonly "aria-invalid": "true" | undefined;
readonly "aria-disabled": "true" | "false";
readonly "aria-readonly": "true" | "false";
readonly "data-invalid": "" | undefined;
readonly "data-disabled": "" | undefined;
readonly "data-segment": string;
readonly role: "textbox";
readonly id: string;
readonly "aria-label": "timezone, ";
readonly style: {
readonly caretColor: "transparent";
};
readonly onkeydown: (e: BitsKeyboardEvent) => void;
};
}
export declare class DateFieldSegmentState {
static create(part: SegmentPart, opts: WithRefOpts): TimeFieldHourSegmentState | TimeFieldMinuteSegmentState | TimeFieldSecondSegmentState | TimeFieldDayPeriodSegmentState | TimeFieldLiteralSegmentState | TimeFieldTimeZoneSegmentState;
}
export {};
+989
View File
@@ -0,0 +1,989 @@
import { CalendarDateTime, Time, ZonedDateTime } from "@internationalized/date";
import { onDestroyEffect, attachRef, DOMContext, simpleBox, } from "svelte-toolbelt";
import { onMount, untrack } from "svelte";
import { Context, watch } from "runed";
import { createBitsAttrs, boolToStr, boolToStrTrueOrUndef, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
import { isBrowser, isNumberString } from "../../internal/is.js";
import { kbd } from "../../internal/kbd.js";
import { useId } from "../../internal/use-id.js";
import { createTimeFormatter } from "../../internal/date-time/formatter.js";
import { getAnnouncer } from "../../internal/date-time/announcer.js";
import { EDITABLE_TIME_SEGMENT_PARTS } from "../../internal/date-time/field/parts.js";
import { toDate } from "../../internal/date-time/utils.js";
import { areAllTimeSegmentsFilled, convertTimeValueToTime, createTimeContent, getISOTimeValue, getTimeValueFromSegments, initTimeSegmentStates, isFirstTimeSegment, isTimeBefore, removeTimeDescriptionElement, setTimeDescription, } from "../../internal/date-time/field/time-helpers.js";
import { getFirstTimeSegment, handleTimeSegmentNavigation, isSegmentNavigationKey, moveToNextTimeSegment, moveToPrevTimeSegment, } from "../../internal/date-time/field/segments.js";
import { getDefaultHourCycle, isAcceptableSegmentKey, } from "../../internal/date-time/field/helpers.js";
export const timeFieldAttrs = createBitsAttrs({
component: "time-field",
parts: ["input", "label"],
});
const TimeFieldRootContext = new Context("TimeField.Root");
const SEGMENT_CONFIGS = {
hour: {
min: (root) => (root.hourCycle === 12 ? 1 : 0),
max: (root) => {
if (root.hourCycle === 24)
return 23;
if ("dayPeriod" in root.segmentValues && root.segmentValues.dayPeriod !== null)
return 12;
return 23;
},
cycle: 1,
canBeZero: true,
padZero: true,
},
minute: {
min: 0,
max: 59,
cycle: 1,
canBeZero: true,
padZero: true,
},
second: {
min: 0,
max: 59,
cycle: 1,
canBeZero: true,
padZero: true,
},
};
export class TimeFieldRootState {
static create(opts, rangeRoot) {
return TimeFieldRootContext.set(new TimeFieldRootState(opts, rangeRoot));
}
value;
placeholder;
validate;
minValue;
maxValue;
disabled;
readonly;
granularity;
readonlySegments;
hourCycleProp;
locale;
hideTimeZone;
required;
onInvalid;
errorMessageId;
isInvalidProp;
descriptionId = useId();
formatter;
initialSegments;
segmentValues = $state();
announcer;
readonlySegmentsSet = $derived.by(() => new Set(this.readonlySegments.current));
segmentStates = initTimeSegmentStates();
#fieldNode = $state(null);
#labelNode = $state(null);
descriptionNode = $state(null);
validationNode = $state(null);
states = initTimeSegmentStates();
dayPeriodNode = $state(null);
name = $state("");
maxValueTime = $derived.by(() => {
if (!this.maxValue.current)
return undefined;
return convertTimeValueToTime(this.maxValue.current);
});
minValueTime = $derived.by(() => {
if (!this.minValue.current)
return undefined;
return convertTimeValueToTime(this.minValue.current);
});
valueTime = $derived.by(() => {
if (!this.value.current)
return undefined;
return convertTimeValueToTime(this.value.current);
});
hourCycle = $derived.by(() => {
if (this.hourCycleProp.current)
return this.hourCycleProp.current;
return getDefaultHourCycle(this.locale.current);
});
rangeRoot = undefined;
domContext = new DOMContext(() => null);
constructor(props, rangeRoot) {
this.rangeRoot = rangeRoot;
/**
* Since the `TimeFieldRootState` can be used in two contexts, as a standalone
* field or as a field within a `TimeRangeField` component, we handle assigning
* the props based on that context.
*/
this.value = props.value;
this.placeholder = rangeRoot ? rangeRoot.opts.placeholder : props.placeholder;
this.validate = rangeRoot ? simpleBox(undefined) : props.validate;
this.minValue = rangeRoot ? rangeRoot.opts.minValue : props.minValue;
this.maxValue = rangeRoot ? rangeRoot.opts.maxValue : props.maxValue;
this.disabled = rangeRoot ? rangeRoot.opts.disabled : props.disabled;
this.readonly = rangeRoot ? rangeRoot.opts.readonly : props.readonly;
this.granularity = rangeRoot ? rangeRoot.opts.granularity : props.granularity;
this.readonlySegments = rangeRoot
? rangeRoot.opts.readonlySegments
: props.readonlySegments;
this.hourCycleProp = rangeRoot ? rangeRoot.opts.hourCycle : props.hourCycle;
this.locale = rangeRoot ? rangeRoot.opts.locale : props.locale;
this.hideTimeZone = rangeRoot ? rangeRoot.opts.hideTimeZone : props.hideTimeZone;
this.required = rangeRoot ? rangeRoot.opts.required : props.required;
this.onInvalid = rangeRoot ? rangeRoot.opts.onInvalid : props.onInvalid;
this.errorMessageId = rangeRoot ? rangeRoot.opts.errorMessageId : props.errorMessageId;
this.isInvalidProp = props.isInvalidProp;
this.formatter = createTimeFormatter(this.locale.current);
this.initialSegments = this.#initializeTimeSegmentValues();
this.segmentValues = this.initialSegments;
this.announcer = getAnnouncer(null);
this.getFieldNode = this.getFieldNode.bind(this);
this.updateSegment = this.updateSegment.bind(this);
this.handleSegmentClick = this.handleSegmentClick.bind(this);
this.getBaseSegmentAttrs = this.getBaseSegmentAttrs.bind(this);
$effect(() => {
untrack(() => {
this.initialSegments = this.#initializeTimeSegmentValues();
});
});
onMount(() => {
this.announcer = getAnnouncer(this.domContext.getDocument());
});
onDestroyEffect(() => {
removeTimeDescriptionElement(this.descriptionId, this.domContext.getDocument());
});
$effect(() => {
if (this.formatter.getLocale() === this.locale.current)
return;
this.formatter.setLocale(this.locale.current);
});
$effect(() => {
if (this.value.current) {
const descriptionId = untrack(() => this.descriptionId);
setTimeDescription({
id: descriptionId,
formatter: this.formatter,
value: this.#toDateValue(this.value.current),
doc: this.domContext.getDocument(),
});
}
const placeholder = untrack(() => this.placeholder.current);
if (this.value.current && placeholder !== this.value.current) {
untrack(() => {
if (this.value.current) {
this.placeholder.current = this.value.current;
}
});
}
});
if (this.value.current) {
this.syncSegmentValues(this.value.current);
}
$effect(() => {
this.locale.current;
if (this.value.current) {
this.syncSegmentValues(this.value.current);
}
this.#clearUpdating();
});
$effect(() => {
if (this.value.current === undefined) {
this.segmentValues = this.#initializeTimeSegmentValues();
}
});
watch(() => this.validationStatus, () => {
if (this.validationStatus !== false) {
this.onInvalid.current?.(this.validationStatus.reason, this.validationStatus.message);
}
});
}
#initializeTimeSegmentValues() {
const granularity = this.inferredGranularity;
const segments = {
hour: null,
minute: null,
second: null,
dayPeriod: "AM",
};
if (granularity === "second") {
segments.second = null;
}
if (this.hourCycle === 24) {
segments.dayPeriod = null;
}
return segments;
}
#toDateValue(timeValue) {
if ("calendar" in timeValue) {
// CalendarDateTime or ZonedDateTime
return timeValue;
}
else {
return new CalendarDateTime(2000, 1, 1, timeValue.hour, timeValue.minute, timeValue.second, timeValue.millisecond);
}
}
#clearUpdating() {
this.states.hour.updating = null;
this.states.minute.updating = null;
this.states.second.updating = null;
this.states.dayPeriod.updating = null;
}
setName(name) {
this.name = name;
}
setFieldNode(node) {
this.#fieldNode = node;
}
/**
* Gets the correct field node for the time field regardless of whether it's being
* used in a standalone context or within a `TimeRangeField` component.
*/
getFieldNode() {
/** If we're not within a TimeRangeField, we return this field. */
if (!this.rangeRoot) {
return this.#fieldNode;
}
else {
/**
* Otherwise, we return the rangeRoot's field node which
* contains both start and end fields.
*/
return this.rangeRoot.fieldNode;
}
}
setLabelNode(node) {
this.#labelNode = node;
}
getLabelNode() {
return this.#labelNode;
}
setValue(value) {
this.value.current = value;
}
syncSegmentValues(value) {
const timeValues = EDITABLE_TIME_SEGMENT_PARTS.map((part) => {
if (part === "dayPeriod") {
if (this.states.dayPeriod.updating) {
return [part, this.states.dayPeriod.updating];
}
else {
return [part, this.formatter.dayPeriod(toDate(this.#toDateValue(value)))];
}
}
else if (part === "hour") {
if (this.states.hour.updating) {
return [part, this.states.hour.updating];
}
if (value[part] !== undefined && value[part] < 10) {
return [part, `0${value[part]}`];
}
if (value[part] === 0 && this.dayPeriodNode) {
return [part, "12"];
}
}
else if (part === "minute") {
if (this.states.minute.updating) {
return [part, this.states.minute.updating];
}
if (value[part] !== undefined && value[part] < 10) {
return [part, `0${value[part]}`];
}
}
else if (part === "second") {
if (this.states.second.updating) {
return [part, this.states.second.updating];
}
if (value[part] !== undefined && value[part] < 10) {
return [part, `0${value[part]}`];
}
}
return [part, `${value[part]}`];
});
this.segmentValues = Object.fromEntries(timeValues);
this.#clearUpdating();
}
validationStatus = $derived.by(() => {
const value = this.value.current;
if (!value)
return false;
const msg = this.validate.current?.(value);
if (msg) {
return {
reason: "custom",
message: msg,
};
}
if (!this.valueTime)
return false;
if (this.minValueTime && isTimeBefore(this.valueTime, this.minValueTime)) {
return {
reason: "min",
};
}
if (this.maxValueTime && isTimeBefore(this.maxValueTime, this.valueTime)) {
return {
reason: "max",
};
}
return false;
});
isInvalid = $derived.by(() => {
if (this.validationStatus === false)
return false;
if (this.isInvalidProp.current)
return true;
return true;
});
inferredGranularity = $derived.by(() => {
return this.granularity.current ?? "minute";
});
timeRef = $derived.by(() => this.value.current ?? this.placeholder.current);
allSegmentContent = $derived.by(() => createTimeContent({
segmentValues: this.segmentValues,
formatter: this.formatter,
locale: this.locale.current,
granularity: this.inferredGranularity,
timeRef: this.timeRef,
hideTimeZone: this.hideTimeZone.current,
hourCycle: this.hourCycle,
}));
segmentContents = $derived.by(() => this.allSegmentContent.arr);
sharedSegmentAttrs = {
role: "spinbutton",
contenteditable: "true",
tabindex: 0,
spellcheck: false,
inputmode: "numeric",
autocorrect: "off",
enterkeyhint: "next",
style: {
caretColor: "transparent",
},
};
#getLabelledBy(segmentId) {
return `${segmentId} ${this.getLabelNode()?.id ?? ""}`;
}
updateSegment(part, cb) {
const disabled = this.disabled.current;
const readonly = this.readonly.current;
const readonlySegmentsSet = this.readonlySegmentsSet;
if (disabled || readonly || readonlySegmentsSet.has(part))
return;
const prev = this.segmentValues;
let newSegmentValues = prev;
if (part === "dayPeriod") {
const next = cb(prev[part]);
this.states.dayPeriod.updating = next;
const value = this.value.current;
if (value && "hour" in value) {
const trueHour = value.hour;
if (next === "AM") {
if (trueHour >= 12) {
prev.hour = `${trueHour - 12}`;
}
}
else if (next === "PM") {
if (trueHour < 12) {
prev.hour = `${trueHour + 12}`;
}
}
}
newSegmentValues = { ...prev, [part]: next };
}
else if (part === "hour") {
const next = cb(prev[part]);
this.states.hour.updating = next;
if (next !== null && prev.dayPeriod !== null) {
const dayPeriod = this.formatter.dayPeriod(toDate(this.#toDateValue(this.timeRef.set({ hour: Number.parseInt(next) }))), this.hourCycle);
if (dayPeriod === "AM" || dayPeriod === "PM") {
prev.dayPeriod = dayPeriod;
}
}
newSegmentValues = { ...prev, [part]: next };
}
else if (part === "minute") {
const next = cb(prev[part]);
this.states.minute.updating = next;
newSegmentValues = { ...prev, [part]: next };
}
else if (part === "second") {
const next = cb(prev[part]);
this.states.second.updating = next;
newSegmentValues = { ...prev, [part]: next };
}
this.segmentValues = newSegmentValues;
if (areAllTimeSegmentsFilled(newSegmentValues, this.#fieldNode)) {
this.setValue(getTimeValueFromSegments({
segmentObj: newSegmentValues,
fieldNode: this.#fieldNode,
timeRef: this.timeRef,
}));
}
else {
// this.setValue(undefined);
// this.segmentValues = newSegmentValues;
}
}
handleSegmentClick(e) {
if (this.disabled.current) {
e.preventDefault();
}
}
getBaseSegmentAttrs(part, segmentId) {
const inReadonlySegments = this.readonlySegmentsSet.has(part);
const defaultAttrs = {
"aria-invalid": boolToStrTrueOrUndef(this.isInvalid),
"aria-disabled": boolToStr(this.disabled.current),
"aria-readonly": boolToStr(this.readonly.current || inReadonlySegments),
"data-invalid": boolToEmptyStrOrUndef(this.isInvalid),
"data-disabled": boolToEmptyStrOrUndef(this.disabled.current),
"data-readonly": boolToEmptyStrOrUndef(this.readonly.current || inReadonlySegments),
"data-segment": `${part}`,
};
if (part === "literal")
return defaultAttrs;
const descriptionId = this.descriptionNode?.id;
const hasDescription = isFirstTimeSegment(segmentId, this.#fieldNode) && descriptionId;
const errorMsgId = this.errorMessageId?.current;
const describedBy = hasDescription
? `${descriptionId} ${this.isInvalid && errorMsgId ? errorMsgId : ""}`
: undefined;
const contenteditable = !(this.readonly.current ||
inReadonlySegments ||
this.disabled.current);
return {
...defaultAttrs,
"aria-labelledby": this.#getLabelledBy(segmentId),
contenteditable: contenteditable ? "true" : undefined,
"aria-describedby": describedBy,
tabindex: this.disabled.current ? undefined : 0,
};
}
}
export class TimeFieldInputState {
static create(opts) {
return new TimeFieldInputState(opts, TimeFieldRootContext.get());
}
opts;
root;
attachment;
domContext;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(opts.ref, (v) => this.root.setFieldNode(v));
this.domContext = new DOMContext(opts.ref);
this.root.setName(this.opts.name.current);
$effect(() => {
this.root.setName(this.opts.name.current);
});
}
#ariaDescribedBy = $derived.by(() => {
if (!isBrowser)
return undefined;
const doesDescriptionExist = this.domContext.getElementById(this.root.descriptionId);
if (!doesDescriptionExist)
return undefined;
return this.root.descriptionId;
});
props = $derived.by(() => ({
id: this.opts.id.current,
role: "group",
"aria-labelledby": this.root.getLabelNode()?.id ?? undefined,
"aria-describedby": this.#ariaDescribedBy,
"aria-disabled": boolToStr(this.root.disabled.current),
"data-invalid": this.root.isInvalid ? "" : undefined,
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled.current),
[timeFieldAttrs.input]: "",
...this.attachment,
}));
}
export class TimeFieldHiddenInputState {
static create() {
return new TimeFieldHiddenInputState(TimeFieldRootContext.get());
}
root;
shouldRender = $derived.by(() => this.root.name !== "");
isoValue = $derived.by(() => this.root.value.current ? getISOTimeValue(this.root.value.current) : undefined);
constructor(root) {
this.root = root;
}
props = $derived.by(() => ({
name: this.root.name,
value: this.isoValue,
required: this.root.required.current,
}));
}
export class TimeFieldLabelState {
static create(opts) {
return new TimeFieldLabelState(opts, TimeFieldRootContext.get());
}
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(opts.ref, (v) => this.root.setLabelNode(v));
this.onclick = this.onclick.bind(this);
}
onclick(_) {
if (this.root.disabled.current)
return;
const firstSegment = getFirstTimeSegment(this.root.getFieldNode());
if (!firstSegment)
return;
firstSegment.focus();
}
props = $derived.by(() => ({
id: this.opts.id.current,
"data-invalid": boolToEmptyStrOrUndef(this.root.isInvalid),
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled.current),
[timeFieldAttrs.label]: "",
onclick: this.onclick,
...this.attachment,
}));
}
// Base class for time segments - simplified from date-field version
class BaseTimeSegmentState {
opts;
root;
announcer;
part;
config;
attachment;
constructor(opts, root, part, config) {
this.opts = opts;
this.root = root;
this.part = part;
this.config = config;
this.announcer = root.announcer;
this.attachment = attachRef(opts.ref);
this.onkeydown = this.onkeydown.bind(this);
this.onfocusout = this.onfocusout.bind(this);
}
#getMax() {
return typeof this.config.max === "function" ? this.config.max(this.root) : this.config.max;
}
#getMin() {
return typeof this.config.min === "function" ? this.config.min(this.root) : this.config.min;
}
#formatValue(value, forDisplay = true) {
const str = String(value);
if (forDisplay && this.config.padZero && str.length === 1) {
return `0${value}`;
}
return str;
}
onkeydown(e) {
const placeholder = this.root.value.current ?? this.root.placeholder.current;
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
return;
if (e.key !== kbd.TAB)
e.preventDefault();
if (!isAcceptableSegmentKey(e.key))
return;
if (isArrowUp(e.key)) {
this.#handleArrowUp(placeholder);
return;
}
if (isArrowDown(e.key)) {
this.#handleArrowDown(placeholder);
return;
}
if (isNumberString(e.key)) {
this.#handleNumberKey(e);
return;
}
if (isBackspace(e.key)) {
this.#handleBackspace(e);
return;
}
if (isSegmentNavigationKey(e.key)) {
handleTimeSegmentNavigation(e, this.root.getFieldNode());
}
}
#handleArrowUp(placeholder) {
const stateKey = this.part;
if (stateKey in this.root.states) {
this.root.states[stateKey].hasLeftFocus = false;
}
// @ts-expect-error shhh
this.root.updateSegment(this.part, (prev) => {
if (prev === null) {
const next = placeholder[this.part];
this.announcer.announce(String(next));
return this.#formatValue(next);
}
const current = placeholder.set({
[this.part]: Number.parseInt(prev),
});
// @ts-expect-error shhh
const next = current.cycle(this.part, this.config.cycle)[this.part];
this.announcer.announce(String(next));
return this.#formatValue(next);
});
}
#handleArrowDown(placeholder) {
const stateKey = this.part;
if (stateKey in this.root.states) {
this.root.states[stateKey].hasLeftFocus = false;
}
// @ts-expect-error - this is a part
this.root.updateSegment(this.part, (prev) => {
if (prev === null) {
const next = placeholder[this.part];
this.announcer.announce(String(next));
return this.#formatValue(next);
}
const current = placeholder.set({
[this.part]: Number.parseInt(prev),
});
// @ts-expect-error shhh
const next = current.cycle(this.part, -this.config.cycle)[this.part];
this.announcer.announce(String(next));
return this.#formatValue(next);
});
}
#handleNumberKey(e) {
const num = Number.parseInt(e.key);
let moveToNext = false;
const max = this.#getMax();
const maxStart = Math.floor(max / 10);
const numIsZero = num === 0;
const stateKey = this.part;
// @ts-expect-error this is a part
this.root.updateSegment(this.part, (prev) => {
if (stateKey in this.root.states && this.root.states[stateKey].hasLeftFocus) {
prev = null;
this.root.states[stateKey].hasLeftFocus = false;
}
if (prev === null) {
if (numIsZero) {
if (stateKey in this.root.states) {
this.root.states[stateKey].lastKeyZero = true;
}
this.announcer.announce("0");
return "0";
}
if (stateKey in this.root.states &&
(this.root.states[stateKey].lastKeyZero || num > maxStart)) {
moveToNext = true;
}
if (stateKey in this.root.states) {
this.root.states[stateKey].lastKeyZero = false;
}
if (moveToNext && String(num).length === 1) {
this.announcer.announce(num);
return `0${num}`;
}
return `${num}`;
}
if (stateKey in this.root.states && this.root.states[stateKey].lastKeyZero) {
if (num !== 0) {
moveToNext = true;
this.root.states[stateKey].lastKeyZero = false;
return `0${num}`;
}
if (this.part === "hour" && num === 0 && this.root.hourCycle === 24) {
moveToNext = true;
this.root.states[stateKey].lastKeyZero = false;
return `00`;
}
if ((this.part === "minute" || this.part === "second") && num === 0) {
moveToNext = true;
this.root.states[stateKey].lastKeyZero = false;
return "00";
}
return prev;
}
const total = Number.parseInt(prev + num.toString());
if (total > max) {
moveToNext = true;
return `0${num}`;
}
moveToNext = true;
return `${total}`;
});
if (moveToNext) {
moveToNextTimeSegment(e, this.root.getFieldNode());
}
}
#handleBackspace(e) {
const stateKey = this.part;
if (stateKey in this.root.states) {
this.root.states[stateKey].hasLeftFocus = false;
}
let moveToPrev = false;
// @ts-expect-error this is a part
this.root.updateSegment(this.part, (prev) => {
if (prev === null) {
moveToPrev = true;
this.announcer.announce(null);
return null;
}
if (prev.length === 2 && prev.startsWith("0")) {
this.announcer.announce(null);
return null;
}
const str = prev.toString();
if (str.length === 1) {
this.announcer.announce(null);
return null;
}
const next = Number.parseInt(str.slice(0, -1));
this.announcer.announce(String(next));
return `${next}`;
});
if (moveToPrev) {
moveToPrevTimeSegment(e, this.root.getFieldNode());
}
}
onfocusout(_) {
const stateKey = this.part;
if (stateKey in this.root.states) {
this.root.states[stateKey].hasLeftFocus = true;
}
// Pad with zero if needed
if (this.config.padZero) {
// @ts-expect-error this is a part
this.root.updateSegment(this.part, (prev) => {
if (prev && prev.length === 1) {
return `0${prev}`;
}
return prev;
});
}
}
getSegmentProps() {
const segmentValues = this.root.segmentValues;
const placeholder = this.root.placeholder.current;
const isEmpty = segmentValues[this.part] === null;
let value = placeholder;
if (segmentValues[this.part]) {
value = placeholder.set({
[this.part]: Number.parseInt(segmentValues[this.part]),
});
}
const valueNow = value[this.part];
const valueMin = this.#getMin();
const valueMax = this.#getMax();
let valueText = isEmpty ? "Empty" : `${valueNow}`;
// special handling for hour segment with dayPeriod
if (this.part === "hour" && "dayPeriod" in segmentValues && segmentValues.dayPeriod) {
valueText = isEmpty ? "Empty" : `${valueNow} ${segmentValues.dayPeriod}`;
}
return {
"aria-label": `${this.part}, `,
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
};
}
props = $derived.by(() => {
return {
...this.root.sharedSegmentAttrs,
id: this.opts.id.current,
...this.getSegmentProps(),
onkeydown: this.onkeydown,
onfocusout: this.onfocusout,
onclick: this.root.handleSegmentClick,
...this.root.getBaseSegmentAttrs(this.part, this.opts.id.current),
...this.attachment,
};
});
}
class TimeFieldHourSegmentState extends BaseTimeSegmentState {
constructor(opts, root) {
super(opts, root, "hour", SEGMENT_CONFIGS.hour);
}
onkeydown(e) {
if (isNumberString(e.key)) {
const oldUpdateSegment = this.root.updateSegment.bind(this.root);
// oxlint-disable-next-line no-explicit-any
this.root.updateSegment = (part, cb) => {
const result = oldUpdateSegment(part, cb);
// after updating hour, check if we need to display "12" instead of "0"
if (part === "hour" && "hour" in this.root.segmentValues) {
const hourValue = this.root.segmentValues.hour;
if (hourValue === "0" &&
this.root.dayPeriodNode &&
this.root.hourCycle !== 24) {
this.root.segmentValues.hour = "12";
}
}
return result;
};
}
super.onkeydown(e);
this.root.updateSegment = this.root.updateSegment.bind(this.root);
}
}
class TimeFieldMinuteSegmentState extends BaseTimeSegmentState {
constructor(opts, root) {
super(opts, root, "minute", SEGMENT_CONFIGS.minute);
}
}
class TimeFieldSecondSegmentState extends BaseTimeSegmentState {
constructor(opts, root) {
super(opts, root, "second", SEGMENT_CONFIGS.second);
}
}
class TimeFieldDayPeriodSegmentState {
opts;
root;
attachment;
#announcer;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.#announcer = this.root.announcer;
this.attachment = attachRef(opts.ref, (v) => (this.root.dayPeriodNode = v));
this.onkeydown = this.onkeydown.bind(this);
}
onkeydown(e) {
if (e.ctrlKey || e.metaKey || this.root.disabled.current)
return;
if (e.key !== kbd.TAB)
e.preventDefault();
if (!isAcceptableDayPeriodKey(e.key))
return;
if (isArrowUp(e.key) || isArrowDown(e.key)) {
this.root.updateSegment("dayPeriod", (prev) => {
if (prev === "AM") {
const next = "PM";
this.#announcer.announce(next);
return next;
}
const next = "AM";
this.#announcer.announce(next);
return next;
});
return;
}
if (isBackspace(e.key)) {
this.root.states.dayPeriod.hasLeftFocus = false;
this.root.updateSegment("dayPeriod", () => {
const next = "AM";
this.#announcer.announce(next);
return next;
});
}
if (e.key === kbd.A || e.key === kbd.P || e.key === kbd.a || e.key === kbd.p) {
this.root.updateSegment("dayPeriod", () => {
const next = e.key === kbd.A || e.key === kbd.a ? "AM" : "PM";
this.#announcer.announce(next);
return next;
});
}
if (isSegmentNavigationKey(e.key)) {
handleTimeSegmentNavigation(e, this.root.getFieldNode());
}
}
props = $derived.by(() => {
const segmentValues = this.root.segmentValues;
if (!("dayPeriod" in segmentValues))
return;
const valueMin = 0;
const valueMax = 12;
const valueNow = segmentValues.dayPeriod === "AM" ? 0 : 12;
const valueText = segmentValues.dayPeriod === "AM" ? "AM" : "PM";
return {
...this.root.sharedSegmentAttrs,
id: this.opts.id.current,
inputmode: "text",
"aria-label": "AM/PM",
"aria-valuemin": valueMin,
"aria-valuemax": valueMax,
"aria-valuenow": valueNow,
"aria-valuetext": valueText,
onkeydown: this.onkeydown,
onclick: this.root.handleSegmentClick,
...this.root.getBaseSegmentAttrs("dayPeriod", this.opts.id.current),
...this.attachment,
};
});
}
class TimeFieldLiteralSegmentState {
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(opts.ref);
}
props = $derived.by(() => ({
id: this.opts.id.current,
"aria-hidden": boolToStrTrueOrUndef(true),
...this.root.getBaseSegmentAttrs("literal", this.opts.id.current),
...this.attachment,
}));
}
class TimeFieldTimeZoneSegmentState {
opts;
root;
attachment;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(opts.ref);
this.onkeydown = this.onkeydown.bind(this);
}
onkeydown(e) {
if (e.key !== kbd.TAB)
e.preventDefault();
if (this.root.disabled.current)
return;
if (isSegmentNavigationKey(e.key)) {
handleTimeSegmentNavigation(e, this.root.getFieldNode());
}
}
props = $derived.by(() => ({
role: "textbox",
id: this.opts.id.current,
"aria-label": "timezone, ",
style: {
caretColor: "transparent",
},
onkeydown: this.onkeydown,
tabindex: 0,
...this.root.getBaseSegmentAttrs("timeZoneName", this.opts.id.current),
"data-readonly": boolToEmptyStrOrUndef(true),
...this.attachment,
}));
}
export class DateFieldSegmentState {
static create(part, opts) {
const root = TimeFieldRootContext.get();
switch (part) {
case "hour":
return new TimeFieldHourSegmentState(opts, root);
case "minute":
return new TimeFieldMinuteSegmentState(opts, root);
case "second":
return new TimeFieldSecondSegmentState(opts, root);
case "dayPeriod":
return new TimeFieldDayPeriodSegmentState(opts, root);
case "literal":
return new TimeFieldLiteralSegmentState(opts, root);
case "timeZoneName":
return new TimeFieldTimeZoneSegmentState(opts, root);
default:
throw new Error(`Invalid part: ${part}`);
}
}
}
// Utils/helpers
function isAcceptableDayPeriodKey(key) {
return (isAcceptableSegmentKey(key) ||
key === kbd.A ||
key === kbd.P ||
key === kbd.a ||
key === kbd.p);
}
function isArrowUp(key) {
return key === kbd.ARROW_UP;
}
function isArrowDown(key) {
return key === kbd.ARROW_DOWN;
}
function isBackspace(key) {
return key === kbd.BACKSPACE;
}
+137
View File
@@ -0,0 +1,137 @@
import type { Time } from "@internationalized/date";
import type { EditableSegmentPart, WithChildren } from "../../shared/index.js";
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
import type { BitsPrimitiveDivAttributes, BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
import type { TimeGranularity, TimeOnInvalid, TimeSegmentPart, TimeValidator, TimeValue } from "../../shared/date/types.js";
export type TimeFieldRootPropsWithoutHTML<T extends TimeValue = Time> = WithChildren<{
/**
* The value of the time field.
*
* @bindable
*/
value?: T;
/**
* A callback that is called when the time field value changes.
*
*/
onValueChange?: OnChangeFn<T | undefined>;
/**
* The placeholder value of the time field. This determines the format
* and what time the field starts at when it is empty.
*
* @bindable
*/
placeholder?: TimeValue;
/**
* A callback that is called when the time field's placeholder value changes.
*/
onPlaceholderChange?: OnChangeFn<TimeValue | undefined>;
/**
* A function that returns a string or array of strings as validation errors if the time is
* invalid, or nothing if the time is valid
*/
validate?: TimeValidator<T>;
/**
* A callback fired when the time field's value is invalid. Use this to display an error
* message to the user.
*/
onInvalid?: TimeOnInvalid;
/**
* The minimum acceptable time. When provided, the time field
* will be marked as invalid if the user enters a time before this time.
*/
minValue?: TimeValue;
/**
* The maximum acceptable time. When provided, the time field
* will be marked as invalid if the user enters a time after this time.
*/
maxValue?: TimeValue;
/**
* If true, the time field will be disabled and users will not be able
* to interact with it. This also disables the hidden input element if
* the time field is used in a form.
*
* @defaultValue false
*/
disabled?: boolean;
/**
* If true, the time 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 time field.
*
* @defaultValue 'en'
*/
locale?: string;
/**
* The granularity of the time field. This determines which
* segments will be includes in the segments array used to
* build the time field.
*
* Granularity is only used for visual purposes, and does not impact
* the value of the time field. You can have the same value synced
* between multiple time fields with different granularities and they
* will all contain the same value.
*
* @default 'minute'
*/
granularity?: TimeGranularity;
/**
* Whether or not to hide the timeZoneName segment from the time field.
*
* @default false;
*/
hideTimeZone?: boolean;
/**
* Whether or not the hidden input of the time field requires a value
* to be submitted.
*
* @defaultValue false
*/
required?: boolean;
/**
* The `id` of the element which contains the error messages for the time field when the
* time is invalid.
*/
errorMessageId?: string;
}>;
export type TimeFieldRootProps<T extends TimeValue = Time> = TimeFieldRootPropsWithoutHTML<T>;
export type TimeFieldInputSnippetProps = {
segments: Array<{
part: TimeSegmentPart;
value: string;
}>;
};
export type TimeFieldInputPropsWithoutHTML = WithChild<{
/**
* The name to use for the hidden input element of the time field,
* which is used to submit the ISO string value of the time field
* to a server. If not provided, the hidden input element will not
* be rendered.
*/
name?: string;
}, TimeFieldInputSnippetProps>;
export type TimeFieldInputProps = TimeFieldInputPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, TimeFieldInputPropsWithoutHTML>;
export type TimeFieldSegmentPropsWithoutHTML = WithChild<{
part: TimeSegmentPart;
}>;
export type TimeFieldSegmentProps = TimeFieldSegmentPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, TimeFieldSegmentPropsWithoutHTML>;
export type TimeFieldLabelPropsWithoutHTML = WithChild;
export type TimeFieldLabelProps = TimeFieldLabelPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, TimeFieldLabelPropsWithoutHTML>;
+1
View File
@@ -0,0 +1 @@
export {};