@cfpb/cfpb-design-system 4.0.4 → 4.1.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 +25 -1
- package/dist/base/index.css +1 -1
- package/dist/base/index.css.map +2 -2
- package/dist/base/index.js +1 -1
- package/dist/base/index.js.map +1 -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 +1 -1
- 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 +1 -1
- 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/cfpb-button/index.js +12 -4
- package/dist/elements/cfpb-button/index.js.map +4 -4
- package/dist/elements/cfpb-file-upload/index.js +11 -4
- package/dist/elements/cfpb-file-upload/index.js.map +4 -4
- package/dist/elements/cfpb-form-choice/index.js +11 -3
- package/dist/elements/cfpb-form-choice/index.js.map +4 -4
- package/dist/elements/cfpb-label/index.js +36 -0
- package/dist/elements/cfpb-label/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-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 +2 -2
- 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 +14 -5
- package/dist/elements/index.js.map +4 -4
- package/dist/index.css +1 -1
- package/dist/index.css.map +2 -2
- package/dist/index.js +14 -5
- 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 +1 -1
- package/package.json +1 -1
- package/src/abstracts/heading-mixins.scss +6 -0
- package/src/abstracts/vars.scss +23 -0
- package/src/base/base.scss +1 -1
- package/src/elements/cfpb-button/cfpb-button.component.scss +8 -0
- package/src/elements/cfpb-button/index.js +100 -17
- package/src/elements/cfpb-file-upload/index.js +1 -1
- package/src/elements/cfpb-form-choice/cfpb-form-choice.component.scss +6 -1
- package/src/elements/cfpb-form-choice/index.js +62 -29
- package/src/elements/cfpb-form-choice/index.spec.js +47 -0
- package/src/elements/cfpb-label/cfpb-label.component.scss +36 -0
- package/src/elements/cfpb-label/index.js +61 -0
- package/src/elements/cfpb-multiselect/cfpb-multiselect.component.scss +225 -0
- package/src/elements/cfpb-multiselect/index.js +444 -0
- package/src/elements/cfpb-multiselect/multiselect-model.js +288 -0
- package/src/elements/cfpb-multiselect/multiselect-model.spec.js +236 -0
- package/src/elements/cfpb-tag-filter/index.js +1 -1
- package/src/elements/cfpb-tag-filter/index.spec.js +1 -1
- package/src/elements/cfpb-tag-group/index.js +2 -1
- package/src/elements/cfpb-tag-topic/index.js +6 -0
- package/src/elements/index.js +2 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @element cfpb-multiselect.
|
|
4
|
+
* @slot - The main content for the upload button.
|
|
5
|
+
*/
|
|
6
|
+
export class MultiselectModel extends EventTarget {
|
|
7
|
+
// Declare private properties.
|
|
8
|
+
#options;
|
|
9
|
+
#name;
|
|
10
|
+
#max;
|
|
11
|
+
#optionsData;
|
|
12
|
+
#selectedIndices;
|
|
13
|
+
#filterIndicesList;
|
|
14
|
+
#lastFilterIndicesList;
|
|
15
|
+
#index;
|
|
16
|
+
|
|
17
|
+
// Undefined return value for void methods.
|
|
18
|
+
#UNDEFINED;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @class
|
|
22
|
+
* MultiselectModel
|
|
23
|
+
* @param {HTMLOptionsCollection} options
|
|
24
|
+
* Set of options from a <select> element.
|
|
25
|
+
* @param {string} name - a unique name for this multiselect.
|
|
26
|
+
* @param {object} config - Customization of Multiselect behavior
|
|
27
|
+
*/
|
|
28
|
+
constructor(options, name, config) {
|
|
29
|
+
super();
|
|
30
|
+
|
|
31
|
+
this.#options = options;
|
|
32
|
+
this.#name = name;
|
|
33
|
+
this.#max = config?.maxSelections || MultiselectModel.MAX_SELECTIONS;
|
|
34
|
+
this.#optionsData = [];
|
|
35
|
+
|
|
36
|
+
this.#selectedIndices = [];
|
|
37
|
+
this.#filterIndicesList = [];
|
|
38
|
+
|
|
39
|
+
/* When the options list is filtered, we store a list of filtered indices
|
|
40
|
+
so that when the filter changes we can reset the last matched options. */
|
|
41
|
+
this.#lastFilterIndicesList = [];
|
|
42
|
+
|
|
43
|
+
// Which option is in focus. -1 means the focus is on the search input.
|
|
44
|
+
this.#index = -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {HTMLElement} item - An option HTML node.
|
|
49
|
+
* @returns {string} A (hopefully) unique ID.
|
|
50
|
+
* If it's not unique, we have a duplicate option value.
|
|
51
|
+
*/
|
|
52
|
+
#getOptionId(item) {
|
|
53
|
+
return (
|
|
54
|
+
this.#name + '-' + item.value.trim().replace(/\s+/g, '-').toLowerCase()
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
* True if the maximum number of options are checked, false otherwise.
|
|
61
|
+
*/
|
|
62
|
+
isAtMaxSelections() {
|
|
63
|
+
return this.#selectedIndices.length >= this.#max;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cleans up a list of options for saving to memory.
|
|
68
|
+
* @param {HTMLOptionsCollection} list - The options from a select element.
|
|
69
|
+
* @returns {Array} An array of option objects.
|
|
70
|
+
*/
|
|
71
|
+
#formatOptions(list) {
|
|
72
|
+
let item;
|
|
73
|
+
const cleaned = [];
|
|
74
|
+
|
|
75
|
+
let isChecked = false;
|
|
76
|
+
let isSelected = false;
|
|
77
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
78
|
+
item = list[i];
|
|
79
|
+
isSelected = item.defaultSelected ? item.defaultSelected : false;
|
|
80
|
+
isChecked = this.isAtMaxSelections() ? false : isSelected;
|
|
81
|
+
cleaned.push({
|
|
82
|
+
id: this.#getOptionId(item),
|
|
83
|
+
value: item.value,
|
|
84
|
+
label: item.text,
|
|
85
|
+
text: item.text,
|
|
86
|
+
checked: isChecked,
|
|
87
|
+
index: i,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// If an option is initially checked, we need to record it.
|
|
91
|
+
if (isChecked) {
|
|
92
|
+
this.#selectedIndices.push(i);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return cleaned;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @returns {MultiselectModel} An instance.
|
|
101
|
+
*/
|
|
102
|
+
init() {
|
|
103
|
+
this.#optionsData = this.#formatOptions(this.#options);
|
|
104
|
+
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Toggle checked value of an option.
|
|
110
|
+
* @param {number} index - The index position of the option in the list.
|
|
111
|
+
* @returns {boolean} A value of true is checked and false is unchecked.
|
|
112
|
+
*/
|
|
113
|
+
toggleOption(index) {
|
|
114
|
+
const isChecked = !this.#optionsData[index].checked;
|
|
115
|
+
|
|
116
|
+
if (isChecked && this.#selectedIndices.length < this.#max) {
|
|
117
|
+
this.#optionsData[index].checked = true;
|
|
118
|
+
this.#selectedIndices.push(index);
|
|
119
|
+
this.#selectedIndices.sort();
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// We're over the max selections, reverse the check of the option.
|
|
125
|
+
this.#optionsData[index].checked = false;
|
|
126
|
+
this.#selectedIndices = this.#selectedIndices.filter(function (currIndex) {
|
|
127
|
+
return currIndex !== index;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Utility function for Array.reduce() used in searchIndices.
|
|
135
|
+
* @param {Array} aggregate - The reducer's accumulator.
|
|
136
|
+
* @param {object} item - Each item in the collection.
|
|
137
|
+
* @param {number} index - The index of item in the collection.
|
|
138
|
+
* @param {string} value - The value of item in the collection.
|
|
139
|
+
* @returns {Array} The reducer's accumulator.
|
|
140
|
+
*/
|
|
141
|
+
#searchAggregator(aggregate, item, index, value) {
|
|
142
|
+
if (MultiselectModel.stringMatch(item.text, value)) {
|
|
143
|
+
aggregate.push(index);
|
|
144
|
+
}
|
|
145
|
+
return aggregate;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Search for a query string in the options text and return the indices of
|
|
150
|
+
* the matching positions in the options array.
|
|
151
|
+
* @param {string} query - A query string.
|
|
152
|
+
* @returns {Array} List of indices of the matching entries from the options.
|
|
153
|
+
*/
|
|
154
|
+
filterIndices(query) {
|
|
155
|
+
// Convert query to a string if it's not.
|
|
156
|
+
if (Object.prototype.toString.call(query) !== '[object String]') {
|
|
157
|
+
query = '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.#lastFilterIndicesList = this.#filterIndicesList;
|
|
161
|
+
|
|
162
|
+
if (this.#optionsData.length > 0) {
|
|
163
|
+
this.#filterIndicesList = this.#optionsData.reduce((acc, item, index) => {
|
|
164
|
+
return this.#searchAggregator(acc, item, index, query);
|
|
165
|
+
}, []);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Reset index position.
|
|
169
|
+
this.#index = -1;
|
|
170
|
+
|
|
171
|
+
return this.#filterIndicesList;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// This is used to search the items in the collection.
|
|
175
|
+
clearFilter() {
|
|
176
|
+
this.#filterIndicesList = this.#lastFilterIndicesList = [];
|
|
177
|
+
return this.#UNDEFINED;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get filterIndicesList() {
|
|
181
|
+
return this.#filterIndicesList;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get lastFilterIndicesList() {
|
|
185
|
+
return this.#lastFilterIndicesList;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// This is used to check an item in the collection.
|
|
189
|
+
get selectedIndices() {
|
|
190
|
+
return this.#selectedIndices;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Retrieve an option object from the options list.
|
|
195
|
+
* @param {number} index - The index position in the options list.
|
|
196
|
+
* @returns {object} The option object with text, value, and checked value.
|
|
197
|
+
*/
|
|
198
|
+
getOption(index) {
|
|
199
|
+
return this.#optionsData[index];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set the index of the collection (represents the highlighted option).
|
|
204
|
+
* @param {number} value - The index to set.
|
|
205
|
+
*/
|
|
206
|
+
set index(value) {
|
|
207
|
+
const filterCount = this.#filterIndicesList.length;
|
|
208
|
+
const count = filterCount === 0 ? this.#optionsData.length : filterCount;
|
|
209
|
+
if (value < 0) {
|
|
210
|
+
this.#index = -1;
|
|
211
|
+
} else if (value >= count) {
|
|
212
|
+
this.#index = count - 1;
|
|
213
|
+
} else {
|
|
214
|
+
this.#index = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @returns {number} The current index (highlighted option).
|
|
220
|
+
*/
|
|
221
|
+
get index() {
|
|
222
|
+
return this.#index;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
resetIndex() {
|
|
226
|
+
this.#index = -1;
|
|
227
|
+
return this.#index;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
get options() {
|
|
231
|
+
return this.#optionsData;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// These are used to highlight items in the collection.
|
|
235
|
+
|
|
236
|
+
/*
|
|
237
|
+
|
|
238
|
+
get items() {
|
|
239
|
+
console.log([...this.#items]);
|
|
240
|
+
return [...this.#items];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
set items(newItems) {
|
|
244
|
+
this.#items = newItems;
|
|
245
|
+
this.#notify();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
addItem(item) {
|
|
249
|
+
this.#items.push(item);
|
|
250
|
+
this.#notify();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
removeItem(indexToRemove) {
|
|
254
|
+
this.#items.splice(indexToRemove, 1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#notify() {
|
|
258
|
+
this.dispatchEvent(new Event('change'));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
subscribe(callback) {
|
|
262
|
+
this.addEventListener('change', callback);
|
|
263
|
+
return () => this.removeEventListener('change', callback);
|
|
264
|
+
}
|
|
265
|
+
*/
|
|
266
|
+
|
|
267
|
+
// How many options may be checked.
|
|
268
|
+
static MAX_SELECTIONS = 5;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Escapes a string.
|
|
272
|
+
* @param {string} str - The string to escape.
|
|
273
|
+
* @returns {string} The escaped string.
|
|
274
|
+
*/
|
|
275
|
+
static stringEscape(str) {
|
|
276
|
+
return str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Tests whether a string matches another.
|
|
281
|
+
* @param {string} x - The control string.
|
|
282
|
+
* @param {string} y - The comparison string.
|
|
283
|
+
* @returns {boolean} True if `x` and `y` match, false otherwise.
|
|
284
|
+
*/
|
|
285
|
+
static stringMatch(x, y) {
|
|
286
|
+
return RegExp(MultiselectModel.stringEscape(y.trim()), 'i').test(x);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { MultiselectModel } from './multiselect-model.js';
|
|
2
|
+
|
|
3
|
+
const HTML_SNIPPET = `
|
|
4
|
+
<select class="o-multiselect" multiple>
|
|
5
|
+
<option value="mortgages">
|
|
6
|
+
Mortgages
|
|
7
|
+
</option>
|
|
8
|
+
<option value="financial-education" selected>
|
|
9
|
+
Financial education
|
|
10
|
+
</option>
|
|
11
|
+
<option value="financial-well-being">
|
|
12
|
+
Financial well-being
|
|
13
|
+
</option>
|
|
14
|
+
<option value="student-loans">
|
|
15
|
+
Student loans
|
|
16
|
+
</option>
|
|
17
|
+
<option value="rulemaking">
|
|
18
|
+
Rulemaking
|
|
19
|
+
</option>
|
|
20
|
+
<option value="banking">
|
|
21
|
+
Banking
|
|
22
|
+
</option>
|
|
23
|
+
</select>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
let multiselectModel;
|
|
27
|
+
let selectDom;
|
|
28
|
+
|
|
29
|
+
describe('MultiselectModel', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
document.body.innerHTML = HTML_SNIPPET;
|
|
32
|
+
|
|
33
|
+
selectDom = document.querySelector('select[multiple]');
|
|
34
|
+
multiselectModel = new MultiselectModel(selectDom.options).init();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('init()', () => {
|
|
38
|
+
it('should correctly initialize when given valid options', () => {
|
|
39
|
+
multiselectModel = new MultiselectModel(selectDom.options).init();
|
|
40
|
+
expect(multiselectModel.constructor).toBe(MultiselectModel);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('toggleOption()', () => {
|
|
45
|
+
it('should toggle checked value of option', () => {
|
|
46
|
+
expect(multiselectModel.getOption(0).checked).toBe(false);
|
|
47
|
+
expect(multiselectModel.toggleOption(0)).toBe(true);
|
|
48
|
+
expect(multiselectModel.getOption(0).checked).toBe(true);
|
|
49
|
+
|
|
50
|
+
expect(multiselectModel.getOption(1).checked).toBe(true);
|
|
51
|
+
expect(multiselectModel.toggleOption(1)).toBe(false);
|
|
52
|
+
expect(multiselectModel.getOption(1).checked).toBe(false);
|
|
53
|
+
|
|
54
|
+
expect(multiselectModel.getOption(2).checked).toBe(false);
|
|
55
|
+
expect(multiselectModel.toggleOption(2)).toBe(true);
|
|
56
|
+
expect(multiselectModel.getOption(2).checked).toBe(true);
|
|
57
|
+
|
|
58
|
+
expect(multiselectModel.getOption(3).checked).toBe(false);
|
|
59
|
+
expect(multiselectModel.toggleOption(3)).toBe(true);
|
|
60
|
+
expect(multiselectModel.getOption(3).checked).toBe(true);
|
|
61
|
+
|
|
62
|
+
expect(multiselectModel.getOption(4).checked).toBe(false);
|
|
63
|
+
expect(multiselectModel.toggleOption(4)).toBe(true);
|
|
64
|
+
expect(multiselectModel.getOption(4).checked).toBe(true);
|
|
65
|
+
|
|
66
|
+
expect(multiselectModel.getOption(5).checked).toBe(false);
|
|
67
|
+
expect(multiselectModel.toggleOption(5)).toBe(true);
|
|
68
|
+
expect(multiselectModel.getOption(5).checked).toBe(true);
|
|
69
|
+
|
|
70
|
+
// Try to push beyond maximum selections.
|
|
71
|
+
expect(multiselectModel.toggleOption(1)).toBe(false);
|
|
72
|
+
expect(multiselectModel.getOption(1).checked).toBe(false);
|
|
73
|
+
expect(multiselectModel.toggleOption(2)).toBe(false);
|
|
74
|
+
expect(multiselectModel.getOption(2).checked).toBe(false);
|
|
75
|
+
expect(multiselectModel.toggleOption(1)).toBe(true);
|
|
76
|
+
expect(multiselectModel.getOption(1).checked).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getSelectedIndices()', () => {
|
|
81
|
+
it('should get the indices of the checked options', () => {
|
|
82
|
+
expect(multiselectModel.selectedIndices[0]).toBe(1);
|
|
83
|
+
multiselectModel.toggleOption(0);
|
|
84
|
+
expect(multiselectModel.selectedIndices[0]).toBe(0);
|
|
85
|
+
expect(multiselectModel.selectedIndices[1]).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('isAtMaxSelections()', () => {
|
|
90
|
+
it('should toggle checked value of option', () => {
|
|
91
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(false);
|
|
92
|
+
|
|
93
|
+
multiselectModel.toggleOption(0);
|
|
94
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(false);
|
|
95
|
+
|
|
96
|
+
// No need to toggle the second option because it's already checked.
|
|
97
|
+
|
|
98
|
+
multiselectModel.toggleOption(2);
|
|
99
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(false);
|
|
100
|
+
|
|
101
|
+
multiselectModel.toggleOption(3);
|
|
102
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(false);
|
|
103
|
+
|
|
104
|
+
multiselectModel.toggleOption(4);
|
|
105
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(true);
|
|
106
|
+
|
|
107
|
+
multiselectModel.toggleOption(5);
|
|
108
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(true);
|
|
109
|
+
|
|
110
|
+
multiselectModel.toggleOption(5);
|
|
111
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(true);
|
|
112
|
+
|
|
113
|
+
expect(multiselectModel.toggleOption(4)).toBe(false);
|
|
114
|
+
expect(multiselectModel.isAtMaxSelections()).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('filterIndices()', () => {
|
|
119
|
+
it('should return indices of matched options in a search', () => {
|
|
120
|
+
expect(multiselectModel.filterIndices('mo').length).toBe(1);
|
|
121
|
+
expect(multiselectModel.filterIndices('mort').length).toBe(1);
|
|
122
|
+
expect(multiselectModel.filterIndices('mort')[0]).toBe(0);
|
|
123
|
+
expect(multiselectModel.filterIndices('fin').length).toBe(2);
|
|
124
|
+
expect(multiselectModel.filterIndices('fin')[0]).toBe(1);
|
|
125
|
+
expect(multiselectModel.filterIndices('fin')[1]).toBe(2);
|
|
126
|
+
expect(multiselectModel.filterIndices('being').length).toBe(1);
|
|
127
|
+
expect(multiselectModel.filterIndices('being')[0]).toBe(2);
|
|
128
|
+
expect(multiselectModel.filterIndices('').length).toBe(6);
|
|
129
|
+
expect(multiselectModel.filterIndices().length).toBe(6);
|
|
130
|
+
expect(multiselectModel.filterIndices([]).length).toBe(6);
|
|
131
|
+
expect(multiselectModel.filterIndices({}).length).toBe(6);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('clearFilter()', () => {
|
|
136
|
+
it('should clear current and last matched options in a search', () => {
|
|
137
|
+
multiselectModel.filterIndices('fin');
|
|
138
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(0);
|
|
139
|
+
expect(multiselectModel.filterIndicesList.length).toBe(2);
|
|
140
|
+
multiselectModel.filterIndices('mo');
|
|
141
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(2);
|
|
142
|
+
expect(multiselectModel.filterIndicesList.length).toBe(1);
|
|
143
|
+
multiselectModel.clearFilter();
|
|
144
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(0);
|
|
145
|
+
expect(multiselectModel.filterIndicesList.length).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('getFilterIndices()', () => {
|
|
150
|
+
it('should return current indices of matched options in a search', () => {
|
|
151
|
+
expect(multiselectModel.filterIndicesList.length).toBe(0);
|
|
152
|
+
multiselectModel.filterIndices('fin');
|
|
153
|
+
expect(multiselectModel.filterIndicesList.length).toBe(2);
|
|
154
|
+
expect(multiselectModel.filterIndicesList[0]).toBe(1);
|
|
155
|
+
expect(multiselectModel.filterIndicesList[1]).toBe(2);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('getLastFilterIndices()', () => {
|
|
160
|
+
it('should return indices of the last matched options in a search', () => {
|
|
161
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(0);
|
|
162
|
+
multiselectModel.filterIndices('fin');
|
|
163
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(0);
|
|
164
|
+
multiselectModel.filterIndices('mo');
|
|
165
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(2);
|
|
166
|
+
expect(multiselectModel.lastFilterIndicesList[0]).toBe(1);
|
|
167
|
+
expect(multiselectModel.lastFilterIndicesList[1]).toBe(2);
|
|
168
|
+
multiselectModel.filterIndices('being');
|
|
169
|
+
expect(multiselectModel.lastFilterIndicesList.length).toBe(1);
|
|
170
|
+
expect(multiselectModel.lastFilterIndicesList[0]).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('getIndex()', () => {
|
|
175
|
+
it('should get correct index when no filter results', () => {
|
|
176
|
+
expect(multiselectModel.index).toBe(-1);
|
|
177
|
+
multiselectModel.index = 2;
|
|
178
|
+
expect(multiselectModel.index).toBe(2);
|
|
179
|
+
multiselectModel.filterIndices('asdf');
|
|
180
|
+
expect(multiselectModel.index).toBe(-1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should get reset index when options are filtered', () => {
|
|
184
|
+
expect(multiselectModel.index).toBe(-1);
|
|
185
|
+
multiselectModel.index = 2;
|
|
186
|
+
expect(multiselectModel.index).toBe(2);
|
|
187
|
+
multiselectModel.filterIndices('mo');
|
|
188
|
+
expect(multiselectModel.index).toBe(-1);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('setIndex()', () => {
|
|
193
|
+
it('should set index within available option range', () => {
|
|
194
|
+
expect(multiselectModel.index).toBe(-1);
|
|
195
|
+
multiselectModel.index = 0;
|
|
196
|
+
expect(multiselectModel.index).toBe(0);
|
|
197
|
+
multiselectModel.index = 1;
|
|
198
|
+
expect(multiselectModel.index).toBe(1);
|
|
199
|
+
multiselectModel.index = 2;
|
|
200
|
+
expect(multiselectModel.index).toBe(2);
|
|
201
|
+
multiselectModel.index = 10;
|
|
202
|
+
expect(multiselectModel.index).toBe(5);
|
|
203
|
+
multiselectModel.index = -2;
|
|
204
|
+
expect(multiselectModel.index).toBe(-1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should set index within filtered option range', () => {
|
|
208
|
+
expect(multiselectModel.index).toBe(-1);
|
|
209
|
+
multiselectModel.filterIndices('fin');
|
|
210
|
+
multiselectModel.index = 0;
|
|
211
|
+
expect(multiselectModel.index).toBe(0);
|
|
212
|
+
expect(multiselectModel.filterIndicesList[multiselectModel.index]).toBe(
|
|
213
|
+
1,
|
|
214
|
+
);
|
|
215
|
+
multiselectModel.index = 1;
|
|
216
|
+
expect(multiselectModel.index).toBe(1);
|
|
217
|
+
multiselectModel.index = 2;
|
|
218
|
+
expect(multiselectModel.index).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('resetIndex()', () => {
|
|
223
|
+
it('should return -1 for index when called', () => {
|
|
224
|
+
expect(multiselectModel.resetIndex()).toBe(-1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('getOption()', () => {
|
|
229
|
+
it('should return the correct option when requested', () => {
|
|
230
|
+
expect(multiselectModel.getOption(2).text).toBe('Financial well-being');
|
|
231
|
+
expect(multiselectModel.getOption(2).value).toBe('financial-well-being');
|
|
232
|
+
expect(multiselectModel.getOption(2).checked).toBe(false);
|
|
233
|
+
expect(multiselectModel.getOption(1).checked).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -31,7 +31,7 @@ describe('<cfpb-tag-filter>', () => {
|
|
|
31
31
|
expect(assignedNodes[0].textContent).toBe('Earth');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
it('dispatches the correct event', async () => {
|
|
35
35
|
const mockHandler = jest.fn();
|
|
36
36
|
elm.addEventListener('tag-click', mockHandler);
|
|
37
37
|
|
|
@@ -23,6 +23,7 @@ export class CfpbTagGroup extends LitElement {
|
|
|
23
23
|
/**
|
|
24
24
|
* @property {boolean} stacked - Whether to stack the tags vertically.
|
|
25
25
|
* @property {Array} tagList - List of the tags in the tag group.
|
|
26
|
+
* @returns {object} The map of properties.
|
|
26
27
|
*/
|
|
27
28
|
static get properties() {
|
|
28
29
|
return {
|
|
@@ -87,7 +88,7 @@ export class CfpbTagGroup extends LitElement {
|
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Handle a change of the light DOM.
|
|
90
|
-
* @param {MutationRecord} mutationList
|
|
91
|
+
* @param {MutationRecord} mutationList - The record of observed DOM changes.
|
|
91
92
|
*/
|
|
92
93
|
#onMutation(mutationList) {
|
|
93
94
|
if (!this.#initialized) return;
|
|
@@ -23,6 +23,12 @@ export class CfpbTagTopic extends LitElement {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/*
|
|
27
|
+
* @property {string} href - The URL to link to (makes the tag a link).
|
|
28
|
+
* @property {boolean} siblingOfJumpLink
|
|
29
|
+
* Whether the preceding sibling is a link or not. This is used to stack the
|
|
30
|
+
* divider lines between the links at mobile.
|
|
31
|
+
*/
|
|
26
32
|
constructor() {
|
|
27
33
|
super();
|
|
28
34
|
this.href = '';
|
package/src/elements/index.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
export * from './cfpb-button';
|
|
7
7
|
export * from './cfpb-form-choice';
|
|
8
8
|
export * from './cfpb-file-upload';
|
|
9
|
+
export * from './cfpb-label';
|
|
9
10
|
export * from './cfpb-tag-filter';
|
|
10
11
|
export * from './cfpb-tag-topic';
|
|
11
12
|
export * from './cfpb-tag-group';
|
|
13
|
+
export * from './cfpb-multiselect';
|