@internetarchive/collection-browser 4.7.1-alpha-webdev9189.0 → 4.8.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 (87) hide show
  1. package/dist/src/app-root.js +0 -2
  2. package/dist/src/app-root.js.map +1 -1
  3. package/dist/src/assets/img/icons/check.d.ts +2 -0
  4. package/dist/src/assets/img/icons/check.js +5 -0
  5. package/dist/src/assets/img/icons/check.js.map +1 -0
  6. package/dist/src/collection-browser.js +2 -24
  7. package/dist/src/collection-browser.js.map +1 -1
  8. package/dist/src/collection-facets/facet-row-template.d.ts +38 -0
  9. package/dist/src/collection-facets/facet-row-template.js +200 -0
  10. package/dist/src/collection-facets/facet-row-template.js.map +1 -0
  11. package/dist/src/collection-facets/facet-row.js +11 -186
  12. package/dist/src/collection-facets/facet-row.js.map +1 -1
  13. package/dist/src/collection-facets/models.d.ts +7 -2
  14. package/dist/src/collection-facets/models.js +7 -2
  15. package/dist/src/collection-facets/models.js.map +1 -1
  16. package/dist/src/collection-facets/more-facets-content.d.ts +87 -21
  17. package/dist/src/collection-facets/more-facets-content.js +477 -200
  18. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  19. package/dist/src/collection-facets/more-facets-scroller.d.ts +85 -0
  20. package/dist/src/collection-facets/more-facets-scroller.js +428 -0
  21. package/dist/src/collection-facets/more-facets-scroller.js.map +1 -0
  22. package/dist/src/collection-facets/page-window.d.ts +73 -0
  23. package/dist/src/collection-facets/page-window.js +202 -0
  24. package/dist/src/collection-facets/page-window.js.map +1 -0
  25. package/dist/src/collection-facets/toggle-switch.js +1 -0
  26. package/dist/src/collection-facets/toggle-switch.js.map +1 -1
  27. package/dist/src/models.d.ts +4 -0
  28. package/dist/src/models.js +16 -0
  29. package/dist/src/models.js.map +1 -1
  30. package/dist/src/sort-filter-bar/sort-filter-bar.js +0 -1
  31. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  32. package/dist/src/utils/log.d.ts +1 -4
  33. package/dist/src/utils/normalize-filter-text.d.ts +28 -0
  34. package/dist/src/utils/normalize-filter-text.js +35 -0
  35. package/dist/src/utils/normalize-filter-text.js.map +1 -0
  36. package/dist/test/collection-browser.test.js +0 -34
  37. package/dist/test/collection-browser.test.js.map +1 -1
  38. package/dist/test/collection-facets/more-facets-content.test.js +367 -17
  39. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  40. package/dist/test/collection-facets/more-facets-scroller.test.d.ts +1 -0
  41. package/dist/test/collection-facets/more-facets-scroller.test.js +288 -0
  42. package/dist/test/collection-facets/more-facets-scroller.test.js.map +1 -0
  43. package/dist/test/collection-facets/page-window.test.d.ts +1 -0
  44. package/dist/test/collection-facets/page-window.test.js +175 -0
  45. package/dist/test/collection-facets/page-window.test.js.map +1 -0
  46. package/dist/test/mocks/mock-search-responses.d.ts +6 -0
  47. package/dist/test/mocks/mock-search-responses.js +52 -0
  48. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  49. package/dist/test/mocks/mock-search-service.js +2 -1
  50. package/dist/test/mocks/mock-search-service.js.map +1 -1
  51. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +0 -22
  52. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  53. package/dist/test/utils/normalize-filter-text.test.d.ts +1 -0
  54. package/dist/test/utils/normalize-filter-text.test.js +69 -0
  55. package/dist/test/utils/normalize-filter-text.test.js.map +1 -0
  56. package/docs/design/column-virtualization.md +214 -0
  57. package/package.json +3 -1
  58. package/src/app-root.ts +0 -2
  59. package/src/assets/img/icons/check.ts +5 -0
  60. package/src/collection-browser.ts +4 -26
  61. package/src/collection-facets/facet-row-template.ts +242 -0
  62. package/src/collection-facets/facet-row.ts +15 -202
  63. package/src/collection-facets/models.ts +8 -2
  64. package/src/collection-facets/more-facets-content.ts +528 -221
  65. package/src/collection-facets/more-facets-scroller.ts +485 -0
  66. package/src/collection-facets/page-window.ts +247 -0
  67. package/src/collection-facets/toggle-switch.ts +1 -0
  68. package/src/models.ts +17 -0
  69. package/src/sort-filter-bar/sort-filter-bar.ts +0 -1
  70. package/src/utils/normalize-filter-text.ts +54 -0
  71. package/test/collection-browser.test.ts +0 -55
  72. package/test/collection-facets/more-facets-content.test.ts +509 -24
  73. package/test/collection-facets/more-facets-scroller.test.ts +401 -0
  74. package/test/collection-facets/page-window.test.ts +208 -0
  75. package/test/mocks/mock-search-responses.ts +56 -0
  76. package/test/mocks/mock-search-service.ts +2 -0
  77. package/test/sort-filter-bar/sort-filter-bar.test.ts +0 -29
  78. package/test/utils/normalize-filter-text.test.ts +87 -0
  79. package/web-test-runner.config.mjs +7 -2
  80. package/dist/src/collection-facets/more-facets-pagination.d.ts +0 -36
  81. package/dist/src/collection-facets/more-facets-pagination.js +0 -260
  82. package/dist/src/collection-facets/more-facets-pagination.js.map +0 -1
  83. package/dist/test/collection-facets/more-facets-pagination.test.d.ts +0 -1
  84. package/dist/test/collection-facets/more-facets-pagination.test.js +0 -126
  85. package/dist/test/collection-facets/more-facets-pagination.test.js.map +0 -1
  86. package/src/collection-facets/more-facets-pagination.ts +0 -294
  87. package/test/collection-facets/more-facets-pagination.test.ts +0 -201
@@ -0,0 +1,214 @@
1
+ # Horizontal column virtualization with page snapping
2
+
3
+ ## Goal
4
+ A Lit component that scrolls thousands of fixed-width data columns horizontally.
5
+ Free scroll/fling during the gesture; on release, eases to the nearest page
6
+ boundary. Only a window of pages is in the DOM at any time.
7
+
8
+ ## Constraints
9
+ - Dataset is fully in memory (thousands of column entries). No fetching.
10
+ - Column width is fixed and uniform. `colsPerPage` is derived from measured
11
+ viewport width, so a page is never wider than the viewport.
12
+ - Lit + LitElement. Page count can reach the high hundreds.
13
+
14
+ ## Chosen approach
15
+ Native scrolling over a full-width sizer, with absolutely positioned page
16
+ elements at computed offsets, plus CSS scroll-snap for the page easing.
17
+ Positions are pure arithmetic — no measurement, no size estimation.
18
+
19
+ ### Windowing controller
20
+ ```ts
21
+ export class PageWindow implements ReactiveController {
22
+ first = 0;
23
+ last = -1;
24
+ private scroller?: HTMLElement;
25
+ private viewportW = 0;
26
+ private frame = 0;
27
+ private ro?: ResizeObserver;
28
+
29
+ constructor(
30
+ private host: ReactiveControllerHost,
31
+ private cfg: {colWidth: number; colsPerPage: number; total: number; buffer?: number}
32
+ ) {
33
+ host.addController(this);
34
+ }
35
+
36
+ get pageWidth() { return this.cfg.colWidth * this.cfg.colsPerPage; }
37
+ get pageCount() { return Math.ceil(this.cfg.total / this.cfg.colsPerPage); }
38
+ get totalWidth() { return this.cfg.colWidth * this.cfg.total; }
39
+
40
+ attach = (el?: Element) => {
41
+ if (!(el instanceof HTMLElement) || el === this.scroller) return;
42
+ this.scroller = el;
43
+ el.addEventListener('scroll', this.onScroll, {passive: true});
44
+ this.ro = new ResizeObserver(([e]) => {
45
+ this.viewportW = e.contentRect.width;
46
+ this.recompute();
47
+ });
48
+ this.ro.observe(el);
49
+ };
50
+
51
+ private onScroll = () => {
52
+ if (this.frame) return;
53
+ this.frame = requestAnimationFrame(() => {
54
+ this.frame = 0;
55
+ this.recompute();
56
+ });
57
+ };
58
+
59
+ private recompute() {
60
+ const pw = this.pageWidth;
61
+ const buf = this.cfg.buffer ?? 1;
62
+ const left = this.scroller!.scrollLeft;
63
+ const first = Math.max(0, Math.floor(left / pw) - buf);
64
+ const last = Math.min(this.pageCount - 1, Math.floor((left + this.viewportW) / pw) + buf);
65
+ if (first === this.first && last === this.last) return; // most scroll events end here
66
+ this.first = first;
67
+ this.last = last;
68
+ this.host.requestUpdate();
69
+ }
70
+
71
+ hostDisconnected() {
72
+ this.scroller?.removeEventListener('scroll', this.onScroll);
73
+ this.ro?.disconnect();
74
+ if (this.frame) cancelAnimationFrame(this.frame);
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Template
80
+ Snap alignment lives on permanent empty stubs, one per page, *not* on the
81
+ rendered pages. See "Snap targets" below for why.
82
+ ```ts
83
+ render() {
84
+ const w = this.win;
85
+ const all = [];
86
+ for (let i = 0; i < w.pageCount; i++) all.push(i);
87
+ const live = [];
88
+ for (let i = w.first; i <= w.last; i++) live.push(i);
89
+
90
+ return html`
91
+ <div class="scroller" ${ref(this.win.attach)}>
92
+ <div class="sizer" style="width:${w.totalWidth}px">
93
+ ${repeat(all, i => i, i => html`
94
+ <div class="snap-stub"
95
+ style="left:${i * w.pageWidth}px;width:${w.pageWidth}px"></div>`)}
96
+ ${repeat(live, i => i, i => html`
97
+ <div class="page"
98
+ style="left:${i * w.pageWidth}px;width:${w.pageWidth}px">
99
+ ${this.renderPage(i)}
100
+ </div>`)}
101
+ </div>
102
+ </div>`;
103
+ }
104
+ ```
105
+
106
+ ### CSS
107
+ ```css
108
+ .scroller {
109
+ overflow-x: auto;
110
+ overflow-y: hidden;
111
+ contain: strict; /* requires explicit dimensions on .scroller */
112
+ scroll-snap-type: x mandatory;
113
+ /* scroll-snap-stop stays at its default `normal` so a fling can cross
114
+ many pages before resting. `always` would force one page per gesture. */
115
+ }
116
+ .sizer { position: relative; height: 100%; }
117
+ .snap-stub {
118
+ position: absolute; top: 0; height: 100%;
119
+ scroll-snap-align: start;
120
+ pointer-events: none;
121
+ contain: strict;
122
+ }
123
+ .page {
124
+ position: absolute; top: 0; height: 100%;
125
+ contain: content;
126
+ content-visibility: auto;
127
+ contain-intrinsic-size: auto none;
128
+ }
129
+ ```
130
+
131
+ ### Landed-page reporting
132
+ ```ts
133
+ const onEnd = () => {
134
+ this.currentPage = Math.round(el.scrollLeft / this.win.pageWidth);
135
+ };
136
+ if ('onscrollend' in window) {
137
+ el.addEventListener('scrollend', onEnd);
138
+ } else {
139
+ el.addEventListener('scroll', () => {
140
+ clearTimeout(this.endTimer);
141
+ this.endTimer = setTimeout(onEnd, 120);
142
+ }, {passive: true});
143
+ }
144
+ ```
145
+
146
+ ## Invariants — do not "optimize" these away
147
+ 1. **The early-out in `recompute` must stay.** Scroll fires far more often than
148
+ the window changes. Re-render happens roughly once per page boundary
149
+ crossed, not once per scroll event.
150
+ 2. **Keyed `repeat()`, keyed by page index.** Switching to `map()` re-renders
151
+ every live page on each window shift instead of creating one and removing one.
152
+ 3. **Nothing reads layout in the scroll handler.** Viewport width comes from
153
+ `ResizeObserver` only. No `getBoundingClientRect` on the scroll path.
154
+ 4. **Cells are plain DOM inside a page component.** Do not make each cell its
155
+ own custom element — element upgrade plus shadow-root creation will dominate
156
+ every other cost here.
157
+
158
+ ## Snap targets
159
+ The browser re-evaluates its snap target continuously as a fling decelerates.
160
+ If the target box isn't mounted, or gets unmounted by a window shift
161
+ mid-fling, the result is a visible jump or a landing on the wrong boundary —
162
+ intermittent, and only on fast flings. The stubs decouple snap geometry from
163
+ virtualization: they always exist, so the geometry is always complete. Empty,
164
+ contained, childless divs cost very little.
165
+
166
+ If page count ever reaches the tens of thousands, flip to the alternative:
167
+ move `scroll-snap-align` onto `.page`, and defer *shrinking* the window until
168
+ `scrollend` so a selected target is never unmounted while scrolling.
169
+
170
+ ## Rejected alternatives (with reasons — do not substitute)
171
+ - **`@lit-labs/virtualizer`** — works horizontally via
172
+ `flow({direction: 'horizontal'})`, but its value is measuring items of
173
+ unknown size, which is exactly what we don't need. Still 0.7.x under the
174
+ `@lit-labs` scope and self-described as late prerelease. Documented
175
+ Chromium flow-layout jank from correcting size estimates during smooth
176
+ scroll (maintainers say unfixable without replacing native scrolling with
177
+ JS scrolling), plus a Safari first-render transform bug
178
+ (https://github.com/lit/lit/issues/3243). Both stem from estimation.
179
+ - **TanStack Virtual** — good library, correct fallback *if column widths ever
180
+ become variable*. Unnecessary overhead while widths are uniform.
181
+ - **`content-visibility: auto` alone** — skips layout/paint for offscreen
182
+ pages but still creates all DOM. Used here as a second layer on top of
183
+ windowing, not as a replacement.
184
+ - **JS-animated `scrollLeft`** — the only way to control snap easing, but it
185
+ costs compositor-driven smoothness. Ship native snap first.
186
+
187
+ ## Browser support notes
188
+ - Scroll snap: Baseline widely available since 2022 (Chrome 69, Firefox 68,
189
+ Safari 11).
190
+ - `scrollend`: Baseline since Dec 2025 (Chrome 114, Firefox 109, Safari 26.2).
191
+ Feature-detect with the timer fallback above if the support floor includes
192
+ older Safari.
193
+ - `content-visibility`: Baseline 2024 (Chrome 85, Firefox 125, Safari 18);
194
+ the web-features entry is more conservative about Safari. Treat as
195
+ progressive enhancement — unsupported browsers just render everything.
196
+ - `scrollsnapchanging` (fires when the browser decides where it *will* land,
197
+ useful for a live page indicator mid-fling): Chromium-only, Chrome 129+,
198
+ no Firefox or Safari. Optional enhancement over the `scrollend` path only.
199
+
200
+ ## Acceptance criteria
201
+ - [ ] Scrolling the full dataset keeps live page elements at `2*buffer + visible`;
202
+ verify in DevTools that DOM node count stays flat.
203
+ - [ ] Performance profile during a sustained fling shows no layout thrash and
204
+ re-render count proportional to pages crossed, not scroll events.
205
+ - [ ] Release mid-page always eases to a page boundary — test slow drag, fast
206
+ fling across 50+ pages, trackpad, mouse wheel, and keyboard.
207
+ - [ ] Verified in Safari specifically (snap and first-render behavior).
208
+ - [ ] Resize recomputes `colsPerPage` and the window without losing position.
209
+ - [ ] `currentPage` is correct after every rest, including programmatic scrolls.
210
+
211
+ ## Deferred
212
+ Page-element recycling (fixed slot pool, `pageIndex % N`, update data and
213
+ `left` in place). Only pursue if a profile shows element *creation* cost
214
+ during flings. Not needed up front.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "The Internet Archive Collection Browser.",
4
4
  "license": "AGPL-3.0-only",
5
5
  "author": "Internet Archive",
6
- "version": "4.7.1-alpha-webdev9189.0",
6
+ "version": "4.8.0",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
9
9
  "packageManager": "pnpm@11.27.1",
@@ -33,6 +33,7 @@
33
33
  "@internetarchive/elements": "^1.1.0",
34
34
  "@internetarchive/feature-feedback": "^1.0.0",
35
35
  "@internetarchive/field-parsers": "^1.0.0",
36
+ "@internetarchive/ia-clearable-text-input": "^1.2.0",
36
37
  "@internetarchive/ia-dropdown": "^2.0.0",
37
38
  "@internetarchive/iaux-item-metadata": "^1.0.5",
38
39
  "@internetarchive/infinite-scroller": "^2.0.0",
@@ -61,6 +62,7 @@
61
62
  "@web/dev-server": "^0.4.6",
62
63
  "@web/dev-server-rollup": "^0.6.4",
63
64
  "@web/test-runner": "^0.20.0",
65
+ "@web/test-runner-commands": "^0.9.0",
64
66
  "concurrently": "^9.1.2",
65
67
  "eslint": "^9.22.0",
66
68
  "eslint-config-prettier": "^10.1.1",
package/src/app-root.ts CHANGED
@@ -1052,9 +1052,7 @@ export class AppRoot extends LitElement {
1052
1052
  --modalBorder: 2px solid var(--primaryButtonBGColor, #194880);
1053
1053
  --modalTitleLineHeight: 4rem;
1054
1054
  --modalTitleFontSize: 1.8rem;
1055
- --modalCornerRadius: 0;
1056
1055
  --modalBottomPadding: 0;
1057
- --modalBottomMargin: 0;
1058
1056
  --modalScrollOffset: 0;
1059
1057
  --modalCornerRadius: 0.5rem;
1060
1058
  }
@@ -0,0 +1,5 @@
1
+ import { svg } from 'lit';
2
+
3
+ export default svg`
4
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="m14 53 24 24 48-52" fill="none" stroke="currentColor" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/></svg>
5
+ `;
@@ -909,17 +909,10 @@ export class CollectionBrowser
909
909
  ? this.manageBarTemplate
910
910
  : this.sortFilterBarTemplate}
911
911
  <slot name="cb-results"></slot>
912
- <div id="results-body">
913
- <div id="results-main">
914
- ${this.displayMode === `list-compact` && this.totalResults
915
- ? this.listHeaderTemplate
916
- : nothing}
917
- ${this.suppressResultTiles
918
- ? nothing
919
- : this.infiniteScrollerTemplate}
920
- </div>
921
- <slot name="results-aside"></slot>
922
- </div>
912
+ ${this.displayMode === `list-compact` && this.totalResults
913
+ ? this.listHeaderTemplate
914
+ : nothing}
915
+ ${this.suppressResultTiles ? nothing : this.infiniteScrollerTemplate}
923
916
  </section>
924
917
  </div>
925
918
  `;
@@ -1022,7 +1015,6 @@ export class CollectionBrowser
1022
1015
  <slot name="sort-options-left" slot="sort-options-left"></slot>
1023
1016
  <slot name="sort-options" slot="sort-options"></slot>
1024
1017
  <slot name="sort-options-right" slot="sort-options-right"></slot>
1025
- <slot name="sort-bar-end" slot="sort-bar-end"></slot>
1026
1018
  </sort-filter-bar>
1027
1019
  `;
1028
1020
  }
@@ -2805,20 +2797,6 @@ export class CollectionBrowser
2805
2797
  padding: 5px 5px 0;
2806
2798
  }
2807
2799
 
2808
- /*
2809
- * The results sit in a row with the optional results-aside slot, which
2810
- * lays out beside them as a flex item and takes its own width and spacing
2811
- */
2812
- #results-body {
2813
- display: flex;
2814
- align-items: flex-start;
2815
- }
2816
-
2817
- #results-main {
2818
- flex: 1;
2819
- min-width: 0;
2820
- }
2821
-
2822
2800
  #left-column {
2823
2801
  width: var(--leftColumnWidth, 18rem);
2824
2802
  /* Prevents Safari from shrinking col at first draw */
@@ -0,0 +1,242 @@
1
+ import { css, html, nothing, TemplateResult } from 'lit';
2
+ import eyeIcon from '../assets/img/icons/eye';
3
+ import eyeClosedIcon from '../assets/img/icons/eye-closed';
4
+ import type { FacetOption, FacetBucket, FacetState } from '../models';
5
+ import type { CollectionTitles } from '../data-source/models';
6
+
7
+ export type FacetRowTemplateOptions = {
8
+ /** The name of the facet group to which this facet belongs (e.g., "mediatype") */
9
+ facetType: FacetOption;
10
+
11
+ /** The facet bucket containing details about the state, count, and key for this row */
12
+ bucket: FacetBucket;
13
+
14
+ /** The collection name cache for converting collection identifiers to titles */
15
+ collectionTitles?: CollectionTitles;
16
+
17
+ /**
18
+ * Whether to omit the negative/hide button from this facet row if possible.
19
+ * Has no effect if the facet bucket is itself hidden, in which case the hide
20
+ * button *must* be shown to represent the state accurately.
21
+ */
22
+ omitHideButton?: boolean;
23
+
24
+ /**
25
+ * Called when either checkbox is clicked. Leave it out when an ancestor
26
+ * handles the rows' clicks by delegation instead.
27
+ */
28
+ onCheckboxClick?: (e: Event, negative: boolean) => void;
29
+ };
30
+
31
+ /**
32
+ * Returns the composed facet state corresponding to a positive or negative facet's checked state
33
+ */
34
+ export function getFacetState(checked: boolean, negative: boolean): FacetState {
35
+ if (!checked) return 'none';
36
+ return negative ? 'hidden' : 'selected';
37
+ }
38
+
39
+ /**
40
+ * Template for a full facet row, including the positive/negative checks,
41
+ * the display name, and the count. The row is plain DOM so that long lists
42
+ * (e.g. the More... dialog) can render many rows without creating an
43
+ * element per row. Render it inside a shadow root that includes
44
+ * `facetRowStyles`.
45
+ */
46
+ export function facetRowTemplate({
47
+ facetType,
48
+ bucket,
49
+ collectionTitles,
50
+ omitHideButton = false,
51
+ onCheckboxClick,
52
+ }: FacetRowTemplateOptions): TemplateResult {
53
+ const showOnlyCheckboxId = `${facetType}:${bucket.key}-show-only`;
54
+ const negativeCheckboxId = `${facetType}:${bucket.key}-negative`;
55
+
56
+ const extraNoteSpan = bucket.extraNote
57
+ ? html`<span class="facet-note">${bucket.extraNote}</span>`
58
+ : nothing;
59
+
60
+ // For collections, we render the collection title as a link.
61
+ // For other facet types, we just have a static value to use.
62
+ const bucketTextDisplay =
63
+ facetType !== 'collection'
64
+ ? html`${bucket.displayText ?? bucket.key} ${extraNoteSpan}`
65
+ : html`<a href="/details/${bucket.key}">
66
+ ${collectionTitles?.get(bucket.key) ?? bucket.key}
67
+ </a> `;
68
+
69
+ const bucketCountText = bucket.count > 0 ? bucket.count.toLocaleString() : '';
70
+
71
+ const facetHidden = bucket.state === 'hidden';
72
+ const facetSelected = bucket.state === 'selected';
73
+
74
+ const titleText = `${facetType}: ${bucket.displayText ?? bucket.key}`;
75
+ const onlyShowText = facetSelected
76
+ ? `Show all ${facetType}s`
77
+ : `Only show ${titleText}`;
78
+ const hideText = `Hide ${titleText}`;
79
+ const unhideText = `Unhide ${titleText}`;
80
+ const showHideText = facetHidden ? unhideText : hideText;
81
+ const ariaLabel = `${titleText}, ${bucket.count} results`;
82
+
83
+ const showOnlyClicked = onCheckboxClick
84
+ ? (e: Event) => onCheckboxClick(e, false)
85
+ : undefined;
86
+ const hideClicked = onCheckboxClick
87
+ ? (e: Event) => onCheckboxClick(e, true)
88
+ : undefined;
89
+
90
+ // Added data-testid for Playwright testing
91
+ return html`
92
+ <div class="facet-row-container">
93
+ <div class="facet-checkboxes">
94
+ <input
95
+ type="checkbox"
96
+ .name=${facetType}
97
+ .value=${bucket.key}
98
+ @click=${showOnlyClicked}
99
+ .checked=${facetSelected}
100
+ class="select-facet-checkbox"
101
+ title=${onlyShowText}
102
+ id=${showOnlyCheckboxId}
103
+ data-testid=${showOnlyCheckboxId}
104
+ />
105
+ <div
106
+ class="hide-facet-container"
107
+ ?hidden=${omitHideButton && !facetHidden}
108
+ >
109
+ <input
110
+ type="checkbox"
111
+ id=${negativeCheckboxId}
112
+ .name=${facetType}
113
+ .value=${bucket.key}
114
+ @click=${hideClicked}
115
+ .checked=${facetHidden}
116
+ class="hide-facet-checkbox"
117
+ />
118
+ <label
119
+ for=${negativeCheckboxId}
120
+ class="hide-facet-icon${facetHidden ? ' active' : ''}"
121
+ title=${showHideText}
122
+ data-testid=${negativeCheckboxId}
123
+ >
124
+ <span class="sr-only">${showHideText}</span>
125
+ <span class="eye eye-open">${eyeIcon}</span>
126
+ <span class="eye eye-closed">${eyeClosedIcon}</span>
127
+ </label>
128
+ </div>
129
+ </div>
130
+ <label
131
+ for=${showOnlyCheckboxId}
132
+ class="facet-info-display"
133
+ title=${onlyShowText}
134
+ aria-label=${ariaLabel}
135
+ >
136
+ <div class="facet-title">${bucketTextDisplay}</div>
137
+ <div class="facet-count">${bucketCountText}</div>
138
+ </label>
139
+ </div>
140
+ `;
141
+ }
142
+
143
+ const facetRowBorderTop = css`var(--facet-row-border-top, 1px solid transparent)`;
144
+ const facetRowBorderBottom = css`var(--facet-row-border-bottom, 1px solid transparent)`;
145
+ const checkboxHeight = css`15px`;
146
+
147
+ /**
148
+ * Styles for `facetRowTemplate`. Include `srOnlyStyle` alongside these.
149
+ */
150
+ export const facetRowStyles = css`
151
+ .facet-checkboxes {
152
+ margin: 0 5px 0 0;
153
+ display: flex;
154
+ height: ${checkboxHeight};
155
+ }
156
+ .facet-checkboxes input:first-child {
157
+ margin-right: 5px;
158
+ }
159
+ .facet-checkboxes input {
160
+ height: ${checkboxHeight};
161
+ width: ${checkboxHeight};
162
+ margin: 0;
163
+ }
164
+ .facet-row-container {
165
+ display: flex;
166
+ font-weight: 500;
167
+ font-size: 1.2rem;
168
+ margin: 0 auto;
169
+ padding: 0.25rem 0;
170
+ height: auto;
171
+ border-top: ${facetRowBorderTop};
172
+ border-bottom: ${facetRowBorderBottom};
173
+ }
174
+ .facet-info-display {
175
+ display: flex;
176
+ flex: 1 1 0%;
177
+ cursor: pointer;
178
+ flex-wrap: wrap;
179
+ }
180
+ .facet-title {
181
+ word-break: break-word;
182
+ display: inline-block;
183
+ flex: 1 1 0%;
184
+ }
185
+ .facet-note {
186
+ color: #bbb;
187
+ }
188
+ .facet-count {
189
+ text-align: right;
190
+ }
191
+ .select-facet-checkbox {
192
+ cursor: pointer;
193
+ display: inline-block;
194
+ }
195
+ .hide-facet-checkbox {
196
+ position: absolute;
197
+ clip: rect(0, 0, 0, 0);
198
+ pointer-events: none;
199
+ }
200
+ .hide-facet-checkbox:focus-visible + .hide-facet-icon {
201
+ outline-style: auto;
202
+ outline-offset: 2px;
203
+ }
204
+ .hide-facet-icon {
205
+ width: ${checkboxHeight};
206
+ height: ${checkboxHeight};
207
+ cursor: pointer;
208
+ display: flex;
209
+ }
210
+ .eye {
211
+ width: ${checkboxHeight};
212
+ height: ${checkboxHeight};
213
+ opacity: 0.3;
214
+ }
215
+ .hide-facet-icon:hover .eye,
216
+ .active .eye {
217
+ opacity: 1;
218
+ }
219
+ .hide-facet-icon:hover .eye-open,
220
+ .hide-facet-icon .eye-closed {
221
+ display: none;
222
+ }
223
+ .hide-facet-icon:hover .eye-closed,
224
+ .hide-facet-icon.active .eye-closed {
225
+ display: inline;
226
+ }
227
+ .hide-facet-icon.active .eye-open {
228
+ display: none;
229
+ }
230
+ .sorting-icon {
231
+ cursor: pointer;
232
+ }
233
+
234
+ a:link,
235
+ a:visited {
236
+ text-decoration: none;
237
+ color: var(--ia-theme-link-color, #4b64ff);
238
+ }
239
+ a:hover {
240
+ text-decoration: underline;
241
+ }
242
+ `;