@internetarchive/collection-browser 0.4.12 → 0.4.13

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 (43) hide show
  1. package/dist/src/collection-browser.js +3 -0
  2. package/dist/src/collection-browser.js.map +1 -1
  3. package/dist/src/collection-facets.js +1 -10
  4. package/dist/src/collection-facets.js.map +1 -1
  5. package/dist/src/models.d.ts +5 -1
  6. package/dist/src/models.js +19 -8
  7. package/dist/src/models.js.map +1 -1
  8. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js +1 -2
  9. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js.map +1 -1
  10. package/dist/src/sort-filter-bar/img/list.js +1 -1
  11. package/dist/src/sort-filter-bar/img/list.js.map +1 -1
  12. package/dist/src/sort-filter-bar/img/sort-toggle-disabled.d.ts +1 -0
  13. package/dist/src/sort-filter-bar/img/sort-toggle-disabled.js +15 -0
  14. package/dist/src/sort-filter-bar/img/sort-toggle-disabled.js.map +1 -0
  15. package/dist/src/sort-filter-bar/img/sort-toggle-down.d.ts +1 -0
  16. package/dist/src/sort-filter-bar/img/sort-toggle-down.js +17 -0
  17. package/dist/src/sort-filter-bar/img/sort-toggle-down.js.map +1 -0
  18. package/dist/src/sort-filter-bar/img/sort-toggle-up.d.ts +1 -0
  19. package/dist/src/sort-filter-bar/img/sort-toggle-up.js +17 -0
  20. package/dist/src/sort-filter-bar/img/sort-toggle-up.js.map +1 -0
  21. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +95 -14
  22. package/dist/src/sort-filter-bar/sort-filter-bar.js +386 -292
  23. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  24. package/dist/src/tiles/grid/item-tile.js +9 -5
  25. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  26. package/dist/src/tiles/grid/tile-stats.d.ts +1 -0
  27. package/dist/src/tiles/grid/tile-stats.js +5 -1
  28. package/dist/src/tiles/grid/tile-stats.js.map +1 -1
  29. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +241 -68
  30. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  31. package/package.json +2 -1
  32. package/src/collection-browser.ts +3 -0
  33. package/src/collection-facets.ts +1 -10
  34. package/src/models.ts +23 -8
  35. package/src/sort-filter-bar/alpha-bar-tooltip.ts +1 -2
  36. package/src/sort-filter-bar/img/list.ts +1 -1
  37. package/src/sort-filter-bar/img/sort-toggle-disabled.ts +15 -0
  38. package/src/sort-filter-bar/img/sort-toggle-down.ts +17 -0
  39. package/src/sort-filter-bar/img/sort-toggle-up.ts +17 -0
  40. package/src/sort-filter-bar/sort-filter-bar.ts +433 -303
  41. package/src/tiles/grid/item-tile.ts +6 -1
  42. package/src/tiles/grid/tile-stats.ts +3 -1
  43. package/test/sort-filter-bar/sort-filter-bar.test.ts +377 -101
@@ -11,8 +11,12 @@ import type {
11
11
  SharedResizeObserverInterface,
12
12
  SharedResizeObserverResizeHandlerInterface,
13
13
  } from '@internetarchive/shared-resize-observer';
14
+ import '@internetarchive/ia-dropdown';
15
+ import type { IaDropdown, optionInterface } from '@internetarchive/ia-dropdown';
16
+ import type { SortDirection } from '@internetarchive/search-service';
14
17
  import {
15
18
  CollectionDisplayMode,
19
+ DefaultSortDirection,
16
20
  PrefixFilterCounts,
17
21
  PrefixFilterType,
18
22
  SortField,
@@ -20,7 +24,9 @@ import {
20
24
  } from '../models';
21
25
  import './alpha-bar';
22
26
 
23
- import { sortIcon } from './img/sort-triangle';
27
+ import { sortUpIcon } from './img/sort-toggle-up';
28
+ import { sortDownIcon } from './img/sort-toggle-down';
29
+ import { sortDisabledIcon } from './img/sort-toggle-disabled';
24
30
  import { tileIcon } from './img/tile';
25
31
  import { listIcon } from './img/list';
26
32
  import { compactIcon } from './img/compact';
@@ -32,18 +38,25 @@ export class SortFilterBar
32
38
  extends LitElement
33
39
  implements SharedResizeObserverResizeHandlerInterface
34
40
  {
41
+ /** Which display mode the tiles are being rendered with (grid/list-detail/list-compact) */
35
42
  @property({ type: String }) displayMode?: CollectionDisplayMode;
36
43
 
44
+ /** The current sort direction (asc/desc), or null if none is set */
37
45
  @property({ type: String }) sortDirection: 'asc' | 'desc' | null = null;
38
46
 
47
+ /** The field currently being sorted on (e.g., 'title'). Defaults to relevance. */
39
48
  @property({ type: String }) selectedSort: SortField = SortField.relevance;
40
49
 
50
+ /** The currently selected title letter filter, or null if none is set */
41
51
  @property({ type: String }) selectedTitleFilter: string | null = null;
42
52
 
53
+ /** The currently selected creator letter filter, or null if none is set */
43
54
  @property({ type: String }) selectedCreatorFilter: string | null = null;
44
55
 
56
+ /** Whether to show the Relevance sort option (default `true`) */
45
57
  @property({ type: Boolean }) showRelevance: boolean = true;
46
58
 
59
+ /** Maps of result counts for letters on the alphabet bar, for each letter filter type */
47
60
  @property({ type: Object }) prefixFilterCountMap?: Record<
48
61
  PrefixFilterType,
49
62
  PrefixFilterCounts
@@ -51,31 +64,64 @@ export class SortFilterBar
51
64
 
52
65
  @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
53
66
 
67
+ /**
68
+ * Which of the alphabet bars (title/creator) should be shown, or null if one
69
+ * should not currently be rendered.
70
+ */
54
71
  @state() alphaSelectorVisible: AlphaSelector | null = null;
55
72
 
56
- @state() dateSortSelectorVisible = false;
57
-
58
- @state() viewSortSelectorVisible = false;
73
+ /**
74
+ * Whether the transparent backdrop to catch clicks outside the dropdown menu
75
+ * should be rendered.
76
+ */
77
+ @state() dropdownBackdropVisible = false;
59
78
 
60
- @state() desktopSelectorBarWidth = 0;
79
+ /**
80
+ * The width of the desktop view sort option container, updated upon each resize.
81
+ * Used for dynamically determining whether to use desktop or mobile view.
82
+ */
83
+ @state() desktopSortContainerWidth = 0;
61
84
 
85
+ /**
86
+ * The width of the full sort bar, updated upon each resize.
87
+ * Used for dynamically determining whether to use desktop or mobile view.
88
+ */
62
89
  @state() selectorBarContainerWidth = 0;
63
90
 
64
- @state() hoveringOverDateSortOptions = false;
65
-
66
- @query('#desktop-sort-selector')
67
- private desktopSortSelector!: HTMLUListElement;
91
+ /**
92
+ * The container for all the desktop view's sort options.
93
+ * Used for dynamically determining whether to use desktop or mobile view.
94
+ */
95
+ @query('#desktop-sort-container')
96
+ private desktopSortContainer!: HTMLUListElement;
68
97
 
98
+ /**
99
+ * The container for the full sort bar.
100
+ * Used for dynamically determining whether to use desktop or mobile view.
101
+ */
69
102
  @query('#sort-selector-container')
70
103
  private sortSelectorContainer!: HTMLDivElement;
71
104
 
105
+ /** The dropdown component containing options for weekly and all-time views */
106
+ @query('#views-dropdown')
107
+ private viewsDropdown!: IaDropdown;
108
+
109
+ /** The dropdown component containing the four date options */
110
+ @query('#date-dropdown')
111
+ private dateDropdown!: IaDropdown;
112
+
113
+ /** The single, consolidated dropdown component shown in mobile view */
114
+ @query('#mobile-dropdown')
115
+ private mobileDropdown!: IaDropdown;
116
+
72
117
  render() {
73
118
  return html`
74
119
  <div id="container">
75
120
  <div id="sort-bar">
76
- <div id="sort-direction-container">
121
+ <div class="sort-direction-container">
77
122
  ${this.sortDirectionSelectorTemplate}
78
123
  </div>
124
+ <span class="sort-by-text">Sort by:</span>
79
125
 
80
126
  <div id="sort-selector-container">
81
127
  ${this.mobileSortSelectorTemplate}
@@ -85,15 +131,8 @@ export class SortFilterBar
85
131
  <div id="display-style-selector">${this.displayOptionTemplate}</div>
86
132
  </div>
87
133
 
88
- ${this.viewSortSelectorVisible && !this.mobileSelectorVisible
89
- ? this.viewSortSelector
90
- : nothing}
91
- ${this.dateSortSelectorVisible && !this.mobileSelectorVisible
92
- ? this.dateSortSelector
93
- : nothing}
134
+ ${this.dropdownBackdropVisible ? this.dropdownBackdrop : nothing}
94
135
  ${this.alphaBarTemplate}
95
-
96
- <div id="bottom-shadow"></div>
97
136
  </div>
98
137
  `;
99
138
  }
@@ -104,7 +143,7 @@ export class SortFilterBar
104
143
  }
105
144
 
106
145
  if (changed.has('selectedSort') && this.sortDirection === null) {
107
- this.sortDirection = 'desc';
146
+ this.sortDirection = DefaultSortDirection[this.selectedSort];
108
147
  }
109
148
 
110
149
  if (changed.has('selectedTitleFilter') && this.selectedTitleFilter) {
@@ -115,10 +154,7 @@ export class SortFilterBar
115
154
  this.alphaSelectorVisible = 'creator';
116
155
  }
117
156
 
118
- if (
119
- changed.has('dateSortSelectorVisible') ||
120
- changed.has('viewSortSelectorVisible')
121
- ) {
157
+ if (changed.has('dropdownBackdropVisible')) {
122
158
  this.setupEscapeListeners();
123
159
  }
124
160
 
@@ -132,7 +168,7 @@ export class SortFilterBar
132
168
  }
133
169
 
134
170
  private setupEscapeListeners() {
135
- if (this.dateSortSelectorVisible || this.viewSortSelectorVisible) {
171
+ if (this.dropdownBackdropVisible) {
136
172
  document.addEventListener(
137
173
  'keydown',
138
174
  this.boundSortBarSelectorEscapeListener
@@ -147,8 +183,7 @@ export class SortFilterBar
147
183
 
148
184
  private boundSortBarSelectorEscapeListener = (e: KeyboardEvent) => {
149
185
  if (e.key === 'Escape') {
150
- this.viewSortSelectorVisible = false;
151
- this.dateSortSelectorVisible = false;
186
+ this.closeDropdowns();
152
187
  }
153
188
  };
154
189
 
@@ -167,7 +202,7 @@ export class SortFilterBar
167
202
  });
168
203
 
169
204
  resizeObserver.removeObserver({
170
- target: this.desktopSortSelector,
205
+ target: this.desktopSortContainer,
171
206
  handler: this,
172
207
  });
173
208
  }
@@ -180,15 +215,31 @@ export class SortFilterBar
180
215
  });
181
216
 
182
217
  this.resizeObserver.addObserver({
183
- target: this.desktopSortSelector,
218
+ target: this.desktopSortContainer,
184
219
  handler: this,
185
220
  });
186
221
  }
187
222
 
223
+ handleResize(entry: ResizeObserverEntry): void {
224
+ if (entry.target === this.desktopSortContainer) {
225
+ this.desktopSortContainerWidth = entry.contentRect.width;
226
+ } else if (entry.target === this.sortSelectorContainer) {
227
+ this.selectorBarContainerWidth = entry.contentRect.width;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Whether to show the mobile sort bar because there is not enough space
233
+ * for the desktop sort bar.
234
+ */
188
235
  private get mobileSelectorVisible() {
189
- return this.selectorBarContainerWidth - 10 < this.desktopSelectorBarWidth;
236
+ return this.selectorBarContainerWidth - 10 < this.desktopSortContainerWidth;
190
237
  }
191
238
 
239
+ /**
240
+ * Template to render the alphabet bar, or `nothing` if it should not be rendered
241
+ * for the current sort
242
+ */
192
243
  private get alphaBarTemplate(): TemplateResult | typeof nothing {
193
244
  if (!['title', 'creator'].includes(this.selectedSort)) return nothing;
194
245
 
@@ -204,122 +255,120 @@ export class SortFilterBar
204
255
  return nothing;
205
256
  }
206
257
 
207
- handleResize(entry: ResizeObserverEntry): void {
208
- if (entry.target === this.desktopSortSelector) {
209
- this.desktopSelectorBarWidth = entry.contentRect.width;
210
- } else if (entry.target === this.sortSelectorContainer) {
211
- this.selectorBarContainerWidth = entry.contentRect.width;
212
- }
258
+ /** Template to render the sort direction toggle button */
259
+ private get sortDirectionSelectorTemplate(): TemplateResult {
260
+ return html`
261
+ <button
262
+ class="sort-direction-selector"
263
+ ?disabled=${this.selectedSort === 'relevance'}
264
+ @click=${this.toggleSortDirection}
265
+ >
266
+ ${this.sortDirectionIcon}
267
+ </button>
268
+ `;
213
269
  }
214
270
 
215
- private get sortDirectionSelectorTemplate() {
271
+ /** Template to render the sort direction button's icon in the correct current state */
272
+ private get sortDirectionIcon(): TemplateResult {
273
+ // For relevance sort, show a fully disabled icon
274
+ if (this.selectedSort === 'relevance') {
275
+ return html`<div class="sort-direction-icon">${sortDisabledIcon}</div>`;
276
+ }
277
+
278
+ // For all other sorts, show the ascending/descending direction
216
279
  return html`
217
- <div id="sort-direction-selector">
218
- <button
219
- id="sort-ascending-btn"
220
- class="sort-button ${this.sortDirection === 'asc' ? 'selected' : ''}"
221
- ?disabled=${this.selectedSort === 'relevance'}
222
- @click=${() => {
223
- this.setSortDirections('asc');
224
- }}
225
- >
226
- ${sortIcon}
227
- </button>
228
- <button
229
- id="sort-descending-btn"
230
- class="sort-button ${this.sortDirection === 'desc' ? 'selected' : ''}"
231
- ?disabled=${this.selectedSort === 'relevance'}
232
- @click=${() => {
233
- this.setSortDirections('desc');
234
- }}
235
- >
236
- ${sortIcon}
237
- </button>
280
+ <div class="sort-direction-icon">
281
+ ${this.sortDirection === 'asc' ? sortUpIcon : sortDownIcon}
238
282
  </div>
239
283
  `;
240
284
  }
241
285
 
286
+ /** The template to render all the sort options in desktop view */
242
287
  private get desktopSortSelectorTemplate() {
243
288
  return html`
244
- <ul
245
- id="desktop-sort-selector"
289
+ <div
290
+ id="desktop-sort-container"
246
291
  class=${this.mobileSelectorVisible ? 'hidden' : 'visible'}
247
292
  >
248
- <li id="sort-by-text">Sort By</li>
249
- <li>
250
- ${this.showRelevance
251
- ? this.getSortDisplayOption(SortField.relevance)
252
- : nothing}
253
- </li>
254
- <li>
255
- ${this.getSortDisplayOption(SortField.weeklyview, {
256
- clickEvent: () => {
257
- if (!this.viewOptionSelected)
258
- this.setSelectedSort(SortField.weeklyview);
259
- this.viewSortSelectorVisible = !this.viewSortSelectorVisible;
260
- this.dateSortSelectorVisible = false;
261
- this.alphaSelectorVisible = null;
262
- this.selectedTitleFilter = null;
263
- this.selectedCreatorFilter = null;
264
- this.emitTitleLetterChangedEvent();
265
- this.emitCreatorLetterChangedEvent();
266
- },
267
- displayName: html`${this.viewSortField}`,
268
- isSelected: () => this.viewOptionSelected,
269
- })}
270
- </li>
271
- <li>
272
- ${this.getSortDisplayOption(SortField.title, {
273
- clickEvent: () => {
274
- this.alphaSelectorVisible = 'title';
275
- this.selectedCreatorFilter = null;
276
- this.dateSortSelectorVisible = false;
277
- this.viewSortSelectorVisible = false;
278
- this.setSelectedSort(SortField.title);
279
- this.emitCreatorLetterChangedEvent();
280
- },
281
- })}
282
- </li>
283
- <li>
284
- ${this.getSortDisplayOption(SortField.date, {
285
- clickEvent: () => {
286
- if (!this.dateOptionSelected)
287
- this.setSelectedSort(SortField.date);
288
- this.dateSortSelectorVisible = !this.dateSortSelectorVisible;
289
- this.viewSortSelectorVisible = false;
290
- this.alphaSelectorVisible = null;
291
- this.selectedTitleFilter = null;
292
- this.selectedCreatorFilter = null;
293
- this.emitTitleLetterChangedEvent();
294
- this.emitCreatorLetterChangedEvent();
295
- },
296
- displayName: html`${this.dateSortField}`,
297
- isSelected: () => this.dateOptionSelected,
298
- })}
299
- </li>
300
- <li>
301
- ${this.getSortDisplayOption(SortField.creator, {
302
- clickEvent: () => {
303
- this.alphaSelectorVisible = 'creator';
304
- this.selectedTitleFilter = null;
305
- this.dateSortSelectorVisible = false;
306
- this.setSelectedSort(SortField.creator);
307
- this.emitTitleLetterChangedEvent();
308
- },
309
- })}
310
- </li>
311
- </ul>
293
+ <ul id="desktop-sort-selector">
294
+ <li>
295
+ ${this.showRelevance
296
+ ? this.getSortDisplayOption(SortField.relevance, {
297
+ onClick: () => {
298
+ this.clearAlphaBarFilters();
299
+ this.dropdownBackdropVisible = false;
300
+ this.setSelectedSort(SortField.relevance);
301
+ this.emitTitleLetterChangedEvent();
302
+ this.emitCreatorLetterChangedEvent();
303
+ },
304
+ })
305
+ : nothing}
306
+ </li>
307
+ <li>${this.viewsDropdownTemplate}</li>
308
+ <li>
309
+ ${this.getSortDisplayOption(SortField.title, {
310
+ onClick: () => {
311
+ this.alphaSelectorVisible = 'title';
312
+ this.selectedCreatorFilter = null;
313
+ this.dropdownBackdropVisible = false;
314
+ this.setSelectedSort(SortField.title);
315
+ this.emitCreatorLetterChangedEvent();
316
+ },
317
+ })}
318
+ </li>
319
+ <li>${this.dateDropdownTemplate}</li>
320
+ <li>
321
+ ${this.getSortDisplayOption(SortField.creator, {
322
+ onClick: () => {
323
+ this.alphaSelectorVisible = 'creator';
324
+ this.selectedTitleFilter = null;
325
+ this.dropdownBackdropVisible = false;
326
+ this.setSelectedSort(SortField.creator);
327
+ this.emitTitleLetterChangedEvent();
328
+ },
329
+ })}
330
+ </li>
331
+ </ul>
332
+ </div>
333
+ `;
334
+ }
335
+
336
+ /** The template to render all the sort options in mobile view */
337
+ private get mobileSortSelectorTemplate() {
338
+ return html`
339
+ <div
340
+ id="mobile-sort-container"
341
+ class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
342
+ >
343
+ ${this.getSortDropdown({
344
+ displayName: html`${SortFieldDisplayName[this.selectedSort] ?? ''}`,
345
+ id: 'mobile-dropdown',
346
+ isSelected: () => true,
347
+ dropdownOptions: Object.keys(SortField).map(field =>
348
+ this.getDropdownOption(field as SortField)
349
+ ),
350
+ selectedOption: this.selectedSort ?? SortField.relevance,
351
+ onOptionSelected: this.mobileSortChanged,
352
+ onDropdownClick: () => {
353
+ this.dropdownBackdropVisible = this.mobileDropdown.open;
354
+ this.mobileDropdown.classList.toggle(
355
+ 'open',
356
+ this.mobileDropdown.open
357
+ );
358
+ },
359
+ })}
360
+ </div>
312
361
  `;
313
362
  }
314
363
 
315
364
  /**
316
- * This generates each of the sort option links.
365
+ * This generates each of the non-dropdown sort option links.
317
366
  *
318
367
  * It manages the display value and the selected state of the option.
319
368
  *
320
369
  * @param sortField
321
370
  * @param options {
322
- * additionalClickEvent?: () => void; If this is provided, it will also be called when the option is clicked.
371
+ * onClick?: (e: Event) => void; If this is provided, it will also be called when the option is clicked.
323
372
  * displayName?: TemplateResult; The name to display for the option. Defaults to the sortField display name.
324
373
  * isSelected?: () => boolean; A function that returns true if the option is selected. Defaults to the selectedSort === sortField.
325
374
  * }
@@ -328,9 +377,9 @@ export class SortFilterBar
328
377
  private getSortDisplayOption(
329
378
  sortField: SortField,
330
379
  options?: {
331
- clickEvent?: (e: Event) => void;
332
- isSelected?: () => boolean;
333
380
  displayName?: TemplateResult;
381
+ onClick?: (e: Event) => void;
382
+ isSelected?: () => boolean;
334
383
  }
335
384
  ): TemplateResult {
336
385
  const isSelected =
@@ -341,46 +390,151 @@ export class SortFilterBar
341
390
  href="#"
342
391
  @click=${(e: Event) => {
343
392
  e.preventDefault();
344
- if (options?.clickEvent) {
345
- options.clickEvent(e);
346
- } else {
347
- this.alphaSelectorVisible = null;
348
- this.dateSortSelectorVisible = false;
349
- this.selectedTitleFilter = null;
350
- this.selectedCreatorFilter = null;
351
- this.setSelectedSort(sortField);
352
- this.emitTitleLetterChangedEvent();
353
- this.emitCreatorLetterChangedEvent();
354
- }
393
+ options?.onClick?.(e);
355
394
  }}
356
- class=${isSelected() ? 'selected' : ''}
395
+ class=${isSelected() ? 'selected' : nothing}
357
396
  >
358
397
  ${displayName}
359
398
  </a>
360
399
  `;
361
400
  }
362
401
 
363
- private get mobileSortSelectorTemplate() {
402
+ /**
403
+ * Generates a dropdown component containing multiple grouped sort options.
404
+ *
405
+ * @param options.displayName The name to use for the dropdown's visible label
406
+ * @param options.id The id to apply to the dropdown element
407
+ * @param options.dropdownOptions An array of option objects used to populate the dropdown
408
+ * @param options.selectedOption The id of the option that should be initially selected
409
+ * @param options.isSelected A function returning a boolean indicating whether this dropdown
410
+ * should use its selected appearance
411
+ * @param options.onOptionSelected A handler for optionSelected events coming from the dropdown
412
+ * @param options.onDropdownClick A handler for click events on the dropdown
413
+ * @param options.onLabelInteraction A handler for click events and Enter/Space keydown events
414
+ * on the dropdown's label
415
+ */
416
+ private getSortDropdown(options: {
417
+ displayName: TemplateResult;
418
+ id?: string;
419
+ dropdownOptions: optionInterface[];
420
+ selectedOption?: string;
421
+ isSelected?: () => boolean;
422
+ onOptionSelected?: (e: CustomEvent<{ option: optionInterface }>) => void;
423
+ onDropdownClick?: (e: PointerEvent) => void;
424
+ onLabelInteraction?: () => void;
425
+ }): TemplateResult {
364
426
  return html`
365
- <select
366
- id="mobile-sort-selector"
367
- @change=${this.mobileSortChanged}
368
- class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
427
+ <ia-dropdown
428
+ id=${options.id ?? nothing}
429
+ class=${options.isSelected?.() ? 'selected' : nothing}
430
+ displayCaret
431
+ closeOnSelect
432
+ includeSelectedOption
433
+ .openViaButton=${false}
434
+ .options=${options.dropdownOptions}
435
+ .selectedOption=${options.selectedOption ?? ''}
436
+ @optionSelected=${options.onOptionSelected ?? nothing}
437
+ @click=${options.onDropdownClick ?? nothing}
369
438
  >
370
- ${Object.keys(SortField).map(
371
- field => html`
372
- <option value="${field}" ?selected=${this.selectedSort === field}>
373
- ${SortFieldDisplayName[field as SortField]}
374
- </option>
375
- `
376
- )}
377
- </select>
439
+ <span
440
+ class="dropdown-label"
441
+ slot="dropdown-label"
442
+ @click=${options.onLabelInteraction ?? nothing}
443
+ @keydown=${options.onLabelInteraction
444
+ ? (e: KeyboardEvent) => {
445
+ if (e.key === 'Enter' || e.key === ' ') {
446
+ options.onLabelInteraction?.();
447
+ }
448
+ }
449
+ : nothing}
450
+ >
451
+ ${options.displayName}
452
+ </span>
453
+ </ia-dropdown>
378
454
  `;
379
455
  }
380
456
 
381
- private mobileSortChanged(e: Event) {
382
- const target = e.target as HTMLSelectElement;
383
- const sortField = target.value as SortField;
457
+ /** Generates a single dropdown option object for the given sort field */
458
+ private getDropdownOption(sortField: SortField): optionInterface {
459
+ return {
460
+ id: sortField,
461
+ selectedHandler: () => {
462
+ this.selectDropdownSortField(sortField);
463
+ },
464
+ label: html`
465
+ <span class="dropdown-option-label">
466
+ ${SortFieldDisplayName[sortField]}
467
+ </span>
468
+ `,
469
+ };
470
+ }
471
+
472
+ /** Handler for when any sort dropdown option is selected */
473
+ private dropdownOptionSelected(e: CustomEvent<{ option: optionInterface }>) {
474
+ this.dropdownBackdropVisible = false;
475
+ this.clearAlphaBarFilters();
476
+ this.setSelectedSort(e.detail.option.id as SortField);
477
+ this.emitTitleLetterChangedEvent();
478
+ this.emitCreatorLetterChangedEvent();
479
+ }
480
+
481
+ /** The template to render for the views dropdown */
482
+ private get viewsDropdownTemplate(): TemplateResult {
483
+ return this.getSortDropdown({
484
+ displayName: html`${this.viewSortField}`,
485
+ id: 'views-dropdown',
486
+ isSelected: () => this.viewOptionSelected,
487
+ dropdownOptions: [
488
+ this.getDropdownOption(SortField.weeklyview),
489
+ this.getDropdownOption(SortField.alltimeview),
490
+ ],
491
+ selectedOption: this.viewOptionSelected ? this.selectedSort : '',
492
+ onOptionSelected: this.dropdownOptionSelected,
493
+ onDropdownClick: () => {
494
+ this.dateDropdown.open = false;
495
+ this.dropdownBackdropVisible = this.viewsDropdown.open;
496
+ this.viewsDropdown.classList.toggle('open', this.viewsDropdown.open);
497
+ },
498
+ onLabelInteraction: () => {
499
+ if (!this.viewsDropdown.open && !this.viewOptionSelected) {
500
+ this.setSelectedSort(SortField.weeklyview);
501
+ }
502
+ },
503
+ });
504
+ }
505
+
506
+ /** The template to render for the date dropdown */
507
+ private get dateDropdownTemplate(): TemplateResult {
508
+ return this.getSortDropdown({
509
+ displayName: html`${this.dateSortField}`,
510
+ id: 'date-dropdown',
511
+ isSelected: () => this.dateOptionSelected,
512
+ dropdownOptions: [
513
+ this.getDropdownOption(SortField.date),
514
+ this.getDropdownOption(SortField.datearchived),
515
+ this.getDropdownOption(SortField.datereviewed),
516
+ this.getDropdownOption(SortField.dateadded),
517
+ ],
518
+ selectedOption: this.dateOptionSelected ? this.selectedSort : '',
519
+ onOptionSelected: this.dropdownOptionSelected,
520
+ onDropdownClick: () => {
521
+ this.viewsDropdown.open = false;
522
+ this.dropdownBackdropVisible = this.dateDropdown.open;
523
+ this.dateDropdown.classList.toggle('open', this.dateDropdown.open);
524
+ },
525
+ onLabelInteraction: () => {
526
+ if (!this.dateDropdown.open && !this.dateOptionSelected) {
527
+ this.setSelectedSort(SortField.date);
528
+ }
529
+ },
530
+ });
531
+ }
532
+
533
+ /** Handler for when a new mobile sort dropdown option is selected */
534
+ private mobileSortChanged(e: CustomEvent<{ option: optionInterface }>) {
535
+ this.dropdownBackdropVisible = false;
536
+
537
+ const sortField = e.detail.option.id as SortField;
384
538
  this.setSelectedSort(sortField);
385
539
 
386
540
  this.alphaSelectorVisible = null;
@@ -394,6 +548,7 @@ export class SortFilterBar
394
548
  }
395
549
  }
396
550
 
551
+ /** Template for rendering the three display mode options */
397
552
  private get displayOptionTemplate() {
398
553
  return html`
399
554
  <ul>
@@ -437,74 +592,60 @@ export class SortFilterBar
437
592
  `;
438
593
  }
439
594
 
440
- private get dateSortSelector() {
595
+ /**
596
+ * Template for rendering the transparent backdrop to capture clicks outside the
597
+ * dropdown menu while it is open.
598
+ */
599
+ private get dropdownBackdrop() {
441
600
  return html`
442
601
  <div
443
- id="date-sort-selector-backdrop"
444
- @keyup=${() => {
445
- this.dateSortSelectorVisible = false;
446
- }}
447
- @click=${() => {
448
- this.dateSortSelectorVisible = false;
449
- }}
602
+ id="sort-selector-backdrop"
603
+ @keyup=${this.closeDropdowns}
604
+ @click=${this.closeDropdowns}
450
605
  ></div>
451
- <div id="date-sort-selector">
452
- <ul>
453
- <li>${this.getDateSortButton(SortField.datearchived)}</li>
454
- <li>${this.getDateSortButton(SortField.date)}</li>
455
- <li>${this.getDateSortButton(SortField.datereviewed)}</li>
456
- <li>${this.getDateSortButton(SortField.dateadded)}</li>
457
- </ul>
458
- </div>
459
606
  `;
460
607
  }
461
608
 
462
- private get viewSortSelector() {
463
- return html`
464
- <div
465
- id="view-sort-selector-backdrop"
466
- @keyup=${() => {
467
- this.viewSortSelectorVisible = false;
468
- }}
469
- @click=${() => {
470
- this.viewSortSelectorVisible = false;
471
- }}
472
- ></div>
473
- <div id="view-sort-selector">
474
- <ul>
475
- <li>${this.getDateSortButton(SortField.alltimeview)}</li>
476
- <li>${this.getDateSortButton(SortField.weeklyview)}</li>
477
- </ul>
478
- </div>
479
- `;
609
+ /** Closes all of the sorting dropdown components' menus */
610
+ private closeDropdowns() {
611
+ this.dropdownBackdropVisible = false;
612
+ const allDropdowns = [
613
+ this.viewsDropdown,
614
+ this.dateDropdown,
615
+ this.mobileDropdown,
616
+ ];
617
+ for (const dropdown of allDropdowns) {
618
+ dropdown.open = false;
619
+ dropdown.classList.remove('open');
620
+ }
480
621
  }
481
622
 
482
- private getDateSortButton(sortField: SortField) {
483
- return html`
484
- <button
485
- @click=${() => {
486
- this.selectDateSort(sortField);
487
- }}
488
- class=${this.selectedSort === sortField ? 'selected' : ''}
489
- >
490
- ${SortFieldDisplayName[sortField]}
491
- </button>
492
- `;
623
+ private selectDropdownSortField(sortField: SortField) {
624
+ // When a dropdown sort option is selected, we additionally need to clear the backdrop
625
+ this.dropdownBackdropVisible = false;
626
+ this.setSelectedSort(sortField);
493
627
  }
494
628
 
495
- private selectDateSort(sortField: SortField) {
496
- this.dateSortSelectorVisible = false;
497
- this.viewSortSelectorVisible = false;
498
- this.setSelectedSort(sortField);
629
+ private clearAlphaBarFilters() {
630
+ this.alphaSelectorVisible = null;
631
+ this.selectedTitleFilter = null;
632
+ this.selectedCreatorFilter = null;
499
633
  }
500
634
 
501
- private setSortDirections(sortDirection: 'asc' | 'desc') {
635
+ private setSortDirection(sortDirection: SortDirection) {
502
636
  this.sortDirection = sortDirection;
503
637
  this.emitSortChangedEvent();
504
638
  }
505
639
 
640
+ /** Toggles the current sort direction between 'asc' and 'desc' */
641
+ private toggleSortDirection() {
642
+ this.setSortDirection(this.sortDirection === 'desc' ? 'asc' : 'desc');
643
+ }
644
+
506
645
  private setSelectedSort(sort: SortField) {
507
646
  this.selectedSort = sort;
647
+ // Apply this field's default sort direction
648
+ this.sortDirection = DefaultSortDirection[this.selectedSort];
508
649
  this.emitSortChangedEvent();
509
650
  }
510
651
 
@@ -656,57 +797,58 @@ export class SortFilterBar
656
797
  #sort-bar {
657
798
  display: flex;
658
799
  justify-content: space-between;
659
- border: 1px solid rgb(232, 232, 232);
660
800
  align-items: center;
661
- padding: 0.5rem 1.5rem;
662
- }
663
-
664
- #sort-direction-container {
665
- flex: 0;
666
- }
667
-
668
- #sort-by-text {
669
- text-transform: uppercase;
670
- }
671
-
672
- #bottom-shadow {
673
- height: 1px;
674
- width: 100%;
675
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
676
- background-color: #bbb;
801
+ border-bottom: 1px solid #2c2c2c;
802
+ font-size: 1.4rem;
677
803
  }
678
804
 
679
805
  ul {
680
806
  list-style: none;
681
807
  display: flex;
808
+ align-items: center;
682
809
  margin: 0;
683
810
  padding: 0;
684
- align-items: center;
685
811
  }
686
812
 
687
813
  li {
688
814
  padding: 0;
689
815
  }
690
816
 
691
- .sort-button {
692
- background: none;
693
- color: inherit;
694
- border: none;
817
+ .sort-by-text {
818
+ margin-right: 5px;
819
+ font-weight: bold;
820
+ white-space: nowrap;
821
+ }
822
+
823
+ .sort-direction-container {
824
+ display: flex;
825
+ align-self: stretch;
826
+ flex: 0;
827
+ margin: 0 5px;
828
+ }
829
+
830
+ .sort-direction-selector {
695
831
  padding: 0;
832
+ border: none;
833
+ appearance: none;
834
+ background: transparent;
696
835
  cursor: pointer;
697
- outline: inherit;
698
- width: 12px;
699
- height: 12px;
700
- opacity: 0.5;
701
836
  }
702
837
 
703
- .sort-button.selected {
704
- opacity: 1;
838
+ .sort-direction-selector:disabled {
839
+ cursor: default;
705
840
  }
706
841
 
707
- .sort-button:disabled {
708
- opacity: 0.25;
709
- cursor: default;
842
+ .sort-direction-icon {
843
+ display: flex;
844
+ align-items: center;
845
+ background: none;
846
+ color: inherit;
847
+ border: none;
848
+ padding: 0;
849
+ outline: inherit;
850
+ width: 14px;
851
+ height: 14px;
710
852
  }
711
853
 
712
854
  #date-sort-selector,
@@ -722,77 +864,45 @@ export class SortFilterBar
722
864
  border: 1px solid #404142;
723
865
  }
724
866
 
725
- #date-sort-selector button,
726
- #view-sort-selector button {
727
- background: none;
728
- border-radius: 15px;
729
- color: #404142;
730
- border: none;
731
- appearance: none;
732
- cursor: pointer;
733
- -webkit-appearance: none;
734
- font-size: 1.4rem;
735
- font-weight: 400;
736
- padding: 0.5rem 1.2rem;
737
- }
738
-
739
- #date-sort-selector button.selected,
740
- #view-sort-selector button.selected {
741
- background-color: #404142;
742
- color: white;
743
- }
744
-
745
- #show-details {
746
- text-transform: uppercase;
747
- cursor: pointer;
867
+ #sort-selector-container {
868
+ flex: 1;
748
869
  display: flex;
870
+ justify-content: flex-start;
871
+ align-items: center;
749
872
  }
750
873
 
751
- #show-details input {
752
- margin-right: 0.5rem;
753
- flex: 0 0 12px;
754
- }
755
-
756
- #sort-descending-btn {
757
- transform: rotate(180deg);
758
- }
759
-
760
- #sort-direction-selector {
874
+ #desktop-sort-container,
875
+ #mobile-sort-container {
761
876
  display: flex;
762
- flex-direction: column;
763
- gap: 3px;
764
- margin-right: 1rem;
765
- }
766
-
767
- #sort-selector-container {
768
- flex: 1;
877
+ justify-content: flex-start;
878
+ align-items: center;
769
879
  }
770
880
 
771
881
  /*
772
882
  we move the desktop sort selector offscreen instead of display: none
773
883
  because we need to observe the width of it vs its container to determine
774
- if it's wide enough to display the desktop version and if you displY: none,
884
+ if it's wide enough to display the desktop version and if you display: none,
775
885
  the width becomes 0
776
886
  */
777
- #desktop-sort-selector.hidden {
887
+ #desktop-sort-container.hidden {
778
888
  position: absolute;
779
889
  top: -9999px;
780
890
  left: -9999px;
891
+ visibility: hidden;
781
892
  }
782
893
 
783
- #mobile-sort-selector.hidden {
894
+ #mobile-sort-container.hidden {
784
895
  display: none;
785
896
  }
786
897
 
787
- #date-sort-selector-backdrop,
788
- #view-sort-selector-backdrop {
898
+ #sort-selector-backdrop {
789
899
  position: fixed;
790
900
  top: 0;
791
901
  left: 0;
792
- width: 100%;
793
- height: 100%;
902
+ width: 100vw;
903
+ height: 100vh;
794
904
  z-index: 1;
795
- background-color: rgba(255, 255, 255, 0.5);
905
+ background-color: transparent;
796
906
  }
797
907
 
798
908
  #desktop-sort-selector {
@@ -802,34 +912,21 @@ export class SortFilterBar
802
912
  #desktop-sort-selector li {
803
913
  display: flex;
804
914
  align-items: center;
915
+ padding-left: 5px;
916
+ padding-right: 5px;
805
917
  }
806
918
 
807
919
  #desktop-sort-selector li a {
920
+ padding: 0 5px;
808
921
  text-decoration: none;
809
- text-transform: uppercase;
810
- font-size: 1.4rem;
811
922
  color: #333;
812
- line-height: 2.5;
923
+ line-height: 2;
813
924
  }
814
925
 
815
926
  #desktop-sort-selector li a.selected {
816
927
  font-weight: bold;
817
928
  }
818
929
 
819
- #desktop-sort-selector li::after {
820
- content: '•';
821
- padding-left: 1rem;
822
- padding-right: 1rem;
823
- }
824
-
825
- #desktop-sort-selector li:first-child::after {
826
- content: '';
827
- }
828
-
829
- #desktop-sort-selector li:last-child::after {
830
- content: '';
831
- }
832
-
833
930
  #display-style-selector {
834
931
  flex: 0;
835
932
  }
@@ -841,16 +938,49 @@ export class SortFilterBar
841
938
  appearance: none;
842
939
  cursor: pointer;
843
940
  -webkit-appearance: none;
844
- opacity: 0.5;
941
+ fill: #bbbbbb;
845
942
  }
846
943
 
847
944
  #display-style-selector button.active {
848
- opacity: 1;
945
+ fill: var(--ia-theme-primary-text-color, #2c2c2c);
849
946
  }
850
947
 
851
948
  #display-style-selector button svg {
852
949
  width: 24px;
853
950
  height: 24px;
854
951
  }
952
+
953
+ ia-dropdown {
954
+ --dropdownTextColor: white;
955
+ --dropdownOffsetTop: 0;
956
+ --dropdownBorderTopWidth: 0;
957
+ --dropdownBorderTopLeftRadius: 0;
958
+ --dropdownBorderTopRightRadius: 0;
959
+ --dropdownWhiteSpace: nowrap;
960
+ --dropdownListZIndex: 2;
961
+ --dropdownCaretColor: var(--ia-theme-primary-text-color, #2c2c2c);
962
+ --dropdownSelectedTextColor: white;
963
+ --dropdownSelectedBgColor: rgba(255, 255, 255, 0.3);
964
+ --dropdownHoverBgColor: rgba(255, 255, 255, 0.3);
965
+ --caretHeight: 9px;
966
+ --caretWidth: 12px;
967
+ --caretPadding: 0 5px 0 0;
968
+ }
969
+ ia-dropdown.selected .dropdown-label {
970
+ font-weight: bold;
971
+ }
972
+ ia-dropdown.open {
973
+ z-index: 2;
974
+ }
975
+
976
+ .dropdown-label {
977
+ display: inline-block;
978
+ height: 100%;
979
+ padding-left: 5px;
980
+ font-size: 1.4rem;
981
+ line-height: 2;
982
+ color: var(--ia-theme-primary-text-color, #2c2c2c);
983
+ white-space: nowrap;
984
+ }
855
985
  `;
856
986
  }