@madgex/design-system 14.1.0 → 14.3.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/dist/assets/icons.json +1 -1
- package/dist/css/index.css +1 -1
- package/dist/js/components/mds-consent-gate-standalone.js +5 -0
- package/dist/js/components/mds-image-cropper-standalone.js +1 -1
- package/dist/js/components/mds-timeout-dialog-standalone.js +1 -1
- package/dist/js/consent-gate-DVi5q-ir.js +1 -0
- package/dist/js/index.js +1 -1
- package/package.json +1 -1
- package/src/components/_macro-index.njk +1 -0
- package/src/components/conditional-section/README.md +2 -2
- package/src/components/conditional-section/conditional-section.config.js +54 -0
- package/src/components/conditional-section/conditional-section.js +91 -26
- package/src/components/conditional-section/conditional-section.spec.js +402 -0
- package/src/components/consent-gate/README.md +168 -0
- package/src/components/consent-gate/_macro.njk +30 -0
- package/src/components/consent-gate/_template.njk +14 -0
- package/src/components/consent-gate/consent-gate-standalone.scss +2 -0
- package/src/components/consent-gate/consent-gate.config.js +6 -0
- package/src/components/consent-gate/consent-gate.js +289 -0
- package/src/components/consent-gate/consent-gate.njk +203 -0
- package/src/components/consent-gate/consent-gate.scss +138 -0
- package/src/components/consent-gate/consent-gate.spec.js +582 -0
- package/src/components/consent-gate/consent-store.js +109 -0
- package/src/components/consent-gate/consent-store.spec.js +164 -0
- package/src/components/consent-gate/mds-consent-gate-standalone.js +40 -0
- package/src/helpers/fluid-video/README.md +8 -0
- package/src/helpers/prose/prose.js +6 -1
- package/src/js/index.js +5 -0
- package/src/scss/components/__index.scss +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// eslint-disable-next-line n/no-unpublished-import
|
|
2
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
3
|
+
import { MdsConditionalSection, shouldShow } from './conditional-section.js';
|
|
4
|
+
|
|
5
|
+
if (!customElements.get('mds-conditional-section')) {
|
|
6
|
+
customElements.define('mds-conditional-section', MdsConditionalSection);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function emitInput(target) {
|
|
10
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mountScenario({ inputsHtml, fieldName, showWhen, includeShowWhen = true }) {
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
const showWhenAttr = includeShowWhen ? `show-when="${String(showWhen).replace(/"/g, '"')}"` : '';
|
|
16
|
+
container.innerHTML = `
|
|
17
|
+
<form id="test-conditional-form">
|
|
18
|
+
${inputsHtml}
|
|
19
|
+
<mds-conditional-section field-name="${fieldName}" ${showWhenAttr}>
|
|
20
|
+
<p data-testid="conditional-content">Conditional content</p>
|
|
21
|
+
</mds-conditional-section>
|
|
22
|
+
</form>
|
|
23
|
+
`;
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
const form = container.querySelector('form');
|
|
26
|
+
const section = container.querySelector('mds-conditional-section');
|
|
27
|
+
return { container, form, section };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('shouldShow', () => {
|
|
31
|
+
it('is true when any checked value matches a comma-separated trigger (OR)', () => {
|
|
32
|
+
const form = document.createElement('form');
|
|
33
|
+
form.innerHTML = `
|
|
34
|
+
<input type="checkbox" name="i" value="sports" checked />
|
|
35
|
+
<input type="checkbox" name="i" value="music" />
|
|
36
|
+
`;
|
|
37
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
|
|
38
|
+
|
|
39
|
+
form.querySelector('[value="sports"]').checked = false;
|
|
40
|
+
form.querySelector('[value="music"]').checked = true;
|
|
41
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is false when no checked value matches triggers', () => {
|
|
45
|
+
const form = document.createElement('form');
|
|
46
|
+
form.innerHTML = `
|
|
47
|
+
<input type="checkbox" name="i" value="sports" />
|
|
48
|
+
<input type="checkbox" name="i" value="music" />
|
|
49
|
+
<input type="checkbox" name="i" value="travel" checked />
|
|
50
|
+
`;
|
|
51
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('is false when a checkbox group has nothing checked', () => {
|
|
55
|
+
const form = document.createElement('form');
|
|
56
|
+
form.innerHTML = `
|
|
57
|
+
<input type="checkbox" name="i" value="sports" />
|
|
58
|
+
<input type="checkbox" name="i" value="music" />
|
|
59
|
+
`;
|
|
60
|
+
expect(shouldShow(form.elements.i, 'sports')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('trims trigger segments and ignores empty parts', () => {
|
|
64
|
+
const form = document.createElement('form');
|
|
65
|
+
form.innerHTML = `
|
|
66
|
+
<input type="checkbox" name="i" value="a" checked />
|
|
67
|
+
<input type="checkbox" name="i" value="b" />
|
|
68
|
+
`;
|
|
69
|
+
expect(shouldShow(form.elements.i, ' a , b ')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('is false when show-when is null or empty', () => {
|
|
73
|
+
const form = document.createElement('form');
|
|
74
|
+
form.innerHTML = `<input type="checkbox" name="i" value="x" checked />`;
|
|
75
|
+
expect(shouldShow(form.elements.i, null)).toBe(false);
|
|
76
|
+
expect(shouldShow(form.elements.i, '')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('for a lone checkbox uses true/false against triggers', () => {
|
|
80
|
+
const form = document.createElement('form');
|
|
81
|
+
form.innerHTML = '<input type="checkbox" name="c" />';
|
|
82
|
+
const field = form.elements.c;
|
|
83
|
+
expect(shouldShow(field, 'false')).toBe(true);
|
|
84
|
+
expect(shouldShow(field, 'true')).toBe(false);
|
|
85
|
+
field.checked = true;
|
|
86
|
+
expect(shouldShow(field, 'true')).toBe(true);
|
|
87
|
+
expect(shouldShow(field, 'false')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('for a lone checkbox with a custom value matches show-when against that value when checked', () => {
|
|
91
|
+
const form = document.createElement('form');
|
|
92
|
+
form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
|
|
93
|
+
const field = form.elements.c;
|
|
94
|
+
expect(shouldShow(field, 'yes')).toBe(false);
|
|
95
|
+
field.checked = true;
|
|
96
|
+
expect(shouldShow(field, 'yes')).toBe(true);
|
|
97
|
+
expect(shouldShow(field, 'no')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('for a lone checkbox with a custom value still supports show-when true/false', () => {
|
|
101
|
+
const form = document.createElement('form');
|
|
102
|
+
form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
|
|
103
|
+
const field = form.elements.c;
|
|
104
|
+
field.checked = true;
|
|
105
|
+
expect(shouldShow(field, 'true')).toBe(true);
|
|
106
|
+
expect(shouldShow(field, 'false')).toBe(false);
|
|
107
|
+
field.checked = false;
|
|
108
|
+
expect(shouldShow(field, 'false')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('for a checkbox group matches checked values', () => {
|
|
112
|
+
const form = document.createElement('form');
|
|
113
|
+
form.innerHTML = `
|
|
114
|
+
<input type="checkbox" name="i" value="sports" />
|
|
115
|
+
<input type="checkbox" name="i" value="music" checked />
|
|
116
|
+
<input type="checkbox" name="i" value="travel" />
|
|
117
|
+
`;
|
|
118
|
+
expect(shouldShow(form.elements.i, 'music')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('uses "on" when a checked checkbox has no value attribute', () => {
|
|
122
|
+
const form = document.createElement('form');
|
|
123
|
+
form.innerHTML = `
|
|
124
|
+
<input type="checkbox" name="i" checked />
|
|
125
|
+
<input type="checkbox" name="i" />
|
|
126
|
+
`;
|
|
127
|
+
expect(shouldShow(form.elements.i, 'on')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('for a radio group matches the selected value', () => {
|
|
131
|
+
const form = document.createElement('form');
|
|
132
|
+
form.innerHTML = `
|
|
133
|
+
<input type="radio" name="r" value="a" />
|
|
134
|
+
<input type="radio" name="r" value="b" checked />
|
|
135
|
+
`;
|
|
136
|
+
expect(shouldShow(form.elements.r, 'b')).toBe(true);
|
|
137
|
+
expect(shouldShow(form.elements.r, 'a')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('reads multiple same-name controls from a RadioNodeList', () => {
|
|
141
|
+
const form = document.createElement('form');
|
|
142
|
+
form.innerHTML = `
|
|
143
|
+
<input type="checkbox" name="i" value="a" />
|
|
144
|
+
<input type="checkbox" name="i" value="b" />
|
|
145
|
+
`;
|
|
146
|
+
const list = form.elements.i;
|
|
147
|
+
expect(list).toBeInstanceOf(RadioNodeList);
|
|
148
|
+
expect(list.length).toBe(2);
|
|
149
|
+
list[1].checked = true;
|
|
150
|
+
expect(shouldShow(list, 'b')).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('for a multiple select matches any selected option against comma-separated triggers', () => {
|
|
154
|
+
const form = document.createElement('form');
|
|
155
|
+
form.innerHTML = `
|
|
156
|
+
<select name="skills" id="skills" multiple>
|
|
157
|
+
<option value="js">JavaScript</option>
|
|
158
|
+
<option value="css">CSS</option>
|
|
159
|
+
<option value="html">HTML</option>
|
|
160
|
+
</select>
|
|
161
|
+
`;
|
|
162
|
+
const select = form.elements.skills;
|
|
163
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
164
|
+
|
|
165
|
+
select.options[1].selected = true;
|
|
166
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
167
|
+
|
|
168
|
+
select.options[1].selected = false;
|
|
169
|
+
select.options[2].selected = true;
|
|
170
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('for a multiple select matches when a non-first selected option matches show-when alone', () => {
|
|
174
|
+
const form = document.createElement('form');
|
|
175
|
+
form.innerHTML = `
|
|
176
|
+
<select name="skills" id="skills" multiple>
|
|
177
|
+
<option value="js">JavaScript</option>
|
|
178
|
+
<option value="css">CSS</option>
|
|
179
|
+
</select>
|
|
180
|
+
`;
|
|
181
|
+
const select = form.elements.skills;
|
|
182
|
+
select.options[0].selected = true;
|
|
183
|
+
select.options[1].selected = true;
|
|
184
|
+
expect(shouldShow(select, 'css')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('for a multiple select stays visible while any matching option remains selected', () => {
|
|
188
|
+
const form = document.createElement('form');
|
|
189
|
+
form.innerHTML = `
|
|
190
|
+
<select name="skills" id="skills" multiple>
|
|
191
|
+
<option value="js">JavaScript</option>
|
|
192
|
+
<option value="css">CSS</option>
|
|
193
|
+
</select>
|
|
194
|
+
`;
|
|
195
|
+
const select = form.elements.skills;
|
|
196
|
+
select.options[0].selected = true;
|
|
197
|
+
select.options[1].selected = true;
|
|
198
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
199
|
+
|
|
200
|
+
select.options[0].selected = false;
|
|
201
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
202
|
+
|
|
203
|
+
select.options[1].selected = false;
|
|
204
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('MdsConditionalSection', () => {
|
|
209
|
+
let container;
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
container?.remove();
|
|
213
|
+
container = undefined;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('checkbox group', () => {
|
|
217
|
+
it('shows when a non-first checkbox matching show-when is checked', () => {
|
|
218
|
+
({ container } = mountScenario({
|
|
219
|
+
fieldName: 'interests',
|
|
220
|
+
showWhen: 'music',
|
|
221
|
+
inputsHtml: `
|
|
222
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
223
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
224
|
+
<input type="checkbox" name="interests" value="travel" id="cb-travel" />
|
|
225
|
+
`,
|
|
226
|
+
}));
|
|
227
|
+
const section = container.querySelector('mds-conditional-section');
|
|
228
|
+
expect(section.hidden).toBe(true);
|
|
229
|
+
|
|
230
|
+
const music = container.querySelector('#cb-music');
|
|
231
|
+
music.checked = true;
|
|
232
|
+
emitInput(music);
|
|
233
|
+
|
|
234
|
+
expect(section.hidden).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('matches any comma-separated trigger (OR)', () => {
|
|
238
|
+
({ container } = mountScenario({
|
|
239
|
+
fieldName: 'interests',
|
|
240
|
+
showWhen: 'sports,music',
|
|
241
|
+
inputsHtml: `
|
|
242
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
243
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
244
|
+
<input type="checkbox" name="interests" value="travel" id="cb-travel" />
|
|
245
|
+
`,
|
|
246
|
+
}));
|
|
247
|
+
const section = container.querySelector('mds-conditional-section');
|
|
248
|
+
const travel = container.querySelector('#cb-travel');
|
|
249
|
+
travel.checked = true;
|
|
250
|
+
emitInput(travel);
|
|
251
|
+
expect(section.hidden).toBe(true);
|
|
252
|
+
|
|
253
|
+
const music = container.querySelector('#cb-music');
|
|
254
|
+
music.checked = true;
|
|
255
|
+
emitInput(music);
|
|
256
|
+
expect(section.hidden).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('stays visible if one matching trigger is unchecked but another remains checked', () => {
|
|
260
|
+
({ container } = mountScenario({
|
|
261
|
+
fieldName: 'interests',
|
|
262
|
+
showWhen: 'sports,music',
|
|
263
|
+
inputsHtml: `
|
|
264
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
265
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
266
|
+
`,
|
|
267
|
+
}));
|
|
268
|
+
const section = container.querySelector('mds-conditional-section');
|
|
269
|
+
const sports = container.querySelector('#cb-sports');
|
|
270
|
+
const music = container.querySelector('#cb-music');
|
|
271
|
+
sports.checked = true;
|
|
272
|
+
music.checked = true;
|
|
273
|
+
emitInput(sports);
|
|
274
|
+
|
|
275
|
+
expect(section.hidden).toBe(false);
|
|
276
|
+
|
|
277
|
+
sports.checked = false;
|
|
278
|
+
emitInput(sports);
|
|
279
|
+
expect(section.hidden).toBe(false);
|
|
280
|
+
|
|
281
|
+
music.checked = false;
|
|
282
|
+
emitInput(music);
|
|
283
|
+
expect(section.hidden).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('regressions', () => {
|
|
288
|
+
it('single checkbox with show-when true toggles visibility', () => {
|
|
289
|
+
({ container } = mountScenario({
|
|
290
|
+
fieldName: 'agree',
|
|
291
|
+
showWhen: 'true',
|
|
292
|
+
inputsHtml: '<input type="checkbox" name="agree" id="agree" />',
|
|
293
|
+
}));
|
|
294
|
+
const section = container.querySelector('mds-conditional-section');
|
|
295
|
+
const cb = container.querySelector('#agree');
|
|
296
|
+
expect(section.hidden).toBe(true);
|
|
297
|
+
cb.checked = true;
|
|
298
|
+
emitInput(cb);
|
|
299
|
+
expect(section.hidden).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('hide-salary pattern: show-when false when checkbox unchecked', () => {
|
|
303
|
+
({ container } = mountScenario({
|
|
304
|
+
fieldName: 'HideSalary',
|
|
305
|
+
showWhen: 'false',
|
|
306
|
+
inputsHtml: '<input type="checkbox" name="HideSalary" id="HideSalary" />',
|
|
307
|
+
}));
|
|
308
|
+
const section = container.querySelector('mds-conditional-section');
|
|
309
|
+
expect(section.hidden).toBe(false);
|
|
310
|
+
const cb = container.querySelector('#HideSalary');
|
|
311
|
+
cb.checked = true;
|
|
312
|
+
emitInput(cb);
|
|
313
|
+
expect(section.hidden).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('single checkbox with custom value and show-when toggles visibility', () => {
|
|
317
|
+
({ container } = mountScenario({
|
|
318
|
+
fieldName: 'newsletter',
|
|
319
|
+
showWhen: 'weekly',
|
|
320
|
+
inputsHtml: '<input type="checkbox" name="newsletter" id="newsletter" value="weekly" />',
|
|
321
|
+
}));
|
|
322
|
+
const section = container.querySelector('mds-conditional-section');
|
|
323
|
+
const cb = container.querySelector('#newsletter');
|
|
324
|
+
expect(section.hidden).toBe(true);
|
|
325
|
+
cb.checked = true;
|
|
326
|
+
emitInput(cb);
|
|
327
|
+
expect(section.hidden).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('radio group show-when matches selected value', () => {
|
|
331
|
+
({ container } = mountScenario({
|
|
332
|
+
fieldName: 'option',
|
|
333
|
+
showWhen: 'Broccoli',
|
|
334
|
+
inputsHtml: `
|
|
335
|
+
<input type="radio" name="option" value="Donkey" id="r1" />
|
|
336
|
+
<input type="radio" name="option" value="Broccoli" id="r2" />
|
|
337
|
+
`,
|
|
338
|
+
}));
|
|
339
|
+
const section = container.querySelector('mds-conditional-section');
|
|
340
|
+
expect(section.hidden).toBe(true);
|
|
341
|
+
container.querySelector('#r2').checked = true;
|
|
342
|
+
emitInput(container.querySelector('#r2'));
|
|
343
|
+
expect(section.hidden).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('select show-when matches option value', () => {
|
|
347
|
+
({ container } = mountScenario({
|
|
348
|
+
fieldName: 'pets',
|
|
349
|
+
showWhen: 'cat',
|
|
350
|
+
inputsHtml: `
|
|
351
|
+
<select name="pets" id="pets">
|
|
352
|
+
<option value="">Choose</option>
|
|
353
|
+
<option value="dog">Dog</option>
|
|
354
|
+
<option value="cat">Cat</option>
|
|
355
|
+
</select>
|
|
356
|
+
`,
|
|
357
|
+
}));
|
|
358
|
+
const section = container.querySelector('mds-conditional-section');
|
|
359
|
+
const sel = container.querySelector('#pets');
|
|
360
|
+
sel.value = 'cat';
|
|
361
|
+
emitInput(sel);
|
|
362
|
+
expect(section.hidden).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('multiple select show-when matches any selected option (OR)', () => {
|
|
366
|
+
({ container } = mountScenario({
|
|
367
|
+
fieldName: 'skills',
|
|
368
|
+
showWhen: 'js,css',
|
|
369
|
+
inputsHtml: `
|
|
370
|
+
<label for="skills">Pick your skills (hold Ctrl/Cmd to select several):</label>
|
|
371
|
+
<select name="skills" id="skills" multiple size="3">
|
|
372
|
+
<option value="js">JavaScript</option>
|
|
373
|
+
<option value="css">CSS</option>
|
|
374
|
+
<option value="html">HTML</option>
|
|
375
|
+
</select>
|
|
376
|
+
`,
|
|
377
|
+
}));
|
|
378
|
+
const section = container.querySelector('mds-conditional-section');
|
|
379
|
+
const sel = container.querySelector('#skills');
|
|
380
|
+
expect(section.hidden).toBe(true);
|
|
381
|
+
|
|
382
|
+
sel.options[1].selected = true;
|
|
383
|
+
emitInput(sel);
|
|
384
|
+
expect(section.hidden).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('missing show-when', () => {
|
|
389
|
+
it('keeps section hidden when show-when attribute is omitted', () => {
|
|
390
|
+
({ container } = mountScenario({
|
|
391
|
+
fieldName: 'interests',
|
|
392
|
+
showWhen: '',
|
|
393
|
+
includeShowWhen: false,
|
|
394
|
+
inputsHtml: `
|
|
395
|
+
<input type="checkbox" name="interests" value="sports" checked />
|
|
396
|
+
`,
|
|
397
|
+
}));
|
|
398
|
+
const section = container.querySelector('mds-conditional-section');
|
|
399
|
+
expect(section.hidden).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# mds-consent-gate
|
|
2
|
+
|
|
3
|
+
Generic consent gate for third-party embeds and widgets.
|
|
4
|
+
|
|
5
|
+
The component keeps third-party markup inside a child `<template>` so it does not load or run until the user grants consent for a category. On grant it clones the template into the gate's content area; on revoke it removes the clone.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Many third-party embeds make outbound network requests on page load, before user intent and consent are established. This component defers those requests until explicit opt-in and provides a built-in revoke flow.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Always wrap third-party content in a direct child `<template>`. Provide a fallback link in `slot="fallback"` for the no-JS / unloaded state.
|
|
14
|
+
|
|
15
|
+
### Bundled (light DOM) example
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<mds-consent-gate category="youtube" aspect-ratio="16 / 9">
|
|
19
|
+
<span slot="heading">Video</span>
|
|
20
|
+
<template>
|
|
21
|
+
<iframe
|
|
22
|
+
src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
|
|
23
|
+
title="Big Buck Bunny"
|
|
24
|
+
loading="lazy"
|
|
25
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
26
|
+
allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
|
|
27
|
+
allowfullscreen
|
|
28
|
+
></iframe>
|
|
29
|
+
</template>
|
|
30
|
+
<a slot="fallback" href="https://www.youtube.com/watch?v=aqz-KE-bpKQ">Watch on YouTube</a>
|
|
31
|
+
</mds-consent-gate>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Standalone (shadow DOM, bundled CSS) example
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<mds-consent-gate-standalone category="youtube" aspect-ratio="16 / 9">
|
|
38
|
+
<span slot="heading">Video</span>
|
|
39
|
+
<template>
|
|
40
|
+
<iframe src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ" title="Big Buck Bunny" loading="lazy"></iframe>
|
|
41
|
+
</template>
|
|
42
|
+
<a slot="fallback" href="https://www.youtube.com/watch?v=aqz-KE-bpKQ">Watch on YouTube</a>
|
|
43
|
+
</mds-consent-gate-standalone>
|
|
44
|
+
|
|
45
|
+
<script type="module" src="/path/to/mds-consent-gate-standalone.js"></script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Script placement
|
|
49
|
+
|
|
50
|
+
Load the module from `<head>`. Module scripts defer by default so this does not block parsing.
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<head>
|
|
54
|
+
<script type="module" src="/path/to/mds-consent-gate-standalone.js"></script>
|
|
55
|
+
</head>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pre-mount and no-JS handling is driven by component CSS keyed off the absence of `data-state`. The fallback link remains visible until the element upgrades.
|
|
59
|
+
|
|
60
|
+
## Consent behaviour
|
|
61
|
+
|
|
62
|
+
Default consent is stored in `localStorage` under the key `mds-consent`.
|
|
63
|
+
|
|
64
|
+
Consent is per-category. Granting one category reveals every consent-gate instance with the same `category` value, including both `<mds-consent-gate>` and `<mds-consent-gate-standalone>`. Revoking hides them all.
|
|
65
|
+
|
|
66
|
+
## CMP integration
|
|
67
|
+
|
|
68
|
+
Use `setConsentAdapter` to delegate to OneTrust or another CMP.
|
|
69
|
+
|
|
70
|
+
> **Note:** The adapter and all consent-gate elements must share the same package instance. A standalone script loaded from a separate bundle will have its own isolated store and will not reflect consent changes made via this adapter.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { setConsentAdapter } from '@madgex/design-system';
|
|
74
|
+
|
|
75
|
+
setConsentAdapter({
|
|
76
|
+
has: (category) => false,
|
|
77
|
+
grant: (category) => {},
|
|
78
|
+
revoke: (category) => {},
|
|
79
|
+
subscribe: (fn) => {
|
|
80
|
+
// call fn(category, granted) whenever consent changes
|
|
81
|
+
return () => {};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Adapter contract:
|
|
87
|
+
|
|
88
|
+
- `has(category): boolean` — must be synchronous
|
|
89
|
+
- `grant(category): void`
|
|
90
|
+
- `revoke(category): void`
|
|
91
|
+
- `subscribe(fn): () => void`
|
|
92
|
+
|
|
93
|
+
Pass `null` to restore the default localStorage adapter.
|
|
94
|
+
|
|
95
|
+
## Attributes
|
|
96
|
+
|
|
97
|
+
| Attribute | Purpose |
|
|
98
|
+
| --------------- | ----------------------------------------------------------------------------------------- |
|
|
99
|
+
| `category` | Consent category key. Required for gating behaviour. |
|
|
100
|
+
| `i18n` | JSON object with optional string keys `acceptLabel` and `consentMessage`. Invalid values, empty strings, or missing keys fall back to English defaults. Omit the attribute to use defaults only. |
|
|
101
|
+
| `aspect-ratio` | Sets `--mds-consent-gate-aspect-ratio` on host. No default ratio is applied by component. |
|
|
102
|
+
| `preview-image` | Sets `--mds-consent-gate-preview-image` on host (`url(...)`). |
|
|
103
|
+
|
|
104
|
+
## Slots
|
|
105
|
+
|
|
106
|
+
| Slot | Purpose |
|
|
107
|
+
| -------------------------- | ------------------------------------------------------------------------ |
|
|
108
|
+
| default `<template>` child | Required source markup, kept inert until consent. |
|
|
109
|
+
| `heading` | Optional heading content shown over the placeholder. |
|
|
110
|
+
| `fallback` | Optional fallback link/content shown before consent and in error states. |
|
|
111
|
+
|
|
112
|
+
## Parts
|
|
113
|
+
|
|
114
|
+
Style internals via `::part(...)`:
|
|
115
|
+
|
|
116
|
+
| Part | Element |
|
|
117
|
+
| ----------------- | ------------------------------------------------------- |
|
|
118
|
+
| `placeholder` | The placeholder UI container shown awaiting consent. |
|
|
119
|
+
| `consent-message` | The disclosure paragraph. |
|
|
120
|
+
| `accept` | The Accept button. |
|
|
121
|
+
| `content` | The container the cloned template content is placed in. |
|
|
122
|
+
|
|
123
|
+
## State
|
|
124
|
+
|
|
125
|
+
Visibility is driven by a single `data-state` host attribute which CSS keys off:
|
|
126
|
+
|
|
127
|
+
| `data-state` | Meaning |
|
|
128
|
+
| ------------- | --------------------------------------------------------- |
|
|
129
|
+
| _absent_ | Pre-mount / not-yet-upgraded. Only fallback visible. |
|
|
130
|
+
| `error` | Config error (no category or no template). Fallback only. |
|
|
131
|
+
| `placeholder` | Awaiting consent. Heading + placeholder + fallback. |
|
|
132
|
+
| `consented` | Content cloned into `[part="content"]`. |
|
|
133
|
+
|
|
134
|
+
## Events
|
|
135
|
+
|
|
136
|
+
| Event | Bubbles | Composed | Detail |
|
|
137
|
+
| ------------------------- | ------- | -------- | ----------------------------------------- |
|
|
138
|
+
| `mds-consent-gate:grant` | yes | yes | `{ category, container }` |
|
|
139
|
+
| `mds-consent-gate:revoke` | yes | yes | `{ category }` |
|
|
140
|
+
| `mds-consent-gate:error` | yes | yes | `{ reason }` |
|
|
141
|
+
| `mds-consent:change` | window | n/a | `{ category, granted }` (default backend) |
|
|
142
|
+
|
|
143
|
+
`error` reasons:
|
|
144
|
+
|
|
145
|
+
- `no-category`
|
|
146
|
+
- `no-template`
|
|
147
|
+
- `unwrapped-content`
|
|
148
|
+
|
|
149
|
+
## VPPA/GDPR safety guard
|
|
150
|
+
|
|
151
|
+
Direct child network-loading elements outside `<template>` are treated as authoring errors:
|
|
152
|
+
|
|
153
|
+
- `<iframe>`
|
|
154
|
+
- `<embed>`
|
|
155
|
+
- `<object>`
|
|
156
|
+
- `<script src>`
|
|
157
|
+
|
|
158
|
+
In that case the component warns in the console and emits `mds-consent-gate:error` with `reason: 'unwrapped-content'`.
|
|
159
|
+
|
|
160
|
+
## Styling
|
|
161
|
+
|
|
162
|
+
Use `::part(...)` for both the bundled and standalone builds. The component exposes the following CSS custom properties for theming:
|
|
163
|
+
|
|
164
|
+
- `--mds-consent-gate-aspect-ratio`
|
|
165
|
+
- `--mds-consent-gate-preview-image`
|
|
166
|
+
- `--mds-consent-gate-bg`
|
|
167
|
+
|
|
168
|
+
Internal layout uses standard DS variables (`--mds-color-button-*`, `--mds-color-neutral-darkest`, `--mds-color-text-invert`, `--mds-size-border-radius-base`).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{% from "../../sub-components/attributes/macro.njk" import MdsAttributes %}
|
|
2
|
+
{#
|
|
3
|
+
MdsConsentGate macro
|
|
4
|
+
|
|
5
|
+
params:
|
|
6
|
+
category — required, consent category key
|
|
7
|
+
i18n — optional object `{ acceptLabel?, consentMessage? }` (passed as `i18n` host attribute via `| dump`)
|
|
8
|
+
aspectRatio — optional, e.g. "16 / 9"
|
|
9
|
+
previewImage — optional, URL of self-hosted preview image
|
|
10
|
+
heading — optional, plain heading text
|
|
11
|
+
contentHtml — required, markup to defer (placed inside <template>)
|
|
12
|
+
fallbackHref — optional
|
|
13
|
+
fallbackText — optional
|
|
14
|
+
attributes — optional extra host attributes
|
|
15
|
+
#}
|
|
16
|
+
<mds-consent-gate
|
|
17
|
+
{% if params.category %}category="{{params.category}}"{% endif %}
|
|
18
|
+
{% if params.i18n is defined %}i18n="{{ params.i18n | dump }}"{% endif %}
|
|
19
|
+
{% if params.aspectRatio %}aspect-ratio="{{params.aspectRatio}}"{% endif %}
|
|
20
|
+
{% if params.previewImage %}preview-image="{{params.previewImage}}"{% endif %}
|
|
21
|
+
{{- MdsAttributes(params.attributes) -}}
|
|
22
|
+
>
|
|
23
|
+
{% if params.heading %}<span slot="heading">{{params.heading}}</span>{% endif %}
|
|
24
|
+
<template>
|
|
25
|
+
{{ params.contentHtml | default('') | safe }}
|
|
26
|
+
</template>
|
|
27
|
+
{% if params.fallbackHref and params.fallbackText %}
|
|
28
|
+
<a slot="fallback" href="{{params.fallbackHref}}">{{params.fallbackText}}</a>
|
|
29
|
+
{% endif %}
|
|
30
|
+
</mds-consent-gate>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<mds-consent-gate
|
|
2
|
+
{% if category %}category="{{category}}"{% endif %}
|
|
3
|
+
{% if i18n is defined %}i18n="{{ i18n | dump }}"{% endif %}
|
|
4
|
+
{% if aspectRatio %}aspect-ratio="{{aspectRatio}}"{% endif %}
|
|
5
|
+
{% if previewImage %}preview-image="{{previewImage}}"{% endif %}
|
|
6
|
+
>
|
|
7
|
+
{% if heading %}<span slot="heading">{{heading}}</span>{% endif %}
|
|
8
|
+
<template>
|
|
9
|
+
{{ contentHtml | default('') | safe }}
|
|
10
|
+
</template>
|
|
11
|
+
{% if fallbackHref and fallbackText %}
|
|
12
|
+
<a slot="fallback" href="{{fallbackHref}}">{{fallbackText}}</a>
|
|
13
|
+
{% endif %}
|
|
14
|
+
</mds-consent-gate>
|