Skip to content
This repository has been archived by the owner on Feb 1, 2025. It is now read-only.

Commit

Permalink
Use Melt combobox for datetime input
Browse files Browse the repository at this point in the history
There's a known issue where the first item isn't selected by default
when the user types. Need to figure that out.
  • Loading branch information
tylermercer committed Sep 13, 2024
1 parent fef111f commit 33fc188
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 55 deletions.
189 changes: 141 additions & 48 deletions src/lib/components/controls/DatetimeInput.svelte
Original file line number Diff line number Diff line change
@@ -1,64 +1,157 @@
<script lang="ts">
import { parse } from 'chrono-node';
import { createCombobox, melt, type ComboboxOptionProps } from '@melt-ui/svelte';
import { parse, type ParsedResult } from 'chrono-node';
import { dateToString } from '$lib/util/dateUtils';
import { fly } from 'svelte/transition';
import UpArrow from 'virtual:icons/teenyicons/up-outline';
export let date = new Date();
export let id = '';
export let autofocus = false;
let inputValue = dateToString(date);
let displayValue = '';
const now = new Date();
function getDate(input: string) {
const results = parse(input);
return results.length > 0 ? results[0].date() : undefined;
}
export let date = now;
export let autofocus = false;
function handleInput(event: Event) {
const input = (event.target as HTMLInputElement).value;
const result = getDate(input);
displayValue = result ? dateToString(result) : '';
}
const toOption = (result: ParsedResult): ComboboxOptionProps<Date> => ({
value: result.date(),
label: dateToString(result.date())
});
function handleBlur() {
inputValue = dateToString(date);
displayValue = '';
}
function handleEnter(event: Event) {
const kbdEvent = event as KeyboardEvent;
if (kbdEvent.key === 'Enter' && displayValue) {
const result = getDate((kbdEvent.target as HTMLInputElement).value);
if (result) {
date = result;
inputValue = result.toLocaleString();
(kbdEvent.target as HTMLInputElement).blur();
const {
elements: { menu, input, option, label },
states: { open, inputValue, touchedInput, selected },
// helpers: { isSelected }
} = createCombobox<Date>({
forceVisible: true,
onSelectedChange({next}) {
console.log("selected change");
if (next) {
date = next.value;
}
return next;
},
onOpenChange({ next }) {
if (!next) handleClose();
return next;
}
}
});
function handleFocus(event: FocusEvent) {
(event.target as HTMLInputElement).select();
function handleClose(){
console.log("Close handled");
$selected = resultDates.at(0);
$inputValue = $selected?.label ?? '';
}
$: resultDates = $touchedInput ? parse($inputValue).map(toOption) : [];
</script>

<!-- svelte-ignore a11y-autofocus -->
<input
{id}
{autofocus}
type="text"
bind:value={inputValue}
on:input={handleInput}
on:blur={handleBlur}
on:keydown={handleEnter}
on:focus={handleFocus}
/>
{#if displayValue}
<div class="result">{displayValue}</div>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control - $label contains the 'for' attribute -->
<label use:melt={$label}>Date and time</label>

<div class="input-container">
<!-- svelte-ignore a11y-autofocus -->
<input use:melt={$input} placeholder="Right now" {autofocus} />
<div class="arrow" class:arrow-open={$open}>
<UpArrow />
</div>
</div>
</div>
{#if $open}
<ul class="menu" use:melt={$menu} transition:fly={{ duration: 150, y: -5 }}>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="inner-menu" tabindex="0">
{#each resultDates as result, index (index)}
<li use:melt={$option(result)} class="option">{result.label}</li>
{:else}
<li class="no-results">
{#if $inputValue}
Invalid date
{:else}
e.g. "1 hour ago" or "yesterday at 4pm"
{/if}
</li>

{/each}
</div>
</ul>
{/if}

<style>
.result {
margin-top: 5px;
<style lang="scss">
.input-container {
position: relative;
}
li {
list-style-type: none;
}
.menu {
list-style-type: none;
padding: 0;
z-index: 10;
display: flex;
max-height: 300px;
flex-direction: column;
overflow: hidden;
border-radius: 0.5rem;
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.inner-menu {
display: flex;
max-height: 100%;
flex-direction: column;
gap: 0;
overflow-y: auto;
background-color: var(--gray-2);
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--gray-12);
}
.arrow {
pointer-events: none;
transform-origin: center;
font-size: var(--step--1);
position: absolute;
right: 0;
aspect-ratio: 1;
height: 100%;
top: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
color: var(--gray-11);
transition: transform ease-in-out 100ms;
&-open {
transform: rotate(-180deg);
}
}
.option,
.no-results {
position: relative;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
}
.option {
cursor: pointer;
color: var(--gray-12);
&:hover {
background-color: var(--primary-3);
color: var(--primary-12);
}
&[data-highlighted] {
background-color: var(--primary-4);
color: var(--primary-12);
}
&[data-disabled] {
opacity: 0.5;
}
}
.no-results {
color: var(--gray-11);
}
</style>
8 changes: 1 addition & 7 deletions src/lib/components/entries/EntryForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,7 @@
{/each}
<p>{status}</p>
<div class="l-switcher l-space-xs date-and-buttons">
<label for="datetime" class="label-datetime">
Date and time
<DatetimeInput id="datetime" bind:date={datetime} autofocus={!answeredQuestions.length} />
</label>
<DatetimeInput bind:date={datetime} autofocus={!answeredQuestions.length} />
<div class="l-cluster-r l-space-xs">
<button type="submit" aria-busy={saving}>
{#if saving}
Expand All @@ -131,9 +128,6 @@
align-items: flex-end;
--l-switcher-threshold: 500px;
}
.label-datetime {
margin-bottom: 0;
}
fieldset.yes-no.l-switcher {
--l-switcher-threshold: 10rem;
}
Expand Down

0 comments on commit 33fc188

Please sign in to comment.