wandering-eye/src/lib/SearchInput.svelte
2023-12-21 04:31:16 -06:00

173 lines
3.5 KiB
Svelte

<script lang="ts">
import { fly } from 'svelte/transition'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let onSubmit: ((_: string) => Promise<void>) | null = null
export let pattern: string | null = null
export let placeholder = ''
export let invalidMessage = 'Input is invalid'
export let value: string | null = null
let inputValue = value
let iconName = 'search'
let disabled = true
let invalid = false
$: {
disabled = inputValue == null || inputValue === ''
}
function onInput(e: Event) {
if (pattern) invalid = !new RegExp(pattern).test(inputValue ?? '')
dispatch('input', e)
}
async function onSubmitWrapper(e: SubmitEvent) {
e.preventDefault()
iconName = 'hourglass_empty'
dispatch('submit', { value: inputValue, ...e })
value = inputValue
if (onSubmit) {
await onSubmit?.(value ?? '')
}
iconName = 'search'
}
</script>
<div class="search-input-container">
<form class="search-input" class:invalid on:submit={onSubmitWrapper}>
<div class="input-wrapper">
<input
{placeholder}
{pattern}
type="text"
bind:value={inputValue}
on:input={onInput}
/>
</div>
<button type="submit" disabled={disabled || invalid}>
<span class="material-icons">
{iconName}
</span>
</button>
</form>
{#if invalid && !disabled}
<div class="warning-message" transition:fly={{ y: -10 }}>
{invalidMessage}
</div>
{/if}
</div>
<style lang="postcss">
.search-input-container {
position: relative;
}
.search-input {
position: relative;
--background: var(--grey-400);
background: var(--background);
display: grid;
grid-template-columns: 1fr 2rem;
border-radius: 2rem;
overflow: hidden;
}
input,
button {
padding: 0.25rem 0.5rem;
}
.input-wrapper {
min-width: 0;
position: relative;
}
.input-wrapper::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1rem;
background-image: linear-gradient(to left, var(--background), transparent);
}
.search-input input {
padding-right: 0;
min-width: 0;
width: 100%;
transition: border-bottom-width 100ms;
}
.search-input input:focus {
outline: none;
}
.search-input.invalid {
--background: var(--red-100);
}
.search-input::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: currentColor;
opacity: 0;
transform: scaleX(0);
transition: transform 200ms;
transition-timing-function: var(--ease-in);
}
.search-input:focus-within::after {
transform: scaleX(1);
opacity: 1;
}
.search-input button {
display: grid;
place-items: center;
cursor: pointer;
transition: background-color 200ms;
transition-timing-function: var(--ease-in);
user-select: none;
border-radius: 2rem;
border: 1px solid transparent;
}
.search-input button:focus-visible {
outline: none;
background-color: var(--lighten-40);
}
.search-input button[disabled] {
cursor: not-allowed;
}
.search-input button .material-icons {
text-decoration: none;
transition: transform 200ms;
transition-timing-function: var(--ease-in);
}
.search-input button:not([disabled]):hover {
background-color: var(--lighten-20);
}
.search-input button:not([disabled]):hover .material-icons {
transform: scale(1.2) rotate(5deg);
}
.warning-message {
position: absolute;
left: 0;
right: 0;
padding: 0.25rem 0.5rem;
background-color: var(--red-400);
border-radius: 5rem;
margin: 0.4rem 0;
}
</style>