@cfpb/cfpb-design-system 4.3.0 → 4.3.1
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 +21 -1
- package/dist/components/cfpb-buttons/index.css +1 -1
- package/dist/components/cfpb-buttons/index.css.map +2 -2
- package/dist/components/cfpb-buttons/index.js +1 -1
- package/dist/components/cfpb-buttons/index.js.map +1 -1
- package/dist/components/cfpb-expandables/index.css +1 -1
- package/dist/components/cfpb-expandables/index.css.map +2 -2
- package/dist/components/cfpb-expandables/index.js +1 -1
- package/dist/components/cfpb-expandables/index.js.map +2 -2
- package/dist/components/cfpb-forms/index.css +1 -1
- package/dist/components/cfpb-forms/index.css.map +2 -2
- package/dist/components/cfpb-forms/index.js +1 -1
- package/dist/components/cfpb-forms/index.js.map +2 -2
- package/dist/components/cfpb-icons/index.css +1 -1
- package/dist/components/cfpb-icons/index.css.map +2 -2
- package/dist/components/cfpb-icons/index.js +1 -1
- package/dist/components/cfpb-icons/index.js.map +1 -1
- package/dist/components/cfpb-layout/index.css +1 -1
- package/dist/components/cfpb-layout/index.css.map +2 -2
- package/dist/components/cfpb-layout/index.js +1 -1
- package/dist/components/cfpb-layout/index.js.map +1 -1
- package/dist/components/cfpb-notifications/index.css +1 -1
- package/dist/components/cfpb-notifications/index.css.map +2 -2
- package/dist/components/cfpb-notifications/index.js +1 -1
- package/dist/components/cfpb-notifications/index.js.map +1 -1
- package/dist/components/cfpb-pagination/index.css +1 -1
- package/dist/components/cfpb-pagination/index.css.map +2 -2
- package/dist/components/cfpb-pagination/index.js +1 -1
- package/dist/components/cfpb-pagination/index.js.map +1 -1
- package/dist/components/cfpb-tables/index.css +1 -1
- package/dist/components/cfpb-tables/index.css.map +2 -2
- package/dist/components/cfpb-tables/index.js +1 -1
- package/dist/components/cfpb-tables/index.js.map +1 -1
- package/dist/components/cfpb-tooltips/index.css +1 -1
- package/dist/components/cfpb-tooltips/index.css.map +2 -2
- package/dist/components/cfpb-tooltips/index.js +1 -1
- package/dist/components/cfpb-tooltips/index.js.map +1 -1
- package/dist/components/cfpb-typography/index.css +1 -1
- package/dist/components/cfpb-typography/index.css.map +2 -2
- package/dist/components/cfpb-typography/index.js +1 -1
- package/dist/components/cfpb-typography/index.js.map +1 -1
- package/dist/elements/abstracts/index.js +2 -0
- package/dist/elements/abstracts/index.js.map +7 -0
- package/dist/elements/base/index.css +3 -0
- package/dist/elements/base/index.css.map +7 -0
- package/dist/elements/base/index.js +2 -0
- package/dist/elements/base/index.js.map +7 -0
- package/dist/elements/cfpb-button/index.js +4 -4
- package/dist/elements/cfpb-button/index.js.map +2 -2
- package/dist/elements/cfpb-checkbox-icon/index.js +3 -3
- package/dist/elements/cfpb-checkbox-icon/index.js.map +2 -2
- package/dist/elements/cfpb-expandable/index.js +4 -4
- package/dist/elements/cfpb-expandable/index.js.map +2 -2
- 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 +3 -3
- package/dist/elements/cfpb-form-alert/index.js.map +2 -2
- package/dist/elements/cfpb-form-choice/index.js +3 -3
- package/dist/elements/cfpb-form-choice/index.js.map +2 -2
- package/dist/elements/cfpb-form-search/index.js +3 -3
- package/dist/elements/cfpb-form-search/index.js.map +3 -3
- package/dist/elements/cfpb-form-search-input/index.js +3 -3
- package/dist/elements/cfpb-form-search-input/index.js.map +3 -3
- package/dist/elements/cfpb-icon-text/index.js +1 -1
- package/dist/elements/cfpb-icon-text/index.js.map +2 -2
- package/dist/elements/cfpb-label/index.js +1 -1
- package/dist/elements/cfpb-label/index.js.map +2 -2
- package/dist/elements/cfpb-list/index.js +3 -3
- package/dist/elements/cfpb-list/index.js.map +3 -3
- package/dist/elements/cfpb-list-item/index.js +3 -3
- package/dist/elements/cfpb-list-item/index.js.map +2 -2
- 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.js +4 -4
- package/dist/elements/cfpb-select/index.js.map +4 -4
- package/dist/elements/cfpb-tag-filter/index.js +2 -2
- package/dist/elements/cfpb-tag-filter/index.js.map +2 -2
- package/dist/elements/cfpb-tag-group/index.js +1 -1
- package/dist/elements/cfpb-tag-group/index.js.map +2 -2
- package/dist/elements/cfpb-tag-topic/index.js +3 -3
- package/dist/elements/cfpb-tag-topic/index.js.map +2 -2
- package/dist/elements/index.js +7 -7
- package/dist/elements/index.js.map +4 -4
- package/dist/index.css +1 -1
- package/dist/index.css.map +3 -3
- package/dist/index.js +7 -7
- package/dist/index.js.map +4 -4
- package/dist/utilities/index.css +1 -1
- package/dist/utilities/index.css.map +2 -2
- package/dist/utilities/index.js +1 -1
- package/dist/utilities/index.js.map +2 -2
- package/package.json +1 -1
- package/src/components/cfpb-buttons/button-group.scss +1 -1
- package/src/components/cfpb-buttons/button-link.scss +10 -54
- package/src/components/cfpb-buttons/button.scss +3 -3
- package/src/components/cfpb-buttons/vars.scss +1 -1
- package/src/components/cfpb-expandables/expandable-group.scss +1 -1
- package/src/components/cfpb-expandables/expandable.scss +1 -1
- package/src/components/cfpb-expandables/summary.scss +1 -1
- package/src/components/cfpb-forms/form-alert.scss +1 -1
- package/src/components/cfpb-forms/form-field.scss +6 -6
- package/src/components/cfpb-forms/form.scss +1 -1
- package/src/components/cfpb-forms/label.scss +2 -2
- package/src/components/cfpb-forms/multiselect.scss +1 -1
- package/src/components/cfpb-forms/range.scss +7 -7
- package/src/components/cfpb-forms/search-input.scss +1 -1
- package/src/components/cfpb-forms/select.scss +1 -1
- package/src/components/cfpb-forms/tag.scss +1 -1
- package/src/components/cfpb-forms/text-input.scss +1 -1
- package/src/components/cfpb-icons/icon.scss +1 -1
- package/src/components/cfpb-layout/card-group.scss +1 -1
- package/src/components/cfpb-layout/card.scss +1 -1
- package/src/components/cfpb-layout/email-signup.scss +1 -1
- package/src/components/cfpb-layout/featured-content-module.scss +1 -1
- package/src/components/cfpb-layout/hero.scss +1 -1
- package/src/components/cfpb-layout/layout.scss +9 -9
- package/src/components/cfpb-layout/well.scss +1 -1
- package/src/components/cfpb-notifications/banner.scss +1 -1
- package/src/components/cfpb-notifications/notification.scss +1 -1
- package/src/components/cfpb-pagination/pagination.scss +1 -1
- package/src/components/cfpb-tables/table.scss +1 -1
- package/src/components/cfpb-tooltips/tooltip.scss +1 -1
- package/src/components/cfpb-typography/date.scss +1 -1
- package/src/components/cfpb-typography/list.scss +1 -1
- package/src/components/cfpb-typography/meta-header.scss +1 -1
- package/src/components/cfpb-typography/mixins.scss +1 -1
- package/src/components/cfpb-typography/pull-quote.scss +1 -1
- package/src/components/cfpb-typography/slug-header.scss +1 -1
- package/src/components/cfpb-typography/tagline.scss +1 -1
- package/src/elements/abstracts/custom-props.css +1 -1
- package/src/elements/abstracts/sizing-vars.scss +2 -1
- package/src/elements/abstracts/vars.css +1 -1
- package/src/elements/cfpb-button/cfpb-button-group.scss +8 -6
- package/src/elements/cfpb-button/cfpb-button-link.scss +54 -47
- package/src/elements/cfpb-button/cfpb-button.scss +168 -172
- package/src/elements/cfpb-button/index.js +16 -1
- package/src/elements/cfpb-button/vars.css +1 -1
- package/src/elements/cfpb-expandable/cfpb-expandable.component.scss +2 -2
- package/src/elements/cfpb-file-upload/index.js +9 -9
- package/src/elements/cfpb-form-search-input/index.js +4 -0
- package/src/elements/cfpb-icon-text/cfpb-icon-text.component.scss +2 -1
- package/src/elements/cfpb-list/cfpb-list.component.scss +19 -8
- package/src/elements/cfpb-list/index.js +62 -40
- package/src/elements/cfpb-list/index.spec.js +66 -21
- package/src/elements/cfpb-select/cfpb-select.component.scss +2 -2
- package/src/elements/cfpb-select/index.js +60 -70
- package/src/elements/cfpb-select/multiple-select-event-proxy.js +88 -0
- package/src/elements/cfpb-select/single-select-event-proxy.js +47 -0
- package/src/elements/cfpb-tag-topic/cfpb-tag-topic.component.scss +2 -2
- package/src/index.js +2 -2
- package/src/index.scss +3 -2
- package/src/utilities/breakpoint-state.js +1 -1
- package/src/utilities/utilities.scss +1 -1
- package/src/abstracts/custom-props.scss +0 -175
- package/src/abstracts/grid-mixins.scss +0 -82
- package/src/abstracts/heading-mixins.scss +0 -345
- package/src/abstracts/index.scss +0 -6
- package/src/abstracts/media-queries.scss +0 -35
- package/src/abstracts/vars-breakpoints.scss +0 -16
- package/src/abstracts/vars.scss +0 -184
- package/src/base/base.scss +0 -375
- package/src/base/font.scss +0 -27
- package/src/base/index.scss +0 -3
- package/src/base/normalize.scss +0 -290
- /package/src/{abstracts → elements/abstracts}/index.js +0 -0
- /package/src/{abstracts → elements/abstracts}/vars-breakpoints.js +0 -0
- /package/src/{base → elements/base}/index.js +0 -0
|
@@ -9,12 +9,14 @@ export class CfpbList extends LitElement {
|
|
|
9
9
|
${unsafeCSS(styles)}
|
|
10
10
|
`;
|
|
11
11
|
|
|
12
|
+
#internalSync = false;
|
|
12
13
|
#container = createRef();
|
|
13
14
|
#items = [];
|
|
14
15
|
#checkedItems = [];
|
|
15
16
|
#visibleItems = [];
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
// index in visibleItems
|
|
19
|
+
#focusedIndex = -1;
|
|
18
20
|
|
|
19
21
|
// WeakMap to store per-item click listeners.
|
|
20
22
|
#clickListeners = new WeakMap();
|
|
@@ -50,7 +52,6 @@ export class CfpbList extends LitElement {
|
|
|
50
52
|
const parsed = parseChildData(this.childData);
|
|
51
53
|
if (parsed) this.#renderItemsFromData(parsed);
|
|
52
54
|
}
|
|
53
|
-
|
|
54
55
|
if (changedProps.has('type')) {
|
|
55
56
|
this.#applyTypeToItems();
|
|
56
57
|
}
|
|
@@ -62,15 +63,12 @@ export class CfpbList extends LitElement {
|
|
|
62
63
|
get items() {
|
|
63
64
|
return this.#items;
|
|
64
65
|
}
|
|
65
|
-
|
|
66
66
|
get checkedItems() {
|
|
67
67
|
return this.#checkedItems;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
69
|
get visibleItems() {
|
|
71
70
|
return this.#visibleItems;
|
|
72
71
|
}
|
|
73
|
-
|
|
74
72
|
get visibleCheckedItems() {
|
|
75
73
|
return this.#visibleItems.filter((item) => item.checked);
|
|
76
74
|
}
|
|
@@ -79,13 +77,12 @@ export class CfpbList extends LitElement {
|
|
|
79
77
|
// RENDER ITEMS
|
|
80
78
|
// -------------------------
|
|
81
79
|
#renderItemsFromData(itemsArray) {
|
|
82
|
-
// Remove all children except <template> and <noscript>.
|
|
83
80
|
[...this.children].forEach((child) => {
|
|
84
81
|
if (child.tagName !== 'TEMPLATE' && child.tagName !== 'NOSCRIPT')
|
|
85
82
|
child.remove();
|
|
86
83
|
});
|
|
87
84
|
|
|
88
|
-
let firstChecked;
|
|
85
|
+
let firstChecked = null;
|
|
89
86
|
itemsArray.forEach((data) => {
|
|
90
87
|
const item = document.createElement('cfpb-list-item');
|
|
91
88
|
item.textContent = data.value ?? '';
|
|
@@ -93,11 +90,12 @@ export class CfpbList extends LitElement {
|
|
|
93
90
|
if ('hidden' in data) item.hidden = data.hidden;
|
|
94
91
|
if ('href' in data) item.href = data.href;
|
|
95
92
|
item.type = data.type ?? this.type;
|
|
93
|
+
|
|
96
94
|
if (this.multiple) {
|
|
97
95
|
if ('checked' in data) item.checked = data.checked;
|
|
98
96
|
} else if (!firstChecked && data.checked) {
|
|
99
|
-
firstChecked =
|
|
100
|
-
|
|
97
|
+
firstChecked = item;
|
|
98
|
+
item.checked = true;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
this.appendChild(item);
|
|
@@ -136,7 +134,7 @@ export class CfpbList extends LitElement {
|
|
|
136
134
|
|
|
137
135
|
// Assign tabindex, role, listeners.
|
|
138
136
|
this.#items.forEach((item, index) => {
|
|
139
|
-
item.setAttribute('tabindex',
|
|
137
|
+
item.setAttribute('tabindex', '-1');
|
|
140
138
|
item.setAttribute('role', 'option');
|
|
141
139
|
|
|
142
140
|
// Remove prior listener if present.
|
|
@@ -147,7 +145,6 @@ export class CfpbList extends LitElement {
|
|
|
147
145
|
const listener = (evt) => {
|
|
148
146
|
// Prevent actual click bubbling to list container.
|
|
149
147
|
evt.stopPropagation();
|
|
150
|
-
|
|
151
148
|
this.#handleToggle(item, item.checked, index);
|
|
152
149
|
};
|
|
153
150
|
|
|
@@ -156,7 +153,8 @@ export class CfpbList extends LitElement {
|
|
|
156
153
|
|
|
157
154
|
// Track focus index.
|
|
158
155
|
item.addEventListener('focus', () => {
|
|
159
|
-
|
|
156
|
+
const visIndex = this.#visibleItems.indexOf(item);
|
|
157
|
+
if (visIndex !== -1) this.#focusedIndex = visIndex;
|
|
160
158
|
});
|
|
161
159
|
});
|
|
162
160
|
|
|
@@ -181,7 +179,6 @@ export class CfpbList extends LitElement {
|
|
|
181
179
|
checked: item.checked,
|
|
182
180
|
disabled: item.disabled,
|
|
183
181
|
}));
|
|
184
|
-
|
|
185
182
|
this.#internalSync = true;
|
|
186
183
|
this.childData = data;
|
|
187
184
|
this.#internalSync = false;
|
|
@@ -199,9 +196,8 @@ export class CfpbList extends LitElement {
|
|
|
199
196
|
if (this.multiple) {
|
|
200
197
|
if (isChecked) {
|
|
201
198
|
// Add if not already present.
|
|
202
|
-
if (!this.#checkedItems.includes(element))
|
|
199
|
+
if (!this.#checkedItems.includes(element))
|
|
203
200
|
this.#checkedItems.push(element);
|
|
204
|
-
}
|
|
205
201
|
} else {
|
|
206
202
|
// Remove cleanly.
|
|
207
203
|
this.#checkedItems = this.#checkedItems.filter(
|
|
@@ -224,6 +220,11 @@ export class CfpbList extends LitElement {
|
|
|
224
220
|
|
|
225
221
|
this.#syncChildDataFromItems();
|
|
226
222
|
|
|
223
|
+
window.queueMicrotask(() => {
|
|
224
|
+
const visIndex = this.#visibleItems.indexOf(element);
|
|
225
|
+
this.focusItemAt(visIndex !== -1 ? visIndex : -1);
|
|
226
|
+
});
|
|
227
|
+
|
|
227
228
|
this.dispatchEvent(
|
|
228
229
|
new CustomEvent('item-click', {
|
|
229
230
|
detail: { index, value: element.value, element },
|
|
@@ -242,31 +243,31 @@ export class CfpbList extends LitElement {
|
|
|
242
243
|
* @returns {Array} List of visible list items.
|
|
243
244
|
*/
|
|
244
245
|
filterItems(queryList) {
|
|
245
|
-
let firstIndex = 0;
|
|
246
246
|
this.#visibleItems = [];
|
|
247
|
+
let firstVisibleIndex = -1;
|
|
247
248
|
|
|
248
|
-
this
|
|
249
|
-
const valueLower = item.value.toLowerCase();
|
|
249
|
+
this.#items.forEach((item) => {
|
|
250
250
|
const matches = queryList.some((q) =>
|
|
251
|
-
|
|
251
|
+
item.value.toLowerCase().includes(q.toLowerCase()),
|
|
252
252
|
);
|
|
253
|
-
if (!matches) firstIndex++;
|
|
254
|
-
else this.#visibleItems.push(item);
|
|
255
253
|
item.hidden = !matches;
|
|
254
|
+
if (matches) {
|
|
255
|
+
if (firstVisibleIndex === -1)
|
|
256
|
+
firstVisibleIndex = this.#visibleItems.length;
|
|
257
|
+
this.#visibleItems.push(item);
|
|
258
|
+
}
|
|
256
259
|
});
|
|
257
260
|
|
|
258
|
-
this.#focusedIndex =
|
|
259
|
-
|
|
261
|
+
this.#focusedIndex = firstVisibleIndex;
|
|
260
262
|
this.#broadcastFiltered();
|
|
261
263
|
|
|
262
264
|
return this.#visibleItems;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
showAllItems() {
|
|
266
|
-
this
|
|
268
|
+
this.#items.forEach((item) => (item.hidden = false));
|
|
269
|
+
this.#visibleItems = [...this.#items];
|
|
267
270
|
this.#focusedIndex = 0;
|
|
268
|
-
this.#visibleItems = this.#items;
|
|
269
|
-
|
|
270
271
|
this.#broadcastFiltered();
|
|
271
272
|
}
|
|
272
273
|
|
|
@@ -285,41 +286,58 @@ export class CfpbList extends LitElement {
|
|
|
285
286
|
);
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
#focusContainer() {
|
|
290
|
+
this.#container.value.focus();
|
|
291
|
+
this.#focusedIndex = -1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Focus a visible item by index.
|
|
296
|
+
* Pass -1 to move focus to the list container (no active item).
|
|
297
|
+
* @param {number} index - The index of the item to focus.
|
|
298
|
+
* @returns {undefined} If nothing to focus.
|
|
299
|
+
*/
|
|
288
300
|
focusItemAt(index) {
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
// No active item (sentinel or invalid input).
|
|
302
|
+
const visibleItems = this.#visibleItems;
|
|
303
|
+
if (
|
|
304
|
+
!visibleItems.length ||
|
|
305
|
+
index == null ||
|
|
306
|
+
typeof index !== 'number' ||
|
|
307
|
+
Number.isNaN(index) ||
|
|
308
|
+
index === -1
|
|
309
|
+
) {
|
|
310
|
+
this.#focusContainer();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
291
313
|
|
|
292
314
|
const normalizedIndex =
|
|
293
315
|
((index % visibleItems.length) + visibleItems.length) %
|
|
294
316
|
visibleItems.length;
|
|
295
|
-
|
|
296
|
-
item.focus();
|
|
317
|
+
visibleItems[normalizedIndex].focus();
|
|
297
318
|
this.#focusedIndex = normalizedIndex;
|
|
298
319
|
}
|
|
299
320
|
|
|
300
321
|
#onFocus(evt) {
|
|
301
322
|
// If the focus is on the container itself (not an item), set index to -1.
|
|
302
|
-
if (evt.target === this.#container.value)
|
|
303
|
-
this.#focusedIndex = -1;
|
|
304
|
-
}
|
|
323
|
+
if (evt.target === this.#container.value) this.#focusContainer();
|
|
305
324
|
}
|
|
306
325
|
|
|
307
|
-
// -------------------------
|
|
308
|
-
// KEYBOARD NAVIGATION
|
|
309
|
-
// -------------------------
|
|
310
326
|
#onKeyDown(evt) {
|
|
311
|
-
const visibleItems = this
|
|
327
|
+
const visibleItems = this.#visibleItems;
|
|
312
328
|
if (!visibleItems.length) return;
|
|
313
329
|
const last = visibleItems.length - 1;
|
|
314
330
|
|
|
315
331
|
switch (evt.key) {
|
|
316
332
|
case 'ArrowDown':
|
|
317
333
|
evt.preventDefault();
|
|
318
|
-
this.focusItemAt(this.#focusedIndex + 1);
|
|
334
|
+
this.focusItemAt(this.#focusedIndex < 0 ? 0 : this.#focusedIndex + 1);
|
|
319
335
|
break;
|
|
320
336
|
case 'ArrowUp':
|
|
321
337
|
evt.preventDefault();
|
|
322
|
-
this.focusItemAt(
|
|
338
|
+
this.focusItemAt(
|
|
339
|
+
this.#focusedIndex < 0 ? last : this.#focusedIndex - 1,
|
|
340
|
+
);
|
|
323
341
|
break;
|
|
324
342
|
case 'Home':
|
|
325
343
|
evt.preventDefault();
|
|
@@ -332,6 +350,10 @@ export class CfpbList extends LitElement {
|
|
|
332
350
|
}
|
|
333
351
|
}
|
|
334
352
|
|
|
353
|
+
get focusedIndex() {
|
|
354
|
+
return this.#focusedIndex;
|
|
355
|
+
}
|
|
356
|
+
|
|
335
357
|
render() {
|
|
336
358
|
return html`
|
|
337
359
|
<div
|
|
@@ -27,7 +27,6 @@ describe('<cfpb-list> tests', () => {
|
|
|
27
27
|
await list.updateComplete;
|
|
28
28
|
|
|
29
29
|
expect(list.items.length).toBe(3);
|
|
30
|
-
// Only first checked stays true in single select
|
|
31
30
|
expect(list.items[0].checked).toBe(true);
|
|
32
31
|
expect(list.items[1].checked).toBe(false);
|
|
33
32
|
expect(list.items[2].checked).toBe(false);
|
|
@@ -48,7 +47,6 @@ describe('<cfpb-list> tests', () => {
|
|
|
48
47
|
expect(list.checkedItems).toContain(list.items[0]);
|
|
49
48
|
expect(list.checkedItems).toContain(list.items[1]);
|
|
50
49
|
|
|
51
|
-
// Uncheck an item
|
|
52
50
|
const event = new CustomEvent('click-item', {
|
|
53
51
|
bubbles: true,
|
|
54
52
|
composed: true,
|
|
@@ -61,13 +59,11 @@ describe('<cfpb-list> tests', () => {
|
|
|
61
59
|
|
|
62
60
|
test('click-item toggles single selection', async () => {
|
|
63
61
|
list.childData = JSON.stringify([{ value: 'A' }, { value: 'B' }]);
|
|
64
|
-
|
|
65
62
|
await list.updateComplete;
|
|
66
63
|
|
|
67
64
|
const clickSpy = jest.fn();
|
|
68
65
|
list.addEventListener('item-click', clickSpy);
|
|
69
66
|
|
|
70
|
-
// Click first item
|
|
71
67
|
list.items[0].checked = true;
|
|
72
68
|
list.items[0].dispatchEvent(
|
|
73
69
|
new CustomEvent('click-item', { bubbles: true, composed: true }),
|
|
@@ -76,7 +72,6 @@ describe('<cfpb-list> tests', () => {
|
|
|
76
72
|
expect(list.checkedItems).toEqual([list.items[0]]);
|
|
77
73
|
expect(clickSpy).toHaveBeenCalledTimes(1);
|
|
78
74
|
|
|
79
|
-
// Click again to uncheck
|
|
80
75
|
list.items[0].checked = false;
|
|
81
76
|
list.items[0].dispatchEvent(
|
|
82
77
|
new CustomEvent('click-item', { bubbles: true, composed: true }),
|
|
@@ -91,7 +86,6 @@ describe('<cfpb-list> tests', () => {
|
|
|
91
86
|
const listenerSpy = jest.fn();
|
|
92
87
|
list.addEventListener('item-click', listenerSpy);
|
|
93
88
|
|
|
94
|
-
// Trigger click-item event
|
|
95
89
|
const event = new CustomEvent('click-item', {
|
|
96
90
|
bubbles: true,
|
|
97
91
|
composed: true,
|
|
@@ -127,24 +121,27 @@ describe('<cfpb-list> tests', () => {
|
|
|
127
121
|
|
|
128
122
|
list.filterItems(['C']); // only C visible
|
|
129
123
|
|
|
130
|
-
list.
|
|
131
|
-
|
|
124
|
+
const container = list.shadowRoot.querySelector('div');
|
|
125
|
+
container.focus();
|
|
126
|
+
expect(document.activeElement.tagName).toBe('CFPB-LIST');
|
|
132
127
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
|
|
138
|
-
);
|
|
128
|
+
// ArrowDown → first visible
|
|
129
|
+
container.dispatchEvent(
|
|
130
|
+
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
|
|
131
|
+
);
|
|
139
132
|
expect(document.activeElement.value).toBe('C');
|
|
140
133
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }),
|
|
146
|
-
);
|
|
134
|
+
// ArrowDown → wrap
|
|
135
|
+
container.dispatchEvent(
|
|
136
|
+
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
|
|
137
|
+
);
|
|
147
138
|
expect(document.activeElement.value).toBe('C');
|
|
139
|
+
|
|
140
|
+
// ArrowUp → wrap
|
|
141
|
+
container.dispatchEvent(
|
|
142
|
+
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }),
|
|
143
|
+
);
|
|
144
|
+
expect(document.activeElement.tagName).toBe('CFPB-LIST');
|
|
148
145
|
});
|
|
149
146
|
|
|
150
147
|
test('showAllItems unhides all items', async () => {
|
|
@@ -163,7 +160,55 @@ describe('<cfpb-list> tests', () => {
|
|
|
163
160
|
console.error = jest.fn();
|
|
164
161
|
list.childData = 'not-json';
|
|
165
162
|
await list.updateComplete;
|
|
166
|
-
|
|
167
163
|
expect(console.error).toHaveBeenCalled();
|
|
168
164
|
});
|
|
165
|
+
|
|
166
|
+
// -------------------------------
|
|
167
|
+
// focusItemAt sentinel tests
|
|
168
|
+
// -------------------------------
|
|
169
|
+
|
|
170
|
+
test('focusItemAt(-1) focuses the container', async () => {
|
|
171
|
+
list.childData = JSON.stringify([{ value: 'A' }, { value: 'B' }]);
|
|
172
|
+
await list.updateComplete;
|
|
173
|
+
list.focusItemAt(-1);
|
|
174
|
+
expect(document.activeElement.tagName).toBe('CFPB-LIST');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('focusItemAt(null) focuses the container', async () => {
|
|
178
|
+
list.childData = JSON.stringify([{ value: 'A' }]);
|
|
179
|
+
await list.updateComplete;
|
|
180
|
+
list.focusItemAt(null);
|
|
181
|
+
expect(document.activeElement.tagName).toBe('CFPB-LIST');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('focusItemAt(undefined) focuses the container', async () => {
|
|
185
|
+
list.childData = JSON.stringify([{ value: 'A' }]);
|
|
186
|
+
await list.updateComplete;
|
|
187
|
+
list.focusItemAt(undefined);
|
|
188
|
+
expect(document.activeElement.tagName).toBe('CFPB-LIST');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('ArrowDown from container focuses first visible item', async () => {
|
|
192
|
+
list.childData = JSON.stringify([{ value: 'A' }, { value: 'B' }]);
|
|
193
|
+
await list.updateComplete;
|
|
194
|
+
|
|
195
|
+
const container = list.shadowRoot.querySelector('div');
|
|
196
|
+
container.focus();
|
|
197
|
+
container.dispatchEvent(
|
|
198
|
+
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
|
|
199
|
+
);
|
|
200
|
+
expect(document.activeElement.value).toBe('A');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('ArrowUp from container focuses last visible item', async () => {
|
|
204
|
+
list.childData = JSON.stringify([{ value: 'A' }, { value: 'B' }]);
|
|
205
|
+
await list.updateComplete;
|
|
206
|
+
|
|
207
|
+
const container = list.shadowRoot.querySelector('div');
|
|
208
|
+
container.focus();
|
|
209
|
+
container.dispatchEvent(
|
|
210
|
+
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }),
|
|
211
|
+
);
|
|
212
|
+
expect(document.activeElement.value).toBe('B');
|
|
213
|
+
});
|
|
169
214
|
});
|
|
@@ -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/base' as *;
|
|
2
|
+
@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *;
|
|
3
|
+
@use '@cfpb/cfpb-design-system/src/elements/base' as *;
|
|
4
4
|
@use '@cfpb/cfpb-design-system/src/utilities' as *;
|
|
5
5
|
@use '@cfpb/cfpb-design-system/src/components/cfpb-icons/icon';
|
|
6
6
|
@use '../cfpb-utilities/transition/transition.scss' as *;
|
|
@@ -11,17 +11,20 @@ import { FlyoutMenu } from '../../utilities/behavior/flyout-menu';
|
|
|
11
11
|
import { CfpbList } from '../cfpb-list';
|
|
12
12
|
import { CfpbTagGroup } from '../cfpb-tag-group';
|
|
13
13
|
|
|
14
|
+
import { SingleSelectEventProxy } from './single-select-event-proxy.js';
|
|
15
|
+
import { MultipleSelectEventProxy } from './multiple-select-event-proxy.js';
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
*
|
|
16
|
-
* @element cfpb-
|
|
17
|
-
* @slot - The main content for the
|
|
19
|
+
* @element cfpb-select
|
|
20
|
+
* @slot - The main content for the select.
|
|
18
21
|
*/
|
|
19
22
|
export class CfpbSelect extends LitElement {
|
|
20
23
|
static styles = css`
|
|
21
24
|
${unsafeCSS(styles)}
|
|
22
25
|
`;
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
#eventProxy;
|
|
25
28
|
#flyoutMenu;
|
|
26
29
|
#transition;
|
|
27
30
|
#search;
|
|
@@ -32,7 +35,7 @@ export class CfpbSelect extends LitElement {
|
|
|
32
35
|
#tagGroup = createRef();
|
|
33
36
|
#list = createRef();
|
|
34
37
|
#displayLabel = createRef();
|
|
35
|
-
#
|
|
38
|
+
#boundOnOutsideFocus;
|
|
36
39
|
#noResults = false;
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -64,23 +67,33 @@ export class CfpbSelect extends LitElement {
|
|
|
64
67
|
constructor() {
|
|
65
68
|
super();
|
|
66
69
|
|
|
70
|
+
this.multiple = false;
|
|
67
71
|
this.options = [];
|
|
68
72
|
this.selectedTexts = [];
|
|
69
73
|
this.optionList = [];
|
|
70
74
|
|
|
71
|
-
this.#
|
|
75
|
+
this.#boundOnOutsideFocus = this.#onFocusOutside.bind(this);
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
firstUpdated() {
|
|
75
79
|
this.#initFlyoutMenu();
|
|
80
|
+
|
|
81
|
+
this.addEventListener('focus', () => {
|
|
82
|
+
this.#eventProxy.onFocus();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.addEventListener('keydown', (evt) => {
|
|
86
|
+
this.#eventProxy.onKeyDown(evt, this);
|
|
87
|
+
});
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
disconnectedCallback() {
|
|
79
|
-
document.removeEventListener('pointerdown', this.#
|
|
91
|
+
document.removeEventListener('pointerdown', this.#boundOnOutsideFocus);
|
|
92
|
+
document.removeEventListener('focusin', this.#boundOnOutsideFocus);
|
|
80
93
|
super.disconnectedCallback();
|
|
81
94
|
}
|
|
82
95
|
|
|
83
|
-
#
|
|
96
|
+
#onFocusOutside(evt) {
|
|
84
97
|
const path = evt.composedPath();
|
|
85
98
|
if (!path.includes(this)) {
|
|
86
99
|
this.isExpanded = false;
|
|
@@ -104,14 +117,20 @@ export class CfpbSelect extends LitElement {
|
|
|
104
117
|
|
|
105
118
|
// Extract list items (with their text or link info)
|
|
106
119
|
const items = [...list[0].querySelectorAll('li')].map((li) => {
|
|
107
|
-
const checked =
|
|
120
|
+
const checked =
|
|
121
|
+
li.hasAttribute('data-checked') || li.hasAttribute('checked');
|
|
122
|
+
|
|
123
|
+
const itemValue = li.textContent.trim();
|
|
124
|
+
|
|
108
125
|
if (checked) {
|
|
126
|
+
if (!this.multiple) this.#displayLabel.value.textContent = itemValue;
|
|
109
127
|
return {
|
|
110
|
-
value:
|
|
128
|
+
value: itemValue,
|
|
111
129
|
checked: 'true',
|
|
112
130
|
};
|
|
113
131
|
}
|
|
114
|
-
|
|
132
|
+
|
|
133
|
+
return { value: itemValue };
|
|
115
134
|
});
|
|
116
135
|
|
|
117
136
|
this.optionList = items;
|
|
@@ -192,6 +211,10 @@ export class CfpbSelect extends LitElement {
|
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
updated(changedProps) {
|
|
214
|
+
if (changedProps.has('multiple')) {
|
|
215
|
+
this.#eventProxy = this.#createEventProxy();
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
if (changedProps.has('isExpanded')) {
|
|
196
219
|
const oldVal = changedProps.get('isExpanded');
|
|
197
220
|
const newVal = this.isExpanded;
|
|
@@ -199,81 +222,49 @@ export class CfpbSelect extends LitElement {
|
|
|
199
222
|
if (newVal !== oldVal) {
|
|
200
223
|
if (newVal) {
|
|
201
224
|
this.#flyoutMenu.expand();
|
|
202
|
-
document.addEventListener('pointerdown', this.#
|
|
225
|
+
document.addEventListener('pointerdown', this.#boundOnOutsideFocus);
|
|
226
|
+
document.addEventListener('focusin', this.#boundOnOutsideFocus);
|
|
203
227
|
} else {
|
|
204
228
|
this.#flyoutMenu.collapse();
|
|
205
229
|
document.removeEventListener(
|
|
206
230
|
'pointerdown',
|
|
207
|
-
this.#
|
|
231
|
+
this.#boundOnOutsideFocus,
|
|
208
232
|
);
|
|
233
|
+
document.removeEventListener('focusin', this.#boundOnOutsideFocus);
|
|
209
234
|
}
|
|
210
235
|
}
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
238
|
|
|
214
|
-
#
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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;
|
|
239
|
+
#createEventProxy() {
|
|
240
|
+
const common = {
|
|
241
|
+
list: this.#list.value,
|
|
242
|
+
flyout: () => this.#flyoutMenu,
|
|
243
|
+
};
|
|
250
244
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
245
|
+
return this.multiple
|
|
246
|
+
? new MultipleSelectEventProxy({
|
|
247
|
+
...common,
|
|
248
|
+
input: this.#input.value,
|
|
249
|
+
tagGroup: this.#tagGroup.value,
|
|
250
|
+
})
|
|
251
|
+
: new SingleSelectEventProxy({
|
|
252
|
+
...common,
|
|
253
|
+
displayLabel: this.#displayLabel.value,
|
|
254
|
+
header: this.#headerDom.value,
|
|
255
|
+
});
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
this.optionList = this.optionList.map((item) => ({
|
|
263
|
-
...item,
|
|
264
|
-
checked: !!tagList.find((tag) => tag.value === item.value),
|
|
265
|
-
}));
|
|
258
|
+
#onClick(evt) {
|
|
259
|
+
this.#eventProxy?.onClick(evt, this);
|
|
260
|
+
}
|
|
266
261
|
|
|
267
|
-
|
|
262
|
+
#onItemClick(evt) {
|
|
263
|
+
this.#eventProxy?.onItemClick(evt, this);
|
|
268
264
|
}
|
|
269
265
|
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
case 'ArrowDown':
|
|
273
|
-
evt.preventDefault();
|
|
274
|
-
this.#contentDom.value.querySelector('cfpb-list-item').focus();
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
266
|
+
#onTagClick(evt) {
|
|
267
|
+
this.#eventProxy?.onTagClick(evt, this);
|
|
277
268
|
}
|
|
278
269
|
|
|
279
270
|
render() {
|
|
@@ -308,7 +299,6 @@ export class CfpbSelect extends LitElement {
|
|
|
308
299
|
title="Expand content"
|
|
309
300
|
data-js-hook="behavior_flyout-menu_trigger"
|
|
310
301
|
${ref(this.#headerDom)}
|
|
311
|
-
@keydown=${this.#onKeyDown}
|
|
312
302
|
>
|
|
313
303
|
<span class="o-select__cues" @click=${this.#onClick}>
|
|
314
304
|
<span class="o-select__cue-open" role="img" aria-label="Show">
|