Skip to content

Commit

Permalink
WIP MenuField / SelectField alignment.
Browse files Browse the repository at this point in the history
  • Loading branch information
willnationsdev committed Dec 11, 2023
1 parent ff1131f commit 407e680
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 73 deletions.
2 changes: 1 addition & 1 deletion packages/svelte-ux/src/lib/components/Menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions packages/svelte-ux/src/lib/components/MenuField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<Menu> | undefined = {
autoPlacement: true,
Expand Down Expand Up @@ -59,6 +58,10 @@
const dispatch = createEventDispatcher();
$: dispatch('change', { value });
function setValue(val: any): void {
value = val;
}
</script>

<Field
Expand Down Expand Up @@ -110,7 +113,7 @@
matchWidth
{...menuProps}
>
<slot {options} {selected} close={() => (open = false)} setValue={(val) => (value = val)}>
<slot {options} {selected} close={() => (open = false)} {setValue}>
<menu class="group p-1">
{#each options as option, index (option.value)}
{@const previousOption = options[index - 1]}
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte-ux/src/lib/components/QuickSearch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
43 changes: 22 additions & 21 deletions packages/svelte-ux/src/lib/components/SelectField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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 = '';
Expand Down Expand Up @@ -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<TextField>['change']) {
Expand Down
27 changes: 14 additions & 13 deletions packages/svelte-ux/src/lib/stores/formStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
enablePatches,
setAutoFreeze,
current,
type Objectish,
type Patch,
} from 'immer';
import type { Schema } from 'zod';
import { set } from 'lodash-es';
Expand All @@ -20,20 +22,20 @@ type FormStoreOptions<T> = {
schema?: Schema<T>;
};

export default function formStore<T = any>(initialState: T, options?: FormStoreOptions<T>) {
export default function formStore<T extends Objectish = any>(initialState: T, options?: FormStoreOptions<T>) {
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<T>(current(get(draftStore)));
let currentDraftValue = writable<T>(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 */
Expand Down Expand Up @@ -79,20 +81,19 @@ export default function formStore<T = any>(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,
};
Expand Down
7 changes: 7 additions & 0 deletions packages/svelte-ux/src/lib/types/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

export type MenuOption = {
label: string;
value: any;
icon?: string;
group?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,7 +161,7 @@
scrollIntoView={index === highlightIndex}
>
<div>
<div>{option.name}</div>
<div>{option.label}</div>
<div class="text-sm text-black/50">{option.value}</div>
</div>
</MenuItem>
Expand All @@ -185,7 +188,7 @@
scrollIntoView={index === highlightIndex}
icon={{ data: option.icon, style: 'color: #0000FF;' }}
>
{option.name}
{option.label}
</MenuItem>
</div>
</SelectField>
Expand All @@ -206,7 +209,7 @@
>
<div class="grid grid-cols-[1fr,auto] items-center w-full">
<div>
<div>{option.name}</div>
<div>{option.label}</div>
<div class="text-sm text-black/50">{option.value}</div>
</div>
<div on:click|stopPropagation>
Expand All @@ -218,7 +221,7 @@
/>
<Drawer {open} on:close={toggleOff} class="w-[400px]">
<div class="p-4">
Editing option: {option.name}
Editing option: {option.label}
</div>
<div
class="fixed bottom-0 w-full flex justify-center bg-gray-500/25 p-1 border-t border-gray-400"
Expand Down Expand Up @@ -267,7 +270,7 @@
</span>
</SelectField>
<Form
initial={{ name: null, value: null }}
initial={newOption()}
on:change={(e) => {
options = [e.detail, ...options];
}}
Expand All @@ -279,10 +282,10 @@
<div slot="title">Create new option</div>
<div class="px-6 py-3 w-96 grid gap-2">
<TextField
label="Name"
value={draft.name}
label="Label"
value={draft.label}
on:change={(e) => {
draft.name = e.detail.value;
draft.label = e.detail.value;
}}
autofocus
/>
Expand Down Expand Up @@ -311,11 +314,12 @@
<Toggle let:on={open} let:toggle>
<Button icon={mdiPlus} color="blue" on:click={toggle}>New item</Button>
<Form
initial={{ name: null, value: null }}
initial={newOption()}
on:change={(e) => {
options = [e.detail, ...options];
}}
let:draft
let:current
let:commit
let:revert
>
Expand All @@ -329,10 +333,10 @@
<div slot="title">Create new option</div>
<div class="px-6 py-3 w-96 grid gap-2">
<TextField
label="Name"
value={draft.name}
label="Label"
value={current.label}
on:change={(e) => {
draft.name = e.detail.value;
draft.label = e.detail.value;
}}
autofocus
/>
Expand Down

0 comments on commit 407e680

Please sign in to comment.