date input: add labels, validation, fix NaN checking

This commit is contained in:
Elijah Duffy
2025-07-03 11:08:02 -07:00
parent a307ffee92
commit 7a7fade491
2 changed files with 52 additions and 30 deletions

View File

@@ -2,7 +2,9 @@
import { InputValidatorEvent, liveValidator, validate } from '@svelte-toolkit/validate'; import { InputValidatorEvent, liveValidator, validate } from '@svelte-toolkit/validate';
import { CalendarBlank } from 'phosphor-svelte'; import { CalendarBlank } from 'phosphor-svelte';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
import type { ClassValue, FormEventHandler, KeyboardEventHandler } from 'svelte/elements'; import type { ClassValue, KeyboardEventHandler } from 'svelte/elements';
import { generateIdentifier } from './util.js';
import Label from './Label.svelte';
type FormatString = 'year' | 'month' | 'day'; type FormatString = 'year' | 'month' | 'day';
const blankState = { const blankState = {
@@ -16,7 +18,9 @@
value?: Date; value?: Date;
min?: Date; min?: Date;
max?: Date; max?: Date;
label?: string;
required?: boolean; required?: boolean;
invalidMessage?: string;
class?: ClassValue | undefined | null; class?: ClassValue | undefined | null;
format?: FormatString[]; format?: FormatString[];
} }
@@ -24,13 +28,19 @@
let { let {
name, name,
value = $bindable<Date | undefined>(), value = $bindable<Date | undefined>(),
/** min specifies lower bounds for the date input (WARNING: NOT IMPLEMENTED) */
min = new Date(1900, 0, 1), min = new Date(1900, 0, 1),
/** max specifies upper bounds for the date input (WARNING: NOT IMPLEMENTED) */
max = new Date(2100, 11, 31), max = new Date(2100, 11, 31),
label,
required = false, required = false,
invalidMessage = 'Valid date is required',
class: classList, class: classList,
format = ['year', 'month', 'day'] format = ['year', 'month', 'day']
}: Props = $props(); }: Props = $props();
const id = $derived(generateIdentifier('dateinput', name));
const inputSnippets = $derived.by(() => { const inputSnippets = $derived.by(() => {
const found: Partial<Record<FormatString, boolean>> = {}; const found: Partial<Record<FormatString, boolean>> = {};
const arr: Snippet[] = []; const arr: Snippet[] = [];
@@ -56,11 +66,8 @@
let valid = $state(true); let valid = $state(true);
let containerElement: HTMLDivElement; let containerElement: HTMLDivElement;
let yearValid = $state(true);
let yearElement = $state<HTMLDivElement | null>(null); let yearElement = $state<HTMLDivElement | null>(null);
let monthValid = $state(true);
let monthElement = $state<HTMLDivElement | null>(null); let monthElement = $state<HTMLDivElement | null>(null);
let dayValid = $state(true);
let dayElement = $state<HTMLDivElement | null>(null); let dayElement = $state<HTMLDivElement | null>(null);
let previousYearValue = $state<string | undefined>(undefined); let previousYearValue = $state<string | undefined>(undefined);
let yearValue = $derived.by(() => { let yearValue = $derived.by(() => {
@@ -108,8 +115,8 @@
setPrevious(); setPrevious();
previousYearValue = undefined; previousYearValue = undefined;
value = undefined; value = undefined;
return;
} }
return;
} }
} }
@@ -121,8 +128,8 @@
setPrevious(); setPrevious();
previousMonthValue = undefined; previousMonthValue = undefined;
value = undefined; value = undefined;
return;
} }
return;
} }
} }
@@ -134,8 +141,8 @@
setPrevious(); setPrevious();
previousDayValue = undefined; previousDayValue = undefined;
value = undefined; value = undefined;
return;
} }
return;
} }
} }
@@ -193,6 +200,12 @@
}; };
</script> </script>
<div>
<!-- Date input Label -->
{#if label}
<Label for={id}>{label}</Label>
{/if}
<div <div
bind:this={containerElement} bind:this={containerElement}
class={[ class={[
@@ -204,7 +217,13 @@
classList classList
]} ]}
> >
<input type="hidden" {name} value={value?.toISOString() ?? ''} /> <input
type="hidden"
{name}
{id}
value={value?.toISOString() ?? ''}
use:validate={{ required }}
/>
{#each inputSnippets as snippet, i} {#each inputSnippets as snippet, i}
{@render snippet()} {@render snippet()}
@@ -217,6 +236,12 @@
<CalendarBlank size="1.5em" class="ml-auto" /> <CalendarBlank size="1.5em" class="ml-auto" />
</div> </div>
<!-- Error message if invalid -->
<div class={['opacity-0 transition-opacity', !valid && 'opacity-100']}>
<Label for={id} error>{invalidMessage}</Label>
</div>
</div>
{#snippet year()} {#snippet year()}
<div <div
bind:this={yearElement} bind:this={yearElement}
@@ -235,7 +260,6 @@
}} }}
use:liveValidator={{ constrain: true }} use:liveValidator={{ constrain: true }}
onvalidate={(e) => { onvalidate={(e) => {
yearValid = e.detail.valid;
handleComponentValidate(e); handleComponentValidate(e);
}} }}
></div> ></div>
@@ -259,7 +283,6 @@
}} }}
use:liveValidator={{ constrain: true }} use:liveValidator={{ constrain: true }}
onvalidate={(e) => { onvalidate={(e) => {
monthValid = e.detail.valid;
handleComponentValidate(e); handleComponentValidate(e);
}} }}
></div> ></div>
@@ -283,7 +306,6 @@
}} }}
use:liveValidator={{ constrain: true }} use:liveValidator={{ constrain: true }}
onvalidate={(e) => { onvalidate={(e) => {
dayValid = e.detail.valid;
handleComponentValidate(e); handleComponentValidate(e);
}} }}
></div> ></div>

View File

@@ -15,7 +15,7 @@
'transition-fontColor block', 'transition-fontColor block',
error && !bigError error && !bigError
? 'mt-1 text-sm font-normal text-red-500' ? 'mt-1 text-sm font-normal text-red-500'
: 'text-text dark:text-background mb-3 text-base font-medium', : 'text-text dark:text-background mb-2 text-base font-medium',
bigError && 'text-red-500!' bigError && 'text-red-500!'
]} ]}
> >