@akinon/pz-theme 2.0.165-rc.0 → 2.0.165
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/CHANGELOG.md +10 -51
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/__tests__/profile-form-composed.test.tsx +795 -0
- package/src/auth/auth-field-errors.tsx +59 -13
- package/src/auth/form-field-defaults.tsx +74 -0
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/auth-consent-block.tsx +78 -19
- package/src/blocks/auth-field-block.tsx +212 -50
- package/src/blocks/auth-form-block.tsx +3 -4
- package/src/blocks/birth-date-selects.tsx +206 -0
- package/src/blocks/forgot-password-block.tsx +3 -4
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/otp-form-block.tsx +3 -4
- package/src/blocks/profile-form-block.tsx +353 -10
- package/src/blocks/register-form-block.tsx +12 -6
- package/src/blocks/reset-password-block.tsx +3 -4
- package/src/blocks/text-block.tsx +2 -5
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/theme-placeholder-wrapper.tsx +0 -3
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -8,8 +8,28 @@ import {
|
|
|
8
8
|
} from '@akinon/next/data/client/account';
|
|
9
9
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
10
10
|
import { useHostModalCloser } from '../overlays/modal-host-context';
|
|
11
|
+
import {
|
|
12
|
+
AuthFieldErrors,
|
|
13
|
+
AuthFieldErrorsProvider,
|
|
14
|
+
clearAuthFieldError,
|
|
15
|
+
collectAuthFieldErrors,
|
|
16
|
+
eventFieldName,
|
|
17
|
+
fieldBoxOf,
|
|
18
|
+
isVisibleControl
|
|
19
|
+
} from '../auth/auth-field-errors';
|
|
20
|
+
import {
|
|
21
|
+
AuthFormErrorProvider,
|
|
22
|
+
containsAuthErrorBlock
|
|
23
|
+
} from '../auth/form-error-context';
|
|
24
|
+
import {
|
|
25
|
+
AuthFormStateProvider,
|
|
26
|
+
AuthFormStateValue,
|
|
27
|
+
showsInFormState
|
|
28
|
+
} from '../auth/form-state-context';
|
|
29
|
+
import { FormFieldDefaultsProvider } from '../auth/form-field-defaults';
|
|
11
30
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
12
31
|
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
32
|
+
import { renderComposedChildren } from './render-composed-children';
|
|
13
33
|
|
|
14
34
|
interface ProfileLike {
|
|
15
35
|
first_name?: string;
|
|
@@ -22,6 +42,102 @@ interface ProfileLike {
|
|
|
22
42
|
email_allowed?: boolean;
|
|
23
43
|
}
|
|
24
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Fields the profile shows but this form cannot change: commerce moves the
|
|
47
|
+
* e-mail through its own verified flow, and its profile serializer marks it
|
|
48
|
+
* read-only, so a value typed here would be silently dropped.
|
|
49
|
+
*/
|
|
50
|
+
const READ_ONLY_FIELDS = ['email'];
|
|
51
|
+
|
|
52
|
+
type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
53
|
+
|
|
54
|
+
/** A tick whose value is the plain agreement flag rather than a brand's own. */
|
|
55
|
+
const isFlagValue = (value: string) => value === 'true' || value === 'on';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a composed profile form saves: the answers of the fields the shopper can
|
|
59
|
+
* actually see, and nothing else.
|
|
60
|
+
*
|
|
61
|
+
* The profile is saved with a PATCH (`updateProfile` in @akinon/next, commerce's
|
|
62
|
+
* `partial_update`), so a key that is not sent is a key that is not changed.
|
|
63
|
+
* That is what makes leaving things out the safe default:
|
|
64
|
+
* - a field the composition does not place, or hides on this breakpoint, keeps
|
|
65
|
+
* its stored value — a consent tick the design took off mobile must not
|
|
66
|
+
* post "unticked" and withdraw a consent the shopper never saw;
|
|
67
|
+
* - a question that was never answered and is still empty (a gender left on
|
|
68
|
+
* its empty option, a half-picked date, a box that started blank) is not
|
|
69
|
+
* written, so it cannot turn into an error from commerce's validation;
|
|
70
|
+
* - a box that started filled and was emptied IS an answer — the shopper
|
|
71
|
+
* deleted their phone — so it is sent as `''`, as the fixed form sent it;
|
|
72
|
+
* - a tick is an answer either way: a visible consent row posts `true` or
|
|
73
|
+
* `false`, because leaving it unticked is how a shopper withdraws. A custom
|
|
74
|
+
* tick with a value of its own posts that value, or `''` when unticked.
|
|
75
|
+
*
|
|
76
|
+
* Visibility is the FIELD's (see `fieldBoxOf`), not the native control's, and
|
|
77
|
+
* first control per name wins — the rules the field validation applies too.
|
|
78
|
+
*/
|
|
79
|
+
export const readProfilePayload = (
|
|
80
|
+
form: HTMLFormElement
|
|
81
|
+
): Record<string, string | boolean> => {
|
|
82
|
+
const payload: Record<string, string | boolean> = {};
|
|
83
|
+
const answered = new Set<string>();
|
|
84
|
+
|
|
85
|
+
form
|
|
86
|
+
.querySelectorAll<FormControl>('input[name], select[name], textarea[name]')
|
|
87
|
+
.forEach((control) => {
|
|
88
|
+
const { name } = control;
|
|
89
|
+
|
|
90
|
+
if (!name || answered.has(name) || READ_ONLY_FIELDS.includes(name)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Disabled controls (or a disabled fieldset around them) are what the
|
|
95
|
+
// form shows but does not own.
|
|
96
|
+
if (control.matches(':disabled')) return;
|
|
97
|
+
|
|
98
|
+
if (!isVisibleControl(fieldBoxOf(control))) return;
|
|
99
|
+
|
|
100
|
+
if (control instanceof HTMLInputElement && control.type === 'checkbox') {
|
|
101
|
+
answered.add(name);
|
|
102
|
+
|
|
103
|
+
if (isFlagValue(control.value)) {
|
|
104
|
+
payload[name] = control.checked;
|
|
105
|
+
} else {
|
|
106
|
+
payload[name] = control.checked ? control.value : '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (control instanceof HTMLInputElement && control.type === 'radio') {
|
|
113
|
+
// The group's answer is its checked radio; one with none checked is a
|
|
114
|
+
// question the shopper left, and later radios may still answer it.
|
|
115
|
+
if (!control.checked) return;
|
|
116
|
+
|
|
117
|
+
answered.add(name);
|
|
118
|
+
payload[name] = control.value;
|
|
119
|
+
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (control.value === '') {
|
|
124
|
+
// Emptied by the shopper: the box started with the stored value.
|
|
125
|
+
const wasFilled =
|
|
126
|
+
(control instanceof HTMLInputElement && control.type !== 'hidden') ||
|
|
127
|
+
control instanceof HTMLTextAreaElement
|
|
128
|
+
? control.defaultValue !== ''
|
|
129
|
+
: false;
|
|
130
|
+
|
|
131
|
+
if (!wasFilled) return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
answered.add(name);
|
|
135
|
+
payload[name] = control.value;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return payload;
|
|
139
|
+
};
|
|
140
|
+
|
|
25
141
|
const EMPTY_FORM = {
|
|
26
142
|
first_name: '',
|
|
27
143
|
last_name: '',
|
|
@@ -49,15 +165,27 @@ const SUBMIT_ALIGNMENTS: Record<string, string> = {
|
|
|
49
165
|
* E-mail is rendered read-only on purpose: `updateProfile` has no e-mail field
|
|
50
166
|
* — commerce moves it through a separate verified `updateEmail` flow — so an
|
|
51
167
|
* editable input here would silently discard what the shopper typed.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
168
|
+
*
|
|
169
|
+
* Composable, the way the Register Form is: with children the form draws THEM
|
|
170
|
+
* — an `auth-field` per input (gender, a day/month/year birth date, a brand's
|
|
171
|
+
* own custom field), an `auth-consent` per contact channel, any text, link or
|
|
172
|
+
* group around them — starts each one from the shopper's profile, and saves
|
|
173
|
+
* what the visible ones say (see `readProfilePayload`). Left empty it renders
|
|
174
|
+
* the fixed rows exactly as before, so a published composition is untouched.
|
|
175
|
+
*
|
|
176
|
+
* Properties: submitLabel, submitAlign, showName, showPhone, showEmail,
|
|
177
|
+
* showBirthDate, showLabels, firstNameLabel, lastNameLabel, emailLabel,
|
|
178
|
+
* phoneLabel, birthDateLabel, helpLabel, errorMessage, successMessage,
|
|
179
|
+
* requiredMessage, invalidMessage, closeHostModalOnSuccess.
|
|
55
180
|
* Parts: form, field, label, input, help, error, success, submit.
|
|
56
181
|
*/
|
|
57
182
|
const ProfileFormBlock = ({
|
|
58
183
|
block,
|
|
184
|
+
placeholderId,
|
|
185
|
+
sectionId,
|
|
59
186
|
currentBreakpoint = 'desktop',
|
|
60
|
-
isDesigner = false
|
|
187
|
+
isDesigner = false,
|
|
188
|
+
selectedBlockId
|
|
61
189
|
}: BlockRendererProps) => {
|
|
62
190
|
const text = (key: string, fallback: string) =>
|
|
63
191
|
String(
|
|
@@ -88,9 +216,13 @@ const ProfileFormBlock = ({
|
|
|
88
216
|
'successMessage',
|
|
89
217
|
'Your profile has been saved.'
|
|
90
218
|
);
|
|
219
|
+
// The copy a composed field shows under itself when it is left empty or
|
|
220
|
+
// filled in wrongly — see `auth/auth-field-errors.tsx`.
|
|
221
|
+
const requiredMessage = text('requiredMessage', 'This field is required.');
|
|
222
|
+
const invalidMessage = text('invalidMessage', 'Please check this field.');
|
|
91
223
|
// Lets a composition scope the form to one field (e.g. a birth-date-only
|
|
92
224
|
// drawer). The hidden name fields still submit their loaded values, so the
|
|
93
|
-
//
|
|
225
|
+
// save leaves them as they were.
|
|
94
226
|
const showName = readToggle(
|
|
95
227
|
block.properties?.showName,
|
|
96
228
|
currentBreakpoint,
|
|
@@ -122,16 +254,30 @@ const ProfileFormBlock = ({
|
|
|
122
254
|
|
|
123
255
|
// Guests have no profile endpoint; skipping avoids a guaranteed 403 on every
|
|
124
256
|
// page that hosts this block.
|
|
125
|
-
const {
|
|
257
|
+
const {
|
|
258
|
+
data: profile,
|
|
259
|
+
isLoading,
|
|
260
|
+
isError: profileFailed
|
|
261
|
+
} = useGetProfileInfoQuery(undefined, {
|
|
126
262
|
skip: !isAuthenticated
|
|
127
263
|
});
|
|
128
264
|
const [updateProfile, { isLoading: isSaving }] = useUpdateProfileMutation();
|
|
129
265
|
|
|
130
266
|
const [values, setValues] = useState(EMPTY_FORM);
|
|
131
267
|
const [status, setStatus] = useState<'idle' | 'saved' | 'error'>('idle');
|
|
268
|
+
const [fieldErrors, setFieldErrors] = useState<AuthFieldErrors>({});
|
|
132
269
|
|
|
133
270
|
const loadedProfile = (profile ?? null) as ProfileLike | null;
|
|
134
271
|
|
|
272
|
+
/**
|
|
273
|
+
* The children a composition authored. Hidden ones are dropped for a shopper
|
|
274
|
+
* and kept on the canvas, so an author can dress a row they switched off.
|
|
275
|
+
*/
|
|
276
|
+
const childBlocks = (block.blocks || []).filter((childBlock) =>
|
|
277
|
+
isDesigner ? true : !childBlock.hidden
|
|
278
|
+
);
|
|
279
|
+
const isComposed = childBlocks.length > 0;
|
|
280
|
+
|
|
135
281
|
useEffect(() => {
|
|
136
282
|
if (!loadedProfile) return;
|
|
137
283
|
|
|
@@ -168,9 +314,9 @@ const ProfileFormBlock = ({
|
|
|
168
314
|
last_name: values.last_name,
|
|
169
315
|
phone: values.phone,
|
|
170
316
|
date_of_birth: values.date_of_birth,
|
|
171
|
-
// The
|
|
172
|
-
//
|
|
173
|
-
//
|
|
317
|
+
// The fields this block does not render are echoed back as loaded.
|
|
318
|
+
// The save is a PATCH, so leaving them out would keep them too; the
|
|
319
|
+
// echo is kept because published forms have always sent it.
|
|
174
320
|
gender: loadedProfile?.gender ?? '',
|
|
175
321
|
sms_allowed: loadedProfile?.sms_allowed ?? false,
|
|
176
322
|
email_allowed: loadedProfile?.email_allowed ?? false
|
|
@@ -183,8 +329,201 @@ const ProfileFormBlock = ({
|
|
|
183
329
|
}
|
|
184
330
|
};
|
|
185
331
|
|
|
332
|
+
/**
|
|
333
|
+
* The composed save: the fields validate themselves (their own Required
|
|
334
|
+
* toggle, their own type), then only the answers on screen are sent.
|
|
335
|
+
*/
|
|
336
|
+
const submitComposed = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
|
|
339
|
+
// A submit inside the designer must never write to the shopper's account.
|
|
340
|
+
if (isDesigner || isSaving) return;
|
|
341
|
+
|
|
342
|
+
setStatus('idle');
|
|
343
|
+
|
|
344
|
+
const { errors, firstInvalid } = collectAuthFieldErrors(
|
|
345
|
+
event.currentTarget,
|
|
346
|
+
{ required: requiredMessage, invalid: invalidMessage }
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
setFieldErrors(errors);
|
|
350
|
+
|
|
351
|
+
if (firstInvalid) {
|
|
352
|
+
firstInvalid.focus();
|
|
353
|
+
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const payload = readProfilePayload(event.currentTarget);
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
// Nothing on screen to save is still a save the shopper asked for; it
|
|
361
|
+
// just has nothing to change.
|
|
362
|
+
if (Object.keys(payload).length > 0) {
|
|
363
|
+
await updateProfile(payload as never).unwrap();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setStatus('saved');
|
|
367
|
+
closeHostModal();
|
|
368
|
+
} catch {
|
|
369
|
+
setStatus('error');
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** A message describes what a field WAS: it goes once that field is edited. */
|
|
374
|
+
const handleComposedInput = (event: React.FormEvent<HTMLFormElement>) => {
|
|
375
|
+
const name = eventFieldName(event.target);
|
|
376
|
+
|
|
377
|
+
setStatus('idle');
|
|
378
|
+
setFieldErrors((current) => clearAuthFieldError(current, name));
|
|
379
|
+
};
|
|
380
|
+
|
|
186
381
|
if (!isDesigner && !isAuthenticated) return null;
|
|
187
382
|
|
|
383
|
+
if (isComposed) {
|
|
384
|
+
/**
|
|
385
|
+
* The fields are uncontrolled — each starts from the profile and the form
|
|
386
|
+
* reads them back on submit — so they are only drawn once the profile is
|
|
387
|
+
* there to start from. Drawing them earlier would show empty boxes and
|
|
388
|
+
* unticked consents that a quick "save" would then write back.
|
|
389
|
+
*
|
|
390
|
+
* The canvas draws them regardless, empty when the editor has no profile,
|
|
391
|
+
* so the author always has something to arrange.
|
|
392
|
+
*/
|
|
393
|
+
if (!isDesigner && !loadedProfile) {
|
|
394
|
+
// Still loading: nothing yet. Failed: say so, rather than leave an empty
|
|
395
|
+
// drawer that looks like a form nobody built.
|
|
396
|
+
if (!profileFailed || isLoading) return null;
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<div
|
|
400
|
+
data-profile-form=""
|
|
401
|
+
data-profile-form-state="unavailable"
|
|
402
|
+
role="alert"
|
|
403
|
+
{...partAttrs('error', { invalid: true })}
|
|
404
|
+
>
|
|
405
|
+
{errorMessage}
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const errorMessageNode = status === 'error' ? errorMessage : '';
|
|
411
|
+
const formStateValue: AuthFormStateValue = {
|
|
412
|
+
screen: 'form',
|
|
413
|
+
busy: isSaving,
|
|
414
|
+
hasError: status === 'error'
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<AuthFormErrorProvider value={errorMessageNode}>
|
|
419
|
+
<AuthFormStateProvider value={formStateValue}>
|
|
420
|
+
<AuthFieldErrorsProvider value={fieldErrors}>
|
|
421
|
+
<FormFieldDefaultsProvider
|
|
422
|
+
value={{
|
|
423
|
+
values: (loadedProfile ?? {}) as Record<string, unknown>,
|
|
424
|
+
readOnly: READ_ONLY_FIELDS
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
<form
|
|
428
|
+
// Remounted once a profile arrives on the canvas, so the
|
|
429
|
+
// fields pick up the values they start from.
|
|
430
|
+
key={loadedProfile ? 'profile' : 'empty'}
|
|
431
|
+
data-profile-form=""
|
|
432
|
+
data-profile-form-composed=""
|
|
433
|
+
data-profile-form-state={isSaving ? 'busy' : status}
|
|
434
|
+
onSubmit={submitComposed}
|
|
435
|
+
onInput={handleComposedInput}
|
|
436
|
+
onChange={handleComposedInput}
|
|
437
|
+
// The fields draw their own messages; see auth-field-errors.
|
|
438
|
+
noValidate
|
|
439
|
+
{...partAttrs('form', {
|
|
440
|
+
loading: isSaving,
|
|
441
|
+
invalid: status === 'error'
|
|
442
|
+
})}
|
|
443
|
+
style={{
|
|
444
|
+
display: 'flex',
|
|
445
|
+
flexDirection: 'column',
|
|
446
|
+
gap: 'var(--profile-form-gap, 12px)'
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
{renderComposedChildren({
|
|
450
|
+
// A Form State container shows only in the state it is
|
|
451
|
+
// scoped to (submitting, refused); anything else is shared
|
|
452
|
+
// chrome. There is no success SCREEN here — the form stays
|
|
453
|
+
// and says "saved" — so a success-scoped container never
|
|
454
|
+
// shows to a shopper, as in a register form.
|
|
455
|
+
blocks: childBlocks.filter((childBlock) =>
|
|
456
|
+
showsInFormState({
|
|
457
|
+
block: childBlock,
|
|
458
|
+
state: formStateValue,
|
|
459
|
+
isDesigner,
|
|
460
|
+
selectedBlockId: selectedBlockId ?? undefined,
|
|
461
|
+
currentBreakpoint
|
|
462
|
+
})
|
|
463
|
+
),
|
|
464
|
+
placeholderId,
|
|
465
|
+
sectionId,
|
|
466
|
+
isDesigner,
|
|
467
|
+
selectedBlockId,
|
|
468
|
+
currentBreakpoint
|
|
469
|
+
})}
|
|
470
|
+
|
|
471
|
+
{helpLabel && (
|
|
472
|
+
<div data-profile-form-help="" {...partAttrs('help')}>
|
|
473
|
+
{helpLabel}
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
|
|
477
|
+
{/* The fallback for a composition that has not placed a Form
|
|
478
|
+
Error block of its own. */}
|
|
479
|
+
{status === 'error' && !containsAuthErrorBlock(childBlocks) && (
|
|
480
|
+
<div
|
|
481
|
+
data-profile-form-error=""
|
|
482
|
+
role="alert"
|
|
483
|
+
{...partAttrs('error', { invalid: true })}
|
|
484
|
+
>
|
|
485
|
+
{errorMessage}
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
|
|
489
|
+
{status === 'saved' && (
|
|
490
|
+
<div
|
|
491
|
+
data-profile-form-success=""
|
|
492
|
+
role="status"
|
|
493
|
+
{...partAttrs('success')}
|
|
494
|
+
>
|
|
495
|
+
{successMessage}
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
<button
|
|
500
|
+
type="submit"
|
|
501
|
+
data-profile-form-submit=""
|
|
502
|
+
disabled={isSaving}
|
|
503
|
+
aria-busy={isSaving}
|
|
504
|
+
{...partAttrs('submit', { loading: isSaving })}
|
|
505
|
+
style={{
|
|
506
|
+
padding: '12px 20px',
|
|
507
|
+
background: 'var(--profile-form-cta-bg, #111827)',
|
|
508
|
+
color: 'var(--profile-form-cta-color, #ffffff)',
|
|
509
|
+
border: 'none',
|
|
510
|
+
cursor: isSaving ? 'not-allowed' : 'pointer',
|
|
511
|
+
opacity: isSaving ? 0.7 : 1,
|
|
512
|
+
...(SUBMIT_ALIGNMENTS[submitAlign]
|
|
513
|
+
? { alignSelf: SUBMIT_ALIGNMENTS[submitAlign] }
|
|
514
|
+
: null)
|
|
515
|
+
}}
|
|
516
|
+
>
|
|
517
|
+
{submitLabel}
|
|
518
|
+
</button>
|
|
519
|
+
</form>
|
|
520
|
+
</FormFieldDefaultsProvider>
|
|
521
|
+
</AuthFieldErrorsProvider>
|
|
522
|
+
</AuthFormStateProvider>
|
|
523
|
+
</AuthFormErrorProvider>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
188
527
|
if (isDesigner && !loadedProfile && !isLoading) {
|
|
189
528
|
return (
|
|
190
529
|
<DesignerPlaceholder
|
|
@@ -286,7 +625,11 @@ const ProfileFormBlock = ({
|
|
|
286
625
|
)}
|
|
287
626
|
|
|
288
627
|
{status === 'saved' && (
|
|
289
|
-
<div
|
|
628
|
+
<div
|
|
629
|
+
data-profile-form-success=""
|
|
630
|
+
role="status"
|
|
631
|
+
{...partAttrs('success')}
|
|
632
|
+
>
|
|
290
633
|
{successMessage}
|
|
291
634
|
</div>
|
|
292
635
|
)}
|
|
@@ -16,8 +16,11 @@ import {
|
|
|
16
16
|
AuthFieldErrors,
|
|
17
17
|
AuthFieldErrorsProvider,
|
|
18
18
|
clearAuthFieldError,
|
|
19
|
+
eventFieldName,
|
|
19
20
|
collectAuthFieldErrors,
|
|
20
|
-
|
|
21
|
+
fieldBoxOf,
|
|
22
|
+
focusFirstAuthFieldError,
|
|
23
|
+
isVisibleControl
|
|
21
24
|
} from '../auth/auth-field-errors';
|
|
22
25
|
import {
|
|
23
26
|
AuthFormErrorProvider,
|
|
@@ -395,11 +398,9 @@ const RegisterFormBlock = ({
|
|
|
395
398
|
}, [formHtmlId, isSubmitting]);
|
|
396
399
|
|
|
397
400
|
const handleInput = (event: React.FormEvent<HTMLFormElement>) => {
|
|
398
|
-
const
|
|
401
|
+
const name = eventFieldName(event.target);
|
|
399
402
|
|
|
400
|
-
setFieldErrors((current) =>
|
|
401
|
-
clearAuthFieldError(current, target?.name || '')
|
|
402
|
-
);
|
|
403
|
+
setFieldErrors((current) => clearAuthFieldError(current, name));
|
|
403
404
|
};
|
|
404
405
|
|
|
405
406
|
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
@@ -551,7 +552,12 @@ const RegisterFormBlock = ({
|
|
|
551
552
|
`input[type="checkbox"][name="${name}"]`
|
|
552
553
|
) as HTMLInputElement | null;
|
|
553
554
|
|
|
554
|
-
|
|
555
|
+
// A tick the design took off this breakpoint is a question nobody was
|
|
556
|
+
// asked here: it falls back to the blanket answer, instead of posting
|
|
557
|
+
// the refusal its untouched box would read as.
|
|
558
|
+
if (!control || !isVisibleControl(fieldBoxOf(control))) return undefined;
|
|
559
|
+
|
|
560
|
+
return control.checked;
|
|
555
561
|
};
|
|
556
562
|
|
|
557
563
|
const agreedToLegal = legalControl ? agreedToTerms : true;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AuthFieldErrors,
|
|
11
11
|
AuthFieldErrorsProvider,
|
|
12
12
|
clearAuthFieldError,
|
|
13
|
+
eventFieldName,
|
|
13
14
|
collectAuthFieldErrors,
|
|
14
15
|
focusFirstAuthFieldError
|
|
15
16
|
} from '../auth/auth-field-errors';
|
|
@@ -290,11 +291,9 @@ const ResetPasswordBlock = ({
|
|
|
290
291
|
const [passwordReset, { isLoading, isSuccess }] = usePasswordResetMutation();
|
|
291
292
|
|
|
292
293
|
const handleInput = (event: React.FormEvent<HTMLFormElement>) => {
|
|
293
|
-
const
|
|
294
|
+
const name = eventFieldName(event.target);
|
|
294
295
|
|
|
295
|
-
setFieldErrors((current) =>
|
|
296
|
-
clearAuthFieldError(current, target?.name || '')
|
|
297
|
-
);
|
|
296
|
+
setFieldErrors((current) => clearAuthFieldError(current, name));
|
|
298
297
|
};
|
|
299
298
|
|
|
300
299
|
/**
|
|
@@ -19,7 +19,7 @@ const TextBlock = ({
|
|
|
19
19
|
}: BlockRendererProps) => {
|
|
20
20
|
const { locale } = useLocalization();
|
|
21
21
|
const localize = useLocalizeHref();
|
|
22
|
-
const
|
|
22
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
23
23
|
const themeSettings = useThemeSettingsContext();
|
|
24
24
|
|
|
25
25
|
const getLocalizedContent = (): string => {
|
|
@@ -35,10 +35,7 @@ const TextBlock = ({
|
|
|
35
35
|
|
|
36
36
|
if (typeof value === 'object' && value !== null) {
|
|
37
37
|
return (
|
|
38
|
-
value[locale] ||
|
|
39
|
-
(defaultLocale && value[defaultLocale]) ||
|
|
40
|
-
Object.values(value)[0] ||
|
|
41
|
-
''
|
|
38
|
+
value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
|
|
42
39
|
);
|
|
43
40
|
}
|
|
44
41
|
|