@madgex/design-system 14.1.0 → 14.2.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/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,164 @@
|
|
|
1
|
+
/* eslint-disable n/no-unsupported-features/node-builtins -- jsdom test env */
|
|
2
|
+
// eslint-disable-next-line n/no-unpublished-import
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
4
|
+
import { has, grant, revoke, subscribe, setConsentAdapter } from './consent-store.js';
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = 'mds-consent';
|
|
7
|
+
|
|
8
|
+
function resetStore() {
|
|
9
|
+
localStorage.clear();
|
|
10
|
+
setConsentAdapter(null);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('consent-store', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
resetStore();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
resetStore();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('default backend', () => {
|
|
23
|
+
it('has() is false for unknown category', () => {
|
|
24
|
+
expect(has('unknown')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('has() is true after grant', () => {
|
|
28
|
+
grant('videos');
|
|
29
|
+
expect(has('videos')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('has() is false after revoke', () => {
|
|
33
|
+
grant('videos');
|
|
34
|
+
revoke('videos');
|
|
35
|
+
expect(has('videos')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('persists to localStorage and reloads into a fresh backend', () => {
|
|
39
|
+
grant('persist-cat');
|
|
40
|
+
expect(JSON.parse(localStorage.getItem(STORAGE_KEY))).toEqual({ 'persist-cat': true });
|
|
41
|
+
setConsentAdapter(null);
|
|
42
|
+
expect(has('persist-cat')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('survives corrupt localStorage JSON when backend is recreated', () => {
|
|
46
|
+
localStorage.setItem(STORAGE_KEY, 'not-json');
|
|
47
|
+
setConsentAdapter(null);
|
|
48
|
+
expect(has('any')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('grant is idempotent (no second notification)', () => {
|
|
52
|
+
const calls = [];
|
|
53
|
+
const off = subscribe((category, granted) => calls.push({ category, granted }));
|
|
54
|
+
grant('dup');
|
|
55
|
+
grant('dup');
|
|
56
|
+
off();
|
|
57
|
+
expect(calls).toEqual([{ category: 'dup', granted: true }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('revoke is idempotent', () => {
|
|
61
|
+
const calls = [];
|
|
62
|
+
const off = subscribe((category, granted) => calls.push({ category, granted }));
|
|
63
|
+
revoke('none');
|
|
64
|
+
off();
|
|
65
|
+
expect(calls).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('categories are independent', () => {
|
|
69
|
+
grant('a');
|
|
70
|
+
expect(has('a')).toBe(true);
|
|
71
|
+
expect(has('b')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('subscribe', () => {
|
|
76
|
+
it('notified with (category, granted) on grant and revoke', () => {
|
|
77
|
+
const calls = [];
|
|
78
|
+
const off = subscribe((c, g) => calls.push([c, g]));
|
|
79
|
+
grant('c1');
|
|
80
|
+
revoke('c1');
|
|
81
|
+
off();
|
|
82
|
+
expect(calls).toEqual([
|
|
83
|
+
['c1', true],
|
|
84
|
+
['c1', false],
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('teardown stops notifications', () => {
|
|
89
|
+
const calls = [];
|
|
90
|
+
const off = subscribe((c, g) => calls.push([c, g]));
|
|
91
|
+
off();
|
|
92
|
+
grant('after');
|
|
93
|
+
expect(calls).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('multiple subscribers receive events', () => {
|
|
97
|
+
const a = [];
|
|
98
|
+
const b = [];
|
|
99
|
+
const offA = subscribe((c, g) => a.push(g));
|
|
100
|
+
const offB = subscribe((c, g) => b.push(g));
|
|
101
|
+
grant('multi');
|
|
102
|
+
offA();
|
|
103
|
+
offB();
|
|
104
|
+
expect(a).toEqual([true]);
|
|
105
|
+
expect(b).toEqual([true]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('mds-consent:change window event', () => {
|
|
110
|
+
it('dispatches with detail { category, granted }', () => {
|
|
111
|
+
const spy = vi.spyOn(window, 'dispatchEvent');
|
|
112
|
+
grant('win-cat');
|
|
113
|
+
const evt = spy.mock.calls.find((args) => args[0]?.type === 'mds-consent:change')?.[0];
|
|
114
|
+
expect(evt).toBeInstanceOf(CustomEvent);
|
|
115
|
+
expect(evt.detail).toEqual({ category: 'win-cat', granted: true });
|
|
116
|
+
spy.mockRestore();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('setConsentAdapter', () => {
|
|
121
|
+
it('forwards module subscribe to custom adapter notifications', () => {
|
|
122
|
+
const subs = new Set();
|
|
123
|
+
/** @type {Record<string, boolean>} */
|
|
124
|
+
const state = {};
|
|
125
|
+
setConsentAdapter({
|
|
126
|
+
has: (cat) => state[cat] === true,
|
|
127
|
+
grant: (cat) => {
|
|
128
|
+
if (state[cat]) return;
|
|
129
|
+
state[cat] = true;
|
|
130
|
+
for (const fn of subs) fn(cat, true);
|
|
131
|
+
},
|
|
132
|
+
revoke: (cat) => {
|
|
133
|
+
if (!state[cat]) return;
|
|
134
|
+
delete state[cat];
|
|
135
|
+
for (const fn of subs) fn(cat, false);
|
|
136
|
+
},
|
|
137
|
+
subscribe: (fn) => {
|
|
138
|
+
subs.add(fn);
|
|
139
|
+
return () => subs.delete(fn);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const calls = [];
|
|
144
|
+
const off = subscribe((c, g) => calls.push([c, g]));
|
|
145
|
+
grant('adapter-cat');
|
|
146
|
+
expect(has('adapter-cat')).toBe(true);
|
|
147
|
+
expect(calls).toEqual([['adapter-cat', true]]);
|
|
148
|
+
off();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('setConsentAdapter(null) restores localStorage backend', () => {
|
|
152
|
+
setConsentAdapter({
|
|
153
|
+
has: () => false,
|
|
154
|
+
grant: () => {},
|
|
155
|
+
revoke: () => {},
|
|
156
|
+
subscribe: () => () => {},
|
|
157
|
+
});
|
|
158
|
+
setConsentAdapter(null);
|
|
159
|
+
grant('back');
|
|
160
|
+
expect(has('back')).toBe(true);
|
|
161
|
+
expect(localStorage.getItem(STORAGE_KEY)).toContain('back');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Web Component build entry — wired into `vite.config.js` as the
|
|
3
|
+
* `js/components/mds-consent-gate-standalone` rollup input. Produces a
|
|
4
|
+
* hashed, side-effectful ESM bundle that any page can load independently.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `MdsConsentGate` in shadow DOM with bundled CSS. The authored heading
|
|
7
|
+
* element is re-parented into `[part="heading-wrap"]` in the shadow root by the
|
|
8
|
+
* base class. A fallback slot is appended to the shadow root so authored fallback
|
|
9
|
+
* content is projected from light DOM into the shadow tree.
|
|
10
|
+
*/
|
|
11
|
+
import { MdsConsentGate } from './consent-gate.js';
|
|
12
|
+
// stripped DS CSS via purgecss, used as a base
|
|
13
|
+
import css from './consent-gate-standalone.scss?purgecss=./consent-gate.js&inline';
|
|
14
|
+
// component-specific CSS, kept in full
|
|
15
|
+
import cssConsentGate from './consent-gate.scss?inline';
|
|
16
|
+
|
|
17
|
+
class MdsConsentGateStandalone extends MdsConsentGate {
|
|
18
|
+
constructor(...args) {
|
|
19
|
+
super(...args);
|
|
20
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
21
|
+
|
|
22
|
+
const sheet = new CSSStyleSheet();
|
|
23
|
+
sheet.replaceSync(`
|
|
24
|
+
* { border: 0; outline: 0; padding: 0; margin: 0; box-sizing: border-box; }
|
|
25
|
+
${css}
|
|
26
|
+
${cssConsentGate}
|
|
27
|
+
`);
|
|
28
|
+
root.adoptedStyleSheets = [sheet];
|
|
29
|
+
|
|
30
|
+
const fallbackSlot = document.createElement('slot');
|
|
31
|
+
fallbackSlot.name = 'fallback';
|
|
32
|
+
root.append(fallbackSlot);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof customElements !== 'undefined' && !customElements.get('mds-consent-gate-standalone')) {
|
|
37
|
+
customElements.define('mds-consent-gate-standalone', MdsConsentGateStandalone);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { grant, revoke, has, subscribe, setConsentAdapter } from './consent-store.js';
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
## Helpers
|
|
2
2
|
|
|
3
|
+
> **Deprecated for new YouTube/Vimeo embeds.** Use the
|
|
4
|
+
> [`mds-consent-gate`](../../components/consent-gate/README.md) custom element
|
|
5
|
+
> instead — it loads zero third-party requests until the user opts in (VPPA),
|
|
6
|
+
> handles responsive sizing internally, and exposes a privacy-enhanced iframe.
|
|
7
|
+
> `mds-fluid-video` remains for non-video iframes (maps, charts, legacy
|
|
8
|
+
> content) and is automatically skipped by the prose helper for any iframe
|
|
9
|
+
> nested inside `mds-consent-gate`.
|
|
10
|
+
|
|
3
11
|
Use the class `mds-fluid-video` wrapping the iframe from the video provider to make it responsive.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const prose = {
|
|
2
2
|
setFluidVideos: () => {
|
|
3
|
-
|
|
3
|
+
// Skip iframes that are descendants of `mds-consent-gate` — that custom
|
|
4
|
+
// element manages its own aspect-ratio/responsive sizing inside its
|
|
5
|
+
// shadow DOM, so wrapping them in `.mds-fluid-video` would double-up.
|
|
6
|
+
const elements = Array.from(
|
|
7
|
+
document.querySelectorAll('.mds-prose iframe, .mds-prose embed, .mds-prose object'),
|
|
8
|
+
).filter((el) => !el.closest('mds-consent-gate'));
|
|
4
9
|
|
|
5
10
|
elements.forEach((element) => {
|
|
6
11
|
const newHtml = document.createElement('div');
|
package/src/js/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { MdsConditionalSection } from '../components/conditional-section/conditi
|
|
|
16
16
|
import { MdsImageCropper } from '../components/image-cropper/image-cropper';
|
|
17
17
|
import { MdsCategoryPicker } from '../components/inputs/category-picker/category-picker';
|
|
18
18
|
import { MdsScrollSpy } from '../components/scroll-spy/scroll-spy';
|
|
19
|
+
import { MdsConsentGate } from '../components/consent-gate/consent-gate.js';
|
|
20
|
+
export { setConsentAdapter } from '../components/consent-gate/consent-gate.js';
|
|
19
21
|
|
|
20
22
|
if (!window.customElements.get('mds-dropdown-nav')) {
|
|
21
23
|
window.customElements.define('mds-dropdown-nav', MdsDropdownNav);
|
|
@@ -41,6 +43,9 @@ if (!window.customElements.get('mds-category-picker')) {
|
|
|
41
43
|
if (!window.customElements.get('mds-scroll-spy')) {
|
|
42
44
|
window.customElements.define('mds-scroll-spy', MdsScrollSpy);
|
|
43
45
|
}
|
|
46
|
+
if (!window.customElements.get('mds-consent-gate')) {
|
|
47
|
+
window.customElements.define('mds-consent-gate', MdsConsentGate);
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
const initAll = () => {
|
|
46
51
|
tabs.init();
|