@descope-ui/descope-combo-box 0.0.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/e2e/descope-combo-box.spec.ts +462 -0
- package/package.json +33 -0
- package/project.json +7 -0
- package/src/component/ComboBoxClass.js +619 -0
- package/src/component/index.js +6 -0
- package/src/theme.js +93 -0
- package/stories/descope-combo-box.stories.js +180 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStoryUrl, loopConfig, loopPresets } from 'e2e-utils';
|
|
3
|
+
import { createComboBoxTestDriver } from 'test-drivers';
|
|
4
|
+
|
|
5
|
+
const storyName = 'descope-combo-box';
|
|
6
|
+
const componentName = 'descope-combo-box';
|
|
7
|
+
|
|
8
|
+
const BooleanValues = ['true', 'false'];
|
|
9
|
+
|
|
10
|
+
const componentAttributes = {
|
|
11
|
+
label: 'Number field label',
|
|
12
|
+
placeholder: 'Test placeholder',
|
|
13
|
+
size: ['xs', 'sm', 'md', 'lg'],
|
|
14
|
+
bordered: BooleanValues,
|
|
15
|
+
readonly: BooleanValues,
|
|
16
|
+
required: BooleanValues,
|
|
17
|
+
'full-width': BooleanValues,
|
|
18
|
+
disabled: 'true',
|
|
19
|
+
'default-value': 'itemId2',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const presets = {
|
|
23
|
+
'Render with alt item renderer': {
|
|
24
|
+
itemsSource: 'prop',
|
|
25
|
+
overrideRenderItem: 'true',
|
|
26
|
+
},
|
|
27
|
+
'Render with data property': { itemsSource: 'prop' },
|
|
28
|
+
'Render with data attribute': { itemsSource: 'attr' },
|
|
29
|
+
'Render with data property and default value': {
|
|
30
|
+
itemsSource: 'prop',
|
|
31
|
+
'default-value': 'itemId2',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const rtlPresets = {
|
|
36
|
+
'direction rtl': {
|
|
37
|
+
direction: 'rtl',
|
|
38
|
+
label: '-Test Label',
|
|
39
|
+
placeholder: '-Test Placeholder',
|
|
40
|
+
itemsSource: 'attr',
|
|
41
|
+
'data[0].displayName': '-Item 1',
|
|
42
|
+
'data[1].displayName': '-Item 2',
|
|
43
|
+
'data[1].label': '-Data Item 2',
|
|
44
|
+
},
|
|
45
|
+
'direction rtl required': {
|
|
46
|
+
direction: 'rtl',
|
|
47
|
+
label: '-Test Label',
|
|
48
|
+
placeholder: '-Test Placeholder',
|
|
49
|
+
itemsSource: 'attr',
|
|
50
|
+
'data[0].displayName': '-Item 1',
|
|
51
|
+
'data[1].displayName': '-Item 2',
|
|
52
|
+
'data[1].label': '-Data Item 2',
|
|
53
|
+
required: 'true',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const floatingLabelProps = {
|
|
58
|
+
'label-type': 'floating',
|
|
59
|
+
label: 'label-',
|
|
60
|
+
placeholder: 'placeholder',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const floatingLabelTypePresets = {
|
|
64
|
+
'floating label ltr': floatingLabelProps,
|
|
65
|
+
'floating label rtl': {
|
|
66
|
+
...floatingLabelProps,
|
|
67
|
+
direction: 'rtl',
|
|
68
|
+
},
|
|
69
|
+
'floating label ltr with value': {
|
|
70
|
+
...floatingLabelProps,
|
|
71
|
+
'default-value': 'itemId2',
|
|
72
|
+
direction: 'ltr',
|
|
73
|
+
},
|
|
74
|
+
'floating label rtl with value': {
|
|
75
|
+
...floatingLabelProps,
|
|
76
|
+
'default-value': 'itemId2',
|
|
77
|
+
direction: 'rtl',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
test.describe('theme', () => {
|
|
82
|
+
const { placeholder, ...restConfig } = componentAttributes;
|
|
83
|
+
|
|
84
|
+
test.describe('with placeholder', () => {
|
|
85
|
+
test.beforeEach(async ({ page }) => {
|
|
86
|
+
await page.goto(getStoryUrl(storyName, { placeholder }), {
|
|
87
|
+
waitUntil: 'networkidle',
|
|
88
|
+
});
|
|
89
|
+
await page.waitForSelector(componentName);
|
|
90
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
91
|
+
await component.clear();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('hover', async ({ page }) => {
|
|
95
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
96
|
+
await component.hover();
|
|
97
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('focus', async ({ page }) => {
|
|
101
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
102
|
+
await component.focus();
|
|
103
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
loopPresets(presets, (preset, name) => {
|
|
108
|
+
test.describe(name, () => {
|
|
109
|
+
test.beforeEach(async ({ page }) => {
|
|
110
|
+
await page.goto(getStoryUrl(storyName, preset), {
|
|
111
|
+
waitUntil: 'networkidle',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('hover', async ({ page }) => {
|
|
116
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
117
|
+
await component.hover();
|
|
118
|
+
expect(
|
|
119
|
+
await component.screenshot({ animations: 'disabled' }),
|
|
120
|
+
).toMatchSnapshot();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('focus', async ({ page }) => {
|
|
124
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
125
|
+
await component.focus();
|
|
126
|
+
expect(
|
|
127
|
+
await component.screenshot({ animations: 'disabled' }),
|
|
128
|
+
).toMatchSnapshot();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test.describe('with value', () => {
|
|
134
|
+
loopConfig(restConfig, (attr, value) => {
|
|
135
|
+
test.describe(`${attr}: ${value}`, () => {
|
|
136
|
+
test.beforeEach(async ({ page }) => {
|
|
137
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), {
|
|
138
|
+
waitUntil: 'networkidle',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const component = createComboBoxTestDriver(
|
|
142
|
+
page.locator(componentName),
|
|
143
|
+
);
|
|
144
|
+
component.setValue('itemId4');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('hover', async ({ page }) => {
|
|
148
|
+
const component = createComboBoxTestDriver(
|
|
149
|
+
page.locator(componentName),
|
|
150
|
+
);
|
|
151
|
+
await component.hover();
|
|
152
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('focus', async ({ page }) => {
|
|
156
|
+
const component = createComboBoxTestDriver(
|
|
157
|
+
page.locator(componentName),
|
|
158
|
+
);
|
|
159
|
+
await component.focus();
|
|
160
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test.describe('dropDown', () => {
|
|
167
|
+
loopConfig(
|
|
168
|
+
{
|
|
169
|
+
size: componentAttributes.size,
|
|
170
|
+
'full-width': componentAttributes['full-width'],
|
|
171
|
+
},
|
|
172
|
+
(attr, value) => {
|
|
173
|
+
test.describe(`${attr}: ${value}`, () => {
|
|
174
|
+
test.beforeEach(async ({ page }) => {
|
|
175
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), {
|
|
176
|
+
waitUntil: 'networkidle',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const component = createComboBoxTestDriver(
|
|
180
|
+
page.locator(componentName),
|
|
181
|
+
);
|
|
182
|
+
await component.openDropdown();
|
|
183
|
+
});
|
|
184
|
+
test('visible', async ({ page }) => {
|
|
185
|
+
const component = createComboBoxTestDriver(
|
|
186
|
+
page.locator(componentName),
|
|
187
|
+
);
|
|
188
|
+
await component.hover();
|
|
189
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test.describe('direction rtl', () => {
|
|
197
|
+
loopPresets(rtlPresets, (preset, name) => {
|
|
198
|
+
test(name, async ({ page }) => {
|
|
199
|
+
await page.goto(getStoryUrl(storyName, preset));
|
|
200
|
+
await page.waitForSelector(componentName);
|
|
201
|
+
const component = page.locator(componentName);
|
|
202
|
+
|
|
203
|
+
expect(
|
|
204
|
+
await component.screenshot({
|
|
205
|
+
animations: 'disabled',
|
|
206
|
+
timeout: 3000,
|
|
207
|
+
caret: 'hide',
|
|
208
|
+
}),
|
|
209
|
+
).toMatchSnapshot();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
test('with error', async ({ page }) => {
|
|
213
|
+
await page.goto(getStoryUrl(storyName, Object.values(rtlPresets)[1]), {
|
|
214
|
+
waitUntil: 'networkidle',
|
|
215
|
+
});
|
|
216
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
217
|
+
await page.getByRole('button').getByText('Submit').click();
|
|
218
|
+
expect(await component.screenshot({ delay: 1000 })).toMatchSnapshot();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('with open dropdown', async ({ page }) => {
|
|
222
|
+
await page.goto(getStoryUrl(storyName, Object.values(rtlPresets)[1]), {
|
|
223
|
+
waitUntil: 'networkidle',
|
|
224
|
+
});
|
|
225
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
226
|
+
await component.openDropdown();
|
|
227
|
+
await component.hover();
|
|
228
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('with open dropdown and value', async ({ page }) => {
|
|
232
|
+
await page.goto(getStoryUrl(storyName, Object.values(rtlPresets)[1]), {
|
|
233
|
+
waitUntil: 'networkidle',
|
|
234
|
+
});
|
|
235
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
236
|
+
await component.setValue('itemId2');
|
|
237
|
+
await component.openDropdown();
|
|
238
|
+
await component.hover();
|
|
239
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('with value', async ({ page }) => {
|
|
243
|
+
await page.goto(getStoryUrl(storyName, Object.values(rtlPresets)[1]), {
|
|
244
|
+
waitUntil: 'networkidle',
|
|
245
|
+
});
|
|
246
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
247
|
+
await component.setValue('itemId2');
|
|
248
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test.describe('logic', () => {
|
|
254
|
+
test.beforeEach(async ({ page }) => {
|
|
255
|
+
await page.goto(getStoryUrl(storyName, { 'full-width': true }), {
|
|
256
|
+
waitUntil: 'networkidle',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('select item', async ({ page }) => {
|
|
261
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
262
|
+
await component.openDropdown();
|
|
263
|
+
await component.selectItem("Achilles' Fateful Wrath");
|
|
264
|
+
|
|
265
|
+
expect(await component.getValue()).toBe('itemId2');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 'required' - not working as expected
|
|
269
|
+
|
|
270
|
+
test.describe('clear button', () => {
|
|
271
|
+
test('clear button visible when has value', async ({ page }) => {
|
|
272
|
+
await page.goto(getStoryUrl(storyName, { 'default-value': 'itemId2' }), {
|
|
273
|
+
waitUntil: 'networkidle',
|
|
274
|
+
});
|
|
275
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
276
|
+
await component.clearSelection();
|
|
277
|
+
expect(await component.getValue()).toBe('');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('loading state', async ({ page }) => {
|
|
282
|
+
await page.goto(getStoryUrl(storyName, {}));
|
|
283
|
+
const componentLocator = page.locator(componentName);
|
|
284
|
+
const component = createComboBoxTestDriver(componentLocator);
|
|
285
|
+
|
|
286
|
+
await componentLocator.evaluate((node: HTMLElement) => {
|
|
287
|
+
node.setAttribute('loading', 'true');
|
|
288
|
+
});
|
|
289
|
+
await component.openDropdown();
|
|
290
|
+
expect(
|
|
291
|
+
await component.dropDown.screenshot({ animations: 'disabled' }),
|
|
292
|
+
).toMatchSnapshot();
|
|
293
|
+
|
|
294
|
+
await componentLocator.evaluate((node: HTMLElement) => {
|
|
295
|
+
node.removeAttribute('loading');
|
|
296
|
+
});
|
|
297
|
+
await page.waitForTimeout(1000);
|
|
298
|
+
expect(
|
|
299
|
+
await component.dropDown.screenshot({ animations: 'disabled' }),
|
|
300
|
+
).toMatchSnapshot();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('allow custom value', async ({ page }) => {
|
|
304
|
+
await page.goto(getStoryUrl(storyName, { 'allow-custom-value': true }));
|
|
305
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
306
|
+
const value = 'new value';
|
|
307
|
+
await component.setValue(value);
|
|
308
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
309
|
+
expect(await component.getValue()).toBe(value);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test.describe('reset value', () => {
|
|
313
|
+
const testResetValue = async (
|
|
314
|
+
page: any,
|
|
315
|
+
allowCustomValue: boolean,
|
|
316
|
+
extraSteps?: (component: any) => Promise<void>,
|
|
317
|
+
) => {
|
|
318
|
+
const urlParams = allowCustomValue ? { 'allow-custom-value': true } : {};
|
|
319
|
+
await page.goto(getStoryUrl(storyName, urlParams));
|
|
320
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
321
|
+
await component.setValue('itemId2');
|
|
322
|
+
expect(await component.getValue()).toBe('itemId2');
|
|
323
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
324
|
+
await component.setValue('');
|
|
325
|
+
expect(await component.getValue()).toBe('');
|
|
326
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
327
|
+
|
|
328
|
+
if (extraSteps) {
|
|
329
|
+
await extraSteps(component);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
test('no custom value', async ({ page }) => {
|
|
334
|
+
await testResetValue(page, false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('allow custom value', async ({ page }) => {
|
|
338
|
+
await testResetValue(page, true, async (component) => {
|
|
339
|
+
await component.setValue('new value');
|
|
340
|
+
expect(await component.getValue()).toBe('new value');
|
|
341
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
342
|
+
await component.setValue('');
|
|
343
|
+
expect(await component.getValue()).toBe('');
|
|
344
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('error message with icon', async ({ page }) => {
|
|
350
|
+
await page.goto(
|
|
351
|
+
getStoryUrl(storyName, { required: true, errorMsgIcon: true }),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
355
|
+
|
|
356
|
+
await page.getByRole('button').getByText('Submit').click();
|
|
357
|
+
|
|
358
|
+
await component.blur();
|
|
359
|
+
|
|
360
|
+
expect(
|
|
361
|
+
await component.screenshot({
|
|
362
|
+
animations: 'disabled',
|
|
363
|
+
caret: 'hide',
|
|
364
|
+
delay: 3000,
|
|
365
|
+
}),
|
|
366
|
+
).toMatchSnapshot();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test.describe('variants', () => {
|
|
371
|
+
loopPresets(floatingLabelTypePresets, (preset, name) => {
|
|
372
|
+
test.describe(name, () => {
|
|
373
|
+
test.beforeEach(async ({ page }) => {
|
|
374
|
+
await page.goto(getStoryUrl(storyName, preset), {
|
|
375
|
+
waitUntil: 'networkidle',
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('hover', async ({ page }) => {
|
|
380
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
381
|
+
await component.setValue('itemId2');
|
|
382
|
+
await component.hover();
|
|
383
|
+
expect(
|
|
384
|
+
await component.screenshot({ animations: 'disabled' }),
|
|
385
|
+
).toMatchSnapshot();
|
|
386
|
+
});
|
|
387
|
+
test('focus', async ({ page }) => {
|
|
388
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
389
|
+
await component.focus();
|
|
390
|
+
expect(
|
|
391
|
+
await component.screenshot({ animations: 'disabled' }),
|
|
392
|
+
).toMatchSnapshot();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test.describe('hide toggle button', () => {
|
|
399
|
+
test('hide toggle button', async ({ page }) => {
|
|
400
|
+
await page.goto(getStoryUrl(storyName, { 'hide-toggle-button': true }));
|
|
401
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
402
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
403
|
+
await component.openDropdown();
|
|
404
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
405
|
+
await component.insertValue('itemname1');
|
|
406
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
407
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test.describe('data attribute', () => {
|
|
412
|
+
test('clear items', async ({ page }) => {
|
|
413
|
+
await page.goto(getStoryUrl(storyName, { itemsSource: 'prop' }));
|
|
414
|
+
const componentLocator = page.locator(componentName);
|
|
415
|
+
const component = createComboBoxTestDriver(componentLocator);
|
|
416
|
+
await component.openDropdown();
|
|
417
|
+
let items = await component.getItems();
|
|
418
|
+
expect(items).toHaveLength(4);
|
|
419
|
+
// Reduce data items
|
|
420
|
+
await componentLocator.evaluate((node: HTMLElement & { data: any[] }) => {
|
|
421
|
+
node.data = [
|
|
422
|
+
{ displayName: 'Item1', value: '1', label: 'data item 1' },
|
|
423
|
+
{ displayName: 'Item2', value: '2', label: 'data item 2' },
|
|
424
|
+
];
|
|
425
|
+
});
|
|
426
|
+
items = await component.getItems();
|
|
427
|
+
expect(items).toHaveLength(2);
|
|
428
|
+
// Clear data items
|
|
429
|
+
await componentLocator.evaluate((node: HTMLElement & { data: any[] }) => {
|
|
430
|
+
node.data = [];
|
|
431
|
+
});
|
|
432
|
+
items = await component.getItems();
|
|
433
|
+
expect(items).toHaveLength(0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('allow custom value with no items', async ({ page }) => {
|
|
437
|
+
await page.goto(
|
|
438
|
+
getStoryUrl(storyName, {
|
|
439
|
+
itemsSource: 'prop',
|
|
440
|
+
'allow-custom-value': true,
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
const componentLocator = page.locator(componentName);
|
|
444
|
+
const component = createComboBoxTestDriver(componentLocator);
|
|
445
|
+
// Clear data items
|
|
446
|
+
await componentLocator.evaluate((node: HTMLElement & { data: any[] }) => {
|
|
447
|
+
node.data = [];
|
|
448
|
+
});
|
|
449
|
+
await component.setValue('new value');
|
|
450
|
+
expect(await component.getValue()).toBe('new value');
|
|
451
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test.describe('override renderer', () => {
|
|
456
|
+
test('custom background', async ({ page }) => {
|
|
457
|
+
await page.goto(getStoryUrl(storyName, { overrideRenderer: true }));
|
|
458
|
+
const component = createComboBoxTestDriver(page.locator(componentName));
|
|
459
|
+
await component.openDropdown();
|
|
460
|
+
expect(await component.dropDown.screenshot()).toMatchSnapshot();
|
|
461
|
+
});
|
|
462
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-combo-box",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": "./src/component/index.js"
|
|
7
|
+
},
|
|
8
|
+
"./theme": {
|
|
9
|
+
"import": "./src/theme.js"
|
|
10
|
+
},
|
|
11
|
+
"./class": {
|
|
12
|
+
"import": "./src/component/ComboBoxClass.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@playwright/test": "1.38.1",
|
|
17
|
+
"e2e-utils": "0.0.1",
|
|
18
|
+
"test-drivers": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@vaadin/combo-box": "24.3.4",
|
|
22
|
+
"@descope-ui/common": "0.0.1",
|
|
23
|
+
"@descope-ui/theme-globals": "0.0.1",
|
|
24
|
+
"theme-input-wrapper": "0.0.1"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"link-workspace-packages": false
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "echo 'No tests defined' && exit 0",
|
|
31
|
+
"test:e2e": "echo 'No e2e tests defined' && exit 0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { compose } from '@descope-ui/common/utils';
|
|
2
|
+
import {
|
|
3
|
+
getComponentName,
|
|
4
|
+
observeAttributes,
|
|
5
|
+
observeChildren,
|
|
6
|
+
} from '@descope-ui/common/components-helpers';
|
|
7
|
+
import {
|
|
8
|
+
resetInputLabelPosition,
|
|
9
|
+
resetInputCursor,
|
|
10
|
+
resetInputPlaceholder,
|
|
11
|
+
resetInputReadonlyStyle,
|
|
12
|
+
useHostExternalPadding,
|
|
13
|
+
inputFloatingLabelStyle,
|
|
14
|
+
} from '@descope-ui/common/theme-helpers';
|
|
15
|
+
import {
|
|
16
|
+
createStyleMixin,
|
|
17
|
+
draggableMixin,
|
|
18
|
+
createProxy,
|
|
19
|
+
componentNameValidationMixin,
|
|
20
|
+
portalMixin,
|
|
21
|
+
proxyInputMixin,
|
|
22
|
+
} from '@descope-ui/common/components-mixins';
|
|
23
|
+
|
|
24
|
+
export const componentName = getComponentName('combo-box');
|
|
25
|
+
|
|
26
|
+
const ComboBoxMixin = (superclass) =>
|
|
27
|
+
class ComboBoxMixinClass extends superclass {
|
|
28
|
+
static get observedAttributes() {
|
|
29
|
+
return ['label-type'];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line class-methods-use-this
|
|
33
|
+
#renderItem = ({ displayName, value, label }) => {
|
|
34
|
+
return `<span data-name="${label}" data-id="${value}">${
|
|
35
|
+
displayName || label
|
|
36
|
+
}</span>`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
#data;
|
|
40
|
+
|
|
41
|
+
get defaultValue() {
|
|
42
|
+
return this.getAttribute('default-value');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get renderItem() {
|
|
46
|
+
return this.#renderItem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set renderItem(renderFn) {
|
|
50
|
+
this.#renderItem = renderFn;
|
|
51
|
+
this.renderItems();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set renderer(fn) {
|
|
55
|
+
// fn takes (root, comboBox, model) as arguments
|
|
56
|
+
this.baseElement.renderer = fn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get loading() {
|
|
60
|
+
return this.getAttribute('loading') === 'true';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set loading(val) {
|
|
64
|
+
if (val) {
|
|
65
|
+
this.setAttribute('loading', 'true');
|
|
66
|
+
} else {
|
|
67
|
+
this.removeAttribute('loading');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get data() {
|
|
72
|
+
if (this.#data) return this.#data;
|
|
73
|
+
|
|
74
|
+
const dataAttr = this.getAttribute('data');
|
|
75
|
+
|
|
76
|
+
if (dataAttr) {
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(dataAttr);
|
|
79
|
+
if (this.isValidDataType(data)) {
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.error(
|
|
85
|
+
'could not parse data string from attribute "data" -',
|
|
86
|
+
e.message,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
set data(data) {
|
|
95
|
+
if (this.isValidDataType(data)) {
|
|
96
|
+
this.#data = data;
|
|
97
|
+
this.renderItems();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// eslint-disable-next-line class-methods-use-this
|
|
102
|
+
isValidDataType(data) {
|
|
103
|
+
const isValid = Array.isArray(data);
|
|
104
|
+
if (!isValid) {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.error('data must be an array, received:', data);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return isValid;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getItemsTemplate() {
|
|
113
|
+
return this.data?.reduce?.(
|
|
114
|
+
(acc, item) => acc + (this.renderItem?.(item || {}) || ''),
|
|
115
|
+
'',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
renderItems() {
|
|
120
|
+
if (this.#data || this.getAttribute('data')) {
|
|
121
|
+
const template = this.getItemsTemplate();
|
|
122
|
+
this.innerHTML = template;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleSelectedItem() {
|
|
127
|
+
const { selectedItem } = this.baseElement;
|
|
128
|
+
const currentSelected = selectedItem?.['data-id'];
|
|
129
|
+
|
|
130
|
+
// If the selected item is still a child, there's no need to update the value
|
|
131
|
+
if (selectedItem && Array.from(this.children).includes(selectedItem)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// if previously selected item ID exists in current children, set it as selected
|
|
136
|
+
if (currentSelected) {
|
|
137
|
+
this.value = currentSelected;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// otherwise, if default value is specified, set default value as selected item
|
|
141
|
+
if (!this.value) {
|
|
142
|
+
this.setDefaultValue();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// eslint-disable-next-line class-methods-use-this
|
|
147
|
+
customValueTransformFn(val) {
|
|
148
|
+
return val;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// We want to override Vaadin's Combo Box value setter. This is needed since Vaadin couples between the
|
|
152
|
+
// field that it searches the value, and the finaly display value of the input.
|
|
153
|
+
// We provide a custom transform function to override that behavior.
|
|
154
|
+
setComboBoxDescriptor() {
|
|
155
|
+
const valueDescriptor = Object.getOwnPropertyDescriptor(
|
|
156
|
+
this.inputElement.constructor.prototype,
|
|
157
|
+
'value',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const comboBox = this;
|
|
161
|
+
|
|
162
|
+
Object.defineProperties(this.inputElement, {
|
|
163
|
+
value: {
|
|
164
|
+
...valueDescriptor,
|
|
165
|
+
set(val) {
|
|
166
|
+
if (
|
|
167
|
+
!comboBox.baseElement.items?.length &&
|
|
168
|
+
!comboBox.allowCustomValue
|
|
169
|
+
) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const transformedValue = comboBox.customValueTransformFn(val) || '';
|
|
174
|
+
|
|
175
|
+
if (transformedValue === this.value) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
valueDescriptor.set.call(this, transformedValue);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// vaadin api is to set props on their combo box node,
|
|
186
|
+
// in order to avoid it, we are passing the children of this component
|
|
187
|
+
// to the items & renderer props, so it will be used as the combo box items
|
|
188
|
+
#onChildrenChange() {
|
|
189
|
+
const items = Array.from(this.children);
|
|
190
|
+
|
|
191
|
+
// we want the data-name attribute to be accessible as an object attribute
|
|
192
|
+
items.forEach((node) => {
|
|
193
|
+
Object.defineProperty(node, 'data-name', {
|
|
194
|
+
value: node.getAttribute('data-name'),
|
|
195
|
+
configurable: true,
|
|
196
|
+
writable: true,
|
|
197
|
+
});
|
|
198
|
+
Object.defineProperty(node, 'data-id', {
|
|
199
|
+
value: node.getAttribute('data-id'),
|
|
200
|
+
configurable: true,
|
|
201
|
+
writable: true,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
this.baseElement.items = items;
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
// set timeout to ensure this runs after customValueTransformFn had the chance to be overriden
|
|
208
|
+
this.handleSelectedItem();
|
|
209
|
+
}, 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// the default vaadin behavior is to attach the overlay to the body when opened
|
|
213
|
+
// we do not want that because it's difficult to style the overlay in this way
|
|
214
|
+
// so we override it to open inside the shadow DOM
|
|
215
|
+
#overrideOverlaySettings() {
|
|
216
|
+
const overlay = this.baseElement.shadowRoot.querySelector(
|
|
217
|
+
'vaadin-combo-box-overlay',
|
|
218
|
+
);
|
|
219
|
+
overlay._attachOverlay = () => {
|
|
220
|
+
overlay.bringToFront();
|
|
221
|
+
};
|
|
222
|
+
overlay._detachOverlay = () => {};
|
|
223
|
+
overlay._enterModalState = () => {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#overrideRenderer() {
|
|
227
|
+
// use vaadin combobox custom renderer to render options as HTML
|
|
228
|
+
// and not via default renderer, which renders only the data-name's value
|
|
229
|
+
// in its own HTML template
|
|
230
|
+
this.baseElement.renderer = (root, combo, model) => {
|
|
231
|
+
// eslint-disable-next-line no-param-reassign
|
|
232
|
+
root.innerHTML = model.item.outerHTML;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
init() {
|
|
237
|
+
super.init?.();
|
|
238
|
+
|
|
239
|
+
// eslint-disable-next-line func-names
|
|
240
|
+
this.getValidity = function () {
|
|
241
|
+
if (!this.value && this.isRequired) {
|
|
242
|
+
return {
|
|
243
|
+
valueMissing: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
this.setComboBoxDescriptor();
|
|
250
|
+
this.#overrideOverlaySettings();
|
|
251
|
+
this.#overrideRenderer();
|
|
252
|
+
|
|
253
|
+
// Set up observers - order matters here since renderItems can clear innerHTML
|
|
254
|
+
observeAttributes(this, this.renderItems.bind(this), {
|
|
255
|
+
includeAttrs: ['data'],
|
|
256
|
+
});
|
|
257
|
+
observeChildren(this, this.#onChildrenChange.bind(this));
|
|
258
|
+
|
|
259
|
+
this.setDefaultValue();
|
|
260
|
+
|
|
261
|
+
this.baseElement.addEventListener('selected-item-changed', () => {
|
|
262
|
+
this.dispatchEvent(
|
|
263
|
+
new Event('input', { bubbles: true, composed: true }),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onLabelClick() {
|
|
269
|
+
if (this.isReadOnly || this.isDisabled) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.focus();
|
|
273
|
+
this.setAttribute('opened', 'true');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
277
|
+
super.attributeChangedCallback?.(attrName, oldValue, newValue);
|
|
278
|
+
|
|
279
|
+
if (oldValue !== newValue) {
|
|
280
|
+
if (attrName === 'label-type') {
|
|
281
|
+
if (newValue === 'floating') {
|
|
282
|
+
this.addEventListener('click', this.onLabelClick);
|
|
283
|
+
} else {
|
|
284
|
+
this.removeEventListener('click', this.onLabelClick);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
setDefaultValue() {
|
|
291
|
+
if (this.defaultValue) {
|
|
292
|
+
this.value = this.defaultValue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#getChildToSelect(val) {
|
|
297
|
+
return this.baseElement.items?.find((item) => item['data-id'] === val);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#preventSelectedItemChangeEventIfNeeded(val, selectedChild) {
|
|
301
|
+
// If the actual value didn't change, but the selected item did (the element changed),
|
|
302
|
+
// we want to stop the event propagation since it's not a real change
|
|
303
|
+
const shouldPreventItemChangeEvent =
|
|
304
|
+
val === this.value && selectedChild !== this.baseElement.selectedItem;
|
|
305
|
+
if (shouldPreventItemChangeEvent) {
|
|
306
|
+
this.baseElement.addEventListener(
|
|
307
|
+
'selected-item-changed',
|
|
308
|
+
(e) => {
|
|
309
|
+
e.stopImmediatePropagation();
|
|
310
|
+
},
|
|
311
|
+
{ once: true, capture: true },
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
set value(val) {
|
|
317
|
+
const selectedChild = this.#getChildToSelect(val);
|
|
318
|
+
this.#preventSelectedItemChangeEventIfNeeded(val, selectedChild);
|
|
319
|
+
if (val && selectedChild) {
|
|
320
|
+
this.baseElement.selectedItem = selectedChild;
|
|
321
|
+
} else if (!selectedChild && this.allowCustomValue) {
|
|
322
|
+
this.baseElement.value = val;
|
|
323
|
+
} else {
|
|
324
|
+
this.baseElement.selectedItem = undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get value() {
|
|
329
|
+
return this.baseElement.selectedItem?.['data-id'] || this.allowCustomValue
|
|
330
|
+
? this.baseElement.__data.value || ''
|
|
331
|
+
: '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
get allowCustomValue() {
|
|
335
|
+
return this.getAttribute('allow-custom-value') === 'true';
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const {
|
|
340
|
+
host,
|
|
341
|
+
inputField,
|
|
342
|
+
inputElement,
|
|
343
|
+
placeholder,
|
|
344
|
+
toggle,
|
|
345
|
+
clearButton,
|
|
346
|
+
label,
|
|
347
|
+
requiredIndicator,
|
|
348
|
+
helperText,
|
|
349
|
+
errorMessage,
|
|
350
|
+
} = {
|
|
351
|
+
host: { selector: () => ':host' },
|
|
352
|
+
inputField: { selector: '::part(input-field)' },
|
|
353
|
+
inputElement: { selector: 'input' },
|
|
354
|
+
placeholder: { selector: '> input:placeholder-shown' },
|
|
355
|
+
toggle: { selector: '::part(toggle-button)' },
|
|
356
|
+
clearButton: { selector: '::part(clear-button)' },
|
|
357
|
+
label: { selector: '::part(label)' },
|
|
358
|
+
requiredIndicator: {
|
|
359
|
+
selector: '[required]::part(required-indicator)::after',
|
|
360
|
+
},
|
|
361
|
+
helperText: { selector: '::part(helper-text)' },
|
|
362
|
+
errorMessage: { selector: '::part(error-message)' },
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const ComboBoxClass = compose(
|
|
366
|
+
createStyleMixin({
|
|
367
|
+
mappings: {
|
|
368
|
+
hostWidth: { ...host, property: 'width' },
|
|
369
|
+
hostDirection: { ...host, property: 'direction' },
|
|
370
|
+
// we apply font-size also on the host so we can set its width with em
|
|
371
|
+
fontSize: [{}, host],
|
|
372
|
+
fontFamily: [label, placeholder, inputField, helperText, errorMessage],
|
|
373
|
+
labelFontSize: { ...label, property: 'font-size' },
|
|
374
|
+
labelFontWeight: { ...label, property: 'font-weight' },
|
|
375
|
+
labelTextColor: [
|
|
376
|
+
{ ...label, property: 'color' },
|
|
377
|
+
{ ...requiredIndicator, property: 'color' },
|
|
378
|
+
],
|
|
379
|
+
errorMessageTextColor: { ...errorMessage, property: 'color' },
|
|
380
|
+
errorMessageIcon: { ...errorMessage, property: 'background-image' },
|
|
381
|
+
errorMessageIconSize: { ...errorMessage, property: 'background-size' },
|
|
382
|
+
errorMessageIconPadding: {
|
|
383
|
+
...errorMessage,
|
|
384
|
+
property: 'padding-inline-start',
|
|
385
|
+
},
|
|
386
|
+
errorMessageIconRepeat: {
|
|
387
|
+
...errorMessage,
|
|
388
|
+
property: 'background-repeat',
|
|
389
|
+
},
|
|
390
|
+
errorMessageIconPosition: {
|
|
391
|
+
...errorMessage,
|
|
392
|
+
property: 'background-position',
|
|
393
|
+
},
|
|
394
|
+
inputHeight: { ...inputField, property: 'height' },
|
|
395
|
+
inputBackgroundColor: { ...inputField, property: 'background-color' },
|
|
396
|
+
inputBorderColor: { ...inputField, property: 'border-color' },
|
|
397
|
+
inputBorderWidth: { ...inputField, property: 'border-width' },
|
|
398
|
+
inputBorderStyle: { ...inputField, property: 'border-style' },
|
|
399
|
+
inputBorderRadius: { ...inputField, property: 'border-radius' },
|
|
400
|
+
labelRequiredIndicator: { ...requiredIndicator, property: 'content' },
|
|
401
|
+
inputValueTextColor: { ...inputField, property: 'color' },
|
|
402
|
+
inputPlaceholderTextColor: { ...placeholder, property: 'color' },
|
|
403
|
+
inputDropdownButtonCursor: [
|
|
404
|
+
{ ...toggle, property: 'cursor' },
|
|
405
|
+
{ ...clearButton, property: 'cursor' },
|
|
406
|
+
],
|
|
407
|
+
inputDropdownButtonColor: [
|
|
408
|
+
{ ...toggle, property: 'color' },
|
|
409
|
+
{ ...clearButton, property: 'color' },
|
|
410
|
+
],
|
|
411
|
+
inputDropdownButtonSize: [
|
|
412
|
+
{ ...toggle, property: 'font-size' },
|
|
413
|
+
{ ...clearButton, property: 'font-size' },
|
|
414
|
+
],
|
|
415
|
+
inputDropdownButtonOffset: [
|
|
416
|
+
{ ...toggle, property: 'margin-right' },
|
|
417
|
+
{ ...toggle, property: 'margin-left' },
|
|
418
|
+
],
|
|
419
|
+
inputOutlineColor: { ...inputField, property: 'outline-color' },
|
|
420
|
+
inputOutlineWidth: { ...inputField, property: 'outline-width' },
|
|
421
|
+
inputOutlineStyle: { ...inputField, property: 'outline-style' },
|
|
422
|
+
inputOutlineOffset: { ...inputField, property: 'outline-offset' },
|
|
423
|
+
inputHorizontalPadding: [
|
|
424
|
+
{ ...inputElement, property: 'padding-left' },
|
|
425
|
+
{ ...inputElement, property: 'padding-right' },
|
|
426
|
+
],
|
|
427
|
+
|
|
428
|
+
labelPosition: { ...label, property: 'position' },
|
|
429
|
+
labelTopPosition: { ...label, property: 'top' },
|
|
430
|
+
labelHorizontalPosition: [
|
|
431
|
+
{ ...label, property: 'left' },
|
|
432
|
+
{ ...label, property: 'right' },
|
|
433
|
+
],
|
|
434
|
+
inputTransformY: { ...label, property: 'transform' },
|
|
435
|
+
inputTransition: { ...label, property: 'transition' },
|
|
436
|
+
marginInlineStart: { ...label, property: 'margin-inline-start' },
|
|
437
|
+
placeholderOpacity: { ...placeholder, property: 'opacity' },
|
|
438
|
+
inputVerticalAlignment: { ...inputField, property: 'align-items' },
|
|
439
|
+
valueInputHeight: { ...inputElement, property: 'height' },
|
|
440
|
+
valueInputMarginBottom: { ...inputElement, property: 'margin-bottom' },
|
|
441
|
+
|
|
442
|
+
// we need to use the variables from the portal mixin
|
|
443
|
+
// so we need to use an arrow function on the selector
|
|
444
|
+
// for that to work, because ComboBox is not available
|
|
445
|
+
// at this time.
|
|
446
|
+
overlayBackground: {
|
|
447
|
+
property: () => ComboBoxClass.cssVarList.overlay.backgroundColor,
|
|
448
|
+
},
|
|
449
|
+
overlayTextColor: {
|
|
450
|
+
property: () => ComboBoxClass.cssVarList.overlay.textColor,
|
|
451
|
+
},
|
|
452
|
+
overlayBorder: {
|
|
453
|
+
property: () => ComboBoxClass.cssVarList.overlay.border,
|
|
454
|
+
},
|
|
455
|
+
overlayFontSize: {
|
|
456
|
+
property: () => ComboBoxClass.cssVarList.overlay.fontSize,
|
|
457
|
+
},
|
|
458
|
+
overlayFontFamily: {
|
|
459
|
+
property: () => ComboBoxClass.cssVarList.overlay.fontFamily,
|
|
460
|
+
},
|
|
461
|
+
overlayCursor: {
|
|
462
|
+
property: () => ComboBoxClass.cssVarList.overlay.cursor,
|
|
463
|
+
},
|
|
464
|
+
overlayItemBoxShadow: {
|
|
465
|
+
property: () => ComboBoxClass.cssVarList.overlay.itemBoxShadow,
|
|
466
|
+
},
|
|
467
|
+
overlayItemPaddingInlineStart: {
|
|
468
|
+
property: () => ComboBoxClass.cssVarList.overlay.itemPaddingInlineStart,
|
|
469
|
+
},
|
|
470
|
+
overlayItemPaddingInlineEnd: {
|
|
471
|
+
property: () => ComboBoxClass.cssVarList.overlay.itemPaddingInlineEnd,
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
}),
|
|
475
|
+
draggableMixin,
|
|
476
|
+
portalMixin({
|
|
477
|
+
name: 'overlay',
|
|
478
|
+
selector: '',
|
|
479
|
+
mappings: {
|
|
480
|
+
backgroundColor: [
|
|
481
|
+
{ selector: 'vaadin-combo-box-scroller' },
|
|
482
|
+
{ selector: 'vaadin-combo-box-overlay::part(overlay)' },
|
|
483
|
+
],
|
|
484
|
+
minHeight: { selector: 'vaadin-combo-box-overlay' },
|
|
485
|
+
margin: { selector: 'vaadin-combo-box-overlay' },
|
|
486
|
+
cursor: { selector: 'vaadin-combo-box-item' },
|
|
487
|
+
fontFamily: { selector: 'vaadin-combo-box-item' },
|
|
488
|
+
textColor: { selector: 'vaadin-combo-box-item', property: 'color' },
|
|
489
|
+
fontSize: { selector: 'vaadin-combo-box-item' },
|
|
490
|
+
itemBoxShadow: {
|
|
491
|
+
selector: 'vaadin-combo-box-item',
|
|
492
|
+
property: 'box-shadow',
|
|
493
|
+
},
|
|
494
|
+
itemPaddingInlineStart: {
|
|
495
|
+
selector: 'vaadin-combo-box-item',
|
|
496
|
+
property: 'padding-inline-start',
|
|
497
|
+
},
|
|
498
|
+
itemPaddingInlineEnd: {
|
|
499
|
+
selector: 'vaadin-combo-box-item',
|
|
500
|
+
property: 'padding-inline-end',
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
loaderTop: {
|
|
504
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
505
|
+
property: 'top',
|
|
506
|
+
},
|
|
507
|
+
loaderLeft: {
|
|
508
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
509
|
+
property: 'left',
|
|
510
|
+
},
|
|
511
|
+
loaderRight: {
|
|
512
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
513
|
+
property: 'right',
|
|
514
|
+
},
|
|
515
|
+
loaderMargin: {
|
|
516
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
517
|
+
property: 'margin',
|
|
518
|
+
},
|
|
519
|
+
loaderWidth: {
|
|
520
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
521
|
+
property: 'width',
|
|
522
|
+
},
|
|
523
|
+
loaderHeight: {
|
|
524
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
525
|
+
property: 'height',
|
|
526
|
+
},
|
|
527
|
+
loaderBorder: {
|
|
528
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
529
|
+
property: 'border',
|
|
530
|
+
},
|
|
531
|
+
loaderBorderColor: {
|
|
532
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
533
|
+
property: 'border-color',
|
|
534
|
+
},
|
|
535
|
+
loaderBorderRadius: {
|
|
536
|
+
selector: 'vaadin-combo-box-overlay::part(loader)',
|
|
537
|
+
property: 'border-radius',
|
|
538
|
+
},
|
|
539
|
+
contentHeight: {
|
|
540
|
+
selector: 'vaadin-combo-box-overlay::part(content)',
|
|
541
|
+
property: 'height',
|
|
542
|
+
},
|
|
543
|
+
contentOpacity: {
|
|
544
|
+
selector: 'vaadin-combo-box-overlay::part(content)',
|
|
545
|
+
property: 'opacity',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
forward: {
|
|
549
|
+
include: false,
|
|
550
|
+
attributes: ['size'],
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
proxyInputMixin({
|
|
554
|
+
proxyProps: ['selectionStart'],
|
|
555
|
+
inputEvent: 'selected-item-changed',
|
|
556
|
+
}),
|
|
557
|
+
componentNameValidationMixin,
|
|
558
|
+
ComboBoxMixin,
|
|
559
|
+
)(
|
|
560
|
+
createProxy({
|
|
561
|
+
slots: ['', 'prefix'],
|
|
562
|
+
wrappedEleName: 'vaadin-combo-box',
|
|
563
|
+
style: () => `
|
|
564
|
+
:host {
|
|
565
|
+
display: inline-flex;
|
|
566
|
+
box-sizing: border-box;
|
|
567
|
+
-webkit-mask-image: none;
|
|
568
|
+
}
|
|
569
|
+
${useHostExternalPadding(ComboBoxClass.cssVarList)}
|
|
570
|
+
${resetInputReadonlyStyle('vaadin-combo-box')}
|
|
571
|
+
${resetInputPlaceholder('vaadin-combo-box')}
|
|
572
|
+
${resetInputCursor('vaadin-combo-box')}
|
|
573
|
+
|
|
574
|
+
vaadin-combo-box {
|
|
575
|
+
padding: 0;
|
|
576
|
+
width: 100%;
|
|
577
|
+
}
|
|
578
|
+
vaadin-combo-box::before {
|
|
579
|
+
height: initial;
|
|
580
|
+
}
|
|
581
|
+
vaadin-combo-box [slot="input"] {
|
|
582
|
+
-webkit-mask-image: none;
|
|
583
|
+
min-height: 0;
|
|
584
|
+
box-sizing: border-box;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
vaadin-combo-box::part(input-field) {
|
|
588
|
+
padding: 0;
|
|
589
|
+
box-shadow: none;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
vaadin-combo-box::part(toggle-button),
|
|
593
|
+
vaadin-combo-box::part(clear-button) {
|
|
594
|
+
align-self: center;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
vaadin-combo-box[hide-toggle-button="true"]::part(toggle-button) {
|
|
598
|
+
display: none;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
vaadin-combo-box[label-type="floating"]:not([focused])[readonly] > input:placeholder-shown {
|
|
602
|
+
opacity: 0;
|
|
603
|
+
}
|
|
604
|
+
vaadin-combo-box[label-type="floating"]:not([focused])[disabled] > input:placeholder-shown {
|
|
605
|
+
opacity: 0;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
${resetInputLabelPosition('vaadin-combo-box')}
|
|
609
|
+
${inputFloatingLabelStyle()}
|
|
610
|
+
`,
|
|
611
|
+
// Note: we exclude `size` to avoid overriding Vaadin's ComboBox property
|
|
612
|
+
// with the same name. Including it will cause Vaadin to calculate NaN size,
|
|
613
|
+
// and reset items to an empty array, and opening the list box with no items
|
|
614
|
+
// to display.
|
|
615
|
+
excludeAttrsSync: ['tabindex', 'size', 'data', 'loading'],
|
|
616
|
+
componentName,
|
|
617
|
+
includeForwardProps: ['items', 'renderer', 'selectedItem'],
|
|
618
|
+
}),
|
|
619
|
+
);
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import globals from '@descope-ui/theme-globals';
|
|
2
|
+
import { ComboBoxClass } from './component/ComboBoxClass';
|
|
3
|
+
import { getThemeRefs } from '@descope-ui/common/theme-helpers';
|
|
4
|
+
import { refs } from 'theme-input-wrapper';
|
|
5
|
+
|
|
6
|
+
const globalRefs = getThemeRefs(globals);
|
|
7
|
+
const vars = ComboBoxClass.cssVarList;
|
|
8
|
+
|
|
9
|
+
export const comboBox = {
|
|
10
|
+
[vars.hostWidth]: refs.width,
|
|
11
|
+
[vars.hostDirection]: refs.direction,
|
|
12
|
+
[vars.fontSize]: refs.fontSize,
|
|
13
|
+
[vars.fontFamily]: refs.fontFamily,
|
|
14
|
+
[vars.labelFontSize]: refs.labelFontSize,
|
|
15
|
+
[vars.labelFontWeight]: refs.labelFontWeight,
|
|
16
|
+
[vars.labelTextColor]: refs.labelTextColor,
|
|
17
|
+
[vars.errorMessageTextColor]: refs.errorMessageTextColor,
|
|
18
|
+
[vars.inputBorderColor]: refs.borderColor,
|
|
19
|
+
[vars.inputBorderWidth]: refs.borderWidth,
|
|
20
|
+
[vars.inputBorderStyle]: refs.borderStyle,
|
|
21
|
+
[vars.inputBorderRadius]: refs.borderRadius,
|
|
22
|
+
[vars.inputOutlineColor]: refs.outlineColor,
|
|
23
|
+
[vars.inputOutlineOffset]: refs.outlineOffset,
|
|
24
|
+
[vars.inputOutlineWidth]: refs.outlineWidth,
|
|
25
|
+
[vars.inputOutlineStyle]: refs.outlineStyle,
|
|
26
|
+
[vars.labelRequiredIndicator]: refs.requiredIndicator,
|
|
27
|
+
[vars.inputValueTextColor]: refs.valueTextColor,
|
|
28
|
+
[vars.inputPlaceholderTextColor]: refs.placeholderTextColor,
|
|
29
|
+
[vars.inputBackgroundColor]: refs.backgroundColor,
|
|
30
|
+
[vars.inputHorizontalPadding]: refs.horizontalPadding,
|
|
31
|
+
[vars.inputHeight]: refs.inputHeight,
|
|
32
|
+
[vars.inputDropdownButtonColor]: globalRefs.colors.surface.dark,
|
|
33
|
+
[vars.inputDropdownButtonCursor]: 'pointer',
|
|
34
|
+
[vars.inputDropdownButtonSize]: refs.toggleButtonSize,
|
|
35
|
+
[vars.inputDropdownButtonOffset]: globalRefs.spacing.xs,
|
|
36
|
+
[vars.overlayItemPaddingInlineStart]: globalRefs.spacing.xs,
|
|
37
|
+
[vars.overlayItemPaddingInlineEnd]: globalRefs.spacing.lg,
|
|
38
|
+
[vars.labelPosition]: refs.labelPosition,
|
|
39
|
+
[vars.labelTopPosition]: refs.labelTopPosition,
|
|
40
|
+
[vars.labelHorizontalPosition]: refs.labelHorizontalPosition,
|
|
41
|
+
[vars.inputTransformY]: refs.inputTransformY,
|
|
42
|
+
[vars.inputTransition]: refs.inputTransition,
|
|
43
|
+
[vars.marginInlineStart]: refs.marginInlineStart,
|
|
44
|
+
[vars.placeholderOpacity]: refs.placeholderOpacity,
|
|
45
|
+
[vars.inputVerticalAlignment]: refs.inputVerticalAlignment,
|
|
46
|
+
[vars.valueInputHeight]: refs.valueInputHeight,
|
|
47
|
+
[vars.valueInputMarginBottom]: refs.valueInputMarginBottom,
|
|
48
|
+
|
|
49
|
+
// error message icon
|
|
50
|
+
[vars.errorMessageIcon]: refs.errorMessageIcon,
|
|
51
|
+
[vars.errorMessageIconSize]: refs.errorMessageIconSize,
|
|
52
|
+
[vars.errorMessageIconPadding]: refs.errorMessageIconPadding,
|
|
53
|
+
[vars.errorMessageIconRepeat]: refs.errorMessageIconRepeat,
|
|
54
|
+
[vars.errorMessageIconPosition]: refs.errorMessageIconPosition,
|
|
55
|
+
|
|
56
|
+
_readonly: {
|
|
57
|
+
[vars.inputDropdownButtonCursor]: 'default',
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Overlay theme exposed via the component:
|
|
61
|
+
[vars.overlayFontSize]: refs.fontSize,
|
|
62
|
+
[vars.overlayFontFamily]: refs.fontFamily,
|
|
63
|
+
[vars.overlayCursor]: 'pointer',
|
|
64
|
+
[vars.overlayItemBoxShadow]: 'none',
|
|
65
|
+
[vars.overlayBackground]: refs.backgroundColor,
|
|
66
|
+
[vars.overlayTextColor]: refs.valueTextColor,
|
|
67
|
+
|
|
68
|
+
// Overlay direct theme:
|
|
69
|
+
[vars.overlay.minHeight]: '400px',
|
|
70
|
+
[vars.overlay.margin]: '0',
|
|
71
|
+
|
|
72
|
+
[vars.overlay.contentHeight]: '100%',
|
|
73
|
+
[vars.overlay.contentOpacity]: '1',
|
|
74
|
+
_loading: {
|
|
75
|
+
[vars.overlay.loaderTop]: '50%',
|
|
76
|
+
[vars.overlay.loaderLeft]: '50%',
|
|
77
|
+
[vars.overlay.loaderRight]: 'auto',
|
|
78
|
+
// Margin has to be negative to center the loader, "transform" can't be used because the animation uses it
|
|
79
|
+
// Margin has to be half of the width/height of the loader to center it
|
|
80
|
+
[vars.overlay.loaderMargin]: '-15px 0 0 -15px',
|
|
81
|
+
[vars.overlay.loaderWidth]: '30px',
|
|
82
|
+
[vars.overlay.loaderHeight]: '30px',
|
|
83
|
+
[vars.overlay.loaderBorder]: '2px solid transparent',
|
|
84
|
+
[vars.overlay
|
|
85
|
+
.loaderBorderColor]: `${globalRefs.colors.primary.highlight} ${globalRefs.colors.primary.highlight} ${globalRefs.colors.primary.main} ${globalRefs.colors.primary.main}`,
|
|
86
|
+
[vars.overlay.loaderBorderRadius]: '50%',
|
|
87
|
+
[vars.overlay.contentHeight]: '100px',
|
|
88
|
+
[vars.overlay.contentOpacity]: '0',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default comboBox;
|
|
93
|
+
export { vars };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint no-param-reassign: 0 */
|
|
2
|
+
|
|
3
|
+
import { componentName } from '../src/component/ComboBoxClass';
|
|
4
|
+
import { withForm } from '@descope-ui/common/sb-helpers';
|
|
5
|
+
import {
|
|
6
|
+
labelControl,
|
|
7
|
+
placeholderControl,
|
|
8
|
+
sizeControl,
|
|
9
|
+
fullWidthControl,
|
|
10
|
+
directionControl,
|
|
11
|
+
disabledControl,
|
|
12
|
+
readOnlyControl,
|
|
13
|
+
requiredControl,
|
|
14
|
+
borderedControl,
|
|
15
|
+
loadingControl,
|
|
16
|
+
errorMissingValueControl,
|
|
17
|
+
overrideRenderItemControl,
|
|
18
|
+
itemsSourceControl,
|
|
19
|
+
inputLabelTypeControl,
|
|
20
|
+
errorMessageIconControl,
|
|
21
|
+
errorMessageIconAttrs,
|
|
22
|
+
} from '@descope-ui/common/sb-controls';
|
|
23
|
+
|
|
24
|
+
const Template = ({
|
|
25
|
+
label,
|
|
26
|
+
placeholder,
|
|
27
|
+
size,
|
|
28
|
+
bordered,
|
|
29
|
+
'full-width': fullWidth,
|
|
30
|
+
readonly,
|
|
31
|
+
required,
|
|
32
|
+
direction,
|
|
33
|
+
disabled,
|
|
34
|
+
data,
|
|
35
|
+
overrideRenderItem,
|
|
36
|
+
itemsSource,
|
|
37
|
+
loading,
|
|
38
|
+
'default-value': defaultValue,
|
|
39
|
+
'data-errormessage-value-missing': customErrorMissingValue,
|
|
40
|
+
'label-type': labelType,
|
|
41
|
+
'hide-toggle-button': hideToggleButton,
|
|
42
|
+
'allow-custom-value': allowCustomValue,
|
|
43
|
+
overrideRenderer,
|
|
44
|
+
errorMsgIcon,
|
|
45
|
+
}) => {
|
|
46
|
+
let serializedData;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
serializedData = JSON.stringify(data);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// do nothing
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// this is another way to pass data to the component (prop instead of attr)
|
|
55
|
+
const dataFromPropScript = `
|
|
56
|
+
<script>
|
|
57
|
+
document.querySelector("descope-combo-box").data = ${serializedData.replace(
|
|
58
|
+
/\n/g,
|
|
59
|
+
'',
|
|
60
|
+
)}
|
|
61
|
+
</script>`;
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line no-shadow
|
|
64
|
+
const altRenderItem = ({ displayName, label, value }) =>
|
|
65
|
+
`<span data-name="${displayName}" data-value="${value}">${label}</span>`;
|
|
66
|
+
|
|
67
|
+
// this allows us to update the render item function
|
|
68
|
+
const overrideRenderItemScript = `
|
|
69
|
+
<script>
|
|
70
|
+
document.querySelector("descope-combo-box").renderItem = ${altRenderItem
|
|
71
|
+
.toString()
|
|
72
|
+
.replace(/\n/g, '')}
|
|
73
|
+
</script>`;
|
|
74
|
+
|
|
75
|
+
const altRenderer = (root, combo, model) => {
|
|
76
|
+
if (model.index === 1) {
|
|
77
|
+
root.style.backgroundColor = '#008000';
|
|
78
|
+
root.style.color = '#ffffff';
|
|
79
|
+
}
|
|
80
|
+
root.innerHTML = model.item.outerHTML;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const overrideRendererScript = `
|
|
84
|
+
<script>
|
|
85
|
+
document.querySelector("descope-combo-box").renderer = ${altRenderer
|
|
86
|
+
.toString()
|
|
87
|
+
.replace(/\n/g, '')}
|
|
88
|
+
</script>`;
|
|
89
|
+
|
|
90
|
+
return withForm(`
|
|
91
|
+
<descope-combo-box
|
|
92
|
+
clear-button-visible
|
|
93
|
+
data='${(itemsSource === 'attr' && serializedData) || ''}'
|
|
94
|
+
size="${size}"
|
|
95
|
+
bordered="${bordered}"
|
|
96
|
+
item-label-path="data-name"
|
|
97
|
+
item-value-path="data-id"
|
|
98
|
+
label="${label || ''}"
|
|
99
|
+
placeholder="${placeholder || ''}"
|
|
100
|
+
default-value="${defaultValue || ''}"
|
|
101
|
+
required="${required || false}"
|
|
102
|
+
full-width="${fullWidth || false}"
|
|
103
|
+
readonly="${readonly || false}"
|
|
104
|
+
disabled="${disabled || false}"
|
|
105
|
+
loading="${loading || false}"
|
|
106
|
+
data-errormessage-value-missing="${customErrorMissingValue || ''}"
|
|
107
|
+
st-host-direction="${direction ?? ''}"
|
|
108
|
+
label-type="${labelType || ''}"
|
|
109
|
+
hide-toggle-button="${hideToggleButton || false}"
|
|
110
|
+
allow-custom-value="${allowCustomValue || false}"
|
|
111
|
+
${errorMsgIcon ? errorMessageIconAttrs : ''}
|
|
112
|
+
>
|
|
113
|
+
<span data-name="ItemName1" data-id="itemId1">Trojan War Heroes' Valor</span>
|
|
114
|
+
<span data-name="ItemName2" data-id="itemId2">Achilles' Fateful Wrath</span>
|
|
115
|
+
<span data-name="ItemName3" data-id="itemId3">Epic Battle of Gods</span>
|
|
116
|
+
<span data-name="ItemName4" data-id="itemId4">Hector's Brave Sacrifice</span>
|
|
117
|
+
<span data-name="ItemName5" data-id="itemId5">Trojan Horse Deception</span>
|
|
118
|
+
<span data-name="ItemName6" data-id="itemId6">Agamemnon's Royal Command</span>
|
|
119
|
+
<span data-name="ItemName7" data-id="itemId7">Odysseus' Cunning Strategy</span>
|
|
120
|
+
<span data-name="ItemName8" data-id="itemId8">Helen's Beauty's Curse</span>
|
|
121
|
+
<span data-name="ItemName9" data-id="itemId9">Achilles' Heel Weakness</span>
|
|
122
|
+
<span data-name="ItemName10" data-id="itemId10">Epic Poem of Ancient</span>
|
|
123
|
+
</descope-combo-box>
|
|
124
|
+
${itemsSource === 'prop' ? dataFromPropScript : ''}
|
|
125
|
+
${overrideRenderItem ? overrideRenderItemScript : ''}
|
|
126
|
+
${overrideRenderer ? overrideRendererScript : ''}
|
|
127
|
+
`);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default {
|
|
131
|
+
component: componentName,
|
|
132
|
+
title: 'descope-combo-box',
|
|
133
|
+
argTypes: {
|
|
134
|
+
...labelControl,
|
|
135
|
+
...placeholderControl,
|
|
136
|
+
...inputLabelTypeControl,
|
|
137
|
+
...sizeControl,
|
|
138
|
+
...fullWidthControl,
|
|
139
|
+
...disabledControl,
|
|
140
|
+
...readOnlyControl,
|
|
141
|
+
...requiredControl,
|
|
142
|
+
...borderedControl,
|
|
143
|
+
...errorMissingValueControl,
|
|
144
|
+
...overrideRenderItemControl,
|
|
145
|
+
...itemsSourceControl,
|
|
146
|
+
...directionControl,
|
|
147
|
+
...loadingControl,
|
|
148
|
+
...errorMessageIconControl,
|
|
149
|
+
'default-value': {
|
|
150
|
+
name: 'Default Value',
|
|
151
|
+
control: { type: 'text' },
|
|
152
|
+
},
|
|
153
|
+
'hide-toggle-button': {
|
|
154
|
+
name: 'Hide Toggle Button',
|
|
155
|
+
control: { type: 'boolean' },
|
|
156
|
+
},
|
|
157
|
+
'allow-custom-value': {
|
|
158
|
+
name: 'Allow Custom Value',
|
|
159
|
+
control: { type: 'boolean' },
|
|
160
|
+
},
|
|
161
|
+
overrideRenderer: {
|
|
162
|
+
name: 'Override the default renderer function',
|
|
163
|
+
control: { type: 'boolean' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const Default = Template.bind({});
|
|
169
|
+
|
|
170
|
+
Default.args = {
|
|
171
|
+
size: 'md',
|
|
172
|
+
bordered: true,
|
|
173
|
+
loading: false,
|
|
174
|
+
data: [
|
|
175
|
+
{ displayName: 'Item1', value: '1', label: 'data item 1' },
|
|
176
|
+
{ displayName: 'Item2', value: 'itemId2', label: 'data item 2' },
|
|
177
|
+
{ displayName: 'Item3', value: '3', label: 'data item 3' },
|
|
178
|
+
{ displayName: 'Item4', value: '4', label: 'data item 4' },
|
|
179
|
+
],
|
|
180
|
+
};
|