@limetech/lime-elements 40.18.2 → 40.18.3
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/CHANGELOG.md +12 -0
- package/dist/cjs/limel-breadcrumbs_6.cjs.entry.js +68 -29
- package/dist/cjs/limel-helper-line_3.cjs.entry.js +9 -19
- package/dist/cjs/limel-picker.cjs.entry.js +9 -14
- package/dist/cjs/limel-select.cjs.entry.js +9 -168
- package/dist/cjs/typeahead-CnyIjog_.js +194 -0
- package/dist/collection/components/input-field/input-field.js +68 -29
- package/dist/collection/components/list/list.js +9 -19
- package/dist/collection/components/picker/picker.js +9 -14
- package/dist/collection/util/typeahead.js +26 -0
- package/dist/esm/limel-breadcrumbs_6.entry.js +69 -30
- package/dist/esm/limel-helper-line_3.entry.js +9 -19
- package/dist/esm/limel-picker.entry.js +9 -14
- package/dist/esm/limel-select.entry.js +1 -160
- package/dist/esm/typeahead-DOlnT3wh.js +188 -0
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-70272ea2.entry.js +1 -0
- package/dist/lime-elements/p-93a2d3b3.entry.js +159 -0
- package/dist/lime-elements/p-c1678d3a.entry.js +23 -0
- package/dist/lime-elements/p-ea11f365.entry.js +1 -0
- package/dist/lime-elements/p-fYs7n6yY.js +1 -0
- package/dist/types/components/input-field/input-field.d.ts +6 -6
- package/dist/types/util/typeahead.d.ts +16 -0
- package/package.json +1 -1
- package/dist/lime-elements/p-27d7c255.entry.js +0 -159
- package/dist/lime-elements/p-3c861ca8.entry.js +0 -23
- package/dist/lime-elements/p-6aae3f58.entry.js +0 -1
- package/dist/lime-elements/p-7af16e1d.entry.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [40.18.3](https://github.com/Lundalogik/lime-elements/compare/v40.18.2...v40.18.3) (2026-10-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
* **input-field:** let Tab leave the field and Esc close the list ([aa83c1d](https://github.com/Lundalogik/lime-elements/commit/aa83c1d93d3acb1c22456fdceff0e22247b94320)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
7
|
+
* **input-field:** type in the field when a key is pressed on an option ([83611ae](https://github.com/Lundalogik/lime-elements/commit/83611aefe9b92aa3d512688a74a33ba5db6dd597)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
8
|
+
* **list:** leave the typed characters to the browser ([d831b6c](https://github.com/Lundalogik/lime-elements/commit/d831b6c1d861cfc8e6844b7793fd36bfa841cf78)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
9
|
+
* **picker:** let Tab leave the field while the list is open ([3be6096](https://github.com/Lundalogik/lime-elements/commit/3be6096c9a8a879a8dfeaacc00703f4c00e27e62)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
10
|
+
* **picker:** move to the suggestions with the down arrow, not to the actions ([7dba41d](https://github.com/Lundalogik/lime-elements/commit/7dba41da0922c252d464450fae31cdb0197466c8)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
11
|
+
* **picker:** type in the field when a key is pressed on a suggestion ([670db45](https://github.com/Lundalogik/lime-elements/commit/670db4592a4e2482543700f0c19404e8f5b4dd34)), closes [#4468](https://github.com/Lundalogik/lime-elements/issues/4468)
|
|
12
|
+
|
|
1
13
|
## [40.18.2](https://github.com/Lundalogik/lime-elements/compare/v40.18.1...v40.18.2) (2026-10-30)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -6,6 +6,7 @@ var randomString = require('./random-string-CFCtFh5l.js');
|
|
|
6
6
|
var getIconProps = require('./get-icon-props-CwpDdQDI.js');
|
|
7
7
|
var keycodes = require('./keycodes-BQ_AERli.js');
|
|
8
8
|
var linkHelper = require('./link-helper-DwlfFJVL.js');
|
|
9
|
+
var typeahead = require('./typeahead-CnyIjog_.js');
|
|
9
10
|
var config = require('./config-dit--4m5.js');
|
|
10
11
|
var debounce = require('./debounce-DgDnRSlt.js');
|
|
11
12
|
var component = require('./component-Cz24PdGy.js');
|
|
@@ -109,6 +110,7 @@ const inputFieldCss = () => `@charset "UTF-8";:host([type=url]) .mdc-text-field-
|
|
|
109
110
|
|
|
110
111
|
const CHANGE_EVENT_DEBOUNCE_TIMEOUT = 300;
|
|
111
112
|
const RESIZE_HANDLER_DEBOUNCE_TIMEOUT = 100;
|
|
113
|
+
const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
|
|
112
114
|
const InputField = class {
|
|
113
115
|
constructor(hostRef) {
|
|
114
116
|
index.registerInstance(this, hostRef);
|
|
@@ -453,41 +455,64 @@ const InputField = class {
|
|
|
453
455
|
}
|
|
454
456
|
return (index.h("span", { class: "lime-formatted-input lime-looks-like-input-value", "aria-hidden": this.readonly ? 'true' : undefined }, renderValue));
|
|
455
457
|
};
|
|
456
|
-
/**
|
|
457
|
-
* Key handler for the input field
|
|
458
|
-
* Will change focus to the first/last item in the dropdown list to enable selection with the keyboard
|
|
459
|
-
*
|
|
460
|
-
* @param event - event
|
|
461
|
-
*/
|
|
462
458
|
this.onKeyDown = (event) => {
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
if (event.key === keycodes.ESCAPE) {
|
|
460
|
+
this.closeListWithEscape(event);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const isTab = event.key === keycodes.TAB &&
|
|
465
464
|
!event.altKey &&
|
|
466
465
|
!event.metaKey &&
|
|
467
|
-
!event.
|
|
468
|
-
|
|
469
|
-
const isDown = event.key === keycodes.ARROW_DOWN;
|
|
470
|
-
if (event.key === keycodes.TAB && event.shiftKey) {
|
|
466
|
+
!event.ctrlKey;
|
|
467
|
+
if (isTab) {
|
|
471
468
|
this.showCompletions = false;
|
|
472
|
-
}
|
|
473
|
-
if (!isForwardTab && !isUp && !isDown) {
|
|
474
469
|
return;
|
|
475
470
|
}
|
|
476
|
-
const
|
|
477
|
-
|
|
471
|
+
const wasOpen = this.isSuggestionListOpen();
|
|
472
|
+
this.showCompletions = true;
|
|
473
|
+
if (event.key === keycodes.ARROW_DOWN || event.key === keycodes.ARROW_UP) {
|
|
474
|
+
this.moveIntoList(event, wasOpen);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
this.moveIntoList = (event, wasOpen) => {
|
|
478
|
+
var _a;
|
|
479
|
+
if (!this.isSuggestionListOpen()) {
|
|
478
480
|
return;
|
|
479
481
|
}
|
|
480
482
|
event.preventDefault();
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
const position = event.key === keycodes.ARROW_DOWN ? 'first' : 'last';
|
|
484
|
+
if (wasOpen) {
|
|
485
|
+
(_a = this.getListOption(position)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
484
486
|
return;
|
|
485
487
|
}
|
|
486
|
-
|
|
488
|
+
this.focusOptionWhenShown(position);
|
|
489
|
+
};
|
|
490
|
+
this.getListOption = (position) => {
|
|
491
|
+
const list = document.querySelector(` #${this.portalId} limel-list`);
|
|
492
|
+
return list === null || list === void 0 ? void 0 : list.shadowRoot.querySelector(`.mdc-deprecated-list-item:${position}-child`);
|
|
493
|
+
};
|
|
494
|
+
this.focusOptionWhenShown = (position) => {
|
|
495
|
+
let frames = 0;
|
|
496
|
+
const focusOption = () => {
|
|
497
|
+
const option = this.getListOption(position);
|
|
498
|
+
if ((option === null || option === void 0 ? void 0 : option.getClientRects().length) > 0) {
|
|
499
|
+
option.focus();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
frames += 1;
|
|
503
|
+
if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
|
|
504
|
+
requestAnimationFrame(focusOption);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
requestAnimationFrame(focusOption);
|
|
508
|
+
};
|
|
509
|
+
this.closeListWithEscape = (event) => {
|
|
510
|
+
if (!this.isSuggestionListOpen()) {
|
|
487
511
|
return;
|
|
488
512
|
}
|
|
489
|
-
|
|
490
|
-
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
event.stopPropagation();
|
|
515
|
+
this.showCompletions = false;
|
|
491
516
|
};
|
|
492
517
|
this.handleCompletionChange = (event) => {
|
|
493
518
|
event.stopPropagation();
|
|
@@ -504,10 +529,18 @@ const InputField = class {
|
|
|
504
529
|
this.changeEmitter(event.detail.text);
|
|
505
530
|
this.changeEmitter.flush();
|
|
506
531
|
};
|
|
532
|
+
this.hasSuggestions = () => {
|
|
533
|
+
return (!this.readonly &&
|
|
534
|
+
this.type !== 'textarea' &&
|
|
535
|
+
this.completions.length > 0);
|
|
536
|
+
};
|
|
537
|
+
this.isSuggestionListOpen = () => {
|
|
538
|
+
return (this.hasSuggestions() &&
|
|
539
|
+
this.showCompletions &&
|
|
540
|
+
this.filterCompletions(this.getCurrentValue()).length > 0);
|
|
541
|
+
};
|
|
507
542
|
this.renderAutocompleteList = () => {
|
|
508
|
-
if (this.
|
|
509
|
-
this.type === 'textarea' ||
|
|
510
|
-
this.completions.length === 0) {
|
|
543
|
+
if (!this.hasSuggestions()) {
|
|
511
544
|
return;
|
|
512
545
|
}
|
|
513
546
|
const dropdownZIndex = getComputedStyle(this.limelInputField).getPropertyValue('--dropdown-z-index');
|
|
@@ -525,8 +558,14 @@ const InputField = class {
|
|
|
525
558
|
return (index.h("limel-list", { onChange: this.handleCompletionChange, onKeyDown: this.handleKeyDownInDropdown, type: "selectable", items: filteredCompletions }));
|
|
526
559
|
};
|
|
527
560
|
this.handleKeyDownInDropdown = (event) => {
|
|
528
|
-
|
|
529
|
-
|
|
561
|
+
if (event.key === keycodes.ESCAPE) {
|
|
562
|
+
event.preventDefault();
|
|
563
|
+
event.stopPropagation();
|
|
564
|
+
this.setFocus();
|
|
565
|
+
this.showCompletions = false;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if ([keycodes.TAB, keycodes.ENTER].includes(event.key) || typeahead.isTextEditingKey(event)) {
|
|
530
569
|
this.setFocus();
|
|
531
570
|
}
|
|
532
571
|
};
|
|
@@ -684,10 +723,10 @@ const InputField = class {
|
|
|
684
723
|
if (this.isInvalid()) {
|
|
685
724
|
properties['aria-invalid'] = 'true';
|
|
686
725
|
}
|
|
687
|
-
if (this.
|
|
726
|
+
if (this.hasSuggestions()) {
|
|
688
727
|
properties['aria-controls'] = this.portalId;
|
|
689
728
|
}
|
|
690
|
-
return (index.h(index.Host, { key: '
|
|
729
|
+
return (index.h(index.Host, { key: 'e09ee9e3102d6a15d0806602c65361001cd20c63' }, index.h("limel-notched-outline", { key: 'c2301d9fd1af41e8b4396dc5f2978e51b260fe2f', labelId: this.inputId, labelElementId: this.labelElementId, label: this.label, required: this.required, invalid: this.invalid || this.isInvalid(), disabled: this.disabled, readonly: this.readonly, hasValue: !!this.value, hasLeadingIcon: !!this.leadingIcon }, index.h("label", { key: '69e02e5df31a6ce10743c3c8a9e78c5765794a18', slot: "content", class: this.getContainerClassList() }, this.renderLeadingIcon(), this.renderPrefix(), this.renderFormattedNumber(), this.renderInput(properties), this.renderSuffix(), this.renderTextarea(properties), this.renderTrailingLinkOrButton())), this.renderHelperLine(), this.renderAutocompleteList()));
|
|
691
730
|
}
|
|
692
731
|
valueWatcher(newValue) {
|
|
693
732
|
if (!this.mdcTextField || this.changeWaiting) {
|
|
@@ -190,26 +190,16 @@ const List = class {
|
|
|
190
190
|
this.listElement.removeEventListener('mousedown', this.handleItemMouseDown);
|
|
191
191
|
this.listElement.addEventListener('mousedown', this.handleItemMouseDown);
|
|
192
192
|
this.mdcList = new component.MDCList(element);
|
|
193
|
-
//
|
|
194
|
-
//
|
|
193
|
+
// MDC's typeahead (`hasTypeahead`) is left off, so that typed
|
|
194
|
+
// characters are not taken from a field with a list under it. The list
|
|
195
|
+
// test in `list.e2e.tsx` fails if it is turned on. `limel-select` has
|
|
196
|
+
// its own typeahead, see `src/util/typeahead.ts`.
|
|
195
197
|
//
|
|
196
|
-
// MDC
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// Do not "fix" this by adding MDC's class name to `limel-list-item`.
|
|
202
|
-
// That revives MDC's typeahead along with three behaviors that cannot
|
|
203
|
-
// be configured from the outside: a hard coded 300ms buffer, `Enter`
|
|
204
|
-
// being ignored for as long as that buffer lives (its `notifyAction`
|
|
205
|
-
// is guarded by `isTypeaheadInProgress`), and the space bar being
|
|
206
|
-
// excluded from matches. It would also collide with `limel-menu`,
|
|
207
|
-
// where single characters are used as item hotkeys.
|
|
208
|
-
//
|
|
209
|
-
// `limel-select` implements its own typeahead instead, see
|
|
210
|
-
// `src/util/typeahead.ts`. It intercepts typed characters in the
|
|
211
|
-
// capture phase on this very element, so they never reach MDC.
|
|
212
|
-
this.mdcList.hasTypeahead = true;
|
|
198
|
+
// Adding MDC's class name to `limel-list-item` does nothing while it is
|
|
199
|
+
// off. Turning it on as well would bring its 300ms buffer, `Enter`
|
|
200
|
+
// being ignored while the buffer lives and the space bar being left out
|
|
201
|
+
// of matches, none of which can be configured, and it would collide
|
|
202
|
+
// with the single characters that `limel-menu` uses as hotkeys.
|
|
213
203
|
};
|
|
214
204
|
/**
|
|
215
205
|
* Focus the pressed list item directly, without scrolling.
|
|
@@ -5,6 +5,7 @@ var translations = require('./translations-CLSqfR3x.js');
|
|
|
5
5
|
var dom = require('./dom-BWMDM6En.js');
|
|
6
6
|
var keycodes = require('./keycodes-BQ_AERli.js');
|
|
7
7
|
var randomString = require('./random-string-CFCtFh5l.js');
|
|
8
|
+
var typeahead = require('./typeahead-CnyIjog_.js');
|
|
8
9
|
var getIconProps = require('./get-icon-props-CwpDdQDI.js');
|
|
9
10
|
var debounce = require('./debounce-DgDnRSlt.js');
|
|
10
11
|
require('./language-v-MMS6ym.js');
|
|
@@ -197,7 +198,7 @@ const Picker = class {
|
|
|
197
198
|
props.maxItems = 1;
|
|
198
199
|
}
|
|
199
200
|
return [
|
|
200
|
-
index.h("limel-chip-set", Object.assign({ key: '
|
|
201
|
+
index.h("limel-chip-set", Object.assign({ key: 'c3f4fd001d63a287a8a9f447f7c9df65279fbc34', type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, language: this.language, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, emptyInputOnChange: false, clearAllButton: this.multiple && !this.chipSetEditMode }, props)),
|
|
201
202
|
this.renderDropdown(),
|
|
202
203
|
];
|
|
203
204
|
}
|
|
@@ -245,7 +246,7 @@ const Picker = class {
|
|
|
245
246
|
'is-on-top': this.actionPosition === 'top',
|
|
246
247
|
'is-at-bottom': this.actionPosition === 'bottom',
|
|
247
248
|
'has-position-sticky': this.actionScrollBehavior === 'sticky',
|
|
248
|
-
}, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
|
|
249
|
+
}, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, onKeyDown: this.onListKeyDown, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
|
|
249
250
|
];
|
|
250
251
|
}
|
|
251
252
|
removeUnusedPropertiesOnAction(action) {
|
|
@@ -309,7 +310,7 @@ const Picker = class {
|
|
|
309
310
|
return index.h("p", { style: style }, text);
|
|
310
311
|
}
|
|
311
312
|
renderListResult() {
|
|
312
|
-
return (index.h("limel-list", { badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
|
|
313
|
+
return (index.h("limel-list", { "data-suggestions": "", badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
|
|
313
314
|
}
|
|
314
315
|
onListKeyDown(event) {
|
|
315
316
|
if (event.key === keycodes.ESCAPE) {
|
|
@@ -321,7 +322,7 @@ const Picker = class {
|
|
|
321
322
|
this.chipSet.setFocus();
|
|
322
323
|
return;
|
|
323
324
|
}
|
|
324
|
-
if ([keycodes.TAB, keycodes.ENTER].includes(event.key)) {
|
|
325
|
+
if ([keycodes.TAB, keycodes.ENTER].includes(event.key) || typeahead.isTextEditingKey(event)) {
|
|
325
326
|
this.chipSet.setFocus();
|
|
326
327
|
}
|
|
327
328
|
}
|
|
@@ -450,10 +451,7 @@ const Picker = class {
|
|
|
450
451
|
* @param event - event
|
|
451
452
|
*/
|
|
452
453
|
handleInputKeyDown(event) {
|
|
453
|
-
|
|
454
|
-
!event.altKey &&
|
|
455
|
-
!event.metaKey &&
|
|
456
|
-
!event.shiftKey;
|
|
454
|
+
var _a;
|
|
457
455
|
const isUp = event.key === keycodes.ARROW_UP;
|
|
458
456
|
const isDown = event.key === keycodes.ARROW_DOWN;
|
|
459
457
|
const isEscape = event.key === keycodes.ESCAPE;
|
|
@@ -463,14 +461,14 @@ const Picker = class {
|
|
|
463
461
|
this.handleEscape();
|
|
464
462
|
return;
|
|
465
463
|
}
|
|
466
|
-
if (!
|
|
464
|
+
if (!isUp && !isDown) {
|
|
467
465
|
return;
|
|
468
466
|
}
|
|
469
|
-
const list = document.querySelector(` #${this.portalId} limel-list`);
|
|
467
|
+
const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
|
|
470
468
|
if (!list) {
|
|
471
469
|
return;
|
|
472
470
|
}
|
|
473
|
-
if (
|
|
471
|
+
if (isDown) {
|
|
474
472
|
// Match by class only (not `:first-child`) so a leading
|
|
475
473
|
// `ListSeparator` doesn't shadow the first focusable item.
|
|
476
474
|
const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
|
|
@@ -481,9 +479,6 @@ const Picker = class {
|
|
|
481
479
|
listElement.focus();
|
|
482
480
|
return;
|
|
483
481
|
}
|
|
484
|
-
if (!isUp) {
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
482
|
const listItems = list.shadowRoot.querySelectorAll('.mdc-deprecated-list-item');
|
|
488
483
|
const listElement = [...listItems].at(-1);
|
|
489
484
|
if (!listElement) {
|
|
@@ -5,6 +5,7 @@ var device = require('./device-C9O7lYI9.js');
|
|
|
5
5
|
var keycodes = require('./keycodes-BQ_AERli.js');
|
|
6
6
|
var multiple = require('./multiple-DYfiga7l.js');
|
|
7
7
|
var randomString = require('./random-string-CFCtFh5l.js');
|
|
8
|
+
var typeahead = require('./typeahead-CnyIjog_.js');
|
|
8
9
|
var renderListComponent = require('./render-list-component-BtAliHQp.js');
|
|
9
10
|
var getIconProps = require('./get-icon-props-CwpDdQDI.js');
|
|
10
11
|
var component = require('./component-DNiVfYHV.js');
|
|
@@ -256,166 +257,6 @@ var MDCSelectHelperText = /** @class */ (function (_super) {
|
|
|
256
257
|
return MDCSelectHelperText;
|
|
257
258
|
}(ponyfill.MDCComponent));
|
|
258
259
|
|
|
259
|
-
/**
|
|
260
|
-
* These helpers implement "type to jump", the behavior a native `<select>` has:
|
|
261
|
-
* typing characters moves to the option whose text starts with those
|
|
262
|
-
* characters. Characters accumulate into a short-lived buffer, so typing
|
|
263
|
-
* `n`, `e` finds "Netherlands" instead of re-matching on `n` every time.
|
|
264
|
-
*
|
|
265
|
-
* The matching itself is pure and stateless — all the state lives in a
|
|
266
|
-
* `TypeaheadBuffer`, which owns the timer that discards the buffer once the
|
|
267
|
-
* user stops typing.
|
|
268
|
-
*/
|
|
269
|
-
/**
|
|
270
|
-
* How long a buffer of typed characters is kept before it is discarded, in
|
|
271
|
-
* milliseconds. Roughly matches a native `<select>`.
|
|
272
|
-
*/
|
|
273
|
-
const TYPEAHEAD_BUFFER_TIMEOUT = 1000;
|
|
274
|
-
/**
|
|
275
|
-
* Returned by `findTypeaheadMatch` when nothing matched.
|
|
276
|
-
*/
|
|
277
|
-
const NO_TYPEAHEAD_MATCH = -1;
|
|
278
|
-
/**
|
|
279
|
-
* Whether a keyboard event should be treated as a typed character.
|
|
280
|
-
*
|
|
281
|
-
* The space bar counts as a character here. Whether a *bare* space should
|
|
282
|
-
* start a buffer, or keep whatever meaning it has in the consuming component,
|
|
283
|
-
* is the consumer's decision.
|
|
284
|
-
*
|
|
285
|
-
* `shiftKey` is allowed, since capital letters are typed with it, and
|
|
286
|
-
* `event.repeat` is allowed, since holding a key down cycles through matches
|
|
287
|
-
* in a native `<select>` too.
|
|
288
|
-
*
|
|
289
|
-
* @param event - the keyboard event to inspect
|
|
290
|
-
* @returns `true` when the event represents a typed character
|
|
291
|
-
*/
|
|
292
|
-
function isTypeaheadKey(event) {
|
|
293
|
-
return (event.key.length === 1 &&
|
|
294
|
-
!event.altKey &&
|
|
295
|
-
!event.ctrlKey &&
|
|
296
|
-
!event.metaKey &&
|
|
297
|
-
!event.isComposing);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Find the candidate to move to for a given buffer of typed characters.
|
|
301
|
-
*
|
|
302
|
-
* Two different searches are used, mirroring a native `<select>`:
|
|
303
|
-
* - When the buffer is a single character, possibly repeated (`b`, `bb`,
|
|
304
|
-
* `bbb`), only that character is matched and the search starts *after*
|
|
305
|
-
* `currentIndex`. Pressing the same key repeatedly therefore cycles through
|
|
306
|
-
* all candidates starting with it.
|
|
307
|
-
* - Otherwise the whole buffer is matched and the search starts *at*
|
|
308
|
-
* `currentIndex`, so continuing to type on an already-matching candidate
|
|
309
|
-
* keeps it rather than jumping to the next one.
|
|
310
|
-
*
|
|
311
|
-
* Both searches wrap around the end of the list.
|
|
312
|
-
*
|
|
313
|
-
* @param candidates - the candidates to search, index-aligned with the rows
|
|
314
|
-
* they are rendered as. Use `null` for rows that can never match, such as
|
|
315
|
-
* separators, so that they still occupy their index.
|
|
316
|
-
* @param buffer - the characters typed so far
|
|
317
|
-
* @param currentIndex - the index to search from. Anything outside
|
|
318
|
-
* `candidates` is treated as "no current candidate", and the search starts
|
|
319
|
-
* from the beginning.
|
|
320
|
-
* @returns the index of the matching candidate, or `NO_TYPEAHEAD_MATCH`
|
|
321
|
-
*/
|
|
322
|
-
function findTypeaheadMatch(candidates, buffer, currentIndex) {
|
|
323
|
-
const count = candidates.length;
|
|
324
|
-
const query = buffer.toLowerCase();
|
|
325
|
-
if (!query || count === 0) {
|
|
326
|
-
return NO_TYPEAHEAD_MATCH;
|
|
327
|
-
}
|
|
328
|
-
const characters = [...query];
|
|
329
|
-
const cycles = isSingleDistinctCharacter(characters);
|
|
330
|
-
const needle = cycles ? characters[0] : query;
|
|
331
|
-
// Cycling starts one past the current candidate, so that pressing the same
|
|
332
|
-
// key again advances. Matching several characters starts at it, so that
|
|
333
|
-
// continuing to type keeps a candidate that already matches.
|
|
334
|
-
const offset = cycles ? 1 : 0;
|
|
335
|
-
const hasCurrent = currentIndex >= 0 && currentIndex < count;
|
|
336
|
-
const start = hasCurrent ? (currentIndex + offset) % count : 0;
|
|
337
|
-
for (let step = 0; step < count; step += 1) {
|
|
338
|
-
const index = (start + step) % count;
|
|
339
|
-
if (candidateMatches(candidates[index], needle)) {
|
|
340
|
-
return index;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return NO_TYPEAHEAD_MATCH;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Whether a string consists of a single character, possibly repeated. Also
|
|
347
|
-
* `true` for one character on its own, which is what makes a single key press
|
|
348
|
-
* advance past the current candidate rather than re-matching it.
|
|
349
|
-
*
|
|
350
|
-
* Takes the characters already split apart, so that the caller and this
|
|
351
|
-
* function agree on what "a character" is for anything outside the basic
|
|
352
|
-
* multilingual plane.
|
|
353
|
-
*
|
|
354
|
-
* @param characters - the characters of the string
|
|
355
|
-
* @returns `true` when every character is the same
|
|
356
|
-
*/
|
|
357
|
-
function isSingleDistinctCharacter(characters) {
|
|
358
|
-
return characters.every((character) => character === characters[0]);
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* @param candidate - the candidate to test, or `null` for a row that can
|
|
362
|
-
* never match
|
|
363
|
-
* @param needle - the lower cased characters to match
|
|
364
|
-
* @returns `true` when the candidate starts with the given characters
|
|
365
|
-
*/
|
|
366
|
-
function candidateMatches(candidate, needle) {
|
|
367
|
-
var _a;
|
|
368
|
-
if (!candidate || candidate.disabled) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
const text = (_a = candidate.text) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
372
|
-
return !!text && text.startsWith(needle);
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Accumulates typed characters, and discards them again once the user stops
|
|
376
|
-
* typing for `TYPEAHEAD_BUFFER_TIMEOUT`.
|
|
377
|
-
*/
|
|
378
|
-
class TypeaheadBuffer {
|
|
379
|
-
constructor(timeout = TYPEAHEAD_BUFFER_TIMEOUT) {
|
|
380
|
-
this.buffer = '';
|
|
381
|
-
this.timeout = timeout;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* The characters typed so far.
|
|
385
|
-
*/
|
|
386
|
-
get value() {
|
|
387
|
-
return this.buffer;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Whether nothing has been typed, or the buffer has been discarded.
|
|
391
|
-
*/
|
|
392
|
-
get isEmpty() {
|
|
393
|
-
return this.buffer === '';
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Append a character, and restart the timer that discards the buffer.
|
|
397
|
-
*
|
|
398
|
-
* @param character - the character to append
|
|
399
|
-
* @returns the buffer, including the appended character
|
|
400
|
-
*/
|
|
401
|
-
append(character) {
|
|
402
|
-
this.buffer += character;
|
|
403
|
-
clearTimeout(this.resetTimeoutId);
|
|
404
|
-
this.resetTimeoutId = setTimeout(() => {
|
|
405
|
-
this.clear();
|
|
406
|
-
}, this.timeout);
|
|
407
|
-
return this.buffer;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Discard the buffer, and cancel the timer that would have discarded it.
|
|
411
|
-
*/
|
|
412
|
-
clear() {
|
|
413
|
-
this.buffer = '';
|
|
414
|
-
clearTimeout(this.resetTimeoutId);
|
|
415
|
-
this.resetTimeoutId = undefined;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
260
|
const TRIGGER_ID = 's-trigger';
|
|
420
261
|
const LABEL_ID = 's-label';
|
|
421
262
|
const SELECTED_TEXT_ID = 's-selected-text';
|
|
@@ -715,7 +556,7 @@ const Select = class {
|
|
|
715
556
|
this.hasChanged = false;
|
|
716
557
|
this.hasRichOptions = false;
|
|
717
558
|
this.checkValid = false;
|
|
718
|
-
this.typeaheadBuffer = new TypeaheadBuffer();
|
|
559
|
+
this.typeaheadBuffer = new typeahead.TypeaheadBuffer();
|
|
719
560
|
this.handleMenuTriggerKeyDown = (event) => {
|
|
720
561
|
if (this.readonly) {
|
|
721
562
|
return;
|
|
@@ -723,7 +564,7 @@ const Select = class {
|
|
|
723
564
|
// Typeahead runs first, so that a space extends an ongoing typeahead
|
|
724
565
|
// instead of just opening the dropdown. A *bare* space is declined by
|
|
725
566
|
// `handleTypeaheadKey`, and keeps its usual meaning below.
|
|
726
|
-
if (this.handleTypeaheadKey(event, NO_TYPEAHEAD_MATCH)) {
|
|
567
|
+
if (this.handleTypeaheadKey(event, typeahead.NO_TYPEAHEAD_MATCH)) {
|
|
727
568
|
return;
|
|
728
569
|
}
|
|
729
570
|
const isEnter = event.key === keycodes.ENTER;
|
|
@@ -868,7 +709,7 @@ const Select = class {
|
|
|
868
709
|
// without closing it, which brings us back here with a row
|
|
869
710
|
// already focused. Moving to the first option then would lose
|
|
870
711
|
// the user's place in the list after every pick.
|
|
871
|
-
if (this.getFocusedMenuItemIndex() !== NO_TYPEAHEAD_MATCH) {
|
|
712
|
+
if (this.getFocusedMenuItemIndex() !== typeahead.NO_TYPEAHEAD_MATCH) {
|
|
872
713
|
return;
|
|
873
714
|
}
|
|
874
715
|
this.focusFirstMenuItem(list);
|
|
@@ -1019,7 +860,7 @@ const Select = class {
|
|
|
1019
860
|
// The native dropdown does its own typeahead. Note that
|
|
1020
861
|
// `setMenuFocus` bails out there too, so a pending index would never
|
|
1021
862
|
// be consumed anyway.
|
|
1022
|
-
if (this.shouldRenderNative() || !isTypeaheadKey(event)) {
|
|
863
|
+
if (this.shouldRenderNative() || !typeahead.isTypeaheadKey(event)) {
|
|
1023
864
|
return false;
|
|
1024
865
|
}
|
|
1025
866
|
// A space only continues an ongoing typeahead. On its own it keeps its
|
|
@@ -1036,8 +877,8 @@ const Select = class {
|
|
|
1036
877
|
const candidates = items.map((item) => ('separator' in item ? null : item));
|
|
1037
878
|
const buffer = this.typeaheadBuffer.append(event.key);
|
|
1038
879
|
const currentIndex = this.resolveTypeaheadIndex(items, focusedIndex);
|
|
1039
|
-
const index = findTypeaheadMatch(candidates, buffer, currentIndex);
|
|
1040
|
-
if (index !== NO_TYPEAHEAD_MATCH) {
|
|
880
|
+
const index = typeahead.findTypeaheadMatch(candidates, buffer, currentIndex);
|
|
881
|
+
if (index !== typeahead.NO_TYPEAHEAD_MATCH) {
|
|
1041
882
|
this.focusTypeaheadMatch(index);
|
|
1042
883
|
}
|
|
1043
884
|
return true;
|
|
@@ -1048,7 +889,7 @@ const Select = class {
|
|
|
1048
889
|
* @returns the index a typeahead search should start from
|
|
1049
890
|
*/
|
|
1050
891
|
resolveTypeaheadIndex(items, focusedIndex) {
|
|
1051
|
-
if (focusedIndex !== NO_TYPEAHEAD_MATCH) {
|
|
892
|
+
if (focusedIndex !== typeahead.NO_TYPEAHEAD_MATCH) {
|
|
1052
893
|
return focusedIndex;
|
|
1053
894
|
}
|
|
1054
895
|
// Typed again before focus had time to land in the dropdown.
|
|
@@ -1102,7 +943,7 @@ const Select = class {
|
|
|
1102
943
|
const focused = (_b = (_a = this.list) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.activeElement;
|
|
1103
944
|
const row = focused === null || focused === void 0 ? void 0 : focused.closest('[data-index]');
|
|
1104
945
|
const index = Number.parseInt((_c = row === null || row === void 0 ? void 0 : row.dataset.index) !== null && _c !== void 0 ? _c : '', 10);
|
|
1105
|
-
return Number.isNaN(index) ? NO_TYPEAHEAD_MATCH : index;
|
|
946
|
+
return Number.isNaN(index) ? typeahead.NO_TYPEAHEAD_MATCH : index;
|
|
1106
947
|
}
|
|
1107
948
|
resetTypeahead() {
|
|
1108
949
|
this.typeaheadBuffer.clear();
|