From 407e6805caa05c02e8d4dff9f2b4f42fdf9e5fac Mon Sep 17 00:00:00 2001 From: willnationsdev Date: Mon, 11 Dec 2023 13:01:04 -0600 Subject: [PATCH] WIP MenuField / SelectField alignment. --- .../svelte-ux/src/lib/components/Menu.svelte | 2 +- .../src/lib/components/MenuField.svelte | 11 +-- .../src/lib/components/QuickSearch.svelte | 3 +- .../src/lib/components/SelectField.svelte | 43 ++++++------ .../svelte-ux/src/lib/stores/formStore.ts | 27 +++---- packages/svelte-ux/src/lib/types/options.ts | 7 ++ .../docs/components/SelectField/+page.svelte | 70 ++++++++++--------- 7 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 packages/svelte-ux/src/lib/types/options.ts diff --git a/packages/svelte-ux/src/lib/components/Menu.svelte b/packages/svelte-ux/src/lib/components/Menu.svelte index 949ed7b35..ddb9cd017 100644 --- a/packages/svelte-ux/src/lib/components/Menu.svelte +++ b/packages/svelte-ux/src/lib/components/Menu.svelte @@ -36,7 +36,7 @@ export let menuItemsEl: HTMLMenuElement | undefined = undefined; - function onClick(e) { + function onClick(e: MouseEvent) { try { if (e.target === menuItemsEl) { // Clicked within menu but outside of any items diff --git a/packages/svelte-ux/src/lib/components/MenuField.svelte b/packages/svelte-ux/src/lib/components/MenuField.svelte index 0ebd5aa52..e028a8e66 100644 --- a/packages/svelte-ux/src/lib/components/MenuField.svelte +++ b/packages/svelte-ux/src/lib/components/MenuField.svelte @@ -11,10 +11,9 @@ import MenuItem from './MenuItem.svelte'; import Button from './Button.svelte'; import { getComponentTheme } from './theme'; + import type { MenuOption } from '$lib/types/options'; - type Options = Array<{ label: string; value: any; icon?: string; group?: string }>; - - export let options: Options; + export let options: MenuOption[] = []; export let value: any = null; export let menuProps: ComponentProps | undefined = { autoPlacement: true, @@ -59,6 +58,10 @@ const dispatch = createEventDispatcher(); $: dispatch('change', { value }); + + function setValue(val: any): void { + value = val; + } - (open = false)} setValue={(val) => (value = val)}> + (open = false)} {setValue}> {#each options as option, index (option.value)} {@const previousOption = options[index - 1]} diff --git a/packages/svelte-ux/src/lib/components/QuickSearch.svelte b/packages/svelte-ux/src/lib/components/QuickSearch.svelte index f6f3a39b5..05c6624c6 100644 --- a/packages/svelte-ux/src/lib/components/QuickSearch.svelte +++ b/packages/svelte-ux/src/lib/components/QuickSearch.svelte @@ -8,8 +8,9 @@ import { cls } from '$lib/utils/styles'; import { smScreen } from '$lib/stores'; import { autoFocus, selectOnFocus } from '$lib/actions'; + import type { MenuOption } from '$lib/types/options'; - export let options: { name: string; value: string; group?: string }[] = []; + export let options: MenuOption[] = []; export let classes: { root?: string; diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index 7801ce5ef..dc6946645 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -16,6 +16,7 @@ import TextField from './TextField.svelte'; import { getComponentTheme } from './theme'; import type { IconInput } from '$lib/utils/icons'; + import type { MenuOption } from '$lib/types/options'; const dispatch = createEventDispatcher<{ change: { value: any; option: any }; @@ -24,8 +25,8 @@ const logger = new Logger('SelectField'); - export let options: any[] = []; - export let optionText = (option: any) => (option?.name as string) ?? ''; + export let options: MenuOption[] = []; + export let optionText = (option: any) => (option?.label as string) ?? ''; export let optionValue = (option: any) => option?.value ?? null; export let label = ''; @@ -166,27 +167,27 @@ const prevHighlightedOption = filteredOptions[highlightIndex]; // Do not search if menu is not open / closing on selection - search(searchText); - - // TODO: Find a way for scrollIntoView to still highlight after the menu height transition finishes - const selectedIndex = filteredOptions.findIndex((o) => optionValue(o) === value); - if (highlightIndex === -1) { - // Highlight selected if none currently - highlightIndex = selectedIndex === -1 ? 0 : selectedIndex; - } else { - // Attempt to re-highlight previously highlighted item after search - const prevHighlightedOptionIndex = filteredOptions.findIndex( - (o) => o === prevHighlightedOption - ); - - if (prevHighlightedOptionIndex !== -1) { - // Maintain previously highlight index after filter update (option still available) - highlightIndex = prevHighlightedOptionIndex; + search(searchText).then(() => { + // TODO: Find a way for scrollIntoView to still highlight after the menu height transition finishes + const selectedIndex = filteredOptions.findIndex((o) => optionValue(o) === value); + if (highlightIndex === -1) { + // Highlight selected if none currently + highlightIndex = selectedIndex === -1 ? 0 : selectedIndex; } else { - // Highlight first item - highlightIndex = 0; + // Attempt to re-highlight previously highlighted item after search + const prevHighlightedOptionIndex = filteredOptions.findIndex( + (o) => o === prevHighlightedOption + ); + + if (prevHighlightedOptionIndex !== -1) { + // Maintain previously highlight index after filter update (option still available) + highlightIndex = prevHighlightedOptionIndex; + } else { + // Highlight first item + highlightIndex = 0; + } } - } + }); } function onChange(e: ComponentEvents['change']) { diff --git a/packages/svelte-ux/src/lib/stores/formStore.ts b/packages/svelte-ux/src/lib/stores/formStore.ts index b144ffa3b..34a729b92 100644 --- a/packages/svelte-ux/src/lib/stores/formStore.ts +++ b/packages/svelte-ux/src/lib/stores/formStore.ts @@ -6,6 +6,8 @@ import { enablePatches, setAutoFreeze, current, + type Objectish, + type Patch, } from 'immer'; import type { Schema } from 'zod'; import { set } from 'lodash-es'; @@ -20,20 +22,20 @@ type FormStoreOptions = { schema?: Schema; }; -export default function formStore(initialState: T, options?: FormStoreOptions) { +export default function formStore(initialState: T, options?: FormStoreOptions) { const stateStore = writable(initialState); const draftStore = writable(createDraft(initialState)); const errorsStore = writable({} as { [key: string]: string }); // TODO: Improve type (`{ [key in keyof T]: string }`?) - const undoList = []; + const undoList: Patch[][] = []; const storeApi = { subscribe: stateStore.subscribe }; - let currentDraftValue = writable(current(get(draftStore))); + let currentDraftValue = writable(current(get(draftStore)) as T); const draftApi = { ...draftStore, - set(newState) { + set(newState: T) { draftStore.set(createDraft(newState)); }, /** Apply draft to state after verifying with schema (if available). Append change to undo stack */ @@ -79,20 +81,19 @@ export default function formStore(initialState: T, options?: FormStoreO }, /** Undo last committed change */ undo() { - if (undoList.length) { - const undo = undoList.pop(); + const undo = undoList.pop(); + if (undo == null) return; - const currentState = get(stateStore); - const newState = applyPatches(currentState, undo); + const currentState = get(stateStore); + const newState = applyPatches(currentState, undo); - stateStore.set(newState); - draftStore.set(createDraft(newState)); - currentDraftValue.set(newState); - } + stateStore.set(newState); + draftStore.set(createDraft(newState)); + currentDraftValue.set(newState); }, /** Refresh `current` draft value (un-proxied) */ refresh() { - currentDraftValue.set(current(get(draftStore))); + currentDraftValue.set(current(get(draftStore)) as T); }, current: currentDraftValue, }; diff --git a/packages/svelte-ux/src/lib/types/options.ts b/packages/svelte-ux/src/lib/types/options.ts new file mode 100644 index 000000000..f70cce646 --- /dev/null +++ b/packages/svelte-ux/src/lib/types/options.ts @@ -0,0 +1,7 @@ + +export type MenuOption = { + label: string; + value: any; + icon?: string; + group?: string +} diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index 650926087..192845cb1 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -14,37 +14,40 @@ import { delay } from '$lib/utils/promise'; import { cls } from '$lib/utils/styles'; import Icon from '$lib/components/Icon.svelte'; + import type { MenuOption } from '$lib/types/options'; - let options = [ - { name: 'One', value: 1, icon: mdiMagnify }, - { name: 'Two', value: 2, icon: mdiPlus }, - { name: 'Three', value: 3, icon: mdiPencil }, - { name: 'Four', value: 4, icon: mdiAccount }, + let options: MenuOption[] = [ + { label: 'One', value: 1, icon: mdiMagnify }, + { label: 'Two', value: 2, icon: mdiPlus }, + { label: 'Three', value: 3, icon: mdiPencil }, + { label: 'Four', value: 4, icon: mdiAccount }, ]; - const optionsWithGroup = [ - { name: 'One', value: 1, group: 'First' }, - { name: 'Two', value: 2, group: 'First' }, - { name: 'Three', value: 3, group: 'Second' }, - { name: 'Four', value: 4, group: 'Second' }, - { name: 'Five', value: 5, group: 'Second' }, - { name: 'Six', value: 6, group: 'Third' }, - { name: 'Seven', value: 7, group: 'Third' }, + const optionsWithGroup: MenuOption[] = [ + { label: 'One', value: 1, group: 'First' }, + { label: 'Two', value: 2, group: 'First' }, + { label: 'Three', value: 3, group: 'Second' }, + { label: 'Four', value: 4, group: 'Second' }, + { label: 'Five', value: 5, group: 'Second' }, + { label: 'Six', value: 6, group: 'Third' }, + { label: 'Seven', value: 7, group: 'Third' }, ]; - const manyOptions = Array.from({ length: 100 }).map((_, i) => ({ - name: `${i + 1}`, + const manyOptions: MenuOption[] = Array.from({ length: 100 }).map((_, i) => ({ + label: `${i + 1}`, value: i + 1, })); - const newOptions = [ - { name: 'Empty', value: null }, - { name: 'Foo', value: 1 }, - { name: 'Bar', value: 2 }, - { name: 'Baz', value: 3 }, + const newOptions: MenuOption[] = [ + { label: 'Empty', value: null }, + { label: 'Foo', value: 1 }, + { label: 'Bar', value: 2 }, + { label: 'Baz', value: 3 }, ]; - let optionsAsync: { name: string; value: number }[] = []; + const newOption: () => MenuOption = () => { return { label: "", value: null }} + + let optionsAsync: MenuOption[] = []; let loading = false; let value = 3; @@ -158,7 +161,7 @@ scrollIntoView={index === highlightIndex} >
-
{option.name}
+
{option.label}
{option.value}
@@ -185,7 +188,7 @@ scrollIntoView={index === highlightIndex} icon={{ data: option.icon, style: 'color: #0000FF;' }} > - {option.name} + {option.label} @@ -206,7 +209,7 @@ >
-
{option.name}
+
{option.label}
{option.value}
@@ -218,7 +221,7 @@ />
- Editing option: {option.name} + Editing option: {option.label}
{ options = [e.detail, ...options]; }} @@ -279,10 +282,10 @@
Create new option
{ - draft.name = e.detail.value; + draft.label = e.detail.value; }} autofocus /> @@ -311,11 +314,12 @@ { options = [e.detail, ...options]; }} let:draft + let:current let:commit let:revert > @@ -329,10 +333,10 @@
Create new option
{ - draft.name = e.detail.value; + draft.label = e.detail.value; }} autofocus />