@akinon/pz-theme 2.0.165-rc.0 → 2.0.166-beta.0
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 +18 -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
|
@@ -56,6 +56,51 @@ export interface AuthFieldValidation {
|
|
|
56
56
|
|
|
57
57
|
type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Every control a form checks: the named ones, plus the parts of a compound
|
|
61
|
+
* field.
|
|
62
|
+
*
|
|
63
|
+
* A compound field — the day, month and year selects of a birth date — submits
|
|
64
|
+
* ONE value under one name through a hidden input, and a hidden input is never
|
|
65
|
+
* validated. So the visible parts carry no name of their own (they would post
|
|
66
|
+
* three stray keys) and say which field they belong to through
|
|
67
|
+
* `data-field-name` instead; their `required` is then reported under that
|
|
68
|
+
* field's name, which is where the field draws its message.
|
|
69
|
+
*/
|
|
70
|
+
const VALIDATED_CONTROLS =
|
|
71
|
+
'input[name], select[name], textarea[name], [data-field-name]';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The box a control is presented in: its Auth Field or Consent Row when it has
|
|
75
|
+
* one, so "can the shopper see this question" is asked of the field rather
|
|
76
|
+
* than of the native control. A brand that draws its own tick or radio hides
|
|
77
|
+
* the native input and styles a sibling — the question is still on screen —
|
|
78
|
+
* and a compound field's hidden input is visible exactly when its parts are.
|
|
79
|
+
*/
|
|
80
|
+
export const fieldBoxOf = (control: HTMLElement): HTMLElement =>
|
|
81
|
+
control.closest<HTMLElement>('[data-auth-field], [data-auth-consent]') ??
|
|
82
|
+
(control instanceof HTMLInputElement && control.type === 'hidden'
|
|
83
|
+
? (control.parentElement ?? control)
|
|
84
|
+
: control);
|
|
85
|
+
|
|
86
|
+
/** The name a control's message is filed under. */
|
|
87
|
+
const fieldNameOf = (control: FormControl): string =>
|
|
88
|
+
control.name || control.dataset.fieldName || '';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The field an `input`/`change` event was about, for the form that clears that
|
|
92
|
+
* field's message as soon as it is edited. Same rule as the collector: a
|
|
93
|
+
* compound field's part answers with the field's name.
|
|
94
|
+
*/
|
|
95
|
+
export const eventFieldName = (target: EventTarget | null): string => {
|
|
96
|
+
const element = target as {
|
|
97
|
+
name?: string;
|
|
98
|
+
dataset?: { fieldName?: string };
|
|
99
|
+
} | null;
|
|
100
|
+
|
|
101
|
+
return element?.name || element?.dataset?.fieldName || '';
|
|
102
|
+
};
|
|
103
|
+
|
|
59
104
|
/**
|
|
60
105
|
* Whether a control is actually on screen for this breakpoint.
|
|
61
106
|
*
|
|
@@ -65,7 +110,7 @@ type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
|
65
110
|
* scrolled out of view, sized to nothing, or covered by another is still a
|
|
66
111
|
* control the shopper can be sent to.
|
|
67
112
|
*/
|
|
68
|
-
const isVisibleControl = (control:
|
|
113
|
+
export const isVisibleControl = (control: HTMLElement): boolean => {
|
|
69
114
|
if (typeof control.checkVisibility === 'function') {
|
|
70
115
|
return control.checkVisibility();
|
|
71
116
|
}
|
|
@@ -98,9 +143,7 @@ export const collectAuthFieldErrors = (
|
|
|
98
143
|
let firstInvalid: HTMLElement | null = null;
|
|
99
144
|
|
|
100
145
|
const controls = Array.from(
|
|
101
|
-
form.querySelectorAll<FormControl>(
|
|
102
|
-
'input[name], select[name], textarea[name]'
|
|
103
|
-
)
|
|
146
|
+
form.querySelectorAll<FormControl>(VALIDATED_CONTROLS)
|
|
104
147
|
);
|
|
105
148
|
|
|
106
149
|
controls.forEach((control) => {
|
|
@@ -116,12 +159,15 @@ export const collectAuthFieldErrors = (
|
|
|
116
159
|
// `noValidate` had already suppressed the browser's bubble, and the
|
|
117
160
|
// message this function returns was drawn inside the hidden wrapper. A
|
|
118
161
|
// field the shopper cannot see is not one they can be asked to fill in.
|
|
119
|
-
if (!isVisibleControl(control)) return;
|
|
162
|
+
if (!isVisibleControl(fieldBoxOf(control))) return;
|
|
163
|
+
|
|
164
|
+
// First one wins per name: a radio group, a compound field's parts, or a
|
|
165
|
+
// name a composition duplicated, is one question to the shopper rather
|
|
166
|
+
// than several.
|
|
167
|
+
const name = fieldNameOf(control);
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!errors[control.name]) {
|
|
124
|
-
errors[control.name] = control.validity.valueMissing
|
|
169
|
+
if (!errors[name]) {
|
|
170
|
+
errors[name] = control.validity.valueMissing
|
|
125
171
|
? messages.required
|
|
126
172
|
: messages.invalid;
|
|
127
173
|
}
|
|
@@ -151,13 +197,13 @@ export const focusFirstAuthFieldError = (
|
|
|
151
197
|
if (!form || Object.keys(errors).length === 0) return;
|
|
152
198
|
|
|
153
199
|
const controls = Array.from(
|
|
154
|
-
form.querySelectorAll<FormControl>(
|
|
155
|
-
'input[name], select[name], textarea[name]'
|
|
156
|
-
)
|
|
200
|
+
form.querySelectorAll<FormControl>(VALIDATED_CONTROLS)
|
|
157
201
|
);
|
|
158
202
|
|
|
159
203
|
const first = controls.find(
|
|
160
|
-
(control) =>
|
|
204
|
+
(control) =>
|
|
205
|
+
Boolean(errors[fieldNameOf(control)]) &&
|
|
206
|
+
isVisibleControl(fieldBoxOf(control))
|
|
161
207
|
);
|
|
162
208
|
|
|
163
209
|
first?.focus();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a composed form already knows about each of its fields, keyed by the
|
|
7
|
+
* name the field submits under.
|
|
8
|
+
*
|
|
9
|
+
* A sign-up starts empty, so the field blocks were written as empty controls
|
|
10
|
+
* the form reads back through `FormData`. A form that EDITS a record — the
|
|
11
|
+
* profile form — has to start from the record instead: the shopper's name in
|
|
12
|
+
* the name box, their birth date in the date selects, their current answer in
|
|
13
|
+
* each consent tick. The fields are child blocks at any depth, so the form has
|
|
14
|
+
* no reference to hand a value to; each field reads its own entry here, the
|
|
15
|
+
* same way it reads its error message from `auth-field-errors.tsx`.
|
|
16
|
+
*
|
|
17
|
+
* `readOnly` names the fields the form shows but cannot change — the profile's
|
|
18
|
+
* e-mail, which commerce only moves through its own verified flow. Such a
|
|
19
|
+
* field is drawn disabled, so it is neither submitted nor validated.
|
|
20
|
+
*
|
|
21
|
+
* Outside such a form nothing is provided and every field starts empty, which
|
|
22
|
+
* is exactly what a register or auth form has always drawn.
|
|
23
|
+
*/
|
|
24
|
+
export interface FormFieldDefaultsValue {
|
|
25
|
+
values: Readonly<Record<string, unknown>>;
|
|
26
|
+
readOnly: ReadonlyArray<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NO_DEFAULTS: FormFieldDefaultsValue = { values: {}, readOnly: [] };
|
|
30
|
+
|
|
31
|
+
const FormFieldDefaultsContext =
|
|
32
|
+
React.createContext<FormFieldDefaultsValue>(NO_DEFAULTS);
|
|
33
|
+
|
|
34
|
+
export const FormFieldDefaultsProvider = FormFieldDefaultsContext.Provider;
|
|
35
|
+
|
|
36
|
+
/** The value a text-like control starts with: '' when the form has none. */
|
|
37
|
+
export const useFormFieldDefault = (name: string): string => {
|
|
38
|
+
const { values } = React.useContext(FormFieldDefaultsContext);
|
|
39
|
+
const value = name ? values[name] : undefined;
|
|
40
|
+
|
|
41
|
+
if (value === null || value === undefined) return '';
|
|
42
|
+
|
|
43
|
+
return String(value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a tick starts ticked. Only an explicit `true` counts: commerce keeps
|
|
48
|
+
* `null` for a consent the shopper was never asked about, and drawing that as
|
|
49
|
+
* ticked would put an agreement in their mouth.
|
|
50
|
+
*/
|
|
51
|
+
export const useFormFieldChecked = (
|
|
52
|
+
name: string,
|
|
53
|
+
tickedValue = 'true'
|
|
54
|
+
): boolean => {
|
|
55
|
+
const { values } = React.useContext(FormFieldDefaultsContext);
|
|
56
|
+
const stored = name ? values[name] : undefined;
|
|
57
|
+
|
|
58
|
+
if (stored === true) return true;
|
|
59
|
+
// A custom tick that sends its own value (`yes`, `1`) is ticked when that is
|
|
60
|
+
// what is stored.
|
|
61
|
+
return (
|
|
62
|
+
tickedValue !== 'true' &&
|
|
63
|
+
stored !== undefined &&
|
|
64
|
+
stored !== null &&
|
|
65
|
+
String(stored) === tickedValue
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Whether the hosting form shows this field without letting it change. */
|
|
70
|
+
export const useFormFieldReadOnly = (name: string): boolean => {
|
|
71
|
+
const { readOnly } = React.useContext(FormFieldDefaultsContext);
|
|
72
|
+
|
|
73
|
+
return Boolean(name) && readOnly.includes(name);
|
|
74
|
+
};
|
|
@@ -2,31 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import TextBlock from '../text-block';
|
|
4
4
|
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
const mockLocalization = { locale: 'en-us', defaultLocaleValue: 'en-us' };
|
|
9
|
-
|
|
10
|
-
jest.mock('@akinon/next/hooks', () => ({
|
|
11
|
-
__esModule: true,
|
|
12
|
-
useLocalization: () => ({
|
|
13
|
-
t: (path: string) => path,
|
|
14
|
-
setLocale: () => {},
|
|
15
|
-
setCurrency: () => {},
|
|
16
|
-
locale: mockLocalization.locale,
|
|
17
|
-
currency: 'USD',
|
|
18
|
-
locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
|
|
19
|
-
currencies: ['USD'],
|
|
20
|
-
defaultLocaleValue: mockLocalization.defaultLocaleValue,
|
|
21
|
-
defaultCurrencyCode: 'USD',
|
|
22
|
-
localeUrlStrategy: 'hide-default-locale'
|
|
23
|
-
})
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
mockLocalization.locale = 'en-us';
|
|
28
|
-
mockLocalization.defaultLocaleValue = 'en-us';
|
|
29
|
-
});
|
|
5
|
+
// The mocked useLocalization (jest/__mocks__/akinon-next-hooks.js) returns
|
|
6
|
+
// locale: 'en-us'. process.env.NEXT_PUBLIC_DEFAULT_LOCALE is unset in the
|
|
7
|
+
// jest env, so the component's defaultLocale falls back to 'en'.
|
|
30
8
|
|
|
31
9
|
const renderBlock = (value: unknown, styles?: Record<string, unknown>) => {
|
|
32
10
|
const block: any = { id: 'b1', type: 'text', value, styles };
|
|
@@ -56,16 +34,10 @@ describe('TextBlock value-shape resolution', () => {
|
|
|
56
34
|
);
|
|
57
35
|
});
|
|
58
36
|
|
|
59
|
-
it(
|
|
60
|
-
|
|
61
|
-
// the block used to show: its default was read from an env variable
|
|
62
|
-
// that falls back to English, not from the storefront (ZERO-5296).
|
|
63
|
-
mockLocalization.locale = 'de-de';
|
|
64
|
-
mockLocalization.defaultLocaleValue = 'tr-tr';
|
|
65
|
-
|
|
66
|
-
const { container } = renderBlock({ en: 'EN', 'tr-tr': 'TR' });
|
|
37
|
+
it('falls back to defaultLocale ("en") when active locale is absent', () => {
|
|
38
|
+
const { container } = renderBlock({ en: 'EN', fr: 'FR' });
|
|
67
39
|
expect((container.querySelector('div') as HTMLDivElement).innerHTML).toBe(
|
|
68
|
-
'
|
|
40
|
+
'EN'
|
|
69
41
|
);
|
|
70
42
|
});
|
|
71
43
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
5
5
|
import { useAuthFieldError } from '../auth/auth-field-errors';
|
|
6
|
+
import { useFormFieldChecked } from '../auth/form-field-defaults';
|
|
6
7
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
7
8
|
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
8
9
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
@@ -27,12 +28,37 @@ import { BlockRendererProps } from './block-renderer-registry';
|
|
|
27
28
|
* reads out of `FormData` — and an unticked checkbox simply is not in there,
|
|
28
29
|
* which is exactly the "did they agree" question the form asks.
|
|
29
30
|
*
|
|
31
|
+
* A contact-channel tick (`email`, `sms`, `call`, `whatsapp`) is one channel's
|
|
32
|
+
* answer under the flag commerce stores for it (`email_allowed` …). A register
|
|
33
|
+
* form reads it in place of its blanket marketing answer for that channel; a
|
|
34
|
+
* profile form starts it from the shopper's stored answer and saves it back.
|
|
35
|
+
*
|
|
30
36
|
* Properties: consentType, text, required, and for a `custom` tick the
|
|
31
37
|
* name it submits and the value it sends when ticked.
|
|
32
38
|
* Parts: consent, checkbox, consent-text, error.
|
|
33
39
|
*/
|
|
34
40
|
|
|
35
|
-
export type
|
|
41
|
+
export type AuthContactChannel = 'email' | 'sms' | 'call' | 'whatsapp';
|
|
42
|
+
|
|
43
|
+
export type AuthConsentType =
|
|
44
|
+
| 'legal'
|
|
45
|
+
| 'marketing'
|
|
46
|
+
| 'custom'
|
|
47
|
+
| AuthContactChannel;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The contact channels and the profile flag each one is stored under. The same
|
|
51
|
+
* four flags the register form fans its marketing answer out to.
|
|
52
|
+
*/
|
|
53
|
+
export const AUTH_CONTACT_CHANNEL_FLAGS: Record<AuthContactChannel, string> = {
|
|
54
|
+
email: 'email_allowed',
|
|
55
|
+
sms: 'sms_allowed',
|
|
56
|
+
call: 'call_allowed',
|
|
57
|
+
whatsapp: 'whatsapp_allowed'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const isContactChannel = (value: string): value is AuthContactChannel =>
|
|
61
|
+
value in AUTH_CONTACT_CHANNEL_FLAGS;
|
|
36
62
|
|
|
37
63
|
/**
|
|
38
64
|
* What each tick is called on the wire.
|
|
@@ -74,7 +100,13 @@ const DEFAULT_TEXT: Record<AuthConsentType, string> = {
|
|
|
74
100
|
marketing: 'I would like to receive campaign and marketing messages.',
|
|
75
101
|
// A custom tick is whatever the brand has to ask this month; nothing here
|
|
76
102
|
// could guess its sentence.
|
|
77
|
-
custom: ''
|
|
103
|
+
custom: '',
|
|
104
|
+
// A channel row is usually one of a list under a heading, so its caption
|
|
105
|
+
// is the channel's name rather than a sentence.
|
|
106
|
+
email: 'E-mail',
|
|
107
|
+
sms: 'SMS',
|
|
108
|
+
call: 'Phone call',
|
|
109
|
+
whatsapp: 'WhatsApp'
|
|
78
110
|
};
|
|
79
111
|
|
|
80
112
|
const AuthConsentBlock = ({
|
|
@@ -90,7 +122,10 @@ const AuthConsentBlock = ({
|
|
|
90
122
|
)
|
|
91
123
|
).trim();
|
|
92
124
|
const consentType: AuthConsentType =
|
|
93
|
-
rawType === 'marketing' || rawType === 'custom'
|
|
125
|
+
rawType === 'marketing' || rawType === 'custom' || isContactChannel(rawType)
|
|
126
|
+
? rawType
|
|
127
|
+
: 'legal';
|
|
128
|
+
const isChannel = isContactChannel(consentType);
|
|
94
129
|
|
|
95
130
|
/**
|
|
96
131
|
* The two known ticks own their names because commerce owns their meaning: the
|
|
@@ -109,7 +144,9 @@ const AuthConsentBlock = ({
|
|
|
109
144
|
const name =
|
|
110
145
|
consentType === 'custom'
|
|
111
146
|
? customName
|
|
112
|
-
:
|
|
147
|
+
: isContactChannel(consentType)
|
|
148
|
+
? AUTH_CONTACT_CHANNEL_FLAGS[consentType]
|
|
149
|
+
: AUTH_CONSENT_NAMES[consentType as 'legal' | 'marketing'];
|
|
113
150
|
|
|
114
151
|
/**
|
|
115
152
|
* What a ticked custom consent sends. `true` by default because that is what
|
|
@@ -117,10 +154,11 @@ const AuthConsentBlock = ({
|
|
|
117
154
|
* by typing that instead. (The browser's own default for a valueless checkbox
|
|
118
155
|
* is `on`, which is why the value is set explicitly rather than left off.)
|
|
119
156
|
*/
|
|
120
|
-
const tickedValue =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
const tickedValue = isChannel
|
|
158
|
+
? 'true'
|
|
159
|
+
: String(
|
|
160
|
+
getResponsiveValue(block.properties?.tickedValue, currentBreakpoint, '')
|
|
161
|
+
).trim() || 'true';
|
|
124
162
|
|
|
125
163
|
const text = String(
|
|
126
164
|
getResponsiveValue(
|
|
@@ -135,18 +173,30 @@ const AuthConsentBlock = ({
|
|
|
135
173
|
* because that is what each one IS — but it stays a toggle: a brand whose
|
|
136
174
|
* agreement is not mandatory in its market must be able to say so without
|
|
137
175
|
* swapping the block for another.
|
|
176
|
+
*
|
|
177
|
+
* A contact permission is the shopper's choice by definition: a form that
|
|
178
|
+
* refused to save until it was ticked would be extracting consent, not
|
|
179
|
+
* asking for it. So a channel tick is never required, whatever the toggle
|
|
180
|
+
* (which the editor seeds on for every new row) says.
|
|
138
181
|
*/
|
|
139
|
-
const required =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
const required =
|
|
183
|
+
!isChannel &&
|
|
184
|
+
readToggle(
|
|
185
|
+
block.properties?.required,
|
|
186
|
+
currentBreakpoint,
|
|
187
|
+
consentType === 'legal'
|
|
188
|
+
);
|
|
144
189
|
|
|
145
190
|
// The form puts its own sentence here — "you have to accept the agreement"
|
|
146
191
|
// beats "this field is required" for a legal opt-in — and clears it as soon
|
|
147
192
|
// as the box is ticked.
|
|
148
193
|
const errorMessage = useAuthFieldError(name);
|
|
149
194
|
const insideConsentHost = React.useContext(AuthConsentHostContext);
|
|
195
|
+
/**
|
|
196
|
+
* The shopper's stored answer, when the hosting form edits a record (the
|
|
197
|
+
* Profile Form). Unticked everywhere else, which is what a sign-up asks.
|
|
198
|
+
*/
|
|
199
|
+
const startsTicked = useFormFieldChecked(name, tickedValue);
|
|
150
200
|
|
|
151
201
|
// Same refusal as an unnamed custom field: a tick that submits nothing is not
|
|
152
202
|
// a consent, and the canvas has to say what it is waiting for.
|
|
@@ -158,8 +208,10 @@ const AuthConsentBlock = ({
|
|
|
158
208
|
|
|
159
209
|
// A membership or marketing tick outside the form that reads it would post a
|
|
160
210
|
// string commerce ignores. A CUSTOM tick is the brand's own key, forwarded
|
|
161
|
-
// verbatim by any composed form,
|
|
162
|
-
|
|
211
|
+
// verbatim by any composed form, and a channel tick is a flag both the
|
|
212
|
+
// register and the profile form read by name, so those belong wherever the
|
|
213
|
+
// author puts them.
|
|
214
|
+
if (consentType !== 'custom' && !isChannel && !insideConsentHost) {
|
|
163
215
|
if (!isDesigner) return null;
|
|
164
216
|
|
|
165
217
|
return (
|
|
@@ -177,12 +229,19 @@ const AuthConsentBlock = ({
|
|
|
177
229
|
style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}
|
|
178
230
|
>
|
|
179
231
|
<input
|
|
232
|
+
// Keyed by name so a tick whose type changes on the canvas (e-mail
|
|
233
|
+
// → WhatsApp) starts again from ITS stored answer: `defaultChecked`
|
|
234
|
+
// is only read when the box mounts.
|
|
235
|
+
key={name}
|
|
180
236
|
type="checkbox"
|
|
181
237
|
name={name}
|
|
182
|
-
// Only the custom
|
|
183
|
-
// by presence and re-expressed as flags, so a value on them
|
|
184
|
-
// a string the form throws away.
|
|
185
|
-
value={
|
|
238
|
+
// Only the custom and channel ticks carry a value: the two known ones
|
|
239
|
+
// are read by presence and re-expressed as flags, so a value on them
|
|
240
|
+
// would be a string the form throws away.
|
|
241
|
+
value={
|
|
242
|
+
consentType === 'custom' || isChannel ? tickedValue : undefined
|
|
243
|
+
}
|
|
244
|
+
defaultChecked={startsTicked || undefined}
|
|
186
245
|
required={required || undefined}
|
|
187
246
|
aria-invalid={errorMessage ? true : undefined}
|
|
188
247
|
{...partAttrs('checkbox')}
|