@cfpb/cfpb-design-system 4.0.2 → 4.0.4

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/dist/components/cfpb-forms/index.css +1 -1
  3. package/dist/components/cfpb-forms/index.css.map +2 -2
  4. package/dist/components/cfpb-forms/index.js.map +1 -1
  5. package/dist/components/cfpb-layout/index.css +1 -1
  6. package/dist/components/cfpb-layout/index.css.map +2 -2
  7. package/dist/components/cfpb-layout/index.js.map +1 -1
  8. package/dist/components/cfpb-typography/index.css +1 -1
  9. package/dist/components/cfpb-typography/index.css.map +2 -2
  10. package/dist/components/cfpb-typography/index.js.map +1 -1
  11. package/dist/elements/cfpb-button/index.js +4 -4
  12. package/dist/elements/cfpb-button/index.js.map +2 -2
  13. package/dist/elements/cfpb-checkbox/index.js +29 -0
  14. package/dist/elements/cfpb-checkbox/index.js.map +7 -0
  15. package/dist/elements/cfpb-file-upload/index.js +4 -4
  16. package/dist/elements/cfpb-file-upload/index.js.map +2 -2
  17. package/dist/elements/cfpb-form-choice/index.js +29 -0
  18. package/dist/elements/cfpb-form-choice/index.js.map +7 -0
  19. package/dist/elements/cfpb-multiselect/index.js +30 -0
  20. package/dist/elements/cfpb-multiselect/index.js.map +7 -0
  21. package/dist/elements/cfpb-tag-filter/index.js +31 -0
  22. package/dist/elements/cfpb-tag-filter/index.js.map +7 -0
  23. package/dist/elements/cfpb-tag-group/index.js +29 -0
  24. package/dist/elements/cfpb-tag-group/index.js.map +7 -0
  25. package/dist/elements/cfpb-tag-topic/index.js +30 -0
  26. package/dist/elements/cfpb-tag-topic/index.js.map +7 -0
  27. package/dist/elements/index.js +42 -0
  28. package/dist/elements/index.js.map +7 -0
  29. package/dist/index.css +1 -1
  30. package/dist/index.css.map +2 -2
  31. package/dist/index.js +6 -4
  32. package/dist/index.js.map +4 -4
  33. package/package.json +1 -1
  34. package/src/components/cfpb-forms/multiselect-model.js +1 -1
  35. package/src/components/cfpb-forms/multiselect.js +1 -1
  36. package/src/components/cfpb-forms/multiselect.scss +2 -1
  37. package/src/components/cfpb-layout/card-group.scss +2 -2
  38. package/src/components/cfpb-typography/mixins.scss +1 -1
  39. package/src/components/cfpb-typography/tagline.scss +1 -1
  40. package/src/elements/cfpb-button/index.js +8 -4
  41. package/src/elements/cfpb-file-upload/index.js +5 -1
  42. package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +261 -0
  43. package/src/elements/cfpb-form-choice/index.js +103 -0
  44. package/src/elements/cfpb-tag-filter/cfpb-tag-filter.component.scss +51 -0
  45. package/src/elements/cfpb-tag-filter/index.js +55 -0
  46. package/src/elements/cfpb-tag-filter/index.spec.js +43 -0
  47. package/src/elements/cfpb-tag-group/cfpb-tag-group.component.scss +43 -0
  48. package/src/elements/cfpb-tag-group/index.js +311 -0
  49. package/src/elements/cfpb-tag-group/index.spec.js +160 -0
  50. package/src/elements/cfpb-tag-topic/cfpb-tag-topic.component.scss +88 -0
  51. package/src/elements/cfpb-tag-topic/index.js +62 -0
  52. package/src/elements/index.js +11 -0
  53. package/src/index.js +1 -2
  54. package/src/utilities/utilities.scss +1 -1
@@ -0,0 +1,311 @@
1
+ import { html, LitElement, css, unsafeCSS } from 'lit';
2
+ import styles from './cfpb-tag-group.component.scss';
3
+
4
+ const SUPPORTED_TAG_LIST = ['CFPB-TAG-FILTER', 'CFPB-TAG-TOPIC'];
5
+
6
+ /**
7
+ * @element cfpb-tag-group.
8
+ * @description A group of tags (filter or topic tags) that can be added and
9
+ * removed.
10
+ *
11
+ * The tag group has a list of tags in the lightDOM that gets re-written
12
+ * inside an unordered list in the shadowDOM so that it is read out
13
+ * as a list of items in VoiceOver.
14
+ * @attribute {string} lang - The element's language.
15
+ * @fires addtag - A tag was added to the group.
16
+ * @fires removetag - A tag was removed from the group.
17
+ */
18
+ export class CfpbTagGroup extends LitElement {
19
+ static styles = css`
20
+ ${unsafeCSS(styles)}
21
+ `;
22
+
23
+ /**
24
+ * @property {boolean} stacked - Whether to stack the tags vertically.
25
+ * @property {Array} tagList - List of the tags in the tag group.
26
+ */
27
+ static get properties() {
28
+ return {
29
+ stacked: { type: Boolean, reflect: true },
30
+ tagList: { type: Array },
31
+ };
32
+ }
33
+
34
+ // Private properties.
35
+ #observer;
36
+ #initialized = false;
37
+ #tagMap;
38
+
39
+ constructor() {
40
+ super();
41
+ this.stacked = false;
42
+ this.tagList = [];
43
+ this.#observer = new MutationObserver(this.#onMutation.bind(this));
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ this.#observeLightDom();
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ this.#observer.disconnect();
53
+ super.disconnectedCallback();
54
+ }
55
+
56
+ firstUpdated() {
57
+ // Wait for the browser to complete its render cycle.
58
+ requestAnimationFrame(() => {
59
+ // Add the tags from the light DOM.
60
+ SUPPORTED_TAG_LIST.forEach((tagName) => {
61
+ const tags = this.querySelectorAll(`${tagName.toLowerCase()}`);
62
+ tags.forEach((tag) => this.addTag(tag));
63
+ });
64
+
65
+ this.#initialized = true;
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Set up a MutationObserver to watch changes in the light DOM.
71
+ */
72
+ #observeLightDom() {
73
+ this.#observer.observe(this, {
74
+ childList: true,
75
+ subtree: false,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Whether a particular node tagName is supported as a tag of this tag group.
81
+ * @param {string} tagName - The name of a supported custom element tag.
82
+ * @returns {boolean} true if the tagName is supported, false otherwise.
83
+ */
84
+ #supportedTag(tagName) {
85
+ return SUPPORTED_TAG_LIST.includes(tagName);
86
+ }
87
+
88
+ /**
89
+ * Handle a change of the light DOM.
90
+ * @param {MutationRecord} mutationList
91
+ */
92
+ #onMutation(mutationList) {
93
+ if (!this.#initialized) return;
94
+ for (const mutation of mutationList) {
95
+ // Ignore mutations that occur within the shadow DOM.
96
+ if (mutation.type === 'childList') {
97
+ mutation.addedNodes.forEach((node) => this.#handleNodeAdded(node));
98
+ mutation.removedNodes.forEach((node) => this.#handleNodeRemoved(node));
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {Node} node - The node that was added to the light DOM.
105
+ */
106
+ #handleNodeAdded(node) {
107
+ if (this.#supportedTag(node.tagName)) {
108
+ const index = Array.from(this.children).indexOf(node);
109
+ this.addTag(node, index);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @param {Node} node - The node that was removed from the light DOM.
115
+ */
116
+ #handleNodeRemoved(node) {
117
+ if (this.#supportedTag(node.tagName)) {
118
+ this.#removeTagNode(node);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Refresh the tagList property from the DOM list.
124
+ */
125
+ #refreshTagList() {
126
+ this.tagList = [...this.renderRoot.querySelectorAll('ul li > *')];
127
+
128
+ // Iterate over the list, and if there are topic tag links adjacent to each
129
+ // other, then we set the siblingOfJumpLink property, which adjusts the
130
+ // styles for adjacent jumplinks so that the borders aren't doubled up.
131
+ if (this.tagList.length > 0) {
132
+ let lastItemIsLink = false;
133
+ this.tagList.forEach((item) => {
134
+ if (lastItemIsLink) {
135
+ item.siblingOfJumpLink = true;
136
+ lastItemIsLink = false;
137
+ }
138
+ if (item.href !== '') {
139
+ lastItemIsLink = true;
140
+ }
141
+ });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Add a tag to the light and dark DOM.
147
+ * @param {*} tag - The tag to add.
148
+ * @param {number} index - The position at which to add the tag.
149
+ * @returns {boolean} false if the tag is already in the light DOM.
150
+ */
151
+ addTag(tag, index = -1) {
152
+ const alreadyInDom = Array.from(this.children).includes(tag);
153
+
154
+ if (!alreadyInDom) {
155
+ this.#insertIntoLightDom(tag, index);
156
+ return false;
157
+ }
158
+
159
+ this.#insertIntoShadowDom(tag, index);
160
+
161
+ this.#refreshTagList();
162
+ }
163
+
164
+ /**
165
+ * Add a tag to the light DOM.
166
+ * @param {*} tag - The tag to add.
167
+ * @param {number} index - The position at which to add the tag.
168
+ */
169
+ #insertIntoLightDom(tag, index) {
170
+ if (index === -1 || index >= this.children.length) {
171
+ this.appendChild(tag);
172
+ } else {
173
+ this.insertBefore(tag, this.children[index]);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Add a tag to the shadow DOM.
179
+ * @param {*} tag - The tag to add.
180
+ * @param {number} index - The position at which to add the tag.
181
+ */
182
+ #insertIntoShadowDom(tag, index) {
183
+ const cloned = tag.cloneNode(true);
184
+ const wrapped = document.createElement('li');
185
+ wrapped.appendChild(cloned);
186
+
187
+ const ul = this.shadowRoot.querySelector('ul');
188
+
189
+ let actualIndex = index;
190
+ if (index === -1 || index >= ul.children.length) {
191
+ ul.appendChild(wrapped);
192
+ actualIndex = ul.children.length - 1;
193
+ } else {
194
+ ul.insertBefore(wrapped, ul.children[index]);
195
+ }
196
+
197
+ cloned.addEventListener('tag-click', () => {
198
+ this.dispatchEvent(
199
+ new CustomEvent('tag-click', {
200
+ detail: { target: cloned, index: actualIndex },
201
+ bubbles: true,
202
+ composed: true,
203
+ }),
204
+ );
205
+ this.#removeTagNode(cloned);
206
+ });
207
+
208
+ this.#tagMap ??= new Map();
209
+ const id = this.#tagIdentifier(tag);
210
+ this.#tagMap.set(id, wrapped);
211
+
212
+ this.dispatchEvent(
213
+ new CustomEvent('tag-added', {
214
+ detail: { target: tag, index: actualIndex },
215
+ bubbles: true,
216
+ composed: true,
217
+ }),
218
+ );
219
+ }
220
+
221
+ /**
222
+ * @param {*} tag - The tag to add.
223
+ * @returns {string} A unique ID.
224
+ */
225
+ #tagIdentifier(tag) {
226
+ return `${tag.tagName}::${tag.textContent.trim()}`;
227
+ }
228
+
229
+ /**
230
+ * Remove a filter tag from the light DOM.
231
+ * This is private because it's called by the mutation observer.
232
+ * @param {*} tag - The tag to remove.
233
+ * @returns {boolean} false if the wrapped tag was not found.
234
+ */
235
+ #removeTagNode(tag) {
236
+ const id = this.#tagIdentifier(tag);
237
+ const wrapped = this.#tagMap.get(id);
238
+
239
+ if (!wrapped) return false;
240
+
241
+ // Try getting the index from the light DOM.
242
+ let index = Array.from(this.children).indexOf(tag);
243
+
244
+ // If not found (e.g. manually removed via DevTools), fallback to shadow DOM.
245
+ if (index === -1 && wrapped.parentElement) {
246
+ const shadowChildren = Array.from(wrapped.parentElement.children);
247
+ index = shadowChildren.indexOf(wrapped);
248
+ }
249
+
250
+ // Remove from light DOM and shadow DOM.
251
+ if (tag.parentElement === this) {
252
+ tag.remove();
253
+ }
254
+
255
+ if (wrapped.parentElement) {
256
+ wrapped.remove();
257
+ }
258
+
259
+ this.#tagMap.delete(id);
260
+
261
+ this.dispatchEvent(
262
+ new CustomEvent('tag-removed', {
263
+ detail: { target: tag, index: index },
264
+ bubbles: true,
265
+ composed: true,
266
+ }),
267
+ );
268
+
269
+ this.#refreshTagList();
270
+ }
271
+
272
+ /**
273
+ * Remove a filter tag from the light and dark DOM.
274
+ * @param {*} tag - The tag to remove.
275
+ */
276
+ removeTag(tag) {
277
+ // Support passing in either light DOM <tag> or shadow DOM <li> if needed
278
+ // Normalize to the light DOM tag element:
279
+
280
+ const lightDomTag = this.#getLightDomTag(tag);
281
+
282
+ this.#removeTagNode(lightDomTag);
283
+ }
284
+
285
+ /**
286
+ * Get light and dark DOM.
287
+ * @param {Node} node - The tag to remove.
288
+ * @returns {Node|null} The tag node.
289
+ */
290
+ #getLightDomTag(node) {
291
+ // If node is a wrapped shadow DOM <li>, get the orignal tag inside it.
292
+ if (node.tagName === 'LI' && node.shadowRoot) {
293
+ // unlikely scenario if you don't expose shadow nodes externally.
294
+ return node.querySelector('cfpb-tag-filter');
295
+ }
296
+
297
+ // If node is already a light DOM tag or child <cfpb-tag-group>, return it.
298
+ if (this.contains(node)) return node;
299
+
300
+ return null;
301
+ }
302
+
303
+ render() {
304
+ return html`<ul ?stacked=${this.stacked}></ul>`;
305
+ }
306
+
307
+ static init() {
308
+ window.customElements.get('cfpb-tag-group') ||
309
+ window.customElements.define('cfpb-tag-group', CfpbTagGroup);
310
+ }
311
+ }
@@ -0,0 +1,160 @@
1
+ import { jest } from '@jest/globals';
2
+ import { CfpbTagFilter } from '../cfpb-tag-filter';
3
+ import { CfpbTagTopic } from '../cfpb-tag-topic';
4
+ import { CfpbTagGroup } from './index.js';
5
+
6
+ describe('<cfpb-tag-group>', () => {
7
+ let elm;
8
+
9
+ beforeEach(async () => {
10
+ CfpbTagGroup.init();
11
+ CfpbTagTopic.init();
12
+ CfpbTagFilter.init();
13
+ elm = document.createElement('cfpb-tag-group');
14
+ document.body.appendChild(elm);
15
+
16
+ await customElements.whenDefined('cfpb-tag-group');
17
+ await elm.updateComplete;
18
+ });
19
+
20
+ afterEach(() => {
21
+ document.body.removeChild(elm);
22
+ });
23
+
24
+ it('renders slotted content', async () => {
25
+ const slottedContent = document.createElement('cfpb-tag-filter');
26
+ slottedContent.textContent = 'Earth';
27
+ elm.appendChild(slottedContent);
28
+ await elm.updateComplete;
29
+
30
+ // Wait for browser render to settle.
31
+ // Needed because requestAnimationFrame is used in firstUpdated.
32
+ await new Promise((resolve) => requestAnimationFrame(resolve));
33
+
34
+ const ul = elm.shadowRoot.querySelector('ul');
35
+
36
+ expect(ul.children.length).toBe(1);
37
+
38
+ // Walk into the first <li> and first <cfpb-tag-filter>.
39
+ expect(ul.firstElementChild.firstElementChild.textContent).toBe('Earth');
40
+ });
41
+
42
+ it('dispatches the correct events', async () => {
43
+ const mockAddedHandler = jest.fn();
44
+ const mockRemoveHandler = jest.fn();
45
+ elm.addEventListener('tag-added', mockAddedHandler);
46
+ elm.addEventListener('tag-removed', mockRemoveHandler);
47
+
48
+ const slottedContent = document.createElement('cfpb-tag-filter');
49
+ slottedContent.textContent = 'Earth';
50
+ elm.appendChild(slottedContent);
51
+ await elm.updateComplete;
52
+
53
+ // Wait for MutationObserver render to settle.
54
+ await new Promise((resolve) => {
55
+ elm.addEventListener('tag-added', () => {
56
+ resolve();
57
+ });
58
+ });
59
+
60
+ expect(mockAddedHandler).toHaveBeenCalledTimes(1);
61
+ expect(
62
+ mockAddedHandler.mock.calls[0][0].detail.target.isEqualNode(
63
+ slottedContent,
64
+ ),
65
+ ).toBe(true);
66
+
67
+ elm.shadowRoot
68
+ .querySelector('cfpb-tag-filter')
69
+ .shadowRoot.querySelector('button')
70
+ .click();
71
+
72
+ expect(mockRemoveHandler).toHaveBeenCalledTimes(1);
73
+ expect(
74
+ mockRemoveHandler.mock.calls[0][0].detail.target.isEqualNode(
75
+ slottedContent,
76
+ ),
77
+ ).toBe(true);
78
+ });
79
+
80
+ it('adds tags to internal list', async () => {
81
+ expect(elm.tagList.length).toBe(0);
82
+
83
+ let slottedContent = document.createElement('cfpb-tag-filter');
84
+ slottedContent.textContent = 'Earth';
85
+ elm.appendChild(slottedContent);
86
+ await elm.updateComplete;
87
+
88
+ // Wait for MutationObserver render to settle.
89
+ await new Promise((resolve) => {
90
+ elm.addEventListener('tag-added', () => {
91
+ resolve();
92
+ });
93
+ });
94
+
95
+ expect(elm.tagList.length).toBe(1);
96
+
97
+ slottedContent = document.createElement('cfpb-tag-filter');
98
+ slottedContent.textContent = 'Mars';
99
+ elm.appendChild(slottedContent);
100
+ await elm.updateComplete;
101
+
102
+ expect(elm.tagList.length).toBe(2);
103
+ expect(elm.tagList[1].isEqualNode(slottedContent)).toBe(true);
104
+ });
105
+
106
+ it('adds class to remove border on topic tag groups', async () => {
107
+ let slottedContent = document.createElement('cfpb-tag-topic');
108
+ slottedContent.textContent = 'Earth';
109
+ slottedContent.setAttribute('href', '#');
110
+ elm.appendChild(slottedContent);
111
+
112
+ slottedContent = document.createElement('cfpb-tag-topic');
113
+ slottedContent.textContent = 'Mars';
114
+ slottedContent.setAttribute('href', '#');
115
+ elm.appendChild(slottedContent);
116
+
117
+ // Wait for MutationObserver render to settle.
118
+ await new Promise((resolve) => {
119
+ elm.addEventListener('tag-added', () => {
120
+ resolve();
121
+ });
122
+ });
123
+
124
+ await elm.tagList[0].updateComplete;
125
+
126
+ expect(
127
+ elm.tagList[0].shadowRoot
128
+ .querySelector('a')
129
+ .classList.contains('a-tag-topic--no-top-border'),
130
+ ).toBe(false);
131
+ expect(
132
+ elm.tagList[1].shadowRoot
133
+ .querySelector('a')
134
+ .classList.contains('a-tag-topic--no-top-border'),
135
+ ).toBe(true);
136
+ });
137
+
138
+ it('adds and removes at chosen indices', async () => {
139
+ const earthContent = document.createElement('cfpb-tag-filter');
140
+ earthContent.textContent = 'Earth';
141
+ elm.addTag(earthContent);
142
+
143
+ const marsContent = document.createElement('cfpb-tag-filter');
144
+ marsContent.textContent = 'Mars';
145
+ elm.addTag(marsContent, 0);
146
+
147
+ // Wait for MutationObserver render to settle.
148
+ await new Promise((resolve) => {
149
+ elm.addEventListener('tag-added', () => {
150
+ resolve();
151
+ });
152
+ });
153
+
154
+ expect(elm.tagList[0].isEqualNode(marsContent)).toBe(true);
155
+
156
+ elm.removeTag(marsContent, 0);
157
+
158
+ expect(elm.tagList[0].isEqualNode(earthContent)).toBe(true);
159
+ });
160
+ });
@@ -0,0 +1,88 @@
1
+ @use 'sass:math';
2
+ @use '@cfpb/cfpb-design-system/src/base' as *;
3
+ @use '@cfpb/cfpb-design-system/src/abstracts' as *;
4
+ @use '@cfpb/cfpb-design-system/src/utilities' as *;
5
+ @use '@cfpb/cfpb-design-system/src/components/cfpb-typography/mixins' as *;
6
+
7
+ :host {
8
+ // Topic tags
9
+ .a-tag-topic {
10
+ text-decoration: none;
11
+ display: flex;
12
+ gap: math.div(5px, $btn-font-size) + rem;
13
+ }
14
+
15
+ @include u-jump-link(
16
+ $jump-link-class: 'a-tag-topic',
17
+ $jump-link-text-class: 'a-tag-topic__text',
18
+ $is-gold: true
19
+ );
20
+
21
+ // @include u-jump-link-group($jump-link-class: 'a-tag-topic');
22
+
23
+ // Mobile only.
24
+ @include respond-to-max($bp-xs-max) {
25
+ // Handle the borders of jump links that are adjacent in a list.
26
+ a.a-tag-topic--no-top-border {
27
+ position: relative;
28
+ border-top: none;
29
+ }
30
+
31
+ a.a-tag-topic:hover::before,
32
+ a.a-tag-topic:focus::before {
33
+ position: absolute;
34
+ top: -1px;
35
+ content: '';
36
+ display: block;
37
+ height: 1px;
38
+ width: 100%;
39
+ border-top: 1px solid currentcolor;
40
+ }
41
+ }
42
+
43
+ .a-tag-topic__bullet {
44
+ font-size: 1rem;
45
+ line-height: 1rem;
46
+ }
47
+
48
+ // Tablet and above.
49
+ @include respond-to-min($bp-sm-min) {
50
+ a.a-tag-topic__text {
51
+ // Colors for :link, :visited, :hover, :focus, :active
52
+ @include u-link-colors(
53
+ var(--gray),
54
+ var(--gray),
55
+ var(--gray),
56
+ var(--gray),
57
+ var(--gray)
58
+ );
59
+ }
60
+
61
+ a.a-tag-topic,
62
+ a.a-tag-topic:visited,
63
+ a.a-tag-topic:hover,
64
+ a.a-tag-topic:focus,
65
+ a.a-tag-topic:active {
66
+ border-bottom: none;
67
+ outline-offset: 1px;
68
+
69
+ .a-tag-topic__text {
70
+ // Move the underline down 1px.
71
+ padding-bottom: 1px;
72
+ border-bottom: 1px dotted var(--gold-80);
73
+ }
74
+ }
75
+
76
+ a.a-tag-topic:hover .a-tag-topic__text {
77
+ border-bottom: 1px solid var(--gold-80);
78
+ }
79
+
80
+ a.a-tag-topic:focus {
81
+ outline-color: var(--gray);
82
+
83
+ .a-tag-topic__text {
84
+ border-bottom-style: solid !important;
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,62 @@
1
+ import { html, LitElement, css, unsafeCSS } from 'lit';
2
+ import styles from './cfpb-tag-topic.component.scss';
3
+
4
+ /**
5
+ *
6
+ * @element cfpb-tag-topic.
7
+ * @slot - The content for the topic tag.
8
+ */
9
+ export class CfpbTagTopic extends LitElement {
10
+ static styles = css`
11
+ ${unsafeCSS(styles)}
12
+ `;
13
+
14
+ /**
15
+ * @property {string} href - href attribute, if this is a topic link.
16
+ * @property {boolean} siblingOfJumpLink
17
+ * Whether the preceding sibling is a jump link or not.
18
+ */
19
+ static get properties() {
20
+ return {
21
+ href: { type: String, reflect: true },
22
+ siblingOfJumpLink: { type: Boolean },
23
+ };
24
+ }
25
+
26
+ constructor() {
27
+ super();
28
+ this.href = '';
29
+ this.siblingOfJumpLink = false;
30
+ }
31
+
32
+ get #tagClass() {
33
+ let tagClass = 'a-tag-topic';
34
+
35
+ if (this.siblingOfJumpLink) {
36
+ tagClass += ' a-tag-topic--no-top-border';
37
+ }
38
+
39
+ return tagClass;
40
+ }
41
+
42
+ render() {
43
+ const bullet = html`<span class="a-tag-topic__bullet" aria-hidden="true"
44
+ >•</span
45
+ >`;
46
+ const slot =
47
+ this.href === ''
48
+ ? html`<span class="a-tag-topic"
49
+ >${bullet}<span class="a-tag-topic__text"><slot></slot></span
50
+ ></span>`
51
+ : html`<a class="${this.#tagClass}" href="${this.href}"
52
+ >${bullet}<span class="a-tag-topic__text"><slot></slot></span
53
+ ></a>`;
54
+ return html`${slot}`;
55
+ }
56
+
57
+ static init() {
58
+ // Initialize parent file upload.
59
+ window.customElements.get('cfpb-tag-topic') ||
60
+ window.customElements.define('cfpb-tag-topic', CfpbTagTopic);
61
+ }
62
+ }
@@ -0,0 +1,11 @@
1
+ /* ==========================================================================
2
+ Design System - Web Components
3
+ ========================================================================== */
4
+
5
+ // TODO: aggregate and export the component subdirectories automatically.
6
+ export * from './cfpb-button';
7
+ export * from './cfpb-form-choice';
8
+ export * from './cfpb-file-upload';
9
+ export * from './cfpb-tag-filter';
10
+ export * from './cfpb-tag-topic';
11
+ export * from './cfpb-tag-group';
package/src/index.js CHANGED
@@ -18,5 +18,4 @@ export * from './components/cfpb-typography';
18
18
 
19
19
  export * from './utilities';
20
20
 
21
- export * from './elements/cfpb-button';
22
- export * from './elements/cfpb-file-upload';
21
+ export * from './elements';
@@ -76,7 +76,7 @@
76
76
  // utility works as expected for screenreaders. Comma-separated syntax is
77
77
  // not used because space-separated is more backward-compatible,
78
78
  // per https://developer.mozilla.org/en-US/docs/Web/CSS/clip
79
- clip: rect(0 0 0 0);
79
+ clip: rect(0 0 0 0); /* stylelint-disable-line property-no-deprecated */
80
80
  }
81
81
 
82
82
  .u-visually-hidden {