time input: refactor to allow setting value reactively

This commit is contained in:
Elijah Duffy
2025-07-22 15:39:52 -07:00
parent 2e3de49851
commit a99384e887
2 changed files with 79 additions and 77 deletions

View File

@@ -20,7 +20,7 @@
import StyledRawInput from './StyledRawInput.svelte';
import moment from 'moment';
import type { ClassValue } from 'svelte/elements';
import { generateIdentifier, targetMust } from './util';
import { generateIdentifier, prefixZero, targetMust } from './util';
import { FocusManager } from './focus';
import { Time } from '@internationalized/date';
@@ -54,9 +54,22 @@
type componentKey = 'hour' | 'minute';
const id = $derived(generateIdentifier('time-input', name));
const values: Record<componentKey, string> = $state({
hour: '',
minute: ''
const values: Record<componentKey, string> = $derived.by(() => {
let hour = '';
if (value) {
if (value.hour > 12) {
hour = (value.hour - 12).toString();
} else if (value.hour === 0) {
hour = '12'; // 12 AM case
} else {
hour = value.hour.toString();
}
}
return {
hour,
minute: !value ? '' : prefixZero(value.minute.toString())
};
});
let ampm: ampmKey = $state('AM');
let valid: boolean = $state(true);
@@ -87,7 +100,6 @@
* @returns true if the value was incremented, false if it looped back to 0
*/
const incrementValue = (input: HTMLInputElement, max: number, start: number): boolean => {
const f = () => {
if (input.value.length === 0) {
input.value = start.toString();
return true;
@@ -106,11 +118,6 @@
}
};
const res = f();
updateValue();
return res;
};
/**
* decrementValue decrements the value of the input by 1
* @param input The input element to decrement
@@ -119,7 +126,6 @@
* @returns true if the value was decremented, false if it looped back to max
*/
const decrementValue = (input: HTMLInputElement, max: number, start: number): boolean => {
const f = () => {
if (input.value.length === 0) {
input.value = max.toString();
return true;
@@ -135,20 +141,15 @@
}
};
const res = f();
updateValue();
return res;
};
/**
* updateValue updates `value` with the current time in 24-hour format.
* If any component is invalid or blank, it sets `value` to an empty string.
*/
const updateValue = () => {
let ampmLocal = ampm;
let hourValue = parseInt(values.hour);
let minuteValue = parseInt(values.minute);
const updateValue = (override?: Partial<Record<componentKey, string>>) => {
let hourValue = parseInt(override?.hour || values.hour);
let minuteValue = parseInt(override?.minute || values.minute);
// if no hour, clear value
if (isNaN(hourValue)) {
value = null;
formattedValue = '';
@@ -156,16 +157,21 @@
return;
}
// internally convert to 24-hour format
// if hour is greater than 12, set to 12-hour format
if (hourValue > 12) {
hourValue = hourValue - 12;
ampmLocal = 'PM';
hourValue -= 12;
ampm = 'PM';
}
// convert to 24-hour format
if (ampm === 'PM' && hourValue < 12) hourValue += 12;
else if (ampm === 'AM' && hourValue >= 12) hourValue -= 12;
// default minutes to zero if empty
if (isNaN(minuteValue)) {
minuteValue = 0;
}
value = new Time(hourValue + (ampmLocal === 'PM' ? 12 : 0), minuteValue, 0);
value = new Time(hourValue, minuteValue);
updateHiddenInput();
// update formatted value
@@ -187,9 +193,9 @@
{
max: number;
pattern: RegExp;
onkeydown: (e: KeyboardEvent) => void;
oninput: (e: Event) => void;
onblur: (e: FocusEvent) => void;
onkeydown?: (e: KeyboardEvent) => void;
oninput?: (e: Event) => void;
onblur?: (e: FocusEvent) => void;
divider?: string;
placeholder?: string;
}
@@ -207,37 +213,27 @@
}
if (e.key === 'ArrowUp') {
if (!incrementValue(target, 12, 1)) {
toggleAMPM();
}
incrementValue(target, 12, 1);
if (target.value === '12') toggleAMPM();
} else if (e.key === 'ArrowDown') {
if (!decrementValue(target, 12, 1)) {
toggleAMPM();
}
decrementValue(target, 12, 1);
if (target.value === '11') toggleAMPM();
} else {
return;
}
updateValue({ hour: target.value });
e.preventDefault();
},
oninput: (e) => {
const target = targetMust<HTMLInputElement>(e);
updateValue();
if (target.value.length === 2) {
focusList.focusNext();
}
},
onblur: (e: FocusEvent) => {
onblur: (e) => {
const target = targetMust<HTMLInputElement>(e);
const hourValue = parseInt(target.value);
if (hourValue > 12) {
target.value = (hourValue - 12).toString();
ampm = 'PM';
}
updateValue();
updateValue({ hour: target.value });
},
divider: ':'
},
@@ -256,13 +252,12 @@
return;
}
updateValue({ minute: target.value });
e.preventDefault();
},
oninput: () => {
updateValue();
},
onblur: (e: FocusEvent) => {
const target = targetMust<HTMLInputElement>(e);
updateValue({ minute: target.value });
if (target.value.length === 1) {
target.value = '0' + target.value;
}
@@ -295,7 +290,7 @@
'text-center text-xl focus:placeholder:text-transparent',
compact ? 'h-9 w-16!' : 'h-16 w-24!'
]}
bind:value={values[key]}
value={values[key]}
inputmode="numeric"
pattern="[0-9]*"
placeholder={opts.placeholder ?? '00'}
@@ -306,7 +301,7 @@
onkeydown={opts.onkeydown}
oninput={opts.oninput}
onblur={opts.onblur}
{@attach focusList.input()}
{@attach focusList.input({ selectAll: true })}
/>
{#if opts.divider}

View File

@@ -266,7 +266,14 @@
<TimeInput label="Compact time" compact bind:value={timeValue} />
</InputGroup>
<InputGroup>
<p>Selected time is {formatTime(timeValue, 'undefined')}</p>
<p>Selected time is {formatTime(timeValue, 'undefined')} ({timeValue?.toString()})</p>
<Button
onclick={() => {
timeValue = new Time(15, 0);
}}
>
Set 3:00 PM
</Button>
</InputGroup>
</div>