add focus tool; time input: refactor away from bind:ref

This commit is contained in:
Elijah Duffy
2025-07-21 19:41:44 -07:00
parent e3c08b4247
commit 123594f828
3 changed files with 231 additions and 122 deletions

View File

@@ -2,10 +2,10 @@
import { liveValidator, validate } from '@svelte-toolkit/validate';
import Label from './Label.svelte';
import StyledRawInput from './StyledRawInput.svelte';
import { onMount } from 'svelte';
import moment from 'moment';
import type { ClassValue } from 'svelte/elements';
import { generateIdentifier } from './util';
import { generateIdentifier, targetMust } from './util';
import { FocusManager } from './focus';
interface Props {
name?: string;
@@ -32,20 +32,19 @@
}: Props = $props();
type ampmKey = 'AM' | 'PM';
type componentsKey = 'hour' | 'minute';
type componentKey = 'hour' | 'minute';
const id = $derived(generateIdentifier('time-input', name));
const values: Record<componentKey, string> = $state({
hour: '',
minute: ''
});
let ampm: ampmKey = $state('AM');
let valid: boolean = $state(true);
let hiddenInput: HTMLInputElement;
let btnrefs: Partial<Record<ampmKey, HTMLButtonElement | null>> = $state({});
const inrefs: Record<componentsKey, HTMLInputElement | null> = $state({
hour: null,
minute: null
});
const focusList = new FocusManager();
const hour24Pattern = /^(0?[0-9]|1[0-9]|2[0-3])$/;
const minutePattern = /^(0?[0-9]|[1-5][0-9])$/;
const keydownValidatorOpts = { constrain: true };
@@ -70,7 +69,6 @@
*/
const incrementValue = (input: HTMLInputElement, max: number, start: number): boolean => {
const f = () => {
console.log('incrementing', input);
if (input.value.length === 0) {
input.value = start.toString();
return true;
@@ -90,7 +88,7 @@
};
const res = f();
selectEnd(input);
updateValue();
return res;
};
@@ -119,24 +117,10 @@
};
const res = f();
selectEnd(input);
updateValue();
return res;
};
/**
* selectEnd selects the end of the input value
* @param input The input element to select
*/
const selectEnd = (input: HTMLInputElement) => {
if (input) {
setTimeout(() => {
console.log('selecting end', input, input.isConnected);
input.setSelectionRange(input.value.length, input.value.length);
console.log('selected end vibes');
}, 0);
}
};
/**
* prefixZero adds a leading zero to the string if it is less than 10
* @param str The string to prefix
@@ -153,11 +137,9 @@
* If any component is invalid or blank, it sets `value` to an empty string.
*/
const updateValue = () => {
if (!inrefs.hour || !inrefs.minute) return;
let ampmLocal = ampm;
let hourValue = parseInt(inrefs.hour.value);
let minuteValue = parseInt(inrefs.minute.value);
let hourValue = parseInt(values.hour);
let minuteValue = parseInt(values.minute);
if (isNaN(hourValue)) {
value = '';
@@ -192,23 +174,14 @@
hiddenInput.dispatchEvent(new KeyboardEvent('keyup'));
};
onMount(() => {
if (inrefs.hour) {
liveValidator(inrefs.hour, keydownValidatorOpts);
}
if (inrefs.minute) {
liveValidator(inrefs.minute, keydownValidatorOpts);
}
});
const components: Record<
componentsKey,
componentKey,
{
max: number;
pattern: RegExp;
onkeydown: (e: KeyboardEvent) => void;
oninput: () => void;
onblur: () => void;
oninput: (e: Event) => void;
onblur: (e: FocusEvent) => void;
divider?: string;
placeholder?: string;
}
@@ -218,44 +191,41 @@
max: 24,
pattern: hour24Pattern,
onkeydown: (e: KeyboardEvent) => {
if (!inrefs.hour) return;
const target = targetMust<HTMLInputElement>(e);
if (e.key === ':' && inrefs.hour.value.length !== 0) {
inrefs.minute?.focus();
if (e.key === ':' && target.value.length !== 0) {
focusList.focusNext();
e.preventDefault();
}
if (e.key === 'ArrowRight' && inrefs.hour.selectionEnd === inrefs.hour.value.length) {
inrefs.minute?.focus();
}
if (e.key === 'ArrowUp') {
if (!incrementValue(inrefs.hour, 12, 1)) {
if (!incrementValue(target, 12, 1)) {
toggleAMPM();
}
}
if (e.key === 'ArrowDown') {
if (!decrementValue(inrefs.hour, 12, 1)) {
} else if (e.key === 'ArrowDown') {
if (!decrementValue(target, 12, 1)) {
toggleAMPM();
}
} else {
return;
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
}
e.preventDefault();
},
oninput: () => {
oninput: (e) => {
const target = targetMust<HTMLInputElement>(e);
updateValue();
if (inrefs.hour?.value.length === 2) {
inrefs.minute?.focus();
if (target.value.length === 2) {
focusList.focusNext();
}
},
onblur: () => {
if (!inrefs.hour) return;
const hourValue = parseInt(inrefs.hour.value);
onblur: (e: FocusEvent) => {
const target = targetMust<HTMLInputElement>(e);
const hourValue = parseInt(target.value);
if (hourValue > 12) {
inrefs.hour.value = (hourValue - 12).toString();
target.value = (hourValue - 12).toString();
ampm = 'PM';
}
@@ -268,44 +238,25 @@
max: 59,
pattern: minutePattern,
onkeydown: (e: KeyboardEvent) => {
if (!inrefs.minute) return;
const target = e.target as HTMLInputElement;
console.log(
e.key,
inrefs.minute.selectionStart,
inrefs.minute.selectionEnd,
target.selectionStart
);
if (e.key === 'ArrowLeft' && inrefs.minute.selectionStart === 0) {
inrefs.hour?.focus();
} else if (
e.key === 'ArrowRight' &&
inrefs.minute.selectionEnd === inrefs.minute.value.length
) {
btnrefs.AM?.focus();
} else if (e.key === 'Backspace' && inrefs.minute.value.length === 0) {
inrefs.hour?.focus();
}
const target = targetMust<HTMLInputElement>(e);
if (e.key === 'ArrowUp') {
incrementValue(target, 59, 0);
}
if (e.key === 'ArrowDown') {
decrementValue(inrefs.minute, 59, 0);
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
} else if (e.key === 'ArrowDown') {
decrementValue(target, 59, 0);
} else {
return;
}
e.preventDefault();
},
oninput: () => {
updateValue();
},
onblur: () => {
if (inrefs.minute?.value.length === 1) {
inrefs.minute.value = '0' + inrefs.minute.value;
onblur: (e: FocusEvent) => {
const target = targetMust<HTMLInputElement>(e);
if (target.value.length === 1) {
target.value = '0' + target.value;
}
}
}
@@ -329,25 +280,26 @@
<div class="flex items-center">
<!-- Hour, Minute Inputs -->
{#each ['hour', 'minute'] as componentsKey[] as key}
{@const value = components[key]}
{#each ['hour', 'minute'] as componentKey[] as key}
{@const opts = components[key]}
<StyledRawInput
bind:ref={inrefs[key as componentsKey]}
bind:value={values[key]}
inputmode="numeric"
pattern="[0-9]*"
placeholder={value.placeholder ?? '00'}
name={name + '_' + key}
max={value.max}
placeholder={opts.placeholder ?? '00'}
max={opts.max}
min={0}
class="time-input"
validate={{ pattern: value.pattern }}
onkeydown={value.onkeydown}
oninput={value.oninput}
onblur={value.onblur}
validate={{ pattern: opts.pattern }}
use={(n) => liveValidator(n, keydownValidatorOpts)}
onkeydown={opts.onkeydown}
oninput={opts.oninput}
onblur={opts.onblur}
{@attach focusList.input()}
/>
{#if value.divider}
<span class="mx-2 text-3xl">{value.divider}</span>
{#if opts.divider}
<span class="mx-2 text-3xl">{opts.divider}</span>
{/if}
{/each}
@@ -355,7 +307,6 @@
<div class="ml-3 flex flex-col">
{#each ['AM', 'PM'] as (typeof ampm)[] as shade, index}
<button
bind:this={btnrefs[shade]}
class={[
'border-sui-accent dark:border-sui-accent/50 cursor-pointer border px-3 py-1 font-medium',
ampm === shade && [
@@ -368,23 +319,7 @@
ampm = shade;
updateValue();
}}
onkeydown={(e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
if (index === 0) {
inrefs.minute?.focus();
} else {
btnrefs.AM?.focus();
}
} else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
if (index === 0) {
btnrefs.PM?.focus();
}
} else {
return;
}
e.preventDefault();
}}
{@attach focusList.button()}
>
{shade}
</button>