From 81f939649051228a150f1409f716d43df219d94e Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Tue, 20 Jan 2026 03:31:55 +0000 Subject: [PATCH] improve dropdown presentation ui --- static/portal-table.css | 2 +- svelte/src/datum-editor.svelte | 82 +++++++++++++++++++++++++------ svelte/src/field-details.svelte | 4 +- svelte/src/presentation.svelte.ts | 2 + 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/static/portal-table.css b/static/portal-table.css index 8297699..e551a4f 100644 --- a/static/portal-table.css +++ b/static/portal-table.css @@ -343,7 +343,7 @@ flex: 1; grid-template: 'null-control value-control' max-content - 'helpers helpers' auto / max-content auto; + 'helpers helpers' 1fr / max-content 1fr; &:has(:focus) { border-left-color: #07f; diff --git a/svelte/src/datum-editor.svelte b/svelte/src/datum-editor.svelte index 72d39f6..a1c0037 100644 --- a/svelte/src/datum-editor.svelte +++ b/svelte/src/datum-editor.svelte @@ -11,7 +11,10 @@ example within the `` or ``. editor_state_from_datum, type EditorState, } from "./editor-state.svelte"; - import { type Presentation } from "./presentation.svelte"; + import { + type DropdownOption, + type Presentation, + } from "./presentation.svelte"; const BLUR_DEBOUNCE_MS = 100; @@ -67,6 +70,8 @@ example within the `` or ``. } }); + let filtered_dropdown_options: DropdownOption[] = $state([]); + export function focus() { if ( current_presentation.t === "Dropdown" || @@ -86,12 +91,16 @@ example within the `` or ``. if (blur_timeout !== undefined) { clearTimeout(blur_timeout); } - blur_timeout = setTimeout(() => on_blur?.(ev), BLUR_DEBOUNCE_MS); + blur_timeout = setTimeout(() => { + on_blur?.(ev); + filtered_dropdown_options = []; + }, BLUR_DEBOUNCE_MS); } function handle_focus(ev: FocusEvent) { if (blur_timeout === undefined) { on_focus?.(ev); + update_dropdown_filter(); } else { clearTimeout(blur_timeout); } @@ -106,6 +115,29 @@ example within the `` or ``. on_change?.(value); } + function handle_textinput_input(input_value: string) { + if (!editor_state) { + console.warn("text input oninput() preconditions not met"); + return; + } + editor_state.text_value = input_value; + if (input_value !== "") { + editor_state.is_null = false; + } + handle_input(); + } + + function update_dropdown_filter() { + if (current_presentation.t === "Dropdown") { + filtered_dropdown_options = current_presentation.c.options.filter( + (option) => + option.value + .toLocaleLowerCase("en") + .includes(editor_state?.text_value.toLocaleLowerCase("en") ?? ""), + ); + } + } + function handle_keydown(ev: KeyboardEvent) { if (ev.key === "Enter") { on_restore_focus?.(); @@ -113,6 +145,33 @@ example within the `` or ``. // Cancel edit before blurring, or else the table will try to commit it. on_cancel_edit?.(); on_restore_focus?.(); + } else if ( + current_presentation.t === "Dropdown" && + filtered_dropdown_options.length > 0 + ) { + const current_option_idx = filtered_dropdown_options.findIndex( + (option) => + option.value.toLocaleLowerCase("en") === + (editor_state?.text_value.toLocaleLowerCase("en") ?? ""), + ); + if (ev.key === "ArrowUp" || (ev.key === "Tab" && ev.shiftKey)) { + // Prevent shifting focus with tab key. User can still use enter or esc. + ev.preventDefault(); + handle_textinput_input( + filtered_dropdown_options[Math.max(current_option_idx - 1, 0)].value, + ); + } else if (ev.key === "ArrowDown" || (ev.key === "Tab" && !ev.shiftKey)) { + // Prevent shifting focus with tab key. User can still use enter or esc. + ev.preventDefault(); + handle_textinput_input( + filtered_dropdown_options[ + Math.min( + current_option_idx + 1, + filtered_dropdown_options.length - 1, + ) + ].value, + ); + } } } @@ -176,15 +235,8 @@ example within the `` or ``. bind:this={text_input_element} value={editor_state.text_value} oninput={({ currentTarget }) => { - if (!editor_state) { - console.warn("text input oninput() preconditions not met"); - return; - } - editor_state.text_value = currentTarget.value; - if (currentTarget.value !== "") { - editor_state.is_null = false; - } - handle_input(); + handle_textinput_input(currentTarget.value); + update_dropdown_filter(); }} class="datum-editor__text-input" type="text" @@ -223,11 +275,10 @@ example within the `` or ``. /> {/if} -
+
{#if current_presentation.t === "Dropdown"} - - {#each current_presentation.c.options as dropdown_option} + {#each filtered_dropdown_options as dropdown_option}
  • ` or ``. editor_state.is_null = false; text_input_element.focus(); handle_input(); + text_input_element.blur(); + on_restore_focus?.(); }} + tabindex="-1" type="button" > {dropdown_option.value} diff --git a/svelte/src/field-details.svelte b/svelte/src/field-details.svelte index ad441cd..4bbcc90 100644 --- a/svelte/src/field-details.svelte +++ b/svelte/src/field-details.svelte @@ -8,6 +8,7 @@ field. This is typically rendered within a popover component, and within an HTML