@internetarchive/collection-browser 4.2.0-alpha-webdev8164.0 → 4.2.0-alpha-webdev8164.2

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.
package/src/app-root.ts CHANGED
@@ -950,10 +950,6 @@ export class AppRoot extends LitElement {
950
950
  --modalBorder: 2px solid var(--primaryButtonBGColor, #194880);
951
951
  --modalTitleLineHeight: 4rem;
952
952
  --modalTitleFontSize: 1.8rem;
953
- --modalCornerRadius: 0;
954
- --modalBottomPadding: 0;
955
- --modalBottomMargin: 0;
956
- --modalScrollOffset: 0;
957
953
  --modalCornerRadius: 0.5rem;
958
954
  }
959
955
  modal-manager.expanded-date-picker {
@@ -161,7 +161,7 @@ export class MoreFacetsContent extends LitElement {
161
161
  /**
162
162
  * Whether the horizontal scroll is at the rightmost position.
163
163
  */
164
- @state() private atScrollEnd = false;
164
+ @state() private atScrollEnd = true;
165
165
 
166
166
  @query('ia-clearable-text-input')
167
167
  private filterInput!: HTMLElement;
@@ -240,25 +240,30 @@ export class MoreFacetsContent extends LitElement {
240
240
  firstUpdated(): void {
241
241
  this.setupEscapeListeners();
242
242
  this.setupCompactViewObserver();
243
+ this.constrainToScrollContainer();
243
244
  }
244
245
 
245
246
  disconnectedCallback(): void {
246
247
  super.disconnectedCallback();
247
248
  this.resizeObserver?.disconnect();
248
249
  this.removeScrollListener();
250
+ document.removeEventListener('keydown', this.escapeHandler);
249
251
  }
250
252
 
251
253
  private scrollHandler = () => this.updateScrollState();
252
254
 
253
255
  private scrollListenerAttached = false;
254
256
 
257
+ private scrollListenerTarget?: HTMLElement;
258
+
255
259
  /**
256
260
  * Attaches a scroll event listener to the facets content element
257
261
  * to track horizontal scroll position for arrow button states.
258
262
  */
259
263
  private attachScrollListener(): void {
260
264
  if (this.scrollListenerAttached || !this.facetsContentEl) return;
261
- this.facetsContentEl.addEventListener('scroll', this.scrollHandler, {
265
+ this.scrollListenerTarget = this.facetsContentEl;
266
+ this.scrollListenerTarget.addEventListener('scroll', this.scrollHandler, {
262
267
  passive: true,
263
268
  });
264
269
  this.scrollListenerAttached = true;
@@ -268,8 +273,9 @@ export class MoreFacetsContent extends LitElement {
268
273
  }
269
274
 
270
275
  private removeScrollListener(): void {
271
- if (!this.scrollListenerAttached || !this.facetsContentEl) return;
272
- this.facetsContentEl.removeEventListener('scroll', this.scrollHandler);
276
+ if (!this.scrollListenerAttached || !this.scrollListenerTarget) return;
277
+ this.scrollListenerTarget.removeEventListener('scroll', this.scrollHandler);
278
+ this.scrollListenerTarget = undefined;
273
279
  this.scrollListenerAttached = false;
274
280
  }
275
281
 
@@ -344,24 +350,61 @@ export class MoreFacetsContent extends LitElement {
344
350
  private setupCompactViewObserver(): void {
345
351
  this.resizeObserver = new ResizeObserver(entries => {
346
352
  for (const entry of entries) {
347
- this.isCompactView = entry.contentRect.width <= 560;
353
+ const compact = entry.contentRect.width <= 560;
354
+ if (this.isCompactView !== compact) this.isCompactView = compact;
348
355
  }
349
356
  });
350
357
  this.resizeObserver.observe(this);
351
358
  }
352
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
+ }
397
+
353
398
  /**
354
399
  * Close more facets modal on Escape click
355
400
  */
401
+ private escapeHandler = (e: KeyboardEvent) => {
402
+ if (e.key === 'Escape') this.modalManager?.closeModal();
403
+ };
404
+
356
405
  private setupEscapeListeners() {
357
406
  if (this.modalManager) {
358
- document.addEventListener('keydown', (e: KeyboardEvent) => {
359
- if (e.key === 'Escape') {
360
- this.modalManager?.closeModal();
361
- }
362
- });
363
- } else {
364
- document.removeEventListener('keydown', () => {});
407
+ document.addEventListener('keydown', this.escapeHandler);
365
408
  }
366
409
  }
367
410
 
@@ -400,15 +443,18 @@ export class MoreFacetsContent extends LitElement {
400
443
  rows: 0, // todo - do we want server-side pagination with offset/page/limit flag?
401
444
  };
402
445
 
403
- const results = await this.searchService?.search(params, this.searchType);
404
- this.aggregations = results?.success?.response.aggregations;
405
- this.facetsLoading = false;
446
+ try {
447
+ const results = await this.searchService?.search(params, this.searchType);
448
+ this.aggregations = results?.success?.response.aggregations;
406
449
 
407
- const collectionTitles = results?.success?.response?.collectionTitles;
408
- if (collectionTitles) {
409
- for (const [id, title] of Object.entries(collectionTitles)) {
410
- this.collectionTitles?.set(id, title);
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
+ }
411
455
  }
456
+ } finally {
457
+ this.facetsLoading = false;
412
458
  }
413
459
  }
414
460
 
@@ -493,7 +539,10 @@ export class MoreFacetsContent extends LitElement {
493
539
 
494
540
  const buckets: FacetBucket[] = Object.entries(selectedFacetsForKey).map(
495
541
  ([value, data]) => {
496
- const displayText: string = value;
542
+ const displayText =
543
+ (this.facetKey === 'collection'
544
+ ? this.collectionTitles?.get(value)
545
+ : undefined) ?? value;
497
546
  return {
498
547
  displayText,
499
548
  key: value,
@@ -537,17 +586,33 @@ export class MoreFacetsContent extends LitElement {
537
586
  });
538
587
  }
539
588
 
540
- // Construct the array of facet buckets from the aggregation buckets
589
+ // Construct the array of facet buckets from the aggregation buckets,
590
+ // using collection display titles where available.
541
591
  const facetBuckets: FacetBucket[] = sortedBuckets.map(bucket => {
542
592
  const bucketKeyStr = `${bucket.key}`;
593
+ const displayText =
594
+ (this.facetKey === 'collection'
595
+ ? this.collectionTitles?.get(bucketKeyStr)
596
+ : undefined) ?? bucketKeyStr;
543
597
  return {
544
- displayText: `${bucketKeyStr}`,
598
+ displayText,
545
599
  key: `${bucketKeyStr}`,
546
600
  count: bucket.doc_count,
547
601
  state: 'none',
548
602
  };
549
603
  });
550
604
 
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
+
551
616
  return {
552
617
  title: facetGroupTitle,
553
618
  key: this.facetKey,
@@ -739,7 +804,11 @@ export class MoreFacetsContent extends LitElement {
739
804
  });
740
805
 
741
806
  this.dispatchEvent(
742
- new CustomEvent('pageChanged', { detail: this.pageNumber }),
807
+ new CustomEvent('pageChanged', {
808
+ detail: this.pageNumber,
809
+ bubbles: true,
810
+ composed: true,
811
+ }),
743
812
  );
744
813
  }
745
814
 
@@ -786,6 +855,46 @@ export class MoreFacetsContent extends LitElement {
786
855
  </span>`;
787
856
  }
788
857
 
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
+
789
898
  render() {
790
899
  const sectionClasses = classMap({
791
900
  'pagination-mode': this.usePagination,
@@ -794,7 +903,6 @@ export class MoreFacetsContent extends LitElement {
794
903
  const contentClasses = classMap({
795
904
  'facets-content': true,
796
905
  'pagination-mode': this.usePagination,
797
- 'horizontal-scroll-mode': !this.usePagination,
798
906
  });
799
907
 
800
908
  return html`
@@ -807,37 +915,7 @@ export class MoreFacetsContent extends LitElement {
807
915
  ? html`<div class=${contentClasses}>
808
916
  ${this.moreFacetsTemplate}
809
917
  </div>`
810
- : html`<div class="scroll-nav-container">
811
- ${when(
812
- !this.atScrollStart || !this.atScrollEnd,
813
- () =>
814
- html`<button
815
- class="scroll-arrow scroll-left"
816
- @click=${this.onScrollLeft}
817
- ?disabled=${this.atScrollStart}
818
- aria-label="Scroll facets left"
819
- >
820
- ${arrowLeftIcon}
821
- </button>`,
822
- )}
823
- <div class=${contentClasses}>
824
- <div class="facets-horizontal-container">
825
- ${this.moreFacetsTemplate}
826
- </div>
827
- </div>
828
- ${when(
829
- !this.atScrollStart || !this.atScrollEnd,
830
- () =>
831
- html`<button
832
- class="scroll-arrow scroll-right"
833
- @click=${this.onScrollRight}
834
- ?disabled=${this.atScrollEnd}
835
- aria-label="Scroll facets right"
836
- >
837
- ${arrowRightIcon}
838
- </button>`,
839
- )}
840
- </div>`}
918
+ : this.horizontalScrollTemplate}
841
919
  ${this.footerTemplate}
842
920
  </section>
843
921
  `}
@@ -895,7 +973,7 @@ export class MoreFacetsContent extends LitElement {
895
973
  section#more-facets {
896
974
  display: flex;
897
975
  flex-direction: column;
898
- max-height: calc(100vh - 16.5rem);
976
+ max-height: calc(100vh - 16.5rem - var(--modalBottomMargin, 2.5rem));
899
977
  padding: 10px;
900
978
  box-sizing: border-box;
901
979
  --facetsColumnCount: 3;
@@ -908,6 +986,9 @@ export class MoreFacetsContent extends LitElement {
908
986
  }
909
987
  .header-content {
910
988
  flex-shrink: 0;
989
+ position: relative;
990
+ z-index: 1;
991
+ background: #fff;
911
992
  }
912
993
 
913
994
  .header-content .title {
@@ -922,8 +1003,8 @@ export class MoreFacetsContent extends LitElement {
922
1003
  display: flex;
923
1004
  flex-wrap: wrap;
924
1005
  align-items: center;
925
- gap: 4px 20px;
926
- padding: 0 10px;
1006
+ gap: 8px 20px;
1007
+ padding: 0 10px 8px;
927
1008
  }
928
1009
 
929
1010
  .sort-controls {
@@ -42,7 +42,10 @@ export class MoreFacetsPagination extends LitElement {
42
42
  ) {
43
43
  this.updatePages();
44
44
  }
45
- if (changed.has('currentPage')) {
45
+ if (
46
+ changed.has('currentPage') &&
47
+ changed.get('currentPage') !== undefined
48
+ ) {
46
49
  this.emitPageClick();
47
50
  }
48
51
  }
@@ -404,6 +404,81 @@ describe('More facets content', () => {
404
404
  expect(clearableInput.value).to.equal('');
405
405
  });
406
406
 
407
+ describe('Modal container height constraint', () => {
408
+ // Register a test wrapper element to simulate the modal's scroll container
409
+ if (!customElements.get('test-scroll-wrapper')) {
410
+ customElements.define(
411
+ 'test-scroll-wrapper',
412
+ class extends HTMLElement {
413
+ constructor() {
414
+ super();
415
+ this.attachShadow({ mode: 'open' });
416
+ this.shadowRoot!.innerHTML = `
417
+ <style>
418
+ :host { display: block; }
419
+ .content { overflow-y: auto; max-height: 300px; }
420
+ </style>
421
+ <div class="content"><slot></slot></div>
422
+ `;
423
+ }
424
+ },
425
+ );
426
+ }
427
+
428
+ it('should constrain section height when inside a scroll container', async () => {
429
+ const el = await fixture<MoreFacetsContent>(html`
430
+ <test-scroll-wrapper>
431
+ <more-facets-content></more-facets-content>
432
+ </test-scroll-wrapper>
433
+ `);
434
+
435
+ const mfc = el.querySelector('more-facets-content') as MoreFacetsContent;
436
+ mfc.facetsLoading = false;
437
+ await mfc.updateComplete;
438
+
439
+ // Wait for the constrainToScrollContainer rAF callback
440
+ await new Promise(r =>
441
+ requestAnimationFrame(() => requestAnimationFrame(r)),
442
+ );
443
+
444
+ const section = mfc.shadowRoot?.querySelector(
445
+ 'section#more-facets',
446
+ ) as HTMLElement;
447
+
448
+ // The section's inline max-height should be set when it would
449
+ // overflow the 300px scroll container
450
+ const sectionHeight = section.getBoundingClientRect().height;
451
+ const wrapper = el.shadowRoot?.querySelector('.content') as HTMLElement;
452
+ const wrapperBottom = wrapper.getBoundingClientRect().bottom;
453
+ const sectionTop = section.getBoundingClientRect().top;
454
+ const availableSpace = wrapperBottom - sectionTop;
455
+
456
+ // The section should not exceed the available space in the container
457
+ expect(sectionHeight).to.be.at.most(availableSpace + 1); // +1 for rounding
458
+ });
459
+
460
+ it('should not constrain section when no scroll container exists', async () => {
461
+ const el = await fixture<MoreFacetsContent>(
462
+ html`<more-facets-content></more-facets-content>`,
463
+ );
464
+
465
+ el.facetsLoading = false;
466
+ await el.updateComplete;
467
+
468
+ // Wait for the constrainToScrollContainer rAF callback
469
+ await new Promise(r =>
470
+ requestAnimationFrame(() => requestAnimationFrame(r)),
471
+ );
472
+
473
+ const section = el.shadowRoot?.querySelector(
474
+ 'section#more-facets',
475
+ ) as HTMLElement;
476
+
477
+ // No inline max-height should be set when there's no scroll container
478
+ expect(section.style.maxHeight).to.equal('');
479
+ });
480
+ });
481
+
407
482
  describe('Horizontal scroll navigation arrows', () => {
408
483
  it('should use scroll-nav-container in horizontal scroll mode', async () => {
409
484
  const searchService = new MockSearchService();