@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 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
- this.showCompletions = true;
464
- const isForwardTab = event.key === keycodes.TAB &&
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.shiftKey;
468
- const isUp = event.key === keycodes.ARROW_UP;
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 list = document.querySelector(` #${this.portalId} limel-list`);
477
- if (!list) {
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
- if (isForwardTab || isDown) {
482
- const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:first-child');
483
- listElement.focus();
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
- if (!isUp) {
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
- const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:last-child');
490
- listElement.focus();
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.readonly ||
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
- const keyFound = [keycodes.TAB, keycodes.ESCAPE, keycodes.ENTER].includes(event.key);
529
- if (keyFound) {
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.renderAutocompleteList()) {
726
+ if (this.hasSuggestions()) {
688
727
  properties['aria-controls'] = this.portalId;
689
728
  }
690
- return (index.h(index.Host, { key: '3abb0e074f55f8e47f46cf77ed885797047e44b9' }, index.h("limel-notched-outline", { key: '0d25df2774e7cda39da90163b0832a1ea117d7c8', 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: 'a60a2bfcdafe232b32a0d67f972c3885fc5b6428', 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()));
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
- // NOTE: This has no effect. It is kept as a marker rather than
194
- // removed, so that its absence isn't mistaken for an oversight.
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 indexes each row for typeahead by looking for
197
- // `.mdc-deprecated-list-item__primary-text` inside it, while
198
- // `limel-list-item` renders its label as `<span class="label">`. MDC
199
- // therefore builds an empty index, and typing does nothing.
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: 'eed0789b538bd50638936e1c6ad7e9deaef954ec', 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
+ 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
- const isForwardTab = event.key === keycodes.TAB &&
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 (!isForwardTab && !isUp && !isDown) {
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 (isForwardTab || isDown) {
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();