Skip to content

Commit c3e3bef

Browse files
authored
WEBDEV-6386 Refactor facets to extract individual facet row component (#297)
* Simplify facet event model * Create new component for individual facet row * Clean up now-unused code in facets-template * Update tests to reflect new DOM structure * Fix facet analytics events * Update remaining tests * Add add'l tests for facet template events * DRY up a duplicate method + one more unit test * Minor simplification of facet event detail obj * Improve readability for facet row CSS vars
1 parent 5118f8e commit c3e3bef

File tree

8 files changed

+862
-430
lines changed

8 files changed

+862
-430
lines changed

src/collection-browser.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,27 +1751,26 @@ export class CollectionBrowser
17511751
}
17521752

17531753
facetClickHandler({
1754-
detail: { key, state: facetState, negative },
1754+
detail: { facetType, bucket, negative },
17551755
}: CustomEvent<FacetEventDetails>): void {
1756+
let action: analyticsActions;
17561757
if (negative) {
1757-
this.analyticsHandler?.sendEvent({
1758-
category: this.searchContext,
1759-
action:
1760-
facetState !== 'none'
1761-
? analyticsActions.facetNegativeSelected
1762-
: analyticsActions.facetNegativeDeselected,
1763-
label: key,
1764-
});
1758+
action =
1759+
bucket.state !== 'none'
1760+
? analyticsActions.facetNegativeSelected
1761+
: analyticsActions.facetNegativeDeselected;
17651762
} else {
1766-
this.analyticsHandler?.sendEvent({
1767-
category: this.searchContext,
1768-
action:
1769-
facetState !== 'none'
1770-
? analyticsActions.facetSelected
1771-
: analyticsActions.facetDeselected,
1772-
label: key,
1773-
});
1763+
action =
1764+
bucket.state !== 'none'
1765+
? analyticsActions.facetSelected
1766+
: analyticsActions.facetDeselected;
17741767
}
1768+
1769+
this.analyticsHandler?.sendEvent({
1770+
category: this.searchContext,
1771+
action,
1772+
label: facetType,
1773+
});
17751774
}
17761775

17771776
private async fetchFacets() {

src/collection-facets/facet-row.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import {
2+
css,
3+
html,
4+
LitElement,
5+
TemplateResult,
6+
CSSResultGroup,
7+
nothing,
8+
} from 'lit';
9+
import { customElement, property } from 'lit/decorators.js';
10+
import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
11+
import eyeIcon from '../assets/img/icons/eye';
12+
import eyeClosedIcon from '../assets/img/icons/eye-closed';
13+
import type {
14+
FacetOption,
15+
FacetBucket,
16+
FacetEventDetails,
17+
FacetState,
18+
} from '../models';
19+
20+
@customElement('facet-row')
21+
export class FacetRow extends LitElement {
22+
//
23+
// UI STATE
24+
//
25+
26+
/** The name of the facet group to which this facet belongs (e.g., "mediatype") */
27+
@property({ type: String }) facetType?: FacetOption;
28+
29+
/** The facet bucket containing details about the state, count, and key for this row */
30+
@property({ type: Object }) bucket?: FacetBucket;
31+
32+
/** The collection name cache for converting collection identifiers to titles */
33+
@property({ type: Object })
34+
collectionNameCache?: CollectionNameCacheInterface;
35+
36+
//
37+
// COMPONENT LIFECYCLE METHODS
38+
//
39+
40+
render() {
41+
return html`${this.facetRowTemplate}`;
42+
}
43+
44+
//
45+
// TEMPLATE GETTERS
46+
//
47+
48+
/**
49+
* Template for the full facet row, including the positive/negative checks,
50+
* the display name, and the count.
51+
*/
52+
private get facetRowTemplate(): TemplateResult | typeof nothing {
53+
const { bucket, facetType } = this;
54+
if (!bucket || !facetType) return nothing;
55+
56+
const showOnlyCheckboxId = `${facetType}:${bucket.key}-show-only`;
57+
const negativeCheckboxId = `${facetType}:${bucket.key}-negative`;
58+
59+
// For collections, we need to asynchronously load the collection name
60+
// so we use the `async-collection-name` widget.
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}`
65+
: html`<a href="/details/${bucket.key}">
66+
<async-collection-name
67+
.collectionNameCache=${this.collectionNameCache}
68+
.identifier=${bucket.key}
69+
placeholder="-"
70+
></async-collection-name>
71+
</a> `;
72+
73+
const facetHidden = bucket.state === 'hidden';
74+
const facetSelected = bucket.state === 'selected';
75+
76+
const titleText = `${facetType}: ${bucket.displayText ?? bucket.key}`;
77+
const onlyShowText = facetSelected
78+
? `Show all ${facetType}s`
79+
: `Only show ${titleText}`;
80+
const hideText = `Hide ${titleText}`;
81+
const unhideText = `Unhide ${titleText}`;
82+
const showHideText = facetHidden ? unhideText : hideText;
83+
const ariaLabel = `${titleText}, ${bucket.count} results`;
84+
85+
return html`
86+
<div class="facet-row-container">
87+
<div class="facet-checkboxes">
88+
<input
89+
type="checkbox"
90+
.name=${facetType}
91+
.value=${bucket.key}
92+
@click=${(e: Event) => {
93+
this.facetClicked(e, false);
94+
}}
95+
.checked=${facetSelected}
96+
class="select-facet-checkbox"
97+
title=${onlyShowText}
98+
id=${showOnlyCheckboxId}
99+
/>
100+
<input
101+
type="checkbox"
102+
id=${negativeCheckboxId}
103+
.name=${facetType}
104+
.value=${bucket.key}
105+
@click=${(e: Event) => {
106+
this.facetClicked(e, true);
107+
}}
108+
.checked=${facetHidden}
109+
class="hide-facet-checkbox"
110+
/>
111+
<label
112+
for=${negativeCheckboxId}
113+
class="hide-facet-icon${facetHidden ? ' active' : ''}"
114+
title=${showHideText}
115+
>
116+
<span class="eye">${eyeIcon}</span>
117+
<span class="eye-closed">${eyeClosedIcon}</span>
118+
</label>
119+
</div>
120+
<label
121+
for=${showOnlyCheckboxId}
122+
class="facet-info-display"
123+
title=${onlyShowText}
124+
aria-label=${ariaLabel}
125+
>
126+
<div class="facet-title">${bucketTextDisplay}</div>
127+
<div class="facet-count">${bucket.count.toLocaleString()}</div>
128+
</label>
129+
</div>
130+
`;
131+
}
132+
133+
//
134+
// EVENT HANDLERS & DISPATCHERS
135+
//
136+
137+
/**
138+
* Handler for whenever this facet is clicked & its state changes
139+
*/
140+
private facetClicked(e: Event, negative: boolean) {
141+
const { bucket, facetType } = this;
142+
if (!bucket || !facetType) return;
143+
144+
const target = e.target as HTMLInputElement;
145+
const { checked } = target;
146+
bucket.state = FacetRow.getFacetState(checked, negative);
147+
148+
this.dispatchFacetClickEvent({
149+
facetType,
150+
bucket,
151+
negative,
152+
});
153+
}
154+
155+
/**
156+
* Emits a `facetClick` event with details about this facet & its current state
157+
*/
158+
private dispatchFacetClickEvent(detail: FacetEventDetails) {
159+
const event = new CustomEvent<FacetEventDetails>('facetClick', {
160+
detail,
161+
});
162+
this.dispatchEvent(event);
163+
}
164+
165+
//
166+
// OTHER METHODS
167+
//
168+
169+
/**
170+
* Returns the composed facet state corresponding to a positive or negative facet's checked state
171+
*/
172+
static getFacetState(checked: boolean, negative: boolean): FacetState {
173+
let state: FacetState;
174+
if (checked) {
175+
state = negative ? 'hidden' : 'selected';
176+
} else {
177+
state = 'none';
178+
}
179+
return state;
180+
}
181+
182+
//
183+
// STYLES
184+
//
185+
186+
static get styles(): CSSResultGroup {
187+
const facetRowBorderTop = css`var(--facet-row-border-top, 1px solid transparent)`;
188+
const facetRowBorderBottom = css`var(--facet-row-border-bottom, 1px solid transparent)`;
189+
190+
return css`
191+
async-collection-name {
192+
display: contents;
193+
}
194+
.facet-checkboxes {
195+
margin: 0 5px 0 0;
196+
display: flex;
197+
height: 15px;
198+
}
199+
.facet-checkboxes input:first-child {
200+
margin-right: 5px;
201+
}
202+
.facet-checkboxes input {
203+
height: 15px;
204+
width: 15px;
205+
margin: 0;
206+
}
207+
.facet-row-container {
208+
display: flex;
209+
font-weight: 500;
210+
font-size: 1.2rem;
211+
margin: 2.5px auto;
212+
height: auto;
213+
border-top: ${facetRowBorderTop};
214+
border-bottom: ${facetRowBorderBottom};
215+
overflow: hidden;
216+
}
217+
.facet-info-display {
218+
display: flex;
219+
flex: 1 1 0%;
220+
cursor: pointer;
221+
flex-wrap: wrap;
222+
}
223+
.facet-title {
224+
word-break: break-word;
225+
display: inline-block;
226+
flex: 1 1 0%;
227+
}
228+
.facet-count {
229+
text-align: right;
230+
}
231+
.select-facet-checkbox {
232+
cursor: pointer;
233+
display: inline-block;
234+
}
235+
.hide-facet-checkbox {
236+
display: none;
237+
}
238+
.hide-facet-icon {
239+
width: 15px;
240+
height: 15px;
241+
cursor: pointer;
242+
opacity: 0.3;
243+
display: inline-block;
244+
}
245+
.hide-facet-icon:hover,
246+
.active {
247+
opacity: 1;
248+
}
249+
.hide-facet-icon:hover .eye,
250+
.hide-facet-icon .eye-closed {
251+
display: none;
252+
}
253+
.hide-facet-icon:hover .eye-closed,
254+
.hide-facet-icon.active .eye-closed {
255+
display: inline;
256+
}
257+
.hide-facet-icon.active .eye {
258+
display: none;
259+
}
260+
.sorting-icon {
261+
cursor: pointer;
262+
}
263+
264+
a:link,
265+
a:visited {
266+
text-decoration: none;
267+
color: var(--ia-theme-link-color, #4b64ff);
268+
}
269+
a:hover {
270+
text-decoration: underline;
271+
}
272+
`;
273+
}
274+
}

0 commit comments

Comments
 (0)