@internetarchive/collection-browser 3.4.1-alpha-webdev7761.2 → 3.4.1-alpha-webdev7761.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.
@@ -1,43 +1,15 @@
1
1
  import { __decorate } from "tslib";
2
- import { html, LitElement, css, } from 'lit';
2
+ import { html, LitElement, nothing, css, } from 'lit';
3
3
  import { customElement, property, state, query } from 'lit/decorators.js';
4
4
  import { classMap } from 'lit/directives/class-map.js';
5
5
  import { ifDefined } from 'lit/directives/if-defined.js';
6
6
  import { live } from 'lit/directives/live.js';
7
7
  import { when } from 'lit/directives/when.js';
8
8
  import { msg } from '@lit/localize';
9
- import { hasAnyOf, } from './models';
10
- import caretClosed from './caret-closed';
11
- import caretOpen from './caret-open';
12
- /**
13
- * Tests whether the given `haystack` string has the given `needle` as a subsequence.
14
- * Returns `true` if the characters of `needle` appear in order within `haystack`,
15
- * regardless of whether they are contiguous. Returns `false` otherwise.
16
- *
17
- * E.g., `ace` is a subsequence of `archive` (but not a contiguous substring).
18
- *
19
- * Note: The empty string is a subsequence of any string, including itself.
20
- *
21
- * @param needle The potential subsequence to check for inside `haystack`.
22
- * @param haystack The string to be tested for containing the `needle` subsequence.
23
- * @returns Whether `haystack` has `needle` as a subsequence.
24
- */
25
- const isSubsequence = (needle, haystack) => {
26
- const needleLen = needle.length;
27
- const haystackLen = haystack.length;
28
- if (needleLen === 0)
29
- return true;
30
- let needleIdx = 0;
31
- let haystackIdx = 0;
32
- while (haystackIdx < haystackLen) {
33
- if (haystack[haystackIdx] === needle[needleIdx])
34
- needleIdx += 1;
35
- if (needleIdx >= needleLen)
36
- return true;
37
- haystackIdx += 1;
38
- }
39
- return false;
40
- };
9
+ import { hasAnyOf, isSubsequence, } from './models';
10
+ import caretClosedIcon from './caret-closed';
11
+ import caretOpenIcon from './caret-open';
12
+ import clearIcon from './clear';
41
13
  /**
42
14
  * Map from filter preset keys to their associated filtering function.
43
15
  * @see {@linkcode IAComboBox.filter}
@@ -119,7 +91,8 @@ let IAComboBox = class IAComboBox extends LitElement {
119
91
  */
120
92
  this.caseSensitive = false;
121
93
  /**
122
- * Whether the filtered options should be listed in lexicographically-sorted order.
94
+ * Whether the filtered options should be listed in lexicographically-sorted order,
95
+ * respecting the current `caseSensitive` setting.
123
96
  * Default is `false`, displaying them in the same order as the provided options array.
124
97
  */
125
98
  this.sort = false;
@@ -141,6 +114,11 @@ let IAComboBox = class IAComboBox extends LitElement {
141
114
  * Default is `false`, closing the options list when a selection is made.
142
115
  */
143
116
  this.stayOpen = false;
117
+ /**
118
+ * Whether the combo box shows a clear button when a value is selected.
119
+ * Default is `false`.
120
+ */
121
+ this.clearable = false;
144
122
  /**
145
123
  * Whether the combo box's option menu is currently expanded. Default is `false`.
146
124
  */
@@ -213,19 +191,17 @@ let IAComboBox = class IAComboBox extends LitElement {
213
191
  this.internals = this.attachInternals();
214
192
  }
215
193
  render() {
194
+ const mainWidgetClasses = classMap({
195
+ disabled: this.disabled,
196
+ focused: this.hasFocus,
197
+ });
216
198
  return html `
217
- <div
218
- id="container"
219
- class=${classMap({ focused: this.hasFocus })}
220
- part="container"
221
- >
199
+ <div id="container" part="container">
222
200
  ${this.labelTemplate}
223
- <div
224
- id="main-widget-row"
225
- class=${classMap({ disabled: this.disabled })}
226
- part="combo-box"
227
- >
228
- ${this.textInputTemplate} ${this.caretButtonTemplate}
201
+ <div id="main-widget-row" class=${mainWidgetClasses} part="combo-box">
202
+ ${this.textInputTemplate}
203
+ ${this.clearable ? this.clearButtonTemplate : nothing}
204
+ ${this.caretButtonTemplate}
229
205
  </div>
230
206
  ${this.optionsListTemplate}
231
207
  </div>
@@ -295,10 +271,14 @@ let IAComboBox = class IAComboBox extends LitElement {
295
271
  /**
296
272
  * Template for the main label for the combo box.
297
273
  *
298
- * Uses the contents of the default (unnamed) slot as the label text.
274
+ * Uses the contents of the `label` named slot as the label text.
299
275
  */
300
276
  get labelTemplate() {
301
- return html `<label id="label" for="text-input"><slot></slot></label>`;
277
+ return html `
278
+ <label id="label" for="text-input">
279
+ <slot name="label"></slot>
280
+ </label>
281
+ `;
302
282
  }
303
283
  /**
304
284
  * Template for the text input field that users can edit to filter the available
@@ -307,7 +287,7 @@ let IAComboBox = class IAComboBox extends LitElement {
307
287
  get textInputTemplate() {
308
288
  var _a;
309
289
  const textInputClasses = classMap({
310
- editable: this.behavior !== 'select-only',
290
+ 'clear-padding': this.clearable && !this.shouldShowClearButton,
311
291
  });
312
292
  return html `
313
293
  <input
@@ -323,6 +303,7 @@ let IAComboBox = class IAComboBox extends LitElement {
323
303
  aria-controls="options-list"
324
304
  aria-expanded=${this.open}
325
305
  aria-activedescendant=${ifDefined((_a = this.highlightedOption) === null || _a === void 0 ? void 0 : _a.id)}
306
+ ?readonly=${this.behavior === 'select-only'}
326
307
  ?disabled=${this.disabled}
327
308
  ?required=${this.required}
328
309
  @click=${this.handleComboBoxClick}
@@ -333,14 +314,39 @@ let IAComboBox = class IAComboBox extends LitElement {
333
314
  />
334
315
  `;
335
316
  }
317
+ /**
318
+ * Template for the clear button that is shown when the `clearable` property
319
+ * is true.
320
+ */
321
+ get clearButtonTemplate() {
322
+ return html `
323
+ <button
324
+ type="button"
325
+ id="clear-button"
326
+ part="clear-button"
327
+ tabindex="-1"
328
+ ?hidden=${!this.shouldShowClearButton}
329
+ @click=${this.handleClearButtonClick}
330
+ >
331
+ <span class="sr-only">${msg('Clear')}</span>
332
+ <slot name="clear-button">
333
+ ${clearIcon}
334
+ </slot>
335
+ </button>
336
+ `;
337
+ }
336
338
  /**
337
339
  * Template for the caret open/closed icons to show beside the text input.
338
340
  * The icons are wrapped in named slots to allow consumers to override them.
339
341
  */
340
342
  get caretTemplate() {
341
343
  return html `
342
- <slot name="caret-closed" ?hidden=${this.open}> ${caretClosed} </slot>
343
- <slot name="caret-open" ?hidden=${!this.open}> ${caretOpen} </slot>
344
+ <slot name="caret-closed" ?hidden=${this.open}>
345
+ ${caretClosedIcon}
346
+ </slot>
347
+ <slot name="caret-open" ?hidden=${!this.open}>
348
+ ${caretOpenIcon}
349
+ </slot>
344
350
  `;
345
351
  }
346
352
  /**
@@ -361,6 +367,7 @@ let IAComboBox = class IAComboBox extends LitElement {
361
367
  @focus=${this.handleFocus}
362
368
  @blur=${this.handleBlur}
363
369
  >
370
+ <span class="sr-only">${msg('Toggle options')}</span>
364
371
  ${this.caretTemplate}
365
372
  </button>
366
373
  `;
@@ -374,6 +381,7 @@ let IAComboBox = class IAComboBox extends LitElement {
374
381
  id="options-list"
375
382
  part="options-list"
376
383
  role="listbox"
384
+ tabindex="-1"
377
385
  popover
378
386
  ?hidden=${!this.open}
379
387
  @focus=${this.handleFocus}
@@ -405,6 +413,7 @@ let IAComboBox = class IAComboBox extends LitElement {
405
413
  id=${opt.id}
406
414
  class=${optionClasses}
407
415
  part="option"
416
+ role="option"
408
417
  tabindex="-1"
409
418
  @pointerenter=${this.handleOptionPointerEnter}
410
419
  @pointermove=${this.handleOptionPointerMove}
@@ -441,7 +450,7 @@ let IAComboBox = class IAComboBox extends LitElement {
441
450
  * Handler for when the pointer device is moved within an option in the dropdown.
442
451
  */
443
452
  handleOptionPointerMove(e) {
444
- const target = e.target;
453
+ const target = e.currentTarget;
445
454
  const option = this.getOptionFor(target.id);
446
455
  if (option)
447
456
  this.setHighlightedOption(option);
@@ -450,7 +459,7 @@ let IAComboBox = class IAComboBox extends LitElement {
450
459
  * Handler for when the user clicks on an option in the dropdown.
451
460
  */
452
461
  handleOptionClick(e) {
453
- const target = e.target;
462
+ const target = e.currentTarget;
454
463
  const option = this.getOptionFor(target.id);
455
464
  if (option) {
456
465
  this.setSelectedOption(option.id);
@@ -485,10 +494,16 @@ let IAComboBox = class IAComboBox extends LitElement {
485
494
  }
486
495
  break;
487
496
  case 'Tab':
488
- this.textInput.focus();
489
- return;
497
+ this.handleTabPressed();
498
+ return; // Never cancel the default behavior for Tab
499
+ case ' ':
500
+ this.handleSpacePressed();
501
+ // In the specific case of picking an option in select-only, we skip the defaults
502
+ if (this.behavior === 'select-only' && this.highlightedOption)
503
+ break;
504
+ return; // Otherwise, don't cancel the default behavior
490
505
  default:
491
- // Do nothing and allow propagation otherwise
506
+ // Do nothing and allow propagation for all other keys
492
507
  return;
493
508
  }
494
509
  e.stopPropagation();
@@ -567,20 +582,46 @@ let IAComboBox = class IAComboBox extends LitElement {
567
582
  handleAltDownArrowPressed() {
568
583
  this.openOptionsMenu();
569
584
  }
585
+ /**
586
+ * Handler for when the Tab key is pressed
587
+ */
588
+ handleTabPressed() {
589
+ if (this.highlightedOption) {
590
+ this.setSelectedOption(this.highlightedOption.id);
591
+ if (!this.stayOpen)
592
+ this.open = false;
593
+ }
594
+ }
595
+ /**
596
+ * Handler for when the Space key is pressed
597
+ */
598
+ handleSpacePressed() {
599
+ if (this.behavior === 'select-only' && this.highlightedOption) {
600
+ this.setSelectedOption(this.highlightedOption.id);
601
+ if (!this.stayOpen)
602
+ this.open = false;
603
+ }
604
+ }
570
605
  /**
571
606
  * Handler for clicks on the combo box input field or caret button.
572
607
  */
573
608
  handleComboBoxClick() {
574
609
  this.toggleOptionsMenu();
575
610
  }
611
+ /**
612
+ * Handler for when the clear button is clicked.
613
+ */
614
+ handleClearButtonClick() {
615
+ this.clearSelectedOption();
616
+ this.textInput.focus();
617
+ this.openOptionsMenu();
618
+ }
576
619
  /**
577
620
  * Handler for when any part of the combo box receives focus.
578
621
  */
579
622
  handleFocus() {
580
- if (this.behavior === 'select-only') {
581
- this.caretButton.focus();
582
- }
583
- else {
623
+ if (this.behavior !== 'select-only') {
624
+ // Always keep focus on the text input if it's editable
584
625
  this.textInput.focus();
585
626
  }
586
627
  this.hasFocus = true;
@@ -592,13 +633,21 @@ let IAComboBox = class IAComboBox extends LitElement {
592
633
  handleBlur() {
593
634
  this.hasFocus = false;
594
635
  this.losingFocus = true;
636
+ // On the next tick, check whether we've actually lost focus to some other element,
637
+ // or just had a momentary internal focus switch. If it's the former, we should
638
+ // close the menu and possibly make a selection (depending on desired behavior).
595
639
  setTimeout(() => {
596
- var _a;
640
+ var _a, _b, _c;
597
641
  if (this.losingFocus && !((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)) {
598
642
  this.losingFocus = false;
599
643
  this.closeOptionsMenu();
600
- if (this.behavior === 'freeform')
644
+ if (this.behavior === 'list') {
645
+ this.setTextValue((_c = (_b = this.selectedOption) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : '', false);
646
+ }
647
+ else if (this.behavior === 'freeform' &&
648
+ (this.enteredText || this.value)) {
601
649
  this.setValue(this.enteredText);
650
+ }
602
651
  }
603
652
  }, 0);
604
653
  }
@@ -693,7 +742,8 @@ let IAComboBox = class IAComboBox extends LitElement {
693
742
  const prevValue = this.value;
694
743
  this.value = option.id;
695
744
  this.internals.setFormValue(this.value);
696
- this.setTextValue(option.text);
745
+ this.setTextValue(option.text, false);
746
+ this.setFilterText('');
697
747
  if (this.value !== prevValue)
698
748
  this.emitChangeEvent();
699
749
  // Invoke the option's select callback if defined
@@ -738,10 +788,20 @@ let IAComboBox = class IAComboBox extends LitElement {
738
788
  /**
739
789
  * Changes the value of the text input box, and updates the filter accordingly.
740
790
  */
741
- setTextValue(value) {
791
+ setTextValue(value, setFilter = true) {
742
792
  this.textInput.value = value;
743
793
  this.enteredText = value;
744
- this.setFilterText(value);
794
+ if (setFilter)
795
+ this.setFilterText(value);
796
+ }
797
+ /**
798
+ * Sets the current filter text based on the provided string. The resulting filter
799
+ * text might not exactly match the provided value, depending on the current case
800
+ * sensitivity.
801
+ */
802
+ setFilterText(baseFilterText) {
803
+ const { caseTransform } = this;
804
+ this.filterText = caseTransform(baseFilterText);
745
805
  }
746
806
  openOptionsMenu() {
747
807
  this.open = true;
@@ -777,6 +837,19 @@ let IAComboBox = class IAComboBox extends LitElement {
777
837
  //
778
838
  // HELPERS
779
839
  //
840
+ /**
841
+ * True iff no selection has been made and no text has been entered.
842
+ */
843
+ get isEmpty() {
844
+ return !this.selectedOption && !this.enteredText;
845
+ }
846
+ /**
847
+ * We only show the clear button when the `clearable` property is set
848
+ * and the combo box is neither empty nor disabled.
849
+ */
850
+ get shouldShowClearButton() {
851
+ return this.clearable && !this.disabled && !this.isEmpty;
852
+ }
780
853
  /**
781
854
  * Sets the size and position of the options menu to match the size and position of
782
855
  * the combo box widget. Prefers to position below the main widget, but will flip
@@ -789,11 +862,11 @@ let IAComboBox = class IAComboBox extends LitElement {
789
862
  const usableHeightAbove = mainWidgetRect.top;
790
863
  const usableHeightBelow = innerHeight - mainWidgetRect.bottom;
791
864
  // We still want to respect any CSS var specified by the consumer
792
- const maxHeightVar = 'var(--comboBoxListMaxHeight, 250px)';
865
+ const maxHeightVar = 'var(--combo-box-list-max-height, 250px)';
793
866
  const optionsListStyles = {
794
867
  top: `${mainWidgetRect.bottom + scrollY}px`,
795
868
  left: `${mainWidgetRect.left + scrollX}px`,
796
- width: `var(--comboBoxListWidth, ${mainWidgetRect.width}px)`,
869
+ width: `var(--combo-box-list-width, ${mainWidgetRect.width}px)`,
797
870
  maxHeight: `min(${usableHeightBelow}px, ${maxHeightVar})`,
798
871
  };
799
872
  Object.assign(optionsList.style, optionsListStyles);
@@ -816,15 +889,6 @@ let IAComboBox = class IAComboBox extends LitElement {
816
889
  get caseTransform() {
817
890
  return this.caseSensitive ? STRING_IDENTITY_FN : STRING_LOWER_CASE_FN;
818
891
  }
819
- /**
820
- * Sets the current filter text based on the provided string. The resulting filter
821
- * text might not exactly match the provided value, depending on the current case
822
- * sensitivity.
823
- */
824
- setFilterText(baseFilterText) {
825
- const { caseTransform } = this;
826
- this.filterText = caseTransform(baseFilterText);
827
- }
828
892
  /**
829
893
  * Returns the combo box option having the given ID, or null if none exists.
830
894
  */
@@ -938,6 +1002,11 @@ let IAComboBox = class IAComboBox extends LitElement {
938
1002
  }
939
1003
  static get styles() {
940
1004
  return css `
1005
+ #container {
1006
+ display: inline-block;
1007
+ width: var(--combo-box-width, auto);
1008
+ }
1009
+
941
1010
  #label {
942
1011
  display: block;
943
1012
  width: fit-content;
@@ -949,9 +1018,15 @@ let IAComboBox = class IAComboBox extends LitElement {
949
1018
  flex-wrap: nowrap;
950
1019
  background: white;
951
1020
  border: 1px solid black;
1021
+ width: 100%;
1022
+ }
1023
+
1024
+ #main-widget-row:not(.focused):hover,
1025
+ #main-widget-row:not(.focused):active {
1026
+ background: #fafafa;
952
1027
  }
953
1028
 
954
- .focused #main-widget-row {
1029
+ #main-widget-row.focused {
955
1030
  outline: black auto 1px;
956
1031
  outline-offset: 3px;
957
1032
  }
@@ -960,27 +1035,47 @@ let IAComboBox = class IAComboBox extends LitElement {
960
1035
  appearance: none;
961
1036
  background: transparent;
962
1037
  border: none;
963
- padding: var(--comboBoxPadding, 5px);
1038
+ padding: var(--combo-box-padding, 5px);
1039
+ padding-right: 0;
964
1040
  width: 100%;
965
1041
  font-size: inherit;
1042
+ color: inherit;
966
1043
  outline: none;
1044
+ text-overflow: ellipsis;
967
1045
  }
968
1046
 
969
- #text-input:not(.editable) {
1047
+ #text-input.clear-padding {
1048
+ padding-right: 30px;
1049
+ }
1050
+
1051
+ #text-input:read-only {
970
1052
  cursor: pointer;
971
1053
  }
972
1054
 
1055
+ #clear-button,
973
1056
  #caret-button {
974
1057
  display: inline-flex;
975
1058
  align-items: center;
976
1059
  appearance: none;
977
1060
  background: transparent;
978
1061
  border: none;
979
- padding: var(--comboBoxPadding, 5px);
1062
+ padding: var(--combo-box-padding, 5px) 5px;
980
1063
  outline: none;
981
1064
  cursor: pointer;
982
1065
  }
983
1066
 
1067
+ #clear-button {
1068
+ flex: 0 0 30px;
1069
+ }
1070
+
1071
+ #clear-button[hidden] {
1072
+ display: none;
1073
+ }
1074
+
1075
+ #caret-button {
1076
+ padding-right: var(--combo-box-padding, 5px);
1077
+ }
1078
+
984
1079
  #options-list {
985
1080
  position: absolute;
986
1081
  list-style-type: none;
@@ -1005,13 +1100,23 @@ let IAComboBox = class IAComboBox extends LitElement {
1005
1100
  text-align: center;
1006
1101
  }
1007
1102
 
1008
- .caret {
1103
+ #caret-button svg {
1009
1104
  width: 14px;
1010
1105
  height: 14px;
1011
1106
  }
1012
1107
 
1108
+ #clear-button svg {
1109
+ width: var(--combo-box-clear-icon-size, 16px);
1110
+ height: var(--combo-box-clear-icon-size, 16px);
1111
+ }
1112
+
1013
1113
  .option {
1014
- padding: 5px;
1114
+ padding: 7px 5px;
1115
+ width: 100%;
1116
+ box-sizing: border-box;
1117
+ line-height: 1.1;
1118
+ text-overflow: ellipsis;
1119
+ overflow: hidden;
1015
1120
  cursor: pointer;
1016
1121
  }
1017
1122
 
@@ -1069,6 +1174,9 @@ __decorate([
1069
1174
  __decorate([
1070
1175
  property({ type: Boolean, reflect: true, attribute: 'stay-open' })
1071
1176
  ], IAComboBox.prototype, "stayOpen", void 0);
1177
+ __decorate([
1178
+ property({ type: Boolean, reflect: true })
1179
+ ], IAComboBox.prototype, "clearable", void 0);
1072
1180
  __decorate([
1073
1181
  property({ type: Boolean, reflect: true })
1074
1182
  ], IAComboBox.prototype, "open", void 0);
@@ -1099,9 +1207,6 @@ __decorate([
1099
1207
  __decorate([
1100
1208
  query('#text-input')
1101
1209
  ], IAComboBox.prototype, "textInput", void 0);
1102
- __decorate([
1103
- query('#caret-button')
1104
- ], IAComboBox.prototype, "caretButton", void 0);
1105
1210
  __decorate([
1106
1211
  query('#options-list')
1107
1212
  ], IAComboBox.prototype, "optionsList", void 0);