vira 32.4.0 → 33.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.
Files changed (58) hide show
  1. package/dist/directives/index.d.ts +2 -0
  2. package/dist/directives/index.js +2 -0
  3. package/dist/directives/popover.directive.d.ts +29 -0
  4. package/dist/directives/popover.directive.js +45 -0
  5. package/dist/directives/tooltip.directive.d.ts +60 -0
  6. package/dist/directives/tooltip.directive.js +99 -0
  7. package/dist/elements/color/vira-color-picker.element.d.ts +1 -1
  8. package/dist/elements/color/vira-color-picker.element.js +17 -15
  9. package/dist/elements/index.d.ts +5 -5
  10. package/dist/elements/index.js +5 -5
  11. package/dist/elements/{pop-up → popover}/vira-menu-item.element.d.ts +4 -4
  12. package/dist/elements/{pop-up → popover}/vira-menu-item.element.js +12 -3
  13. package/dist/elements/popover/vira-menu-trigger.element.d.ts +56 -0
  14. package/dist/elements/{pop-up → popover}/vira-menu-trigger.element.js +19 -30
  15. package/dist/elements/popover/vira-menu.element.d.ts +35 -0
  16. package/dist/elements/popover/vira-menu.element.js +262 -0
  17. package/dist/elements/popover/vira-popover-trigger.element.d.ts +50 -0
  18. package/dist/elements/popover/vira-popover-trigger.element.js +307 -0
  19. package/dist/elements/popover/vira-tooltip.element.d.ts +10 -0
  20. package/dist/elements/popover/vira-tooltip.element.js +176 -0
  21. package/dist/elements/vira-drawer.element.js +1 -1
  22. package/dist/elements/vira-dropdown.element.d.ts +40 -13
  23. package/dist/elements/vira-dropdown.element.js +375 -69
  24. package/dist/elements/vira-form.element.js +10 -16
  25. package/dist/elements/vira-json-form.element.js +40 -36
  26. package/dist/elements/vira-modal.element.js +1 -1
  27. package/dist/elements/vira-tabs.element.d.ts +3 -9
  28. package/dist/elements/vira-tabs.element.js +5 -6
  29. package/dist/elements/vira-theme-switcher.element.js +3 -3
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +1 -0
  32. package/dist/util/anchored-popover.d.ts +40 -0
  33. package/dist/util/anchored-popover.js +70 -0
  34. package/dist/util/fuzzy-match.d.ts +16 -0
  35. package/dist/util/fuzzy-match.js +24 -0
  36. package/dist/util/index.d.ts +5 -3
  37. package/dist/util/index.js +5 -3
  38. package/dist/util/{pop-up-helpers.d.ts → menu-helpers.d.ts} +8 -19
  39. package/dist/util/{pop-up-helpers.js → menu-helpers.js} +12 -19
  40. package/dist/util/popover-manager.d.ts +123 -0
  41. package/dist/util/popover-manager.js +414 -0
  42. package/dist/util/vira-dropdown-option.d.ts +30 -0
  43. package/dist/util/vira-dropdown-option.js +8 -0
  44. package/dist/util/vira-form-fields.d.ts +2 -2
  45. package/dist/util/vira-theme-client.d.ts +1 -1
  46. package/dist/util/vira-theme-client.js +3 -3
  47. package/package.json +2 -1
  48. package/dist/elements/pop-up/vira-menu-trigger.element.d.ts +0 -45
  49. package/dist/elements/pop-up/vira-menu.element.d.ts +0 -36
  50. package/dist/elements/pop-up/vira-menu.element.js +0 -88
  51. package/dist/elements/pop-up/vira-pop-up-trigger.element.d.ts +0 -109
  52. package/dist/elements/pop-up/vira-pop-up-trigger.element.js +0 -408
  53. package/dist/elements/vira-select.element.d.ts +0 -44
  54. package/dist/elements/vira-select.element.js +0 -418
  55. package/dist/util/pop-up-manager.d.ts +0 -200
  56. package/dist/util/pop-up-manager.js +0 -258
  57. package/dist/util/vira-select-option.d.ts +0 -31
  58. package/dist/util/vira-select-option.js +0 -8
@@ -1,18 +1,23 @@
1
1
  import { check } from '@augment-vir/assert';
2
2
  import { filterMap, randomString } from '@augment-vir/common';
3
3
  import { extractEventTarget } from '@augment-vir/web';
4
- import { classMap, css, defineElementEvent, html, ifDefined, listen, nothing, testId, } from 'element-vir';
4
+ import { nav, NavDirection } from 'device-navigation';
5
+ import { attributes, classMap, css, defineElementEvent, html, ifDefined, listen, nothing, testId, } from 'element-vir';
5
6
  import { ChevronUp16Icon } from '../icons/index.js';
7
+ import { createFocusStyles } from '../styles/focus.js';
6
8
  import { viraFormCssVars } from '../styles/form-styles.js';
7
- import { noUserSelect, viraAnimationDurations } from '../styles/index.js';
9
+ import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
10
+ import { noNativeFormStyles, noUserSelect, viraAnimationDurations } from '../styles/index.js';
8
11
  import { defineViraElement } from '../util/define-vira-element.js';
9
- import { renderMenuItemEntries } from '../util/pop-up-helpers.js';
10
- import { ViraMenuItem } from './pop-up/vira-menu-item.element.js';
11
- import { ViraMenu, ViraMenuPopUpDirection } from './pop-up/vira-menu.element.js';
12
- import { HorizontalAnchor, ViraPopUpTrigger, } from './pop-up/vira-pop-up-trigger.element.js';
12
+ import { fuzzyMatch } from '../util/fuzzy-match.js';
13
+ import { renderMenuItemEntries } from '../util/menu-helpers.js';
14
+ import { isViraDropdownOptionGroup, } from '../util/vira-dropdown-option.js';
15
+ import { ViraMenuItem } from './popover/vira-menu-item.element.js';
16
+ import { ViraMenu, ViraMenuCornerStyle } from './popover/vira-menu.element.js';
17
+ import { ViraPopoverTrigger } from './popover/vira-popover-trigger.element.js';
13
18
  import { ViraIcon } from './vira-icon.element.js';
14
19
  /**
15
- * A dropdown element that uses pop-up menus.
20
+ * A dropdown element that uses popover menus.
16
21
  *
17
22
  * @category Dropdown
18
23
  * @category Elements
@@ -23,18 +28,19 @@ export const ViraDropdown = defineViraElement()({
23
28
  testIds: [
24
29
  'leadingIcon',
25
30
  'prefixText',
31
+ 'searchInput',
26
32
  'trigger',
27
33
  ],
28
34
  styles: css `
29
35
  :host {
30
36
  display: inline-flex;
31
37
  vertical-align: middle;
32
- width: 256px;
38
+ width: 224px;
33
39
  position: relative;
34
40
  max-width: 100%;
35
41
  }
36
42
 
37
- ${ViraPopUpTrigger} {
43
+ ${ViraPopoverTrigger} {
38
44
  width: 100%;
39
45
  }
40
46
 
@@ -44,6 +50,38 @@ export const ViraDropdown = defineViraElement()({
44
50
  white-space: nowrap;
45
51
  }
46
52
 
53
+ .search-input {
54
+ ${noNativeFormStyles};
55
+ flex-grow: 1;
56
+ min-width: 0;
57
+ align-self: stretch;
58
+ text-overflow: ellipsis;
59
+ cursor: inherit;
60
+ user-select: text;
61
+ -webkit-user-select: text;
62
+ outline: none;
63
+
64
+ &::placeholder {
65
+ color: inherit;
66
+ opacity: 0.4;
67
+ }
68
+ }
69
+
70
+ .open .search-input {
71
+ cursor: text;
72
+ }
73
+
74
+ .dropdown-trigger:has(.search-input:focus-visible:not([disabled])) {
75
+ position: relative;
76
+
77
+ &::after {
78
+ ${createFocusStyles({
79
+ elementBorderSize: '1px',
80
+ noNesting: true,
81
+ })}
82
+ }
83
+ }
84
+
47
85
  .trigger-icon {
48
86
  width: 24px;
49
87
  aspect-ratio: 1;
@@ -60,18 +98,8 @@ export const ViraDropdown = defineViraElement()({
60
98
  justify-content: flex-end;
61
99
  }
62
100
 
63
- .open {
64
- & .trigger-icon {
65
- transform: rotate(0);
66
- }
67
-
68
- &:not(.open-upwards).dropdown-trigger {
69
- border-bottom-left-radius: 0;
70
- }
71
-
72
- &.open-upwards.dropdown-trigger {
73
- border-top-left-radius: 0;
74
- }
101
+ .open .trigger-icon {
102
+ transform: rotate(0);
75
103
  }
76
104
 
77
105
  .dropdown-trigger {
@@ -84,13 +112,50 @@ export const ViraDropdown = defineViraElement()({
84
112
  gap: 8px;
85
113
  text-align: left;
86
114
  align-items: center;
87
- padding: 3px;
88
- padding-left: 10px;
115
+ min-height: ${viraSizeHeights[ViraSize.Medium]}px;
116
+ padding: 0 3px 0 10px;
89
117
  border-radius: ${viraFormCssVars['vira-form-radius'].value};
90
118
  background-color: ${viraFormCssVars['vira-form-background-color'].value};
91
119
  color: ${viraFormCssVars['vira-form-foreground-color'].value};
92
120
  }
93
121
 
122
+ .dropdown-wrapper {
123
+ display: flex;
124
+ width: 100%;
125
+ }
126
+
127
+ .has-error .dropdown-trigger {
128
+ border-color: ${viraFormCssVars['vira-form-error-color'].value};
129
+ }
130
+
131
+ .option-group-label,
132
+ .no-options {
133
+ ${noUserSelect};
134
+ /* Aligns with the label text of each ViraMenuItem, past its check icon. */
135
+ padding: 8px 12px 4px
136
+ calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
137
+ opacity: 0.6;
138
+ text-align: left;
139
+ }
140
+
141
+ .option-group-label {
142
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
143
+ }
144
+
145
+ .no-options {
146
+ cursor: default;
147
+ padding-bottom: 8px;
148
+ }
149
+
150
+ ${ViraMenuItem}.option-group-label {
151
+ padding: 0 0 0 calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 2);
152
+ ${ViraMenuItem.cssVars['vira-menu-item-padding'].name}: 8px 12px 4px 0;
153
+ }
154
+
155
+ .readonly-value {
156
+ overflow-wrap: anywhere;
157
+ }
158
+
94
159
  .using-placeholder {
95
160
  opacity: 0.4;
96
161
  }
@@ -114,22 +179,29 @@ export const ViraDropdown = defineViraElement()({
114
179
  events: {
115
180
  /** Emits all currently selected values. */
116
181
  selectedValuesChange: defineElementEvent(),
182
+ /** `true` when the popover just opened, `false` when it just closed. */
117
183
  openChange: defineElementEvent(),
118
184
  },
119
185
  state() {
120
186
  return {
121
- /** `undefined` means the pop up is not currently showing. */
122
- showPopUpResult: undefined,
187
+ isOpen: false,
123
188
  shouldSelectOnMouseUp: false,
124
189
  /**
125
190
  * Used to couple the label and trigger together. This is not applied if no label is
126
191
  * provided.
127
192
  */
128
193
  randomId: randomString(32),
194
+ /** The text typed into the search input while the dropdown is open. */
195
+ searchText: '',
196
+ navController: undefined,
197
+ popoverManager: undefined,
129
198
  };
130
199
  },
131
- render({ state, inputs, dispatch, events, updateState, testIds }) {
132
- const selectedOptions = filterMap(inputs.selected, (selectedValue) => inputs.options.find((option) => option.value === selectedValue), check.isTruthy);
200
+ render({ state, inputs, dispatch, events, updateState, testIds, host }) {
201
+ const flatOptions = inputs.options.flatMap((entry) => {
202
+ return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
203
+ });
204
+ const selectedOptions = filterMap(inputs.selected, (selectedValue) => flatOptions.find((option) => option.value === selectedValue), check.isTruthy);
133
205
  const leadingIconTemplate = inputs.icon
134
206
  ? html `
135
207
  <${ViraIcon.assign({
@@ -165,13 +237,115 @@ export const ViraDropdown = defineViraElement()({
165
237
  detail: newSelectedValues,
166
238
  }));
167
239
  }
240
+ const filteredOptions = state.searchText
241
+ ? filterMap(inputs.options, (entry) => {
242
+ return isViraDropdownOptionGroup(entry)
243
+ ? {
244
+ ...entry,
245
+ options: entry.options.filter((option) => {
246
+ return fuzzyMatch({
247
+ search: state.searchText,
248
+ text: option.label,
249
+ });
250
+ }),
251
+ }
252
+ : entry;
253
+ }, (entry) => {
254
+ return isViraDropdownOptionGroup(entry)
255
+ ? !!entry.options.length
256
+ : fuzzyMatch({
257
+ search: state.searchText,
258
+ text: entry.label,
259
+ });
260
+ })
261
+ : inputs.options;
262
+ const filteredFlatOptions = filteredOptions.flatMap((entry) => {
263
+ return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
264
+ });
265
+ function findSearchInput() {
266
+ const searchInput = host.shadowRoot.querySelector('.search-input');
267
+ return searchInput instanceof HTMLInputElement ? searchInput : undefined;
268
+ }
269
+ function clearSearch(searchInput) {
270
+ /** Set directly because the `.value` binding skips values it has already committed. */
271
+ searchInput.value = '';
272
+ updateState({
273
+ searchText: '',
274
+ });
275
+ }
276
+ function toggleGroup(group) {
277
+ const enabledGroupOptions = group.options.filter((option) => !option.disabled);
278
+ const isGroupSelected = enabledGroupOptions.every((option) => {
279
+ return selectedOptions.includes(option);
280
+ });
281
+ dispatch(new events.selectedValuesChange({
282
+ detail: (isGroupSelected
283
+ ? selectedOptions.filter((option) => !enabledGroupOptions.includes(option))
284
+ : [
285
+ ...selectedOptions,
286
+ ...enabledGroupOptions.filter((option) => {
287
+ return !selectedOptions.includes(option);
288
+ }),
289
+ ]).map((option) => option.value),
290
+ }));
291
+ }
292
+ function renderGroupLabel(group) {
293
+ return inputs.isMultiSelect
294
+ ? html `
295
+ <${ViraMenuItem.assign({
296
+ disablePointerStyles: true,
297
+ })}
298
+ class="option-group-label"
299
+ ${state.navController ? nav(state.navController) : nothing}
300
+ ${listen('click', () => {
301
+ toggleGroup(group);
302
+ })}
303
+ >
304
+ ${group.groupName}
305
+ </${ViraMenuItem}>
306
+ `
307
+ : html `
308
+ <div class="option-group-label">${group.groupName}</div>
309
+ `;
310
+ }
311
+ /** Drag-select actions in the same order as the rendered menu items. */
312
+ const menuItemActions = filteredOptions.flatMap((entry) => {
313
+ const options = isViraDropdownOptionGroup(entry) ? entry.options : [entry];
314
+ const optionActions = options.map((option) => {
315
+ return option.disabled
316
+ ? undefined
317
+ : () => {
318
+ selectOption(option);
319
+ };
320
+ });
321
+ return isViraDropdownOptionGroup(entry) && inputs.isMultiSelect
322
+ ? [
323
+ () => {
324
+ toggleGroup(entry);
325
+ },
326
+ ...optionActions,
327
+ ]
328
+ : optionActions;
329
+ });
330
+ function renderOptionEntries(options) {
331
+ return renderMenuItemEntries(options.map((option) => {
332
+ return {
333
+ content: option.label,
334
+ onClick() {
335
+ selectOption(option);
336
+ },
337
+ disabled: option.disabled,
338
+ selected: selectedOptions.includes(option),
339
+ };
340
+ }), state.navController);
341
+ }
342
+ const isMenuShown = state.isOpen && (!!filteredFlatOptions.length || !!inputs.noOptionsText);
168
343
  const menuTemplate = html `
169
344
  <${ViraMenu.assign({
170
- direction: state.showPopUpResult?.popDown
171
- ? ViraMenuPopUpDirection.Downwards
172
- : ViraMenuPopUpDirection.Upwards,
345
+ cornerStyle: ViraMenuCornerStyle.Round,
346
+ hoverScrollSpeed: inputs.menuHoverScrollSpeed,
173
347
  })}
174
- slot=${ViraPopUpTrigger.slotNames['vira-pop-up-trigger-pop-up']}
348
+ slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-popover']}
175
349
  ${listen('mouseup', (event) => {
176
350
  if (state.shouldSelectOnMouseUp) {
177
351
  const menuItem = event
@@ -179,47 +353,82 @@ export const ViraDropdown = defineViraElement()({
179
353
  .find((eventTarget) => {
180
354
  return eventTarget instanceof ViraMenuItem;
181
355
  });
182
- const option = menuItem
183
- ? inputs.options[Array.from(extractEventTarget(event, ViraMenu).querySelectorAll(ViraMenuItem.tagName)).indexOf(menuItem)]
356
+ const menuItemAction = menuItem
357
+ ? menuItemActions[Array.from(extractEventTarget(event, ViraMenu).querySelectorAll(ViraMenuItem.tagName)).indexOf(menuItem)]
184
358
  : undefined;
185
- if (check.isDefined(option) && !option.disabled) {
186
- selectOption(option);
187
- }
359
+ menuItemAction?.();
188
360
  }
189
361
  })}
190
362
  >
191
- ${renderMenuItemEntries(inputs.options.map((option) => {
192
- return {
193
- content: option.label,
194
- onClick() {
195
- selectOption(option);
196
- },
197
- disabled: option.disabled,
198
- selected: selectedOptions.includes(option),
199
- };
200
- }))}
363
+ ${filteredFlatOptions.length
364
+ ? filteredOptions.map((entry) => {
365
+ return isViraDropdownOptionGroup(entry)
366
+ ? html `
367
+ ${renderGroupLabel(entry)} ${renderOptionEntries(entry.options)}
368
+ `
369
+ : renderOptionEntries([entry]);
370
+ })
371
+ : html `
372
+ <div class="no-options">${inputs.noOptionsText}</div>
373
+ `}
201
374
  </${ViraMenu}>
202
375
  `;
203
376
  const triggerTemplate = html `
204
- <${ViraPopUpTrigger.assign({
377
+ <${ViraPopoverTrigger.assign({
205
378
  ...inputs,
206
379
  keepOpenAfterInteraction: inputs.isMultiSelect,
207
380
  focusOnClose: true,
208
- popUpOffset: {
381
+ popoverOffset: {
209
382
  vertical: -1,
210
383
  right: 24,
211
384
  },
212
- horizontalAnchor: inputs.horizontalAnchor || HorizontalAnchor.Both,
213
385
  })}
214
- ${listen(ViraPopUpTrigger.events.openChange, (event) => {
215
- if (!!state.showPopUpResult !== !!event.detail) {
386
+ ${listen(ViraPopoverTrigger.events.init, (event) => {
387
+ updateState({
388
+ navController: event.detail.navController,
389
+ popoverManager: event.detail.popoverManager,
390
+ });
391
+ })}
392
+ ${listen(ViraPopoverTrigger.events.openChange, (event) => {
393
+ if (state.isOpen !== event.detail) {
216
394
  dispatch(new events.openChange({
217
395
  detail: event.detail,
218
396
  }));
219
397
  }
220
398
  updateState({
221
- showPopUpResult: event.detail,
399
+ isOpen: event.detail,
222
400
  });
401
+ const searchInput = findSearchInput();
402
+ if (!searchInput) {
403
+ return;
404
+ }
405
+ else if (event.detail) {
406
+ searchInput.focus();
407
+ }
408
+ else {
409
+ clearSearch(searchInput);
410
+ }
411
+ })}
412
+ ${listen('keydown', (event) => {
413
+ const searchInput = findSearchInput();
414
+ const isSearchKey = event.key === 'Backspace' ||
415
+ (event.key.length === 1 &&
416
+ event.key !== ' ' &&
417
+ !event.ctrlKey &&
418
+ !event.metaKey &&
419
+ !event.altKey);
420
+ if (!searchInput || !isSearchKey) {
421
+ return;
422
+ }
423
+ if (!state.isOpen) {
424
+ clearSearch(searchInput);
425
+ }
426
+ /**
427
+ * Focusing during keydown makes the browser type the pressed key into the
428
+ * search input, even when the key was pressed on the trigger or a menu item.
429
+ */
430
+ searchInput.focus();
431
+ state.popoverManager?.show();
223
432
  })}
224
433
  ${listen('mouseup', () => {
225
434
  if (state.shouldSelectOnMouseUp) {
@@ -231,15 +440,15 @@ export const ViraDropdown = defineViraElement()({
231
440
  >
232
441
  <div
233
442
  class="dropdown-trigger ${classMap({
234
- open: !!state.showPopUpResult,
235
- 'open-upwards': !state.showPopUpResult?.popDown,
443
+ open: state.isOpen,
236
444
  })}"
237
- slot=${ViraPopUpTrigger.slotNames['vira-pop-up-trigger-trigger']}
445
+ slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-trigger']}
238
446
  id=${ifDefined(inputs.label ? state.randomId : undefined)}
239
447
  aria-label=${ifDefined((check.isString(inputs.label) && inputs.label) || undefined)}
448
+ ${attributes(inputs.attributePassthrough)}
240
449
  ${testId(testIds.trigger)}
241
450
  ${listen('mousedown', () => {
242
- if (!state.showPopUpResult) {
451
+ if (!state.isOpen) {
243
452
  updateState({
244
453
  shouldSelectOnMouseUp: true,
245
454
  });
@@ -247,14 +456,93 @@ export const ViraDropdown = defineViraElement()({
247
456
  })}
248
457
  >
249
458
  ${leadingIconTemplate}
250
- <span
251
- class="selection-display ${classMap({
252
- 'using-placeholder': shouldUsePlaceholder,
253
- })}"
254
- title=${ifDefined(shouldUsePlaceholder ? undefined : selectionDisplay)}
255
- >
256
- ${prefixTemplate} ${selectionDisplay}
257
- </span>
459
+ ${inputs.isSearchable
460
+ ? html `
461
+ ${state.isOpen ? nothing : prefixTemplate}
462
+ <input
463
+ class="search-input"
464
+ ${testId(testIds.searchInput)}
465
+ tabindex="-1"
466
+ autocomplete="off"
467
+ spellcheck="false"
468
+ aria-label=${ifDefined((check.isString(inputs.label) && inputs.label) || undefined)}
469
+ ?disabled=${!!inputs.isDisabled}
470
+ title=${ifDefined(shouldUsePlaceholder || state.isOpen
471
+ ? undefined
472
+ : selectionDisplay)}
473
+ placeholder=${state.isOpen && !shouldUsePlaceholder
474
+ ? selectionDisplay
475
+ : inputs.placeholder || ''}
476
+ .value=${state.isOpen
477
+ ? state.searchText
478
+ : shouldUsePlaceholder
479
+ ? ''
480
+ : selectionDisplay}
481
+ ${listen('mousedown', (event) => {
482
+ /** Keeps the popover open when clicking into the input. */
483
+ if (state.isOpen) {
484
+ event.stopPropagation();
485
+ }
486
+ })}
487
+ ${listen('input', (event) => {
488
+ updateState({
489
+ searchText: extractEventTarget(event, HTMLInputElement)
490
+ .value,
491
+ });
492
+ state.popoverManager?.show();
493
+ })}
494
+ ${listen('keydown', (event) => {
495
+ if (!state.isOpen) {
496
+ return;
497
+ }
498
+ else if (event.key === 'ArrowDown' ||
499
+ event.key === 'ArrowUp') {
500
+ event.preventDefault();
501
+ state.navController?.navigate({
502
+ direction: event.key === 'ArrowDown'
503
+ ? NavDirection.Down
504
+ : NavDirection.Up,
505
+ allowWrapping: false,
506
+ });
507
+ }
508
+ else if (event.key === 'Tab' && !event.shiftKey) {
509
+ /**
510
+ * Without this, browsers move focus from this input past
511
+ * the menu items (sometimes closing the menu).
512
+ */
513
+ const firstMenuItem = Array.from(host.shadowRoot.querySelectorAll(ViraMenuItem.tagName)).find((menuItem) => menuItem.tabIndex >= 0);
514
+ if (firstMenuItem) {
515
+ event.preventDefault();
516
+ firstMenuItem.focus();
517
+ }
518
+ }
519
+ else if (event.key === 'Enter') {
520
+ const firstOption = filteredFlatOptions.find((option) => !option.disabled);
521
+ if (!firstOption) {
522
+ return;
523
+ }
524
+ event.preventDefault();
525
+ selectOption(firstOption);
526
+ if (inputs.isMultiSelect) {
527
+ clearSearch(extractEventTarget(event, HTMLInputElement));
528
+ }
529
+ else {
530
+ state.popoverManager?.hide();
531
+ }
532
+ }
533
+ })}
534
+ />
535
+ `
536
+ : html `
537
+ <span
538
+ class="selection-display ${classMap({
539
+ 'using-placeholder': shouldUsePlaceholder,
540
+ })}"
541
+ title=${ifDefined(shouldUsePlaceholder ? undefined : selectionDisplay)}
542
+ >
543
+ ${prefixTemplate} ${selectionDisplay}
544
+ </span>
545
+ `}
258
546
 
259
547
  <span class="trigger-icon-wrapper">
260
548
  <${ViraIcon.assign({
@@ -264,19 +552,37 @@ export const ViraDropdown = defineViraElement()({
264
552
  ></${ViraIcon}>
265
553
  </span>
266
554
  </div>
267
- ${state.showPopUpResult ? menuTemplate : nothing}
268
- </${ViraPopUpTrigger}>
555
+ ${isMenuShown ? menuTemplate : nothing}
556
+ </${ViraPopoverTrigger}>
269
557
  `;
558
+ const contentTemplate = inputs.isReadonly
559
+ ? html `
560
+ <span class="readonly-value">${selectionDisplay}</span>
561
+ `
562
+ : triggerTemplate;
270
563
  if (inputs.label) {
271
564
  return html `
272
- <label for=${state.randomId}>
565
+ <label
566
+ for=${ifDefined(inputs.isReadonly ? undefined : state.randomId)}
567
+ class=${classMap({
568
+ 'has-error': !!inputs.hasError,
569
+ })}
570
+ >
273
571
  <span class="dropdown-label">${inputs.label}</span>
274
- ${triggerTemplate}
572
+ ${contentTemplate}
275
573
  </label>
276
574
  `;
277
575
  }
278
576
  else {
279
- return triggerTemplate;
577
+ return html `
578
+ <span
579
+ class="dropdown-wrapper ${classMap({
580
+ 'has-error': !!inputs.hasError,
581
+ })}"
582
+ >
583
+ ${contentTemplate}
584
+ </span>
585
+ `;
280
586
  }
281
587
  },
282
588
  });
@@ -6,8 +6,8 @@ import { defineViraElement } from '../util/define-vira-element.js';
6
6
  import { applyRequiredLabel, areFormFieldsValid, ViraFormFieldType, } from '../util/vira-form-fields.js';
7
7
  import { ViraCheckbox } from './vira-checkbox.element.js';
8
8
  import { ViraDateInput } from './vira-date-input.element.js';
9
+ import { ViraDropdown } from './vira-dropdown.element.js';
9
10
  import { ViraInput, ViraInputType } from './vira-input.element.js';
10
- import { ViraSelect } from './vira-select.element.js';
11
11
  import { ViraTextArea } from './vira-text-area.element.js';
12
12
  /**
13
13
  * A form element.
@@ -70,9 +70,9 @@ export const ViraForm = defineViraElement()({
70
70
  &
71
71
  > ${ViraDateInput},
72
72
  &
73
- > ${ViraInput},
73
+ > ${ViraDropdown},
74
74
  &
75
- > ${ViraSelect},
75
+ > ${ViraInput},
76
76
  &
77
77
  > ${ViraTextArea} {
78
78
  width: 100%;
@@ -159,34 +159,28 @@ export const ViraForm = defineViraElement()({
159
159
  return wrapFormField({
160
160
  label,
161
161
  fieldTemplate: html `
162
- <${ViraSelect.assign({
162
+ <${ViraDropdown.assign({
163
163
  options: field.options,
164
- value: field.value,
164
+ selected: field.value ? [field.value] : [],
165
165
  placeholder: field.placeholder,
166
- disabled: isDisabled,
166
+ isDisabled,
167
167
  isReadonly: inputs.isReadonly,
168
168
  label: childLabel,
169
169
  hasError: field.hasError,
170
170
  icon: field.icon,
171
- ...(inputs.useHorizontalLabels && label
172
- ? {
173
- attributePassthrough: {
174
- select: horizontalLabelAttributes,
175
- },
176
- }
177
- : {}),
171
+ attributePassthrough: horizontalLabelAttributes,
178
172
  })}
179
173
  ${field.testId ? testId(field.testId) : nothing}
180
- ${listen(ViraSelect.events.valueChange, (event) => {
174
+ ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
181
175
  dispatch(new events.valueChange({
182
176
  detail: {
183
177
  key,
184
178
  ...field,
185
- value: event.detail,
179
+ value: event.detail[0],
186
180
  },
187
181
  }));
188
182
  })}
189
- ></${ViraSelect}>
183
+ ></${ViraDropdown}>
190
184
  `,
191
185
  });
192
186
  }