Files
console/src/lib/elements/forms/inputSelectSearch.svelte
T
2023-08-31 18:44:06 +02:00

211 lines
6.5 KiB
Svelte

<script lang="ts">
import { DropList } from '$lib/components';
import { clickOnEnter } from '$lib/helpers/a11y';
import { createEventDispatcher, onMount } from 'svelte';
import { Label } from '.';
/* eslint no-undef: "off" */
type Option = $$Generic<{
value: string | boolean | number;
label: string;
data?: string[];
}>;
type OptionArray = Option[];
export let options: OptionArray;
export let id: string;
export let label: string;
export let name = 'elements';
export let optionalText: string | undefined = undefined;
export let tooltip: string | undefined = undefined;
export let showLabel = true;
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let fullWidth = false;
export let autofocus = false;
export let interactiveOutput = false;
// stretch is used inside of a flex container to give the element flex:1
export let stretch = true;
export let search = '';
// The actual selected value
export let value: Option['value'];
$: selectedOption = options.find((option) => option.value === value);
let element: HTMLInputElement;
let hasFocus = false;
let selected: number = null;
$: if (!hasFocus) {
selected = null;
}
const dispatch = createEventDispatcher();
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
function handleInput() {
hasFocus = true;
}
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();
selectOption(options[selected]);
}
break;
}
}
function selectOption(option: Option) {
value = option.value;
search = option.label;
// It's not working without this line.
if (!$$slots.output) {
element.value = search;
} else {
search = '';
}
hasFocus = false;
dispatch('select', option);
}
function clearOption() {
search = '';
value = null;
!$$slots.output && (element.value = search);
hasFocus = false;
dispatch('reset');
}
$: showClearBtn = (hasFocus && search) || value;
</script>
<li
class="u-position-relative form-item"
class:u-width-full-line={fullWidth}
class:u-stretch={stretch}>
<DropList
bind:show={hasFocus}
noStyle
noArrow
scrollable
placement="bottom-end"
position="static"
fullWidth={true}
fixed>
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
{label}
</Label>
<div class="custom-select">
<div class="input-text-wrapper" style="--amount-of-buttons:2">
{#if $$slots.output && selectedOption}
<div
role="button"
tabindex="0"
on:keyup={clickOnEnter}
on:click={() => {
if (interactiveOutput) hasFocus = !hasFocus;
}}>
<slot name="output" option={selectedOption} />
</div>
{:else}
<input
type="text"
class="input-text"
{placeholder}
{disabled}
{required}
bind:value={search}
bind:this={element}
on:focus={() => (hasFocus = true)}
on:click={() => (hasFocus = true)}
on:input={handleInput}
on:keydown={handleKeydown} />
{/if}
<div class="options-list">
{#if showClearBtn}
<button
class="options-list-button"
aria-label="clear field"
type="button"
disabled={!interactiveOutput}
on:click|preventDefault={clearOption}>
<span class="icon-x" aria-hidden="true" />
</button>
{/if}
<button
class="options-list-button"
type="button"
disabled={!interactiveOutput}
on:click={() => (hasFocus = !hasFocus)}>
<span class="icon-cheveron-down" aria-hidden="true" />
</button>
</div>
</div>
</div>
<svelte:fragment slot="list">
{#each options as option, i}
<li class="drop-list-item">
<button
class="drop-button"
class:is-selected={selected === i}
type="button"
on:click|preventDefault={() => {
selectOption(option);
}}>
<slot {option}>
<span class="text" data-private>{option.label}</span>
</slot>
</button>
</li>
{:else}
<li class="drop-list-item">
<span class="text">There are no {name} that match your search</span>
</li>
{/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>