@internetarchive/collection-browser 4.2.0-alpha-webdev8164.3 → 4.2.1-alpha-webdev7004.0

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 (88) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.editorconfig +29 -29
  3. package/.github/workflows/ci.yml +27 -27
  4. package/.github/workflows/gh-pages-main.yml +39 -39
  5. package/.github/workflows/npm-publish.yml +39 -39
  6. package/.github/workflows/pr-preview.yml +38 -38
  7. package/.husky/pre-commit +1 -1
  8. package/.prettierignore +1 -1
  9. package/LICENSE +661 -661
  10. package/README.md +83 -83
  11. package/dist/src/app-root.js +4 -0
  12. package/dist/src/app-root.js.map +1 -1
  13. package/dist/src/collection-browser.js +32 -27
  14. package/dist/src/collection-browser.js.map +1 -1
  15. package/dist/src/collection-facets/facets-template.js +0 -5
  16. package/dist/src/collection-facets/facets-template.js.map +1 -1
  17. package/dist/src/collection-facets/more-facets-content.d.ts +8 -106
  18. package/dist/src/collection-facets/more-facets-content.js +103 -612
  19. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  20. package/dist/src/collection-facets/more-facets-pagination.d.ts +3 -12
  21. package/dist/src/collection-facets/more-facets-pagination.js +9 -71
  22. package/dist/src/collection-facets/more-facets-pagination.js.map +1 -1
  23. package/dist/src/collection-facets/toggle-switch.js +0 -1
  24. package/dist/src/collection-facets/toggle-switch.js.map +1 -1
  25. package/dist/src/collection-facets.js +9 -10
  26. package/dist/src/collection-facets.js.map +1 -1
  27. package/dist/src/data-source/collection-browser-data-source.d.ts +7 -0
  28. package/dist/src/data-source/collection-browser-data-source.js +27 -10
  29. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  30. package/dist/src/mediatype/mediatype-config.js +1 -1
  31. package/dist/src/mediatype/mediatype-config.js.map +1 -1
  32. package/dist/src/models.d.ts +12 -2
  33. package/dist/src/models.js +13 -8
  34. package/dist/src/models.js.map +1 -1
  35. package/dist/src/restoration-state-handler.js +9 -3
  36. package/dist/src/restoration-state-handler.js.map +1 -1
  37. package/dist/src/tiles/hover/hover-pane-controller.js +2 -1
  38. package/dist/src/tiles/hover/hover-pane-controller.js.map +1 -1
  39. package/dist/src/tiles/tile-dispatcher.d.ts +6 -0
  40. package/dist/src/tiles/tile-dispatcher.js +11 -3
  41. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  42. package/dist/test/collection-browser.test.js +72 -0
  43. package/dist/test/collection-browser.test.js.map +1 -1
  44. package/dist/test/collection-facets/more-facets-content.test.js +3 -212
  45. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  46. package/dist/test/collection-facets/more-facets-pagination.test.js +3 -63
  47. package/dist/test/collection-facets/more-facets-pagination.test.js.map +1 -1
  48. package/dist/test/data-source/collection-browser-data-source.test.js +52 -0
  49. package/dist/test/data-source/collection-browser-data-source.test.js.map +1 -1
  50. package/dist/test/mocks/mock-search-responses.d.ts +0 -5
  51. package/dist/test/mocks/mock-search-responses.js +0 -44
  52. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  53. package/dist/test/mocks/mock-search-service.js +1 -2
  54. package/dist/test/mocks/mock-search-service.js.map +1 -1
  55. package/dist/test/tiles/tile-dispatcher.test.js +14 -0
  56. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -1
  57. package/dist/test/tiles/tile-mediatype-icon.test.js +4 -4
  58. package/dist/test/tiles/tile-mediatype-icon.test.js.map +1 -1
  59. package/eslint.config.mjs +53 -53
  60. package/index.html +24 -24
  61. package/local.archive.org.cert +86 -86
  62. package/local.archive.org.key +27 -27
  63. package/package.json +120 -121
  64. package/renovate.json +6 -6
  65. package/src/app-root.ts +4 -0
  66. package/src/collection-browser.ts +43 -36
  67. package/src/collection-facets/facets-template.ts +0 -5
  68. package/src/collection-facets/more-facets-content.ts +113 -662
  69. package/src/collection-facets/more-facets-pagination.ts +10 -84
  70. package/src/collection-facets/toggle-switch.ts +0 -1
  71. package/src/collection-facets.ts +13 -10
  72. package/src/data-source/collection-browser-data-source.ts +31 -10
  73. package/src/mediatype/mediatype-config.ts +1 -1
  74. package/src/models.ts +30 -8
  75. package/src/restoration-state-handler.ts +7 -3
  76. package/src/tiles/hover/hover-pane-controller.ts +2 -1
  77. package/src/tiles/tile-dispatcher.ts +12 -3
  78. package/test/collection-browser.test.ts +105 -0
  79. package/test/collection-facets/more-facets-content.test.ts +4 -326
  80. package/test/collection-facets/more-facets-pagination.test.ts +3 -87
  81. package/test/data-source/collection-browser-data-source.test.ts +62 -0
  82. package/test/mocks/mock-search-responses.ts +0 -48
  83. package/test/mocks/mock-search-service.ts +0 -2
  84. package/test/tiles/tile-dispatcher.test.ts +17 -0
  85. package/test/tiles/tile-mediatype-icon.test.ts +4 -4
  86. package/tsconfig.json +25 -25
  87. package/web-dev-server.config.mjs +30 -30
  88. package/web-test-runner.config.mjs +52 -52
@@ -7,9 +7,7 @@ import {
7
7
  PropertyValues,
8
8
  TemplateResult,
9
9
  } from 'lit';
10
- import { customElement, property, query, state } from 'lit/decorators.js';
11
- import { classMap } from 'lit/directives/class-map.js';
12
- import { when } from 'lit/directives/when.js';
10
+ import { customElement, property, state } from 'lit/decorators.js';
13
11
  import {
14
12
  Aggregation,
15
13
  Bucket,
@@ -42,16 +40,13 @@ import type {
42
40
  TVChannelAliases,
43
41
  } from '../data-source/models';
44
42
  import '@internetarchive/elements/ia-status-indicator/ia-status-indicator';
43
+ import './more-facets-pagination';
45
44
  import './facets-template';
46
45
  import {
47
46
  analyticsActions,
48
47
  analyticsCategories,
49
48
  } from '../utils/analytics-events';
50
49
  import './toggle-switch';
51
- import './more-facets-pagination';
52
- import '@internetarchive/ia-clearable-text-input';
53
- import arrowLeftIcon from '../assets/img/icons/arrow-left';
54
- import arrowRightIcon from '../assets/img/icons/arrow-right';
55
50
  import { srOnlyStyle } from '../styles/sr-only';
56
51
  import {
57
52
  mergeSelectedFacets,
@@ -63,12 +58,6 @@ import {
63
58
  MORE_FACETS__MAX_AGGREGATIONS,
64
59
  } from './models';
65
60
 
66
- /**
67
- * Threshold for switching from horizontal scroll to pagination.
68
- * If facet count >= this value, use pagination. Otherwise use horizontal scroll.
69
- */
70
- const PAGINATION_THRESHOLD = 1000;
71
-
72
61
  @customElement('more-facets-content')
73
62
  export class MoreFacetsContent extends LitElement {
74
63
  @property({ type: String }) facetKey?: FacetOption;
@@ -137,46 +126,10 @@ export class MoreFacetsContent extends LitElement {
137
126
  getDefaultSelectedFacets();
138
127
 
139
128
  /**
140
- * Text entered by the user to filter facet buckets.
141
- * Applied to bucket.key for case-insensitive matching.
142
- */
143
- @state() private filterText = '';
144
-
145
- /**
146
- * Current page number for pagination (when facet count >= PAGINATION_THRESHOLD).
129
+ * Which page of facets we are showing.
147
130
  */
148
131
  @state() private pageNumber = 1;
149
132
 
150
- /**
151
- * Whether the component is narrow enough to warrant compact pagination.
152
- * Updated via a ResizeObserver-based container query approach.
153
- */
154
- @state() private isCompactView = false;
155
-
156
- /** Column gap in px — matches the --facetsColumnGap default (never overridden). */
157
- private static readonly COLUMN_GAP = 15;
158
-
159
- /** Column count derived from the same breakpoint as the CSS media query. */
160
- private get columnCount(): number {
161
- return this.isCompactView ? 1 : 3;
162
- }
163
-
164
- /**
165
- * Whether the horizontal scroll is at the leftmost position.
166
- */
167
- @state() private atScrollStart = true;
168
-
169
- /**
170
- * Whether the horizontal scroll is at the rightmost position.
171
- */
172
- @state() private atScrollEnd = true;
173
-
174
- @query('ia-clearable-text-input')
175
- private filterInput!: HTMLElement;
176
-
177
- @query('.facets-content')
178
- private facetsContentEl!: HTMLElement;
179
-
180
133
  willUpdate(changed: PropertyValues): void {
181
134
  if (
182
135
  changed.has('aggregations') ||
@@ -190,13 +143,6 @@ export class MoreFacetsContent extends LitElement {
190
143
  this.facetGroup = this.mergedFacets;
191
144
  }
192
145
 
193
- // Reset to page 1 when filter text changes (only matters for pagination mode)
194
- if (changed.has('filterText')) {
195
- this.pageNumber = 1;
196
- }
197
- }
198
-
199
- updated(changed: PropertyValues): void {
200
146
  // If any of the search properties change, it triggers a facet fetch
201
147
  if (
202
148
  changed.has('facetKey') ||
@@ -213,196 +159,24 @@ export class MoreFacetsContent extends LitElement {
213
159
 
214
160
  this.updateSpecificFacets();
215
161
  }
216
-
217
- // Reset horizontal scroll when filter text changes (e.g., switching from
218
- // horizontal-scroll mode back to pagination mode)
219
- if (changed.has('filterText')) {
220
- const facetsContent = this.shadowRoot?.querySelector('.facets-content');
221
- if (facetsContent) {
222
- facetsContent.scrollLeft = 0;
223
- }
224
- }
225
-
226
- // Manage scroll listener for horizontal scroll mode arrows.
227
- // Only re-evaluate when properties that affect the displayed content change.
228
- if (
229
- changed.has('filterText') ||
230
- changed.has('aggregations') ||
231
- changed.has('facetKey') ||
232
- changed.has('sortedBy') ||
233
- changed.has('selectedFacets') ||
234
- changed.has('unappliedFacetChanges')
235
- ) {
236
- if (!this.usePagination) {
237
- this.attachScrollListener();
238
- // Refresh scroll state whenever content may have changed (e.g., filtering)
239
- requestAnimationFrame(() => this.updateScrollState());
240
- } else {
241
- this.removeScrollListener();
242
- }
243
- }
244
162
  }
245
163
 
246
- private resizeObserver?: ResizeObserver;
247
-
248
164
  firstUpdated(): void {
249
165
  this.setupEscapeListeners();
250
- this.setupCompactViewObserver();
251
- this.constrainToScrollContainer();
252
- }
253
-
254
- disconnectedCallback(): void {
255
- super.disconnectedCallback();
256
- this.resizeObserver?.disconnect();
257
- this.removeScrollListener();
258
- document.removeEventListener('keydown', this.escapeHandler);
259
- }
260
-
261
- private scrollHandler = () => this.updateScrollState();
262
-
263
- private scrollListenerAttached = false;
264
-
265
- private scrollListenerTarget?: HTMLElement;
266
-
267
- /**
268
- * Attaches a scroll event listener to the facets content element
269
- * to track horizontal scroll position for arrow button states.
270
- */
271
- private attachScrollListener(): void {
272
- if (this.scrollListenerAttached || !this.facetsContentEl) return;
273
- this.scrollListenerTarget = this.facetsContentEl;
274
- this.scrollListenerTarget.addEventListener('scroll', this.scrollHandler, {
275
- passive: true,
276
- });
277
- this.scrollListenerAttached = true;
278
- // Defer initial state check until after browser layout, so scrollWidth
279
- // reflects the actual content dimensions.
280
- requestAnimationFrame(() => this.updateScrollState());
281
- }
282
-
283
- private removeScrollListener(): void {
284
- if (!this.scrollListenerAttached || !this.scrollListenerTarget) return;
285
- this.scrollListenerTarget.removeEventListener('scroll', this.scrollHandler);
286
- this.scrollListenerTarget = undefined;
287
- this.scrollListenerAttached = false;
288
- }
289
-
290
- /**
291
- * Updates the scroll arrow disabled states based on current scroll position.
292
- */
293
- private updateScrollState(): void {
294
- const el = this.facetsContentEl;
295
- if (!el) return;
296
- this.atScrollStart = el.scrollLeft <= 0;
297
- this.atScrollEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
298
- }
299
-
300
- /**
301
- * Calculates the width of one column step (column width + gap) based on
302
- * the CSS multi-column layout of the scroll container.
303
- */
304
- private getColumnStep(): number {
305
- const el = this.facetsContentEl;
306
- if (!el) return 0;
307
- // Column step = column width + gap = (visible width + gap) / column count
308
- return (el.clientWidth + MoreFacetsContent.COLUMN_GAP) / this.columnCount;
309
- }
310
-
311
- /**
312
- * Snaps a scroll target to the nearest column boundary.
313
- */
314
- private snapToColumn(target: number): number {
315
- const step = this.getColumnStep();
316
- if (step <= 0) return target;
317
- return Math.round(target / step) * step;
318
- }
319
-
320
- /**
321
- * Scrolls the facet content left by approximately one page, snapping to
322
- * the nearest column boundary.
323
- */
324
- private onScrollLeft(): void {
325
- const el = this.facetsContentEl;
326
- if (!el) return;
327
- const rawTarget = el.scrollLeft - el.clientWidth;
328
- const snapped = Math.max(0, this.snapToColumn(rawTarget));
329
- el.scrollTo({ left: snapped, behavior: 'smooth' });
330
- }
331
-
332
- /**
333
- * Scrolls the facet content right by approximately one page, snapping to
334
- * the nearest column boundary.
335
- */
336
- private onScrollRight(): void {
337
- const el = this.facetsContentEl;
338
- if (!el) return;
339
- const maxScroll = el.scrollWidth - el.clientWidth;
340
- const rawTarget = el.scrollLeft + el.clientWidth;
341
- const snapped = Math.min(maxScroll, this.snapToColumn(rawTarget));
342
- el.scrollTo({ left: snapped, behavior: 'smooth' });
343
- }
344
-
345
- /**
346
- * Sets up a ResizeObserver to toggle compact pagination based on component width.
347
- */
348
- private setupCompactViewObserver(): void {
349
- this.resizeObserver = new ResizeObserver(entries => {
350
- for (const entry of entries) {
351
- const compact = entry.contentRect.width <= 560;
352
- if (this.isCompactView !== compact) this.isCompactView = compact;
353
- }
354
- });
355
- this.resizeObserver.observe(this);
356
- }
357
-
358
- /**
359
- * Constrains the section's max-height to fit within the nearest
360
- * scroll-container ancestor (e.g., the modal's content area).
361
- * This is a safety net for cases where the CSS max-height calculation
362
- * doesn't perfectly match the container's available space.
363
- */
364
- private constrainToScrollContainer(): void {
365
- requestAnimationFrame(() => {
366
- const section = this.shadowRoot?.querySelector(
367
- 'section#more-facets',
368
- ) as HTMLElement;
369
- if (!section) return;
370
-
371
- // Walk up from the assigned slot to find the nearest overflow container
372
- let el = this.assignedSlot?.parentElement;
373
- while (el) {
374
- const cs = getComputedStyle(el);
375
- if (
376
- cs.overflowY === 'auto' ||
377
- cs.overflowY === 'scroll' ||
378
- cs.overflowY === 'hidden'
379
- ) {
380
- const containerBottom = el.getBoundingClientRect().bottom;
381
- const sectionTop = section.getBoundingClientRect().top;
382
- const available = containerBottom - sectionTop;
383
- // Compare against the CSS max-height rather than actual height,
384
- // since content may not have loaded yet at firstUpdated time
385
- const computedMax = parseFloat(getComputedStyle(section).maxHeight);
386
- if (available > 0 && available < computedMax) {
387
- section.style.maxHeight = `${available}px`;
388
- }
389
- return;
390
- }
391
- el = el.parentElement;
392
- }
393
- });
394
166
  }
395
167
 
396
168
  /**
397
169
  * Close more facets modal on Escape click
398
170
  */
399
- private escapeHandler = (e: KeyboardEvent) => {
400
- if (e.key === 'Escape') this.modalManager?.closeModal();
401
- };
402
-
403
171
  private setupEscapeListeners() {
404
172
  if (this.modalManager) {
405
- document.addEventListener('keydown', this.escapeHandler);
173
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
174
+ if (e.key === 'Escape') {
175
+ this.modalManager?.closeModal();
176
+ }
177
+ });
178
+ } else {
179
+ document.removeEventListener('keydown', () => {});
406
180
  }
407
181
  }
408
182
 
@@ -441,21 +215,34 @@ export class MoreFacetsContent extends LitElement {
441
215
  rows: 0, // todo - do we want server-side pagination with offset/page/limit flag?
442
216
  };
443
217
 
444
- try {
445
- const results = await this.searchService?.search(params, this.searchType);
446
- this.aggregations = results?.success?.response.aggregations;
218
+ const results = await this.searchService?.search(params, this.searchType);
219
+ this.aggregations = results?.success?.response.aggregations;
220
+ this.facetsLoading = false;
447
221
 
448
- const collectionTitles = results?.success?.response?.collectionTitles;
449
- if (collectionTitles) {
450
- for (const [id, title] of Object.entries(collectionTitles)) {
451
- this.collectionTitles?.set(id, title);
452
- }
222
+ const collectionTitles = results?.success?.response?.collectionTitles;
223
+ if (collectionTitles) {
224
+ for (const [id, title] of Object.entries(collectionTitles)) {
225
+ this.collectionTitles?.set(id, title);
453
226
  }
454
- } finally {
455
- this.facetsLoading = false;
456
227
  }
457
228
  }
458
229
 
230
+ /**
231
+ * Handler for page number changes from the pagination widget.
232
+ */
233
+ private pageNumberClicked(e: CustomEvent<{ page: number }>) {
234
+ const page = e?.detail?.page;
235
+ if (page) {
236
+ this.pageNumber = Number(page);
237
+ }
238
+
239
+ this.analyticsHandler?.sendEvent({
240
+ category: analyticsCategories.default,
241
+ action: analyticsActions.moreFacetsPageChange,
242
+ label: `${this.pageNumber}`,
243
+ });
244
+ }
245
+
459
246
  /**
460
247
  * Combines the selected facets with the aggregations to create a single list of facets
461
248
  */
@@ -537,10 +324,7 @@ export class MoreFacetsContent extends LitElement {
537
324
 
538
325
  const buckets: FacetBucket[] = Object.entries(selectedFacetsForKey).map(
539
326
  ([value, data]) => {
540
- const displayText =
541
- (this.facetKey === 'collection'
542
- ? this.collectionTitles?.get(value)
543
- : undefined) ?? value;
327
+ const displayText: string = value;
544
328
  return {
545
329
  displayText,
546
330
  key: value,
@@ -584,33 +368,17 @@ export class MoreFacetsContent extends LitElement {
584
368
  });
585
369
  }
586
370
 
587
- // Construct the array of facet buckets from the aggregation buckets,
588
- // using collection display titles where available.
371
+ // Construct the array of facet buckets from the aggregation buckets
589
372
  const facetBuckets: FacetBucket[] = sortedBuckets.map(bucket => {
590
373
  const bucketKeyStr = `${bucket.key}`;
591
- const displayText =
592
- (this.facetKey === 'collection'
593
- ? this.collectionTitles?.get(bucketKeyStr)
594
- : undefined) ?? bucketKeyStr;
595
374
  return {
596
- displayText,
375
+ displayText: `${bucketKeyStr}`,
597
376
  key: `${bucketKeyStr}`,
598
377
  count: bucket.doc_count,
599
378
  state: 'none',
600
379
  };
601
380
  });
602
381
 
603
- // For collection facets sorted alphabetically, re-sort by display title
604
- // instead of the raw identifier used by getSortedBuckets.
605
- if (
606
- this.facetKey === 'collection' &&
607
- this.sortedBy === AggregationSortType.ALPHABETICAL
608
- ) {
609
- facetBuckets.sort((a, b) =>
610
- (a.displayText ?? a.key).localeCompare(b.displayText ?? b.key),
611
- );
612
- }
613
-
614
382
  return {
615
383
  title: facetGroupTitle,
616
384
  key: this.facetKey,
@@ -619,81 +387,29 @@ export class MoreFacetsContent extends LitElement {
619
387
  }
620
388
 
621
389
  /**
622
- * Returns the facet group with buckets filtered by the current filter text.
623
- * Filters are applied to the full bucket list before pagination.
390
+ * Returns a FacetGroup representing only the current page of facet buckets to show.
624
391
  */
625
- private get filteredFacetGroup(): FacetGroup | undefined {
626
- const { facetGroup, filterText } = this;
392
+ private get facetGroupForCurrentPage(): FacetGroup | undefined {
393
+ const { facetGroup } = this;
627
394
  if (!facetGroup) return undefined;
628
395
 
629
- // If no filter text, return the full group
630
- if (!filterText.trim()) {
631
- return facetGroup;
632
- }
633
-
634
- // Filter buckets by the text the user actually sees.
635
- // For collections, match against the displayed collection title (not the identifier).
636
- // For other facet types, match against the bucket key (which is also the display text).
637
- const lowerFilter = filterText.toLowerCase().trim();
638
- const filteredBuckets = facetGroup.buckets.filter(bucket => {
639
- const displayText = this.collectionTitles?.get(bucket.key) ?? bucket.key;
640
- return displayText.toLowerCase().includes(lowerFilter);
641
- });
396
+ // Slice out only the current page of facet buckets
397
+ const firstBucketIndexOnPage = (this.pageNumber - 1) * this.facetsPerPage;
398
+ const truncatedBuckets = facetGroup.buckets.slice(
399
+ firstBucketIndexOnPage,
400
+ firstBucketIndexOnPage + this.facetsPerPage,
401
+ );
642
402
 
643
403
  return {
644
404
  ...facetGroup,
645
- buckets: filteredBuckets,
646
- };
647
- }
648
-
649
- /**
650
- * Determines whether to use pagination based on the number of filtered facets.
651
- * Returns true if facet count >= PAGINATION_THRESHOLD, false otherwise.
652
- */
653
- private get usePagination(): boolean {
654
- const facetCount = this.filteredFacetGroup?.buckets.length ?? 0;
655
- return facetCount >= PAGINATION_THRESHOLD;
656
- }
657
-
658
- /**
659
- * Returns the facet group for the current page.
660
- * If using pagination (>= 1000 facets), slices to show only the current page.
661
- * Otherwise, returns all facets for horizontal scrolling.
662
- */
663
- private get facetGroupForCurrentPage(): FacetGroup | undefined {
664
- const filteredGroup = this.filteredFacetGroup;
665
- if (!filteredGroup) return undefined;
666
-
667
- // If facet count is below threshold, show all facets with horizontal scroll
668
- if (!this.usePagination) {
669
- return filteredGroup;
670
- }
671
-
672
- // Otherwise, use pagination - slice to current page
673
- const startIndex = (this.pageNumber - 1) * this.facetsPerPage;
674
- const endIndex = startIndex + this.facetsPerPage;
675
- const slicedBuckets = filteredGroup.buckets.slice(startIndex, endIndex);
676
-
677
- return {
678
- ...filteredGroup,
679
- buckets: slicedBuckets,
405
+ buckets: truncatedBuckets,
680
406
  };
681
407
  }
682
408
 
683
409
  private get moreFacetsTemplate(): TemplateResult {
684
- const facetGroup = this.facetGroupForCurrentPage;
685
-
686
- // Show empty state if filtering returned no results
687
- if (
688
- this.filterText.trim() &&
689
- (!facetGroup || facetGroup.buckets.length === 0)
690
- ) {
691
- return this.emptyFilterResultsTemplate;
692
- }
693
-
694
410
  return html`
695
411
  <facets-template
696
- .facetGroup=${facetGroup}
412
+ .facetGroup=${this.facetGroupForCurrentPage}
697
413
  .selectedFacets=${this.selectedFacets}
698
414
  .collectionTitles=${this.collectionTitles}
699
415
  @facetClick=${(e: CustomEvent<FacetEventDetails>) => {
@@ -718,51 +434,50 @@ export class MoreFacetsContent extends LitElement {
718
434
  `;
719
435
  }
720
436
 
721
- private get emptyFilterResultsTemplate(): TemplateResult {
722
- return html`
723
- <div class="empty-results">
724
- <p>${msg('No matching values found.')}</p>
725
- <p class="hint">${msg('Try a different search term.')}</p>
726
- </div>
727
- `;
728
- }
729
-
730
437
  /**
731
- * Number of pages for pagination (only used when facet count >= PAGINATION_THRESHOLD).
438
+ * How many pages of facets to show in the modal pagination widget
732
439
  */
733
440
  private get paginationSize(): number {
734
- const filteredBuckets = this.filteredFacetGroup?.buckets ?? [];
735
- return Math.ceil(filteredBuckets.length / this.facetsPerPage);
441
+ if (!this.aggregations || !this.facetKey) return 0;
442
+
443
+ // Calculate the appropriate number of pages to show in the modal pagination widget
444
+ const length = this.aggregations[this.facetKey]?.buckets.length;
445
+ return Math.ceil(length / this.facetsPerPage);
736
446
  }
737
447
 
738
- /**
739
- * Template for pagination component.
740
- */
448
+ // render pagination if more then 1 page
741
449
  private get facetsPaginationTemplate() {
742
- return html`<more-facets-pagination
743
- .size=${this.paginationSize}
744
- .currentPage=${this.pageNumber}
745
- .compact=${this.isCompactView}
746
- @pageNumberClicked=${this.pageNumberClicked}
747
- ></more-facets-pagination>`;
450
+ return this.paginationSize > 1
451
+ ? html`<more-facets-pagination
452
+ .size=${this.paginationSize}
453
+ .currentPage=${1}
454
+ @pageNumberClicked=${this.pageNumberClicked}
455
+ ></more-facets-pagination>`
456
+ : nothing;
748
457
  }
749
458
 
750
459
  private get footerTemplate() {
751
- return html`
752
- ${when(this.usePagination, () => this.facetsPaginationTemplate)}
753
- <div class="footer">
754
- <button class="btn btn-cancel" type="button" @click=${this.cancelClick}>
755
- Cancel
756
- </button>
757
- <button
758
- class="btn btn-submit"
759
- type="button"
760
- @click=${this.applySearchFacetsClicked}
761
- >
762
- Apply filters
763
- </button>
764
- </div>
765
- `;
460
+ if (this.paginationSize > 0) {
461
+ return html`${this.facetsPaginationTemplate}
462
+ <div class="footer">
463
+ <button
464
+ class="btn btn-cancel"
465
+ type="button"
466
+ @click=${this.cancelClick}
467
+ >
468
+ Cancel
469
+ </button>
470
+ <button
471
+ class="btn btn-submit"
472
+ type="button"
473
+ @click=${this.applySearchFacetsClicked}
474
+ >
475
+ Apply filters
476
+ </button>
477
+ </div> `;
478
+ }
479
+
480
+ return nothing;
766
481
  }
767
482
 
768
483
  private sortFacetAggregation(facetSortType: AggregationSortType) {
@@ -772,44 +487,6 @@ export class MoreFacetsContent extends LitElement {
772
487
  );
773
488
  }
774
489
 
775
- /**
776
- * Handler for filter input changes. Updates the filter text and triggers re-render.
777
- */
778
- private handleFilterInput(e: Event): void {
779
- const input = e.target as HTMLElement & { value: string };
780
- this.filterText = input.value;
781
- }
782
-
783
- /**
784
- * Handler for when the filter input is cleared via the clear button.
785
- */
786
- private handleFilterClear(): void {
787
- this.filterText = '';
788
- }
789
-
790
- /**
791
- * Handler for pagination page number clicks.
792
- * Only used when facet count >= PAGINATION_THRESHOLD.
793
- */
794
- private pageNumberClicked(e: CustomEvent<{ page: number }>) {
795
- this.pageNumber = e.detail.page;
796
-
797
- // Track page navigation in analytics
798
- this.analyticsHandler?.sendEvent({
799
- category: analyticsCategories.default,
800
- action: analyticsActions.moreFacetsPageChange,
801
- label: `${this.pageNumber}`,
802
- });
803
-
804
- this.dispatchEvent(
805
- new CustomEvent('pageChanged', {
806
- detail: this.pageNumber,
807
- bubbles: true,
808
- composed: true,
809
- }),
810
- );
811
- }
812
-
813
490
  private get modalHeaderTemplate(): TemplateResult {
814
491
  const facetSort =
815
492
  this.sortedBy ?? defaultFacetSort[this.facetKey as FacetOption];
@@ -817,103 +494,36 @@ export class MoreFacetsContent extends LitElement {
817
494
  facetSort === AggregationSortType.COUNT ? 'left' : 'right';
818
495
 
819
496
  return html`<span class="sr-only">${msg('More facets for:')}</span>
820
- <span class="title"> ${this.facetGroup?.title} </span>
821
- <span class="header-controls">
822
- <span class="sort-controls">
823
- <label class="sort-label">${msg('Sort by:')}</label>
824
- ${this.facetKey
825
- ? html`<toggle-switch
826
- class="sort-toggle"
827
- leftValue=${AggregationSortType.COUNT}
828
- leftLabel="Count"
829
- rightValue=${valueFacetSort[this.facetKey]}
830
- .rightLabel=${this.facetGroup?.title}
831
- side=${defaultSwitchSide}
832
- @change=${(e: CustomEvent<string>) => {
833
- this.sortFacetAggregation(
834
- Number(e.detail) as AggregationSortType,
835
- );
836
- }}
837
- ></toggle-switch>`
838
- : nothing}
839
- </span>
840
-
841
- <span class="filter-controls">
842
- <label class="filter-label">${msg('Filter by:')}</label>
843
- <ia-clearable-text-input
844
- class="filter-input"
845
- .value=${this.filterText}
846
- .placeholder=${msg('Search...')}
847
- .screenReaderLabel=${msg('Filter facets')}
848
- .clearButtonScreenReaderLabel=${msg('Clear filter')}
849
- @input=${this.handleFilterInput}
850
- @clear=${this.handleFilterClear}
851
- ></ia-clearable-text-input>
852
- </span>
497
+ <span class="title">
498
+ ${this.facetGroup?.title}
499
+
500
+ <label class="sort-label">${msg('Sort by:')}</label>
501
+ ${this.facetKey
502
+ ? html`<toggle-switch
503
+ class="sort-toggle"
504
+ leftValue=${AggregationSortType.COUNT}
505
+ leftLabel="Count"
506
+ rightValue=${valueFacetSort[this.facetKey]}
507
+ .rightLabel=${this.facetGroup?.title}
508
+ side=${defaultSwitchSide}
509
+ @change=${(e: CustomEvent<string>) => {
510
+ this.sortFacetAggregation(
511
+ Number(e.detail) as AggregationSortType,
512
+ );
513
+ }}
514
+ ></toggle-switch>`
515
+ : nothing}
853
516
  </span>`;
854
517
  }
855
518
 
856
- private get horizontalScrollTemplate(): TemplateResult {
857
- const contentClasses = classMap({
858
- 'facets-content': true,
859
- 'horizontal-scroll-mode': true,
860
- });
861
- const showArrows = !this.atScrollStart || !this.atScrollEnd;
862
-
863
- return html`<div class="scroll-nav-container">
864
- ${when(
865
- showArrows,
866
- () =>
867
- html`<button
868
- class="scroll-arrow scroll-left"
869
- @click=${this.onScrollLeft}
870
- ?disabled=${this.atScrollStart}
871
- aria-label="Scroll facets left"
872
- >
873
- ${arrowLeftIcon}
874
- </button>`,
875
- )}
876
- <div class=${contentClasses}>
877
- <div class="facets-horizontal-container">
878
- ${this.moreFacetsTemplate}
879
- </div>
880
- </div>
881
- ${when(
882
- showArrows,
883
- () =>
884
- html`<button
885
- class="scroll-arrow scroll-right"
886
- @click=${this.onScrollRight}
887
- ?disabled=${this.atScrollEnd}
888
- aria-label="Scroll facets right"
889
- >
890
- ${arrowRightIcon}
891
- </button>`,
892
- )}
893
- </div>`;
894
- }
895
-
896
519
  render() {
897
- const sectionClasses = classMap({
898
- 'pagination-mode': this.usePagination,
899
- 'horizontal-scroll-mode': !this.usePagination,
900
- });
901
- const contentClasses = classMap({
902
- 'facets-content': true,
903
- 'pagination-mode': this.usePagination,
904
- });
905
-
906
520
  return html`
907
521
  ${this.facetsLoading
908
522
  ? this.loaderTemplate
909
523
  : html`
910
- <section id="more-facets" class=${sectionClasses}>
524
+ <section id="more-facets">
911
525
  <div class="header-content">${this.modalHeaderTemplate}</div>
912
- ${this.usePagination
913
- ? html`<div class=${contentClasses}>
914
- ${this.moreFacetsTemplate}
915
- </div>`
916
- : this.horizontalScrollTemplate}
526
+ <div class="facets-content">${this.moreFacetsTemplate}</div>
917
527
  ${this.footerTemplate}
918
528
  </section>
919
529
  `}
@@ -936,9 +546,6 @@ export class MoreFacetsContent extends LitElement {
936
546
  // Reset the unapplied changes back to default, now that they have been applied
937
547
  this.unappliedFacetChanges = getDefaultSelectedFacets();
938
548
 
939
- // Reset filter text
940
- this.filterText = '';
941
-
942
549
  this.modalManager?.closeModal();
943
550
  this.analyticsHandler?.sendEvent({
944
551
  category: analyticsCategories.default,
@@ -951,9 +558,6 @@ export class MoreFacetsContent extends LitElement {
951
558
  // Reset the unapplied changes back to default
952
559
  this.unappliedFacetChanges = getDefaultSelectedFacets();
953
560
 
954
- // Reset filter text
955
- this.filterText = '';
956
-
957
561
  this.modalManager?.closeModal();
958
562
  this.analyticsHandler?.sendEvent({
959
563
  category: analyticsCategories.default,
@@ -969,26 +573,10 @@ export class MoreFacetsContent extends LitElement {
969
573
  srOnlyStyle,
970
574
  css`
971
575
  section#more-facets {
972
- display: flex;
973
- flex-direction: column;
974
- max-height: calc(100vh - 16.5rem - var(--modalBottomMargin, 2.5rem));
975
- padding: 10px;
976
- box-sizing: border-box;
576
+ overflow: auto;
577
+ padding: 10px; /* leaves room for scroll bar to appear without overlaying on content */
977
578
  --facetsColumnCount: 3;
978
579
  }
979
-
980
- /* Both modes need a height constraint for proper column flow */
981
- section#more-facets.horizontal-scroll-mode,
982
- section#more-facets.pagination-mode {
983
- --facetsMaxHeight: 280px;
984
- }
985
- .header-content {
986
- flex-shrink: 0;
987
- position: relative;
988
- z-index: 1;
989
- background: #fff;
990
- }
991
-
992
580
  .header-content .title {
993
581
  display: block;
994
582
  text-align: left;
@@ -997,22 +585,8 @@ export class MoreFacetsContent extends LitElement {
997
585
  font-weight: bold;
998
586
  }
999
587
 
1000
- .header-controls {
1001
- display: flex;
1002
- flex-wrap: wrap;
1003
- align-items: center;
1004
- gap: 8px 20px;
1005
- padding: 0 10px 8px;
1006
- }
1007
-
1008
- .sort-controls {
1009
- display: inline-flex;
1010
- align-items: center;
1011
- white-space: nowrap;
1012
- gap: 5px;
1013
- }
1014
-
1015
588
  .sort-label {
589
+ margin-left: 20px;
1016
590
  font-size: 1.3rem;
1017
591
  }
1018
592
 
@@ -1020,115 +594,11 @@ export class MoreFacetsContent extends LitElement {
1020
594
  font-weight: normal;
1021
595
  }
1022
596
 
1023
- .filter-controls {
1024
- display: inline-flex;
1025
- align-items: center;
1026
- white-space: nowrap;
1027
- }
1028
-
1029
- .filter-label {
1030
- font-size: 1.3rem;
1031
- }
1032
-
1033
- .filter-input {
1034
- --input-height: 2.5rem;
1035
- --input-font-size: 1.3rem;
1036
- --input-border-radius: 4px;
1037
- --input-padding: 4px 8px;
1038
- --input-focused-border-color: ${modalSubmitButton};
1039
- width: 150px;
1040
- margin-left: 5px;
1041
- }
1042
-
1043
- .empty-results {
1044
- text-align: center;
1045
- padding: 40px 20px;
1046
- color: #666;
1047
- }
1048
-
1049
- .empty-results .hint {
1050
- margin-top: 10px;
1051
- }
1052
-
1053
597
  .facets-content {
1054
598
  font-size: 1.2rem;
1055
- flex: 1 1 auto;
1056
- min-height: 0;
1057
- overflow-y: auto;
1058
- overflow-x: hidden;
599
+ max-height: 300px;
600
+ overflow: auto;
1059
601
  padding: 10px;
1060
- /* Force scrollbar to always be visible */
1061
- scrollbar-width: thin; /* Firefox */
1062
- scrollbar-color: #888 #f1f1f1; /* Firefox - thumb and track colors */
1063
- }
1064
-
1065
- /* Horizontal scroll mode: horizontal scrolling only */
1066
- .facets-content.horizontal-scroll-mode {
1067
- overflow-x: auto;
1068
- overflow-y: hidden;
1069
- }
1070
-
1071
- /* Webkit browsers scrollbar styling - always visible */
1072
- .facets-content::-webkit-scrollbar {
1073
- width: 12px; /* Vertical scrollbar width */
1074
- height: 12px; /* Horizontal scrollbar height */
1075
- }
1076
-
1077
- .facets-content::-webkit-scrollbar-track {
1078
- background: #f1f1f1;
1079
- border-radius: 6px;
1080
- }
1081
-
1082
- .facets-content::-webkit-scrollbar-thumb {
1083
- background: #888;
1084
- border-radius: 6px;
1085
- min-height: 30px; /* Ensure thumb is always visible when scrolling is possible */
1086
- }
1087
-
1088
- .facets-content::-webkit-scrollbar-thumb:hover {
1089
- background: #555;
1090
- }
1091
-
1092
- /* Force corner to match track color */
1093
- .facets-content::-webkit-scrollbar-corner {
1094
- background: #f1f1f1;
1095
- }
1096
-
1097
- .facets-horizontal-container {
1098
- display: inline-block;
1099
- min-width: 100%;
1100
- /* Allow natural width expansion based on content */
1101
- width: fit-content;
1102
- }
1103
-
1104
- .scroll-nav-container {
1105
- display: flex;
1106
- align-items: center;
1107
- flex: 1 1 auto;
1108
- min-height: 0;
1109
- }
1110
-
1111
- .scroll-nav-container .facets-content {
1112
- flex: 1 1 auto;
1113
- min-width: 0;
1114
- }
1115
-
1116
- .scroll-arrow {
1117
- background: none;
1118
- border: none;
1119
- cursor: pointer;
1120
- padding: 5px;
1121
- flex-shrink: 0;
1122
- }
1123
-
1124
- .scroll-arrow svg {
1125
- height: 14px;
1126
- fill: #2c2c2c;
1127
- }
1128
-
1129
- .scroll-arrow:disabled {
1130
- opacity: 0.3;
1131
- cursor: default;
1132
602
  }
1133
603
  .facets-loader {
1134
604
  --icon-width: 70px;
@@ -1144,7 +614,6 @@ export class MoreFacetsContent extends LitElement {
1144
614
  width: auto;
1145
615
  border-radius: 4px;
1146
616
  cursor: pointer;
1147
- font-family: inherit;
1148
617
  }
1149
618
  .btn-cancel {
1150
619
  background-color: #2c2c2c;
@@ -1154,37 +623,19 @@ export class MoreFacetsContent extends LitElement {
1154
623
  background-color: ${modalSubmitButton};
1155
624
  color: white;
1156
625
  }
1157
- more-facets-pagination {
1158
- flex-shrink: 0;
1159
- }
1160
-
1161
626
  .footer {
1162
627
  text-align: center;
1163
628
  margin-top: 10px;
1164
- flex-shrink: 0;
1165
629
  }
1166
630
 
1167
631
  @media (max-width: 560px) {
1168
- section#more-facets.horizontal-scroll-mode,
1169
- section#more-facets.pagination-mode {
1170
- --facetsColumnCount: 1; /* Single column on mobile */
1171
- --facetsMaxHeight: none; /* Remove fixed height for vertical scrolling */
632
+ section#more-facets {
633
+ max-height: 450px;
634
+ --facetsColumnCount: 1;
1172
635
  }
1173
- /* On mobile, always use vertical scrolling regardless of mode */
1174
- .facets-content,
1175
- .facets-content.horizontal-scroll-mode {
636
+ .facets-content {
1176
637
  overflow-y: auto;
1177
- overflow-x: hidden;
1178
- }
1179
- .scroll-nav-container {
1180
- display: contents; /* Remove wrapper from layout so section flex-column works */
1181
- }
1182
- .scroll-arrow {
1183
- display: none;
1184
- }
1185
- .filter-input {
1186
- width: 120px;
1187
- --input-font-size: 1.2rem;
638
+ height: 300px;
1188
639
  }
1189
640
  }
1190
641
  `,