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
@@ -0,0 +1,123 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type AnyDuration } from 'date-vir';
3
+ import { type Coords, type NavController } from 'device-navigation';
4
+ /**
5
+ * Used to prevent popovers from closing when a text input is active.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export declare function isInputLikeElement(element: Element): boolean;
10
+ /**
11
+ * Whether a keyboard click (a click with `detail` of `0`) came from pressing Enter in a text input,
12
+ * which browsers turn into a click on the form's first button.
13
+ *
14
+ * @category Internal
15
+ */
16
+ export declare function isTextInputActive(): boolean;
17
+ /**
18
+ * Determines if a mouse event landed on a scrollbar rather than on element content. Scrollbar
19
+ * interactions fire mouse events on the scrolling element itself (or on `<html>` for the page
20
+ * scrollbar), so an outside-click check would otherwise treat dragging a scrollbar as a click away
21
+ * from the popover and dismiss it mid-drag.
22
+ *
23
+ * @category Internal
24
+ */
25
+ export declare function isMouseEventOnScrollbar(event: Readonly<MouseEvent>): boolean;
26
+ /**
27
+ * The interaction on the anchor element that shows and hides a popover.
28
+ *
29
+ * @category Popover
30
+ */
31
+ export declare enum PopoverTrigger {
32
+ /**
33
+ * Shows the popover shortly after the anchor is hovered or focused and hides it when the anchor
34
+ * is no longer hovered or focused.
35
+ */
36
+ Hover = "hover",
37
+ /** Toggles the popover when the anchor is clicked. An arrow key opens it. */
38
+ Click = "click",
39
+ /**
40
+ * Toggles the popover as soon as the mouse is pressed on the anchor, so it can be dragged over
41
+ * the popover's contents and released on one of them. Keyboard clicks and arrow keys also
42
+ * work.
43
+ */
44
+ Mousedown = "mousedown"
45
+ }
46
+ /**
47
+ * Options for {@link PopoverManager.update}, usually passed through the `popover` directive.
48
+ *
49
+ * @category Popover
50
+ */
51
+ export type PopoverOptions = {
52
+ /** `true` or `false` force the popover open or closed, ignoring all interactions. */
53
+ trigger: boolean | PopoverTrigger;
54
+ /**
55
+ * Returns the `popover` element to open. Called each time the popover opens, so it may create
56
+ * the element.
57
+ */
58
+ getPopover: () => HTMLElement;
59
+ } & PartialWithUndefined<{
60
+ /** Called when the user selects an item in the popover through the `NavController`. */
61
+ onNavSelect: (coords: Coords) => void;
62
+ /**
63
+ * How long to wait after the popover is triggered before opening it. Defaults to 300
64
+ * milliseconds for {@link PopoverTrigger.Hover} and no delay for everything else.
65
+ */
66
+ delay: AnyDuration;
67
+ /** Closes the popover once it has been open this long. By default it stays open. */
68
+ timeout: AnyDuration;
69
+ }>;
70
+ /**
71
+ * Opens and closes a single `popover` element that is anchored to another element: it opens the
72
+ * popover in response to the anchor's interactions (see {@link PopoverTrigger}) and closes it on
73
+ * Escape, an outside mousedown, or the page going inactive. Style the popover with
74
+ * `createAnchoredPopoverStyles`.
75
+ *
76
+ * @category Popover
77
+ */
78
+ export declare class PopoverManager {
79
+ /**
80
+ * Enables arrow key navigation within the open popover. Omit this for popovers that have
81
+ * nothing to navigate, like tooltips.
82
+ */
83
+ readonly navController?: NavController | undefined;
84
+ protected anchor: HTMLElement | undefined;
85
+ protected options: Readonly<PopoverOptions> | undefined;
86
+ /** The most recently opened popover. */
87
+ protected popover: HTMLElement | undefined;
88
+ protected showTimeout: ReturnType<typeof setTimeout> | undefined;
89
+ /** Closes the popover after `options.timeout`. */
90
+ protected hideTimeout: ReturnType<typeof setTimeout> | undefined;
91
+ /** Removes the listeners for the current anchor and `trigger`. */
92
+ protected removeAnchorListeners: (() => void) | undefined;
93
+ /** Removes the global listeners attached while the popover is open. */
94
+ protected removeOpenListeners: (() => void) | undefined;
95
+ constructor(
96
+ /**
97
+ * Enables arrow key navigation within the open popover. Omit this for popovers that have
98
+ * nothing to navigate, like tooltips.
99
+ */
100
+ navController?: NavController | undefined);
101
+ /** Whether the popover is open. */
102
+ isOpen(): boolean;
103
+ /** Call this each time the anchor or options might have changed. The `popover` directive does. */
104
+ update(anchor: HTMLElement, options: Readonly<PopoverOptions>): void;
105
+ /** Opens or closes the popover when `trigger` is `true` or `false`. */
106
+ applyForcedState(): void;
107
+ /** Opens the popover right away. */
108
+ show(): void;
109
+ /** Closes the popover, unless `trigger` forces it open, and cancels a scheduled show. */
110
+ hide(): void;
111
+ /** Opens the popover after `options.delay`. */
112
+ scheduleShow(): void;
113
+ /** Opens the popover, or closes it (and cancels a delayed open). */
114
+ toggle(): void;
115
+ /** Opens a closed popover when an arrow key is pressed. */
116
+ openOnArrow(event: Readonly<KeyboardEvent>): void;
117
+ /** Closes the popover and removes all listeners. */
118
+ destroy(): void;
119
+ /** Closes the popover even when `trigger` forces it open. */
120
+ close(): void;
121
+ /** Attaches the global listeners that only matter while the popover is open. */
122
+ protected attachOpenListeners(): () => void;
123
+ }
@@ -0,0 +1,414 @@
1
+ import { check } from '@augment-vir/assert';
2
+ import { walkActiveElement } from '@augment-vir/web';
3
+ import { convertDuration } from 'date-vir';
4
+ import { NavActivateEvent, NavDirection } from 'device-navigation';
5
+ import { listenToPageActivation } from 'page-active';
6
+ import { listenTo, listenToGlobal } from 'typed-event-target';
7
+ import { anchoredPopoverRetryClass } from './anchored-popover.js';
8
+ /**
9
+ * Used to prevent popovers from closing when a text input is active.
10
+ *
11
+ * @category Internal
12
+ */
13
+ export function isInputLikeElement(element) {
14
+ return ((element instanceof HTMLInputElement &&
15
+ (element.type === 'text' ||
16
+ element.type === 'search' ||
17
+ element.type === 'email' ||
18
+ element.type === 'url' ||
19
+ element.type === 'tel' ||
20
+ element.type === 'password' ||
21
+ element.type === 'number')) ||
22
+ element instanceof HTMLTextAreaElement ||
23
+ (element instanceof HTMLElement && element.isContentEditable));
24
+ }
25
+ /**
26
+ * Whether a keyboard click (a click with `detail` of `0`) came from pressing Enter in a text input,
27
+ * which browsers turn into a click on the form's first button.
28
+ *
29
+ * @category Internal
30
+ */
31
+ export function isTextInputActive() {
32
+ let isActive = false;
33
+ walkActiveElement(({ element }) => {
34
+ isActive = isInputLikeElement(element);
35
+ return isActive;
36
+ });
37
+ return isActive;
38
+ }
39
+ /**
40
+ * Determines if a mouse event landed on a scrollbar rather than on element content. Scrollbar
41
+ * interactions fire mouse events on the scrolling element itself (or on `<html>` for the page
42
+ * scrollbar), so an outside-click check would otherwise treat dragging a scrollbar as a click away
43
+ * from the popover and dismiss it mid-drag.
44
+ *
45
+ * @category Internal
46
+ */
47
+ export function isMouseEventOnScrollbar(event) {
48
+ const target = event.composedPath()[0];
49
+ if (!(target instanceof Element)) {
50
+ return false;
51
+ }
52
+ const rect = target.getBoundingClientRect();
53
+ /**
54
+ * `clientLeft` and `clientTop` are the border widths, which also include the scrollbar itself
55
+ * when it's rendered on the leading edge (RTL layouts).
56
+ */
57
+ const clientLeft = rect.left + target.clientLeft;
58
+ const clientTop = rect.top + target.clientTop;
59
+ const outsideHorizontally = event.clientX < clientLeft || event.clientX >= clientLeft + target.clientWidth;
60
+ const outsideVertically = event.clientY < clientTop || event.clientY >= clientTop + target.clientHeight;
61
+ return ((outsideHorizontally && target.scrollHeight > target.clientHeight) ||
62
+ (outsideVertically && target.scrollWidth > target.clientWidth));
63
+ }
64
+ /**
65
+ * The interaction on the anchor element that shows and hides a popover.
66
+ *
67
+ * @category Popover
68
+ */
69
+ export var PopoverTrigger;
70
+ (function (PopoverTrigger) {
71
+ /**
72
+ * Shows the popover shortly after the anchor is hovered or focused and hides it when the anchor
73
+ * is no longer hovered or focused.
74
+ */
75
+ PopoverTrigger["Hover"] = "hover";
76
+ /** Toggles the popover when the anchor is clicked. An arrow key opens it. */
77
+ PopoverTrigger["Click"] = "click";
78
+ /**
79
+ * Toggles the popover as soon as the mouse is pressed on the anchor, so it can be dragged over
80
+ * the popover's contents and released on one of them. Keyboard clicks and arrow keys also
81
+ * work.
82
+ */
83
+ PopoverTrigger["Mousedown"] = "mousedown";
84
+ })(PopoverTrigger || (PopoverTrigger = {}));
85
+ const defaultDelays = {
86
+ [PopoverTrigger.Hover]: {
87
+ milliseconds: 300,
88
+ },
89
+ };
90
+ function listenToAnchor(anchor, eventName, listener) {
91
+ return listenTo(anchor, eventName, (event) => {
92
+ listener(event);
93
+ });
94
+ }
95
+ const anchorListeners = {
96
+ [PopoverTrigger.Hover](popoverManager, anchor) {
97
+ return [
98
+ listenToAnchor(anchor, 'mouseenter', () => {
99
+ popoverManager.scheduleShow();
100
+ }),
101
+ listenToAnchor(anchor, 'focusin', () => {
102
+ popoverManager.scheduleShow();
103
+ }),
104
+ listenToAnchor(anchor, 'mouseleave', () => {
105
+ popoverManager.hide();
106
+ }),
107
+ listenToAnchor(anchor, 'focusout', () => {
108
+ popoverManager.hide();
109
+ }),
110
+ ];
111
+ },
112
+ [PopoverTrigger.Click](popoverManager, anchor) {
113
+ return [
114
+ listenToAnchor(anchor, 'click', (event) => {
115
+ if (event.detail !== 0 || !isTextInputActive()) {
116
+ popoverManager.toggle();
117
+ }
118
+ }),
119
+ listenToAnchor(anchor, 'keydown', (event) => {
120
+ popoverManager.openOnArrow(event);
121
+ }),
122
+ ];
123
+ },
124
+ [PopoverTrigger.Mousedown](popoverManager, anchor) {
125
+ return [
126
+ listenToAnchor(anchor, 'mousedown', (event) => {
127
+ if (event.button === 0 && !isMouseEventOnScrollbar(event)) {
128
+ const target = event.composedPath()[0];
129
+ if (!(target instanceof Element && isInputLikeElement(target))) {
130
+ /**
131
+ * Without this, WebKit scrolls the anchor's scroll containers when the
132
+ * pointer is dragged past their edges on the way to a popover item.
133
+ */
134
+ event.preventDefault();
135
+ /** Preventing the default also prevents the focus that mousedown gives. */
136
+ event
137
+ .composedPath()
138
+ .find((eventTarget) => {
139
+ return (eventTarget instanceof HTMLElement && eventTarget.tabIndex >= 0);
140
+ })
141
+ ?.focus({
142
+ preventScroll: true,
143
+ /** Otherwise a mouse click shows the keyboard focus ring. */
144
+ focusVisible: false,
145
+ });
146
+ }
147
+ popoverManager.toggle();
148
+ }
149
+ }),
150
+ listenToAnchor(anchor, 'click', (event) => {
151
+ /** Mouse clicks were already handled by the mousedown. */
152
+ if (event.detail === 0 && !isTextInputActive()) {
153
+ popoverManager.toggle();
154
+ }
155
+ }),
156
+ listenToAnchor(anchor, 'keydown', (event) => {
157
+ popoverManager.openOnArrow(event);
158
+ }),
159
+ ];
160
+ },
161
+ };
162
+ /**
163
+ * Opens and closes a single `popover` element that is anchored to another element: it opens the
164
+ * popover in response to the anchor's interactions (see {@link PopoverTrigger}) and closes it on
165
+ * Escape, an outside mousedown, or the page going inactive. Style the popover with
166
+ * `createAnchoredPopoverStyles`.
167
+ *
168
+ * @category Popover
169
+ */
170
+ export class PopoverManager {
171
+ navController;
172
+ anchor;
173
+ options;
174
+ /** The most recently opened popover. */
175
+ popover;
176
+ showTimeout;
177
+ /** Closes the popover after `options.timeout`. */
178
+ hideTimeout;
179
+ /** Removes the listeners for the current anchor and `trigger`. */
180
+ removeAnchorListeners;
181
+ /** Removes the global listeners attached while the popover is open. */
182
+ removeOpenListeners;
183
+ constructor(
184
+ /**
185
+ * Enables arrow key navigation within the open popover. Omit this for popovers that have
186
+ * nothing to navigate, like tooltips.
187
+ */
188
+ navController) {
189
+ this.navController = navController;
190
+ }
191
+ /** Whether the popover is open. */
192
+ isOpen() {
193
+ return !!this.popover?.matches(':popover-open');
194
+ }
195
+ /** Call this each time the anchor or options might have changed. The `popover` directive does. */
196
+ update(anchor, options) {
197
+ const previousTrigger = this.options?.trigger;
198
+ this.options = options;
199
+ if (anchor === this.anchor && options.trigger === previousTrigger) {
200
+ return;
201
+ }
202
+ this.removeAnchorListeners?.();
203
+ this.anchor = anchor;
204
+ const removeListeners = check.isBoolean(options.trigger)
205
+ ? []
206
+ : anchorListeners[options.trigger](this, anchor);
207
+ this.removeAnchorListeners = () => {
208
+ removeListeners.forEach((removeListener) => removeListener());
209
+ };
210
+ this.applyForcedState();
211
+ }
212
+ /** Opens or closes the popover when `trigger` is `true` or `false`. */
213
+ applyForcedState() {
214
+ if (this.options?.trigger === true) {
215
+ /**
216
+ * Deferred because on first render lit updates directives before inserting the template
217
+ * into the document, and a disconnected popover cannot open.
218
+ */
219
+ queueMicrotask(() => {
220
+ if (this.options?.trigger === true) {
221
+ this.scheduleShow();
222
+ }
223
+ });
224
+ }
225
+ else if (this.options?.trigger === false) {
226
+ this.close();
227
+ }
228
+ }
229
+ /** Opens the popover right away. */
230
+ show() {
231
+ clearTimeout(this.showTimeout);
232
+ this.showTimeout = undefined;
233
+ if (!this.anchor || !this.options || this.isOpen()) {
234
+ return;
235
+ }
236
+ this.popover = this.options.getPopover();
237
+ this.popover.showPopover({
238
+ source: this.anchor,
239
+ });
240
+ this.removeOpenListeners = this.attachOpenListeners();
241
+ if (this.options.timeout) {
242
+ this.hideTimeout = setTimeout(() => {
243
+ this.hide();
244
+ }, convertDuration(this.options.timeout, {
245
+ milliseconds: true,
246
+ }).milliseconds);
247
+ }
248
+ }
249
+ /** Closes the popover, unless `trigger` forces it open, and cancels a scheduled show. */
250
+ hide() {
251
+ if (this.options?.trigger !== true) {
252
+ this.close();
253
+ }
254
+ }
255
+ /** Opens the popover after `options.delay`. */
256
+ scheduleShow() {
257
+ if (this.showTimeout || this.isOpen() || !this.options) {
258
+ return;
259
+ }
260
+ const rawDelay = this.options.delay ||
261
+ (check.isBoolean(this.options.trigger) ? {} : defaultDelays[this.options.trigger]);
262
+ const delay = rawDelay &&
263
+ convertDuration(rawDelay, {
264
+ milliseconds: true,
265
+ });
266
+ if (delay && delay.milliseconds > 0) {
267
+ this.showTimeout = setTimeout(() => {
268
+ this.show();
269
+ }, delay.milliseconds);
270
+ }
271
+ else {
272
+ this.show();
273
+ }
274
+ }
275
+ /** Opens the popover, or closes it (and cancels a delayed open). */
276
+ toggle() {
277
+ if (this.isOpen() || this.showTimeout) {
278
+ this.hide();
279
+ }
280
+ else {
281
+ this.scheduleShow();
282
+ }
283
+ }
284
+ /** Opens a closed popover when an arrow key is pressed. */
285
+ openOnArrow(event) {
286
+ if (event.code.startsWith('Arrow')) {
287
+ this.scheduleShow();
288
+ }
289
+ }
290
+ /** Closes the popover and removes all listeners. */
291
+ destroy() {
292
+ this.removeAnchorListeners?.();
293
+ this.removeAnchorListeners = undefined;
294
+ this.anchor = undefined;
295
+ this.close();
296
+ }
297
+ /** Closes the popover even when `trigger` forces it open. */
298
+ close() {
299
+ clearTimeout(this.showTimeout);
300
+ this.showTimeout = undefined;
301
+ clearTimeout(this.hideTimeout);
302
+ this.hideTimeout = undefined;
303
+ this.removeOpenListeners?.();
304
+ this.removeOpenListeners = undefined;
305
+ if (this.isOpen()) {
306
+ this.popover?.hidePopover();
307
+ }
308
+ }
309
+ /** Attaches the global listeners that only matter while the popover is open. */
310
+ attachOpenListeners() {
311
+ const navController = this.navController;
312
+ const removeListeners = [
313
+ listenToGlobal('resize', () => {
314
+ this.popover?.classList.toggle(anchoredPopoverRetryClass);
315
+ /** `nearest` does not scroll at all when the anchor is already fully visible. */
316
+ this.anchor?.scrollIntoView({
317
+ block: 'nearest',
318
+ inline: 'nearest',
319
+ });
320
+ }, {
321
+ passive: true,
322
+ }),
323
+ listenToGlobal('scroll', () => {
324
+ this.popover?.classList.toggle(anchoredPopoverRetryClass);
325
+ }, {
326
+ /** Scroll events do not bubble, so this catches scrolling in any element. */
327
+ capture: true,
328
+ passive: true,
329
+ }),
330
+ listenToPageActivation(false, (isPageActive) => {
331
+ if (!isPageActive) {
332
+ this.hide();
333
+ }
334
+ }),
335
+ navController?.listen(NavActivateEvent, (event) => {
336
+ const target = event.composedPath()[0];
337
+ if (target instanceof Element && isInputLikeElement(target)) {
338
+ return;
339
+ }
340
+ if (event.detail.success) {
341
+ this.options?.onNavSelect?.(event.detail.coords);
342
+ navController.currentNavEntry?.entry.focus(true);
343
+ event.stopImmediatePropagation();
344
+ event.preventDefault();
345
+ }
346
+ }),
347
+ listenToGlobal('mousedown', (event) => {
348
+ const path = event.composedPath();
349
+ if ((this.anchor && path.includes(this.anchor)) ||
350
+ (this.popover && path.includes(this.popover)) ||
351
+ isMouseEventOnScrollbar(event)) {
352
+ return;
353
+ }
354
+ this.hide();
355
+ }, {
356
+ passive: true,
357
+ }),
358
+ listenToGlobal('keydown', (event) => {
359
+ const keyCode = event.code;
360
+ if (keyCode === 'Escape') {
361
+ this.hide();
362
+ }
363
+ else if (navController) {
364
+ const target = event.composedPath()[0];
365
+ if (target instanceof Element && isInputLikeElement(target)) {
366
+ return;
367
+ }
368
+ if (keyCode === 'ArrowDown') {
369
+ event.stopImmediatePropagation();
370
+ event.preventDefault();
371
+ navController.navigate({
372
+ direction: NavDirection.Down,
373
+ allowWrapping: false,
374
+ });
375
+ }
376
+ else if (keyCode === 'ArrowUp') {
377
+ event.stopImmediatePropagation();
378
+ event.preventDefault();
379
+ navController.navigate({
380
+ direction: NavDirection.Up,
381
+ allowWrapping: false,
382
+ });
383
+ }
384
+ else if (keyCode === 'ArrowLeft') {
385
+ event.stopImmediatePropagation();
386
+ event.preventDefault();
387
+ navController.navigate({
388
+ direction: NavDirection.Left,
389
+ allowWrapping: false,
390
+ });
391
+ }
392
+ else if (keyCode === 'ArrowRight') {
393
+ event.stopImmediatePropagation();
394
+ event.preventDefault();
395
+ navController.navigate({
396
+ direction: NavDirection.Right,
397
+ allowWrapping: false,
398
+ });
399
+ }
400
+ else if ((keyCode === 'Enter' || keyCode === 'Return' || keyCode === 'Space') &&
401
+ navController.enterInto({
402
+ fallbackToActivate: true,
403
+ }).success) {
404
+ event.stopImmediatePropagation();
405
+ event.preventDefault();
406
+ }
407
+ }
408
+ }),
409
+ ].filter(check.isDefined);
410
+ return () => {
411
+ removeListeners.forEach((removeListener) => removeListener());
412
+ };
413
+ }
414
+ }
@@ -0,0 +1,30 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ /**
3
+ * Options for `ViraDropdown`.
4
+ *
5
+ * @category Dropdown
6
+ * @category Elements
7
+ */
8
+ export type ViraDropdownOption = {
9
+ /** A value or id, used to keep track of which option is selected. */
10
+ value: string;
11
+ label: string;
12
+ } & PartialWithUndefined<{
13
+ disabled: boolean;
14
+ }>;
15
+ /**
16
+ * A group of options for `ViraDropdown`, rendered under a group label.
17
+ *
18
+ * @category Dropdown
19
+ * @category Elements
20
+ */
21
+ export type ViraDropdownOptionGroup = {
22
+ groupName: string;
23
+ options: ReadonlyArray<Readonly<ViraDropdownOption>>;
24
+ };
25
+ /**
26
+ * Type guard to determine if a `ViraDropdown` options entry is a group.
27
+ *
28
+ * @category Internal
29
+ */
30
+ export declare function isViraDropdownOptionGroup(entry: Readonly<ViraDropdownOption> | Readonly<ViraDropdownOptionGroup>): entry is Readonly<ViraDropdownOptionGroup>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type guard to determine if a `ViraDropdown` options entry is a group.
3
+ *
4
+ * @category Internal
5
+ */
6
+ export function isViraDropdownOptionGroup(entry) {
7
+ return 'groupName' in entry;
8
+ }
@@ -2,7 +2,7 @@ import { type PartialWithUndefined } from '@augment-vir/common';
2
2
  import { type FullDate, type Timezone } from 'date-vir';
3
3
  import { type HtmlInterpolation } from 'element-vir';
4
4
  import { type ViraIconSvg } from '../icons/icon-svg.js';
5
- import { type ViraSelectOption } from './vira-select-option.js';
5
+ import { type ViraDropdownOption } from './vira-dropdown-option.js';
6
6
  /**
7
7
  * Form field types for {@link ViraFormField}.
8
8
  *
@@ -73,7 +73,7 @@ export type ViraFormField = ({
73
73
  }> & CommonViraFormFields) | ({
74
74
  type: ViraFormFieldType.Select;
75
75
  value: string | undefined;
76
- options: ReadonlyArray<Readonly<ViraSelectOption>>;
76
+ options: ReadonlyArray<Readonly<ViraDropdownOption>>;
77
77
  } & PartialWithUndefined<{
78
78
  placeholder: string;
79
79
  icon: ViraIconSvg;
@@ -72,7 +72,7 @@ export declare class ViraThemeClient {
72
72
  * The currently selected theme. If you use multiple clients to set the same theme, this might
73
73
  * get out of sync.
74
74
  */
75
- get currentTheme(): ViraThemeSelection;
75
+ getCurrentTheme(): ViraThemeSelection;
76
76
  /** Set the selected theme. */
77
77
  setSelectedTheme(selection: ViraThemeSelection): void;
78
78
  /** Cleanup internal state and listeners. */
@@ -47,7 +47,7 @@ export class ViraThemeClient {
47
47
  /** A callback to remove the global theme preference listener. */
48
48
  removeThemePreferenceListener = listenTo(globalThis.matchMedia(darkSchemeMediaQuery), 'change', (event) => {
49
49
  assert.instanceOf(event, MediaQueryListEvent);
50
- if (this.currentTheme === ViraThemeSelection.Auto) {
50
+ if (this.getCurrentTheme() === ViraThemeSelection.Auto) {
51
51
  void this.applyThemeCallback({
52
52
  useDarkTheme: event.matches,
53
53
  });
@@ -60,13 +60,13 @@ export class ViraThemeClient {
60
60
  this.localStorageClient = new LocalStorageClient(themeStorageShapes, {
61
61
  storeName: params.storeName || 'vira-theme',
62
62
  });
63
- this.applySelection(this.currentTheme);
63
+ this.applySelection(this.getCurrentTheme());
64
64
  }
65
65
  /**
66
66
  * The currently selected theme. If you use multiple clients to set the same theme, this might
67
67
  * get out of sync.
68
68
  */
69
- get currentTheme() {
69
+ getCurrentTheme() {
70
70
  return this.localStorageClient.get.selectedTheme()?.theme || ViraThemeSelection.Auto;
71
71
  }
72
72
  /** Set the selected theme. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "32.4.0",
3
+ "version": "33.1.0",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",
@@ -46,6 +46,7 @@
46
46
  "date-vir": "^9.1.1",
47
47
  "device-navigation": "^5.0.0",
48
48
  "json-schema-to-ts": "^3.1.1",
49
+ "lit": "^3.3.3",
49
50
  "lit-css-vars": "^3.6.3",
50
51
  "object-shape-tester": "^6.15.0",
51
52
  "observavir": "^2.3.3",