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

View File

@@ -266,7 +266,14 @@
<TimeInput label="Compact time" compact bind:value={timeValue} /> <TimeInput label="Compact time" compact bind:value={timeValue} />
</InputGroup> </InputGroup>
<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> </InputGroup>
</div> </div>