@internetarchive/collection-browser 4.1.0 → 4.2.0-alpha-webdev8164.1

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 +95 -8
  15. package/dist/src/collection-facets/more-facets-content.js +576 -102
  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 +71 -9
  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 +625 -113
  48. package/src/collection-facets/more-facets-pagination.ts +84 -10
  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
@@ -5,7 +5,10 @@ import '../../src/collection-facets/more-facets-content';
5
5
  import { MockSearchService } from '../mocks/mock-search-service';
6
6
  import { MockAnalyticsHandler } from '../mocks/mock-analytics-handler';
7
7
  import type { FacetsTemplate } from '../../src/collection-facets/facets-template';
8
- import type { SelectedFacets } from '../../src/models';
8
+ import {
9
+ getDefaultSelectedFacets,
10
+ type SelectedFacets,
11
+ } from '../../src/models';
9
12
 
10
13
  const selectedFacetsGroup = {
11
14
  title: 'Media Type',
@@ -54,7 +57,7 @@ describe('More facets content', () => {
54
57
  expect(el.shadowRoot?.querySelector('.facets-loader')).to.exist;
55
58
  });
56
59
 
57
- it('should render pagination for more facets', async () => {
60
+ it('should NOT render pagination when facet count < 1000', async () => {
58
61
  const searchService = new MockSearchService();
59
62
 
60
63
  const el = await fixture<MoreFacetsContent>(
@@ -64,11 +67,22 @@ describe('More facets content', () => {
64
67
  );
65
68
 
66
69
  el.facetKey = 'year';
67
- el.query = 'more-facets'; // Produces a response with 40+ aggregations for multiple pages
70
+ el.query = 'more-facets'; // Produces a response with 45 aggregations (< 1000)
68
71
  await el.updateComplete;
69
72
  await aTimeout(50); // Give it a moment to perform the (mock) search query after the initial update
70
73
 
71
- expect(el.shadowRoot?.querySelectorAll('more-facets-pagination')).to.exist;
74
+ // Verify pagination component is NOT present (horizontal scroll mode)
75
+ expect(el.shadowRoot?.querySelector('more-facets-pagination')).to.not.exist;
76
+
77
+ // Verify horizontal scroll mode CSS class is applied
78
+ expect(
79
+ el.shadowRoot?.querySelector('.facets-content.horizontal-scroll-mode'),
80
+ ).to.exist;
81
+
82
+ // Verify footer still exists with buttons
83
+ expect(el.shadowRoot?.querySelector('.footer')).to.exist;
84
+ expect(el.shadowRoot?.querySelector('.btn-cancel')).to.exist;
85
+ expect(el.shadowRoot?.querySelector('.btn-submit')).to.exist;
72
86
  });
73
87
 
74
88
  it('query for more facets content using search service', async () => {
@@ -228,4 +242,237 @@ describe('More facets content', () => {
228
242
  expect(mockAnalyticsHandler.callAction).to.equal('applyMoreFacetsModal');
229
243
  expect(mockAnalyticsHandler.callLabel).to.equal('collection');
230
244
  });
245
+
246
+ it('should have horizontal scrolling enabled', async () => {
247
+ const searchService = new MockSearchService();
248
+
249
+ const el = await fixture<MoreFacetsContent>(
250
+ html`<more-facets-content
251
+ .searchService=${searchService}
252
+ ></more-facets-content>`,
253
+ );
254
+
255
+ el.facetKey = 'year';
256
+ el.query = 'more-facets';
257
+ await el.updateComplete;
258
+ await aTimeout(50);
259
+
260
+ const facetsContent = el.shadowRoot?.querySelector(
261
+ '.facets-content',
262
+ ) as HTMLElement;
263
+ const styles = window.getComputedStyle(facetsContent);
264
+
265
+ expect(styles.overflowX).to.equal('auto');
266
+ expect(styles.overflowY).to.equal('hidden');
267
+ });
268
+
269
+ it('should have horizontal container wrapper', async () => {
270
+ const searchService = new MockSearchService();
271
+
272
+ const el = await fixture<MoreFacetsContent>(
273
+ html`<more-facets-content
274
+ .searchService=${searchService}
275
+ ></more-facets-content>`,
276
+ );
277
+
278
+ el.facetKey = 'year';
279
+ el.query = 'more-facets';
280
+ await el.updateComplete;
281
+ await aTimeout(50);
282
+
283
+ const container = el.shadowRoot?.querySelector(
284
+ '.facets-horizontal-container',
285
+ );
286
+ expect(container).to.exist;
287
+
288
+ const facetsTemplate = container?.querySelector('facets-template');
289
+ expect(facetsTemplate).to.exist;
290
+ });
291
+
292
+ it('should render pagination when facet count >= 1000', async () => {
293
+ const searchService = new MockSearchService();
294
+
295
+ const el = await fixture<MoreFacetsContent>(
296
+ html`<more-facets-content
297
+ .searchService=${searchService}
298
+ .selectedFacets=${getDefaultSelectedFacets()}
299
+ ></more-facets-content>`,
300
+ );
301
+
302
+ el.facetKey = 'subject';
303
+ el.query = 'large-facets'; // Produces a response with 1100 aggregations (>= 1000)
304
+ await el.updateComplete;
305
+ await aTimeout(50);
306
+
307
+ // Verify pagination component IS present
308
+ expect(el.shadowRoot?.querySelector('more-facets-pagination')).to.exist;
309
+
310
+ // Verify pagination mode CSS class is applied
311
+ expect(el.shadowRoot?.querySelector('.facets-content.pagination-mode')).to
312
+ .exist;
313
+
314
+ // Verify horizontal container wrapper does NOT exist in pagination mode
315
+ expect(el.shadowRoot?.querySelector('.facets-horizontal-container')).to.not
316
+ .exist;
317
+ });
318
+
319
+ it('pagination page change should send analytics event', async () => {
320
+ const searchService = new MockSearchService();
321
+ const mockAnalyticsHandler = new MockAnalyticsHandler();
322
+
323
+ const el = await fixture<MoreFacetsContent>(
324
+ html`<more-facets-content
325
+ .searchService=${searchService}
326
+ .selectedFacets=${getDefaultSelectedFacets()}
327
+ .analyticsHandler=${mockAnalyticsHandler}
328
+ ></more-facets-content>`,
329
+ );
330
+
331
+ el.facetKey = 'subject';
332
+ el.query = 'large-facets'; // Produces a response with 1100 aggregations (>= 1000)
333
+ await el.updateComplete;
334
+ await aTimeout(50);
335
+
336
+ // Get the pagination component
337
+ const pagination = el.shadowRoot?.querySelector(
338
+ 'more-facets-pagination',
339
+ ) as any;
340
+ expect(pagination).to.exist;
341
+
342
+ // Simulate clicking page 2
343
+ pagination.currentPage = 2;
344
+ await pagination.updateComplete;
345
+
346
+ // Verify analytics event was sent
347
+ expect(mockAnalyticsHandler.callCategory).to.equal('collection-browser');
348
+ expect(mockAnalyticsHandler.callAction).to.equal('moreFacetsPageChange');
349
+ expect(mockAnalyticsHandler.callLabel).to.equal('2');
350
+ });
351
+
352
+ it('should render clearable text input for filtering', async () => {
353
+ const searchService = new MockSearchService();
354
+
355
+ const el = await fixture<MoreFacetsContent>(
356
+ html`<more-facets-content
357
+ .facetKey=${'year'}
358
+ .query=${'more-facets'}
359
+ .searchService=${searchService}
360
+ .selectedFacets=${yearSelectedFacets}
361
+ ></more-facets-content>`,
362
+ );
363
+
364
+ await el.updateComplete;
365
+ await aTimeout(50);
366
+
367
+ // Verify the clearable text input component is present
368
+ const clearableInput = el.shadowRoot?.querySelector(
369
+ 'ia-clearable-text-input',
370
+ ) as HTMLElement;
371
+ expect(clearableInput).to.exist;
372
+ });
373
+
374
+ it('should clear filter text when clear event is dispatched', async () => {
375
+ const searchService = new MockSearchService();
376
+
377
+ const el = await fixture<MoreFacetsContent>(
378
+ html`<more-facets-content
379
+ .facetKey=${'year'}
380
+ .query=${'more-facets'}
381
+ .searchService=${searchService}
382
+ .selectedFacets=${yearSelectedFacets}
383
+ ></more-facets-content>`,
384
+ );
385
+
386
+ await el.updateComplete;
387
+ await aTimeout(50);
388
+
389
+ // Simulate typing into the clearable input by dispatching input event
390
+ const clearableInput = el.shadowRoot?.querySelector(
391
+ 'ia-clearable-text-input',
392
+ ) as HTMLElement & { value: string };
393
+ expect(clearableInput).to.exist;
394
+
395
+ clearableInput.value = 'test';
396
+ clearableInput.dispatchEvent(new Event('input'));
397
+ await el.updateComplete;
398
+
399
+ // Dispatch clear event
400
+ clearableInput.dispatchEvent(new CustomEvent('clear', { detail: 'test' }));
401
+ await el.updateComplete;
402
+
403
+ // Verify the filter was cleared
404
+ expect(clearableInput.value).to.equal('');
405
+ });
406
+
407
+ describe('Horizontal scroll navigation arrows', () => {
408
+ it('should use scroll-nav-container in horizontal scroll mode', async () => {
409
+ const searchService = new MockSearchService();
410
+
411
+ const el = await fixture<MoreFacetsContent>(
412
+ html`<more-facets-content
413
+ .searchService=${searchService}
414
+ ></more-facets-content>`,
415
+ );
416
+
417
+ el.facetKey = 'year';
418
+ el.query = 'more-facets'; // Produces < 1000 aggregations
419
+ await el.updateComplete;
420
+ await aTimeout(50);
421
+
422
+ // Verify scroll navigation container exists in horizontal scroll mode
423
+ expect(el.shadowRoot?.querySelector('.scroll-nav-container')).to.exist;
424
+
425
+ // Verify horizontal container and facets-content exist inside it
426
+ expect(
427
+ el.shadowRoot?.querySelector(
428
+ '.scroll-nav-container .facets-content.horizontal-scroll-mode',
429
+ ),
430
+ ).to.exist;
431
+ expect(
432
+ el.shadowRoot?.querySelector(
433
+ '.scroll-nav-container .facets-horizontal-container',
434
+ ),
435
+ ).to.exist;
436
+ });
437
+
438
+ it('should NOT show scroll arrows in pagination mode', async () => {
439
+ const searchService = new MockSearchService();
440
+
441
+ const el = await fixture<MoreFacetsContent>(
442
+ html`<more-facets-content
443
+ .searchService=${searchService}
444
+ .selectedFacets=${getDefaultSelectedFacets()}
445
+ ></more-facets-content>`,
446
+ );
447
+
448
+ el.facetKey = 'subject';
449
+ el.query = 'large-facets'; // Produces >= 1000 aggregations
450
+ await el.updateComplete;
451
+ await aTimeout(50);
452
+
453
+ // Verify scroll navigation container does NOT exist
454
+ expect(el.shadowRoot?.querySelector('.scroll-nav-container')).to.not
455
+ .exist;
456
+ expect(el.shadowRoot?.querySelector('.scroll-arrow')).to.not.exist;
457
+ });
458
+
459
+ it('should hide scroll arrows when content does not overflow', async () => {
460
+ const searchService = new MockSearchService();
461
+
462
+ const el = await fixture<MoreFacetsContent>(
463
+ html`<more-facets-content
464
+ .searchService=${searchService}
465
+ ></more-facets-content>`,
466
+ );
467
+
468
+ el.facetKey = 'year';
469
+ el.query = 'more-facets';
470
+ await el.updateComplete;
471
+ await aTimeout(50);
472
+
473
+ // In test environment, there's no real layout so scrollWidth === clientWidth.
474
+ // Arrows should be hidden when there's no horizontal overflow.
475
+ expect(el.shadowRoot?.querySelector('.scroll-arrow')).to.not.exist;
476
+ });
477
+ });
231
478
  });
@@ -112,7 +112,7 @@ describe('More facets pagination', () => {
112
112
 
113
113
  const fake1 = sinon.fake();
114
114
  const fake2 = sinon.fake();
115
- el.observePageCount = fake1;
115
+ el.updatePages = fake1;
116
116
  el.emitPageClick = fake2;
117
117
 
118
118
  // select first page button
@@ -146,7 +146,7 @@ describe('More facets pagination', () => {
146
146
 
147
147
  const fake1 = sinon.fake();
148
148
  const fake2 = sinon.fake();
149
- el.observePageCount = fake1;
149
+ el.updatePages = fake1;
150
150
  el.emitPageClick = fake2;
151
151
 
152
152
  // select first page button
@@ -182,7 +182,7 @@ describe('More facets pagination', () => {
182
182
 
183
183
  const fake1 = sinon.fake();
184
184
  const fake2 = sinon.fake();
185
- el.observePageCount = fake1;
185
+ el.updatePages = fake1;
186
186
  el.emitPageClick = fake2;
187
187
 
188
188
  // select first page button
@@ -198,4 +198,88 @@ describe('More facets pagination', () => {
198
198
  expect(el.currentPage).to.equal(6); // brings us forward 1 page
199
199
  });
200
200
  });
201
+
202
+ describe('Compact mode', () => {
203
+ it('shows all pages when size <= 3', async () => {
204
+ const el = await fixture<MoreFacetsPagination>(
205
+ html`<more-facets-pagination
206
+ .size=${3}
207
+ .compact=${true}
208
+ ></more-facets-pagination>`,
209
+ );
210
+
211
+ await el.updateComplete;
212
+ expect(el.pages).to.deep.equal([1, 2, 3]);
213
+ });
214
+
215
+ it('shows first, prev, current, next, ..., last for middle page', async () => {
216
+ const el = await fixture<MoreFacetsPagination>(
217
+ html`<more-facets-pagination
218
+ .size=${20}
219
+ .currentPage=${10}
220
+ .compact=${true}
221
+ ></more-facets-pagination>`,
222
+ );
223
+
224
+ await el.updateComplete;
225
+ // first, ..., prev, current, next, ..., last
226
+ expect(el.pages).to.deep.equal([1, 0, 9, 10, 11, 0, 20]);
227
+ });
228
+
229
+ it('shows correct pages when on page 1', async () => {
230
+ const el = await fixture<MoreFacetsPagination>(
231
+ html`<more-facets-pagination
232
+ .size=${20}
233
+ .currentPage=${1}
234
+ .compact=${true}
235
+ ></more-facets-pagination>`,
236
+ );
237
+
238
+ await el.updateComplete;
239
+ // first (current), next, ..., last
240
+ expect(el.pages).to.deep.equal([1, 2, 0, 20]);
241
+ });
242
+
243
+ it('shows correct pages when on last page', async () => {
244
+ const el = await fixture<MoreFacetsPagination>(
245
+ html`<more-facets-pagination
246
+ .size=${20}
247
+ .currentPage=${20}
248
+ .compact=${true}
249
+ ></more-facets-pagination>`,
250
+ );
251
+
252
+ await el.updateComplete;
253
+ // first, ..., prev, last (current)
254
+ expect(el.pages).to.deep.equal([1, 0, 19, 20]);
255
+ });
256
+
257
+ it('shows correct pages when on page 2', async () => {
258
+ const el = await fixture<MoreFacetsPagination>(
259
+ html`<more-facets-pagination
260
+ .size=${20}
261
+ .currentPage=${2}
262
+ .compact=${true}
263
+ ></more-facets-pagination>`,
264
+ );
265
+
266
+ await el.updateComplete;
267
+ // first, current, next, ..., last
268
+ expect(el.pages).to.deep.equal([1, 2, 3, 0, 20]);
269
+ });
270
+
271
+ it('shows correct pages when on second-to-last page', async () => {
272
+ const el = await fixture<MoreFacetsPagination>(
273
+ html`<more-facets-pagination
274
+ .size=${20}
275
+ .currentPage=${19}
276
+ .compact=${true}
277
+ ></more-facets-pagination>`,
278
+ );
279
+
280
+ await el.updateComplete;
281
+ // first, ..., prev, current, last
282
+ expect(el.pages).to.deep.equal([1, 0, 18, 19, 20]);
283
+ });
284
+ });
201
285
  });
@@ -1349,6 +1349,54 @@ export const getMockSuccessWithManyAggregations: () => Result<
1349
1349
  },
1350
1350
  });
1351
1351
 
1352
+ /**
1353
+ * Returns a mock response with 1000+ subject aggregation buckets,
1354
+ * used to test pagination mode in the More Facets dialog.
1355
+ */
1356
+ export const getMockSuccessWithLargeAggregations: () => Result<
1357
+ SearchResponse,
1358
+ SearchServiceError
1359
+ > = () => {
1360
+ const buckets = Array.from({ length: 1100 }, (_, i) => ({
1361
+ key: `subject-${i}`,
1362
+ doc_count: 1100 - i,
1363
+ }));
1364
+ return {
1365
+ success: {
1366
+ request: {
1367
+ kind: 'aggregations' as const,
1368
+ clientParameters: {
1369
+ user_query: 'large-facets',
1370
+ sort: [],
1371
+ },
1372
+ backendRequests: {
1373
+ primary: {
1374
+ kind: 'aggregations' as const,
1375
+ finalized_parameters: {
1376
+ user_query: 'large-facets',
1377
+ sort: [],
1378
+ },
1379
+ },
1380
+ },
1381
+ },
1382
+ rawResponse: {},
1383
+ sessionContext: {},
1384
+ response: {
1385
+ totalResults: 0,
1386
+ returnedCount: 0,
1387
+ results: [],
1388
+ aggregations: {
1389
+ subject: new Aggregation({ buckets }),
1390
+ },
1391
+ },
1392
+ responseHeader: {
1393
+ succeeded: true,
1394
+ query_time: 0,
1395
+ },
1396
+ },
1397
+ };
1398
+ };
1399
+
1352
1400
  export const getMockErrorResult: () => Result<
1353
1401
  SearchResponse,
1354
1402
  SearchServiceError
@@ -33,6 +33,7 @@ import {
33
33
  getMockSuccessNoResults,
34
34
  getMockSuccessWithWebArchiveHits,
35
35
  getMockSuccessWithManyAggregations,
36
+ getMockSuccessWithLargeAggregations,
36
37
  getMockSuccessTvFields,
37
38
  getMockSuccessArchiveOrgUserResult,
38
39
  getMockSuccessArchiveOrgUserNoBlurResult,
@@ -64,6 +65,7 @@ const responses: Record<
64
65
  'tv-collection': getMockSuccessForTvCollection,
65
66
  'web-archive': getMockSuccessWithWebArchiveHits,
66
67
  'more-facets': getMockSuccessWithManyAggregations,
68
+ 'large-facets': getMockSuccessWithLargeAggregations,
67
69
  'many-fields': getMockSuccessManyFields,
68
70
  'tv-fields': getMockSuccessTvFields,
69
71
  'no-results': getMockSuccessNoResults,