feat: inputs

This commit is contained in:
Torsten Dittmann
2024-09-02 19:37:09 +02:00
parent 5fe0f0e37d
commit 0eac7d5023
6 changed files with 630 additions and 847 deletions
+11 -11
View File
@@ -23,14 +23,14 @@
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "1.0.0-next.3",
"@appwrite.io/pink-svelte": "1.0.0-next.20",
"@appwrite.io/pink-svelte": "1.0.0-next.22",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.26.0",
"@sentry/sveltekit": "^8.27.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.14",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.12",
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"echarts": "^5.5.1",
"envfile": "^7.1.0",
@@ -44,12 +44,12 @@
"devDependencies": {
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.46.0",
"@playwright/test": "^1.46.1",
"@sveltejs/adapter-static": "^3.0.4",
"@sveltejs/kit": "^2.5.22",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@sveltejs/kit": "^2.5.25",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/svelte": "^5.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
@@ -65,14 +65,14 @@
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.77.8",
"svelte": "^4.2.18",
"svelte-check": "^3.8.5",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.3",
"tslib": "^2.7.0",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vite": "^5.4.2",
"vitest": "^1.6.0"
},
"type": "module",
+553 -539
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -3,10 +3,10 @@
import { getContext, hasContext } from 'svelte';
import { readable } from 'svelte/store';
import type { FormContext } from './form.svelte';
import { Button, Anchor } from '@appwrite.io/pink-svelte';
import { Button } from '@appwrite.io/pink-svelte';
import type { ComponentProps } from 'svelte';
type Props = ComponentProps<Button>;
type Props = ComponentProps<Button.Button>;
export let submit = false;
export let secondary = false;
@@ -53,7 +53,7 @@
</script>
{#if href}
<Anchor
<Button.Anchor
on:click
on:click={track}
{href}
@@ -67,9 +67,9 @@
aria-label={ariaLabel}
--button-width={fullWidth ? '100%' : 'fit-content'}>
<slot />
</Anchor>
</Button.Anchor>
{:else}
<Button
<Button.Button
on:click
on:click={track}
{size}
@@ -86,5 +86,5 @@
aria-hidden="true" />
{/if}
<slot isSubmitting={$isSubmitting} />
</Button>
</Button.Button>
{/if}
+21 -82
View File
@@ -3,6 +3,7 @@
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import { Drop } from '$lib/components';
import { Input } from '@appwrite.io/pink-svelte';
export let label: string;
export let optionalText: string | undefined = undefined;
@@ -10,6 +11,7 @@
export let id: string;
export let value = '';
export let placeholder = '';
export let name: string | undefined = undefined;
export let required = false;
export let nullable = false;
export let disabled = false;
@@ -21,101 +23,38 @@
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
if (element.validity.typeMismatch) {
if (event.currentTarget.validity.typeMismatch) {
error = 'Emails should be formatted as: name@example.com';
return;
}
if (element.validity.valueMissing) {
if (event.currentTarget.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
error = event.currentTarget.validationMessage;
};
$: if (value) {
error = null;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
</script>
<FormItem {fullWidth}>
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div
class:input-text-wrapper={!$$slots.default}
class:u-flex={$$slots.default}
class:u-gap-16={$$slots.default}
class:u-cross-center={$$slots.default}>
<input
{id}
{placeholder}
{disabled}
{required}
{readonly}
type="email"
class="input-text"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{#if nullable && !required}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
</ul>
{/if}
<slot />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
<Input.Text
{id}
{name}
{placeholder}
{disabled}
{required}
{label}
{nullable}
{autofocus}
on:input
on:invalid={handleInvalid}
type="email"
helper={error}
autocomplete={autocomplete ? 'on' : 'off'}
bind:value />
+19 -104
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { Drop } from '$lib/components';
import { SvelteComponent } from 'svelte';
import { Input } from '@appwrite.io/pink-svelte';
export let id: string;
export let label: string;
@@ -20,121 +19,37 @@
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
let showInPlainText = false;
let showPopover = false;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
if (element.validity.valueMissing) {
if (event.currentTarget.validity.valueMissing) {
error = 'This field is required';
return;
}
if (element.validity.tooShort) {
if (event.currentTarget.validity.tooShort) {
error = 'Password should contain at least 8 characters';
return;
}
error = element.validationMessage;
error = event.currentTarget.validationMessage;
};
$: strength = value ? value.length * 10 : 0;
$: if (value) {
error = null;
}
</script>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}{#if popover}
<Drop isPopover bind:show={showPopover} display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (showPopover = !showPopover)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
{#if showInPlainText}
<input
{id}
{placeholder}
{disabled}
{minlength}
{maxlength}
name={id}
type="text"
class="input-text"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{:else}
<input
{id}
{placeholder}
{disabled}
{required}
{minlength}
{maxlength}
type="password"
class="input-text"
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{/if}
{#if meter}
<meter
value={strength > 100 ? 100 : strength}
min="0"
max="100"
class="password-meter"
class:is-weak={strength !== 0 && strength <= 33}
class:is-medium={strength > 33 && strength <= 66}
class:is-strong={strength > 66 && strength <= 100}
aria-label="Password strength weak" />
{/if}
{#if showPasswordButton}
<button
type="button"
on:click={() => (showInPlainText = !showInPlainText)}
class="show-password-button"
aria-label="show password">
<span
class:icon-eye={!showInPlainText}
class:icon-eye-off={showInPlainText}
aria-hidden="true" />
</button>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
<Input.Password
{id}
{label}
{required}
{disabled}
{autofocus}
{minlength}
{maxlength}
autocomplete={autocomplete ? 'on' : 'off'}
{placeholder}
helper={error}
on:invalid={handleInvalid}
bind:value />
+20 -105
View File
@@ -1,9 +1,6 @@
<script lang="ts">
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, FormItemPart, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
import { Drop } from '$lib/components';
import { Input } from '@appwrite.io/pink-svelte';
export let label: string = undefined;
export let optionalText: string | undefined = undefined;
@@ -26,124 +23,42 @@
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
event.preventDefault();
if (element.validity.valueMissing) {
if (event.currentTarget.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
error = event.currentTarget.validationMessage;
};
$: {
value;
if (element?.validity?.valid) {
if (value) {
error = null;
}
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
}
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
type $$Events = {
input: Event & { target: HTMLInputElement };
};
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style:font-size="var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--p-card-padding=".75rem"
style:--card-border-radius="var(--border-radius-small)"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
<div
class:input-text-wrapper={!$$slots.default}
class:u-flex={$$slots.default}
class:u-gap-8={$$slots.default}
class:u-cross-center={$$slots.default}>
<input
{id}
{name}
{placeholder}
{disabled}
{readonly}
{required}
{maxlength}
autocomplete={autocomplete ? 'on' : 'off'}
type="text"
class="input-text"
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid}
on:input />
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
{#if showTextCounter}
<li class="buttons-list-item">
<TextCounter max={maxlength} count={value?.length ?? 0} />
</li>
{/if}
{#if showNullCheckbox}
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
{/if}
</ul>
{/if}
<slot />
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</svelte:component>
<Input.Text
{id}
{name}
{placeholder}
{disabled}
{required}
{maxlength}
{label}
{nullable}
{autofocus}
helper={error}
on:invalid={handleInvalid}
on:input
autocomplete={autocomplete ? 'on' : 'off'}
bind:value />