vira 32.4.0 → 33.0.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 (56) 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 +3 -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 +49 -0
  18. package/dist/elements/popover/vira-popover-trigger.element.js +301 -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 +28 -12
  23. package/dist/elements/vira-dropdown.element.js +172 -59
  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/index.d.ts +4 -3
  35. package/dist/util/index.js +4 -3
  36. package/dist/util/{pop-up-helpers.d.ts → menu-helpers.d.ts} +4 -18
  37. package/dist/util/{pop-up-helpers.js → menu-helpers.js} +2 -17
  38. package/dist/util/popover-manager.d.ts +123 -0
  39. package/dist/util/popover-manager.js +412 -0
  40. package/dist/util/vira-dropdown-option.d.ts +30 -0
  41. package/dist/util/vira-dropdown-option.js +8 -0
  42. package/dist/util/vira-form-fields.d.ts +2 -2
  43. package/dist/util/vira-theme-client.d.ts +1 -1
  44. package/dist/util/vira-theme-client.js +3 -3
  45. package/package.json +2 -1
  46. package/dist/elements/pop-up/vira-menu-trigger.element.d.ts +0 -45
  47. package/dist/elements/pop-up/vira-menu.element.d.ts +0 -36
  48. package/dist/elements/pop-up/vira-menu.element.js +0 -88
  49. package/dist/elements/pop-up/vira-pop-up-trigger.element.d.ts +0 -109
  50. package/dist/elements/pop-up/vira-pop-up-trigger.element.js +0 -408
  51. package/dist/elements/vira-select.element.d.ts +0 -44
  52. package/dist/elements/vira-select.element.js +0 -418
  53. package/dist/util/pop-up-manager.d.ts +0 -200
  54. package/dist/util/pop-up-manager.js +0 -258
  55. package/dist/util/vira-select-option.d.ts +0 -31
  56. package/dist/util/vira-select-option.js +0 -8
@@ -0,0 +1,176 @@
1
+ import { colorCss } from '@electrovir/color';
2
+ import { css, html } from 'element-vir';
3
+ import { listenTo } from 'typed-event-target';
4
+ import { viraFormCssVars } from '../../styles/form-styles.js';
5
+ import { viraTheme } from '../../styles/vira-color-theme.js';
6
+ import { createAnchoredPopoverStyles } from '../../util/anchored-popover.js';
7
+ import { defineViraElement } from '../../util/define-vira-element.js';
8
+ /**
9
+ * Centered with insets instead of `position-area`, because browsers shift a centered
10
+ * `position-area` box back onto the screen rather than moving on to the next
11
+ * `position-try-fallbacks` entry. These insets leave a space that is centered on the anchor and as
12
+ * wide as possible, and `unsafe` stops the browser from shifting the tooltip, so a tooltip wider
13
+ * than that space overflows it and moves on to an aligned fallback. Each `anchor(center)` is
14
+ * measured from the side its inset is on, so the same formula works for `left` and `right`.
15
+ */
16
+ const centeredStyles = css `
17
+ position-area: none;
18
+ top: 0;
19
+ bottom: anchor(top);
20
+ left: max(0px, 2 * anchor(center) - 100%);
21
+ right: max(0px, 2 * anchor(center) - 100%);
22
+ align-self: end;
23
+ justify-self: unsafe center;
24
+ `;
25
+ /** `unsafe` for the same reason as {@link centeredStyles}. */
26
+ const leftAlignedStyles = css `
27
+ position-area: block-start span-inline-end;
28
+ inset: auto;
29
+ align-self: normal;
30
+ justify-self: unsafe start;
31
+ `;
32
+ /** `unsafe` for the same reason as {@link centeredStyles}. */
33
+ const rightAlignedStyles = css `
34
+ position-area: block-start span-inline-start;
35
+ inset: auto;
36
+ align-self: normal;
37
+ justify-self: unsafe end;
38
+ `;
39
+ /**
40
+ * Centered but shifted to stay on the screen. For a tooltip that fits on neither side of the
41
+ * anchor.
42
+ */
43
+ const shiftedStyles = css `
44
+ position-area: block-start span-all;
45
+ inset: auto;
46
+ align-self: normal;
47
+ justify-self: anchor-center;
48
+ `;
49
+ /**
50
+ * The popover rendered by the `tooltip` directive. The element itself is the popover, centered
51
+ * above `anchor` when there's room. Its children are the tooltip contents.
52
+ *
53
+ * @category Popover
54
+ * @category Elements
55
+ */
56
+ export const ViraTooltip = defineViraElement()({
57
+ tagName: 'vira-tooltip',
58
+ styles: css `
59
+ ${createAnchoredPopoverStyles({
60
+ fallbacks: [
61
+ {
62
+ name: 'vira-tooltip-above-left-aligned',
63
+ styles: leftAlignedStyles,
64
+ },
65
+ {
66
+ name: 'vira-tooltip-above-right-aligned',
67
+ styles: rightAlignedStyles,
68
+ },
69
+ {
70
+ name: 'vira-tooltip-below-centered',
71
+ styles: centeredStyles,
72
+ flipBlock: true,
73
+ },
74
+ {
75
+ name: 'vira-tooltip-below-left-aligned',
76
+ styles: leftAlignedStyles,
77
+ flipBlock: true,
78
+ },
79
+ {
80
+ name: 'vira-tooltip-below-right-aligned',
81
+ styles: rightAlignedStyles,
82
+ flipBlock: true,
83
+ },
84
+ {
85
+ name: 'vira-tooltip-above-shifted',
86
+ styles: shiftedStyles,
87
+ },
88
+ {
89
+ name: 'vira-tooltip-below-shifted',
90
+ styles: shiftedStyles,
91
+ flipBlock: true,
92
+ },
93
+ ],
94
+ })}
95
+
96
+ :host {
97
+ anchor-name: --vira-tooltip;
98
+ ${colorCss(viraTheme.colors['vira-grey-behind-bg-highest-contrast'])};
99
+ ${centeredStyles}
100
+ margin-bottom: 7px;
101
+ padding: 4px 8px;
102
+ border-radius: 6px;
103
+ box-sizing: border-box;
104
+ width: max-content;
105
+ /*
106
+ Not 100%, which is the width of the current position-area. A tooltip limited to that
107
+ width shrinks to fit it rather than overflowing onto the next fallback.
108
+ */
109
+ max-width: min(300px, 100vw);
110
+ font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
111
+ overflow-wrap: break-word;
112
+ pointer-events: none;
113
+ }
114
+
115
+ /*
116
+ Separate popovers rather than children or pseudo-elements of the tooltip, because those can't
117
+ use anchor() to reach the anchor element from inside the tooltip's top-layer box.
118
+ */
119
+ .caret {
120
+ /*
121
+ position-anchor is set explicitly because its initial value differs between browsers.
122
+ */
123
+ position-anchor: auto;
124
+ position: fixed;
125
+ inset: auto;
126
+ margin: 0;
127
+ padding: 0;
128
+ border: none;
129
+ height: auto;
130
+ width: 10px;
131
+ left: calc(anchor(center) - 5px);
132
+ background-color: ${viraTheme.colors['vira-grey-behind-bg-highest-contrast'].background
133
+ .value};
134
+ pointer-events: none;
135
+ }
136
+
137
+ /*
138
+ Each caret spans the gap between the tooltip and the anchor on its own side, so the caret
139
+ on the side without the tooltip gets a negative height, which renders as nothing. This
140
+ follows the tooltip's position-try-fallbacks choice without knowing which one won.
141
+ */
142
+ .caret-down {
143
+ top: anchor(--vira-tooltip bottom);
144
+ bottom: calc(anchor(top) + 2px);
145
+ clip-path: polygon(0 0, 100% 0, 50% 100%);
146
+ }
147
+
148
+ .caret-up {
149
+ bottom: anchor(--vira-tooltip top);
150
+ top: calc(anchor(bottom) + 2px);
151
+ clip-path: polygon(50% 0, 100% 100%, 0 100%);
152
+ }
153
+ `,
154
+ init({ host }) {
155
+ /** The carets must open after the tooltip so they can anchor to it. */
156
+ listenTo(host, 'toggle', () => {
157
+ if (!host.matches(':popover-open')) {
158
+ return;
159
+ }
160
+ host.shadowRoot.querySelectorAll('.caret').forEach((caret) => {
161
+ if (caret instanceof HTMLElement && !caret.matches(':popover-open')) {
162
+ caret.showPopover({
163
+ source: host.instanceInputs.anchor,
164
+ });
165
+ }
166
+ });
167
+ });
168
+ },
169
+ render() {
170
+ return html `
171
+ <slot></slot>
172
+ <div class="caret caret-down" popover="manual"></div>
173
+ <div class="caret caret-up" popover="manual"></div>
174
+ `;
175
+ },
176
+ });
@@ -11,7 +11,7 @@ import { viraShadows } from '../styles/shadows.js';
11
11
  import { noUserSelect } from '../styles/user-select.js';
12
12
  import { viraTheme } from '../styles/vira-color-theme.js';
13
13
  import { defineViraElement } from '../util/define-vira-element.js';
14
- import { isMouseEventOnScrollbar } from '../util/pop-up-manager.js';
14
+ import { isMouseEventOnScrollbar } from '../util/popover-manager.js';
15
15
  import { ViraIcon } from './vira-icon.element.js';
16
16
  const globalEventsToCloseDrawerOn = [
17
17
  'pagehide',
@@ -1,23 +1,26 @@
1
1
  import { type PartialWithUndefined } from '@augment-vir/common';
2
- import { type HtmlInterpolation } from 'element-vir';
2
+ import { type AttributeValues, type HtmlInterpolation } from 'element-vir';
3
3
  import { type ViraIconSvg } from '../icons/icon-svg.js';
4
- import { type ShowPopUpResult } from '../util/pop-up-manager.js';
5
- import { type ViraSelectOption } from '../util/vira-select-option.js';
6
- import { type PopUpTriggerPosition } from './pop-up/vira-pop-up-trigger.element.js';
4
+ import { type ViraDropdownOption, type ViraDropdownOptionGroup } from '../util/vira-dropdown-option.js';
7
5
  /**
8
- * A dropdown element that uses pop-up menus.
6
+ * A dropdown element that uses popover menus.
9
7
  *
10
8
  * @category Dropdown
11
9
  * @category Elements
12
10
  * @see https://electrovir.github.io/vira/book/elements/dropdown/vira-dropdown
13
11
  */
14
12
  export declare const ViraDropdown: import("element-vir").DeclarativeElementDefinition<"vira-dropdown", {
15
- options: ReadonlyArray<Readonly<ViraSelectOption>>;
13
+ options: ReadonlyArray<Readonly<ViraDropdownOption> | Readonly<ViraDropdownOptionGroup>>;
16
14
  /** The selected id from the given options. */
17
15
  selected: ReadonlyArray<PropertyKey>;
18
16
  } & PartialWithUndefined<{
19
17
  /** Text to show if nothing is selected. */
20
18
  placeholder: string;
19
+ /**
20
+ * Text shown in the opened popover when `options` has no options. When omitted, opening a
21
+ * dropdown with no options shows no popover at all.
22
+ */
23
+ noOptionsText: string;
21
24
  /**
22
25
  * If false, this will behave like a single select dropdown, otherwise you can select
23
26
  * multiple.
@@ -26,12 +29,24 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
26
29
  icon: ViraIconSvg;
27
30
  selectionPrefix: string;
28
31
  isDisabled: boolean;
32
+ /**
33
+ * When `true`, the currently selected options' labels are rendered as plain text with no
34
+ * trigger or menu.
35
+ */
36
+ isReadonly: boolean;
37
+ hasError: boolean;
29
38
  label: string | HtmlInterpolation;
30
- /** For debugging purposes only. Very bad for actual production code use. */
31
- z_debug_forceOpenState: boolean;
32
- } & PopUpTriggerPosition>, {
33
- /** `undefined` means the pop up is not currently showing. */
34
- showPopUpResult: ShowPopUpResult | undefined;
39
+ /** Attributes applied to the trigger element. */
40
+ attributePassthrough: Readonly<AttributeValues>;
41
+ /**
42
+ * Passed to {@link ViraMenu}'s `hoverScrollSpeed` input. How many menu items per second
43
+ * hovering a scroll arrow scrolls through.
44
+ *
45
+ * @default 24
46
+ */
47
+ menuHoverScrollSpeed: number;
48
+ }>, {
49
+ isOpen: boolean;
35
50
  shouldSelectOnMouseUp: boolean;
36
51
  /**
37
52
  * Used to couple the label and trigger together. This is not applied if no label is
@@ -41,5 +56,6 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
41
56
  }, {
42
57
  /** Emits all currently selected values. */
43
58
  selectedValuesChange: import("element-vir").DefineElementEvent<string[]>;
44
- openChange: import("element-vir").DefineElementEvent<ShowPopUpResult | undefined>;
59
+ /** `true` when the popover just opened, `false` when it just closed. */
60
+ openChange: import("element-vir").DefineElementEvent<boolean>;
45
61
  }, "vira-dropdown-", "vira-dropdown-", readonly [], readonly ["leadingIcon", "prefixText", "trigger"]>;
@@ -1,18 +1,20 @@
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 { attributes, classMap, css, defineElementEvent, html, ifDefined, listen, nothing, testId, } from 'element-vir';
5
5
  import { ChevronUp16Icon } from '../icons/index.js';
6
6
  import { viraFormCssVars } from '../styles/form-styles.js';
7
+ import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
7
8
  import { noUserSelect, viraAnimationDurations } from '../styles/index.js';
8
9
  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';
10
+ import { renderMenuItemEntries } from '../util/menu-helpers.js';
11
+ import { isViraDropdownOptionGroup, } from '../util/vira-dropdown-option.js';
12
+ import { ViraMenuItem } from './popover/vira-menu-item.element.js';
13
+ import { ViraMenu, ViraMenuCornerStyle } from './popover/vira-menu.element.js';
14
+ import { ViraPopoverTrigger } from './popover/vira-popover-trigger.element.js';
13
15
  import { ViraIcon } from './vira-icon.element.js';
14
16
  /**
15
- * A dropdown element that uses pop-up menus.
17
+ * A dropdown element that uses popover menus.
16
18
  *
17
19
  * @category Dropdown
18
20
  * @category Elements
@@ -29,12 +31,12 @@ export const ViraDropdown = defineViraElement()({
29
31
  :host {
30
32
  display: inline-flex;
31
33
  vertical-align: middle;
32
- width: 256px;
34
+ width: 224px;
33
35
  position: relative;
34
36
  max-width: 100%;
35
37
  }
36
38
 
37
- ${ViraPopUpTrigger} {
39
+ ${ViraPopoverTrigger} {
38
40
  width: 100%;
39
41
  }
40
42
 
@@ -60,18 +62,8 @@ export const ViraDropdown = defineViraElement()({
60
62
  justify-content: flex-end;
61
63
  }
62
64
 
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
- }
65
+ .open .trigger-icon {
66
+ transform: rotate(0);
75
67
  }
76
68
 
77
69
  .dropdown-trigger {
@@ -84,13 +76,50 @@ export const ViraDropdown = defineViraElement()({
84
76
  gap: 8px;
85
77
  text-align: left;
86
78
  align-items: center;
87
- padding: 3px;
88
- padding-left: 10px;
79
+ min-height: ${viraSizeHeights[ViraSize.Medium]}px;
80
+ padding: 0 3px 0 10px;
89
81
  border-radius: ${viraFormCssVars['vira-form-radius'].value};
90
82
  background-color: ${viraFormCssVars['vira-form-background-color'].value};
91
83
  color: ${viraFormCssVars['vira-form-foreground-color'].value};
92
84
  }
93
85
 
86
+ .dropdown-wrapper {
87
+ display: flex;
88
+ width: 100%;
89
+ }
90
+
91
+ .has-error .dropdown-trigger {
92
+ border-color: ${viraFormCssVars['vira-form-error-color'].value};
93
+ }
94
+
95
+ .option-group-label,
96
+ .no-options {
97
+ ${noUserSelect};
98
+ /* Aligns with the label text of each ViraMenuItem, past its check icon. */
99
+ padding: 8px 12px 4px
100
+ calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
101
+ opacity: 0.6;
102
+ text-align: left;
103
+ }
104
+
105
+ .option-group-label {
106
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
107
+ }
108
+
109
+ .no-options {
110
+ cursor: default;
111
+ padding-bottom: 8px;
112
+ }
113
+
114
+ ${ViraMenuItem}.option-group-label {
115
+ padding: 0 0 0 calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 2);
116
+ ${ViraMenuItem.cssVars['vira-menu-item-padding'].name}: 8px 12px 4px 0;
117
+ }
118
+
119
+ .readonly-value {
120
+ overflow-wrap: anywhere;
121
+ }
122
+
94
123
  .using-placeholder {
95
124
  opacity: 0.4;
96
125
  }
@@ -114,12 +143,12 @@ export const ViraDropdown = defineViraElement()({
114
143
  events: {
115
144
  /** Emits all currently selected values. */
116
145
  selectedValuesChange: defineElementEvent(),
146
+ /** `true` when the popover just opened, `false` when it just closed. */
117
147
  openChange: defineElementEvent(),
118
148
  },
119
149
  state() {
120
150
  return {
121
- /** `undefined` means the pop up is not currently showing. */
122
- showPopUpResult: undefined,
151
+ isOpen: false,
123
152
  shouldSelectOnMouseUp: false,
124
153
  /**
125
154
  * Used to couple the label and trigger together. This is not applied if no label is
@@ -129,7 +158,10 @@ export const ViraDropdown = defineViraElement()({
129
158
  };
130
159
  },
131
160
  render({ state, inputs, dispatch, events, updateState, testIds }) {
132
- const selectedOptions = filterMap(inputs.selected, (selectedValue) => inputs.options.find((option) => option.value === selectedValue), check.isTruthy);
161
+ const flatOptions = inputs.options.flatMap((entry) => {
162
+ return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
163
+ });
164
+ const selectedOptions = filterMap(inputs.selected, (selectedValue) => flatOptions.find((option) => option.value === selectedValue), check.isTruthy);
133
165
  const leadingIconTemplate = inputs.icon
134
166
  ? html `
135
167
  <${ViraIcon.assign({
@@ -165,13 +197,78 @@ export const ViraDropdown = defineViraElement()({
165
197
  detail: newSelectedValues,
166
198
  }));
167
199
  }
200
+ function toggleGroup(group) {
201
+ const enabledGroupOptions = group.options.filter((option) => !option.disabled);
202
+ const isGroupSelected = enabledGroupOptions.every((option) => {
203
+ return selectedOptions.includes(option);
204
+ });
205
+ dispatch(new events.selectedValuesChange({
206
+ detail: (isGroupSelected
207
+ ? selectedOptions.filter((option) => !enabledGroupOptions.includes(option))
208
+ : [
209
+ ...selectedOptions,
210
+ ...enabledGroupOptions.filter((option) => {
211
+ return !selectedOptions.includes(option);
212
+ }),
213
+ ]).map((option) => option.value),
214
+ }));
215
+ }
216
+ function renderGroupLabel(group) {
217
+ return inputs.isMultiSelect
218
+ ? html `
219
+ <${ViraMenuItem.assign({
220
+ disablePointerStyles: true,
221
+ })}
222
+ class="option-group-label"
223
+ ${listen('click', () => {
224
+ toggleGroup(group);
225
+ })}
226
+ >
227
+ ${group.groupName}
228
+ </${ViraMenuItem}>
229
+ `
230
+ : html `
231
+ <div class="option-group-label">${group.groupName}</div>
232
+ `;
233
+ }
234
+ /** Drag-select actions in the same order as the rendered menu items. */
235
+ const menuItemActions = inputs.options.flatMap((entry) => {
236
+ const options = isViraDropdownOptionGroup(entry) ? entry.options : [entry];
237
+ const optionActions = options.map((option) => {
238
+ return option.disabled
239
+ ? undefined
240
+ : () => {
241
+ selectOption(option);
242
+ };
243
+ });
244
+ return isViraDropdownOptionGroup(entry) && inputs.isMultiSelect
245
+ ? [
246
+ () => {
247
+ toggleGroup(entry);
248
+ },
249
+ ...optionActions,
250
+ ]
251
+ : optionActions;
252
+ });
253
+ function renderOptionEntries(options) {
254
+ return renderMenuItemEntries(options.map((option) => {
255
+ return {
256
+ content: option.label,
257
+ onClick() {
258
+ selectOption(option);
259
+ },
260
+ disabled: option.disabled,
261
+ selected: selectedOptions.includes(option),
262
+ };
263
+ }));
264
+ }
265
+ const isMenuShown = state.isOpen && (!!flatOptions.length || !!inputs.noOptionsText);
168
266
  const menuTemplate = html `
169
267
  <${ViraMenu.assign({
170
- direction: state.showPopUpResult?.popDown
171
- ? ViraMenuPopUpDirection.Downwards
172
- : ViraMenuPopUpDirection.Upwards,
268
+ cornerStyle: ViraMenuCornerStyle.Round,
269
+ hoverScrollSpeed: inputs.menuHoverScrollSpeed,
173
270
  })}
174
- slot=${ViraPopUpTrigger.slotNames['vira-pop-up-trigger-pop-up']}
271
+ slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-popover']}
175
272
  ${listen('mouseup', (event) => {
176
273
  if (state.shouldSelectOnMouseUp) {
177
274
  const menuItem = event
@@ -179,46 +276,44 @@ export const ViraDropdown = defineViraElement()({
179
276
  .find((eventTarget) => {
180
277
  return eventTarget instanceof ViraMenuItem;
181
278
  });
182
- const option = menuItem
183
- ? inputs.options[Array.from(extractEventTarget(event, ViraMenu).querySelectorAll(ViraMenuItem.tagName)).indexOf(menuItem)]
279
+ const menuItemAction = menuItem
280
+ ? menuItemActions[Array.from(extractEventTarget(event, ViraMenu).querySelectorAll(ViraMenuItem.tagName)).indexOf(menuItem)]
184
281
  : undefined;
185
- if (check.isDefined(option) && !option.disabled) {
186
- selectOption(option);
187
- }
282
+ menuItemAction?.();
188
283
  }
189
284
  })}
190
285
  >
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
- }))}
286
+ ${flatOptions.length
287
+ ? inputs.options.map((entry) => {
288
+ return isViraDropdownOptionGroup(entry)
289
+ ? html `
290
+ ${renderGroupLabel(entry)} ${renderOptionEntries(entry.options)}
291
+ `
292
+ : renderOptionEntries([entry]);
293
+ })
294
+ : html `
295
+ <div class="no-options">${inputs.noOptionsText}</div>
296
+ `}
201
297
  </${ViraMenu}>
202
298
  `;
203
299
  const triggerTemplate = html `
204
- <${ViraPopUpTrigger.assign({
300
+ <${ViraPopoverTrigger.assign({
205
301
  ...inputs,
206
302
  keepOpenAfterInteraction: inputs.isMultiSelect,
207
303
  focusOnClose: true,
208
- popUpOffset: {
304
+ popoverOffset: {
209
305
  vertical: -1,
210
306
  right: 24,
211
307
  },
212
- horizontalAnchor: inputs.horizontalAnchor || HorizontalAnchor.Both,
213
308
  })}
214
- ${listen(ViraPopUpTrigger.events.openChange, (event) => {
215
- if (!!state.showPopUpResult !== !!event.detail) {
309
+ ${listen(ViraPopoverTrigger.events.openChange, (event) => {
310
+ if (state.isOpen !== event.detail) {
216
311
  dispatch(new events.openChange({
217
312
  detail: event.detail,
218
313
  }));
219
314
  }
220
315
  updateState({
221
- showPopUpResult: event.detail,
316
+ isOpen: event.detail,
222
317
  });
223
318
  })}
224
319
  ${listen('mouseup', () => {
@@ -231,15 +326,15 @@ export const ViraDropdown = defineViraElement()({
231
326
  >
232
327
  <div
233
328
  class="dropdown-trigger ${classMap({
234
- open: !!state.showPopUpResult,
235
- 'open-upwards': !state.showPopUpResult?.popDown,
329
+ open: state.isOpen,
236
330
  })}"
237
- slot=${ViraPopUpTrigger.slotNames['vira-pop-up-trigger-trigger']}
331
+ slot=${ViraPopoverTrigger.slotNames['vira-popover-trigger-trigger']}
238
332
  id=${ifDefined(inputs.label ? state.randomId : undefined)}
239
333
  aria-label=${ifDefined((check.isString(inputs.label) && inputs.label) || undefined)}
334
+ ${attributes(inputs.attributePassthrough)}
240
335
  ${testId(testIds.trigger)}
241
336
  ${listen('mousedown', () => {
242
- if (!state.showPopUpResult) {
337
+ if (!state.isOpen) {
243
338
  updateState({
244
339
  shouldSelectOnMouseUp: true,
245
340
  });
@@ -264,19 +359,37 @@ export const ViraDropdown = defineViraElement()({
264
359
  ></${ViraIcon}>
265
360
  </span>
266
361
  </div>
267
- ${state.showPopUpResult ? menuTemplate : nothing}
268
- </${ViraPopUpTrigger}>
362
+ ${isMenuShown ? menuTemplate : nothing}
363
+ </${ViraPopoverTrigger}>
269
364
  `;
365
+ const contentTemplate = inputs.isReadonly
366
+ ? html `
367
+ <span class="readonly-value">${selectionDisplay}</span>
368
+ `
369
+ : triggerTemplate;
270
370
  if (inputs.label) {
271
371
  return html `
272
- <label for=${state.randomId}>
372
+ <label
373
+ for=${ifDefined(inputs.isReadonly ? undefined : state.randomId)}
374
+ class=${classMap({
375
+ 'has-error': !!inputs.hasError,
376
+ })}
377
+ >
273
378
  <span class="dropdown-label">${inputs.label}</span>
274
- ${triggerTemplate}
379
+ ${contentTemplate}
275
380
  </label>
276
381
  `;
277
382
  }
278
383
  else {
279
- return triggerTemplate;
384
+ return html `
385
+ <span
386
+ class="dropdown-wrapper ${classMap({
387
+ 'has-error': !!inputs.hasError,
388
+ })}"
389
+ >
390
+ ${contentTemplate}
391
+ </span>
392
+ `;
280
393
  }
281
394
  },
282
395
  });
@@ -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
  }