Add time field and sanitise all typeable fields to prevent errors and improve experience

This commit is contained in:
Andrew Kingston 2023-11-02 20:34:40 +00:00
parent 1b5bb8dd04
commit f078039aa4
1 changed files with 121 additions and 23 deletions

View File

@ -7,6 +7,8 @@
import dayjs from "dayjs" import dayjs from "dayjs"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import Select from "../Select.svelte" import Select from "../Select.svelte"
import Icon from "../../Icon/Icon.svelte"
import ActionButton from "../../ActionButton/ActionButton.svelte"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -150,16 +152,50 @@
dispatch("change", newValue) dispatch("change", newValue)
} }
const handleYearChange = e => { const handleMinuteChange = e => {
let year = parseInt(e.target.value) handleChange(parsedValue.minute(parseInt(e.target.value)))
if (isNaN(year)) {
year = calendarDate.year()
} else {
year = Math.max(0, Math.min(9999, year))
}
e.target.value = year
calendarDate = calendarDate.year(year)
} }
const handleHourChange = e => {
handleChange(parsedValue.hour(parseInt(e.target.value)))
}
const handleDateChange = date => {
// Select this date at midnight if no current date
if (!parsedValue) {
handleChange(date)
}
// Otherwise persist selected time
else {
handleChange(
parsedValue.year(date.year()).month(date.month()).date(date.date())
)
}
}
const handleCalendarYearChange = e => {
calendarDate = calendarDate.year(parseInt(e.target.value))
}
const cleanNumber = ({ max, pad, fallback }) => {
return e => {
if (e.target.value) {
const value = parseInt(e.target.value)
if (isNaN(value)) {
e.target.value = fallback
} else {
e.target.value = Math.min(max, value).toString().padStart(pad, "0")
}
} else {
e.target.value = fallback
}
}
}
// Sanitization utils
const cleanYear = cleanNumber({ max: 9999, pad: 0, fallback: today.year() })
const cleanHour = cleanNumber({ max: 23, pad: 2, fallback: "00" })
const cleanMinute = cleanNumber({ max: 59, pad: 2, fallback: "00" })
</script> </script>
<div <div
@ -233,20 +269,22 @@
> >
<div class="month-selector"> <div class="month-selector">
<Select <Select
autoWidth
placeholder={null} placeholder={null}
options={MonthsOfYear.map((m, idx) => ({ label: m, value: idx }))} options={MonthsOfYear.map((m, idx) => ({ label: m, value: idx }))}
value={calendarDate.month()} value={calendarDate.month()}
on:change={e => (calendarDate = calendarDate.month(e.detail))} on:change={e => (calendarDate = calendarDate.month(e.detail))}
autoWidth
/> />
</div> </div>
<input <input
class="year-selector" class="custom-num-input"
on:change={handleYearChange}
type="number" type="number"
value={calendarDate.year()} value={calendarDate.year()}
min="0" min="0"
max="9999" max="9999"
onclick="this.select()"
on:change={handleCalendarYearChange}
on:input={cleanYear}
/> />
</div> </div>
<button <button
@ -308,7 +346,7 @@
aria-selected="false" aria-selected="false"
aria-invalid="false" aria-invalid="false"
title={date.format("dddd, MMMM D, YYYY")} title={date.format("dddd, MMMM D, YYYY")}
on:click={() => handleChange(date)} on:click={() => handleDateChange(date)}
> >
<span <span
role="presentation" role="presentation"
@ -327,9 +365,35 @@
</table> </table>
</div> </div>
</div> </div>
{#if parsedValue && enableTime}
<div class="time-picker">
<input
class="custom-num-input"
type="number"
value={parsedValue.hour().toString().padStart(2, "0")}
min="0"
max="23"
onclick="this.select()"
on:input={cleanHour}
on:change={handleHourChange}
/>
<span>:</span>
<input
class="custom-num-input"
type="number"
value={parsedValue.minute().toString().padStart(2, "0")}
min="0"
max="59"
onclick="this.select()"
on:input={cleanMinute}
on:change={handleMinuteChange}
/>
</div>
{/if}
</Popover> </Popover>
<style> <style>
/* Date label overrides */
.spectrum-Textfield-input { .spectrum-Textfield-input {
pointer-events: none; pointer-events: none;
} }
@ -347,15 +411,15 @@
pointer-events: none !important; pointer-events: none !important;
} }
/* Calendar */ /* Calendar overrides */
.spectrum-Calendar { .spectrum-Calendar {
padding: 8px; padding: 8px;
} }
.spectrum-Calendar-title { .spectrum-Calendar-title {
display: flex; display: flex;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: stretch;
gap: 4px; flex: 1 1 auto;
} }
.spectrum-Calendar-header button { .spectrum-Calendar-header button {
border-radius: 4px; border-radius: 4px;
@ -364,9 +428,24 @@
visibility: visible; visibility: visible;
color: var(--spectrum-global-color-gray-400); color: var(--spectrum-global-color-gray-400);
} }
.spectrum-Calendar-date.is-today:before {
border-color: var(--spectrum-global-color-gray-400);
}
.spectrum-Calendar-date.is-today {
border-color: var(--spectrum-global-color-gray-400);
}
.spectrum-Calendar-date.is-selected:not(.is-range-selection) {
background: var(--spectrum-global-color-blue-400);
}
.spectrum-Calendar-nextMonth,
.spectrum-Calendar-prevMonth {
order: 1;
padding: 4px 6px;
}
/* Month and year selector */
.month-selector :global(.spectrum-Picker), .month-selector :global(.spectrum-Picker),
.year-selector { .custom-num-input {
background: none; background: none;
border: none; border: none;
outline: none; outline: none;
@ -374,13 +453,14 @@
padding: 4px 6px; padding: 4px 6px;
border-radius: 4px; border-radius: 4px;
transition: background 130ms ease-out; transition: background 130ms ease-out;
font-size: var(--spectrum-calendar-title-text-size); font-size: 18px;
font-weight: bold; font-weight: bold;
font-family: var(--font-sans); font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
} }
.month-selector :global(.spectrum-Picker:hover), .month-selector :global(.spectrum-Picker:hover),
.month-selector :global(.spectrum-Picker.is-open), .month-selector :global(.spectrum-Picker.is-open),
.year-selector:hover { .custom-num-input:hover {
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);
} }
.month-selector :global(.spectrum-Picker-label) { .month-selector :global(.spectrum-Picker-label) {
@ -388,8 +468,26 @@
font-weight: bold; font-weight: bold;
color: var(--spectrum-alias-text-color); color: var(--spectrum-alias-text-color);
} }
.year-selector {
-webkit-font-smoothing: antialiased; /* Time picker */
margin-right: -20px; .time-picker {
margin-top: 4px;
border-top: 1px solid var(--spectrum-global-color-gray-200);
padding: 12px;
display: flex;
flex-direction: row;
align-items: center;
}
.time-picker span {
font-weight: bold;
font-size: 18px;
z-index: -1;
color: var(--spectrum-global-color-gray-700);
}
.time-picker .custom-num-input:first-child {
margin-right: -16px;
}
.time-picker .custom-num-input:last-child {
margin-left: 8px;
} }
</style> </style>