@spaethtech/svelte-ui 0.21.1-dev.110.48d2aba → 0.21.1-dev.111.c0572e9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Input from "./Input.svelte";
|
|
3
3
|
import Eye from "~icons/mdi/eye";
|
|
4
4
|
import EyeOff from "~icons/mdi/eye-off";
|
|
5
|
+
import Loading from "~icons/mdi/loading";
|
|
5
6
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
6
7
|
import type { Size } from "../types/sizes.js";
|
|
7
8
|
import type { Responsive } from "../types/responsive.js";
|
|
@@ -20,6 +21,17 @@
|
|
|
20
21
|
/** Shared axes — forwarded to the underlying Input. */
|
|
21
22
|
variant?: Variant;
|
|
22
23
|
borderless?: boolean;
|
|
24
|
+
/** Whether the value is shown as plain text (eye toggled on). Bindable; defaults `false`.
|
|
25
|
+
* Drive it to open the field already-visible, or re-hide it after a save. */
|
|
26
|
+
visible?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Called when the user clicks the eye, with the state being moved TO (`!visible`).
|
|
29
|
+
* Return `false` (or a `Promise` resolving to `false`) to CANCEL the toggle; a returned
|
|
30
|
+
* `Promise` DEFERS the flip until it settles — the eye shows a busy spinner and ignores
|
|
31
|
+
* further clicks meanwhile, so an audited async reveal can populate `value` before it
|
|
32
|
+
* becomes visible (the field never shows plain text over an empty value).
|
|
33
|
+
*/
|
|
34
|
+
onVisibilityChange?: (next: boolean) => void | boolean | Promise<void | boolean>;
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
let {
|
|
@@ -27,28 +39,49 @@
|
|
|
27
39
|
valid = $bindable(true),
|
|
28
40
|
touched = $bindable(false),
|
|
29
41
|
element = $bindable(),
|
|
42
|
+
visible = $bindable(false),
|
|
43
|
+
onVisibilityChange,
|
|
30
44
|
...restProps
|
|
31
45
|
}: Props = $props();
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
// True while a deferred (async) `onVisibilityChange` is in flight — disables the icon so double
|
|
48
|
+
// clicks are ignored and shows a spinner instead of the eye.
|
|
49
|
+
let pending = $state(false);
|
|
34
50
|
|
|
35
|
-
function toggleVisibility() {
|
|
36
|
-
|
|
51
|
+
async function toggleVisibility() {
|
|
52
|
+
if (pending) return;
|
|
53
|
+
const next = !visible;
|
|
54
|
+
const result = onVisibilityChange?.(next);
|
|
55
|
+
if (result instanceof Promise) {
|
|
56
|
+
pending = true;
|
|
57
|
+
try {
|
|
58
|
+
if ((await result) === false) return; // cancelled
|
|
59
|
+
} finally {
|
|
60
|
+
pending = false;
|
|
61
|
+
}
|
|
62
|
+
} else if (result === false) {
|
|
63
|
+
return; // cancelled synchronously
|
|
64
|
+
}
|
|
65
|
+
visible = next;
|
|
37
66
|
}
|
|
38
67
|
</script>
|
|
39
68
|
|
|
69
|
+
<!-- restProps is spread FIRST so the eye wiring below always wins (a consumer `onIconClick`/
|
|
70
|
+
`iconClickable` can't silently replace the toggle). `autocomplete` is then re-applied AFTER the
|
|
71
|
+
spread but folds in the consumer's value, so a login field's `autocomplete="current-password"`
|
|
72
|
+
still overrides the `"new-password"` default (per docs/components.md). -->
|
|
40
73
|
<Input
|
|
74
|
+
{...restProps}
|
|
41
75
|
bind:value
|
|
42
76
|
bind:valid
|
|
43
77
|
bind:touched
|
|
44
78
|
bind:element
|
|
45
|
-
type={
|
|
46
|
-
autocomplete="new-password"
|
|
47
|
-
iconClickable={
|
|
79
|
+
type={visible ? "text" : "password"}
|
|
80
|
+
autocomplete={restProps.autocomplete ?? "new-password"}
|
|
81
|
+
iconClickable={!pending}
|
|
48
82
|
onIconClick={toggleVisibility}
|
|
49
|
-
{...restProps}
|
|
50
83
|
>
|
|
51
84
|
{#snippet icon()}
|
|
52
|
-
{#if
|
|
85
|
+
{#if pending}<Loading class="animate-spin" />{:else if visible}<EyeOff />{:else}<Eye />{/if}
|
|
53
86
|
{/snippet}
|
|
54
87
|
</Input>
|
|
@@ -15,7 +15,18 @@ interface Props extends Omit<HTMLInputAttributes, "type" | "size">, FieldChromeP
|
|
|
15
15
|
/** Shared axes — forwarded to the underlying Input. */
|
|
16
16
|
variant?: Variant;
|
|
17
17
|
borderless?: boolean;
|
|
18
|
+
/** Whether the value is shown as plain text (eye toggled on). Bindable; defaults `false`.
|
|
19
|
+
* Drive it to open the field already-visible, or re-hide it after a save. */
|
|
20
|
+
visible?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Called when the user clicks the eye, with the state being moved TO (`!visible`).
|
|
23
|
+
* Return `false` (or a `Promise` resolving to `false`) to CANCEL the toggle; a returned
|
|
24
|
+
* `Promise` DEFERS the flip until it settles — the eye shows a busy spinner and ignores
|
|
25
|
+
* further clicks meanwhile, so an audited async reveal can populate `value` before it
|
|
26
|
+
* becomes visible (the field never shows plain text over an empty value).
|
|
27
|
+
*/
|
|
28
|
+
onVisibilityChange?: (next: boolean) => void | boolean | Promise<void | boolean>;
|
|
18
29
|
}
|
|
19
|
-
declare const PasswordInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched">;
|
|
30
|
+
declare const PasswordInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched" | "visible">;
|
|
20
31
|
type PasswordInput = ReturnType<typeof PasswordInput>;
|
|
21
32
|
export default PasswordInput;
|
package/docs/components.md
CHANGED
|
@@ -307,9 +307,35 @@ Thin wrappers over `Input`, each preconfigured with an MDI icon (rendered into I
|
|
|
307
307
|
|
|
308
308
|
- **Location**: `src/lib/components/PasswordInput.svelte`
|
|
309
309
|
- **Features**: built-in visibility toggle (eye / eye-off MDI icons)
|
|
310
|
+
- **`visible`** (bindable, default `false`): whether the value shows as plain text. Read it, or drive
|
|
311
|
+
it — open the field already-visible, or re-hide it after a save (`visible = false`). Previously this
|
|
312
|
+
was private; a consumer had to watch the input's `type` attribute via a `MutationObserver`.
|
|
313
|
+
- **`onVisibilityChange?: (next: boolean) => void | boolean | Promise<void | boolean>`**: fired on each
|
|
314
|
+
eye click with the state being moved **to**. Return `false` (or a `Promise<false>`) to **cancel** the
|
|
315
|
+
toggle; return a `Promise` to **defer** the flip until it settles — the eye shows a **busy spinner**
|
|
316
|
+
and ignores clicks meanwhile, so an async reveal can populate `value` before it becomes visible (the
|
|
317
|
+
field never shows plain text over an empty value).
|
|
318
|
+
- **Icon wiring**: a consumer `onIconClick`/`iconClickable` no longer overrides the toggle — the eye
|
|
319
|
+
wiring is applied after the prop spread, so the toggle always wins.
|
|
310
320
|
- **`autocomplete`**: defaults to `"new-password"` (clears Chrome's "should have autocomplete"
|
|
311
321
|
advisory and stops unrelated saved-credential autofill). Overridable per usage — a login field
|
|
312
|
-
passes `autocomplete="current-password"` and it wins
|
|
322
|
+
passes `autocomplete="current-password"` and it wins.
|
|
323
|
+
|
|
324
|
+
```svelte
|
|
325
|
+
<!-- On-demand reveal: read the stored secret from an audited backend the FIRST time the eye opens,
|
|
326
|
+
BEFORE the field flips to plain text. Re-hide after save with `pwVisible = false`. -->
|
|
327
|
+
<script>
|
|
328
|
+
import { PasswordInput } from "@spaethtech/svelte-ui";
|
|
329
|
+
let value = $state("");
|
|
330
|
+
let pwVisible = $state(false);
|
|
331
|
+
let loaded = $state(false);
|
|
332
|
+
async function beforeShow(next) {
|
|
333
|
+
if (next && !loaded) { value = await fetchSecret(); loaded = true; } // Promise defers the flip
|
|
334
|
+
}
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<PasswordInput label="Password" bind:value bind:visible={pwVisible} onVisibilityChange={beforeShow} />
|
|
338
|
+
```
|
|
313
339
|
|
|
314
340
|
### EmailInput
|
|
315
341
|
|