forked from 2sys/phonograph
improve dropdown presentation ui
This commit is contained in:
parent
849f981243
commit
81f9396490
4 changed files with 74 additions and 16 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue