vira 33.0.0 → 33.2.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.
- package/dist/elements/popover/vira-menu-item.element.js +9 -0
- package/dist/elements/popover/vira-popover-trigger.element.d.ts +1 -0
- package/dist/elements/popover/vira-popover-trigger.element.js +8 -2
- package/dist/elements/vira-dropdown.element.d.ts +12 -1
- package/dist/elements/vira-dropdown.element.js +251 -17
- package/dist/util/fuzzy-match.d.ts +16 -0
- package/dist/util/fuzzy-match.js +24 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/menu-helpers.d.ts +4 -1
- package/dist/util/menu-helpers.js +10 -2
- package/dist/util/popover-manager.js +2 -0
- package/dist/util/vira-dropdown-option.d.ts +10 -0
- package/package.json +1 -1
|
@@ -207,6 +207,15 @@ export const ViraMenuItem = defineViraElement()({
|
|
|
207
207
|
}, {
|
|
208
208
|
capture: true,
|
|
209
209
|
}),
|
|
210
|
+
listenTo(host, 'keydown', (event) => {
|
|
211
|
+
if (event instanceof KeyboardEvent &&
|
|
212
|
+
event.target === host &&
|
|
213
|
+
(event.key === 'Enter' || event.key === ' ')) {
|
|
214
|
+
/** Prevents Space from scrolling the menu. */
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
host.click();
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
210
219
|
listenTo(host, 'mouseenter', () => {
|
|
211
220
|
if (!inputs.disabled) {
|
|
212
221
|
host.focus();
|
|
@@ -45,5 +45,6 @@ export declare const ViraPopoverTrigger: import("element-vir").DeclarativeElemen
|
|
|
45
45
|
openChange: import("element-vir").DefineElementEvent<boolean>;
|
|
46
46
|
init: import("element-vir").DefineElementEvent<{
|
|
47
47
|
navController: NavController;
|
|
48
|
+
popoverManager: PopoverManager;
|
|
48
49
|
}>;
|
|
49
50
|
}, "vira-popover-trigger-disabled" | "vira-popover-trigger-inside-focus" | "vira-popover-trigger-outside-focus", "vira-popover-trigger-offset-vertical" | "vira-popover-trigger-offset-left" | "vira-popover-trigger-offset-right", readonly ["vira-popover-trigger-trigger", "vira-popover-trigger-popover"], readonly []>;
|
|
@@ -165,8 +165,13 @@ export const ViraPopoverTrigger = defineViraElement()({
|
|
|
165
165
|
it flip upwards when there is not much room below the trigger.
|
|
166
166
|
*/
|
|
167
167
|
min-height: 200px;
|
|
168
|
-
/*
|
|
169
|
-
|
|
168
|
+
/*
|
|
169
|
+
Margins are not included in the percentage. The extra 4px keeps the popover off
|
|
170
|
+
the window's top or bottom edge.
|
|
171
|
+
*/
|
|
172
|
+
max-height: calc(
|
|
173
|
+
100% - ${cssVars['vira-popover-trigger-offset-vertical'].value} - 4px
|
|
174
|
+
);
|
|
170
175
|
width: max-content;
|
|
171
176
|
min-width: calc(
|
|
172
177
|
anchor-size(width) - ${cssVars['vira-popover-trigger-offset-left'].value} -
|
|
@@ -225,6 +230,7 @@ export const ViraPopoverTrigger = defineViraElement()({
|
|
|
225
230
|
dispatch(new events.init({
|
|
226
231
|
detail: {
|
|
227
232
|
navController: state.navController,
|
|
233
|
+
popoverManager: state.popoverManager,
|
|
228
234
|
},
|
|
229
235
|
}));
|
|
230
236
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type NavController } from 'device-navigation';
|
|
2
3
|
import { type AttributeValues, type HtmlInterpolation } from 'element-vir';
|
|
3
4
|
import { type ViraIconSvg } from '../icons/icon-svg.js';
|
|
5
|
+
import { type PopoverManager } from '../util/popover-manager.js';
|
|
4
6
|
import { type ViraDropdownOption, type ViraDropdownOptionGroup } from '../util/vira-dropdown-option.js';
|
|
5
7
|
/**
|
|
6
8
|
* A dropdown element that uses popover menus.
|
|
@@ -26,6 +28,11 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
|
|
|
26
28
|
* multiple.
|
|
27
29
|
*/
|
|
28
30
|
isMultiSelect: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Replaces the selection text with a text input. Opening the dropdown focuses it and typing
|
|
33
|
+
* into it filters the options to those whose labels fuzzy match the typed text.
|
|
34
|
+
*/
|
|
35
|
+
isSearchable: boolean;
|
|
29
36
|
icon: ViraIconSvg;
|
|
30
37
|
selectionPrefix: string;
|
|
31
38
|
isDisabled: boolean;
|
|
@@ -53,9 +60,13 @@ export declare const ViraDropdown: import("element-vir").DeclarativeElementDefin
|
|
|
53
60
|
* provided.
|
|
54
61
|
*/
|
|
55
62
|
randomId: string;
|
|
63
|
+
/** The text typed into the search input while the dropdown is open. */
|
|
64
|
+
searchText: string;
|
|
65
|
+
navController: undefined | NavController;
|
|
66
|
+
popoverManager: undefined | PopoverManager;
|
|
56
67
|
}, {
|
|
57
68
|
/** Emits all currently selected values. */
|
|
58
69
|
selectedValuesChange: import("element-vir").DefineElementEvent<string[]>;
|
|
59
70
|
/** `true` when the popover just opened, `false` when it just closed. */
|
|
60
71
|
openChange: import("element-vir").DefineElementEvent<boolean>;
|
|
61
|
-
}, "vira-dropdown-", "vira-dropdown-", readonly [], readonly ["leadingIcon", "prefixText", "trigger"]>;
|
|
72
|
+
}, "vira-dropdown-", "vira-dropdown-", readonly [], readonly ["leadingIcon", "prefixText", "searchInput", "trigger"]>;
|
|
@@ -1,12 +1,15 @@
|
|
|
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 { nav, NavDirection } from 'device-navigation';
|
|
4
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
9
|
import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
|
|
8
|
-
import { noUserSelect, viraAnimationDurations } from '../styles/index.js';
|
|
10
|
+
import { noNativeFormStyles, noUserSelect, viraAnimationDurations } from '../styles/index.js';
|
|
9
11
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
12
|
+
import { fuzzyMatch } from '../util/fuzzy-match.js';
|
|
10
13
|
import { renderMenuItemEntries } from '../util/menu-helpers.js';
|
|
11
14
|
import { isViraDropdownOptionGroup, } from '../util/vira-dropdown-option.js';
|
|
12
15
|
import { ViraMenuItem } from './popover/vira-menu-item.element.js';
|
|
@@ -25,6 +28,7 @@ export const ViraDropdown = defineViraElement()({
|
|
|
25
28
|
testIds: [
|
|
26
29
|
'leadingIcon',
|
|
27
30
|
'prefixText',
|
|
31
|
+
'searchInput',
|
|
28
32
|
'trigger',
|
|
29
33
|
],
|
|
30
34
|
styles: css `
|
|
@@ -46,6 +50,56 @@ export const ViraDropdown = defineViraElement()({
|
|
|
46
50
|
white-space: nowrap;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
.option-label {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
vertical-align: bottom;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.option-icon {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.option-label-text {
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
text-overflow: ellipsis;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.search-input {
|
|
72
|
+
${noNativeFormStyles};
|
|
73
|
+
flex-grow: 1;
|
|
74
|
+
min-width: 0;
|
|
75
|
+
align-self: stretch;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
cursor: inherit;
|
|
78
|
+
user-select: text;
|
|
79
|
+
-webkit-user-select: text;
|
|
80
|
+
outline: none;
|
|
81
|
+
|
|
82
|
+
&::placeholder {
|
|
83
|
+
color: inherit;
|
|
84
|
+
opacity: 0.4;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.open .search-input {
|
|
89
|
+
cursor: text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dropdown-trigger:has(.search-input:focus-visible:not([disabled])) {
|
|
93
|
+
position: relative;
|
|
94
|
+
|
|
95
|
+
&::after {
|
|
96
|
+
${createFocusStyles({
|
|
97
|
+
elementBorderSize: '1px',
|
|
98
|
+
noNesting: true,
|
|
99
|
+
})}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
.trigger-icon {
|
|
50
104
|
width: 24px;
|
|
51
105
|
aspect-ratio: 1;
|
|
@@ -155,9 +209,13 @@ export const ViraDropdown = defineViraElement()({
|
|
|
155
209
|
* provided.
|
|
156
210
|
*/
|
|
157
211
|
randomId: randomString(32),
|
|
212
|
+
/** The text typed into the search input while the dropdown is open. */
|
|
213
|
+
searchText: '',
|
|
214
|
+
navController: undefined,
|
|
215
|
+
popoverManager: undefined,
|
|
158
216
|
};
|
|
159
217
|
},
|
|
160
|
-
render({ state, inputs, dispatch, events, updateState, testIds }) {
|
|
218
|
+
render({ state, inputs, dispatch, events, updateState, testIds, host }) {
|
|
161
219
|
const flatOptions = inputs.options.flatMap((entry) => {
|
|
162
220
|
return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
|
|
163
221
|
});
|
|
@@ -184,6 +242,29 @@ export const ViraDropdown = defineViraElement()({
|
|
|
184
242
|
: inputs.isMultiSelect && selectedOptions.length > 1
|
|
185
243
|
? `${selectedOptions.length} Selected`
|
|
186
244
|
: selectedOptions[0]?.label || '';
|
|
245
|
+
function renderOptionLabel(option) {
|
|
246
|
+
return option.icon || option.labelTemplate
|
|
247
|
+
? html `
|
|
248
|
+
<span class="option-label">
|
|
249
|
+
${option.icon
|
|
250
|
+
? html `
|
|
251
|
+
<${ViraIcon.assign({
|
|
252
|
+
icon: option.icon,
|
|
253
|
+
})}
|
|
254
|
+
class="option-icon"
|
|
255
|
+
></${ViraIcon}>
|
|
256
|
+
`
|
|
257
|
+
: nothing}
|
|
258
|
+
<span class="option-label-text">
|
|
259
|
+
${option.labelTemplate ?? option.label}
|
|
260
|
+
</span>
|
|
261
|
+
</span>
|
|
262
|
+
`
|
|
263
|
+
: option.label;
|
|
264
|
+
}
|
|
265
|
+
const selectionTemplate = check.isLengthExactly(selectedOptions, 1)
|
|
266
|
+
? renderOptionLabel(selectedOptions[0])
|
|
267
|
+
: selectionDisplay;
|
|
187
268
|
function selectOption(option) {
|
|
188
269
|
const newSelectedValues = inputs.isMultiSelect
|
|
189
270
|
? selectedOptions.includes(option)
|
|
@@ -197,6 +278,42 @@ export const ViraDropdown = defineViraElement()({
|
|
|
197
278
|
detail: newSelectedValues,
|
|
198
279
|
}));
|
|
199
280
|
}
|
|
281
|
+
const filteredOptions = state.searchText
|
|
282
|
+
? filterMap(inputs.options, (entry) => {
|
|
283
|
+
return isViraDropdownOptionGroup(entry)
|
|
284
|
+
? {
|
|
285
|
+
...entry,
|
|
286
|
+
options: entry.options.filter((option) => {
|
|
287
|
+
return fuzzyMatch({
|
|
288
|
+
search: state.searchText,
|
|
289
|
+
text: option.label,
|
|
290
|
+
});
|
|
291
|
+
}),
|
|
292
|
+
}
|
|
293
|
+
: entry;
|
|
294
|
+
}, (entry) => {
|
|
295
|
+
return isViraDropdownOptionGroup(entry)
|
|
296
|
+
? !!entry.options.length
|
|
297
|
+
: fuzzyMatch({
|
|
298
|
+
search: state.searchText,
|
|
299
|
+
text: entry.label,
|
|
300
|
+
});
|
|
301
|
+
})
|
|
302
|
+
: inputs.options;
|
|
303
|
+
const filteredFlatOptions = filteredOptions.flatMap((entry) => {
|
|
304
|
+
return isViraDropdownOptionGroup(entry) ? entry.options : [entry];
|
|
305
|
+
});
|
|
306
|
+
function findSearchInput() {
|
|
307
|
+
const searchInput = host.shadowRoot.querySelector('.search-input');
|
|
308
|
+
return searchInput instanceof HTMLInputElement ? searchInput : undefined;
|
|
309
|
+
}
|
|
310
|
+
function clearSearch(searchInput) {
|
|
311
|
+
/** Set directly because the `.value` binding skips values it has already committed. */
|
|
312
|
+
searchInput.value = '';
|
|
313
|
+
updateState({
|
|
314
|
+
searchText: '',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
200
317
|
function toggleGroup(group) {
|
|
201
318
|
const enabledGroupOptions = group.options.filter((option) => !option.disabled);
|
|
202
319
|
const isGroupSelected = enabledGroupOptions.every((option) => {
|
|
@@ -220,6 +337,7 @@ export const ViraDropdown = defineViraElement()({
|
|
|
220
337
|
disablePointerStyles: true,
|
|
221
338
|
})}
|
|
222
339
|
class="option-group-label"
|
|
340
|
+
${state.navController ? nav(state.navController) : nothing}
|
|
223
341
|
${listen('click', () => {
|
|
224
342
|
toggleGroup(group);
|
|
225
343
|
})}
|
|
@@ -232,7 +350,7 @@ export const ViraDropdown = defineViraElement()({
|
|
|
232
350
|
`;
|
|
233
351
|
}
|
|
234
352
|
/** Drag-select actions in the same order as the rendered menu items. */
|
|
235
|
-
const menuItemActions =
|
|
353
|
+
const menuItemActions = filteredOptions.flatMap((entry) => {
|
|
236
354
|
const options = isViraDropdownOptionGroup(entry) ? entry.options : [entry];
|
|
237
355
|
const optionActions = options.map((option) => {
|
|
238
356
|
return option.disabled
|
|
@@ -253,16 +371,16 @@ export const ViraDropdown = defineViraElement()({
|
|
|
253
371
|
function renderOptionEntries(options) {
|
|
254
372
|
return renderMenuItemEntries(options.map((option) => {
|
|
255
373
|
return {
|
|
256
|
-
content: option
|
|
374
|
+
content: renderOptionLabel(option),
|
|
257
375
|
onClick() {
|
|
258
376
|
selectOption(option);
|
|
259
377
|
},
|
|
260
378
|
disabled: option.disabled,
|
|
261
379
|
selected: selectedOptions.includes(option),
|
|
262
380
|
};
|
|
263
|
-
}));
|
|
381
|
+
}), state.navController);
|
|
264
382
|
}
|
|
265
|
-
const isMenuShown = state.isOpen && (!!
|
|
383
|
+
const isMenuShown = state.isOpen && (!!filteredFlatOptions.length || !!inputs.noOptionsText);
|
|
266
384
|
const menuTemplate = html `
|
|
267
385
|
<${ViraMenu.assign({
|
|
268
386
|
cornerStyle: ViraMenuCornerStyle.Round,
|
|
@@ -283,8 +401,8 @@ export const ViraDropdown = defineViraElement()({
|
|
|
283
401
|
}
|
|
284
402
|
})}
|
|
285
403
|
>
|
|
286
|
-
${
|
|
287
|
-
?
|
|
404
|
+
${filteredFlatOptions.length
|
|
405
|
+
? filteredOptions.map((entry) => {
|
|
288
406
|
return isViraDropdownOptionGroup(entry)
|
|
289
407
|
? html `
|
|
290
408
|
${renderGroupLabel(entry)} ${renderOptionEntries(entry.options)}
|
|
@@ -305,6 +423,12 @@ export const ViraDropdown = defineViraElement()({
|
|
|
305
423
|
vertical: -1,
|
|
306
424
|
right: 24,
|
|
307
425
|
},
|
|
426
|
+
})}
|
|
427
|
+
${listen(ViraPopoverTrigger.events.init, (event) => {
|
|
428
|
+
updateState({
|
|
429
|
+
navController: event.detail.navController,
|
|
430
|
+
popoverManager: event.detail.popoverManager,
|
|
431
|
+
});
|
|
308
432
|
})}
|
|
309
433
|
${listen(ViraPopoverTrigger.events.openChange, (event) => {
|
|
310
434
|
if (state.isOpen !== event.detail) {
|
|
@@ -315,6 +439,37 @@ export const ViraDropdown = defineViraElement()({
|
|
|
315
439
|
updateState({
|
|
316
440
|
isOpen: event.detail,
|
|
317
441
|
});
|
|
442
|
+
const searchInput = findSearchInput();
|
|
443
|
+
if (!searchInput) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
else if (event.detail) {
|
|
447
|
+
searchInput.focus();
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
clearSearch(searchInput);
|
|
451
|
+
}
|
|
452
|
+
})}
|
|
453
|
+
${listen('keydown', (event) => {
|
|
454
|
+
const searchInput = findSearchInput();
|
|
455
|
+
const isSearchKey = event.key === 'Backspace' ||
|
|
456
|
+
(event.key.length === 1 &&
|
|
457
|
+
event.key !== ' ' &&
|
|
458
|
+
!event.ctrlKey &&
|
|
459
|
+
!event.metaKey &&
|
|
460
|
+
!event.altKey);
|
|
461
|
+
if (!searchInput || !isSearchKey) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (!state.isOpen) {
|
|
465
|
+
clearSearch(searchInput);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Focusing during keydown makes the browser type the pressed key into the
|
|
469
|
+
* search input, even when the key was pressed on the trigger or a menu item.
|
|
470
|
+
*/
|
|
471
|
+
searchInput.focus();
|
|
472
|
+
state.popoverManager?.show();
|
|
318
473
|
})}
|
|
319
474
|
${listen('mouseup', () => {
|
|
320
475
|
if (state.shouldSelectOnMouseUp) {
|
|
@@ -342,14 +497,93 @@ export const ViraDropdown = defineViraElement()({
|
|
|
342
497
|
})}
|
|
343
498
|
>
|
|
344
499
|
${leadingIconTemplate}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
500
|
+
${inputs.isSearchable
|
|
501
|
+
? html `
|
|
502
|
+
${state.isOpen ? nothing : prefixTemplate}
|
|
503
|
+
<input
|
|
504
|
+
class="search-input"
|
|
505
|
+
${testId(testIds.searchInput)}
|
|
506
|
+
tabindex="-1"
|
|
507
|
+
autocomplete="off"
|
|
508
|
+
spellcheck="false"
|
|
509
|
+
aria-label=${ifDefined((check.isString(inputs.label) && inputs.label) || undefined)}
|
|
510
|
+
?disabled=${!!inputs.isDisabled}
|
|
511
|
+
title=${ifDefined(shouldUsePlaceholder || state.isOpen
|
|
512
|
+
? undefined
|
|
513
|
+
: selectionDisplay)}
|
|
514
|
+
placeholder=${state.isOpen && !shouldUsePlaceholder
|
|
515
|
+
? selectionDisplay
|
|
516
|
+
: inputs.placeholder || ''}
|
|
517
|
+
.value=${state.isOpen
|
|
518
|
+
? state.searchText
|
|
519
|
+
: shouldUsePlaceholder
|
|
520
|
+
? ''
|
|
521
|
+
: selectionDisplay}
|
|
522
|
+
${listen('mousedown', (event) => {
|
|
523
|
+
/** Keeps the popover open when clicking into the input. */
|
|
524
|
+
if (state.isOpen) {
|
|
525
|
+
event.stopPropagation();
|
|
526
|
+
}
|
|
527
|
+
})}
|
|
528
|
+
${listen('input', (event) => {
|
|
529
|
+
updateState({
|
|
530
|
+
searchText: extractEventTarget(event, HTMLInputElement)
|
|
531
|
+
.value,
|
|
532
|
+
});
|
|
533
|
+
state.popoverManager?.show();
|
|
534
|
+
})}
|
|
535
|
+
${listen('keydown', (event) => {
|
|
536
|
+
if (!state.isOpen) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
else if (event.key === 'ArrowDown' ||
|
|
540
|
+
event.key === 'ArrowUp') {
|
|
541
|
+
event.preventDefault();
|
|
542
|
+
state.navController?.navigate({
|
|
543
|
+
direction: event.key === 'ArrowDown'
|
|
544
|
+
? NavDirection.Down
|
|
545
|
+
: NavDirection.Up,
|
|
546
|
+
allowWrapping: false,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
else if (event.key === 'Tab' && !event.shiftKey) {
|
|
550
|
+
/**
|
|
551
|
+
* Without this, browsers move focus from this input past
|
|
552
|
+
* the menu items (sometimes closing the menu).
|
|
553
|
+
*/
|
|
554
|
+
const firstMenuItem = Array.from(host.shadowRoot.querySelectorAll(ViraMenuItem.tagName)).find((menuItem) => menuItem.tabIndex >= 0);
|
|
555
|
+
if (firstMenuItem) {
|
|
556
|
+
event.preventDefault();
|
|
557
|
+
firstMenuItem.focus();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else if (event.key === 'Enter') {
|
|
561
|
+
const firstOption = filteredFlatOptions.find((option) => !option.disabled);
|
|
562
|
+
if (!firstOption) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
event.preventDefault();
|
|
566
|
+
selectOption(firstOption);
|
|
567
|
+
if (inputs.isMultiSelect) {
|
|
568
|
+
clearSearch(extractEventTarget(event, HTMLInputElement));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
state.popoverManager?.hide();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})}
|
|
575
|
+
/>
|
|
576
|
+
`
|
|
577
|
+
: html `
|
|
578
|
+
<span
|
|
579
|
+
class="selection-display ${classMap({
|
|
580
|
+
'using-placeholder': shouldUsePlaceholder,
|
|
581
|
+
})}"
|
|
582
|
+
title=${ifDefined(shouldUsePlaceholder ? undefined : selectionDisplay)}
|
|
583
|
+
>
|
|
584
|
+
${prefixTemplate} ${selectionTemplate}
|
|
585
|
+
</span>
|
|
586
|
+
`}
|
|
353
587
|
|
|
354
588
|
<span class="trigger-icon-wrapper">
|
|
355
589
|
<${ViraIcon.assign({
|
|
@@ -364,7 +598,7 @@ export const ViraDropdown = defineViraElement()({
|
|
|
364
598
|
`;
|
|
365
599
|
const contentTemplate = inputs.isReadonly
|
|
366
600
|
? html `
|
|
367
|
-
<span class="readonly-value">${
|
|
601
|
+
<span class="readonly-value">${selectionTemplate}</span>
|
|
368
602
|
`
|
|
369
603
|
: triggerTemplate;
|
|
370
604
|
if (inputs.label) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if every non-whitespace character of `search` appears in `text`, in order, ignoring case.
|
|
3
|
+
* An empty `search` matches everything.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
* @example
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* fuzzyMatch({search: 'opa', text: 'Option A'}); // true
|
|
10
|
+
* fuzzyMatch({search: 'apo', text: 'Option A'}); // false
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function fuzzyMatch({ search, text, }: Readonly<{
|
|
14
|
+
search: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}>): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if every non-whitespace character of `search` appears in `text`, in order, ignoring case.
|
|
3
|
+
* An empty `search` matches everything.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
* @example
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* fuzzyMatch({search: 'opa', text: 'Option A'}); // true
|
|
10
|
+
* fuzzyMatch({search: 'apo', text: 'Option A'}); // false
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function fuzzyMatch({ search, text, }) {
|
|
14
|
+
const lowerText = text.toLowerCase();
|
|
15
|
+
return (search
|
|
16
|
+
.replaceAll(/\s/g, '')
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.split('')
|
|
19
|
+
.reduce((textIndex, character) => {
|
|
20
|
+
return textIndex < 0
|
|
21
|
+
? textIndex
|
|
22
|
+
: lowerText.indexOf(character, textIndex) + 1 || -1;
|
|
23
|
+
}, 0) >= 0);
|
|
24
|
+
}
|
package/dist/util/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './anchored-popover.js';
|
|
|
2
2
|
export * from './define-table.js';
|
|
3
3
|
export * from './define-vira-element.js';
|
|
4
4
|
export * from './dynamic-element.js';
|
|
5
|
+
export * from './fuzzy-match.js';
|
|
5
6
|
export * from './menu-helpers.js';
|
|
6
7
|
export * from './overflow-observer.js';
|
|
7
8
|
export * from './popover-manager.js';
|
package/dist/util/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from './anchored-popover.js';
|
|
|
2
2
|
export * from './define-table.js';
|
|
3
3
|
export * from './define-vira-element.js';
|
|
4
4
|
export * from './dynamic-element.js';
|
|
5
|
+
export * from './fuzzy-match.js';
|
|
5
6
|
export * from './menu-helpers.js';
|
|
6
7
|
export * from './overflow-observer.js';
|
|
7
8
|
export * from './popover-manager.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MaybePromise, type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type NavController } from 'device-navigation';
|
|
2
3
|
import { type HtmlInterpolation } from 'element-vir';
|
|
3
4
|
import { ViraMenuItem } from '../elements/popover/vira-menu-item.element.js';
|
|
4
5
|
/**
|
|
@@ -42,4 +43,6 @@ export type MenuItemClickCallback = (params: Readonly<{
|
|
|
42
43
|
*
|
|
43
44
|
* @category Popover
|
|
44
45
|
*/
|
|
45
|
-
export declare function renderMenuItemEntries(items: ReadonlyArray<Readonly<ViraMenuItemEntry
|
|
46
|
+
export declare function renderMenuItemEntries(items: ReadonlyArray<Readonly<ViraMenuItemEntry>>,
|
|
47
|
+
/** Enables arrow key navigation between the menu items. */
|
|
48
|
+
navController?: NavController | undefined): import("lit-html").HTMLTemplateResult[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { filterMap, joinWithFinalConjunction, } from '@augment-vir/common';
|
|
2
|
-
import {
|
|
2
|
+
import { nav } from 'device-navigation';
|
|
3
|
+
import { html, listen, nothing } from 'element-vir';
|
|
3
4
|
import { ViraMenuItem } from '../elements/popover/vira-menu-item.element.js';
|
|
4
5
|
/**
|
|
5
6
|
* Verifies that all items have unique ids.
|
|
@@ -26,12 +27,19 @@ export function assertUniqueIdProps(items) {
|
|
|
26
27
|
*
|
|
27
28
|
* @category Popover
|
|
28
29
|
*/
|
|
29
|
-
export function renderMenuItemEntries(items
|
|
30
|
+
export function renderMenuItemEntries(items,
|
|
31
|
+
/** Enables arrow key navigation between the menu items. */
|
|
32
|
+
navController) {
|
|
30
33
|
return filterMap(items, (item, index) => {
|
|
31
34
|
return html `
|
|
32
35
|
<${ViraMenuItem.assign({
|
|
33
36
|
...item,
|
|
34
37
|
})}
|
|
38
|
+
${navController
|
|
39
|
+
? nav(navController, {
|
|
40
|
+
disabled: item.disabled,
|
|
41
|
+
})
|
|
42
|
+
: nothing}
|
|
35
43
|
${listen('click', async (event) => {
|
|
36
44
|
if (item.disabled) {
|
|
37
45
|
event.stopImmediatePropagation();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type HtmlInterpolation } from 'element-vir';
|
|
3
|
+
import { type ViraIconSvg } from '../icons/icon-svg.js';
|
|
2
4
|
/**
|
|
3
5
|
* Options for `ViraDropdown`.
|
|
4
6
|
*
|
|
@@ -8,9 +10,17 @@ import { type PartialWithUndefined } from '@augment-vir/common';
|
|
|
8
10
|
export type ViraDropdownOption = {
|
|
9
11
|
/** A value or id, used to keep track of which option is selected. */
|
|
10
12
|
value: string;
|
|
13
|
+
/**
|
|
14
|
+
* Plain text for the option. Still used for search filtering, tooltips, and the search input's
|
|
15
|
+
* text when `labelTemplate` is set.
|
|
16
|
+
*/
|
|
11
17
|
label: string;
|
|
12
18
|
} & PartialWithUndefined<{
|
|
13
19
|
disabled: boolean;
|
|
20
|
+
/** Drawn before the label. Not drawn inside a searchable dropdown's text input. */
|
|
21
|
+
icon: ViraIconSvg;
|
|
22
|
+
/** Rendered instead of `label`. Not rendered inside a searchable dropdown's text input. */
|
|
23
|
+
labelTemplate: HtmlInterpolation;
|
|
14
24
|
}>;
|
|
15
25
|
/**
|
|
16
26
|
* A group of options for `ViraDropdown`, rendered under a group label.
|