INIT
This commit is contained in:
Generated
Vendored
+10
@@ -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}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
||||
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
||||
$$bindings?: Bindings;
|
||||
} & Exports;
|
||||
(internal: unknown, props: {
|
||||
$$events?: Events;
|
||||
$$slots?: Slots;
|
||||
}): Exports & {
|
||||
$set?: any;
|
||||
$on?: any;
|
||||
};
|
||||
z_$$bindings?: Bindings;
|
||||
}
|
||||
declare const TimeFieldHiddenInput: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
||||
[evt: string]: CustomEvent<any>;
|
||||
}, {}, {}, string>;
|
||||
type TimeFieldHiddenInput = InstanceType<typeof TimeFieldHiddenInput>;
|
||||
export default TimeFieldHiddenInput;
|
||||
+39
@@ -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 />
|
||||
Generated
Vendored
+4
@@ -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;
|
||||
+34
@@ -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}
|
||||
Generated
Vendored
+4
@@ -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;
|
||||
+37
@@ -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}
|
||||
Generated
Vendored
+4
@@ -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
@@ -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?.()}
|
||||
+20
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export * as TimeField from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as TimeField from "./exports.js";
|
||||
+427
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user