@madgex/design-system 14.2.0 → 14.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/dist/css/index.css +1 -1
- package/dist/js/index.js +1 -1
- package/package.json +1 -1
- package/src/components/conditional-section/README.md +2 -2
- package/src/components/conditional-section/conditional-section.config.js +54 -0
- package/src/components/conditional-section/conditional-section.js +91 -26
- package/src/components/conditional-section/conditional-section.spec.js +402 -0
- package/src/components/inputs/combobox/README.md +44 -51
- package/src/components/inputs/combobox/_template.njk +11 -31
- package/src/components/inputs/combobox/combobox.config.js +23 -22
- package/src/components/inputs/combobox/combobox.njk +4 -4
- package/src/components/inputs/combobox/combobox.scss +3 -0
- package/src/layout/forms/forms.config.js +7 -1
- package/src/layout/forms/forms.njk +8 -4
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// eslint-disable-next-line n/no-unpublished-import
|
|
2
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
3
|
+
import { MdsConditionalSection, shouldShow } from './conditional-section.js';
|
|
4
|
+
|
|
5
|
+
if (!customElements.get('mds-conditional-section')) {
|
|
6
|
+
customElements.define('mds-conditional-section', MdsConditionalSection);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function emitInput(target) {
|
|
10
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mountScenario({ inputsHtml, fieldName, showWhen, includeShowWhen = true }) {
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
const showWhenAttr = includeShowWhen ? `show-when="${String(showWhen).replace(/"/g, '"')}"` : '';
|
|
16
|
+
container.innerHTML = `
|
|
17
|
+
<form id="test-conditional-form">
|
|
18
|
+
${inputsHtml}
|
|
19
|
+
<mds-conditional-section field-name="${fieldName}" ${showWhenAttr}>
|
|
20
|
+
<p data-testid="conditional-content">Conditional content</p>
|
|
21
|
+
</mds-conditional-section>
|
|
22
|
+
</form>
|
|
23
|
+
`;
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
const form = container.querySelector('form');
|
|
26
|
+
const section = container.querySelector('mds-conditional-section');
|
|
27
|
+
return { container, form, section };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('shouldShow', () => {
|
|
31
|
+
it('is true when any checked value matches a comma-separated trigger (OR)', () => {
|
|
32
|
+
const form = document.createElement('form');
|
|
33
|
+
form.innerHTML = `
|
|
34
|
+
<input type="checkbox" name="i" value="sports" checked />
|
|
35
|
+
<input type="checkbox" name="i" value="music" />
|
|
36
|
+
`;
|
|
37
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
|
|
38
|
+
|
|
39
|
+
form.querySelector('[value="sports"]').checked = false;
|
|
40
|
+
form.querySelector('[value="music"]').checked = true;
|
|
41
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is false when no checked value matches triggers', () => {
|
|
45
|
+
const form = document.createElement('form');
|
|
46
|
+
form.innerHTML = `
|
|
47
|
+
<input type="checkbox" name="i" value="sports" />
|
|
48
|
+
<input type="checkbox" name="i" value="music" />
|
|
49
|
+
<input type="checkbox" name="i" value="travel" checked />
|
|
50
|
+
`;
|
|
51
|
+
expect(shouldShow(form.elements.i, 'sports,music')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('is false when a checkbox group has nothing checked', () => {
|
|
55
|
+
const form = document.createElement('form');
|
|
56
|
+
form.innerHTML = `
|
|
57
|
+
<input type="checkbox" name="i" value="sports" />
|
|
58
|
+
<input type="checkbox" name="i" value="music" />
|
|
59
|
+
`;
|
|
60
|
+
expect(shouldShow(form.elements.i, 'sports')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('trims trigger segments and ignores empty parts', () => {
|
|
64
|
+
const form = document.createElement('form');
|
|
65
|
+
form.innerHTML = `
|
|
66
|
+
<input type="checkbox" name="i" value="a" checked />
|
|
67
|
+
<input type="checkbox" name="i" value="b" />
|
|
68
|
+
`;
|
|
69
|
+
expect(shouldShow(form.elements.i, ' a , b ')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('is false when show-when is null or empty', () => {
|
|
73
|
+
const form = document.createElement('form');
|
|
74
|
+
form.innerHTML = `<input type="checkbox" name="i" value="x" checked />`;
|
|
75
|
+
expect(shouldShow(form.elements.i, null)).toBe(false);
|
|
76
|
+
expect(shouldShow(form.elements.i, '')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('for a lone checkbox uses true/false against triggers', () => {
|
|
80
|
+
const form = document.createElement('form');
|
|
81
|
+
form.innerHTML = '<input type="checkbox" name="c" />';
|
|
82
|
+
const field = form.elements.c;
|
|
83
|
+
expect(shouldShow(field, 'false')).toBe(true);
|
|
84
|
+
expect(shouldShow(field, 'true')).toBe(false);
|
|
85
|
+
field.checked = true;
|
|
86
|
+
expect(shouldShow(field, 'true')).toBe(true);
|
|
87
|
+
expect(shouldShow(field, 'false')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('for a lone checkbox with a custom value matches show-when against that value when checked', () => {
|
|
91
|
+
const form = document.createElement('form');
|
|
92
|
+
form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
|
|
93
|
+
const field = form.elements.c;
|
|
94
|
+
expect(shouldShow(field, 'yes')).toBe(false);
|
|
95
|
+
field.checked = true;
|
|
96
|
+
expect(shouldShow(field, 'yes')).toBe(true);
|
|
97
|
+
expect(shouldShow(field, 'no')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('for a lone checkbox with a custom value still supports show-when true/false', () => {
|
|
101
|
+
const form = document.createElement('form');
|
|
102
|
+
form.innerHTML = '<input type="checkbox" name="c" value="yes" />';
|
|
103
|
+
const field = form.elements.c;
|
|
104
|
+
field.checked = true;
|
|
105
|
+
expect(shouldShow(field, 'true')).toBe(true);
|
|
106
|
+
expect(shouldShow(field, 'false')).toBe(false);
|
|
107
|
+
field.checked = false;
|
|
108
|
+
expect(shouldShow(field, 'false')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('for a checkbox group matches checked values', () => {
|
|
112
|
+
const form = document.createElement('form');
|
|
113
|
+
form.innerHTML = `
|
|
114
|
+
<input type="checkbox" name="i" value="sports" />
|
|
115
|
+
<input type="checkbox" name="i" value="music" checked />
|
|
116
|
+
<input type="checkbox" name="i" value="travel" />
|
|
117
|
+
`;
|
|
118
|
+
expect(shouldShow(form.elements.i, 'music')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('uses "on" when a checked checkbox has no value attribute', () => {
|
|
122
|
+
const form = document.createElement('form');
|
|
123
|
+
form.innerHTML = `
|
|
124
|
+
<input type="checkbox" name="i" checked />
|
|
125
|
+
<input type="checkbox" name="i" />
|
|
126
|
+
`;
|
|
127
|
+
expect(shouldShow(form.elements.i, 'on')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('for a radio group matches the selected value', () => {
|
|
131
|
+
const form = document.createElement('form');
|
|
132
|
+
form.innerHTML = `
|
|
133
|
+
<input type="radio" name="r" value="a" />
|
|
134
|
+
<input type="radio" name="r" value="b" checked />
|
|
135
|
+
`;
|
|
136
|
+
expect(shouldShow(form.elements.r, 'b')).toBe(true);
|
|
137
|
+
expect(shouldShow(form.elements.r, 'a')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('reads multiple same-name controls from a RadioNodeList', () => {
|
|
141
|
+
const form = document.createElement('form');
|
|
142
|
+
form.innerHTML = `
|
|
143
|
+
<input type="checkbox" name="i" value="a" />
|
|
144
|
+
<input type="checkbox" name="i" value="b" />
|
|
145
|
+
`;
|
|
146
|
+
const list = form.elements.i;
|
|
147
|
+
expect(list).toBeInstanceOf(RadioNodeList);
|
|
148
|
+
expect(list.length).toBe(2);
|
|
149
|
+
list[1].checked = true;
|
|
150
|
+
expect(shouldShow(list, 'b')).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('for a multiple select matches any selected option against comma-separated triggers', () => {
|
|
154
|
+
const form = document.createElement('form');
|
|
155
|
+
form.innerHTML = `
|
|
156
|
+
<select name="skills" id="skills" multiple>
|
|
157
|
+
<option value="js">JavaScript</option>
|
|
158
|
+
<option value="css">CSS</option>
|
|
159
|
+
<option value="html">HTML</option>
|
|
160
|
+
</select>
|
|
161
|
+
`;
|
|
162
|
+
const select = form.elements.skills;
|
|
163
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
164
|
+
|
|
165
|
+
select.options[1].selected = true;
|
|
166
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
167
|
+
|
|
168
|
+
select.options[1].selected = false;
|
|
169
|
+
select.options[2].selected = true;
|
|
170
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('for a multiple select matches when a non-first selected option matches show-when alone', () => {
|
|
174
|
+
const form = document.createElement('form');
|
|
175
|
+
form.innerHTML = `
|
|
176
|
+
<select name="skills" id="skills" multiple>
|
|
177
|
+
<option value="js">JavaScript</option>
|
|
178
|
+
<option value="css">CSS</option>
|
|
179
|
+
</select>
|
|
180
|
+
`;
|
|
181
|
+
const select = form.elements.skills;
|
|
182
|
+
select.options[0].selected = true;
|
|
183
|
+
select.options[1].selected = true;
|
|
184
|
+
expect(shouldShow(select, 'css')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('for a multiple select stays visible while any matching option remains selected', () => {
|
|
188
|
+
const form = document.createElement('form');
|
|
189
|
+
form.innerHTML = `
|
|
190
|
+
<select name="skills" id="skills" multiple>
|
|
191
|
+
<option value="js">JavaScript</option>
|
|
192
|
+
<option value="css">CSS</option>
|
|
193
|
+
</select>
|
|
194
|
+
`;
|
|
195
|
+
const select = form.elements.skills;
|
|
196
|
+
select.options[0].selected = true;
|
|
197
|
+
select.options[1].selected = true;
|
|
198
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
199
|
+
|
|
200
|
+
select.options[0].selected = false;
|
|
201
|
+
expect(shouldShow(select, 'js,css')).toBe(true);
|
|
202
|
+
|
|
203
|
+
select.options[1].selected = false;
|
|
204
|
+
expect(shouldShow(select, 'js,css')).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('MdsConditionalSection', () => {
|
|
209
|
+
let container;
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
container?.remove();
|
|
213
|
+
container = undefined;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('checkbox group', () => {
|
|
217
|
+
it('shows when a non-first checkbox matching show-when is checked', () => {
|
|
218
|
+
({ container } = mountScenario({
|
|
219
|
+
fieldName: 'interests',
|
|
220
|
+
showWhen: 'music',
|
|
221
|
+
inputsHtml: `
|
|
222
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
223
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
224
|
+
<input type="checkbox" name="interests" value="travel" id="cb-travel" />
|
|
225
|
+
`,
|
|
226
|
+
}));
|
|
227
|
+
const section = container.querySelector('mds-conditional-section');
|
|
228
|
+
expect(section.hidden).toBe(true);
|
|
229
|
+
|
|
230
|
+
const music = container.querySelector('#cb-music');
|
|
231
|
+
music.checked = true;
|
|
232
|
+
emitInput(music);
|
|
233
|
+
|
|
234
|
+
expect(section.hidden).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('matches any comma-separated trigger (OR)', () => {
|
|
238
|
+
({ container } = mountScenario({
|
|
239
|
+
fieldName: 'interests',
|
|
240
|
+
showWhen: 'sports,music',
|
|
241
|
+
inputsHtml: `
|
|
242
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
243
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
244
|
+
<input type="checkbox" name="interests" value="travel" id="cb-travel" />
|
|
245
|
+
`,
|
|
246
|
+
}));
|
|
247
|
+
const section = container.querySelector('mds-conditional-section');
|
|
248
|
+
const travel = container.querySelector('#cb-travel');
|
|
249
|
+
travel.checked = true;
|
|
250
|
+
emitInput(travel);
|
|
251
|
+
expect(section.hidden).toBe(true);
|
|
252
|
+
|
|
253
|
+
const music = container.querySelector('#cb-music');
|
|
254
|
+
music.checked = true;
|
|
255
|
+
emitInput(music);
|
|
256
|
+
expect(section.hidden).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('stays visible if one matching trigger is unchecked but another remains checked', () => {
|
|
260
|
+
({ container } = mountScenario({
|
|
261
|
+
fieldName: 'interests',
|
|
262
|
+
showWhen: 'sports,music',
|
|
263
|
+
inputsHtml: `
|
|
264
|
+
<input type="checkbox" name="interests" value="sports" id="cb-sports" />
|
|
265
|
+
<input type="checkbox" name="interests" value="music" id="cb-music" />
|
|
266
|
+
`,
|
|
267
|
+
}));
|
|
268
|
+
const section = container.querySelector('mds-conditional-section');
|
|
269
|
+
const sports = container.querySelector('#cb-sports');
|
|
270
|
+
const music = container.querySelector('#cb-music');
|
|
271
|
+
sports.checked = true;
|
|
272
|
+
music.checked = true;
|
|
273
|
+
emitInput(sports);
|
|
274
|
+
|
|
275
|
+
expect(section.hidden).toBe(false);
|
|
276
|
+
|
|
277
|
+
sports.checked = false;
|
|
278
|
+
emitInput(sports);
|
|
279
|
+
expect(section.hidden).toBe(false);
|
|
280
|
+
|
|
281
|
+
music.checked = false;
|
|
282
|
+
emitInput(music);
|
|
283
|
+
expect(section.hidden).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('regressions', () => {
|
|
288
|
+
it('single checkbox with show-when true toggles visibility', () => {
|
|
289
|
+
({ container } = mountScenario({
|
|
290
|
+
fieldName: 'agree',
|
|
291
|
+
showWhen: 'true',
|
|
292
|
+
inputsHtml: '<input type="checkbox" name="agree" id="agree" />',
|
|
293
|
+
}));
|
|
294
|
+
const section = container.querySelector('mds-conditional-section');
|
|
295
|
+
const cb = container.querySelector('#agree');
|
|
296
|
+
expect(section.hidden).toBe(true);
|
|
297
|
+
cb.checked = true;
|
|
298
|
+
emitInput(cb);
|
|
299
|
+
expect(section.hidden).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('hide-salary pattern: show-when false when checkbox unchecked', () => {
|
|
303
|
+
({ container } = mountScenario({
|
|
304
|
+
fieldName: 'HideSalary',
|
|
305
|
+
showWhen: 'false',
|
|
306
|
+
inputsHtml: '<input type="checkbox" name="HideSalary" id="HideSalary" />',
|
|
307
|
+
}));
|
|
308
|
+
const section = container.querySelector('mds-conditional-section');
|
|
309
|
+
expect(section.hidden).toBe(false);
|
|
310
|
+
const cb = container.querySelector('#HideSalary');
|
|
311
|
+
cb.checked = true;
|
|
312
|
+
emitInput(cb);
|
|
313
|
+
expect(section.hidden).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('single checkbox with custom value and show-when toggles visibility', () => {
|
|
317
|
+
({ container } = mountScenario({
|
|
318
|
+
fieldName: 'newsletter',
|
|
319
|
+
showWhen: 'weekly',
|
|
320
|
+
inputsHtml: '<input type="checkbox" name="newsletter" id="newsletter" value="weekly" />',
|
|
321
|
+
}));
|
|
322
|
+
const section = container.querySelector('mds-conditional-section');
|
|
323
|
+
const cb = container.querySelector('#newsletter');
|
|
324
|
+
expect(section.hidden).toBe(true);
|
|
325
|
+
cb.checked = true;
|
|
326
|
+
emitInput(cb);
|
|
327
|
+
expect(section.hidden).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('radio group show-when matches selected value', () => {
|
|
331
|
+
({ container } = mountScenario({
|
|
332
|
+
fieldName: 'option',
|
|
333
|
+
showWhen: 'Broccoli',
|
|
334
|
+
inputsHtml: `
|
|
335
|
+
<input type="radio" name="option" value="Donkey" id="r1" />
|
|
336
|
+
<input type="radio" name="option" value="Broccoli" id="r2" />
|
|
337
|
+
`,
|
|
338
|
+
}));
|
|
339
|
+
const section = container.querySelector('mds-conditional-section');
|
|
340
|
+
expect(section.hidden).toBe(true);
|
|
341
|
+
container.querySelector('#r2').checked = true;
|
|
342
|
+
emitInput(container.querySelector('#r2'));
|
|
343
|
+
expect(section.hidden).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('select show-when matches option value', () => {
|
|
347
|
+
({ container } = mountScenario({
|
|
348
|
+
fieldName: 'pets',
|
|
349
|
+
showWhen: 'cat',
|
|
350
|
+
inputsHtml: `
|
|
351
|
+
<select name="pets" id="pets">
|
|
352
|
+
<option value="">Choose</option>
|
|
353
|
+
<option value="dog">Dog</option>
|
|
354
|
+
<option value="cat">Cat</option>
|
|
355
|
+
</select>
|
|
356
|
+
`,
|
|
357
|
+
}));
|
|
358
|
+
const section = container.querySelector('mds-conditional-section');
|
|
359
|
+
const sel = container.querySelector('#pets');
|
|
360
|
+
sel.value = 'cat';
|
|
361
|
+
emitInput(sel);
|
|
362
|
+
expect(section.hidden).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('multiple select show-when matches any selected option (OR)', () => {
|
|
366
|
+
({ container } = mountScenario({
|
|
367
|
+
fieldName: 'skills',
|
|
368
|
+
showWhen: 'js,css',
|
|
369
|
+
inputsHtml: `
|
|
370
|
+
<label for="skills">Pick your skills (hold Ctrl/Cmd to select several):</label>
|
|
371
|
+
<select name="skills" id="skills" multiple size="3">
|
|
372
|
+
<option value="js">JavaScript</option>
|
|
373
|
+
<option value="css">CSS</option>
|
|
374
|
+
<option value="html">HTML</option>
|
|
375
|
+
</select>
|
|
376
|
+
`,
|
|
377
|
+
}));
|
|
378
|
+
const section = container.querySelector('mds-conditional-section');
|
|
379
|
+
const sel = container.querySelector('#skills');
|
|
380
|
+
expect(section.hidden).toBe(true);
|
|
381
|
+
|
|
382
|
+
sel.options[1].selected = true;
|
|
383
|
+
emitInput(sel);
|
|
384
|
+
expect(section.hidden).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('missing show-when', () => {
|
|
389
|
+
it('keeps section hidden when show-when attribute is omitted', () => {
|
|
390
|
+
({ container } = mountScenario({
|
|
391
|
+
fieldName: 'interests',
|
|
392
|
+
showWhen: '',
|
|
393
|
+
includeShowWhen: false,
|
|
394
|
+
inputsHtml: `
|
|
395
|
+
<input type="checkbox" name="interests" value="sports" checked />
|
|
396
|
+
`,
|
|
397
|
+
}));
|
|
398
|
+
const section = container.querySelector('mds-conditional-section');
|
|
399
|
+
expect(section.hidden).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -2,34 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
This component provides autocomplete search functionality to select an option from a list of options.
|
|
4
4
|
|
|
5
|
-
Options can be provided via
|
|
6
|
-
|
|
7
|
-
`value` is an Object or Array of Objects representing the selected option or options.
|
|
5
|
+
Options can be provided via `<option value="value">label</option>` children, or obtained from an API using the `apiUrl` parameter.
|
|
8
6
|
|
|
9
7
|
## Parameters - Nunjucks
|
|
10
8
|
|
|
11
9
|
- `id`: the id of your combobox **required**
|
|
12
10
|
- `name`: the name of the input for form submission. Uses ID unless specified - **recommended**
|
|
13
11
|
- `labelText`: the text used in the label **required**
|
|
14
|
-
- `value`: the populated option `object` ( `{ label: 'Orange', value: 45 }`) or `array` of option `objects` ( `[{ label: 'Orange', value: 45 }, { label: 'Green', value: 33 }]`)
|
|
15
12
|
- `searchText`: _Not_ the value, but the current text inside the search input.
|
|
16
|
-
- `
|
|
17
|
-
- `apiUrl`: when populated, `options` is ignored and data is fetched from an API URL instead
|
|
13
|
+
- `apiUrl`: when populated, options data is fetched from an API URL
|
|
18
14
|
- `apiQueryKey`: the query parameter name added to `apiUrl` - (defaults to 'searchText')
|
|
19
15
|
- `apiOptionsPath`: where to grab an array of options on api response, e.g. `data.options` would be an array of options. leave undefined to use root api response as array
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
16
|
+
- `apiOptionLabelPath`: relative to options object returned from API response, e.g. `label` or `title` or `nested.object.label` (defaults to `label`)
|
|
17
|
+
- `apiOptionValuePath`: relative to options object returned from API response, e.g. `value` or `score` or `nested.object.value` (defaults to `value`)
|
|
22
18
|
- `multiple`: Boolean, whether to treat `value` input and output as an Array, or a singular. Also to display pills. default `false`
|
|
23
19
|
- `fallbackTo`: the form element to use as a fallback. Should be either 'select' or 'input' **recommended** (see notes underneath)
|
|
24
|
-
- `
|
|
20
|
+
- `fallbackValue`: only when `fallbackTo` is `input`, the `value` attribute of the fallback `input` element
|
|
21
|
+
- `placeholder`: the placeholder for your input **recommended**
|
|
25
22
|
- `classes`: add extra classes to the trigger
|
|
26
23
|
- `helpText`: Helper text to display under the label
|
|
27
24
|
- `tooltipMessage`: Toggles a tooltip with this message to appear on the input
|
|
28
25
|
- `validationError`: The error message provided by validation
|
|
29
26
|
- `state`: The current state of the input, currently the only allowed value is `error`
|
|
30
|
-
- `type`: applied as data-type` attribute, the name of the options api e.g "location-lookup"
|
|
27
|
+
- `type`: applied as `data-type` attribute, the name of the options api e.g "location-lookup"
|
|
31
28
|
- `i18n`: an `object`, Text to translate/customise
|
|
32
29
|
- `minSearchCharacters`: The minimum number of characters inside the input before a search is performed to avoid low specificity searches. This should be matched by your implementation's search handler.
|
|
30
|
+
- `attributes`: an `object`, attribute key/values applied directly to `mds-combobox` element
|
|
31
|
+
- `hideLabel` — Boolean, visually hides the label.
|
|
32
|
+
- `optional` - Boolean, marks field as optional in the label only
|
|
33
33
|
|
|
34
34
|
```
|
|
35
35
|
i18n: {
|
|
@@ -48,13 +48,7 @@ i18n: {
|
|
|
48
48
|
|
|
49
49
|
## Usage
|
|
50
50
|
|
|
51
|
-
MdsCombobox usage revolves around
|
|
52
|
-
If `multiple` mode is true,`params.value` must be undefined or an `array`, otherwise undefined or an `object`.
|
|
53
|
-
|
|
54
|
-
The params `value`, `options`, `optionLabelPath` and `optionValuePath` are tightly coupled and must be compatible with each other:
|
|
55
|
-
|
|
56
|
-
- `options`: an array of objects, each object has the property stated by `optionLabelPath` and `optionValuePath`
|
|
57
|
-
- `value`: an object ( or array of objects if `multiple:true`), object is the same shape as the ones in `options`, each object has the property stated by `optionLabelPath` and `optionValuePath`
|
|
51
|
+
MdsCombobox usage revolves around options provided by `<option value="">label</option>` child elements as available options and the initial value comes from `<option selected>` children, and optionally an API (`apiUrl` in use), and being in `multiple` mode via `params.multiple`.
|
|
58
52
|
|
|
59
53
|
## Fallback
|
|
60
54
|
|
|
@@ -63,65 +57,52 @@ When JavaScript is unavailable, the combobox will gracefully degrade to a native
|
|
|
63
57
|
Both fallback elements share the same `id` as the combobox to ensure the label remains properly associated. However, the fallback's `name` attribute will be `[name]_fallback` to distinguish it from the JavaScript-enabled version. This allows your server-side code to detect when the fallback was used and handle the form submission appropriately.
|
|
64
58
|
|
|
65
59
|
**Choosing a fallback type:**
|
|
60
|
+
|
|
66
61
|
- Use `fallbackTo: 'select'` when you have a static, predefined list of options that work well in a standard dropdown.
|
|
67
62
|
- Use `fallbackTo: 'input'` when options are dynamic (from an API) or when you prefer a text input. With a text input fallback, users can type and submit values that your server can then process (e.g., look up a location ID from a location name).
|
|
63
|
+
- Use `fallbackValue` when using `fallbackTo: 'input'` to populate the fallback's value
|
|
68
64
|
|
|
69
|
-
### Example - Single mode - no API -
|
|
70
|
-
|
|
71
|
-
As our options object uses the default property names, we don't need to specify them.
|
|
72
|
-
|
|
73
|
-
```njk
|
|
74
|
-
{\% call MdsCombobox({
|
|
75
|
-
id:'my-id',
|
|
76
|
-
name: 'my-combo',
|
|
77
|
-
labelText:'My Combo',
|
|
78
|
-
options: [{label: 'my-label-1', value: 'value-1'},{label: 'my-label-2', value: 'value-2'} ]
|
|
79
|
-
}) \%}
|
|
80
|
-
{\% endcall \%}
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
#### with prefilled value
|
|
65
|
+
### Example - Single mode - no API - no option selected
|
|
85
66
|
|
|
86
67
|
```njk
|
|
87
68
|
{\% call MdsCombobox({
|
|
88
69
|
id:'my-id',
|
|
89
70
|
name: 'my-combo',
|
|
90
71
|
labelText:'My Combo',
|
|
91
|
-
value: {value: 'value-2', label: 'my-label-2'},
|
|
92
|
-
options: [{label: 'my-label-1', value: 'value-1'},{label: 'my-label-2', value: 'value-2'} ]
|
|
93
72
|
}) \%}
|
|
73
|
+
<option value="value-1">My label 1</option>
|
|
74
|
+
<option value="value-2">My label 2</option>
|
|
94
75
|
{\% endcall \%}
|
|
95
76
|
|
|
96
77
|
```
|
|
97
78
|
|
|
98
|
-
|
|
79
|
+
#### with pre-selected value
|
|
99
80
|
|
|
100
81
|
```njk
|
|
101
82
|
{\% call MdsCombobox({
|
|
102
83
|
id:'my-id',
|
|
103
84
|
name: 'my-combo',
|
|
104
85
|
labelText:'My Combo',
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
fallbackTo:'input',
|
|
87
|
+
fallbackValue:'value-2'
|
|
107
88
|
}) \%}
|
|
89
|
+
<option value="value-1">My label 1</option>
|
|
90
|
+
<option value="value-2" selected>My label 2</option>
|
|
108
91
|
{\% endcall \%}
|
|
109
92
|
|
|
110
93
|
```
|
|
111
94
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Note the value should be an array when using multiple mode.
|
|
95
|
+
### Example - Multiple mode - no API
|
|
115
96
|
|
|
116
97
|
```njk
|
|
117
98
|
{\% call MdsCombobox({
|
|
118
99
|
id:'my-id',
|
|
119
100
|
name: 'my-combo',
|
|
120
101
|
labelText:'My Combo',
|
|
121
|
-
value: [{value: 'value-2', label: 'my-label-2'}],
|
|
122
|
-
options: [{label: 'my-label-1', value: 'value-1'},{label: 'my-label-2', value: 'value-2'} ],
|
|
123
102
|
multiple: true
|
|
124
103
|
}) \%}
|
|
104
|
+
<option value="value-1">My label 1</option>
|
|
105
|
+
<option value="value-2">My label 2</option>
|
|
125
106
|
{\% endcall \%}
|
|
126
107
|
|
|
127
108
|
```
|
|
@@ -138,8 +119,8 @@ like so `[{word: 'something', score: 12},...]`. It also used `?keyword` for the
|
|
|
138
119
|
labelText:'My Combo',
|
|
139
120
|
apiUrl: '/api/location-lookup',
|
|
140
121
|
apiQueryKey: 'keyword',
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
apiOptionLabelPath: 'word',
|
|
123
|
+
apiOptionValuePath: 'score'
|
|
143
124
|
}) \%}
|
|
144
125
|
{\% endcall \%}
|
|
145
126
|
|
|
@@ -152,12 +133,13 @@ like so `[{word: 'something', score: 12},...]`. It also used `?keyword` for the
|
|
|
152
133
|
id:'my-id',
|
|
153
134
|
name: 'my-combo',
|
|
154
135
|
labelText:'My Combo',
|
|
155
|
-
value: {word: 'my-label-2', score: 'value-2'},
|
|
156
136
|
apiUrl: '/api/location-lookup',
|
|
157
137
|
apiQueryKey: 'keyword',
|
|
158
|
-
|
|
159
|
-
|
|
138
|
+
apiOptionLabelPath: 'word',
|
|
139
|
+
apiOptionValuePath: 'score'
|
|
160
140
|
}) \%}
|
|
141
|
+
{# we can render selected options on the server even when using API #}
|
|
142
|
+
<option value="value-2" selected>My label 2</option>
|
|
161
143
|
{\% endcall \%}
|
|
162
144
|
|
|
163
145
|
```
|
|
@@ -183,18 +165,18 @@ options:
|
|
|
183
165
|
|
|
184
166
|
#### with prefilled value
|
|
185
167
|
|
|
186
|
-
Note the value is an array for multiple mode.
|
|
187
|
-
|
|
188
168
|
```njk
|
|
189
169
|
{\% call MdsCombobox({
|
|
190
170
|
id:'my-id',
|
|
191
171
|
name: 'my-combo',
|
|
192
172
|
labelText:'My Combo',
|
|
193
|
-
value: [{label: 'my-label-1', value: 'value-1'},{label: 'my-label-2', value: 'value-2'} ],
|
|
194
173
|
apiUrl: '/api/location-lookup',
|
|
195
174
|
apiOptionsPath: 'data',
|
|
196
175
|
multiple: true
|
|
197
176
|
}) \%}
|
|
177
|
+
{# we can render selected options on the server even when using API #}
|
|
178
|
+
<option value="value-1" selected>My label 1</option>
|
|
179
|
+
<option value="value-2" selected>My label 2</option>
|
|
198
180
|
{\% endcall \%}
|
|
199
181
|
|
|
200
182
|
```
|
|
@@ -208,3 +190,14 @@ When Javascript is not available, either a native combobox or native input will
|
|
|
208
190
|
aria-describedBy has been added to notify screen reader users how to interact with the autocomplete suggestions.
|
|
209
191
|
|
|
210
192
|
Note: There is a known issue with the `aria-activedescendant` attribute when using VoiceOver + Safari (it works on other browsers). VO doesn't read the current option. See https://bugs.webkit.org/show_bug.cgi?id=231724
|
|
193
|
+
|
|
194
|
+
## Vue only usage - `<mds-combobox/>`
|
|
195
|
+
|
|
196
|
+
### Props
|
|
197
|
+
|
|
198
|
+
- `value` : `{Array|Object}`, the selected option(s)
|
|
199
|
+
- `options`: Alterative to child Web Component's `<option/>` elements
|
|
200
|
+
|
|
201
|
+
### Events
|
|
202
|
+
|
|
203
|
+
- `update:value` & `change` : on value change
|