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;
|
flex: 1;
|
||||||
grid-template:
|
grid-template:
|
||||||
'null-control value-control' max-content
|
'null-control value-control' max-content
|
||||||
'helpers helpers' auto / max-content auto;
|
'helpers helpers' 1fr / max-content 1fr;
|
||||||
|
|
||||||
&:has(:focus) {
|
&:has(:focus) {
|
||||||
border-left-color: #07f;
|
border-left-color: #07f;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
editor_state_from_datum,
|
editor_state_from_datum,
|
||||||
type EditorState,
|
type EditorState,
|
||||||
} from "./editor-state.svelte";
|
} from "./editor-state.svelte";
|
||||||
import { type Presentation } from "./presentation.svelte";
|
import {
|
||||||
|
type DropdownOption,
|
||||||
|
type Presentation,
|
||||||
|
} from "./presentation.svelte";
|
||||||
|
|
||||||
const BLUR_DEBOUNCE_MS = 100;
|
const BLUR_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
|
|
@ -67,6 +70,8 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let filtered_dropdown_options: DropdownOption[] = $state([]);
|
||||||
|
|
||||||
export function focus() {
|
export function focus() {
|
||||||
if (
|
if (
|
||||||
current_presentation.t === "Dropdown" ||
|
current_presentation.t === "Dropdown" ||
|
||||||
|
|
@ -86,12 +91,16 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
if (blur_timeout !== undefined) {
|
if (blur_timeout !== undefined) {
|
||||||
clearTimeout(blur_timeout);
|
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) {
|
function handle_focus(ev: FocusEvent) {
|
||||||
if (blur_timeout === undefined) {
|
if (blur_timeout === undefined) {
|
||||||
on_focus?.(ev);
|
on_focus?.(ev);
|
||||||
|
update_dropdown_filter();
|
||||||
} else {
|
} else {
|
||||||
clearTimeout(blur_timeout);
|
clearTimeout(blur_timeout);
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +115,29 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
on_change?.(value);
|
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) {
|
function handle_keydown(ev: KeyboardEvent) {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
on_restore_focus?.();
|
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.
|
// Cancel edit before blurring, or else the table will try to commit it.
|
||||||
on_cancel_edit?.();
|
on_cancel_edit?.();
|
||||||
on_restore_focus?.();
|
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}
|
bind:this={text_input_element}
|
||||||
value={editor_state.text_value}
|
value={editor_state.text_value}
|
||||||
oninput={({ currentTarget }) => {
|
oninput={({ currentTarget }) => {
|
||||||
if (!editor_state) {
|
handle_textinput_input(currentTarget.value);
|
||||||
console.warn("text input oninput() preconditions not met");
|
update_dropdown_filter();
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor_state.text_value = currentTarget.value;
|
|
||||||
if (currentTarget.value !== "") {
|
|
||||||
editor_state.is_null = false;
|
|
||||||
}
|
|
||||||
handle_input();
|
|
||||||
}}
|
}}
|
||||||
class="datum-editor__text-input"
|
class="datum-editor__text-input"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -223,11 +275,10 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="datum-editor__helpers" tabindex="-1">
|
<div class="datum-editor__helpers">
|
||||||
{#if current_presentation.t === "Dropdown"}
|
{#if current_presentation.t === "Dropdown"}
|
||||||
<!-- TODO: This is an awkward way to implement a keyboard-navigable listbox. -->
|
|
||||||
<menu class="datum-editor__dropdown-options">
|
<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 -->
|
<!-- FIXME: validate or escape dropdown_option.color -->
|
||||||
<li
|
<li
|
||||||
class={[
|
class={[
|
||||||
|
|
@ -251,7 +302,10 @@ example within the `<TableViewer />` or `<ExpressionEditor />`.
|
||||||
editor_state.is_null = false;
|
editor_state.is_null = false;
|
||||||
text_input_element.focus();
|
text_input_element.focus();
|
||||||
handle_input();
|
handle_input();
|
||||||
|
text_input_element.blur();
|
||||||
|
on_restore_focus?.();
|
||||||
}}
|
}}
|
||||||
|
tabindex="-1"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{dropdown_option.value}
|
{dropdown_option.value}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ field. This is typically rendered within a popover component, and within an HTML
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
type Presentation,
|
type Presentation,
|
||||||
|
RFC_3339_S,
|
||||||
all_presentation_tags,
|
all_presentation_tags,
|
||||||
all_text_input_modes,
|
all_text_input_modes,
|
||||||
} from "./presentation.svelte";
|
} 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: {} } } };
|
return { t: "Text", c: { input_mode: { t: "SingleLine", c: {} } } };
|
||||||
}
|
}
|
||||||
if (tag === "Timestamp") {
|
if (tag === "Timestamp") {
|
||||||
return { t: "Timestamp", c: {} };
|
return { t: "Timestamp", c: { format: RFC_3339_S } };
|
||||||
}
|
}
|
||||||
if (tag === "Uuid") {
|
if (tag === "Uuid") {
|
||||||
return { t: "Uuid", c: {} };
|
return { t: "Uuid", c: {} };
|
||||||
|
|
@ -144,6 +145,7 @@ field. This is typically rendered within a popover component, and within an HTML
|
||||||
<select
|
<select
|
||||||
class="field-details__dropdown-option-color"
|
class="field-details__dropdown-option-color"
|
||||||
name="dropdown_option_colors"
|
name="dropdown_option_colors"
|
||||||
|
value={option.color}
|
||||||
>
|
>
|
||||||
{#each COLORS as color}
|
{#each COLORS as color}
|
||||||
<option value={color}>{color}</option>
|
<option value={color}>{color}</option>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ const dropdown_option_schema = z.object({
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type DropdownOption = z.infer<typeof dropdown_option_schema>;
|
||||||
|
|
||||||
const presentation_dropdown_schema = z.object({
|
const presentation_dropdown_schema = z.object({
|
||||||
t: z.literal("Dropdown"),
|
t: z.literal("Dropdown"),
|
||||||
c: z.object({
|
c: z.object({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue