@cfpb/cfpb-design-system 4.2.4 → 4.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/CHANGELOG.md +166 -1
- package/dist/components/cfpb-expandables/index.js +1 -1
- package/dist/components/cfpb-expandables/index.js.map +3 -3
- package/dist/components/cfpb-forms/index.js +1 -1
- package/dist/components/cfpb-forms/index.js.map +2 -2
- package/dist/elements/cfpb-button/index.js +4 -4
- package/dist/elements/cfpb-button/index.js.map +3 -3
- package/dist/elements/cfpb-checkbox-icon/index.js +29 -0
- package/dist/elements/{cfpb-checkbox → cfpb-checkbox-icon}/index.js.map +4 -4
- package/dist/elements/cfpb-expandable/index.css +2 -0
- package/dist/elements/cfpb-expandable/index.css.map +7 -0
- package/dist/elements/cfpb-expandable/index.js +33 -0
- package/dist/elements/cfpb-expandable/index.js.map +7 -0
- package/dist/elements/cfpb-file-upload/index.js +4 -4
- package/dist/elements/cfpb-file-upload/index.js.map +3 -3
- package/dist/elements/cfpb-form-alert/index.js +32 -0
- package/dist/elements/cfpb-form-alert/index.js.map +7 -0
- package/dist/elements/cfpb-form-choice/index.js +12 -3
- package/dist/elements/cfpb-form-choice/index.js.map +4 -4
- package/dist/elements/cfpb-form-search/index.js +41 -0
- package/dist/elements/cfpb-form-search/index.js.map +7 -0
- package/dist/elements/cfpb-form-search-input/index.js +41 -0
- package/dist/elements/cfpb-form-search-input/index.js.map +7 -0
- package/dist/elements/cfpb-icon-text/index.js +3 -3
- package/dist/elements/cfpb-icon-text/index.js.map +3 -3
- package/dist/elements/cfpb-label/index.js +3 -3
- package/dist/elements/cfpb-label/index.js.map +2 -2
- package/dist/elements/cfpb-list/index.js +39 -0
- package/dist/elements/cfpb-list/index.js.map +7 -0
- package/dist/elements/cfpb-list-item/index.js +39 -0
- package/dist/elements/cfpb-list-item/index.js.map +7 -0
- package/dist/elements/cfpb-multiselect/index.js +13 -4
- package/dist/elements/cfpb-multiselect/index.js.map +4 -4
- package/dist/elements/cfpb-pagination/index.js +3 -3
- package/dist/elements/cfpb-pagination/index.js.map +2 -2
- package/dist/elements/cfpb-select/index.css +2 -0
- package/dist/elements/cfpb-select/index.css.map +7 -0
- package/dist/elements/cfpb-select/index.js +42 -0
- package/dist/elements/cfpb-select/index.js.map +7 -0
- package/dist/elements/cfpb-select-list/index.js +39 -0
- package/dist/elements/cfpb-select-list/index.js.map +7 -0
- package/dist/elements/cfpb-tag-filter/index.js +3 -3
- package/dist/elements/cfpb-tag-filter/index.js.map +3 -3
- package/dist/elements/cfpb-tag-group/index.js +3 -3
- package/dist/elements/cfpb-tag-group/index.js.map +4 -4
- package/dist/elements/cfpb-tag-topic/index.js +4 -4
- package/dist/elements/cfpb-tag-topic/index.js.map +1 -1
- package/dist/elements/index.css +2 -0
- package/dist/elements/index.css.map +7 -0
- package/dist/elements/index.js +7 -6
- package/dist/elements/index.js.map +4 -4
- package/dist/index.js +7 -6
- package/dist/index.js.map +4 -4
- package/dist/utilities/index.js +1 -1
- package/dist/utilities/index.js.map +3 -3
- package/package.json +1 -1
- package/src/components/cfpb-expandables/expandable.js +3 -0
- package/src/components/cfpb-forms/multiselect.js +1 -1
- package/src/elements/abstracts/custom-props.css +123 -0
- package/src/elements/abstracts/grid-mixins.scss +83 -0
- package/src/elements/abstracts/heading-mixins.scss +346 -0
- package/src/elements/abstracts/index.scss +7 -0
- package/src/elements/abstracts/media-queries.scss +35 -0
- package/src/elements/abstracts/sizing-vars.scss +65 -0
- package/src/elements/abstracts/vars-breakpoints.scss +16 -0
- package/src/elements/abstracts/vars.css +79 -0
- package/src/elements/base/base.scss +375 -0
- package/src/elements/base/font.scss +27 -0
- package/src/elements/base/index.scss +3 -0
- package/src/elements/base/normalize.scss +290 -0
- package/src/elements/cfpb-button/cfpb-button-group.scss +10 -0
- package/src/elements/cfpb-button/cfpb-button-link.scss +96 -0
- package/src/elements/cfpb-button/cfpb-button.component.scss +11 -4
- package/src/elements/cfpb-button/cfpb-button.scss +222 -0
- package/src/elements/cfpb-button/index.js +28 -29
- package/src/elements/cfpb-button/vars.css +30 -0
- package/src/elements/cfpb-checkbox-icon/cfpb-checkbox-icon.component.scss +88 -0
- package/src/elements/cfpb-checkbox-icon/index.js +104 -0
- package/src/elements/cfpb-expandable/cfpb-expandable.component.scss +218 -0
- package/src/elements/cfpb-expandable/index.js +127 -0
- package/src/elements/cfpb-file-upload/cfpb-file-upload.component.scss +2 -2
- package/src/elements/cfpb-file-upload/index.js +16 -18
- package/src/elements/cfpb-form-alert/cfpb-form-alert.component.scss +36 -0
- package/src/elements/cfpb-form-alert/index.js +55 -0
- package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +42 -81
- package/src/elements/cfpb-form-choice/index.js +58 -18
- package/src/elements/cfpb-form-search/cfpb-form-search.component.scss +54 -0
- package/src/elements/cfpb-form-search/index.js +194 -0
- package/src/elements/cfpb-form-search-input/cfpb-form-search-input.component.scss +217 -0
- package/src/elements/cfpb-form-search-input/index.js +136 -0
- package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +32 -39
- package/src/elements/cfpb-icon-text/index.js +32 -104
- package/src/elements/cfpb-label/cfpb-label.component.scss +2 -2
- package/src/elements/cfpb-label/index.js +6 -9
- package/src/elements/cfpb-list/cfpb-list.component.scss +23 -0
- package/src/elements/cfpb-list/index.js +357 -0
- package/src/elements/cfpb-list/index.spec.js +169 -0
- package/src/elements/cfpb-list-item/cfpb-list-item.component.scss +69 -0
- package/src/elements/cfpb-list-item/index.js +215 -0
- package/src/elements/cfpb-pagination/cfpb-pagination.component.scss +2 -7
- package/src/elements/cfpb-pagination/index.js +6 -8
- package/src/elements/cfpb-select/cfpb-select.component.scss +241 -0
- package/src/elements/cfpb-select/index.js +381 -0
- package/src/elements/cfpb-tag-filter/cfpb-tag-filter.component.scss +6 -3
- package/src/elements/cfpb-tag-filter/index.js +15 -7
- package/src/elements/cfpb-tag-group/cfpb-tag-group.component.scss +2 -2
- package/src/elements/cfpb-tag-group/index.js +53 -6
- package/src/elements/cfpb-tag-topic/index.js +5 -7
- package/src/elements/cfpb-utilities/parse-child-data.js +50 -0
- package/src/elements/cfpb-utilities/parse-child-data.spec.js +56 -0
- package/src/elements/cfpb-utilities/search-service.js +46 -0
- package/src/elements/cfpb-utilities/search-service.spec.js +138 -0
- package/src/elements/cfpb-utilities/transition/transition.scss +98 -0
- package/src/elements/index.js +7 -1
- package/src/index.scss +11 -0
- package/src/tokens/abstracts/custom-props.json +1642 -0
- package/src/tokens/abstracts/vars.json +1319 -0
- package/src/tokens/cfpb-button/vars.json +436 -0
- package/src/utilities/transition/max-height-transition.js +74 -0
- package/dist/elements/cfpb-checkbox/index.js +0 -29
- package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +0 -225
- package/src/elements/cfpb-multiselect/index.js +0 -444
- package/src/elements/cfpb-multiselect/multiselect-model.js +0 -288
- package/src/elements/cfpb-multiselect/multiselect-model.spec.js +0 -236
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { html, LitElement, css, unsafeCSS, nothing } from 'lit';
|
|
2
|
+
import { ref, createRef } from 'lit/directives/ref.js';
|
|
3
|
+
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
4
|
+
import styles from './cfpb-select.component.scss';
|
|
5
|
+
import expandIcon from '../../components/cfpb-icons/icons/down.svg';
|
|
6
|
+
import collapseIcon from '../../components/cfpb-icons/icons/up.svg';
|
|
7
|
+
import { CfpbFormSearchInput } from '../cfpb-form-search-input';
|
|
8
|
+
import { SearchService } from '../cfpb-utilities/search-service.js';
|
|
9
|
+
import { MaxHeightTransition } from '../../utilities/transition/max-height-transition';
|
|
10
|
+
import { FlyoutMenu } from '../../utilities/behavior/flyout-menu';
|
|
11
|
+
import { CfpbList } from '../cfpb-list';
|
|
12
|
+
import { CfpbTagGroup } from '../cfpb-tag-group';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @element cfpb-button
|
|
17
|
+
* @slot - The main content for the button.
|
|
18
|
+
*/
|
|
19
|
+
export class CfpbSelect extends LitElement {
|
|
20
|
+
static styles = css`
|
|
21
|
+
${unsafeCSS(styles)}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
// Flyout menu options.
|
|
25
|
+
#flyoutMenu;
|
|
26
|
+
#transition;
|
|
27
|
+
#search;
|
|
28
|
+
#root = createRef();
|
|
29
|
+
#headerDom = createRef();
|
|
30
|
+
#contentDom = createRef();
|
|
31
|
+
#input = createRef();
|
|
32
|
+
#tagGroup = createRef();
|
|
33
|
+
#list = createRef();
|
|
34
|
+
#displayLabel = createRef();
|
|
35
|
+
#boundOnOutsideClick;
|
|
36
|
+
#noResults = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @property {boolean} multiple - Whether the select supports multiple or not.
|
|
40
|
+
* @property {boolean} isExpanded - Whether the select is expanded or not.
|
|
41
|
+
* @property {Array} selectedTexts - Text of selected options.
|
|
42
|
+
* @returns {object} The map of properties.
|
|
43
|
+
*/
|
|
44
|
+
static get properties() {
|
|
45
|
+
return {
|
|
46
|
+
multiple: { type: Boolean, reflect: true },
|
|
47
|
+
disabled: { type: Boolean },
|
|
48
|
+
validation: { type: String },
|
|
49
|
+
label: { type: String },
|
|
50
|
+
name: { type: String },
|
|
51
|
+
title: { type: Boolean, attribute: true },
|
|
52
|
+
value: { type: String },
|
|
53
|
+
maxlength: { type: Number },
|
|
54
|
+
placeholder: { type: String },
|
|
55
|
+
ariaLabelInput: { type: String, attribute: 'aria-label-input' },
|
|
56
|
+
ariaLabelList: { type: String, attribute: 'aria-label-list' },
|
|
57
|
+
|
|
58
|
+
isExpanded: { type: Boolean, attribute: 'open', reflect: true },
|
|
59
|
+
selectedTexts: { type: Array },
|
|
60
|
+
optionList: { type: Array },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
|
|
67
|
+
this.options = [];
|
|
68
|
+
this.selectedTexts = [];
|
|
69
|
+
this.optionList = [];
|
|
70
|
+
|
|
71
|
+
this.#boundOnOutsideClick = this.#onOutsideClick.bind(this);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
firstUpdated() {
|
|
75
|
+
this.#initFlyoutMenu();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
disconnectedCallback() {
|
|
79
|
+
document.removeEventListener('pointerdown', this.#boundOnOutsideClick);
|
|
80
|
+
super.disconnectedCallback();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#onOutsideClick(evt) {
|
|
84
|
+
const path = evt.composedPath();
|
|
85
|
+
if (!path.includes(this)) {
|
|
86
|
+
this.isExpanded = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#onSlotChange(evt) {
|
|
91
|
+
const slot = evt.target;
|
|
92
|
+
|
|
93
|
+
const list = slot
|
|
94
|
+
.assignedNodes({ flatten: true })
|
|
95
|
+
.filter(
|
|
96
|
+
(node) =>
|
|
97
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
98
|
+
(node.tagName === 'UL' || node.tagName === 'OL'),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!list || !list[0]) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract list items (with their text or link info)
|
|
106
|
+
const items = [...list[0].querySelectorAll('li')].map((li) => {
|
|
107
|
+
const checked = li.querySelector('b');
|
|
108
|
+
if (checked) {
|
|
109
|
+
return {
|
|
110
|
+
value: li.textContent.trim(),
|
|
111
|
+
checked: 'true',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { value: li.textContent.trim() };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.optionList = items;
|
|
118
|
+
|
|
119
|
+
this.#search = new SearchService(
|
|
120
|
+
items.map((item) => {
|
|
121
|
+
return item.value;
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#onInput(evt) {
|
|
127
|
+
this.#flyoutMenu.suspend();
|
|
128
|
+
if (!this.isExpanded) this.isExpanded = true;
|
|
129
|
+
const visibleItems = this.#list.value.filterItems(
|
|
130
|
+
this.#search.search(evt.target.value),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (visibleItems.length === 0) {
|
|
134
|
+
this.#noResults = true;
|
|
135
|
+
this.requestUpdate();
|
|
136
|
+
} else {
|
|
137
|
+
this.#noResults = false;
|
|
138
|
+
this.requestUpdate();
|
|
139
|
+
this.#flyoutMenu.resume();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#onClear() {
|
|
144
|
+
this.#list.value.showAllItems();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#initFlyoutMenu() {
|
|
148
|
+
const root = this.#root.value;
|
|
149
|
+
const contentDom = this.#contentDom.value;
|
|
150
|
+
|
|
151
|
+
// If it's expanded we don't set an initial height,
|
|
152
|
+
// as it will be calculated internally.
|
|
153
|
+
const initialClass = this.isExpanded
|
|
154
|
+
? MaxHeightTransition.CLASSES.MH_DEFAULT
|
|
155
|
+
: MaxHeightTransition.CLASSES.MH_ZERO;
|
|
156
|
+
this.#transition = new MaxHeightTransition(contentDom).init(initialClass);
|
|
157
|
+
|
|
158
|
+
this.#flyoutMenu = new FlyoutMenu(root);
|
|
159
|
+
|
|
160
|
+
this.#flyoutMenu.setTransition(
|
|
161
|
+
this.#transition,
|
|
162
|
+
this.#transition.maxHeightZero,
|
|
163
|
+
this.#transition.maxHeightDynamic,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
this.#flyoutMenu.init(this.isExpanded);
|
|
167
|
+
|
|
168
|
+
// Add events.
|
|
169
|
+
this.#flyoutMenu.addEventListener('expandbegin', () => {
|
|
170
|
+
this.isExpanded = true;
|
|
171
|
+
contentDom.classList.remove('u-hidden');
|
|
172
|
+
this.dispatchEvent(
|
|
173
|
+
new CustomEvent('expandbegin', {
|
|
174
|
+
detail: { target: this },
|
|
175
|
+
bubbles: true,
|
|
176
|
+
composed: true,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
this.#flyoutMenu.addEventListener('collapseend', () => {
|
|
181
|
+
this.isExpanded = false;
|
|
182
|
+
contentDom.classList.add('u-hidden');
|
|
183
|
+
|
|
184
|
+
// Remove direction classes.
|
|
185
|
+
this.#root.value.classList.remove(`o-select--up`);
|
|
186
|
+
this.#root.value.classList.remove(`o-select--down`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.#transition.addEventListener('transitiondir', (evt) => {
|
|
190
|
+
this.#root.value.classList.add(`o-select--${evt.dir}`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
updated(changedProps) {
|
|
195
|
+
if (changedProps.has('isExpanded')) {
|
|
196
|
+
const oldVal = changedProps.get('isExpanded');
|
|
197
|
+
const newVal = this.isExpanded;
|
|
198
|
+
|
|
199
|
+
if (newVal !== oldVal) {
|
|
200
|
+
if (newVal) {
|
|
201
|
+
this.#flyoutMenu.expand();
|
|
202
|
+
document.addEventListener('pointerdown', this.#boundOnOutsideClick);
|
|
203
|
+
} else {
|
|
204
|
+
this.#flyoutMenu.collapse();
|
|
205
|
+
document.removeEventListener(
|
|
206
|
+
'pointerdown',
|
|
207
|
+
this.#boundOnOutsideClick,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#onClick(evt) {
|
|
215
|
+
const target = evt.currentTarget;
|
|
216
|
+
|
|
217
|
+
if (this.multiple) {
|
|
218
|
+
if (target.tagName === 'CFPB-FORM-SEARCH-INPUT') {
|
|
219
|
+
if (this.isExpanded) this.#flyoutMenu.suspend();
|
|
220
|
+
else this.#flyoutMenu.expand();
|
|
221
|
+
} else {
|
|
222
|
+
this.#flyoutMenu.resume();
|
|
223
|
+
}
|
|
224
|
+
} else if (target.classList.contains('o-select__label')) {
|
|
225
|
+
this.#headerDom.value.focus();
|
|
226
|
+
this.isExpanded = !this.isExpanded;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#onItemClick() {
|
|
231
|
+
if (this.multiple) {
|
|
232
|
+
this.optionList = this.#list.value.childData;
|
|
233
|
+
this.requestUpdate();
|
|
234
|
+
} else {
|
|
235
|
+
const checkedItems = this.#list.value.checkedItems;
|
|
236
|
+
const selectedValue = checkedItems[0]?.value;
|
|
237
|
+
|
|
238
|
+
this.#displayLabel.value.innerHTML = selectedValue ? selectedValue : '';
|
|
239
|
+
|
|
240
|
+
// Update optionList so the selection persists.
|
|
241
|
+
this.optionList = this.optionList.map((item) => ({
|
|
242
|
+
...item,
|
|
243
|
+
checked: item.value === selectedValue,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
this.requestUpdate();
|
|
247
|
+
|
|
248
|
+
// Now close the dropdown.
|
|
249
|
+
this.isExpanded = false;
|
|
250
|
+
|
|
251
|
+
// Move focus back to the header.
|
|
252
|
+
if (this.multiple) this.#input.value.focus();
|
|
253
|
+
else this.#headerDom.value.focus();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#onTagClick(evt) {
|
|
258
|
+
const tagList = this.#tagGroup.value.tagList.filter((item) => {
|
|
259
|
+
return item !== evt.detail.target;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
this.optionList = this.optionList.map((item) => ({
|
|
263
|
+
...item,
|
|
264
|
+
checked: !!tagList.find((tag) => tag.value === item.value),
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
this.requestUpdate();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#onKeyDown(evt) {
|
|
271
|
+
switch (evt.key) {
|
|
272
|
+
case 'ArrowDown':
|
|
273
|
+
evt.preventDefault();
|
|
274
|
+
this.#contentDom.value.querySelector('cfpb-list-item').focus();
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render() {
|
|
280
|
+
return html`
|
|
281
|
+
<!--Light DOM content-->
|
|
282
|
+
<slot @slotchange=${this.#onSlotChange}></slot>
|
|
283
|
+
|
|
284
|
+
${this.multiple
|
|
285
|
+
? html`<cfpb-tag-group
|
|
286
|
+
${ref(this.#tagGroup)}
|
|
287
|
+
.childData=${this.optionList
|
|
288
|
+
.filter((item) => {
|
|
289
|
+
return item.checked;
|
|
290
|
+
})
|
|
291
|
+
.map((item) => {
|
|
292
|
+
return { text: item.value, tagName: 'cfpb-tag-filter' };
|
|
293
|
+
})}
|
|
294
|
+
@tag-click=${this.#onTagClick}
|
|
295
|
+
>
|
|
296
|
+
</cfpb-tag-group>`
|
|
297
|
+
: nothing}
|
|
298
|
+
|
|
299
|
+
<div
|
|
300
|
+
class="o-select o-select--border"
|
|
301
|
+
data-js-hook="behavior_flyout-menu"
|
|
302
|
+
${ref(this.#root)}
|
|
303
|
+
>
|
|
304
|
+
${this.#renderInput()}
|
|
305
|
+
|
|
306
|
+
<button
|
|
307
|
+
class="o-select__header"
|
|
308
|
+
title="Expand content"
|
|
309
|
+
data-js-hook="behavior_flyout-menu_trigger"
|
|
310
|
+
${ref(this.#headerDom)}
|
|
311
|
+
@keydown=${this.#onKeyDown}
|
|
312
|
+
>
|
|
313
|
+
<span class="o-select__cues" @click=${this.#onClick}>
|
|
314
|
+
<span class="o-select__cue-open" role="img" aria-label="Show">
|
|
315
|
+
${unsafeSVG(expandIcon)}
|
|
316
|
+
<span class="u-visually-hidden">Show</span>
|
|
317
|
+
</span>
|
|
318
|
+
<span class="o-select__cue-close" role="img" aria-label="Hide">
|
|
319
|
+
${unsafeSVG(collapseIcon)}
|
|
320
|
+
<span class="u-visually-hidden">Hide</span>
|
|
321
|
+
</span>
|
|
322
|
+
</span>
|
|
323
|
+
</button>
|
|
324
|
+
<div
|
|
325
|
+
class="o-select__content"
|
|
326
|
+
data-js-hook="behavior_flyout-menu_content"
|
|
327
|
+
${ref(this.#contentDom)}
|
|
328
|
+
>
|
|
329
|
+
<cfpb-list
|
|
330
|
+
@item-click=${this.#onItemClick}
|
|
331
|
+
?multiple=${this.multiple}
|
|
332
|
+
.childData=${this.optionList}
|
|
333
|
+
type=${this.multiple ? 'checkbox' : 'check'}
|
|
334
|
+
aria-label=${this.ariaLabelList
|
|
335
|
+
? this.ariaLabelList
|
|
336
|
+
: 'Choose an item…'}
|
|
337
|
+
${ref(this.#list)}
|
|
338
|
+
>
|
|
339
|
+
</cfpb-list>
|
|
340
|
+
<div class=${this.#noResults ? 'no-results' : 'u-hidden'}>
|
|
341
|
+
No results found
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#renderInput() {
|
|
349
|
+
return this.multiple
|
|
350
|
+
? html`
|
|
351
|
+
<cfpb-form-search-input
|
|
352
|
+
${ref(this.#input)}
|
|
353
|
+
borderless
|
|
354
|
+
?name=${this.name}
|
|
355
|
+
?value=${this.value}
|
|
356
|
+
?placeholder=${this.placeholder}
|
|
357
|
+
title=${this.title}
|
|
358
|
+
?maxlength=${this.maxlength}
|
|
359
|
+
aria-label=${this.ariaLabelInput}
|
|
360
|
+
?validation=${this.validation}
|
|
361
|
+
@clear=${this.#onClear}
|
|
362
|
+
@input=${this.#onInput}
|
|
363
|
+
@click=${this.#onClick}
|
|
364
|
+
></cfpb-form-search-input>
|
|
365
|
+
`
|
|
366
|
+
: html`<div
|
|
367
|
+
class="o-select__label"
|
|
368
|
+
${ref(this.#displayLabel)}
|
|
369
|
+
@click=${this.#onClick}
|
|
370
|
+
></div>`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
static init() {
|
|
374
|
+
CfpbFormSearchInput.init();
|
|
375
|
+
CfpbList.init();
|
|
376
|
+
CfpbTagGroup.init();
|
|
377
|
+
|
|
378
|
+
window.customElements.get('cfpb-select') ||
|
|
379
|
+
window.customElements.define('cfpb-select', CfpbSelect);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
@use 'sass:math';
|
|
2
|
-
@use '@cfpb/cfpb-design-system/src/abstracts' as *;
|
|
3
|
-
@use '@cfpb/cfpb-design-system/src/
|
|
2
|
+
@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
|
|
3
|
+
@use '@cfpb/cfpb-design-system/src/elements/cfpb-button/vars' as *;
|
|
4
4
|
|
|
5
5
|
:host {
|
|
6
6
|
button {
|
|
7
|
+
// This line-height isn't 19 or 22, as 20 creates a 30px high tag.
|
|
8
|
+
line-height: math.div(20px, $base-font-size-px);
|
|
9
|
+
|
|
7
10
|
// Filter tags appear in filtered contexts, often as part of multiselects.
|
|
8
|
-
line-height: math.div(19px, $base-font-size-px);
|
|
9
11
|
font-size: math.div(16px, $btn-font-size) + rem;
|
|
10
12
|
|
|
11
13
|
display: flex;
|
|
12
14
|
gap: math.div(10px, $btn-font-size) + rem;
|
|
15
|
+
align-items: center;
|
|
13
16
|
|
|
14
17
|
border: 1px solid var(--teal);
|
|
15
18
|
padding: 4px 6px;
|
|
@@ -17,15 +17,15 @@ export class CfpbTagFilter extends LitElement {
|
|
|
17
17
|
* @property {string} for - Associate the label with an ID elsewhere.
|
|
18
18
|
* @returns {object} The map of properties.
|
|
19
19
|
*/
|
|
20
|
-
static
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
20
|
+
static properties = {
|
|
21
|
+
for: { type: String },
|
|
22
|
+
value: { type: String },
|
|
23
|
+
};
|
|
25
24
|
|
|
26
25
|
constructor() {
|
|
27
26
|
super();
|
|
28
27
|
this.for = '';
|
|
28
|
+
this.value = '';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
#onClick() {
|
|
@@ -38,11 +38,19 @@ export class CfpbTagFilter extends LitElement {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
#onSlotChange() {
|
|
42
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
43
|
+
this.value = slot
|
|
44
|
+
.assignedNodes({ flatten: true })
|
|
45
|
+
.map((node) => node.textContent.trim())
|
|
46
|
+
.join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
render() {
|
|
42
50
|
const slot =
|
|
43
51
|
this.for === ''
|
|
44
|
-
? html`<slot></slot>`
|
|
45
|
-
: html`<label for
|
|
52
|
+
? html`<slot @slotchange=${this.#onSlotChange}></slot>`
|
|
53
|
+
: html`<label for=${this.for}><slot></slot></label>`;
|
|
46
54
|
return html`<button @click=${this.#onClick}>
|
|
47
55
|
${slot} ${unsafeHTML(icon)}
|
|
48
56
|
</button>`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@use 'sass:math';
|
|
2
|
-
@use '@cfpb/cfpb-design-system/src/abstracts' as *;
|
|
3
|
-
@use '@cfpb/cfpb-design-system/src/
|
|
2
|
+
@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
|
|
3
|
+
@use '@cfpb/cfpb-design-system/src/elements/cfpb-button/vars' as *;
|
|
4
4
|
|
|
5
5
|
:host {
|
|
6
6
|
// Tag group sets the spacing between tags.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, LitElement, css, unsafeCSS } from 'lit';
|
|
2
2
|
import styles from './cfpb-tag-group.component.scss';
|
|
3
|
+
import { parseChildData } from '../cfpb-utilities/parse-child-data';
|
|
3
4
|
|
|
4
5
|
const SUPPORTED_TAG_LIST = ['CFPB-TAG-FILTER', 'CFPB-TAG-TOPIC'];
|
|
5
6
|
|
|
@@ -21,16 +22,16 @@ export class CfpbTagGroup extends LitElement {
|
|
|
21
22
|
`;
|
|
22
23
|
|
|
23
24
|
/**
|
|
25
|
+
* @property {string} childData - Structure data to create child components.
|
|
24
26
|
* @property {boolean} stacked - Whether to stack the tags vertically.
|
|
25
27
|
* @property {Array} tagList - List of the tags in the tag group.
|
|
26
28
|
* @returns {object} The map of properties.
|
|
27
29
|
*/
|
|
28
|
-
static
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
30
|
+
static properties = {
|
|
31
|
+
childData: { type: String, attribute: 'childdata' },
|
|
32
|
+
stacked: { type: Boolean, reflect: true },
|
|
33
|
+
tagList: { type: Array },
|
|
34
|
+
};
|
|
34
35
|
|
|
35
36
|
// Private properties.
|
|
36
37
|
#observer;
|
|
@@ -39,6 +40,7 @@ export class CfpbTagGroup extends LitElement {
|
|
|
39
40
|
|
|
40
41
|
constructor() {
|
|
41
42
|
super();
|
|
43
|
+
this.childData = '';
|
|
42
44
|
this.stacked = false;
|
|
43
45
|
this.tagList = [];
|
|
44
46
|
this.#observer = new MutationObserver(this.#onMutation.bind(this));
|
|
@@ -67,6 +69,51 @@ export class CfpbTagGroup extends LitElement {
|
|
|
67
69
|
});
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
updated(changedProps) {
|
|
73
|
+
if (changedProps.has('childData')) {
|
|
74
|
+
const parsed = parseChildData(this.childData);
|
|
75
|
+
if (parsed) {
|
|
76
|
+
this.#renderTagsFromData(parsed);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#renderTagsFromData(arr) {
|
|
82
|
+
if (!Array.isArray(arr)) return;
|
|
83
|
+
|
|
84
|
+
this.#clearAllTags();
|
|
85
|
+
|
|
86
|
+
arr.forEach((data, index) => {
|
|
87
|
+
const tag = document.createElement(data.tagName);
|
|
88
|
+
// e.g. 'cfpb-tag-filter' or 'cfpb-tag-topic'
|
|
89
|
+
if (data.text) tag.textContent = data.text;
|
|
90
|
+
if (data.href) tag.href = data.href;
|
|
91
|
+
// any other props from `data`
|
|
92
|
+
this.addTag(tag, index);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove all previous tags from shadow DOM and light DOM.
|
|
98
|
+
*/
|
|
99
|
+
#clearAllTags() {
|
|
100
|
+
// Remove shadow DOM wrappers.
|
|
101
|
+
if (this.#tagMap) {
|
|
102
|
+
this.#tagMap.forEach((wrapped) => {
|
|
103
|
+
if (wrapped.parentElement) wrapped.remove();
|
|
104
|
+
});
|
|
105
|
+
this.#tagMap.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Remove light DOM tags.
|
|
109
|
+
[...this.children].forEach((child) => {
|
|
110
|
+
if (SUPPORTED_TAG_LIST.includes(child.tagName)) child.remove();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Reset tagList
|
|
114
|
+
this.tagList = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
/**
|
|
71
118
|
* Set up a MutationObserver to watch changes in the light DOM.
|
|
72
119
|
*/
|
|
@@ -17,12 +17,10 @@ export class CfpbTagTopic extends LitElement {
|
|
|
17
17
|
* Whether the preceding sibling is a jump link or not.
|
|
18
18
|
* @returns {object} The map of properties.
|
|
19
19
|
*/
|
|
20
|
-
static
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
}
|
|
20
|
+
static properties = {
|
|
21
|
+
href: { type: String, reflect: true },
|
|
22
|
+
siblingOfJumpLink: { type: Boolean },
|
|
23
|
+
};
|
|
26
24
|
|
|
27
25
|
/*
|
|
28
26
|
* @property {string} href - The URL to link to (makes the tag a link).
|
|
@@ -55,7 +53,7 @@ export class CfpbTagTopic extends LitElement {
|
|
|
55
53
|
? html`<span class="a-tag-topic"
|
|
56
54
|
>${bullet}<span class="a-tag-topic__text"><slot></slot></span
|
|
57
55
|
></span>`
|
|
58
|
-
: html`<a class
|
|
56
|
+
: html`<a class=${this.#tagClass} href=${this.href}
|
|
59
57
|
>${bullet}<span class="a-tag-topic__text"><slot></slot></span
|
|
60
58
|
></a>`;
|
|
61
59
|
return html`${slot}`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize "childData" style inputs into an array.
|
|
3
|
+
* Accepts:
|
|
4
|
+
* - JS arrays
|
|
5
|
+
* - JSON strings
|
|
6
|
+
* - JSON-like strings with single quotes
|
|
7
|
+
* @param {Array | string} input
|
|
8
|
+
* @param {object} options - optional settings.
|
|
9
|
+
* @param {boolean} options.allowSingleQuotes - default true.
|
|
10
|
+
* @returns {Array|null} Parsed array/string, or null if invalid.
|
|
11
|
+
*/
|
|
12
|
+
export function parseChildData(input, options = {}) {
|
|
13
|
+
const { allowSingleQuotes = true } = options;
|
|
14
|
+
|
|
15
|
+
if (!input) return null;
|
|
16
|
+
|
|
17
|
+
// Already an array - most desirable case.
|
|
18
|
+
if (Array.isArray(input)) {
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof input !== 'string') {
|
|
23
|
+
console.error('childData must be a string or array.');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let text = input.trim();
|
|
28
|
+
|
|
29
|
+
// String is empty after trim.
|
|
30
|
+
if (!text) return null;
|
|
31
|
+
|
|
32
|
+
// Optional conversation: single -> double quotes for HTML convenience.
|
|
33
|
+
if (allowSingleQuotes) {
|
|
34
|
+
text = text.replace(/'/g, '"');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(text);
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(parsed)) {
|
|
41
|
+
console.error('childData JSON must parse to an array.');
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parsed;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to parse childData JSON:', err);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseChildData } from './parse-child-data.js';
|
|
2
|
+
|
|
3
|
+
describe('parseChildData', () => {
|
|
4
|
+
it('returns null for empty input', () => {
|
|
5
|
+
expect(parseChildData(null)).toBeNull();
|
|
6
|
+
expect(parseChildData(undefined)).toBeNull();
|
|
7
|
+
expect(parseChildData('')).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns the array unchanged if input is already an array', () => {
|
|
11
|
+
const arr = [1, 2, 3];
|
|
12
|
+
expect(parseChildData(arr)).toBe(arr);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parses valid JSON string into array', () => {
|
|
16
|
+
const json = '[{"text":"a"},{"text":"b"}]';
|
|
17
|
+
const result = parseChildData(json);
|
|
18
|
+
expect(result).toEqual([{ text: 'a' }, { text: 'b' }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses JSON-like string with single quotes when allowSingleQuotes=true', () => {
|
|
22
|
+
const jsonLike = "[{'text':'a'},{'text':'b'}]";
|
|
23
|
+
const result = parseChildData(jsonLike, { allowSingleQuotes: true });
|
|
24
|
+
expect(result).toEqual([{ text: 'a' }, { text: 'b' }]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('throws error / returns null for invalid JSON', () => {
|
|
28
|
+
const invalid = '[{text:a},{text:b}]';
|
|
29
|
+
const result = parseChildData(invalid);
|
|
30
|
+
expect(result).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null when parsed JSON is not an array', () => {
|
|
34
|
+
const objString = '{"a":1,"b":2}';
|
|
35
|
+
const result = parseChildData(objString);
|
|
36
|
+
expect(result).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not convert single quotes if allowSingleQuotes=false', () => {
|
|
40
|
+
const singleQuoteStr = "[{'text':'a'}]";
|
|
41
|
+
const result = parseChildData(singleQuoteStr, { allowSingleQuotes: false });
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('trims whitespace before parsing', () => {
|
|
46
|
+
const json = ' [ {"text":"x"} ] ';
|
|
47
|
+
const result = parseChildData(json);
|
|
48
|
+
expect(result).toEqual([{ text: 'x' }]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('works with mixed content and multiple spaces', () => {
|
|
52
|
+
const jsonLike = " [ { 'text' : 'x' } , { 'text':'y' } ] ";
|
|
53
|
+
const result = parseChildData(jsonLike, { allowSingleQuotes: true });
|
|
54
|
+
expect(result).toEqual([{ text: 'x' }, { text: 'y' }]);
|
|
55
|
+
});
|
|
56
|
+
});
|