@internetarchive/collection-browser 0.4.16-alpha.9 → 0.4.16

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 (63) hide show
  1. package/dist/src/app-root.js +12 -0
  2. package/dist/src/app-root.js.map +1 -1
  3. package/dist/src/collection-browser.d.ts +53 -14
  4. package/dist/src/collection-browser.js +264 -110
  5. package/dist/src/collection-browser.js.map +1 -1
  6. package/dist/src/collection-facets/facets-template.d.ts +3 -0
  7. package/dist/src/collection-facets/facets-template.js +20 -1
  8. package/dist/src/collection-facets/facets-template.js.map +1 -1
  9. package/dist/src/collection-facets/more-facets-content.js +7 -4
  10. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  11. package/dist/src/collection-facets.d.ts +0 -2
  12. package/dist/src/collection-facets.js +1 -4
  13. package/dist/src/collection-facets.js.map +1 -1
  14. package/dist/src/empty-placeholder.js +1 -0
  15. package/dist/src/empty-placeholder.js.map +1 -1
  16. package/dist/src/models.d.ts +5 -0
  17. package/dist/src/models.js.map +1 -1
  18. package/dist/src/tiles/grid/item-tile.js +10 -3
  19. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  20. package/dist/src/tiles/list/tile-list-compact.d.ts +1 -0
  21. package/dist/src/tiles/list/tile-list-compact.js +13 -1
  22. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  23. package/dist/src/tiles/list/tile-list.js +10 -1
  24. package/dist/src/tiles/list/tile-list.js.map +1 -1
  25. package/dist/src/utils/format-date.d.ts +1 -1
  26. package/dist/src/utils/format-date.js +3 -0
  27. package/dist/src/utils/format-date.js.map +1 -1
  28. package/dist/src/utils/local-date-from-utc.d.ts +9 -0
  29. package/dist/src/utils/local-date-from-utc.js +16 -0
  30. package/dist/src/utils/local-date-from-utc.js.map +1 -0
  31. package/dist/test/collection-browser.test.js +41 -10
  32. package/dist/test/collection-browser.test.js.map +1 -1
  33. package/dist/test/collection-facets/facets-template.test.js +80 -0
  34. package/dist/test/collection-facets/facets-template.test.js.map +1 -1
  35. package/dist/test/tiles/grid/item-tile.test.js +124 -3
  36. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  37. package/dist/test/tiles/list/tile-list-compact.test.js +65 -20
  38. package/dist/test/tiles/list/tile-list-compact.test.js.map +1 -1
  39. package/dist/test/tiles/list/tile-list.test.js +106 -4
  40. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  41. package/dist/test/utils/local-date-from-utc.test.d.ts +1 -0
  42. package/dist/test/utils/local-date-from-utc.test.js +27 -0
  43. package/dist/test/utils/local-date-from-utc.test.js.map +1 -0
  44. package/index.html +1 -0
  45. package/package.json +1 -1
  46. package/src/app-root.ts +12 -0
  47. package/src/collection-browser.ts +294 -112
  48. package/src/collection-facets/facets-template.ts +32 -1
  49. package/src/collection-facets/more-facets-content.ts +4 -1
  50. package/src/collection-facets.ts +1 -8
  51. package/src/empty-placeholder.ts +1 -0
  52. package/src/models.ts +6 -0
  53. package/src/tiles/grid/item-tile.ts +11 -4
  54. package/src/tiles/list/tile-list-compact.ts +16 -2
  55. package/src/tiles/list/tile-list.ts +12 -5
  56. package/src/utils/format-date.ts +4 -0
  57. package/src/utils/local-date-from-utc.ts +15 -0
  58. package/test/collection-browser.test.ts +57 -12
  59. package/test/collection-facets/facets-template.test.ts +98 -0
  60. package/test/tiles/grid/item-tile.test.ts +145 -3
  61. package/test/tiles/list/tile-list-compact.test.ts +70 -19
  62. package/test/tiles/list/tile-list.test.ts +118 -4
  63. package/test/utils/local-date-from-utc.test.ts +37 -0
package/src/app-root.ts CHANGED
@@ -548,6 +548,18 @@ export class AppRoot extends LitElement {
548
548
  --facet-row-border-bottom: 1px solid blue;
549
549
  }
550
550
 
551
+ collection-browser {
552
+ /* Same as production */
553
+ max-width: 135rem;
554
+ margin: auto;
555
+ }
556
+
557
+ #collection-browser-container {
558
+ /* Same as production */
559
+ padding-left: 0.5rem;
560
+ margin-bottom: 2rem;
561
+ }
562
+
551
563
  #base-query-field {
552
564
  width: 300px;
553
565
  }
@@ -55,7 +55,7 @@ import {
55
55
  PrefixFilterType,
56
56
  PrefixFilterCounts,
57
57
  prefixFilterAggregationKeys,
58
- FacetOption,
58
+ FacetEventDetails,
59
59
  } from './models';
60
60
  import {
61
61
  RestorationStateHandlerInterface,
@@ -104,10 +104,6 @@ export class CollectionBrowser
104
104
 
105
105
  @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
106
106
 
107
- @property({ type: String }) titleQuery?: string;
108
-
109
- @property({ type: String }) creatorQuery?: string;
110
-
111
107
  @property({ type: Number }) currentPage?: number;
112
108
 
113
109
  @property({ type: String }) minSelectedDate?: string;
@@ -189,6 +185,8 @@ export class CollectionBrowser
189
185
 
190
186
  @query('#content-container') private contentContainer!: HTMLDivElement;
191
187
 
188
+ @query('#left-column') private leftColumn?: HTMLDivElement;
189
+
192
190
  @property({ type: Object, attribute: false })
193
191
  private analyticsHandler?: AnalyticsManagerInterface;
194
192
 
@@ -209,6 +207,10 @@ export class CollectionBrowser
209
207
  */
210
208
  private isResizeToMobile = false;
211
209
 
210
+ private leftColIntersectionObserver?: IntersectionObserver;
211
+
212
+ private facetsIntersectionObserver?: IntersectionObserver;
213
+
212
214
  private placeholderCellTemplate = html`<collection-browser-loading-tile></collection-browser-loading-tile>`;
213
215
 
214
216
  private tileModelAtCellIndex(index: number): TileModel | undefined {
@@ -294,8 +296,6 @@ export class CollectionBrowser
294
296
  if (letterFilters) {
295
297
  this.selectedTitleFilter = null;
296
298
  this.selectedCreatorFilter = null;
297
- this.titleQuery = undefined;
298
- this.creatorQuery = undefined;
299
299
  }
300
300
 
301
301
  if (sort) {
@@ -339,11 +339,9 @@ export class CollectionBrowser
339
339
  this.placeholderType = null;
340
340
  if (!this.baseQuery?.trim()) {
341
341
  this.placeholderType = 'empty-query';
342
- }
343
-
344
- if (
345
- (!this.searchResultsLoading && this.totalResults === 0) ||
346
- !this.searchService
342
+ } else if (
343
+ !this.searchResultsLoading &&
344
+ (this.totalResults === 0 || !this.searchService)
347
345
  ) {
348
346
  this.placeholderType = 'null-result';
349
347
  }
@@ -370,7 +368,8 @@ export class CollectionBrowser
370
368
  this.searchResultsLoading || this.totalResults === undefined;
371
369
  const resultsCount = this.totalResults?.toLocaleString();
372
370
  const resultsLabel = this.totalResults === 1 ? 'Result' : 'Results';
373
- return html`<div
371
+ return html` <div id="left-column-scroll-sentinel"></div>
372
+ <div
374
373
  id="left-column"
375
374
  class="column${this.isResizeToMobile ? ' preload' : ''}"
376
375
  >
@@ -392,7 +391,9 @@ export class CollectionBrowser
392
391
  : ''}
393
392
  >
394
393
  ${this.facetsTemplate}
394
+ <div id="facets-scroll-sentinel"></div>
395
395
  </div>
396
+ ${this.mobileView ? nothing : html`<div id="facets-bottom-fade"></div>`}
396
397
  </div>
397
398
  <div id="right-column" class="column">
398
399
  ${this.sortFilterBarTemplate}
@@ -497,9 +498,30 @@ export class CollectionBrowser
497
498
  }
498
499
  }
499
500
 
500
- /** Send Analytics when sorting by title's first letter
501
+ /**
502
+ * Returns a query clause identifying the currently selected title filter,
503
+ * e.g., `firstTitle:X`.
504
+ */
505
+ private get titleQuery(): string | undefined {
506
+ return this.selectedTitleFilter
507
+ ? `firstTitle:${this.selectedTitleFilter}`
508
+ : undefined;
509
+ }
510
+
511
+ /**
512
+ * Returns a query clause identifying the currently selected creator filter,
513
+ * e.g., `firstCreator:X`.
514
+ */
515
+ private get creatorQuery(): string | undefined {
516
+ return this.selectedCreatorFilter
517
+ ? `firstCreator:${this.selectedCreatorFilter}`
518
+ : undefined;
519
+ }
520
+
521
+ /**
522
+ * Send Analytics when sorting by title's first letter
501
523
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
502
- * */
524
+ */
503
525
  private sendFilterByTitleAnalytics(prevSelectedLetter: string | null): void {
504
526
  if (!prevSelectedLetter && !this.selectedTitleFilter) {
505
527
  return;
@@ -515,15 +537,10 @@ export class CollectionBrowser
515
537
  });
516
538
  }
517
539
 
518
- private selectedTitleLetterChanged(): void {
519
- this.titleQuery = this.selectedTitleFilter
520
- ? `firstTitle:${this.selectedTitleFilter}`
521
- : undefined;
522
- }
523
-
524
- /** Send Analytics when filtering by creator's first letter
540
+ /**
541
+ * Send Analytics when filtering by creator's first letter
525
542
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
526
- * */
543
+ */
527
544
  private sendFilterByCreatorAnalytics(
528
545
  prevSelectedLetter: string | null
529
546
  ): void {
@@ -541,26 +558,24 @@ export class CollectionBrowser
541
558
  });
542
559
  }
543
560
 
544
- private selectedCreatorLetterChanged(): void {
545
- this.creatorQuery = this.selectedCreatorFilter
546
- ? `firstCreator:${this.selectedCreatorFilter}`
547
- : undefined;
548
- }
549
-
561
+ /**
562
+ * Handler for changes to which letter is selected in the title alphabet bar.
563
+ */
550
564
  private titleLetterSelected(
551
565
  e: CustomEvent<{ selectedLetter: string | null }>
552
566
  ): void {
553
567
  this.selectedCreatorFilter = null;
554
568
  this.selectedTitleFilter = e.detail.selectedLetter;
555
- this.selectedTitleLetterChanged();
556
569
  }
557
570
 
571
+ /**
572
+ * Handler for changes to which letter is selected in the creator alphabet bar.
573
+ */
558
574
  private creatorLetterSelected(
559
575
  e: CustomEvent<{ selectedLetter: string | null }>
560
576
  ): void {
561
577
  this.selectedTitleFilter = null;
562
578
  this.selectedCreatorFilter = e.detail.selectedLetter;
563
- this.selectedCreatorLetterChanged();
564
579
  }
565
580
 
566
581
  private get mobileFacetsTemplate() {
@@ -603,13 +618,13 @@ export class CollectionBrowser
603
618
  .selectedFacets=${this.selectedFacets}
604
619
  .collectionNameCache=${this.collectionNameCache}
605
620
  .showHistogramDatePicker=${this.showHistogramDatePicker}
606
- .query=${this.filteredQuery}
621
+ .query=${this.baseQuery}
607
622
  .filterMap=${this.filterMap}
608
623
  .modalManager=${this.modalManager}
609
624
  ?collapsableFacets=${this.mobileView}
610
625
  ?facetsLoading=${this.facetsLoading}
611
626
  ?fullYearAggregationLoading=${this.facetsLoading}
612
- .onFacetClick=${this.facetClickHandler}
627
+ @facetClick=${this.facetClickHandler}
613
628
  .analyticsHandler=${this.analyticsHandler}
614
629
  >
615
630
  </collection-facets>
@@ -669,6 +684,16 @@ export class CollectionBrowser
669
684
  }
670
685
 
671
686
  updated(changed: PropertyValues) {
687
+ if (changed.has('placeholderType') && this.placeholderType === null) {
688
+ if (!this.leftColIntersectionObserver) {
689
+ this.setupLeftColumnScrollListeners();
690
+ }
691
+ if (!this.facetsIntersectionObserver) {
692
+ this.setupFacetsScrollListeners();
693
+ }
694
+ this.updateLeftColumnHeight();
695
+ }
696
+
672
697
  if (
673
698
  changed.has('displayMode') ||
674
699
  changed.has('baseNavigationUrl') ||
@@ -729,13 +754,11 @@ export class CollectionBrowser
729
754
  this.sendFilterByTitleAnalytics(
730
755
  changed.get('selectedTitleFilter') as string
731
756
  );
732
- this.selectedTitleLetterChanged();
733
757
  }
734
758
  if (changed.has('selectedCreatorFilter')) {
735
759
  this.sendFilterByCreatorAnalytics(
736
760
  changed.get('selectedCreatorFilter') as string
737
761
  );
738
- this.selectedCreatorLetterChanged();
739
762
  }
740
763
 
741
764
  if (
@@ -774,6 +797,10 @@ export class CollectionBrowser
774
797
  if (this.boundNavigationHandler) {
775
798
  window.removeEventListener('popstate', this.boundNavigationHandler);
776
799
  }
800
+
801
+ this.leftColIntersectionObserver?.disconnect();
802
+ this.facetsIntersectionObserver?.disconnect();
803
+ window.removeEventListener('resize', this.updateLeftColumnHeight);
777
804
  }
778
805
 
779
806
  handleResize(entry: ResizeObserverEntry): void {
@@ -785,8 +812,80 @@ export class CollectionBrowser
785
812
  this.isResizeToMobile = true;
786
813
  }
787
814
  }
815
+
816
+ // Ensure the facet sidebar remains sized correctly
817
+ this.updateLeftColumnHeight();
818
+ }
819
+
820
+ /**
821
+ * Sets up listeners for events that may require updating the left column height.
822
+ */
823
+ private setupLeftColumnScrollListeners(): void {
824
+ // We observe intersections between the left column's scroll sentinel and
825
+ // the viewport, so that we can ensure the left column is always sized to
826
+ // match the _available_ viewport height. This should generally be more
827
+ // performant than listening to scroll events on the page or column.
828
+ const leftColumnSentinel = this.shadowRoot?.querySelector(
829
+ '#left-column-scroll-sentinel'
830
+ );
831
+ if (leftColumnSentinel) {
832
+ this.leftColIntersectionObserver = new IntersectionObserver(
833
+ this.updateLeftColumnHeight,
834
+ {
835
+ threshold: [...Array(101).keys()].map(n => n / 100), // Threshold every 1%
836
+ }
837
+ );
838
+ this.leftColIntersectionObserver.observe(leftColumnSentinel);
839
+ }
840
+
841
+ // We also listen for window resize events, as they are not always captured
842
+ // by the resize observer and can affect the desired height of the left column.
843
+ window.addEventListener('resize', this.updateLeftColumnHeight);
844
+ }
845
+
846
+ /**
847
+ * Sets up listeners to control whether the facet sidebar shows its bottom fade-out.
848
+ * Note this uses a separate IntersectionObserver from the left column, because we
849
+ * don't need granular intersection thresholds for this.
850
+ */
851
+ private setupFacetsScrollListeners(): void {
852
+ const facetsSentinel = this.shadowRoot?.querySelector(
853
+ '#facets-scroll-sentinel'
854
+ );
855
+ if (facetsSentinel) {
856
+ this.facetsIntersectionObserver = new IntersectionObserver(
857
+ this.updateFacetFadeOut
858
+ );
859
+ this.facetsIntersectionObserver.observe(facetsSentinel);
860
+ }
788
861
  }
789
862
 
863
+ /**
864
+ * Updates the height of the left column according to its position on the page.
865
+ * Arrow function ensures proper `this` binding.
866
+ */
867
+ private updateLeftColumnHeight = (): void => {
868
+ if (this.mobileView) {
869
+ this.leftColumn?.style?.removeProperty('height');
870
+ } else {
871
+ const clientTop = this.leftColumn?.getBoundingClientRect().top;
872
+ this.leftColumn?.style?.setProperty(
873
+ 'height',
874
+ `${window.innerHeight - (clientTop ?? 0) - 3}px`
875
+ );
876
+ }
877
+ };
878
+
879
+ /**
880
+ * Toggles whether the fade-out is visible at the bottom of the facets.
881
+ * It should only be visible if the facets are not scrolled to the bottom.
882
+ * Arrow function ensures proper `this` binding.
883
+ */
884
+ private updateFacetFadeOut = (entries: IntersectionObserverEntry[]): void => {
885
+ const fadeElmt = this.shadowRoot?.getElementById('facets-bottom-fade');
886
+ fadeElmt?.classList.toggle('hidden', entries?.[0]?.isIntersecting);
887
+ };
888
+
790
889
  private emitBaseQueryChanged() {
791
890
  this.dispatchEvent(
792
891
  new CustomEvent<{ baseQuery?: string }>('baseQueryChanged', {
@@ -952,6 +1051,11 @@ export class CollectionBrowser
952
1051
  this.searchResultsLoading = false;
953
1052
  }
954
1053
 
1054
+ /**
1055
+ * Constructs a search service FilterMap object from the combination of
1056
+ * all the currently-applied filters. This includes any facets, letter
1057
+ * filters, and date range.
1058
+ */
955
1059
  private get filterMap(): FilterMap {
956
1060
  const builder = new FilterMapBuilder();
957
1061
 
@@ -995,21 +1099,24 @@ export class CollectionBrowser
995
1099
  }
996
1100
  }
997
1101
 
998
- const filterMap = builder.build();
999
- return filterMap;
1000
- }
1001
-
1002
- /** The base query joined with any title/creator letter filters */
1003
- private get filteredQuery(): string | undefined {
1004
- if (!this.baseQuery) return undefined;
1005
- let filteredQuery = this.baseQuery.trim();
1006
-
1007
- const { sortFilterQueries } = this;
1008
- if (sortFilterQueries) {
1009
- filteredQuery += ` AND ${sortFilterQueries}`;
1102
+ // Add any letter filters
1103
+ if (this.selectedTitleFilter) {
1104
+ builder.addFilter(
1105
+ 'firstTitle',
1106
+ this.selectedTitleFilter,
1107
+ FilterConstraint.INCLUDE
1108
+ );
1109
+ }
1110
+ if (this.selectedCreatorFilter) {
1111
+ builder.addFilter(
1112
+ 'firstCreator',
1113
+ this.selectedCreatorFilter,
1114
+ FilterConstraint.INCLUDE
1115
+ );
1010
1116
  }
1011
1117
 
1012
- return filteredQuery.trim();
1118
+ const filterMap = builder.build();
1119
+ return filterMap;
1013
1120
  }
1014
1121
 
1015
1122
  /** The full query, including year facets and date range clauses */
@@ -1031,22 +1138,6 @@ export class CollectionBrowser
1031
1138
  return fullQuery.trim();
1032
1139
  }
1033
1140
 
1034
- /** The full query without any title/creator letter filters */
1035
- private get fullQueryWithoutAlphaFilters(): string | undefined {
1036
- if (!this.baseQuery) return undefined;
1037
- let fullQuery = this.baseQuery.trim();
1038
-
1039
- const { facetQuery, dateRangeQueryClause } = this;
1040
-
1041
- if (facetQuery) {
1042
- fullQuery += ` AND ${facetQuery}`;
1043
- }
1044
- if (dateRangeQueryClause) {
1045
- fullQuery += ` AND ${dateRangeQueryClause}`;
1046
- }
1047
- return fullQuery.trim();
1048
- }
1049
-
1050
1141
  /**
1051
1142
  * Generates a query string for the given facets
1052
1143
  *
@@ -1137,36 +1228,39 @@ export class CollectionBrowser
1137
1228
  this.selectedFacets = e.detail;
1138
1229
  }
1139
1230
 
1140
- facetClickHandler(
1141
- name: FacetOption,
1142
- facetSelected: boolean,
1143
- negative: boolean
1144
- ): void {
1231
+ facetClickHandler({
1232
+ detail: { key, state: facetState, negative },
1233
+ }: CustomEvent<FacetEventDetails>): void {
1145
1234
  if (negative) {
1146
1235
  this.analyticsHandler?.sendEvent({
1147
1236
  category: this.searchContext,
1148
- action: facetSelected
1149
- ? analyticsActions.facetNegativeSelected
1150
- : analyticsActions.facetNegativeDeselected,
1151
- label: name,
1237
+ action:
1238
+ facetState !== 'none'
1239
+ ? analyticsActions.facetNegativeSelected
1240
+ : analyticsActions.facetNegativeDeselected,
1241
+ label: key,
1152
1242
  });
1153
1243
  } else {
1154
1244
  this.analyticsHandler?.sendEvent({
1155
1245
  category: this.searchContext,
1156
- action: facetSelected
1157
- ? analyticsActions.facetSelected
1158
- : analyticsActions.facetDeselected,
1159
- label: name,
1246
+ action:
1247
+ facetState !== 'none'
1248
+ ? analyticsActions.facetSelected
1249
+ : analyticsActions.facetDeselected,
1250
+ label: key,
1160
1251
  });
1161
1252
  }
1162
1253
  }
1163
1254
 
1164
1255
  private async fetchFacets() {
1165
- if (!this.filteredQuery) return;
1256
+ const trimmedQuery = this.baseQuery?.trim();
1257
+ if (!trimmedQuery) return;
1166
1258
  if (!this.searchService) return;
1167
1259
 
1260
+ const { facetFetchQueryKey } = this;
1261
+
1168
1262
  const params: SearchParams = {
1169
- query: this.filteredQuery,
1263
+ query: trimmedQuery,
1170
1264
  rows: 0,
1171
1265
  filters: this.filterMap,
1172
1266
  // Fetch a few extra buckets beyond the 6 we show, in case some get suppressed
@@ -1182,7 +1276,13 @@ export class CollectionBrowser
1182
1276
  this.searchType
1183
1277
  );
1184
1278
  const success = searchResponse?.success;
1185
- this.facetsLoading = false;
1279
+
1280
+ // This is checking to see if the query has changed since the data was fetched.
1281
+ // If so, we just want to discard this set of aggregations because they are
1282
+ // likely no longer valid for the newer query.
1283
+ const queryChangedSinceFetch =
1284
+ facetFetchQueryKey !== this.facetFetchQueryKey;
1285
+ if (queryChangedSinceFetch) return;
1186
1286
 
1187
1287
  if (!success) {
1188
1288
  const errorMsg = searchResponse?.error?.message;
@@ -1199,17 +1299,12 @@ export class CollectionBrowser
1199
1299
  return;
1200
1300
  }
1201
1301
 
1202
- // This is checking to see if the query has changed since the data was fetched.
1203
- // If so, we just want to discard this set of aggregations because they are
1204
- // likely no longer valid for the newer query.
1205
- const returnedUid = (success.request.clientParameters as any).uid;
1206
- const queryChangedSinceFetch = returnedUid !== this.facetFetchQueryKey;
1207
- if (queryChangedSinceFetch) return;
1208
-
1209
1302
  this.aggregations = success?.response.aggregations;
1210
1303
 
1211
1304
  this.fullYearsHistogramAggregation =
1212
1305
  success?.response?.aggregations?.year_histogram;
1306
+
1307
+ this.facetsLoading = false;
1213
1308
  }
1214
1309
 
1215
1310
  private scrollToPage(pageNumber: number): Promise<void> {
@@ -1264,7 +1359,8 @@ export class CollectionBrowser
1264
1359
  private pageFetchesInProgress: Record<string, Set<number>> = {};
1265
1360
 
1266
1361
  async fetchPage(pageNumber: number) {
1267
- if (!this.filteredQuery) return;
1362
+ const trimmedQuery = this.baseQuery?.trim();
1363
+ if (!trimmedQuery) return;
1268
1364
  if (!this.searchService) return;
1269
1365
 
1270
1366
  // if we already have data, don't fetch again
@@ -1282,7 +1378,7 @@ export class CollectionBrowser
1282
1378
 
1283
1379
  const sortParams = this.sortParam ? [this.sortParam] : [];
1284
1380
  const params: SearchParams = {
1285
- query: this.filteredQuery,
1381
+ query: trimmedQuery,
1286
1382
  page: pageNumber,
1287
1383
  rows: this.pageSize,
1288
1384
  sort: sortParams,
@@ -1296,6 +1392,12 @@ export class CollectionBrowser
1296
1392
  );
1297
1393
  const success = searchResponse?.success;
1298
1394
 
1395
+ // This is checking to see if the query has changed since the data was fetched.
1396
+ // If so, we just want to discard the data since there should be a new query
1397
+ // right behind it.
1398
+ const queryChangedSinceFetch = pageFetchQueryKey !== this.pageFetchQueryKey;
1399
+ if (queryChangedSinceFetch) return;
1400
+
1299
1401
  if (!success) {
1300
1402
  const errorMsg = searchResponse?.error?.message;
1301
1403
  const detailMsg = searchResponse?.error?.details?.message;
@@ -1310,16 +1412,11 @@ export class CollectionBrowser
1310
1412
  window?.Sentry?.captureMessage?.(this.queryErrorMessage, 'error');
1311
1413
  }
1312
1414
 
1415
+ this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1416
+ this.searchResultsLoading = false;
1313
1417
  return;
1314
1418
  }
1315
1419
 
1316
- // This is checking to see if the query has changed since the data was fetched.
1317
- // If so, we just want to discard the data since there should be a new query
1318
- // right behind it.
1319
- const returnedUid = (success.request.clientParameters as any).uid;
1320
- const queryChangedSinceFetch = returnedUid !== this.pageFetchQueryKey;
1321
- if (queryChangedSinceFetch) return;
1322
-
1323
1420
  this.totalResults = success.response.totalResults;
1324
1421
 
1325
1422
  const { results } = success.response;
@@ -1337,6 +1434,7 @@ export class CollectionBrowser
1337
1434
  this.infiniteScroller.itemCount = this.totalResults;
1338
1435
  }
1339
1436
  }
1437
+
1340
1438
  this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1341
1439
  this.searchResultsLoading = false;
1342
1440
  }
@@ -1442,12 +1540,14 @@ export class CollectionBrowser
1442
1540
  private async fetchPrefixFilterBuckets(
1443
1541
  filterType: PrefixFilterType
1444
1542
  ): Promise<Bucket[]> {
1445
- if (!this.fullQueryWithoutAlphaFilters) return [];
1543
+ const trimmedQuery = this.baseQuery?.trim();
1544
+ if (!trimmedQuery) return [];
1446
1545
 
1447
1546
  const filterAggregationKey = prefixFilterAggregationKeys[filterType];
1448
1547
  const params: SearchParams = {
1449
- query: this.fullQueryWithoutAlphaFilters,
1548
+ query: trimmedQuery,
1450
1549
  rows: 0,
1550
+ filters: this.filterMap,
1451
1551
  // Only fetch the firstTitle or firstCreator aggregation
1452
1552
  aggregations: { simpleParams: [filterAggregationKey] },
1453
1553
  // Fetch all 26 letter buckets
@@ -1468,7 +1568,15 @@ export class CollectionBrowser
1468
1568
  private async updatePrefixFilterCounts(
1469
1569
  filterType: PrefixFilterType
1470
1570
  ): Promise<void> {
1571
+ const { facetFetchQueryKey } = this;
1471
1572
  const buckets = await this.fetchPrefixFilterBuckets(filterType);
1573
+
1574
+ // Don't update the filter counts for an outdated query (if it has been changed
1575
+ // since we sent the request)
1576
+ const queryChangedSinceFetch =
1577
+ facetFetchQueryKey !== this.facetFetchQueryKey;
1578
+ if (queryChangedSinceFetch) return;
1579
+
1472
1580
  // Unpack the aggregation buckets into a simple map like { 'A': 50, 'B': 25, ... }
1473
1581
  this.prefixFilterCountMap = { ...this.prefixFilterCountMap }; // Clone the object to trigger an update
1474
1582
  this.prefixFilterCountMap[filterType] = buckets.reduce(
@@ -1560,6 +1668,9 @@ export class CollectionBrowser
1560
1668
  static styles = css`
1561
1669
  :host {
1562
1670
  display: block;
1671
+
1672
+ --leftColumnWidth: 18rem;
1673
+ --leftColumnPaddingRight: 2.5rem;
1563
1674
  }
1564
1675
 
1565
1676
  /**
@@ -1620,15 +1731,91 @@ export class CollectionBrowser
1620
1731
  }
1621
1732
 
1622
1733
  #left-column {
1623
- width: 18rem;
1624
- min-width: 18rem; /* Prevents Safari from shrinking col at first draw */
1625
- padding-right: 12px;
1626
- padding-right: 2.5rem;
1734
+ width: var(--leftColumnWidth, 18rem);
1735
+ /* Prevents Safari from shrinking col at first draw */
1736
+ min-width: var(--leftColumnWidth, 18rem);
1737
+ padding-top: 0;
1738
+ /* Reduced padding by 0.2rem to add the invisible border in the rule below */
1739
+ padding-right: calc(var(--leftColumnPaddingRight, 2.5rem) - 0.2rem);
1740
+ border-right: 0.2rem solid transparent; /* Pads to the right of the scrollbar a bit */
1627
1741
  z-index: 1;
1628
1742
  }
1629
1743
 
1744
+ .desktop #left-column {
1745
+ top: 0;
1746
+ position: sticky;
1747
+ height: calc(100vh - 2rem);
1748
+ max-height: calc(100vh - 2rem);
1749
+ overflow-x: hidden;
1750
+ overflow-y: scroll;
1751
+
1752
+ /*
1753
+ * Firefox doesn't support any of the -webkit-scrollbar stuff below, but
1754
+ * does at least give us a tiny bit of control over width & color.
1755
+ */
1756
+ scrollbar-width: thin;
1757
+ scrollbar-color: transparent transparent;
1758
+ }
1759
+ .desktop #left-column:hover {
1760
+ scrollbar-color: auto;
1761
+ }
1630
1762
  .desktop #left-column::-webkit-scrollbar {
1631
- display: none;
1763
+ appearance: none;
1764
+ width: 6px;
1765
+ }
1766
+ .desktop #left-column::-webkit-scrollbar-button {
1767
+ height: 3px;
1768
+ background: transparent;
1769
+ }
1770
+ .desktop #left-column::-webkit-scrollbar-corner {
1771
+ background: transparent;
1772
+ }
1773
+ .desktop #left-column::-webkit-scrollbar-thumb {
1774
+ border-radius: 4px;
1775
+ }
1776
+ .desktop #left-column:hover::-webkit-scrollbar-thumb {
1777
+ background: rgba(0, 0, 0, 0.15);
1778
+ }
1779
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:hover {
1780
+ background: rgba(0, 0, 0, 0.2);
1781
+ }
1782
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:active {
1783
+ background: rgba(0, 0, 0, 0.3);
1784
+ }
1785
+
1786
+ #facets-bottom-fade {
1787
+ background: linear-gradient(
1788
+ to bottom,
1789
+ #f5f5f700 0%,
1790
+ #f5f5f7c0 50%,
1791
+ #f5f5f7 80%,
1792
+ #f5f5f7 100%
1793
+ );
1794
+ position: fixed;
1795
+ bottom: 0;
1796
+ height: 50px;
1797
+ /* Wide enough to cover the content, but leave the scrollbar uncovered */
1798
+ width: calc(
1799
+ var(--leftColumnWidth) + var(--leftColumnPaddingRight) - 10px
1800
+ );
1801
+ z-index: 2;
1802
+ pointer-events: none;
1803
+ transition: height 0.1s ease;
1804
+ }
1805
+ #facets-bottom-fade.hidden {
1806
+ height: 0;
1807
+ }
1808
+
1809
+ .desktop #left-column-scroll-sentinel {
1810
+ width: 1px;
1811
+ height: 100vh;
1812
+ background: transparent;
1813
+ }
1814
+
1815
+ .desktop #facets-scroll-sentinel {
1816
+ width: 1px;
1817
+ height: 1px;
1818
+ background: transparent;
1632
1819
  }
1633
1820
 
1634
1821
  .mobile #left-column {
@@ -1636,21 +1823,16 @@ export class CollectionBrowser
1636
1823
  padding: 0;
1637
1824
  }
1638
1825
 
1639
- .desktop #left-column {
1640
- top: 0;
1641
- position: sticky;
1642
- max-height: 100vh;
1643
- overflow: scroll;
1644
- -ms-overflow-style: none; /* hide scrollbar IE and Edge */
1645
- scrollbar-width: none; /* hide scrollbar Firefox */
1646
- }
1647
-
1648
1826
  #mobile-header-container {
1649
1827
  display: flex;
1650
1828
  justify-content: space-between;
1651
1829
  align-items: center;
1652
1830
  }
1653
1831
 
1832
+ .desktop #mobile-header-container {
1833
+ padding-top: 2rem;
1834
+ }
1835
+
1654
1836
  #facets-container {
1655
1837
  position: relative;
1656
1838
  max-height: 0;