@limetech/lime-elements 40.18.1 → 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.
Files changed (33) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/limel-breadcrumbs_6.cjs.entry.js +68 -29
  3. package/dist/cjs/limel-callout.cjs.entry.js +1 -1
  4. package/dist/cjs/limel-helper-line_3.cjs.entry.js +9 -19
  5. package/dist/cjs/limel-picker.cjs.entry.js +9 -14
  6. package/dist/cjs/limel-select.cjs.entry.js +9 -168
  7. package/dist/cjs/typeahead-CnyIjog_.js +194 -0
  8. package/dist/collection/components/callout/callout.css +1 -0
  9. package/dist/collection/components/input-field/input-field.js +68 -29
  10. package/dist/collection/components/list/list.js +9 -19
  11. package/dist/collection/components/picker/picker.js +9 -14
  12. package/dist/collection/util/typeahead.js +26 -0
  13. package/dist/esm/limel-breadcrumbs_6.entry.js +69 -30
  14. package/dist/esm/limel-callout.entry.js +1 -1
  15. package/dist/esm/limel-helper-line_3.entry.js +9 -19
  16. package/dist/esm/limel-picker.entry.js +9 -14
  17. package/dist/esm/limel-select.entry.js +1 -160
  18. package/dist/esm/typeahead-DOlnT3wh.js +188 -0
  19. package/dist/lime-elements/lime-elements.esm.js +1 -1
  20. package/dist/lime-elements/p-70272ea2.entry.js +1 -0
  21. package/dist/lime-elements/p-93a2d3b3.entry.js +159 -0
  22. package/dist/lime-elements/p-a2dc6645.entry.js +1 -0
  23. package/dist/lime-elements/p-c1678d3a.entry.js +23 -0
  24. package/dist/lime-elements/p-ea11f365.entry.js +1 -0
  25. package/dist/lime-elements/p-fYs7n6yY.js +1 -0
  26. package/dist/types/components/input-field/input-field.d.ts +6 -6
  27. package/dist/types/util/typeahead.d.ts +16 -0
  28. package/package.json +1 -1
  29. package/dist/lime-elements/p-27d7c255.entry.js +0 -159
  30. package/dist/lime-elements/p-3c861ca8.entry.js +0 -23
  31. package/dist/lime-elements/p-5f82ea0b.entry.js +0 -1
  32. package/dist/lime-elements/p-6aae3f58.entry.js +0 -1
  33. package/dist/lime-elements/p-7af16e1d.entry.js +0 -1
@@ -2,8 +2,9 @@ import { r as registerInstance, c as createEvent, h, a as getElement, H as Host
2
2
  import { m as makeEnterClickable, r as removeEnterClickable } from './make-enter-clickable-CIf8aGSd.js';
3
3
  import { c as createRandomString } from './random-string-BrjD-ITe.js';
4
4
  import { g as getIconName, b as getIconColor } from './get-icon-props-CgNJbSP4.js';
5
- import { T as TAB, A as ARROW_UP, b as ARROW_DOWN, E as ESCAPE, a as ENTER, f as SPACE, c as ARROW_LEFT, d as ARROW_RIGHT } from './keycodes-Bu6-QXrA.js';
5
+ import { E as ESCAPE, b as ARROW_DOWN, A as ARROW_UP, T as TAB, a as ENTER, f as SPACE, c as ARROW_LEFT, d as ARROW_RIGHT } from './keycodes-Bu6-QXrA.js';
6
6
  import { b as getHref, a as getTarget, g as getRel } from './link-helper-5KsJICvh.js';
7
+ import { i as isTextEditingKey } from './typeahead-DOlnT3wh.js';
7
8
  import { g as globalConfig } from './config-Dnt5w_Bp.js';
8
9
  import { d as debounce } from './debounce-B67JMchz.js';
9
10
  import { M as MDCTextField } from './component-CVihTrav.js';
@@ -107,6 +108,7 @@ const inputFieldCss = () => `@charset "UTF-8";:host([type=url]) .mdc-text-field-
107
108
 
108
109
  const CHANGE_EVENT_DEBOUNCE_TIMEOUT = 300;
109
110
  const RESIZE_HANDLER_DEBOUNCE_TIMEOUT = 100;
111
+ const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
110
112
  const InputField = class {
111
113
  constructor(hostRef) {
112
114
  registerInstance(this, hostRef);
@@ -451,41 +453,64 @@ const InputField = class {
451
453
  }
452
454
  return (h("span", { class: "lime-formatted-input lime-looks-like-input-value", "aria-hidden": this.readonly ? 'true' : undefined }, renderValue));
453
455
  };
454
- /**
455
- * Key handler for the input field
456
- * Will change focus to the first/last item in the dropdown list to enable selection with the keyboard
457
- *
458
- * @param event - event
459
- */
460
456
  this.onKeyDown = (event) => {
461
- this.showCompletions = true;
462
- const isForwardTab = event.key === TAB &&
457
+ if (event.key === ESCAPE) {
458
+ this.closeListWithEscape(event);
459
+ return;
460
+ }
461
+ const isTab = event.key === TAB &&
463
462
  !event.altKey &&
464
463
  !event.metaKey &&
465
- !event.shiftKey;
466
- const isUp = event.key === ARROW_UP;
467
- const isDown = event.key === ARROW_DOWN;
468
- if (event.key === TAB && event.shiftKey) {
464
+ !event.ctrlKey;
465
+ if (isTab) {
469
466
  this.showCompletions = false;
470
- }
471
- if (!isForwardTab && !isUp && !isDown) {
472
467
  return;
473
468
  }
474
- const list = document.querySelector(` #${this.portalId} limel-list`);
475
- if (!list) {
469
+ const wasOpen = this.isSuggestionListOpen();
470
+ this.showCompletions = true;
471
+ if (event.key === ARROW_DOWN || event.key === ARROW_UP) {
472
+ this.moveIntoList(event, wasOpen);
473
+ }
474
+ };
475
+ this.moveIntoList = (event, wasOpen) => {
476
+ var _a;
477
+ if (!this.isSuggestionListOpen()) {
476
478
  return;
477
479
  }
478
480
  event.preventDefault();
479
- if (isForwardTab || isDown) {
480
- const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:first-child');
481
- listElement.focus();
481
+ const position = event.key === ARROW_DOWN ? 'first' : 'last';
482
+ if (wasOpen) {
483
+ (_a = this.getListOption(position)) === null || _a === void 0 ? void 0 : _a.focus();
482
484
  return;
483
485
  }
484
- if (!isUp) {
486
+ this.focusOptionWhenShown(position);
487
+ };
488
+ this.getListOption = (position) => {
489
+ const list = document.querySelector(` #${this.portalId} limel-list`);
490
+ return list === null || list === void 0 ? void 0 : list.shadowRoot.querySelector(`.mdc-deprecated-list-item:${position}-child`);
491
+ };
492
+ this.focusOptionWhenShown = (position) => {
493
+ let frames = 0;
494
+ const focusOption = () => {
495
+ const option = this.getListOption(position);
496
+ if ((option === null || option === void 0 ? void 0 : option.getClientRects().length) > 0) {
497
+ option.focus();
498
+ return;
499
+ }
500
+ frames += 1;
501
+ if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
502
+ requestAnimationFrame(focusOption);
503
+ }
504
+ };
505
+ requestAnimationFrame(focusOption);
506
+ };
507
+ this.closeListWithEscape = (event) => {
508
+ if (!this.isSuggestionListOpen()) {
485
509
  return;
486
510
  }
487
- const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item:last-child');
488
- listElement.focus();
511
+ event.preventDefault();
512
+ event.stopPropagation();
513
+ this.showCompletions = false;
489
514
  };
490
515
  this.handleCompletionChange = (event) => {
491
516
  event.stopPropagation();
@@ -502,10 +527,18 @@ const InputField = class {
502
527
  this.changeEmitter(event.detail.text);
503
528
  this.changeEmitter.flush();
504
529
  };
530
+ this.hasSuggestions = () => {
531
+ return (!this.readonly &&
532
+ this.type !== 'textarea' &&
533
+ this.completions.length > 0);
534
+ };
535
+ this.isSuggestionListOpen = () => {
536
+ return (this.hasSuggestions() &&
537
+ this.showCompletions &&
538
+ this.filterCompletions(this.getCurrentValue()).length > 0);
539
+ };
505
540
  this.renderAutocompleteList = () => {
506
- if (this.readonly ||
507
- this.type === 'textarea' ||
508
- this.completions.length === 0) {
541
+ if (!this.hasSuggestions()) {
509
542
  return;
510
543
  }
511
544
  const dropdownZIndex = getComputedStyle(this.limelInputField).getPropertyValue('--dropdown-z-index');
@@ -523,8 +556,14 @@ const InputField = class {
523
556
  return (h("limel-list", { onChange: this.handleCompletionChange, onKeyDown: this.handleKeyDownInDropdown, type: "selectable", items: filteredCompletions }));
524
557
  };
525
558
  this.handleKeyDownInDropdown = (event) => {
526
- const keyFound = [TAB, ESCAPE, ENTER].includes(event.key);
527
- if (keyFound) {
559
+ if (event.key === ESCAPE) {
560
+ event.preventDefault();
561
+ event.stopPropagation();
562
+ this.setFocus();
563
+ this.showCompletions = false;
564
+ return;
565
+ }
566
+ if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
528
567
  this.setFocus();
529
568
  }
530
569
  };
@@ -682,10 +721,10 @@ const InputField = class {
682
721
  if (this.isInvalid()) {
683
722
  properties['aria-invalid'] = 'true';
684
723
  }
685
- if (this.renderAutocompleteList()) {
724
+ if (this.hasSuggestions()) {
686
725
  properties['aria-controls'] = this.portalId;
687
726
  }
688
- return (h(Host, { key: '3abb0e074f55f8e47f46cf77ed885797047e44b9' }, 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 }, 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()));
727
+ return (h(Host, { key: 'e09ee9e3102d6a15d0806602c65361001cd20c63' }, 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 }, 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()));
689
728
  }
690
729
  valueWatcher(newValue) {
691
730
  if (!this.mdcTextField || this.changeWaiting) {
@@ -40,7 +40,7 @@ function getHeading(heading, type = 'note', language = 'en') {
40
40
  }
41
41
  }
42
42
 
43
- const calloutCss = () => `*,*::before,*::after{box-sizing:border-box;min-height:0;min-width:0}:host(limel-callout){box-sizing:border-box;display:flex;border-radius:0.5rem;overflow:hidden;--limel-callout-tint-color:var( --callout-color, rgb(var(--color-contrast-100)) );color:var(--callout-text-color, rgb(var(--contrast-1100)));font-size:var(--limel-theme-default-font-size)}:host([type=note]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-gray-default)) )}:host([type=important]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-sky-default)) )}:host([type=tip]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-green-default)) )}:host([type=caution]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-orange-light)) )}:host([type=warning]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-red-dark)) )}.side{padding:0.25rem;position:relative}.side:before{content:"";position:absolute;inset:0;opacity:0.2;background-color:var(--limel-callout-tint-color)}.side limel-icon{width:1.5rem;color:var(--limel-callout-tint-color);margin-top:0.0625rem}.main{display:flex;flex:1;flex-direction:column;gap:0.5rem;padding:0.25rem 0.5rem 0.5rem 0.5rem;background-color:var(--callout-background-color, rgb(var(--contrast-300)))}.heading{margin:0;font-size:1rem;font-weight:600}`;
43
+ const calloutCss = () => `*,*::before,*::after{box-sizing:border-box;min-height:0;min-width:0}:host(limel-callout){box-sizing:border-box;display:flex;border-radius:0.5rem;overflow:hidden;border:0.0625rem solid color-mix(in srgb, var(--limel-callout-tint-color), transparent 70%);--limel-callout-tint-color:var( --callout-color, rgb(var(--color-contrast-100)) );color:var(--callout-text-color, rgb(var(--contrast-1100)));font-size:var(--limel-theme-default-font-size)}:host([type=note]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-gray-default)) )}:host([type=important]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-sky-default)) )}:host([type=tip]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-green-default)) )}:host([type=caution]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-orange-light)) )}:host([type=warning]){--limel-callout-tint-color:var( --callout-color, rgb(var(--color-red-dark)) )}.side{padding:0.25rem;position:relative}.side:before{content:"";position:absolute;inset:0;opacity:0.2;background-color:var(--limel-callout-tint-color)}.side limel-icon{width:1.5rem;color:var(--limel-callout-tint-color);margin-top:0.0625rem}.main{display:flex;flex:1;flex-direction:column;gap:0.5rem;padding:0.25rem 0.5rem 0.5rem 0.5rem;background-color:var(--callout-background-color, rgb(var(--contrast-300)))}.heading{margin:0;font-size:1rem;font-weight:600}`;
44
44
 
45
45
  const Callout = class {
46
46
  constructor(hostRef) {
@@ -188,26 +188,16 @@ const List = class {
188
188
  this.listElement.removeEventListener('mousedown', this.handleItemMouseDown);
189
189
  this.listElement.addEventListener('mousedown', this.handleItemMouseDown);
190
190
  this.mdcList = new MDCList(element);
191
- // NOTE: This has no effect. It is kept as a marker rather than
192
- // removed, so that its absence isn't mistaken for an oversight.
191
+ // MDC's typeahead (`hasTypeahead`) is left off, so that typed
192
+ // characters are not taken from a field with a list under it. The list
193
+ // test in `list.e2e.tsx` fails if it is turned on. `limel-select` has
194
+ // its own typeahead, see `src/util/typeahead.ts`.
193
195
  //
194
- // MDC indexes each row for typeahead by looking for
195
- // `.mdc-deprecated-list-item__primary-text` inside it, while
196
- // `limel-list-item` renders its label as `<span class="label">`. MDC
197
- // therefore builds an empty index, and typing does nothing.
198
- //
199
- // Do not "fix" this by adding MDC's class name to `limel-list-item`.
200
- // That revives MDC's typeahead along with three behaviors that cannot
201
- // be configured from the outside: a hard coded 300ms buffer, `Enter`
202
- // being ignored for as long as that buffer lives (its `notifyAction`
203
- // is guarded by `isTypeaheadInProgress`), and the space bar being
204
- // excluded from matches. It would also collide with `limel-menu`,
205
- // where single characters are used as item hotkeys.
206
- //
207
- // `limel-select` implements its own typeahead instead, see
208
- // `src/util/typeahead.ts`. It intercepts typed characters in the
209
- // capture phase on this very element, so they never reach MDC.
210
- this.mdcList.hasTypeahead = true;
196
+ // Adding MDC's class name to `limel-list-item` does nothing while it is
197
+ // off. Turning it on as well would bring its 300ms buffer, `Enter`
198
+ // being ignored while the buffer lives and the space bar being left out
199
+ // of matches, none of which can be configured, and it would collide
200
+ // with the single characters that `limel-menu` uses as hotkeys.
211
201
  };
212
202
  /**
213
203
  * Focus the pressed list item directly, without scrolling.
@@ -3,6 +3,7 @@ import { t as translations } from './translations-Drl5LNAD.js';
3
3
  import { i as isDescendant } from './dom-B9Ofc5RB.js';
4
4
  import { E as ESCAPE, T as TAB, a as ENTER, A as ARROW_UP, b as ARROW_DOWN } from './keycodes-Bu6-QXrA.js';
5
5
  import { c as createRandomString } from './random-string-BrjD-ITe.js';
6
+ import { i as isTextEditingKey } from './typeahead-DOlnT3wh.js';
6
7
  import { a as getIconFillColor, g as getIconName } from './get-icon-props-CgNJbSP4.js';
7
8
  import { d as debounce } from './debounce-B67JMchz.js';
8
9
  import './language-ChagAaNf.js';
@@ -195,7 +196,7 @@ const Picker = class {
195
196
  props.maxItems = 1;
196
197
  }
197
198
  return [
198
- 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)),
199
+ 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)),
199
200
  this.renderDropdown(),
200
201
  ];
201
202
  }
@@ -243,7 +244,7 @@ const Picker = class {
243
244
  'is-on-top': this.actionPosition === 'top',
244
245
  'is-at-bottom': this.actionPosition === 'bottom',
245
246
  'has-position-sticky': this.actionScrollBehavior === 'sticky',
246
- }, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
247
+ }, badgeIcons: true, type: 'selectable', onChange: this.handleActionListChange, onKeyDown: this.onListKeyDown, items: this.actions.map(this.removeUnusedPropertiesOnAction) }),
247
248
  ];
248
249
  }
249
250
  removeUnusedPropertiesOnAction(action) {
@@ -307,7 +308,7 @@ const Picker = class {
307
308
  return h("p", { style: style }, text);
308
309
  }
309
310
  renderListResult() {
310
- return (h("limel-list", { badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
311
+ return (h("limel-list", { "data-suggestions": "", badgeIcons: this.badgeIcons, onChange: this.handleListChange, onKeyDown: this.onListKeyDown, type: "selectable", items: this.items }));
311
312
  }
312
313
  onListKeyDown(event) {
313
314
  if (event.key === ESCAPE) {
@@ -319,7 +320,7 @@ const Picker = class {
319
320
  this.chipSet.setFocus();
320
321
  return;
321
322
  }
322
- if ([TAB, ENTER].includes(event.key)) {
323
+ if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
323
324
  this.chipSet.setFocus();
324
325
  }
325
326
  }
@@ -448,10 +449,7 @@ const Picker = class {
448
449
  * @param event - event
449
450
  */
450
451
  handleInputKeyDown(event) {
451
- const isForwardTab = event.key === TAB &&
452
- !event.altKey &&
453
- !event.metaKey &&
454
- !event.shiftKey;
452
+ var _a;
455
453
  const isUp = event.key === ARROW_UP;
456
454
  const isDown = event.key === ARROW_DOWN;
457
455
  const isEscape = event.key === ESCAPE;
@@ -461,14 +459,14 @@ const Picker = class {
461
459
  this.handleEscape();
462
460
  return;
463
461
  }
464
- if (!isForwardTab && !isUp && !isDown) {
462
+ if (!isUp && !isDown) {
465
463
  return;
466
464
  }
467
- const list = document.querySelector(` #${this.portalId} limel-list`);
465
+ const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
468
466
  if (!list) {
469
467
  return;
470
468
  }
471
- if (isForwardTab || isDown) {
469
+ if (isDown) {
472
470
  // Match by class only (not `:first-child`) so a leading
473
471
  // `ListSeparator` doesn't shadow the first focusable item.
474
472
  const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
@@ -479,9 +477,6 @@ const Picker = class {
479
477
  listElement.focus();
480
478
  return;
481
479
  }
482
- if (!isUp) {
483
- return;
484
- }
485
480
  const listItems = list.shadowRoot.querySelectorAll('.mdc-deprecated-list-item');
486
481
  const listElement = [...listItems].at(-1);
487
482
  if (!listElement) {
@@ -3,6 +3,7 @@ import { b as isMobileDevice } from './device-B-tmXAXV.js';
3
3
  import { b as ARROW_DOWN, A as ARROW_UP, S as SPACEBAR, a as ENTER } from './keycodes-Bu6-QXrA.js';
4
4
  import { i as isMultiple } from './multiple-BylwpiM6.js';
5
5
  import { c as createRandomString } from './random-string-BrjD-ITe.js';
6
+ import { T as TypeaheadBuffer, N as NO_TYPEAHEAD_MATCH, a as isTypeaheadKey, f as findTypeaheadMatch } from './typeahead-DOlnT3wh.js';
6
7
  import { r as renderListComponent } from './render-list-component-J1IzNpbv.js';
7
8
  import { b as getIconColor, g as getIconName } from './get-icon-props-CgNJbSP4.js';
8
9
  import { M as MDCFloatingLabel } from './component-CyFeUQ0l.js';
@@ -254,166 +255,6 @@ var MDCSelectHelperText = /** @class */ (function (_super) {
254
255
  return MDCSelectHelperText;
255
256
  }(MDCComponent));
256
257
 
257
- /**
258
- * These helpers implement "type to jump", the behavior a native `<select>` has:
259
- * typing characters moves to the option whose text starts with those
260
- * characters. Characters accumulate into a short-lived buffer, so typing
261
- * `n`, `e` finds "Netherlands" instead of re-matching on `n` every time.
262
- *
263
- * The matching itself is pure and stateless — all the state lives in a
264
- * `TypeaheadBuffer`, which owns the timer that discards the buffer once the
265
- * user stops typing.
266
- */
267
- /**
268
- * How long a buffer of typed characters is kept before it is discarded, in
269
- * milliseconds. Roughly matches a native `<select>`.
270
- */
271
- const TYPEAHEAD_BUFFER_TIMEOUT = 1000;
272
- /**
273
- * Returned by `findTypeaheadMatch` when nothing matched.
274
- */
275
- const NO_TYPEAHEAD_MATCH = -1;
276
- /**
277
- * Whether a keyboard event should be treated as a typed character.
278
- *
279
- * The space bar counts as a character here. Whether a *bare* space should
280
- * start a buffer, or keep whatever meaning it has in the consuming component,
281
- * is the consumer's decision.
282
- *
283
- * `shiftKey` is allowed, since capital letters are typed with it, and
284
- * `event.repeat` is allowed, since holding a key down cycles through matches
285
- * in a native `<select>` too.
286
- *
287
- * @param event - the keyboard event to inspect
288
- * @returns `true` when the event represents a typed character
289
- */
290
- function isTypeaheadKey(event) {
291
- return (event.key.length === 1 &&
292
- !event.altKey &&
293
- !event.ctrlKey &&
294
- !event.metaKey &&
295
- !event.isComposing);
296
- }
297
- /**
298
- * Find the candidate to move to for a given buffer of typed characters.
299
- *
300
- * Two different searches are used, mirroring a native `<select>`:
301
- * - When the buffer is a single character, possibly repeated (`b`, `bb`,
302
- * `bbb`), only that character is matched and the search starts *after*
303
- * `currentIndex`. Pressing the same key repeatedly therefore cycles through
304
- * all candidates starting with it.
305
- * - Otherwise the whole buffer is matched and the search starts *at*
306
- * `currentIndex`, so continuing to type on an already-matching candidate
307
- * keeps it rather than jumping to the next one.
308
- *
309
- * Both searches wrap around the end of the list.
310
- *
311
- * @param candidates - the candidates to search, index-aligned with the rows
312
- * they are rendered as. Use `null` for rows that can never match, such as
313
- * separators, so that they still occupy their index.
314
- * @param buffer - the characters typed so far
315
- * @param currentIndex - the index to search from. Anything outside
316
- * `candidates` is treated as "no current candidate", and the search starts
317
- * from the beginning.
318
- * @returns the index of the matching candidate, or `NO_TYPEAHEAD_MATCH`
319
- */
320
- function findTypeaheadMatch(candidates, buffer, currentIndex) {
321
- const count = candidates.length;
322
- const query = buffer.toLowerCase();
323
- if (!query || count === 0) {
324
- return NO_TYPEAHEAD_MATCH;
325
- }
326
- const characters = [...query];
327
- const cycles = isSingleDistinctCharacter(characters);
328
- const needle = cycles ? characters[0] : query;
329
- // Cycling starts one past the current candidate, so that pressing the same
330
- // key again advances. Matching several characters starts at it, so that
331
- // continuing to type keeps a candidate that already matches.
332
- const offset = cycles ? 1 : 0;
333
- const hasCurrent = currentIndex >= 0 && currentIndex < count;
334
- const start = hasCurrent ? (currentIndex + offset) % count : 0;
335
- for (let step = 0; step < count; step += 1) {
336
- const index = (start + step) % count;
337
- if (candidateMatches(candidates[index], needle)) {
338
- return index;
339
- }
340
- }
341
- return NO_TYPEAHEAD_MATCH;
342
- }
343
- /**
344
- * Whether a string consists of a single character, possibly repeated. Also
345
- * `true` for one character on its own, which is what makes a single key press
346
- * advance past the current candidate rather than re-matching it.
347
- *
348
- * Takes the characters already split apart, so that the caller and this
349
- * function agree on what "a character" is for anything outside the basic
350
- * multilingual plane.
351
- *
352
- * @param characters - the characters of the string
353
- * @returns `true` when every character is the same
354
- */
355
- function isSingleDistinctCharacter(characters) {
356
- return characters.every((character) => character === characters[0]);
357
- }
358
- /**
359
- * @param candidate - the candidate to test, or `null` for a row that can
360
- * never match
361
- * @param needle - the lower cased characters to match
362
- * @returns `true` when the candidate starts with the given characters
363
- */
364
- function candidateMatches(candidate, needle) {
365
- var _a;
366
- if (!candidate || candidate.disabled) {
367
- return false;
368
- }
369
- const text = (_a = candidate.text) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
370
- return !!text && text.startsWith(needle);
371
- }
372
- /**
373
- * Accumulates typed characters, and discards them again once the user stops
374
- * typing for `TYPEAHEAD_BUFFER_TIMEOUT`.
375
- */
376
- class TypeaheadBuffer {
377
- constructor(timeout = TYPEAHEAD_BUFFER_TIMEOUT) {
378
- this.buffer = '';
379
- this.timeout = timeout;
380
- }
381
- /**
382
- * The characters typed so far.
383
- */
384
- get value() {
385
- return this.buffer;
386
- }
387
- /**
388
- * Whether nothing has been typed, or the buffer has been discarded.
389
- */
390
- get isEmpty() {
391
- return this.buffer === '';
392
- }
393
- /**
394
- * Append a character, and restart the timer that discards the buffer.
395
- *
396
- * @param character - the character to append
397
- * @returns the buffer, including the appended character
398
- */
399
- append(character) {
400
- this.buffer += character;
401
- clearTimeout(this.resetTimeoutId);
402
- this.resetTimeoutId = setTimeout(() => {
403
- this.clear();
404
- }, this.timeout);
405
- return this.buffer;
406
- }
407
- /**
408
- * Discard the buffer, and cancel the timer that would have discarded it.
409
- */
410
- clear() {
411
- this.buffer = '';
412
- clearTimeout(this.resetTimeoutId);
413
- this.resetTimeoutId = undefined;
414
- }
415
- }
416
-
417
258
  const TRIGGER_ID = 's-trigger';
418
259
  const LABEL_ID = 's-label';
419
260
  const SELECTED_TEXT_ID = 's-selected-text';
@@ -0,0 +1,188 @@
1
+ import { B as BACKSPACE, D as DELETE, S as SPACEBAR } from './keycodes-Bu6-QXrA.js';
2
+
3
+ /**
4
+ * These helpers implement "type to jump", the behavior a native `<select>` has:
5
+ * typing characters moves to the option whose text starts with those
6
+ * characters. Characters accumulate into a short-lived buffer, so typing
7
+ * `n`, `e` finds "Netherlands" instead of re-matching on `n` every time.
8
+ *
9
+ * The matching itself is pure and stateless — all the state lives in a
10
+ * `TypeaheadBuffer`, which owns the timer that discards the buffer once the
11
+ * user stops typing.
12
+ *
13
+ * `isTextEditingKey` is for the other side of a list: a field with a list
14
+ * under it uses it to tell which keys belong to the field.
15
+ */
16
+ /**
17
+ * How long a buffer of typed characters is kept before it is discarded, in
18
+ * milliseconds. Roughly matches a native `<select>`.
19
+ */
20
+ const TYPEAHEAD_BUFFER_TIMEOUT = 1000;
21
+ /**
22
+ * Returned by `findTypeaheadMatch` when nothing matched.
23
+ */
24
+ const NO_TYPEAHEAD_MATCH = -1;
25
+ /**
26
+ * Whether a keyboard event should be treated as a typed character.
27
+ *
28
+ * The space bar counts as a character here. Whether a *bare* space should
29
+ * start a buffer, or keep whatever meaning it has in the consuming component,
30
+ * is the consumer's decision.
31
+ *
32
+ * `shiftKey` is allowed, since capital letters are typed with it, and
33
+ * `event.repeat` is allowed, since holding a key down cycles through matches
34
+ * in a native `<select>` too.
35
+ *
36
+ * @param event - the keyboard event to inspect
37
+ * @returns `true` when the event represents a typed character
38
+ */
39
+ function isTypeaheadKey(event) {
40
+ return (event.key.length === 1 &&
41
+ !event.altKey &&
42
+ !event.ctrlKey &&
43
+ !event.metaKey &&
44
+ !event.isComposing);
45
+ }
46
+ /**
47
+ * Find the candidate to move to for a given buffer of typed characters.
48
+ *
49
+ * Two different searches are used, mirroring a native `<select>`:
50
+ * - When the buffer is a single character, possibly repeated (`b`, `bb`,
51
+ * `bbb`), only that character is matched and the search starts *after*
52
+ * `currentIndex`. Pressing the same key repeatedly therefore cycles through
53
+ * all candidates starting with it.
54
+ * - Otherwise the whole buffer is matched and the search starts *at*
55
+ * `currentIndex`, so continuing to type on an already-matching candidate
56
+ * keeps it rather than jumping to the next one.
57
+ *
58
+ * Both searches wrap around the end of the list.
59
+ *
60
+ * @param candidates - the candidates to search, index-aligned with the rows
61
+ * they are rendered as. Use `null` for rows that can never match, such as
62
+ * separators, so that they still occupy their index.
63
+ * @param buffer - the characters typed so far
64
+ * @param currentIndex - the index to search from. Anything outside
65
+ * `candidates` is treated as "no current candidate", and the search starts
66
+ * from the beginning.
67
+ * @returns the index of the matching candidate, or `NO_TYPEAHEAD_MATCH`
68
+ */
69
+ function findTypeaheadMatch(candidates, buffer, currentIndex) {
70
+ const count = candidates.length;
71
+ const query = buffer.toLowerCase();
72
+ if (!query || count === 0) {
73
+ return NO_TYPEAHEAD_MATCH;
74
+ }
75
+ const characters = [...query];
76
+ const cycles = isSingleDistinctCharacter(characters);
77
+ const needle = cycles ? characters[0] : query;
78
+ // Cycling starts one past the current candidate, so that pressing the same
79
+ // key again advances. Matching several characters starts at it, so that
80
+ // continuing to type keeps a candidate that already matches.
81
+ const offset = cycles ? 1 : 0;
82
+ const hasCurrent = currentIndex >= 0 && currentIndex < count;
83
+ const start = hasCurrent ? (currentIndex + offset) % count : 0;
84
+ for (let step = 0; step < count; step += 1) {
85
+ const index = (start + step) % count;
86
+ if (candidateMatches(candidates[index], needle)) {
87
+ return index;
88
+ }
89
+ }
90
+ return NO_TYPEAHEAD_MATCH;
91
+ }
92
+ /**
93
+ * Whether a string consists of a single character, possibly repeated. Also
94
+ * `true` for one character on its own, which is what makes a single key press
95
+ * advance past the current candidate rather than re-matching it.
96
+ *
97
+ * Takes the characters already split apart, so that the caller and this
98
+ * function agree on what "a character" is for anything outside the basic
99
+ * multilingual plane.
100
+ *
101
+ * @param characters - the characters of the string
102
+ * @returns `true` when every character is the same
103
+ */
104
+ function isSingleDistinctCharacter(characters) {
105
+ return characters.every((character) => character === characters[0]);
106
+ }
107
+ /**
108
+ * @param candidate - the candidate to test, or `null` for a row that can
109
+ * never match
110
+ * @param needle - the lower cased characters to match
111
+ * @returns `true` when the candidate starts with the given characters
112
+ */
113
+ function candidateMatches(candidate, needle) {
114
+ var _a;
115
+ if (!candidate || candidate.disabled) {
116
+ return false;
117
+ }
118
+ const text = (_a = candidate.text) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
119
+ return !!text && text.startsWith(needle);
120
+ }
121
+ /**
122
+ * Accumulates typed characters, and discards them again once the user stops
123
+ * typing for `TYPEAHEAD_BUFFER_TIMEOUT`.
124
+ */
125
+ class TypeaheadBuffer {
126
+ constructor(timeout = TYPEAHEAD_BUFFER_TIMEOUT) {
127
+ this.buffer = '';
128
+ this.timeout = timeout;
129
+ }
130
+ /**
131
+ * The characters typed so far.
132
+ */
133
+ get value() {
134
+ return this.buffer;
135
+ }
136
+ /**
137
+ * Whether nothing has been typed, or the buffer has been discarded.
138
+ */
139
+ get isEmpty() {
140
+ return this.buffer === '';
141
+ }
142
+ /**
143
+ * Append a character, and restart the timer that discards the buffer.
144
+ *
145
+ * @param character - the character to append
146
+ * @returns the buffer, including the appended character
147
+ */
148
+ append(character) {
149
+ this.buffer += character;
150
+ clearTimeout(this.resetTimeoutId);
151
+ this.resetTimeoutId = setTimeout(() => {
152
+ this.clear();
153
+ }, this.timeout);
154
+ return this.buffer;
155
+ }
156
+ /**
157
+ * Discard the buffer, and cancel the timer that would have discarded it.
158
+ */
159
+ clear() {
160
+ this.buffer = '';
161
+ clearTimeout(this.resetTimeoutId);
162
+ this.resetTimeoutId = undefined;
163
+ }
164
+ }
165
+ /**
166
+ * Whether a keyboard event edits text: a typed character, or Backspace or
167
+ * Delete. A bare space is left out, since on an option of a list it selects
168
+ * the option.
169
+ *
170
+ * A character typed with Option, or with AltGr, which is reported as Ctrl and
171
+ * Alt together, counts, since many layouts type `@` and `$` that way. A
172
+ * shortcut with Ctrl or Meta does not.
173
+ *
174
+ * @param event - the keyboard event to inspect
175
+ * @returns `true` when the event edits text
176
+ */
177
+ function isTextEditingKey(event) {
178
+ if (event.key === BACKSPACE || event.key === DELETE) {
179
+ return true;
180
+ }
181
+ const isCharacter = event.key.length === 1 &&
182
+ event.key !== SPACEBAR &&
183
+ !event.metaKey &&
184
+ !event.isComposing;
185
+ return isCharacter && (!event.ctrlKey || event.altKey);
186
+ }
187
+
188
+ export { NO_TYPEAHEAD_MATCH as N, TypeaheadBuffer as T, isTypeaheadKey as a, findTypeaheadMatch as f, isTextEditingKey as i };