@marianmeres/stuic 3.76.0 → 3.76.2
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.
- package/dist/actions/on-submit-validity-check.svelte.js +12 -0
- package/dist/actions/validate.svelte.js +6 -1
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte +28 -4
- package/dist/components/LoginForm/LoginForm.svelte +28 -0
- package/dist/components/LoginForm/LoginFormModal.svelte +9 -0
- package/dist/components/LoginForm/LoginFormModal.svelte.d.ts +6 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +14 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +7 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +19 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +13 -0
- package/dist/components/Modal/Modal.svelte +4 -0
- package/dist/components/Modal/Modal.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -55,6 +55,18 @@ export function onSubmitValidityCheck(node) {
|
|
|
55
55
|
// input (last radio input), which is not desired
|
|
56
56
|
if (el.type === "radio")
|
|
57
57
|
continue;
|
|
58
|
+
// Clear any stale `customError` flag from a prior submit attempt before
|
|
59
|
+
// re-dispatching the validate listeners. Without this, if the field's
|
|
60
|
+
// per-field validate $effect was torn down/re-mounted in a way that
|
|
61
|
+
// skipped re-running its `customValidator` (e.g., the parent re-rendered
|
|
62
|
+
// for an unrelated reason between submits), the previous submit's
|
|
63
|
+
// customValidity message lingers on the DOM element. Then `el.validity.valid`
|
|
64
|
+
// would return false here even though the customValidator now reports no
|
|
65
|
+
// error, silently routing the form to `submit_invalid` and never calling
|
|
66
|
+
// the consumer's onSubmit. The dispatched change event below re-runs the
|
|
67
|
+
// per-field validator which re-applies a real customValidity if needed.
|
|
68
|
+
if (typeof el.setCustomValidity === "function")
|
|
69
|
+
el.setCustomValidity("");
|
|
58
70
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
59
71
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
60
72
|
// typeof el.checkValidity === "function" && !el.checkValidity();
|
|
@@ -165,8 +165,13 @@ export function validate(el, fn) {
|
|
|
165
165
|
let customValidatorMessage = "";
|
|
166
166
|
if (typeof customValidator === "function") {
|
|
167
167
|
customValidatorMessage = customValidator(el.value, context, el) || "";
|
|
168
|
-
el.setCustomValidity(customValidatorMessage);
|
|
169
168
|
}
|
|
169
|
+
// Always write — including the no-validator case (which writes "" and
|
|
170
|
+
// thus clears any stale `customError` flag the element may be carrying
|
|
171
|
+
// from a prior listener generation). Without this, a `customValidator`
|
|
172
|
+
// being removed across re-renders would leave the previous message
|
|
173
|
+
// stuck on the element forever.
|
|
174
|
+
el.setCustomValidity(customValidatorMessage);
|
|
170
175
|
// this triggers the bubble, which is not what we want
|
|
171
176
|
// el.reportValidity();
|
|
172
177
|
const validityState = el.validity;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<script lang="ts">
|
|
21
|
+
import { untrack } from "svelte";
|
|
21
22
|
import { slide } from "svelte/transition";
|
|
22
23
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
23
24
|
import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
|
|
@@ -44,13 +45,36 @@
|
|
|
44
45
|
intent,
|
|
45
46
|
forceAsHtml = true,
|
|
46
47
|
duration = 150,
|
|
47
|
-
onDismiss
|
|
48
|
+
onDismiss,
|
|
48
49
|
withIcon,
|
|
49
50
|
iconFn,
|
|
50
51
|
}: Props = $props();
|
|
51
52
|
|
|
53
|
+
// Track dismissal in local state instead of mutating the (non-bindable) `message`
|
|
54
|
+
// prop. Mutating a destructured prop var creates a local shadow that Svelte 5
|
|
55
|
+
// won't always overwrite when the parent re-passes the same value — so a user
|
|
56
|
+
// who dismissed an error would never see the SAME error message again, even
|
|
57
|
+
// after the parent re-set it. Keeping `_dismissed` separate sidesteps that and
|
|
58
|
+
// makes the dismiss state reset cleanly whenever the message changes.
|
|
52
59
|
let _message = $derived(message ? String(message) : "");
|
|
53
|
-
let
|
|
60
|
+
let _dismissed = $state(false);
|
|
61
|
+
let _show = $derived(isTHCNotEmpty(_message) && !_dismissed);
|
|
62
|
+
|
|
63
|
+
// Reset the dismissed flag whenever the message changes — a new (or re-set)
|
|
64
|
+
// message from the parent should re-show, even if the user previously dismissed.
|
|
65
|
+
$effect(() => {
|
|
66
|
+
void _message;
|
|
67
|
+
untrack(() => {
|
|
68
|
+
if (_dismissed) _dismissed = false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Default dismiss handler hides the message locally. Parent state is left alone
|
|
73
|
+
// (this prop isn't bindable). Consumers wanting parent-side cleanup pass their own.
|
|
74
|
+
// `null`/`false` mean "no dismiss button" and are handled at the render site.
|
|
75
|
+
let _onDismiss = $derived(
|
|
76
|
+
typeof onDismiss === "function" ? onDismiss : () => (_dismissed = true)
|
|
77
|
+
);
|
|
54
78
|
|
|
55
79
|
let _iconHtml = $derived.by(() => {
|
|
56
80
|
if (iconFn === false) return "";
|
|
@@ -77,7 +101,7 @@
|
|
|
77
101
|
<Thc thc={_message} {forceAsHtml} />
|
|
78
102
|
</div>
|
|
79
103
|
|
|
80
|
-
{#if
|
|
104
|
+
{#if onDismiss !== false && onDismiss !== null}
|
|
81
105
|
<div class="dismiss">
|
|
82
106
|
<Button
|
|
83
107
|
x
|
|
@@ -86,7 +110,7 @@
|
|
|
86
110
|
roundedFull
|
|
87
111
|
size="sm"
|
|
88
112
|
type="button"
|
|
89
|
-
onclick={() =>
|
|
113
|
+
onclick={() => _onDismiss()}
|
|
90
114
|
/>
|
|
91
115
|
</div>
|
|
92
116
|
{/if}
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
</script>
|
|
78
78
|
|
|
79
79
|
<script lang="ts">
|
|
80
|
+
import { untrack } from "svelte";
|
|
80
81
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
81
82
|
import { t_default } from "./_internal/login-form-i18n-defaults.js";
|
|
82
83
|
import {
|
|
@@ -130,6 +131,19 @@
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function handleSubmitValid() {
|
|
134
|
+
// Defensively clear any stale customValidity left on form fields by a prior
|
|
135
|
+
// validation pass. The `validate` action already clears it when the field's
|
|
136
|
+
// customValidator returns an empty string, but a field can skip that path
|
|
137
|
+
// (e.g., it was disabled, or its $effect torn down between submits) and the
|
|
138
|
+
// stale flag would then make `el.validity.valid` return false on the next
|
|
139
|
+
// submit, silently routing the form to `submit_invalid` and never calling
|
|
140
|
+
// `onSubmit`. Resetting here gives every retry a clean slate.
|
|
141
|
+
if (el) {
|
|
142
|
+
for (const node of Array.from(el.elements) as HTMLInputElement[]) {
|
|
143
|
+
if (typeof node.setCustomValidity === "function") node.setCustomValidity("");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
const validationErrors = validateLoginForm(formData, t);
|
|
134
148
|
internalErrors = validationErrors;
|
|
135
149
|
|
|
@@ -138,6 +152,20 @@
|
|
|
138
152
|
}
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
// Clear internal field errors as soon as the user edits the form, so a previous
|
|
156
|
+
// failed-submit's errors don't linger after the user has fixed them. Re-validation
|
|
157
|
+
// on the next submit will repopulate `internalErrors` if anything is still wrong.
|
|
158
|
+
// `untrack` for the read+write so this effect only re-runs on formData changes —
|
|
159
|
+
// otherwise `handleSubmitValid` setting `internalErrors` would immediately re-fire
|
|
160
|
+
// this effect and wipe the errors back out.
|
|
161
|
+
$effect(() => {
|
|
162
|
+
void formData.email;
|
|
163
|
+
void formData.password;
|
|
164
|
+
untrack(() => {
|
|
165
|
+
if (internalErrors.length) internalErrors = [];
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
141
169
|
$effect(() => {
|
|
142
170
|
if (error && notifications) notifications.error(error);
|
|
143
171
|
});
|
|
@@ -88,6 +88,13 @@
|
|
|
88
88
|
|
|
89
89
|
noXClose?: boolean;
|
|
90
90
|
onClose?: () => false | void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
94
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
95
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
96
|
+
*/
|
|
97
|
+
noClickOutsideClose?: boolean;
|
|
91
98
|
}
|
|
92
99
|
</script>
|
|
93
100
|
|
|
@@ -126,6 +133,7 @@
|
|
|
126
133
|
unstyled = false,
|
|
127
134
|
noXClose = false,
|
|
128
135
|
onClose,
|
|
136
|
+
noClickOutsideClose = true,
|
|
129
137
|
}: Props = $props();
|
|
130
138
|
|
|
131
139
|
let t = $derived(tProp ?? t_default);
|
|
@@ -152,6 +160,7 @@
|
|
|
152
160
|
class={classModal}
|
|
153
161
|
classInner={twMerge("max-w-sm md:max-w-sm", "h-auto md:h-auto m-auto", classInner)}
|
|
154
162
|
classDialog="flex items-center justify-center"
|
|
163
|
+
{noClickOutsideClose}
|
|
155
164
|
>
|
|
156
165
|
{#snippet header()}
|
|
157
166
|
<div class="flex items-center justify-between p-4">
|
|
@@ -67,6 +67,12 @@ export interface Props {
|
|
|
67
67
|
unstyled?: boolean;
|
|
68
68
|
noXClose?: boolean;
|
|
69
69
|
onClose?: () => false | void;
|
|
70
|
+
/**
|
|
71
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
72
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
73
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
74
|
+
*/
|
|
75
|
+
noClickOutsideClose?: boolean;
|
|
70
76
|
}
|
|
71
77
|
declare const LoginFormModal: import("svelte").Component<Props, {
|
|
72
78
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
@@ -117,6 +117,15 @@
|
|
|
117
117
|
>;
|
|
118
118
|
|
|
119
119
|
notifications?: NotificationsStack;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Called when the active mode changes (login/register/verify). Receives
|
|
123
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
124
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
125
|
+
* and Sign up.
|
|
126
|
+
*/
|
|
127
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
128
|
+
|
|
120
129
|
t?: TranslateFn;
|
|
121
130
|
unstyled?: boolean;
|
|
122
131
|
class?: string;
|
|
@@ -155,6 +164,7 @@
|
|
|
155
164
|
socialDividerLabel,
|
|
156
165
|
footer,
|
|
157
166
|
notifications,
|
|
167
|
+
onModeChange,
|
|
158
168
|
t: tProp,
|
|
159
169
|
unstyled = false,
|
|
160
170
|
class: classProp,
|
|
@@ -173,6 +183,7 @@
|
|
|
173
183
|
// effect (which would be prone to loops).
|
|
174
184
|
function setMode(next: LoginOrRegisterFormMode) {
|
|
175
185
|
if (next === mode) return;
|
|
186
|
+
const prev = mode;
|
|
176
187
|
const sourceEmail =
|
|
177
188
|
mode === "verify"
|
|
178
189
|
? verifyEmail
|
|
@@ -187,6 +198,9 @@
|
|
|
187
198
|
// next === "verify"
|
|
188
199
|
verifyEmail = sourceEmail;
|
|
189
200
|
}
|
|
201
|
+
// Notify before mutating so consumers can clear parent-owned, mode-specific
|
|
202
|
+
// state (e.g., a stale `error`) before the new view renders with it.
|
|
203
|
+
onModeChange?.(next, prev);
|
|
190
204
|
mode = next;
|
|
191
205
|
}
|
|
192
206
|
|
|
@@ -78,6 +78,13 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
78
78
|
}
|
|
79
79
|
]>;
|
|
80
80
|
notifications?: NotificationsStack;
|
|
81
|
+
/**
|
|
82
|
+
* Called when the active mode changes (login/register/verify). Receives
|
|
83
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
84
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
85
|
+
* and Sign up.
|
|
86
|
+
*/
|
|
87
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
81
88
|
t?: TranslateFn;
|
|
82
89
|
unstyled?: boolean;
|
|
83
90
|
class?: string;
|
|
@@ -84,6 +84,21 @@
|
|
|
84
84
|
|
|
85
85
|
noXClose?: boolean;
|
|
86
86
|
onClose?: () => false | void;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
90
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
91
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
92
|
+
*/
|
|
93
|
+
noClickOutsideClose?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Called when the active form mode changes (login/register/verify). Receives
|
|
97
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
98
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
99
|
+
* and Sign up.
|
|
100
|
+
*/
|
|
101
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
87
102
|
}
|
|
88
103
|
</script>
|
|
89
104
|
|
|
@@ -129,6 +144,8 @@
|
|
|
129
144
|
unstyled = false,
|
|
130
145
|
noXClose = false,
|
|
131
146
|
onClose,
|
|
147
|
+
noClickOutsideClose = true,
|
|
148
|
+
onModeChange,
|
|
132
149
|
}: Props = $props();
|
|
133
150
|
|
|
134
151
|
let t = $derived(tProp ?? t_default);
|
|
@@ -163,6 +180,7 @@
|
|
|
163
180
|
class={classModal}
|
|
164
181
|
classInner={twMerge("max-w-sm md:max-w-sm", "h-auto md:h-auto m-auto", classInner)}
|
|
165
182
|
classDialog="flex items-center justify-center"
|
|
183
|
+
{noClickOutsideClose}
|
|
166
184
|
>
|
|
167
185
|
{#snippet header()}
|
|
168
186
|
<div class="flex items-center justify-between p-4">
|
|
@@ -207,6 +225,7 @@
|
|
|
207
225
|
{socialDividerLabel}
|
|
208
226
|
{footer}
|
|
209
227
|
{notifications}
|
|
228
|
+
{onModeChange}
|
|
210
229
|
t={tProp}
|
|
211
230
|
{unstyled}
|
|
212
231
|
class={classForm}
|
|
@@ -59,6 +59,19 @@ export interface Props {
|
|
|
59
59
|
unstyled?: boolean;
|
|
60
60
|
noXClose?: boolean;
|
|
61
61
|
onClose?: () => false | void;
|
|
62
|
+
/**
|
|
63
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
64
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
65
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
66
|
+
*/
|
|
67
|
+
noClickOutsideClose?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Called when the active form mode changes (login/register/verify). Receives
|
|
70
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
71
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
72
|
+
* and Sign up.
|
|
73
|
+
*/
|
|
74
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
62
75
|
}
|
|
63
76
|
declare const LoginOrRegisterFormModal: import("svelte").Component<Props, {
|
|
64
77
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
onEscape?: () => void;
|
|
23
23
|
/** Disable body scroll lock when modal is open */
|
|
24
24
|
noScrollLock?: boolean;
|
|
25
|
+
/** Disable close on backdrop / outside click */
|
|
26
|
+
noClickOutsideClose?: boolean;
|
|
25
27
|
}
|
|
26
28
|
</script>
|
|
27
29
|
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
el = $bindable(),
|
|
46
48
|
onEscape,
|
|
47
49
|
noScrollLock = false,
|
|
50
|
+
noClickOutsideClose = false,
|
|
48
51
|
}: Props = $props();
|
|
49
52
|
|
|
50
53
|
let modalDialog: ModalDialog = $state()!;
|
|
@@ -89,6 +92,7 @@
|
|
|
89
92
|
ariaLabelledby={labelledby}
|
|
90
93
|
ariaDescribedby={describedby}
|
|
91
94
|
{noScrollLock}
|
|
95
|
+
{noClickOutsideClose}
|
|
92
96
|
preEscapeClose={handlePreEscapeClose}
|
|
93
97
|
preClose={handlePreClose}
|
|
94
98
|
class={twMerge(
|
|
@@ -20,6 +20,8 @@ export interface Props {
|
|
|
20
20
|
onEscape?: () => void;
|
|
21
21
|
/** Disable body scroll lock when modal is open */
|
|
22
22
|
noScrollLock?: boolean;
|
|
23
|
+
/** Disable close on backdrop / outside click */
|
|
24
|
+
noClickOutsideClose?: boolean;
|
|
23
25
|
}
|
|
24
26
|
declare const Modal: import("svelte").Component<Props, {
|
|
25
27
|
close: () => void;
|