@internetarchive/collection-browser 4.1.0 → 4.2.0-alpha-webdev8164.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 (62) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +27 -27
  3. package/.github/workflows/gh-pages-main.yml +39 -39
  4. package/.github/workflows/npm-publish.yml +39 -39
  5. package/.github/workflows/pr-preview.yml +38 -38
  6. package/.husky/pre-commit +1 -1
  7. package/.prettierignore +1 -1
  8. package/LICENSE +661 -661
  9. package/README.md +83 -83
  10. package/dist/src/collection-browser.js +761 -761
  11. package/dist/src/collection-browser.js.map +1 -1
  12. package/dist/src/collection-facets/facets-template.js +5 -0
  13. package/dist/src/collection-facets/facets-template.js.map +1 -1
  14. package/dist/src/collection-facets/more-facets-content.d.ts +92 -8
  15. package/dist/src/collection-facets/more-facets-content.js +526 -84
  16. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  17. package/dist/src/collection-facets/more-facets-pagination.d.ts +12 -3
  18. package/dist/src/collection-facets/more-facets-pagination.js +69 -8
  19. package/dist/src/collection-facets/more-facets-pagination.js.map +1 -1
  20. package/dist/src/collection-facets/toggle-switch.js +1 -0
  21. package/dist/src/collection-facets/toggle-switch.js.map +1 -1
  22. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  23. package/dist/src/data-source/collection-browser-query-state.js.map +1 -1
  24. package/dist/src/sort-filter-bar/sort-filter-bar.js +280 -280
  25. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  26. package/dist/test/collection-browser.test.js +189 -189
  27. package/dist/test/collection-browser.test.js.map +1 -1
  28. package/dist/test/collection-facets/more-facets-content.test.js +162 -3
  29. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  30. package/dist/test/collection-facets/more-facets-pagination.test.js +63 -3
  31. package/dist/test/collection-facets/more-facets-pagination.test.js.map +1 -1
  32. package/dist/test/mocks/mock-search-responses.d.ts +5 -0
  33. package/dist/test/mocks/mock-search-responses.js +44 -0
  34. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  35. package/dist/test/mocks/mock-search-service.js +2 -1
  36. package/dist/test/mocks/mock-search-service.js.map +1 -1
  37. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +22 -22
  38. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  39. package/eslint.config.mjs +53 -53
  40. package/index.html +24 -24
  41. package/local.archive.org.cert +86 -86
  42. package/local.archive.org.key +27 -27
  43. package/package.json +121 -120
  44. package/renovate.json +6 -6
  45. package/src/collection-browser.ts +3070 -3070
  46. package/src/collection-facets/facets-template.ts +5 -0
  47. package/src/collection-facets/more-facets-content.ts +566 -96
  48. package/src/collection-facets/more-facets-pagination.ts +80 -9
  49. package/src/collection-facets/toggle-switch.ts +1 -0
  50. package/src/data-source/collection-browser-data-source.ts +1444 -1444
  51. package/src/data-source/collection-browser-query-state.ts +60 -60
  52. package/src/sort-filter-bar/sort-filter-bar.ts +733 -733
  53. package/test/collection-browser.test.ts +2402 -2402
  54. package/test/collection-facets/more-facets-content.test.ts +251 -4
  55. package/test/collection-facets/more-facets-pagination.test.ts +87 -3
  56. package/test/mocks/mock-search-responses.ts +48 -0
  57. package/test/mocks/mock-search-service.ts +2 -0
  58. package/test/sort-filter-bar/sort-filter-bar.test.ts +443 -443
  59. package/tsconfig.json +25 -25
  60. package/web-dev-server.config.mjs +30 -30
  61. package/web-test-runner.config.mjs +52 -52
  62. package/.claude/settings.local.json +0 -8
@@ -1,17 +1,27 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { css, html, LitElement, nothing, } from 'lit';
3
- import { customElement, property, state } from 'lit/decorators.js';
3
+ import { customElement, property, query, state } from 'lit/decorators.js';
4
+ import { classMap } from 'lit/directives/class-map.js';
5
+ import { when } from 'lit/directives/when.js';
4
6
  import { SearchType, AggregationSortType, } from '@internetarchive/search-service';
5
7
  import { msg } from '@lit/localize';
6
8
  import { facetTitles, suppressedCollections, valueFacetSort, defaultFacetSort, getDefaultSelectedFacets, tvMoreFacetSort, } from '../models';
7
9
  import '@internetarchive/elements/ia-status-indicator/ia-status-indicator';
8
- import './more-facets-pagination';
9
10
  import './facets-template';
10
11
  import { analyticsActions, analyticsCategories, } from '../utils/analytics-events';
11
12
  import './toggle-switch';
13
+ import './more-facets-pagination';
14
+ import '@internetarchive/ia-clearable-text-input';
15
+ import arrowLeftIcon from '../assets/img/icons/arrow-left';
16
+ import arrowRightIcon from '../assets/img/icons/arrow-right';
12
17
  import { srOnlyStyle } from '../styles/sr-only';
13
18
  import { mergeSelectedFacets, sortBucketsBySelectionState, updateSelectedFacetBucket, } from '../utils/facet-utils';
14
19
  import { MORE_FACETS__DEFAULT_PAGE_SIZE, MORE_FACETS__MAX_AGGREGATIONS, } from './models';
20
+ /**
21
+ * Threshold for switching from horizontal scroll to pagination.
22
+ * If facet count >= this value, use pagination. Otherwise use horizontal scroll.
23
+ */
24
+ const PAGINATION_THRESHOLD = 1000;
15
25
  let MoreFacetsContent = class MoreFacetsContent extends LitElement {
16
26
  constructor() {
17
27
  super(...arguments);
@@ -34,9 +44,29 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
34
44
  */
35
45
  this.unappliedFacetChanges = getDefaultSelectedFacets();
36
46
  /**
37
- * Which page of facets we are showing.
47
+ * Text entered by the user to filter facet buckets.
48
+ * Applied to bucket.key for case-insensitive matching.
49
+ */
50
+ this.filterText = '';
51
+ /**
52
+ * Current page number for pagination (when facet count >= PAGINATION_THRESHOLD).
38
53
  */
39
54
  this.pageNumber = 1;
55
+ /**
56
+ * Whether the component is narrow enough to warrant compact pagination.
57
+ * Updated via a ResizeObserver-based container query approach.
58
+ */
59
+ this.isCompactView = false;
60
+ /**
61
+ * Whether the horizontal scroll is at the leftmost position.
62
+ */
63
+ this.atScrollStart = true;
64
+ /**
65
+ * Whether the horizontal scroll is at the rightmost position.
66
+ */
67
+ this.atScrollEnd = false;
68
+ this.scrollHandler = () => this.updateScrollState();
69
+ this.scrollListenerAttached = false;
40
70
  }
41
71
  willUpdate(changed) {
42
72
  if (changed.has('aggregations') ||
@@ -48,6 +78,12 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
48
78
  // store it for reuse across pages.
49
79
  this.facetGroup = this.mergedFacets;
50
80
  }
81
+ // Reset to page 1 when filter text changes (only matters for pagination mode)
82
+ if (changed.has('filterText')) {
83
+ this.pageNumber = 1;
84
+ }
85
+ }
86
+ updated(changed) {
51
87
  // If any of the search properties change, it triggers a facet fetch
52
88
  if (changed.has('facetKey') ||
53
89
  changed.has('query') ||
@@ -61,9 +97,134 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
61
97
  : defaultFacetSort[this.facetKey];
62
98
  this.updateSpecificFacets();
63
99
  }
100
+ // Reset horizontal scroll when filter text changes (e.g., switching from
101
+ // horizontal-scroll mode back to pagination mode)
102
+ if (changed.has('filterText')) {
103
+ const facetsContent = this.shadowRoot?.querySelector('.facets-content');
104
+ if (facetsContent) {
105
+ facetsContent.scrollLeft = 0;
106
+ }
107
+ }
108
+ // Manage scroll listener for horizontal scroll mode arrows.
109
+ // Only re-evaluate when properties that affect the displayed content change.
110
+ if (changed.has('filterText') ||
111
+ changed.has('aggregations') ||
112
+ changed.has('facetKey') ||
113
+ changed.has('sortedBy') ||
114
+ changed.has('selectedFacets') ||
115
+ changed.has('unappliedFacetChanges')) {
116
+ if (!this.usePagination) {
117
+ this.attachScrollListener();
118
+ // Refresh scroll state whenever content may have changed (e.g., filtering)
119
+ requestAnimationFrame(() => this.updateScrollState());
120
+ }
121
+ else {
122
+ this.removeScrollListener();
123
+ }
124
+ }
64
125
  }
65
126
  firstUpdated() {
66
127
  this.setupEscapeListeners();
128
+ this.setupCompactViewObserver();
129
+ }
130
+ disconnectedCallback() {
131
+ super.disconnectedCallback();
132
+ this.resizeObserver?.disconnect();
133
+ this.removeScrollListener();
134
+ }
135
+ /**
136
+ * Attaches a scroll event listener to the facets content element
137
+ * to track horizontal scroll position for arrow button states.
138
+ */
139
+ attachScrollListener() {
140
+ if (this.scrollListenerAttached || !this.facetsContentEl)
141
+ return;
142
+ this.facetsContentEl.addEventListener('scroll', this.scrollHandler, {
143
+ passive: true,
144
+ });
145
+ this.scrollListenerAttached = true;
146
+ // Defer initial state check until after browser layout, so scrollWidth
147
+ // reflects the actual content dimensions.
148
+ requestAnimationFrame(() => this.updateScrollState());
149
+ }
150
+ removeScrollListener() {
151
+ if (!this.scrollListenerAttached || !this.facetsContentEl)
152
+ return;
153
+ this.facetsContentEl.removeEventListener('scroll', this.scrollHandler);
154
+ this.scrollListenerAttached = false;
155
+ }
156
+ /**
157
+ * Updates the scroll arrow disabled states based on current scroll position.
158
+ */
159
+ updateScrollState() {
160
+ const el = this.facetsContentEl;
161
+ if (!el)
162
+ return;
163
+ this.atScrollStart = el.scrollLeft <= 0;
164
+ this.atScrollEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
165
+ }
166
+ /**
167
+ * Calculates the width of one column step (column width + gap) based on
168
+ * the CSS multi-column layout of the scroll container.
169
+ */
170
+ getColumnStep() {
171
+ const el = this.facetsContentEl;
172
+ if (!el)
173
+ return 0;
174
+ const facetRows = el.querySelector('.facet-rows');
175
+ const styles = facetRows
176
+ ? getComputedStyle(facetRows)
177
+ : getComputedStyle(el);
178
+ const columnCount = parseInt(styles.columnCount, 10) || 3;
179
+ const columnGap = parseInt(styles.columnGap, 10) || 15;
180
+ // Column width = (visible width - total gaps) / column count
181
+ // Column step = column width + gap = (visible width + gap) / column count
182
+ return (el.clientWidth + columnGap) / columnCount;
183
+ }
184
+ /**
185
+ * Snaps a scroll target to the nearest column boundary.
186
+ */
187
+ snapToColumn(target) {
188
+ const step = this.getColumnStep();
189
+ if (step <= 0)
190
+ return target;
191
+ return Math.round(target / step) * step;
192
+ }
193
+ /**
194
+ * Scrolls the facet content left by approximately one page, snapping to
195
+ * the nearest column boundary.
196
+ */
197
+ onScrollLeft() {
198
+ const el = this.facetsContentEl;
199
+ if (!el)
200
+ return;
201
+ const rawTarget = el.scrollLeft - el.clientWidth;
202
+ const snapped = Math.max(0, this.snapToColumn(rawTarget));
203
+ el.scrollTo({ left: snapped, behavior: 'smooth' });
204
+ }
205
+ /**
206
+ * Scrolls the facet content right by approximately one page, snapping to
207
+ * the nearest column boundary.
208
+ */
209
+ onScrollRight() {
210
+ const el = this.facetsContentEl;
211
+ if (!el)
212
+ return;
213
+ const maxScroll = el.scrollWidth - el.clientWidth;
214
+ const rawTarget = el.scrollLeft + el.clientWidth;
215
+ const snapped = Math.min(maxScroll, this.snapToColumn(rawTarget));
216
+ el.scrollTo({ left: snapped, behavior: 'smooth' });
217
+ }
218
+ /**
219
+ * Sets up a ResizeObserver to toggle compact pagination based on component width.
220
+ */
221
+ setupCompactViewObserver() {
222
+ this.resizeObserver = new ResizeObserver(entries => {
223
+ for (const entry of entries) {
224
+ this.isCompactView = entry.contentRect.width <= 560;
225
+ }
226
+ });
227
+ this.resizeObserver.observe(this);
67
228
  }
68
229
  /**
69
230
  * Close more facets modal on Escape click
@@ -122,20 +283,6 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
122
283
  }
123
284
  }
124
285
  }
125
- /**
126
- * Handler for page number changes from the pagination widget.
127
- */
128
- pageNumberClicked(e) {
129
- const page = e?.detail?.page;
130
- if (page) {
131
- this.pageNumber = Number(page);
132
- }
133
- this.analyticsHandler?.sendEvent({
134
- category: analyticsCategories.default,
135
- action: analyticsActions.moreFacetsPageChange,
136
- label: `${this.pageNumber}`,
137
- });
138
- }
139
286
  /**
140
287
  * Combines the selected facets with the aggregations to create a single list of facets
141
288
  */
@@ -253,24 +400,70 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
253
400
  };
254
401
  }
255
402
  /**
256
- * Returns a FacetGroup representing only the current page of facet buckets to show.
403
+ * Returns the facet group with buckets filtered by the current filter text.
404
+ * Filters are applied to the full bucket list before pagination.
257
405
  */
258
- get facetGroupForCurrentPage() {
259
- const { facetGroup } = this;
406
+ get filteredFacetGroup() {
407
+ const { facetGroup, filterText } = this;
260
408
  if (!facetGroup)
261
409
  return undefined;
262
- // Slice out only the current page of facet buckets
263
- const firstBucketIndexOnPage = (this.pageNumber - 1) * this.facetsPerPage;
264
- const truncatedBuckets = facetGroup.buckets.slice(firstBucketIndexOnPage, firstBucketIndexOnPage + this.facetsPerPage);
410
+ // If no filter text, return the full group
411
+ if (!filterText.trim()) {
412
+ return facetGroup;
413
+ }
414
+ // Filter buckets by the text the user actually sees.
415
+ // For collections, match against the displayed collection title (not the identifier).
416
+ // For other facet types, match against the bucket key (which is also the display text).
417
+ const lowerFilter = filterText.toLowerCase().trim();
418
+ const filteredBuckets = facetGroup.buckets.filter(bucket => {
419
+ const displayText = this.collectionTitles?.get(bucket.key) ?? bucket.key;
420
+ return displayText.toLowerCase().includes(lowerFilter);
421
+ });
265
422
  return {
266
423
  ...facetGroup,
267
- buckets: truncatedBuckets,
424
+ buckets: filteredBuckets,
425
+ };
426
+ }
427
+ /**
428
+ * Determines whether to use pagination based on the number of filtered facets.
429
+ * Returns true if facet count >= PAGINATION_THRESHOLD, false otherwise.
430
+ */
431
+ get usePagination() {
432
+ const facetCount = this.filteredFacetGroup?.buckets.length ?? 0;
433
+ return facetCount >= PAGINATION_THRESHOLD;
434
+ }
435
+ /**
436
+ * Returns the facet group for the current page.
437
+ * If using pagination (>= 1000 facets), slices to show only the current page.
438
+ * Otherwise, returns all facets for horizontal scrolling.
439
+ */
440
+ get facetGroupForCurrentPage() {
441
+ const filteredGroup = this.filteredFacetGroup;
442
+ if (!filteredGroup)
443
+ return undefined;
444
+ // If facet count is below threshold, show all facets with horizontal scroll
445
+ if (!this.usePagination) {
446
+ return filteredGroup;
447
+ }
448
+ // Otherwise, use pagination - slice to current page
449
+ const startIndex = (this.pageNumber - 1) * this.facetsPerPage;
450
+ const endIndex = startIndex + this.facetsPerPage;
451
+ const slicedBuckets = filteredGroup.buckets.slice(startIndex, endIndex);
452
+ return {
453
+ ...filteredGroup,
454
+ buckets: slicedBuckets,
268
455
  };
269
456
  }
270
457
  get moreFacetsTemplate() {
458
+ const facetGroup = this.facetGroupForCurrentPage;
459
+ // Show empty state if filtering returned no results
460
+ if (this.filterText.trim() &&
461
+ (!facetGroup || facetGroup.buckets.length === 0)) {
462
+ return this.emptyFilterResultsTemplate;
463
+ }
271
464
  return html `
272
465
  <facets-template
273
- .facetGroup=${this.facetGroupForCurrentPage}
466
+ .facetGroup=${facetGroup}
274
467
  .selectedFacets=${this.selectedFacets}
275
468
  .collectionTitles=${this.collectionTitles}
276
469
  @facetClick=${(e) => {
@@ -289,83 +482,160 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
289
482
  ></ia-status-indicator>
290
483
  `;
291
484
  }
485
+ get emptyFilterResultsTemplate() {
486
+ return html `
487
+ <div class="empty-results">
488
+ <p>${msg('No matching values found.')}</p>
489
+ <p class="hint">${msg('Try a different search term.')}</p>
490
+ </div>
491
+ `;
492
+ }
292
493
  /**
293
- * How many pages of facets to show in the modal pagination widget
494
+ * Number of pages for pagination (only used when facet count >= PAGINATION_THRESHOLD).
294
495
  */
295
496
  get paginationSize() {
296
- if (!this.aggregations || !this.facetKey)
297
- return 0;
298
- // Calculate the appropriate number of pages to show in the modal pagination widget
299
- const length = this.aggregations[this.facetKey]?.buckets.length;
300
- return Math.ceil(length / this.facetsPerPage);
497
+ const filteredBuckets = this.filteredFacetGroup?.buckets ?? [];
498
+ return Math.ceil(filteredBuckets.length / this.facetsPerPage);
301
499
  }
302
- // render pagination if more then 1 page
500
+ /**
501
+ * Template for pagination component.
502
+ */
303
503
  get facetsPaginationTemplate() {
304
- return this.paginationSize > 1
305
- ? html `<more-facets-pagination
306
- .size=${this.paginationSize}
307
- .currentPage=${1}
308
- @pageNumberClicked=${this.pageNumberClicked}
309
- ></more-facets-pagination>`
310
- : nothing;
504
+ return html `<more-facets-pagination
505
+ .size=${this.paginationSize}
506
+ .currentPage=${this.pageNumber}
507
+ .compact=${this.isCompactView}
508
+ @pageNumberClicked=${this.pageNumberClicked}
509
+ ></more-facets-pagination>`;
311
510
  }
312
511
  get footerTemplate() {
313
- if (this.paginationSize > 0) {
314
- return html `${this.facetsPaginationTemplate}
315
- <div class="footer">
316
- <button
317
- class="btn btn-cancel"
318
- type="button"
319
- @click=${this.cancelClick}
320
- >
321
- Cancel
322
- </button>
323
- <button
324
- class="btn btn-submit"
325
- type="button"
326
- @click=${this.applySearchFacetsClicked}
327
- >
328
- Apply filters
329
- </button>
330
- </div> `;
331
- }
332
- return nothing;
512
+ return html `
513
+ ${when(this.usePagination, () => this.facetsPaginationTemplate)}
514
+ <div class="footer">
515
+ <button class="btn btn-cancel" type="button" @click=${this.cancelClick}>
516
+ Cancel
517
+ </button>
518
+ <button
519
+ class="btn btn-submit"
520
+ type="button"
521
+ @click=${this.applySearchFacetsClicked}
522
+ >
523
+ Apply filters
524
+ </button>
525
+ </div>
526
+ `;
333
527
  }
334
528
  sortFacetAggregation(facetSortType) {
335
529
  this.sortedBy = facetSortType;
336
530
  this.dispatchEvent(new CustomEvent('sortedFacets', { detail: this.sortedBy }));
337
531
  }
532
+ /**
533
+ * Handler for filter input changes. Updates the filter text and triggers re-render.
534
+ */
535
+ handleFilterInput(e) {
536
+ const input = e.target;
537
+ this.filterText = input.value;
538
+ }
539
+ /**
540
+ * Handler for when the filter input is cleared via the clear button.
541
+ */
542
+ handleFilterClear() {
543
+ this.filterText = '';
544
+ }
545
+ /**
546
+ * Handler for pagination page number clicks.
547
+ * Only used when facet count >= PAGINATION_THRESHOLD.
548
+ */
549
+ pageNumberClicked(e) {
550
+ this.pageNumber = e.detail.page;
551
+ // Track page navigation in analytics
552
+ this.analyticsHandler?.sendEvent({
553
+ category: analyticsCategories.default,
554
+ action: analyticsActions.moreFacetsPageChange,
555
+ label: `${this.pageNumber}`,
556
+ });
557
+ this.dispatchEvent(new CustomEvent('pageChanged', { detail: this.pageNumber }));
558
+ }
338
559
  get modalHeaderTemplate() {
339
560
  const facetSort = this.sortedBy ?? defaultFacetSort[this.facetKey];
340
561
  const defaultSwitchSide = facetSort === AggregationSortType.COUNT ? 'left' : 'right';
341
562
  return html `<span class="sr-only">${msg('More facets for:')}</span>
342
- <span class="title">
343
- ${this.facetGroup?.title}
344
-
345
- <label class="sort-label">${msg('Sort by:')}</label>
346
- ${this.facetKey
563
+ <span class="title"> ${this.facetGroup?.title} </span>
564
+ <span class="header-controls">
565
+ <span class="sort-controls">
566
+ <label class="sort-label">${msg('Sort by:')}</label>
567
+ ${this.facetKey
347
568
  ? html `<toggle-switch
348
- class="sort-toggle"
349
- leftValue=${AggregationSortType.COUNT}
350
- leftLabel="Count"
351
- rightValue=${valueFacetSort[this.facetKey]}
352
- .rightLabel=${this.facetGroup?.title}
353
- side=${defaultSwitchSide}
354
- @change=${(e) => {
569
+ class="sort-toggle"
570
+ leftValue=${AggregationSortType.COUNT}
571
+ leftLabel="Count"
572
+ rightValue=${valueFacetSort[this.facetKey]}
573
+ .rightLabel=${this.facetGroup?.title}
574
+ side=${defaultSwitchSide}
575
+ @change=${(e) => {
355
576
  this.sortFacetAggregation(Number(e.detail));
356
577
  }}
357
- ></toggle-switch>`
578
+ ></toggle-switch>`
358
579
  : nothing}
580
+ </span>
581
+
582
+ <span class="filter-controls">
583
+ <label class="filter-label">${msg('Filter by:')}</label>
584
+ <ia-clearable-text-input
585
+ class="filter-input"
586
+ .value=${this.filterText}
587
+ .placeholder=${msg('Search...')}
588
+ .screenReaderLabel=${msg('Filter facets')}
589
+ .clearButtonScreenReaderLabel=${msg('Clear filter')}
590
+ @input=${this.handleFilterInput}
591
+ @clear=${this.handleFilterClear}
592
+ ></ia-clearable-text-input>
593
+ </span>
359
594
  </span>`;
360
595
  }
361
596
  render() {
597
+ const sectionClasses = classMap({
598
+ 'pagination-mode': this.usePagination,
599
+ 'horizontal-scroll-mode': !this.usePagination,
600
+ });
601
+ const contentClasses = classMap({
602
+ 'facets-content': true,
603
+ 'pagination-mode': this.usePagination,
604
+ 'horizontal-scroll-mode': !this.usePagination,
605
+ });
362
606
  return html `
363
607
  ${this.facetsLoading
364
608
  ? this.loaderTemplate
365
609
  : html `
366
- <section id="more-facets">
610
+ <section id="more-facets" class=${sectionClasses}>
367
611
  <div class="header-content">${this.modalHeaderTemplate}</div>
368
- <div class="facets-content">${this.moreFacetsTemplate}</div>
612
+ ${this.usePagination
613
+ ? html `<div class=${contentClasses}>
614
+ ${this.moreFacetsTemplate}
615
+ </div>`
616
+ : html `<div class="scroll-nav-container">
617
+ ${when(!this.atScrollStart || !this.atScrollEnd, () => html `<button
618
+ class="scroll-arrow scroll-left"
619
+ @click=${this.onScrollLeft}
620
+ ?disabled=${this.atScrollStart}
621
+ aria-label="Scroll facets left"
622
+ >
623
+ ${arrowLeftIcon}
624
+ </button>`)}
625
+ <div class=${contentClasses}>
626
+ <div class="facets-horizontal-container">
627
+ ${this.moreFacetsTemplate}
628
+ </div>
629
+ </div>
630
+ ${when(!this.atScrollStart || !this.atScrollEnd, () => html `<button
631
+ class="scroll-arrow scroll-right"
632
+ @click=${this.onScrollRight}
633
+ ?disabled=${this.atScrollEnd}
634
+ aria-label="Scroll facets right"
635
+ >
636
+ ${arrowRightIcon}
637
+ </button>`)}
638
+ </div>`}
369
639
  ${this.footerTemplate}
370
640
  </section>
371
641
  `}
@@ -381,6 +651,8 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
381
651
  this.dispatchEvent(event);
382
652
  // Reset the unapplied changes back to default, now that they have been applied
383
653
  this.unappliedFacetChanges = getDefaultSelectedFacets();
654
+ // Reset filter text
655
+ this.filterText = '';
384
656
  this.modalManager?.closeModal();
385
657
  this.analyticsHandler?.sendEvent({
386
658
  category: analyticsCategories.default,
@@ -391,6 +663,8 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
391
663
  cancelClick() {
392
664
  // Reset the unapplied changes back to default
393
665
  this.unappliedFacetChanges = getDefaultSelectedFacets();
666
+ // Reset filter text
667
+ this.filterText = '';
394
668
  this.modalManager?.closeModal();
395
669
  this.analyticsHandler?.sendEvent({
396
670
  category: analyticsCategories.default,
@@ -404,10 +678,23 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
404
678
  srOnlyStyle,
405
679
  css `
406
680
  section#more-facets {
407
- overflow: auto;
408
- padding: 10px; /* leaves room for scroll bar to appear without overlaying on content */
681
+ display: flex;
682
+ flex-direction: column;
683
+ max-height: calc(100vh - 16.5rem);
684
+ padding: 10px;
685
+ box-sizing: border-box;
409
686
  --facetsColumnCount: 3;
410
687
  }
688
+
689
+ /* Both modes need a height constraint for proper column flow */
690
+ section#more-facets.horizontal-scroll-mode,
691
+ section#more-facets.pagination-mode {
692
+ --facetsMaxHeight: 280px;
693
+ }
694
+ .header-content {
695
+ flex-shrink: 0;
696
+ }
697
+
411
698
  .header-content .title {
412
699
  display: block;
413
700
  text-align: left;
@@ -416,8 +703,22 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
416
703
  font-weight: bold;
417
704
  }
418
705
 
706
+ .header-controls {
707
+ display: flex;
708
+ flex-wrap: wrap;
709
+ align-items: center;
710
+ gap: 4px 20px;
711
+ padding: 0 10px;
712
+ }
713
+
714
+ .sort-controls {
715
+ display: inline-flex;
716
+ align-items: center;
717
+ white-space: nowrap;
718
+ gap: 5px;
719
+ }
720
+
419
721
  .sort-label {
420
- margin-left: 20px;
421
722
  font-size: 1.3rem;
422
723
  }
423
724
 
@@ -425,11 +726,115 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
425
726
  font-weight: normal;
426
727
  }
427
728
 
729
+ .filter-controls {
730
+ display: inline-flex;
731
+ align-items: center;
732
+ white-space: nowrap;
733
+ }
734
+
735
+ .filter-label {
736
+ font-size: 1.3rem;
737
+ }
738
+
739
+ .filter-input {
740
+ --input-height: 2.5rem;
741
+ --input-font-size: 1.3rem;
742
+ --input-border-radius: 4px;
743
+ --input-padding: 4px 8px;
744
+ --input-focused-border-color: ${modalSubmitButton};
745
+ width: 150px;
746
+ margin-left: 5px;
747
+ }
748
+
749
+ .empty-results {
750
+ text-align: center;
751
+ padding: 40px 20px;
752
+ color: #666;
753
+ }
754
+
755
+ .empty-results .hint {
756
+ margin-top: 10px;
757
+ }
758
+
428
759
  .facets-content {
429
760
  font-size: 1.2rem;
430
- max-height: 300px;
431
- overflow: auto;
761
+ flex: 1 1 auto;
762
+ min-height: 0;
763
+ overflow-y: auto;
764
+ overflow-x: hidden;
432
765
  padding: 10px;
766
+ /* Force scrollbar to always be visible */
767
+ scrollbar-width: thin; /* Firefox */
768
+ scrollbar-color: #888 #f1f1f1; /* Firefox - thumb and track colors */
769
+ }
770
+
771
+ /* Horizontal scroll mode: horizontal scrolling only */
772
+ .facets-content.horizontal-scroll-mode {
773
+ overflow-x: auto;
774
+ overflow-y: hidden;
775
+ }
776
+
777
+ /* Webkit browsers scrollbar styling - always visible */
778
+ .facets-content::-webkit-scrollbar {
779
+ width: 12px; /* Vertical scrollbar width */
780
+ height: 12px; /* Horizontal scrollbar height */
781
+ }
782
+
783
+ .facets-content::-webkit-scrollbar-track {
784
+ background: #f1f1f1;
785
+ border-radius: 6px;
786
+ }
787
+
788
+ .facets-content::-webkit-scrollbar-thumb {
789
+ background: #888;
790
+ border-radius: 6px;
791
+ min-height: 30px; /* Ensure thumb is always visible when scrolling is possible */
792
+ }
793
+
794
+ .facets-content::-webkit-scrollbar-thumb:hover {
795
+ background: #555;
796
+ }
797
+
798
+ /* Force corner to match track color */
799
+ .facets-content::-webkit-scrollbar-corner {
800
+ background: #f1f1f1;
801
+ }
802
+
803
+ .facets-horizontal-container {
804
+ display: inline-block;
805
+ min-width: 100%;
806
+ /* Allow natural width expansion based on content */
807
+ width: fit-content;
808
+ }
809
+
810
+ .scroll-nav-container {
811
+ display: flex;
812
+ align-items: center;
813
+ flex: 1 1 auto;
814
+ min-height: 0;
815
+ }
816
+
817
+ .scroll-nav-container .facets-content {
818
+ flex: 1 1 auto;
819
+ min-width: 0;
820
+ }
821
+
822
+ .scroll-arrow {
823
+ background: none;
824
+ border: none;
825
+ cursor: pointer;
826
+ padding: 5px;
827
+ flex-shrink: 0;
828
+ }
829
+
830
+ .scroll-arrow svg {
831
+ height: 14px;
832
+ fill: #2c2c2c;
833
+ }
834
+
835
+ .scroll-arrow:disabled {
836
+ opacity: 0.3;
837
+ cursor: default;
433
838
  }
434
839
  .facets-loader {
435
840
  --icon-width: 70px;
@@ -445,6 +850,7 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
445
850
  width: auto;
446
851
  border-radius: 4px;
447
852
  cursor: pointer;
853
+ font-family: inherit;
448
854
  }
449
855
  .btn-cancel {
450
856
  background-color: #2c2c2c;
@@ -454,19 +860,37 @@ let MoreFacetsContent = class MoreFacetsContent extends LitElement {
454
860
  background-color: ${modalSubmitButton};
455
861
  color: white;
456
862
  }
863
+ more-facets-pagination {
864
+ flex-shrink: 0;
865
+ }
866
+
457
867
  .footer {
458
868
  text-align: center;
459
869
  margin-top: 10px;
870
+ flex-shrink: 0;
460
871
  }
461
872
 
462
873
  @media (max-width: 560px) {
463
- section#more-facets {
464
- max-height: 450px;
465
- --facetsColumnCount: 1;
874
+ section#more-facets.horizontal-scroll-mode,
875
+ section#more-facets.pagination-mode {
876
+ --facetsColumnCount: 1; /* Single column on mobile */
877
+ --facetsMaxHeight: none; /* Remove fixed height for vertical scrolling */
466
878
  }
467
- .facets-content {
879
+ /* On mobile, always use vertical scrolling regardless of mode */
880
+ .facets-content,
881
+ .facets-content.horizontal-scroll-mode {
468
882
  overflow-y: auto;
469
- height: 300px;
883
+ overflow-x: hidden;
884
+ }
885
+ .scroll-nav-container {
886
+ display: contents; /* Remove wrapper from layout so section flex-column works */
887
+ }
888
+ .scroll-arrow {
889
+ display: none;
890
+ }
891
+ .filter-input {
892
+ width: 120px;
893
+ --input-font-size: 1.2rem;
470
894
  }
471
895
  }
472
896
  `,
@@ -530,9 +954,27 @@ __decorate([
530
954
  __decorate([
531
955
  state()
532
956
  ], MoreFacetsContent.prototype, "unappliedFacetChanges", void 0);
957
+ __decorate([
958
+ state()
959
+ ], MoreFacetsContent.prototype, "filterText", void 0);
533
960
  __decorate([
534
961
  state()
535
962
  ], MoreFacetsContent.prototype, "pageNumber", void 0);
963
+ __decorate([
964
+ state()
965
+ ], MoreFacetsContent.prototype, "isCompactView", void 0);
966
+ __decorate([
967
+ state()
968
+ ], MoreFacetsContent.prototype, "atScrollStart", void 0);
969
+ __decorate([
970
+ state()
971
+ ], MoreFacetsContent.prototype, "atScrollEnd", void 0);
972
+ __decorate([
973
+ query('ia-clearable-text-input')
974
+ ], MoreFacetsContent.prototype, "filterInput", void 0);
975
+ __decorate([
976
+ query('.facets-content')
977
+ ], MoreFacetsContent.prototype, "facetsContentEl", void 0);
536
978
  MoreFacetsContent = __decorate([
537
979
  customElement('more-facets-content')
538
980
  ], MoreFacetsContent);