DurationInput: limit precision to TimeDuration type

Remove support for 'days', adds support for 'milliseconds'
This commit is contained in:
Elijah Duffy
2025-12-11 15:42:59 -08:00
parent f843c91284
commit 0608343741

View File

@@ -1,6 +1,6 @@
<script lang="ts" module> <script lang="ts" module>
/** /**
* formatDuration returns a human readable string for a DateTimeDuration. * formatDuration returns a human readable string for a TimeDuration.
* *
* @param duration The duration to format, can be null or undefined. * @param duration The duration to format, can be null or undefined.
* @param fallback Optional fallback string to return if duration is null or undefined. * @param fallback Optional fallback string to return if duration is null or undefined.
@@ -9,12 +9,11 @@
* @returns The duration in human readable format or the fallback string if undefined. * @returns The duration in human readable format or the fallback string if undefined.
*/ */
export const formatDuration = ( export const formatDuration = (
duration: DateTimeDuration | null | undefined, duration: TimeDuration | null | undefined,
fallback?: string fallback?: string
): string => { ): string => {
if (!duration) return fallback ?? ''; if (!duration) return fallback ?? '';
const parts: string[] = []; const parts: string[] = [];
if (duration.days) parts.push(`${duration.days}d`);
if (duration.hours) parts.push(`${duration.hours}h`); if (duration.hours) parts.push(`${duration.hours}h`);
if (duration.minutes) parts.push(`${duration.minutes}m`); if (duration.minutes) parts.push(`${duration.minutes}m`);
if (duration.seconds) parts.push(`${duration.seconds}s`); if (duration.seconds) parts.push(`${duration.seconds}s`);
@@ -22,13 +21,12 @@
}; };
/** /**
* durationToISO8601 converts a DateTimeDuration to an ISO 8601 duration string. * durationToISO8601 converts a TimeDuration to an ISO 8601 duration string.
* @param duration The duration to convert. * @param duration The duration to convert.
* @returns The ISO 8601 duration string. * @returns The ISO 8601 duration string.
*/ */
export const durationToISO8601 = (duration: DateTimeDuration): string => { export const durationToISO8601 = (duration: TimeDuration): string => {
let str = 'P'; let str = 'P';
if (duration.days) str += `${duration.days}D`;
if (duration.hours || duration.minutes || duration.seconds) str += 'T'; if (duration.hours || duration.minutes || duration.seconds) str += 'T';
if (duration.hours) str += `${duration.hours}H`; if (duration.hours) str += `${duration.hours}H`;
if (duration.minutes) str += `${duration.minutes}M`; if (duration.minutes) str += `${duration.minutes}M`;
@@ -37,18 +35,17 @@
}; };
/** /**
* iso8601ToDuration converts an ISO 8601 duration string to a DateTimeDuration. * iso8601ToDuration converts an ISO 8601 duration string to a TimeDuration.
* @param str The ISO 8601 duration string. * @param str The ISO 8601 duration string.
* @returns The DateTimeDuration object. * @returns The TimeDuration object.
*/ */
export const iso8601ToDuration = (str: string): DateTimeDuration => { export const iso8601ToDuration = (str: string): TimeDuration => {
const regex = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/; const regex = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
const matches = str.match(regex); const matches = str.match(regex);
if (!matches) { if (!matches) {
throw new Error('Invalid ISO 8601 duration string'); throw new Error('Invalid ISO 8601 duration string');
} }
return { return {
days: matches[1] ? parseInt(matches[1], 10) : 0,
hours: matches[2] ? parseInt(matches[2], 10) : 0, hours: matches[2] ? parseInt(matches[2], 10) : 0,
minutes: matches[3] ? parseInt(matches[3], 10) : 0, minutes: matches[3] ? parseInt(matches[3], 10) : 0,
seconds: matches[4] ? parseInt(matches[4], 10) : 0 seconds: matches[4] ? parseInt(matches[4], 10) : 0
@@ -57,10 +54,10 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import type { DateTimeDuration } from '@internationalized/date'; import type { TimeDuration } from '@internationalized/date';
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
import { FocusManager } from './focus'; import { FocusManager } from './focus';
import { generateIdentifier, prefixZero, targetMust } from './util'; import { generateIdentifier, targetMust } from './util';
import { decrementValue, incrementValue } from './numeric-utils'; import { decrementValue, incrementValue } from './numeric-utils';
import Label from './Label.svelte'; import Label from './Label.svelte';
import { liveValidator, validate } from '@svelte-toolkit/validate'; import { liveValidator, validate } from '@svelte-toolkit/validate';
@@ -76,8 +73,8 @@
label?: string; label?: string;
/** Precision for duration input */ /** Precision for duration input */
precision?: { min: componentKey; max: componentKey }; precision?: { min: componentKey; max: componentKey };
/** Bindable DateTimeDuration value */ /** Bindable TimeDuration value */
value?: DateTimeDuration | null; value?: TimeDuration | null;
/** Bindable formatted duration string, always matches current value and cannot be set */ /** Bindable formatted duration string, always matches current value and cannot be set */
formattedValue?: string; formattedValue?: string;
/** Whether the input is required */ /** Whether the input is required */
@@ -89,7 +86,7 @@
/** Additional classes are applied to the root container (div element) */ /** Additional classes are applied to the root container (div element) */
class?: ClassValue | null; class?: ClassValue | null;
/** Triggered whenever the duration value is changed by the user */ /** Triggered whenever the duration value is changed by the user */
onchange?: (details: { duration: DateTimeDuration | null; formattedDuration: string }) => void; onchange?: (details: { duration: TimeDuration | null; formattedDuration: string }) => void;
} }
let { let {
@@ -105,7 +102,7 @@
onchange onchange
}: Props = $props(); }: Props = $props();
const COMPONENT_KEYS = ['days', 'hours', 'minutes', 'seconds'] as const; const COMPONENT_KEYS = ['hours', 'minutes', 'seconds', 'milliseconds'] as const;
type componentKey = (typeof COMPONENT_KEYS)[number]; type componentKey = (typeof COMPONENT_KEYS)[number];
/** selected components are controlled by min and max precision */ /** selected components are controlled by min and max precision */
let selectedComponents: componentKey[] = $derived.by(() => { let selectedComponents: componentKey[] = $derived.by(() => {
@@ -128,7 +125,7 @@
if (!value || value[key] === 0) { if (!value || value[key] === 0) {
acc[key] = ''; acc[key] = '';
} else { } else {
acc[key] = prefixZero(value[key]?.toString() || '0'); acc[key] = value[key]?.toString() || '0';
} }
return acc; return acc;
}, },
@@ -141,10 +138,10 @@
/** updateValue updates `value` with the current values from the input fields */ /** updateValue updates `value` with the current values from the input fields */
const updateValue = (override?: Partial<Record<componentKey, string>>) => { const updateValue = (override?: Partial<Record<componentKey, string>>) => {
const newValues: Record<componentKey, number> = { const newValues: Record<componentKey, number> = {
days: parseInt(override?.days ?? values.days) || 0,
hours: parseInt(override?.hours ?? values.hours) || 0, hours: parseInt(override?.hours ?? values.hours) || 0,
minutes: parseInt(override?.minutes ?? values.minutes) || 0, minutes: parseInt(override?.minutes ?? values.minutes) || 0,
seconds: parseInt(override?.seconds ?? values.seconds) || 0 seconds: parseInt(override?.seconds ?? values.seconds) || 0,
milliseconds: parseInt(override?.milliseconds ?? values.milliseconds) || 0
}; };
let zero = true; let zero = true;
@@ -178,11 +175,9 @@
* detect the change. * detect the change.
*/ */
const updateHiddenInput = () => { const updateHiddenInput = () => {
// TODO: Make sure this formats correctly
hiddenInput.value = value ? durationToISO8601(value) : ''; hiddenInput.value = value ? durationToISO8601(value) : '';
hiddenInput.dispatchEvent(new KeyboardEvent('keyup')); hiddenInput.dispatchEvent(new KeyboardEvent('keyup'));
onchange?.({ duration: value, formattedDuration: formattedValue }); onchange?.({ duration: value, formattedDuration: formattedValue });
console.log('updated hidden input', hiddenInput.value);
}; };
/** /**
@@ -217,6 +212,7 @@
const components: Record< const components: Record<
componentKey, componentKey,
{ {
label?: string;
pattern: RegExp; pattern: RegExp;
onkeydown?: (e: KeyboardEvent) => void; onkeydown?: (e: KeyboardEvent) => void;
oninput?: (e: Event) => void; oninput?: (e: Event) => void;
@@ -225,10 +221,6 @@
placeholder?: string; placeholder?: string;
} }
> = { > = {
days: {
pattern: numericPattern,
...buildEventHandlers('days')
},
hours: { hours: {
pattern: numericPattern, pattern: numericPattern,
divider: ':', divider: ':',
@@ -243,8 +235,15 @@
}, },
seconds: { seconds: {
pattern: numericPattern, pattern: numericPattern,
divider: '.',
placeholder: '0', placeholder: '0',
...buildEventHandlers('seconds') ...buildEventHandlers('seconds')
},
milliseconds: {
label: 'MS',
pattern: numericPattern,
placeholder: '000',
...buildEventHandlers('milliseconds')
} }
}; };
</script> </script>
@@ -287,7 +286,7 @@
{@attach focusList.input({ selectAll: true })} {@attach focusList.input({ selectAll: true })}
/> />
<Label for={partID} class={['capitalize', compact && '-mt-0.5']}>{key}</Label> <Label for={partID} class={['capitalize', compact && '-mt-0.5']}>{opts.label ?? key}</Label>
</div> </div>
{#if opts.divider && index < selectedComponents.length - 1} {#if opts.divider && index < selectedComponents.length - 1}