1
0
Fork 0
forked from 2sys/phonograph

improve dropdown presentation ui

This commit is contained in:
Brent Schroeter 2026-01-20 03:31:55 +00:00
parent 849f981243
commit 81f9396490
4 changed files with 74 additions and 16 deletions

View file

@ -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;

View file

@ -11,7 +11,10 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
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 `<TableViewer />` or `<ExpressionEditor />`.
}
});
let filtered_dropdown_options: DropdownOption[] = $state([]);
export function focus() {
if (
current_presentation.t === "Dropdown" ||
@ -86,12 +91,16 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
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 `<TableViewer />` or `<ExpressionEditor />`.
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 `<TableViewer />` or `<ExpressionEditor />`.
// 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 `<TableViewer />` or `<ExpressionEditor />`.
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 `<TableViewer />` or `<ExpressionEditor />`.
/>
</div>
{/if}
<div class="datum-editor__helpers" tabindex="-1">
<div class="datum-editor__helpers">
{#if current_presentation.t === "Dropdown"}
<!-- TODO: This is an awkward way to implement a keyboard-navigable listbox. -->
<menu class="datum-editor__dropdown-options">
{#each current_presentation.c.options as dropdown_option}
{#each filtered_dropdown_options as dropdown_option}
<!-- FIXME: validate or escape dropdown_option.color -->
<li
class={[
@ -251,7 +302,10 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
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}

View file

@ -8,6 +8,7 @@ field. This is typically rendered within a popover component, and within an HTML
<script lang="ts">
import {
type Presentation,
RFC_3339_S,
all_presentation_tags,
all_text_input_modes,
} from "./presentation.svelte";
@ -66,7 +67,7 @@ field. This is typically rendered within a popover component, and within an HTML
return { t: "Text", c: { input_mode: { t: "SingleLine", c: {} } } };
}
if (tag === "Timestamp") {
return { t: "Timestamp", c: {} };
return { t: "Timestamp", c: { format: RFC_3339_S } };
}
if (tag === "Uuid") {
return { t: "Uuid", c: {} };
@ -144,6 +145,7 @@ field. This is typically rendered within a popover component, and within an HTML
<select
class="field-details__dropdown-option-color"
name="dropdown_option_colors"
value={option.color}
>
{#each COLORS as color}
<option value={color}>{color}</option>

View file

@ -62,6 +62,8 @@ const dropdown_option_schema = z.object({
value: z.string(),
});
export type DropdownOption = z.infer<typeof dropdown_option_schema>;
const presentation_dropdown_schema = z.object({
t: z.literal("Dropdown"),
c: z.object({