INIT
This commit is contained in:
+86
@@ -0,0 +1,86 @@
|
||||
import type { DateValue } from "@internationalized/date";
|
||||
import type { Formatter } from "../formatter.js";
|
||||
import type { DateAndTimeSegmentObj, DateSegmentPart, EditableSegmentPart, SegmentContentObj, SegmentPart, SegmentStateMap, SegmentValueObj } from "./types.js";
|
||||
import type { Granularity, HourCycle } from "../../../shared/date/types.js";
|
||||
export declare function initializeSegmentValues(granularity: Granularity): SegmentValueObj;
|
||||
type SharedContentProps = {
|
||||
granularity: Granularity;
|
||||
dateRef: DateValue;
|
||||
formatter: Formatter;
|
||||
hideTimeZone: boolean;
|
||||
hourCycle: HourCycle | undefined;
|
||||
};
|
||||
type CreateContentObjProps = SharedContentProps & {
|
||||
segmentValues: SegmentValueObj;
|
||||
locale: string;
|
||||
};
|
||||
type CreateContentProps = CreateContentObjProps;
|
||||
export declare function createContent(props: CreateContentProps): {
|
||||
obj: SegmentContentObj;
|
||||
arr: {
|
||||
part: SegmentPart;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
export declare function initSegmentStates(): SegmentStateMap;
|
||||
export declare function initSegmentIds(): any;
|
||||
export declare function isDateSegmentPart(part: unknown): part is DateSegmentPart;
|
||||
export declare function isSegmentPart(part: string): part is EditableSegmentPart;
|
||||
export declare function isAnySegmentPart(part: unknown): part is SegmentPart;
|
||||
type GetValueFromSegments = {
|
||||
segmentObj: SegmentValueObj;
|
||||
fieldNode: HTMLElement | null;
|
||||
dateRef: DateValue;
|
||||
};
|
||||
export declare function getValueFromSegments(props: GetValueFromSegments): DateValue;
|
||||
/**
|
||||
* Check if all the segments being used have been filled.
|
||||
* We use this to determine when we should set the value
|
||||
* store of the date field(s).
|
||||
*
|
||||
* @param segmentValues - The current `SegmentValueObj`
|
||||
* @param fieldNode - The id of the date field
|
||||
*/
|
||||
export declare function areAllSegmentsFilled(segmentValues: SegmentValueObj, fieldNode: HTMLElement | null): boolean;
|
||||
/**
|
||||
* Determines if the provided object is a valid `DateAndTimeSegmentObj`
|
||||
* by checking if it has the correct keys and values for each key.
|
||||
*/
|
||||
export declare function isDateAndTimeSegmentObj(obj: unknown): obj is DateAndTimeSegmentObj;
|
||||
/**
|
||||
* Infer the granularity to use based on the
|
||||
* value and granularity props.
|
||||
*/
|
||||
export declare function inferGranularity(value: DateValue, granularity: Granularity | undefined): Granularity;
|
||||
export declare function isAcceptableSegmentKey(key: string): boolean;
|
||||
/**
|
||||
* Determines if the element with the provided id is the first focusable
|
||||
* segment in the date field with the provided fieldId.
|
||||
*
|
||||
* @param id - The id of the element to check if it's the first segment
|
||||
* @param fieldNode - The id of the date field associated with the segment
|
||||
*/
|
||||
export declare function isFirstSegment(id: string, fieldNode: HTMLElement | null): boolean;
|
||||
type SetDescriptionProps = {
|
||||
id: string;
|
||||
formatter: Formatter;
|
||||
value: DateValue;
|
||||
doc: Document;
|
||||
};
|
||||
/**
|
||||
* Creates or updates a description element for a date field
|
||||
* which enables screen readers to read the date field's value.
|
||||
*
|
||||
* This element is hidden from view, and is portalled to the body
|
||||
* so it can be associated via `aria-describedby` and read by
|
||||
* screen readers as the user interacts with the date field.
|
||||
*/
|
||||
export declare function setDescription(props: SetDescriptionProps): void;
|
||||
/**
|
||||
* Removes the description element for the date field with
|
||||
* the provided ID. This function should be called when the
|
||||
* date field is unmounted.
|
||||
*/
|
||||
export declare function removeDescriptionElement(id: string, doc: Document): void;
|
||||
export declare function getDefaultHourCycle(locale: string): 12 | 24;
|
||||
export {};
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
import { styleToString } from "svelte-toolbelt";
|
||||
import { getPlaceholder } from "../placeholders.js";
|
||||
import { hasTime, isZonedDateTime } from "../utils.js";
|
||||
import { ALL_SEGMENT_PARTS, DATE_SEGMENT_PARTS, EDITABLE_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS, } from "./parts.js";
|
||||
import { getSegments } from "./segments.js";
|
||||
import { isBrowser, isNull, isNumberString } from "../../is.js";
|
||||
import { useId } from "../../use-id.js";
|
||||
import { kbd } from "../../kbd.js";
|
||||
export function initializeSegmentValues(granularity) {
|
||||
const calendarDateTimeGranularities = ["hour", "minute", "second"];
|
||||
const initialParts = EDITABLE_SEGMENT_PARTS.map((part) => {
|
||||
if (part === "dayPeriod") {
|
||||
return [part, "AM"];
|
||||
}
|
||||
return [part, null];
|
||||
}).filter(([key]) => {
|
||||
if (key === "literal" || key === null)
|
||||
return false;
|
||||
if (granularity === "day") {
|
||||
return !calendarDateTimeGranularities.includes(key);
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return Object.fromEntries(initialParts);
|
||||
}
|
||||
function createContentObj(props) {
|
||||
const { segmentValues, formatter, locale, dateRef } = props;
|
||||
const content = Object.keys(segmentValues).reduce((obj, part) => {
|
||||
if (!isSegmentPart(part))
|
||||
return obj;
|
||||
if ("hour" in segmentValues && part === "dayPeriod") {
|
||||
const value = segmentValues[part];
|
||||
if (!isNull(value)) {
|
||||
obj[part] = value;
|
||||
}
|
||||
else {
|
||||
obj[part] = getPlaceholder(part, "AM", locale);
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj[part] = getPartContent(part);
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
function getPartContent(part) {
|
||||
if ("hour" in segmentValues) {
|
||||
const value = segmentValues[part];
|
||||
const leadingZero = typeof value === "string" && value?.startsWith("0");
|
||||
const intValue = value !== null ? Number.parseInt(value) : null;
|
||||
if (value === "0" && part !== "year") {
|
||||
return "0";
|
||||
}
|
||||
else if (!isNull(value) && !isNull(intValue)) {
|
||||
const formatted = formatter.part(dateRef.set({ [part]: value }), part, {
|
||||
hourCycle: props.hourCycle === 24 ? "h23" : undefined,
|
||||
});
|
||||
/**
|
||||
* If we're operating in a 12 hour clock and the part is an hour, we handle
|
||||
* the conversion to 12 hour format with 2 digit hours and leading zeros here.
|
||||
*/
|
||||
const is12HourMode = props.hourCycle === 12 ||
|
||||
(props.hourCycle === undefined && getDefaultHourCycle(locale) === 12);
|
||||
if (part === "hour" && is12HourMode) {
|
||||
/**
|
||||
* If the value is over 12, we convert to 12 hour format and add leading
|
||||
* zeroes if the value is less than 10.
|
||||
*/
|
||||
if (intValue > 12) {
|
||||
const hour = intValue - 12;
|
||||
if (hour === 0) {
|
||||
return "12";
|
||||
}
|
||||
else if (hour < 10) {
|
||||
return `0${hour}`;
|
||||
}
|
||||
else {
|
||||
return `${hour}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If the value is 0, we convert to 12, since 0 is not a valid 12 hour time.
|
||||
*/
|
||||
if (intValue === 0) {
|
||||
return "12";
|
||||
}
|
||||
/**
|
||||
* If the value is less than 10, we add a leading zero to the value.
|
||||
*/
|
||||
if (intValue < 10) {
|
||||
return `0${intValue}`;
|
||||
}
|
||||
/**
|
||||
* Otherwise, we don't need to do anything to the value.
|
||||
*/
|
||||
return `${intValue}`;
|
||||
}
|
||||
if (part === "year") {
|
||||
return `${value}`;
|
||||
}
|
||||
if (leadingZero && formatted.length === 1) {
|
||||
return `0${formatted}`;
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
else {
|
||||
return getPlaceholder(part, "", locale);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isDateSegmentPart(part)) {
|
||||
const value = segmentValues[part];
|
||||
const leadingZero = typeof value === "string" && value?.startsWith("0");
|
||||
if (value === "0") {
|
||||
return "0";
|
||||
}
|
||||
else if (!isNull(value)) {
|
||||
const formatted = formatter.part(dateRef.set({ [part]: value }), part);
|
||||
if (part === "year") {
|
||||
return `${value}`;
|
||||
}
|
||||
if (leadingZero && formatted.length === 1) {
|
||||
return `0${formatted}`;
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
else {
|
||||
return getPlaceholder(part, "", locale);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
function createContentArr(props) {
|
||||
const { granularity, dateRef, formatter, contentObj, hideTimeZone, hourCycle } = props;
|
||||
const parts = formatter.toParts(dateRef, getOptsByGranularity(granularity, hourCycle));
|
||||
const segmentContentArr = parts
|
||||
.map((part) => {
|
||||
const defaultParts = ["literal", "dayPeriod", "timeZoneName", null];
|
||||
if (defaultParts.includes(part.type) || !isSegmentPart(part.type)) {
|
||||
return {
|
||||
part: part.type,
|
||||
value: part.value,
|
||||
};
|
||||
}
|
||||
return {
|
||||
part: part.type,
|
||||
value: contentObj[part.type],
|
||||
};
|
||||
})
|
||||
.filter((segment) => {
|
||||
if (isNull(segment.part) || isNull(segment.value))
|
||||
return false;
|
||||
if (segment.part === "timeZoneName" && (!isZonedDateTime(dateRef) || hideTimeZone)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return segmentContentArr;
|
||||
}
|
||||
export function createContent(props) {
|
||||
const contentObj = createContentObj(props);
|
||||
const contentArr = createContentArr({
|
||||
contentObj,
|
||||
...props,
|
||||
});
|
||||
return {
|
||||
obj: contentObj,
|
||||
arr: contentArr,
|
||||
};
|
||||
}
|
||||
function getOptsByGranularity(granularity, hourCycle) {
|
||||
const opts = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZoneName: "short",
|
||||
hourCycle: hourCycle === 24 ? "h23" : undefined,
|
||||
hour12: hourCycle === 24 ? false : undefined,
|
||||
};
|
||||
if (granularity === "day") {
|
||||
delete opts.second;
|
||||
delete opts.hour;
|
||||
delete opts.minute;
|
||||
delete opts.timeZoneName;
|
||||
}
|
||||
if (granularity === "hour") {
|
||||
delete opts.minute;
|
||||
}
|
||||
if (granularity === "minute") {
|
||||
delete opts.second;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
export function initSegmentStates() {
|
||||
return EDITABLE_SEGMENT_PARTS.reduce((acc, key) => {
|
||||
acc[key] = {
|
||||
lastKeyZero: false,
|
||||
hasLeftFocus: true,
|
||||
updating: null,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
export function initSegmentIds() {
|
||||
return Object.fromEntries(ALL_SEGMENT_PARTS.map((part) => {
|
||||
return [part, useId()];
|
||||
}).filter(([key]) => key !== "literal"));
|
||||
}
|
||||
export function isDateSegmentPart(part) {
|
||||
return DATE_SEGMENT_PARTS.includes(part);
|
||||
}
|
||||
export function isSegmentPart(part) {
|
||||
return EDITABLE_SEGMENT_PARTS.includes(part);
|
||||
}
|
||||
export function isAnySegmentPart(part) {
|
||||
return ALL_SEGMENT_PARTS.includes(part);
|
||||
}
|
||||
/**
|
||||
* Get the segments being used/ are rendered in the DOM.
|
||||
* We're using this to determine when to set the value of
|
||||
* the date picker, which is when all the segments have
|
||||
* been filled.
|
||||
*/
|
||||
function getUsedSegments(fieldNode) {
|
||||
if (!isBrowser || !fieldNode)
|
||||
return [];
|
||||
const usedSegments = getSegments(fieldNode)
|
||||
.map((el) => el.dataset.segment)
|
||||
.filter((part) => {
|
||||
return EDITABLE_SEGMENT_PARTS.includes(part);
|
||||
});
|
||||
return usedSegments;
|
||||
}
|
||||
export function getValueFromSegments(props) {
|
||||
const { segmentObj, fieldNode, dateRef } = props;
|
||||
const usedSegments = getUsedSegments(fieldNode);
|
||||
let date = dateRef;
|
||||
for (const part of usedSegments) {
|
||||
if ("hour" in segmentObj) {
|
||||
const value = segmentObj[part];
|
||||
if (isNull(value))
|
||||
continue;
|
||||
date = date.set({ [part]: segmentObj[part] });
|
||||
}
|
||||
else if (isDateSegmentPart(part)) {
|
||||
const value = segmentObj[part];
|
||||
if (isNull(value))
|
||||
continue;
|
||||
date = date.set({ [part]: segmentObj[part] });
|
||||
}
|
||||
}
|
||||
return date;
|
||||
}
|
||||
/**
|
||||
* Check if all the segments being used have been filled.
|
||||
* We use this to determine when we should set the value
|
||||
* store of the date field(s).
|
||||
*
|
||||
* @param segmentValues - The current `SegmentValueObj`
|
||||
* @param fieldNode - The id of the date field
|
||||
*/
|
||||
export function areAllSegmentsFilled(segmentValues, fieldNode) {
|
||||
const usedSegments = getUsedSegments(fieldNode);
|
||||
for (const part of usedSegments) {
|
||||
if ("hour" in segmentValues) {
|
||||
if (segmentValues[part] === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (isDateSegmentPart(part)) {
|
||||
if (segmentValues[part] === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Determines if the provided object is a valid `DateAndTimeSegmentObj`
|
||||
* by checking if it has the correct keys and values for each key.
|
||||
*/
|
||||
export function isDateAndTimeSegmentObj(obj) {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
return false;
|
||||
}
|
||||
return Object.entries(obj).every(([key, value]) => {
|
||||
const validKey = EDITABLE_TIME_SEGMENT_PARTS.includes(key) ||
|
||||
DATE_SEGMENT_PARTS.includes(key);
|
||||
const validValue = key === "dayPeriod"
|
||||
? value === "AM" || value === "PM" || value === null
|
||||
: typeof value === "string" || typeof value === "number" || value === null;
|
||||
return validKey && validValue;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Infer the granularity to use based on the
|
||||
* value and granularity props.
|
||||
*/
|
||||
export function inferGranularity(value, granularity) {
|
||||
if (granularity)
|
||||
return granularity;
|
||||
if (hasTime(value))
|
||||
return "minute";
|
||||
return "day";
|
||||
}
|
||||
export function isAcceptableSegmentKey(key) {
|
||||
const acceptableSegmentKeys = [
|
||||
kbd.ENTER,
|
||||
kbd.ARROW_UP,
|
||||
kbd.ARROW_DOWN,
|
||||
kbd.ARROW_LEFT,
|
||||
kbd.ARROW_RIGHT,
|
||||
kbd.BACKSPACE,
|
||||
kbd.SPACE,
|
||||
];
|
||||
if (acceptableSegmentKeys.includes(key))
|
||||
return true;
|
||||
if (isNumberString(key))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Determines if the element with the provided id is the first focusable
|
||||
* segment in the date field with the provided fieldId.
|
||||
*
|
||||
* @param id - The id of the element to check if it's the first segment
|
||||
* @param fieldNode - The id of the date field associated with the segment
|
||||
*/
|
||||
export function isFirstSegment(id, fieldNode) {
|
||||
if (!isBrowser)
|
||||
return false;
|
||||
const segments = getSegments(fieldNode);
|
||||
return segments.length ? segments[0].id === id : false;
|
||||
}
|
||||
/**
|
||||
* Creates or updates a description element for a date field
|
||||
* which enables screen readers to read the date field's value.
|
||||
*
|
||||
* This element is hidden from view, and is portalled to the body
|
||||
* so it can be associated via `aria-describedby` and read by
|
||||
* screen readers as the user interacts with the date field.
|
||||
*/
|
||||
export function setDescription(props) {
|
||||
const { id, formatter, value, doc } = props;
|
||||
if (!isBrowser)
|
||||
return;
|
||||
const valueString = formatter.selectedDate(value);
|
||||
const el = doc.getElementById(id);
|
||||
if (!el) {
|
||||
const div = doc.createElement("div");
|
||||
div.style.cssText = styleToString({
|
||||
display: "none",
|
||||
});
|
||||
div.id = id;
|
||||
div.innerText = `Selected Date: ${valueString}`;
|
||||
doc.body.appendChild(div);
|
||||
}
|
||||
else {
|
||||
el.innerText = `Selected Date: ${valueString}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes the description element for the date field with
|
||||
* the provided ID. This function should be called when the
|
||||
* date field is unmounted.
|
||||
*/
|
||||
export function removeDescriptionElement(id, doc) {
|
||||
if (!isBrowser)
|
||||
return;
|
||||
const el = doc.getElementById(id);
|
||||
if (!el)
|
||||
return;
|
||||
doc.body.removeChild(el);
|
||||
}
|
||||
export function getDefaultHourCycle(locale) {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { hour: "numeric" });
|
||||
const parts = formatter.formatToParts(new Date("2023-01-01T13:00:00"));
|
||||
const hourPart = parts.find((part) => part.type === "hour");
|
||||
return hourPart?.value === "1" ? 12 : 24;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export declare const DATE_SEGMENT_PARTS: readonly ["day", "month", "year"];
|
||||
export declare const EDITABLE_TIME_SEGMENT_PARTS: readonly ["hour", "minute", "second", "dayPeriod"];
|
||||
export declare const NON_EDITABLE_SEGMENT_PARTS: readonly ["literal", "timeZoneName"];
|
||||
export declare const EDITABLE_SEGMENT_PARTS: readonly ["day", "month", "year", "hour", "minute", "second", "dayPeriod"];
|
||||
export declare const ALL_SEGMENT_PARTS: readonly ["day", "month", "year", "hour", "minute", "second", "dayPeriod", "literal", "timeZoneName"];
|
||||
export declare const ALL_TIME_SEGMENT_PARTS: readonly ["hour", "minute", "second", "dayPeriod", "literal", "timeZoneName"];
|
||||
export declare const ALL_EXCEPT_LITERAL_PARTS: ("day" | "month" | "year" | "hour" | "minute" | "second" | "dayPeriod" | "timeZoneName")[];
|
||||
export declare const ALL_TIME_EXCEPT_LITERAL_PARTS: ("hour" | "minute" | "second" | "dayPeriod" | "timeZoneName")[];
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export const DATE_SEGMENT_PARTS = ["day", "month", "year"];
|
||||
export const EDITABLE_TIME_SEGMENT_PARTS = ["hour", "minute", "second", "dayPeriod"];
|
||||
export const NON_EDITABLE_SEGMENT_PARTS = ["literal", "timeZoneName"];
|
||||
export const EDITABLE_SEGMENT_PARTS = [
|
||||
...DATE_SEGMENT_PARTS,
|
||||
...EDITABLE_TIME_SEGMENT_PARTS,
|
||||
];
|
||||
export const ALL_SEGMENT_PARTS = [
|
||||
...EDITABLE_SEGMENT_PARTS,
|
||||
...NON_EDITABLE_SEGMENT_PARTS,
|
||||
];
|
||||
export const ALL_TIME_SEGMENT_PARTS = [
|
||||
...EDITABLE_TIME_SEGMENT_PARTS,
|
||||
...NON_EDITABLE_SEGMENT_PARTS,
|
||||
];
|
||||
export const ALL_EXCEPT_LITERAL_PARTS = ALL_SEGMENT_PARTS.filter((part) => part !== "literal");
|
||||
export const ALL_TIME_EXCEPT_LITERAL_PARTS = ALL_TIME_SEGMENT_PARTS.filter((part) => part !== "literal");
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Handles segment navigation based on the provided keyboard event and field ID.
|
||||
*
|
||||
* @param e - The keyboard event
|
||||
* @param fieldNode - The ID of the field we're navigating within
|
||||
*/
|
||||
export declare function handleSegmentNavigation(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
export declare function handleTimeSegmentNavigation(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
/**
|
||||
* Retrieves the next segment in the list of segments relative to the provided node.
|
||||
*
|
||||
* @param node - The node we're starting from
|
||||
* @param segments - The list of candidate segments to navigate through
|
||||
*/
|
||||
export declare function getNextSegment(node: HTMLElement, segments: HTMLElement[]): HTMLElement | null | undefined;
|
||||
/**
|
||||
* Retrieves the previous segment in the list of segments relative to the provided node.
|
||||
*
|
||||
* @param node - The node we're starting from
|
||||
* @param segments - The list of candidate segments to navigate through
|
||||
*/
|
||||
export declare function getPrevSegment(node: HTMLElement, segments: HTMLElement[]): HTMLElement | null | undefined;
|
||||
/**
|
||||
* Retrieves an object containing the next and previous segments relative to the current node.
|
||||
*
|
||||
* @param startingNode - The node we're starting from
|
||||
* @param fieldNode - The ID of the field we're navigating within
|
||||
*/
|
||||
export declare function getPrevNextSegments(startingNode: HTMLElement, fieldNode: HTMLElement | null): {
|
||||
next: HTMLElement | null | undefined;
|
||||
prev: HTMLElement | null | undefined;
|
||||
};
|
||||
export declare function getPrevNextTimeSegments(startingNode: HTMLElement, fieldNode: HTMLElement | null): {
|
||||
next: HTMLElement | null | undefined;
|
||||
prev: HTMLElement | null | undefined;
|
||||
};
|
||||
/**
|
||||
* Shifts the focus to the next segment in the list of segments
|
||||
* within the field identified by the provided ID.
|
||||
*/
|
||||
export declare function moveToNextSegment(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
export declare function moveToNextTimeSegment(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
export declare function moveToPrevTimeSegment(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
/**
|
||||
* Shifts the focus to the previous segment in the list of segments
|
||||
* within the field identified by the provided ID. If this is the first
|
||||
* segment, focus will not be shifted.
|
||||
*/
|
||||
export declare function moveToPrevSegment(e: KeyboardEvent, fieldNode: HTMLElement | null): void;
|
||||
export declare function isSegmentNavigationKey(key: string): boolean;
|
||||
/**
|
||||
* Retrieves all the interactive segments within the field identified by the provided ID.
|
||||
*/
|
||||
export declare function getSegments(fieldNode: HTMLElement | null): HTMLElement[];
|
||||
export declare function getTimeSegments(fieldNode: HTMLElement | null): HTMLElement[];
|
||||
export declare function getFirstTimeSegment(fieldNode: HTMLElement | null): HTMLElement | undefined;
|
||||
/**
|
||||
* Get the first interactive segment within the field identified by the provided ID.
|
||||
*/
|
||||
export declare function getFirstSegment(fieldNode: HTMLElement | null): HTMLElement | undefined;
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import { isAnySegmentPart } from "./helpers.js";
|
||||
import { isHTMLElement } from "../../is.js";
|
||||
import { kbd } from "../../kbd.js";
|
||||
/**
|
||||
* Handles segment navigation based on the provided keyboard event and field ID.
|
||||
*
|
||||
* @param e - The keyboard event
|
||||
* @param fieldNode - The ID of the field we're navigating within
|
||||
*/
|
||||
export function handleSegmentNavigation(e, fieldNode) {
|
||||
const currentTarget = e.currentTarget;
|
||||
if (!isHTMLElement(currentTarget))
|
||||
return;
|
||||
const { prev, next } = getPrevNextSegments(currentTarget, fieldNode);
|
||||
if (e.key === kbd.ARROW_LEFT) {
|
||||
if (!prev)
|
||||
return;
|
||||
prev.focus();
|
||||
}
|
||||
else if (e.key === kbd.ARROW_RIGHT) {
|
||||
if (!next)
|
||||
return;
|
||||
next.focus();
|
||||
}
|
||||
}
|
||||
export function handleTimeSegmentNavigation(e, fieldNode) {
|
||||
const currentTarget = e.currentTarget;
|
||||
if (!isHTMLElement(currentTarget))
|
||||
return;
|
||||
const { prev, next } = getPrevNextTimeSegments(currentTarget, fieldNode);
|
||||
if (e.key === kbd.ARROW_LEFT) {
|
||||
if (!prev)
|
||||
return;
|
||||
prev.focus();
|
||||
}
|
||||
else if (e.key === kbd.ARROW_RIGHT) {
|
||||
if (!next)
|
||||
return;
|
||||
next.focus();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Retrieves the next segment in the list of segments relative to the provided node.
|
||||
*
|
||||
* @param node - The node we're starting from
|
||||
* @param segments - The list of candidate segments to navigate through
|
||||
*/
|
||||
export function getNextSegment(node, segments) {
|
||||
const index = segments.indexOf(node);
|
||||
if (index === segments.length - 1 || index === -1)
|
||||
return null;
|
||||
const nextIndex = index + 1;
|
||||
const nextSegment = segments[nextIndex];
|
||||
return nextSegment;
|
||||
}
|
||||
/**
|
||||
* Retrieves the previous segment in the list of segments relative to the provided node.
|
||||
*
|
||||
* @param node - The node we're starting from
|
||||
* @param segments - The list of candidate segments to navigate through
|
||||
*/
|
||||
export function getPrevSegment(node, segments) {
|
||||
const index = segments.indexOf(node);
|
||||
if (index === 0 || index === -1)
|
||||
return null;
|
||||
const prevIndex = index - 1;
|
||||
const prevSegment = segments[prevIndex];
|
||||
return prevSegment;
|
||||
}
|
||||
/**
|
||||
* Retrieves an object containing the next and previous segments relative to the current node.
|
||||
*
|
||||
* @param startingNode - The node we're starting from
|
||||
* @param fieldNode - The ID of the field we're navigating within
|
||||
*/
|
||||
export function getPrevNextSegments(startingNode, fieldNode) {
|
||||
const segments = getSegments(fieldNode);
|
||||
if (!segments.length) {
|
||||
return {
|
||||
next: null,
|
||||
prev: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
next: getNextSegment(startingNode, segments),
|
||||
prev: getPrevSegment(startingNode, segments),
|
||||
};
|
||||
}
|
||||
export function getPrevNextTimeSegments(startingNode, fieldNode) {
|
||||
const segments = getTimeSegments(fieldNode);
|
||||
if (!segments.length) {
|
||||
return {
|
||||
next: null,
|
||||
prev: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
next: getNextSegment(startingNode, segments),
|
||||
prev: getPrevSegment(startingNode, segments),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Shifts the focus to the next segment in the list of segments
|
||||
* within the field identified by the provided ID.
|
||||
*/
|
||||
export function moveToNextSegment(e, fieldNode) {
|
||||
const node = e.currentTarget;
|
||||
if (!isHTMLElement(node))
|
||||
return;
|
||||
const { next } = getPrevNextSegments(node, fieldNode);
|
||||
if (!next)
|
||||
return;
|
||||
next.focus();
|
||||
}
|
||||
export function moveToNextTimeSegment(e, fieldNode) {
|
||||
const node = e.currentTarget;
|
||||
if (!isHTMLElement(node))
|
||||
return;
|
||||
const { next } = getPrevNextTimeSegments(node, fieldNode);
|
||||
if (!next)
|
||||
return;
|
||||
next.focus();
|
||||
}
|
||||
export function moveToPrevTimeSegment(e, fieldNode) {
|
||||
const node = e.currentTarget;
|
||||
if (!isHTMLElement(node))
|
||||
return;
|
||||
const { prev } = getPrevNextTimeSegments(node, fieldNode);
|
||||
if (!prev)
|
||||
return;
|
||||
prev.focus();
|
||||
}
|
||||
/**
|
||||
* Shifts the focus to the previous segment in the list of segments
|
||||
* within the field identified by the provided ID. If this is the first
|
||||
* segment, focus will not be shifted.
|
||||
*/
|
||||
export function moveToPrevSegment(e, fieldNode) {
|
||||
const node = e.currentTarget;
|
||||
if (!isHTMLElement(node))
|
||||
return;
|
||||
const { prev } = getPrevNextSegments(node, fieldNode);
|
||||
if (!prev)
|
||||
return;
|
||||
prev.focus();
|
||||
}
|
||||
export function isSegmentNavigationKey(key) {
|
||||
if (key === kbd.ARROW_RIGHT || key === kbd.ARROW_LEFT)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Retrieves all the interactive segments within the field identified by the provided ID.
|
||||
*/
|
||||
export function getSegments(fieldNode) {
|
||||
if (!fieldNode)
|
||||
return [];
|
||||
const segments = Array.from(fieldNode.querySelectorAll("[data-segment]")).filter((el) => {
|
||||
if (!isHTMLElement(el))
|
||||
return false;
|
||||
const segment = el.dataset.segment;
|
||||
if (segment === "trigger")
|
||||
return true;
|
||||
if (!isAnySegmentPart(segment) || segment === "literal")
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return segments;
|
||||
}
|
||||
export function getTimeSegments(fieldNode) {
|
||||
if (!fieldNode)
|
||||
return [];
|
||||
const segments = Array.from(fieldNode.querySelectorAll("[data-segment]")).filter((el) => {
|
||||
if (!isHTMLElement(el))
|
||||
return false;
|
||||
const segment = el.dataset.segment;
|
||||
if (segment === "trigger")
|
||||
return true;
|
||||
if (segment === "literal")
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return segments;
|
||||
}
|
||||
export function getFirstTimeSegment(fieldNode) {
|
||||
return getTimeSegments(fieldNode)[0];
|
||||
}
|
||||
/**
|
||||
* Get the first interactive segment within the field identified by the provided ID.
|
||||
*/
|
||||
export function getFirstSegment(fieldNode) {
|
||||
return getSegments(fieldNode)[0];
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import type { EditableTimeSegmentPart, HourCycle, TimeGranularity, TimeSegmentContentObj, TimeSegmentStateMap, TimeSegmentValueObj, TimeValue } from "../../../shared/date/types.js";
|
||||
import { CalendarDateTime, Time, ZonedDateTime } from "@internationalized/date";
|
||||
import type { TimeFormatter } from "../formatter.js";
|
||||
import type { TimeSegmentPart } from "./types.js";
|
||||
export declare function initializeSegmentValues(): TimeSegmentValueObj;
|
||||
type SharedTimeContentProps = {
|
||||
granularity: TimeGranularity;
|
||||
timeRef: TimeValue;
|
||||
formatter: TimeFormatter;
|
||||
hideTimeZone: boolean;
|
||||
hourCycle: HourCycle | undefined;
|
||||
};
|
||||
type CreateTimeContentObjProps = SharedTimeContentProps & {
|
||||
segmentValues: TimeSegmentValueObj;
|
||||
locale: string;
|
||||
};
|
||||
type CreateTimeContentProps = CreateTimeContentObjProps;
|
||||
export declare function createTimeContent(props: CreateTimeContentProps): {
|
||||
obj: TimeSegmentContentObj;
|
||||
arr: {
|
||||
part: TimeSegmentPart;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
export declare function initTimeSegmentStates(): TimeSegmentStateMap;
|
||||
export declare function initTimeSegmentIds(): any;
|
||||
export declare function isEditableTimeSegmentPart(part: unknown): part is EditableTimeSegmentPart;
|
||||
export declare function isAnyTimeSegmentPart(part: unknown): part is TimeSegmentPart;
|
||||
type GetTimeValueFromSegments<T extends TimeValue = Time> = {
|
||||
segmentObj: TimeSegmentValueObj;
|
||||
fieldNode: HTMLElement | null;
|
||||
timeRef: T;
|
||||
};
|
||||
export declare function getTimeValueFromSegments<T extends TimeValue = Time>(props: GetTimeValueFromSegments<T>): T;
|
||||
/**
|
||||
* Check if all the segments being used have been filled.
|
||||
* We use this to determine when we should set the value
|
||||
* store of the date field(s).
|
||||
*
|
||||
* @param segmentValues - The current `SegmentValueObj`
|
||||
* @param fieldNode - The id of the date field
|
||||
*/
|
||||
export declare function areAllTimeSegmentsFilled(segmentValues: TimeSegmentValueObj, fieldNode: HTMLElement | null): boolean;
|
||||
/**
|
||||
* Infer the granularity to use based on the
|
||||
* value and granularity props.
|
||||
*/
|
||||
export declare function inferTimeGranularity(granularity: TimeGranularity | undefined): TimeGranularity;
|
||||
/**
|
||||
* Determines if the element with the provided id is the first focusable
|
||||
* segment in the date field with the provided fieldId.
|
||||
*
|
||||
* @param id - The id of the element to check if it's the first segment
|
||||
* @param fieldNode - The id of the date field associated with the segment
|
||||
*/
|
||||
export declare function isFirstTimeSegment(id: string, fieldNode: HTMLElement | null): boolean;
|
||||
type SetTimeDescriptionProps = {
|
||||
id: string;
|
||||
formatter: TimeFormatter;
|
||||
value: TimeValue;
|
||||
doc: Document;
|
||||
};
|
||||
/**
|
||||
* Creates or updates a description element for a date field
|
||||
* which enables screen readers to read the date field's value.
|
||||
*
|
||||
* This element is hidden from view, and is portalled to the body
|
||||
* so it can be associated via `aria-describedby` and read by
|
||||
* screen readers as the user interacts with the date field.
|
||||
*/
|
||||
export declare function setTimeDescription(props: SetTimeDescriptionProps): void;
|
||||
/**
|
||||
* Removes the description element for the date field with
|
||||
* the provided ID. This function should be called when the
|
||||
* date field is unmounted.
|
||||
*/
|
||||
export declare function removeTimeDescriptionElement(id: string, doc: Document): void;
|
||||
export declare function convertTimeValueToDateValue(time: TimeValue): CalendarDateTime | ZonedDateTime;
|
||||
export declare function convertTimeValueToTime(time: TimeValue): Time;
|
||||
export declare function isTimeBefore(timeToCompare: Time, referenceTime: Time): boolean;
|
||||
export declare function isTimeAfter(timeToCompare: Time, referenceTime: Time): boolean;
|
||||
export declare function getISOTimeValue(time: TimeValue): string;
|
||||
export {};
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
import { isBrowser, isNull } from "../../is.js";
|
||||
import { CalendarDateTime, Time, ZonedDateTime } from "@internationalized/date";
|
||||
import { ALL_TIME_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS } from "./parts.js";
|
||||
import { getTimeSegments } from "./segments.js";
|
||||
import { styleToString } from "svelte-toolbelt";
|
||||
import { useId } from "../../use-id.js";
|
||||
import { getPlaceholder } from "../placeholders.js";
|
||||
import { isZonedDateTime } from "../utils.js";
|
||||
import { getDefaultHourCycle } from "./helpers.js";
|
||||
export function initializeSegmentValues() {
|
||||
const initialParts = EDITABLE_TIME_SEGMENT_PARTS.map((part) => {
|
||||
if (part === "dayPeriod") {
|
||||
return [part, "AM"];
|
||||
}
|
||||
return [part, null];
|
||||
}).filter(([key]) => {
|
||||
if (key === "literal" || key === null)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return Object.fromEntries(initialParts);
|
||||
}
|
||||
function createTimeContentObj(props) {
|
||||
const { segmentValues, formatter, locale, timeRef } = props;
|
||||
const content = Object.keys(segmentValues).reduce((obj, part) => {
|
||||
if (!isEditableTimeSegmentPart(part))
|
||||
return obj;
|
||||
if (part === "dayPeriod") {
|
||||
const value = segmentValues[part];
|
||||
if (!isNull(value)) {
|
||||
obj[part] = value;
|
||||
}
|
||||
else {
|
||||
obj[part] = getPlaceholder(part, "AM", locale);
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj[part] = getPartContent(part);
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
function getPartContent(part) {
|
||||
const value = segmentValues[part];
|
||||
const leadingZero = typeof value === "string" && value?.startsWith("0");
|
||||
const intValue = value !== null ? Number.parseInt(value) : null;
|
||||
if (!isNull(value) && !isNull(intValue)) {
|
||||
const formatted = formatter.part(timeRef.set({ [part]: value }), part, {
|
||||
hourCycle: props.hourCycle === 24 ? "h23" : undefined,
|
||||
});
|
||||
/**
|
||||
* If we're operating in a 12 hour clock and the part is an hour, we handle
|
||||
* the conversion to 12 hour format with 2 digit hours and leading zeros here.
|
||||
*/
|
||||
const is12HourMode = props.hourCycle === 12 ||
|
||||
(props.hourCycle === undefined && getDefaultHourCycle(locale) === 12);
|
||||
if (part === "hour" && is12HourMode) {
|
||||
/**
|
||||
* If the value is over 12, we convert to 12 hour format and add leading
|
||||
* zeroes if the value is less than 10.
|
||||
*/
|
||||
if (intValue > 12) {
|
||||
const hour = intValue - 12;
|
||||
if (hour === 0) {
|
||||
return "12";
|
||||
}
|
||||
else if (hour < 10) {
|
||||
return `0${hour}`;
|
||||
}
|
||||
else {
|
||||
return `${hour}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If the value is 0, we convert to 12, since 0 is not a valid 12 hour time.
|
||||
*/
|
||||
if (intValue === 0) {
|
||||
return "12";
|
||||
}
|
||||
/**
|
||||
* If the value is less than 10, we add a leading zero to the value.
|
||||
*/
|
||||
if (intValue < 10) {
|
||||
return `0${intValue}`;
|
||||
}
|
||||
/**
|
||||
* Otherwise, we don't need to do anything to the value.
|
||||
*/
|
||||
return `${intValue}`;
|
||||
}
|
||||
if (leadingZero && formatted.length === 1) {
|
||||
return `0${formatted}`;
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
else {
|
||||
return getPlaceholder(part, "", locale);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
function createTimeContentArr(props) {
|
||||
const { granularity, timeRef, formatter, contentObj, hideTimeZone, hourCycle } = props;
|
||||
const parts = formatter.toParts(timeRef, getOptsByGranularity(granularity, hourCycle));
|
||||
const timeSegmentContentArr = parts
|
||||
.map((part) => {
|
||||
const defaultParts = ["literal", "timeZoneName", null];
|
||||
if (defaultParts.includes(part.type) || !isEditableTimeSegmentPart(part.type)) {
|
||||
return {
|
||||
part: part.type,
|
||||
value: part.value,
|
||||
};
|
||||
}
|
||||
return {
|
||||
part: part.type,
|
||||
value: contentObj[part.type],
|
||||
};
|
||||
})
|
||||
.filter((segment) => {
|
||||
if (isNull(segment.part) || isNull(segment.value))
|
||||
return false;
|
||||
if (segment.part === "timeZoneName" && (!isZonedDateTime(timeRef) || hideTimeZone)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return timeSegmentContentArr;
|
||||
}
|
||||
export function createTimeContent(props) {
|
||||
const contentObj = createTimeContentObj(props);
|
||||
const contentArr = createTimeContentArr({
|
||||
contentObj,
|
||||
...props,
|
||||
});
|
||||
return {
|
||||
obj: contentObj,
|
||||
arr: contentArr,
|
||||
};
|
||||
}
|
||||
function getOptsByGranularity(granularity, hourCycle) {
|
||||
const opts = {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZoneName: "short",
|
||||
hourCycle: hourCycle === 24 ? "h23" : undefined,
|
||||
hour12: hourCycle === 24 ? false : undefined,
|
||||
};
|
||||
if (granularity === "hour") {
|
||||
delete opts.minute;
|
||||
delete opts.second;
|
||||
}
|
||||
if (granularity === "minute") {
|
||||
delete opts.second;
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
export function initTimeSegmentStates() {
|
||||
return EDITABLE_TIME_SEGMENT_PARTS.reduce((acc, key) => {
|
||||
acc[key] = {
|
||||
lastKeyZero: false,
|
||||
hasLeftFocus: true,
|
||||
updating: null,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
export function initTimeSegmentIds() {
|
||||
return Object.fromEntries(ALL_TIME_SEGMENT_PARTS.map((part) => {
|
||||
return [part, useId()];
|
||||
}).filter(([key]) => key !== "literal"));
|
||||
}
|
||||
export function isEditableTimeSegmentPart(part) {
|
||||
return EDITABLE_TIME_SEGMENT_PARTS.includes(part);
|
||||
}
|
||||
export function isAnyTimeSegmentPart(part) {
|
||||
return ALL_TIME_SEGMENT_PARTS.includes(part);
|
||||
}
|
||||
/**
|
||||
* Get the segments being used/ are rendered in the DOM.
|
||||
* We're using this to determine when to set the value of
|
||||
* the date picker, which is when all the segments have
|
||||
* been filled.
|
||||
*/
|
||||
function getUsedTimeSegments(fieldNode) {
|
||||
if (!isBrowser || !fieldNode)
|
||||
return [];
|
||||
const usedSegments = getTimeSegments(fieldNode)
|
||||
.map((el) => el.dataset.segment)
|
||||
.filter((part) => {
|
||||
return EDITABLE_TIME_SEGMENT_PARTS.includes(part);
|
||||
});
|
||||
return usedSegments;
|
||||
}
|
||||
export function getTimeValueFromSegments(props) {
|
||||
const usedSegments = getUsedTimeSegments(props.fieldNode);
|
||||
for (const part of usedSegments) {
|
||||
const value = props.segmentObj[part];
|
||||
if (isNull(value))
|
||||
continue;
|
||||
// @ts-expect-error shhh
|
||||
props.timeRef = props.timeRef.set({ [part]: props.segmentObj[part] });
|
||||
}
|
||||
return props.timeRef;
|
||||
}
|
||||
/**
|
||||
* Check if all the segments being used have been filled.
|
||||
* We use this to determine when we should set the value
|
||||
* store of the date field(s).
|
||||
*
|
||||
* @param segmentValues - The current `SegmentValueObj`
|
||||
* @param fieldNode - The id of the date field
|
||||
*/
|
||||
export function areAllTimeSegmentsFilled(segmentValues, fieldNode) {
|
||||
const usedSegments = getUsedTimeSegments(fieldNode);
|
||||
for (const part of usedSegments) {
|
||||
if (segmentValues[part] === null)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Infer the granularity to use based on the
|
||||
* value and granularity props.
|
||||
*/
|
||||
export function inferTimeGranularity(granularity) {
|
||||
if (granularity)
|
||||
return granularity;
|
||||
return "minute";
|
||||
}
|
||||
/**
|
||||
* Determines if the element with the provided id is the first focusable
|
||||
* segment in the date field with the provided fieldId.
|
||||
*
|
||||
* @param id - The id of the element to check if it's the first segment
|
||||
* @param fieldNode - The id of the date field associated with the segment
|
||||
*/
|
||||
export function isFirstTimeSegment(id, fieldNode) {
|
||||
if (!isBrowser)
|
||||
return false;
|
||||
const segments = getTimeSegments(fieldNode);
|
||||
return segments.length ? segments[0].id === id : false;
|
||||
}
|
||||
/**
|
||||
* Creates or updates a description element for a date field
|
||||
* which enables screen readers to read the date field's value.
|
||||
*
|
||||
* This element is hidden from view, and is portalled to the body
|
||||
* so it can be associated via `aria-describedby` and read by
|
||||
* screen readers as the user interacts with the date field.
|
||||
*/
|
||||
export function setTimeDescription(props) {
|
||||
if (!isBrowser)
|
||||
return;
|
||||
const valueString = props.formatter.selectedTime(props.value);
|
||||
const el = props.doc.getElementById(props.id);
|
||||
if (!el) {
|
||||
const div = props.doc.createElement("div");
|
||||
div.style.cssText = styleToString({
|
||||
display: "none",
|
||||
});
|
||||
div.id = props.id;
|
||||
div.innerText = `Selected Time: ${valueString}`;
|
||||
props.doc.body.appendChild(div);
|
||||
}
|
||||
else {
|
||||
el.innerText = `Selected Time: ${valueString}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes the description element for the date field with
|
||||
* the provided ID. This function should be called when the
|
||||
* date field is unmounted.
|
||||
*/
|
||||
export function removeTimeDescriptionElement(id, doc) {
|
||||
if (!isBrowser)
|
||||
return;
|
||||
const el = doc.getElementById(id);
|
||||
if (!el)
|
||||
return;
|
||||
doc.body.removeChild(el);
|
||||
}
|
||||
export function convertTimeValueToDateValue(time) {
|
||||
if (time instanceof Time) {
|
||||
return new CalendarDateTime(2020, 1, 1, time.hour, time.minute, time.second, time.millisecond);
|
||||
}
|
||||
return time;
|
||||
}
|
||||
export function convertTimeValueToTime(time) {
|
||||
if (time instanceof Time)
|
||||
return time;
|
||||
return new Time(time.hour, time.minute, time.second, time.millisecond);
|
||||
}
|
||||
export function isTimeBefore(timeToCompare, referenceTime) {
|
||||
return timeToCompare.compare(referenceTime) < 0;
|
||||
}
|
||||
export function isTimeAfter(timeToCompare, referenceTime) {
|
||||
return timeToCompare.compare(referenceTime) > 0;
|
||||
}
|
||||
export function getISOTimeValue(time) {
|
||||
return convertTimeValueToTime(time).toString();
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import type { DATE_SEGMENT_PARTS, EDITABLE_SEGMENT_PARTS, NON_EDITABLE_SEGMENT_PARTS, EDITABLE_TIME_SEGMENT_PARTS } from "./parts.js";
|
||||
export type DateSegmentPart = (typeof DATE_SEGMENT_PARTS)[number];
|
||||
export type TimeSegmentPart = (typeof EDITABLE_TIME_SEGMENT_PARTS)[number];
|
||||
export type EditableSegmentPart = (typeof EDITABLE_SEGMENT_PARTS)[number];
|
||||
export type NonEditableSegmentPart = (typeof NON_EDITABLE_SEGMENT_PARTS)[number];
|
||||
export type SegmentPart = EditableSegmentPart | NonEditableSegmentPart;
|
||||
export type AnyExceptLiteral = Exclude<SegmentPart, "literal">;
|
||||
export type DayPeriod = "AM" | "PM" | null;
|
||||
export type DateSegmentObj = {
|
||||
[K in DateSegmentPart]: string | null;
|
||||
};
|
||||
export type TimeSegmentObj = {
|
||||
[K in TimeSegmentPart]: K extends "dayPeriod" ? DayPeriod : string | null;
|
||||
};
|
||||
export type DateAndTimeSegmentObj = DateSegmentObj & TimeSegmentObj;
|
||||
export type SegmentValueObj = DateSegmentObj | DateAndTimeSegmentObj;
|
||||
export type SegmentContentObj = Record<EditableSegmentPart, string>;
|
||||
export type SegmentState = {
|
||||
lastKeyZero: boolean;
|
||||
hasLeftFocus: boolean;
|
||||
updating: string | null;
|
||||
};
|
||||
export type SegmentStateMap = {
|
||||
[K in EditableSegmentPart]: SegmentState;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user