feat: proper select search positioning & keyboard controls

This commit is contained in:
tglide
2023-03-10 19:49:31 +00:00
parent d9334116aa
commit dfd40e2bde
6 changed files with 140 additions and 56 deletions
+16
View File
@@ -12,6 +12,7 @@
export let childStart = false;
export let noStyle = false;
export let fullWidth = false;
export let fixed = false;
let element: HTMLDivElement;
let tooltip: HTMLDivElement;
@@ -21,6 +22,7 @@
onMount(() => {
instance = createPopper(element, tooltip, {
placement,
strategy: fixed ? 'fixed' : 'absolute',
modifiers: [
{
name: 'arrow',
@@ -39,6 +41,20 @@
options: {
fallbackPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end']
}
},
{
name: 'sameWidth',
enabled: fixed,
phase: 'beforeWrite',
requires: ['computeStyles'],
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
effect: ({ state }) => {
state.elements.popper.style.width = `${
(state.elements.reference as HTMLElement)?.offsetWidth
}px`;
}
}
]
});
+2 -1
View File
@@ -4,6 +4,7 @@
export let show = false;
export let placement: Placement = 'bottom-start';
export let fixed = false;
export let scrollable = false;
export let childStart = false;
export let noArrow = false;
@@ -13,7 +14,7 @@
export let position: 'relative' | 'static' = 'relative';
</script>
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth}>
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed}>
<slot />
<svelte:fragment slot="list">
<div
+2 -1
View File
@@ -77,7 +77,8 @@
class="modal"
class:is-small={size === 'small'}
class:is-big={size === 'big'}
bind:this={dialog}>
bind:this={dialog}
on:cancel|preventDefault>
{#if show}
<!-- svelte-ignore a11y-no-redundant-roles -->
<form class="modal-form" role="form" on:submit|preventDefault>
+77 -26
View File
@@ -7,8 +7,6 @@
export let label: string;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean;
export let search: string = null;
export let placeholder = '';
export let required = false;
export let disabled = false;
@@ -18,10 +16,19 @@
value: string | boolean | number;
label: string;
}[];
// Input value
export let search: string = null;
// The actual selected value
export let value: string | number | boolean;
let element: HTMLInputElement;
let timer: ReturnType<typeof setTimeout>;
let hasFocus = false;
let selected: number = null;
$: if (!hasFocus) {
selected = null;
}
onMount(() => {
if (element && autofocus) {
@@ -29,17 +36,46 @@
}
});
const valueChange = (event: Event) => {
hasFocus = false;
function handleInput(event: Event) {
hasFocus = true;
clearTimeout(timer);
timer = setTimeout(() => {
const target = event.target as HTMLInputElement;
search = target.value;
}, debounce);
};
}
$: console.log(options);
function handleKeydown(event: KeyboardEvent) {
event.stopImmediatePropagation();
switch (event.key) {
case 'Escape':
hasFocus = false;
break;
case 'ArrowDown':
if (selected === null) {
selected = 0;
} else {
selected = (selected + 1) % options.length;
}
break;
case 'ArrowUp':
if (selected === null) {
selected = options.length - 1;
} else {
selected = (selected - 1 + options.length) % options.length;
}
break;
case 'Enter':
if (selected !== null) {
event.preventDefault();
value = options[selected].value;
search = options[selected].label;
hasFocus = false;
}
break;
}
}
</script>
<li class="u-position-relative form-item">
@@ -50,7 +86,8 @@
scrollable
placement="bottom-end"
position="static"
fullWidth={true}>
fullWidth={true}
fixed>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
@@ -66,7 +103,9 @@
bind:value={search}
bind:this={element}
on:focus={() => (hasFocus = true)}
on:input={valueChange} />
on:click={() => (hasFocus = true)}
on:input={handleInput}
on:keydown={handleKeydown} />
<div class="options-list">
<button
@@ -74,7 +113,8 @@
aria-label="clear field"
type="button"
on:click|preventDefault={() => {
element.value = null;
search = '';
value = null;
}}>
<span class="icon-x" aria-hidden="true" />
</button>
@@ -85,26 +125,37 @@
</div>
</div>
<svelte:fragment slot="list">
{#if options?.length}
{#each options as option}
<li class="drop-list-item">
<button
class="drop-button"
type="button"
on:click|preventDefault={() => {
value = option.value;
search = option.label;
hasFocus = false;
}}>
<span class="text">{option.label}</span>
</button>
</li>
{/each}
{#each options as option, i}
<li class="drop-list-item">
<button
class="drop-button"
class:is-selected={selected === i}
type="button"
on:click|preventDefault={() => {
value = option.value;
search = option.label;
hasFocus = false;
}}>
<span class="text">{option.label}</span>
</button>
</li>
{:else}
<li class="drop-list-item">
<span class="text">There are no documents that match your search</span>
</li>
{/if}
{/each}
</svelte:fragment>
</DropList>
</li>
<style>
.form-item :global(.drop) {
translate: 0 4px;
}
.form-item :global(.drop-section) {
width: 100%;
margin-inline: initial;
max-inline-size: initial;
}
</style>
@@ -1,23 +1,23 @@
<script lang="ts">
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
import { Pill } from '$lib/elements';
import { Button } from '$lib/elements/forms';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableCell,
TableCellHead,
TableCellText,
TableCell
TableHeader,
TableRow
} from '$lib/elements/table';
import { Button } from '$lib/elements/forms';
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
import { attributes, type Attributes } from '../store';
import { Container } from '$lib/layout';
import { Pill } from '$lib/elements';
import Create from '../createAttribute.svelte';
import CreateIndex from '../indexes/createIndex.svelte';
import { attributes, type Attributes } from '../store';
import Delete from './deleteAttribute.svelte';
import { options } from './store';
import Edit from './edit.svelte';
import { options } from './store';
let showCreateDropdown = false;
let showDropdown = [];
@@ -45,14 +45,25 @@
import arrowOne from './arrow-one.svg';
import arrowTwo from './arrow-two.svg';
// Props
export let selectedAttribute: Models.AttributeString;
export let data: Partial<Models.AttributeString> = {
type Data = Partial<Models.AttributeString> & {
relation: 'one' | 'many';
related?: string | number | boolean;
keyRelated?: string;
del?: string | number | boolean;
};
export let data: Data = {
required: false,
size: 0,
default: null,
array: false
array: false,
relation: 'one'
};
// Constants
const databaseId = $page.params.database;
const oneWay = [
{ value: 'one', label: 'One to one' },
@@ -70,16 +81,13 @@
{ value: 'null', label: 'Null' },
{ value: 'restrict', label: 'Restrict' }
];
// Variables
let search: string = null;
let collectionList: Models.CollectionList;
let way = 'one';
onMount(async () => {
collectionList = await getCollections();
data.relation = 'one';
});
// Lifecycle hooks
async function getCollections(search: string = null) {
if (search) {
const collections = await sdkForProject.databases.listCollections(
@@ -87,24 +95,31 @@
[Query.orderDesc('$createdAt')],
search
);
collectionList = collections;
return collections;
} else {
const collections = await sdkForProject.databases.listCollections(databaseId);
collectionList = collections;
return collections;
}
}
$: getCollections(search);
onMount(async () => {
collectionList = await getCollections();
});
// Reactive statements
$: getCollections(search).then((res) => (collectionList = res));
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
$: if (selectedAttribute) {
({
required: data.required,
array: data.array,
size: data.size,
default: data.default
} = selectedAttribute);
data = {
...data,
required: selectedAttribute.required,
array: selectedAttribute.array,
size: selectedAttribute.size,
default: selectedAttribute.default
};
}
$: if (data.required || data.array) {
data.default = null;
}
@@ -137,7 +152,7 @@
options={collectionList?.collections?.map((n) => ({ value: n.$id, label: n.name })) ?? []} />
{#if data?.related}
{@const selectedCol = collections.find((n) => n.$id === data.related)}
{@const selectedCol = collections?.find((n) => n.$id === data.related)}
<div>
<InputText
id="key"
@@ -193,12 +208,12 @@
{:else}
<img src={arrowTwo} alt={'Two way relationship'} />
{/if}
<span>{selectedCol.name}</span>
<span>{selectedCol?.name}</span>
</div>
</div>
<p class="u-text-center ">
<b> {$collection.name}</b> has {data.relation === 'one' ? 'one' : 'many'}
<b>{selectedCol.name}</b>
<b>{selectedCol?.name}</b>
</p>
<InputSelect