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