@internetarchive/collection-browser 0.4.9-alpha.1 → 0.4.9

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 (263) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +26 -26
  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 +4 -4
  7. package/LICENSE +661 -661
  8. package/README.md +83 -83
  9. package/dist/index.d.ts +9 -9
  10. package/dist/index.js +9 -9
  11. package/dist/src/app-root.d.ts +50 -50
  12. package/dist/src/app-root.js +278 -278
  13. package/dist/src/assets/img/icons/arrow-left.d.ts +2 -2
  14. package/dist/src/assets/img/icons/arrow-left.js +2 -2
  15. package/dist/src/assets/img/icons/arrow-right.d.ts +2 -2
  16. package/dist/src/assets/img/icons/arrow-right.js +2 -2
  17. package/dist/src/assets/img/icons/chevron.d.ts +2 -2
  18. package/dist/src/assets/img/icons/chevron.js +2 -2
  19. package/dist/src/assets/img/icons/empty-query.d.ts +2 -2
  20. package/dist/src/assets/img/icons/empty-query.js +2 -2
  21. package/dist/src/assets/img/icons/eye-closed.d.ts +2 -2
  22. package/dist/src/assets/img/icons/eye-closed.js +2 -2
  23. package/dist/src/assets/img/icons/eye.d.ts +2 -2
  24. package/dist/src/assets/img/icons/eye.js +2 -2
  25. package/dist/src/assets/img/icons/favorite-filled.d.ts +1 -1
  26. package/dist/src/assets/img/icons/favorite-filled.js +2 -2
  27. package/dist/src/assets/img/icons/login-required.d.ts +1 -1
  28. package/dist/src/assets/img/icons/login-required.js +2 -2
  29. package/dist/src/assets/img/icons/mediatype/account.d.ts +1 -1
  30. package/dist/src/assets/img/icons/mediatype/account.js +2 -2
  31. package/dist/src/assets/img/icons/mediatype/audio.d.ts +1 -1
  32. package/dist/src/assets/img/icons/mediatype/audio.js +2 -2
  33. package/dist/src/assets/img/icons/mediatype/collection.d.ts +1 -1
  34. package/dist/src/assets/img/icons/mediatype/collection.js +2 -2
  35. package/dist/src/assets/img/icons/mediatype/data.d.ts +1 -1
  36. package/dist/src/assets/img/icons/mediatype/data.js +2 -2
  37. package/dist/src/assets/img/icons/mediatype/etree.d.ts +1 -1
  38. package/dist/src/assets/img/icons/mediatype/etree.js +2 -2
  39. package/dist/src/assets/img/icons/mediatype/film.d.ts +1 -1
  40. package/dist/src/assets/img/icons/mediatype/film.js +2 -2
  41. package/dist/src/assets/img/icons/mediatype/images.d.ts +1 -1
  42. package/dist/src/assets/img/icons/mediatype/images.js +2 -2
  43. package/dist/src/assets/img/icons/mediatype/radio.d.ts +1 -1
  44. package/dist/src/assets/img/icons/mediatype/radio.js +2 -2
  45. package/dist/src/assets/img/icons/mediatype/software.d.ts +1 -1
  46. package/dist/src/assets/img/icons/mediatype/software.js +2 -2
  47. package/dist/src/assets/img/icons/mediatype/texts.d.ts +1 -1
  48. package/dist/src/assets/img/icons/mediatype/texts.js +2 -2
  49. package/dist/src/assets/img/icons/mediatype/tv.d.ts +1 -1
  50. package/dist/src/assets/img/icons/mediatype/tv.js +2 -2
  51. package/dist/src/assets/img/icons/mediatype/video.d.ts +1 -1
  52. package/dist/src/assets/img/icons/mediatype/video.js +2 -2
  53. package/dist/src/assets/img/icons/mediatype/web.d.ts +1 -1
  54. package/dist/src/assets/img/icons/mediatype/web.js +2 -2
  55. package/dist/src/assets/img/icons/null-result.d.ts +2 -2
  56. package/dist/src/assets/img/icons/null-result.js +2 -2
  57. package/dist/src/assets/img/icons/restricted.d.ts +1 -1
  58. package/dist/src/assets/img/icons/restricted.js +2 -2
  59. package/dist/src/assets/img/icons/reviews.d.ts +1 -1
  60. package/dist/src/assets/img/icons/reviews.js +2 -2
  61. package/dist/src/assets/img/icons/upload.d.ts +1 -1
  62. package/dist/src/assets/img/icons/upload.js +2 -2
  63. package/dist/src/assets/img/icons/views.d.ts +1 -1
  64. package/dist/src/assets/img/icons/views.js +2 -2
  65. package/dist/src/circular-activity-indicator.d.ts +5 -5
  66. package/dist/src/circular-activity-indicator.js +17 -17
  67. package/dist/src/collection-browser.d.ts +281 -281
  68. package/dist/src/collection-browser.js +1204 -1200
  69. package/dist/src/collection-browser.js.map +1 -1
  70. package/dist/src/collection-facets/facet-tombstone-row.d.ts +5 -5
  71. package/dist/src/collection-facets/facet-tombstone-row.js +15 -15
  72. package/dist/src/collection-facets/facets-template.d.ts +16 -16
  73. package/dist/src/collection-facets/facets-template.js +125 -125
  74. package/dist/src/collection-facets/more-facets-content.d.ts +77 -77
  75. package/dist/src/collection-facets/more-facets-content.js +357 -357
  76. package/dist/src/collection-facets/more-facets-pagination.d.ts +36 -36
  77. package/dist/src/collection-facets/more-facets-pagination.js +192 -192
  78. package/dist/src/collection-facets.d.ts +83 -83
  79. package/dist/src/collection-facets.js +392 -392
  80. package/dist/src/collection-facets.js.map +1 -1
  81. package/dist/src/empty-placeholder.d.ts +11 -11
  82. package/dist/src/empty-placeholder.js +42 -42
  83. package/dist/src/language-code-handler/language-code-handler.d.ts +37 -37
  84. package/dist/src/language-code-handler/language-code-handler.js +26 -26
  85. package/dist/src/language-code-handler/language-code-mapping.d.ts +1 -1
  86. package/dist/src/language-code-handler/language-code-mapping.js +562 -562
  87. package/dist/src/mediatype/mediatype-config.d.ts +3 -3
  88. package/dist/src/mediatype/mediatype-config.js +85 -85
  89. package/dist/src/models.d.ts +115 -114
  90. package/dist/src/models.js +125 -125
  91. package/dist/src/models.js.map +1 -1
  92. package/dist/src/restoration-state-handler.d.ts +45 -45
  93. package/dist/src/restoration-state-handler.js +230 -230
  94. package/dist/src/sort-filter-bar/alpha-bar-tooltip.d.ts +6 -0
  95. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js +61 -0
  96. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js.map +1 -0
  97. package/dist/src/sort-filter-bar/alpha-bar.d.ts +20 -12
  98. package/dist/src/sort-filter-bar/alpha-bar.js +166 -58
  99. package/dist/src/sort-filter-bar/alpha-bar.js.map +1 -1
  100. package/dist/src/sort-filter-bar/img/compact.d.ts +1 -1
  101. package/dist/src/sort-filter-bar/img/compact.js +2 -2
  102. package/dist/src/sort-filter-bar/img/list.d.ts +1 -1
  103. package/dist/src/sort-filter-bar/img/list.js +2 -2
  104. package/dist/src/sort-filter-bar/img/sort-triangle.d.ts +1 -1
  105. package/dist/src/sort-filter-bar/img/sort-triangle.js +2 -2
  106. package/dist/src/sort-filter-bar/img/tile.d.ts +1 -1
  107. package/dist/src/sort-filter-bar/img/tile.js +2 -2
  108. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +108 -108
  109. package/dist/src/sort-filter-bar/sort-filter-bar.js +438 -438
  110. package/dist/src/styles/item-image-styles.d.ts +8 -8
  111. package/dist/src/styles/item-image-styles.js +9 -9
  112. package/dist/src/tiles/collection-browser-loading-tile.d.ts +5 -5
  113. package/dist/src/tiles/collection-browser-loading-tile.js +15 -15
  114. package/dist/src/tiles/grid/account-tile.d.ts +20 -17
  115. package/dist/src/tiles/grid/account-tile.js +68 -46
  116. package/dist/src/tiles/grid/account-tile.js.map +1 -1
  117. package/dist/src/tiles/grid/collection-tile.d.ts +17 -14
  118. package/dist/src/tiles/grid/collection-tile.js +75 -53
  119. package/dist/src/tiles/grid/collection-tile.js.map +1 -1
  120. package/dist/src/tiles/grid/item-tile.d.ts +32 -29
  121. package/dist/src/tiles/grid/item-tile.js +126 -108
  122. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  123. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.d.ts +1 -1
  124. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js +44 -8
  125. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js.map +1 -1
  126. package/dist/src/tiles/grid/tile-stats.d.ts +10 -10
  127. package/dist/src/tiles/grid/tile-stats.js +40 -40
  128. package/dist/src/tiles/hover/hover-pane-controller.d.ts +219 -197
  129. package/dist/src/tiles/hover/hover-pane-controller.js +352 -331
  130. package/dist/src/tiles/hover/hover-pane-controller.js.map +1 -1
  131. package/dist/src/tiles/hover/tile-hover-pane.d.ts +15 -15
  132. package/dist/src/tiles/hover/tile-hover-pane.js +38 -38
  133. package/dist/src/tiles/image-block.d.ts +17 -17
  134. package/dist/src/tiles/image-block.js +72 -72
  135. package/dist/src/tiles/item-image.d.ts +35 -35
  136. package/dist/src/tiles/item-image.js +117 -117
  137. package/dist/src/tiles/list/account-label.d.ts +1 -1
  138. package/dist/src/tiles/list/account-label.js +6 -6
  139. package/dist/src/tiles/list/date-label.d.ts +1 -1
  140. package/dist/src/tiles/list/date-label.js +12 -12
  141. package/dist/src/tiles/list/tile-list-compact-header.d.ts +12 -12
  142. package/dist/src/tiles/list/tile-list-compact-header.js +41 -41
  143. package/dist/src/tiles/list/tile-list-compact.d.ts +21 -21
  144. package/dist/src/tiles/list/tile-list-compact.js +99 -99
  145. package/dist/src/tiles/list/tile-list.d.ts +55 -55
  146. package/dist/src/tiles/list/tile-list.js +303 -297
  147. package/dist/src/tiles/list/tile-list.js.map +1 -1
  148. package/dist/src/tiles/mediatype-icon.d.ts +9 -9
  149. package/dist/src/tiles/mediatype-icon.js +47 -47
  150. package/dist/src/tiles/overlay/icon-overlay.d.ts +10 -10
  151. package/dist/src/tiles/overlay/icon-overlay.js +40 -40
  152. package/dist/src/tiles/overlay/icon-text-overlay.d.ts +9 -9
  153. package/dist/src/tiles/overlay/icon-text-overlay.js +38 -38
  154. package/dist/src/tiles/overlay/text-overlay.d.ts +10 -10
  155. package/dist/src/tiles/overlay/text-overlay.js +42 -42
  156. package/dist/src/tiles/text-snippet-block.d.ts +27 -27
  157. package/dist/src/tiles/text-snippet-block.js +73 -73
  158. package/dist/src/tiles/tile-dispatcher.d.ts +58 -55
  159. package/dist/src/tiles/tile-dispatcher.js +203 -179
  160. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  161. package/dist/src/utils/analytics-events.d.ts +22 -22
  162. package/dist/src/utils/analytics-events.js +24 -24
  163. package/dist/src/utils/format-count.d.ts +7 -7
  164. package/dist/src/utils/format-count.js +76 -76
  165. package/dist/src/utils/format-date.d.ts +2 -2
  166. package/dist/src/utils/format-date.js +23 -23
  167. package/dist/src/utils/format-unit-size.d.ts +2 -2
  168. package/dist/src/utils/format-unit-size.js +33 -33
  169. package/dist/test/collection-browser.test.d.ts +1 -1
  170. package/dist/test/collection-browser.test.js +584 -584
  171. package/dist/test/collection-facets/facets-template.test.d.ts +1 -1
  172. package/dist/test/collection-facets/facets-template.test.js +62 -62
  173. package/dist/test/collection-facets/more-facets-content.test.d.ts +1 -1
  174. package/dist/test/collection-facets/more-facets-content.test.js +114 -114
  175. package/dist/test/collection-facets/more-facets-pagination.test.d.ts +1 -1
  176. package/dist/test/collection-facets/more-facets-pagination.test.js +117 -117
  177. package/dist/test/collection-facets.test.d.ts +2 -2
  178. package/dist/test/collection-facets.test.js +508 -508
  179. package/dist/test/empty-placeholder.test.d.ts +1 -1
  180. package/dist/test/empty-placeholder.test.js +33 -33
  181. package/dist/test/icon-overlay.test.d.ts +1 -1
  182. package/dist/test/icon-overlay.test.js +24 -24
  183. package/dist/test/image-block.test.d.ts +1 -1
  184. package/dist/test/image-block.test.js +48 -48
  185. package/dist/test/item-image.test.d.ts +1 -1
  186. package/dist/test/item-image.test.js +84 -84
  187. package/dist/test/mediatype-config.test.d.ts +1 -1
  188. package/dist/test/mediatype-config.test.js +16 -16
  189. package/dist/test/mocks/mock-analytics-handler.d.ts +10 -10
  190. package/dist/test/mocks/mock-analytics-handler.js +15 -15
  191. package/dist/test/mocks/mock-collection-name-cache.d.ts +7 -7
  192. package/dist/test/mocks/mock-collection-name-cache.js +13 -13
  193. package/dist/test/mocks/mock-search-responses.d.ts +12 -12
  194. package/dist/test/mocks/mock-search-responses.js +341 -341
  195. package/dist/test/mocks/mock-search-service.d.ts +13 -13
  196. package/dist/test/mocks/mock-search-service.js +40 -40
  197. package/dist/test/restoration-state-handler.test.d.ts +1 -1
  198. package/dist/test/restoration-state-handler.test.js +125 -125
  199. package/dist/test/sort-filter-bar/alpha-bar-tooltip.test.d.ts +1 -0
  200. package/dist/test/sort-filter-bar/alpha-bar-tooltip.test.js +13 -0
  201. package/dist/test/sort-filter-bar/alpha-bar-tooltip.test.js.map +1 -0
  202. package/dist/test/sort-filter-bar/alpha-bar.test.d.ts +1 -1
  203. package/dist/test/sort-filter-bar/alpha-bar.test.js +73 -43
  204. package/dist/test/sort-filter-bar/alpha-bar.test.js.map +1 -1
  205. package/dist/test/sort-filter-bar/sort-filter-bar.test.d.ts +1 -1
  206. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +197 -197
  207. package/dist/test/text-overlay.test.d.ts +1 -1
  208. package/dist/test/text-overlay.test.js +48 -48
  209. package/dist/test/text-snippet-block.test.d.ts +1 -1
  210. package/dist/test/text-snippet-block.test.js +57 -57
  211. package/dist/test/tile-stats.test.d.ts +1 -1
  212. package/dist/test/tile-stats.test.js +33 -33
  213. package/dist/test/tiles/grid/account-tile.test.d.ts +1 -1
  214. package/dist/test/tiles/grid/account-tile.test.js +81 -60
  215. package/dist/test/tiles/grid/account-tile.test.js.map +1 -1
  216. package/dist/test/tiles/grid/collection-tile.test.d.ts +1 -1
  217. package/dist/test/tiles/grid/collection-tile.test.js +81 -57
  218. package/dist/test/tiles/grid/collection-tile.test.js.map +1 -1
  219. package/dist/test/tiles/grid/item-tile.test.d.ts +1 -1
  220. package/dist/test/tiles/grid/item-tile.test.js +163 -142
  221. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  222. package/dist/test/tiles/hover/hover-pane-controller.test.d.ts +1 -1
  223. package/dist/test/tiles/hover/hover-pane-controller.test.js +257 -257
  224. package/dist/test/tiles/hover/tile-hover-pane.test.d.ts +1 -1
  225. package/dist/test/tiles/hover/tile-hover-pane.test.js +13 -13
  226. package/dist/test/tiles/list/tile-list-compact.test.d.ts +1 -1
  227. package/dist/test/tiles/list/tile-list-compact.test.js +92 -92
  228. package/dist/test/tiles/list/tile-list.test.d.ts +1 -1
  229. package/dist/test/tiles/list/tile-list.test.js +163 -163
  230. package/dist/test/tiles/tile-dispatcher.test.d.ts +1 -0
  231. package/dist/test/tiles/tile-dispatcher.test.js +101 -0
  232. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -0
  233. package/dist/test/utils/format-count.test.d.ts +1 -1
  234. package/dist/test/utils/format-count.test.js +23 -23
  235. package/dist/test/utils/format-date.test.d.ts +1 -1
  236. package/dist/test/utils/format-date.test.js +17 -17
  237. package/dist/test/utils/format-unit-size.test.d.ts +1 -1
  238. package/dist/test/utils/format-unit-size.test.js +17 -17
  239. package/local.archive.org.cert +86 -86
  240. package/local.archive.org.key +27 -27
  241. package/package.json +1 -1
  242. package/renovate.json +6 -6
  243. package/src/collection-browser.ts +4 -0
  244. package/src/collection-facets.ts +3 -5
  245. package/src/models.ts +1 -0
  246. package/src/sort-filter-bar/alpha-bar-tooltip.ts +54 -0
  247. package/src/sort-filter-bar/alpha-bar.ts +133 -9
  248. package/src/tiles/grid/account-tile.ts +23 -1
  249. package/src/tiles/grid/collection-tile.ts +31 -2
  250. package/src/tiles/grid/item-tile.ts +22 -2
  251. package/src/tiles/grid/styles/tile-grid-shared-styles.ts +37 -1
  252. package/src/tiles/hover/hover-pane-controller.ts +38 -1
  253. package/src/tiles/list/tile-list.ts +8 -1
  254. package/src/tiles/tile-dispatcher.ts +29 -2
  255. package/test/sort-filter-bar/alpha-bar-tooltip.test.ts +17 -0
  256. package/test/sort-filter-bar/alpha-bar.test.ts +46 -1
  257. package/test/tiles/grid/account-tile.test.ts +27 -0
  258. package/test/tiles/grid/collection-tile.test.ts +30 -0
  259. package/test/tiles/grid/item-tile.test.ts +27 -0
  260. package/test/tiles/tile-dispatcher.test.ts +122 -0
  261. package/tsconfig.json +21 -21
  262. package/web-dev-server.config.mjs +30 -30
  263. package/web-test-runner.config.mjs +41 -41
@@ -1,13 +1,27 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
1
+ import { LitElement, html, css, nothing, TemplateResult } from 'lit';
2
+ import { customElement, property, state, query } from 'lit/decorators.js';
3
3
  import type { PrefixFilterCounts } from '../models';
4
4
 
5
+ import './alpha-bar-tooltip';
6
+ import type { AlphaBarTooltip } from './alpha-bar-tooltip';
7
+
5
8
  @customElement('alpha-bar')
6
9
  export class AlphaBar extends LitElement {
7
10
  @property({ type: String }) selectedLetter: string | null = null;
8
11
 
9
12
  @property({ type: Object }) letterCounts?: PrefixFilterCounts;
10
13
 
14
+ @state()
15
+ private tooltipShown: boolean = false;
16
+
17
+ @state()
18
+ private hoveredLetter?: string;
19
+
20
+ @query('alpha-bar-tooltip')
21
+ private tooltip?: AlphaBarTooltip;
22
+
23
+ private readonly alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
24
+
11
25
  private get selectedUppercaseLetter(): string | undefined {
12
26
  return this.selectedLetter?.toUpperCase();
13
27
  }
@@ -22,11 +36,14 @@ export class AlphaBar extends LitElement {
22
36
  <li
23
37
  class=${letter === this.selectedUppercaseLetter
24
38
  ? 'selected'
25
- : ''}
39
+ : nothing}
40
+ @mousemove=${this.handleMouseMove}
41
+ @mouseleave=${this.handleMouseLeave}
26
42
  >
27
43
  ${this.letterCounts?.[letter]
28
44
  ? this.letterLinkTemplate(letter)
29
45
  : html`<span>${letter}</span>`}
46
+ ${this.tooltipTemplate(letter)}
30
47
  </li>
31
48
  `
32
49
  )}
@@ -49,6 +66,17 @@ export class AlphaBar extends LitElement {
49
66
  `;
50
67
  }
51
68
 
69
+ private tooltipTemplate(letter: string): TemplateResult | typeof nothing {
70
+ if (this.hoveredLetter !== letter) return nothing;
71
+
72
+ return this.tooltipShown
73
+ ? html`<alpha-bar-tooltip
74
+ data-letter=${letter}
75
+ .numResults=${this.letterCounts?.[this.hoveredLetter] ?? 0}
76
+ ></alpha-bar-tooltip>`
77
+ : nothing;
78
+ }
79
+
52
80
  private letterClicked(letter: string) {
53
81
  if (letter === this.selectedUppercaseLetter) {
54
82
  this.selectedLetter = null;
@@ -62,7 +90,77 @@ export class AlphaBar extends LitElement {
62
90
  );
63
91
  }
64
92
 
65
- private readonly alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
93
+ private async handleMouseMove(e: MouseEvent) {
94
+ const target = e.target as HTMLLIElement;
95
+ if (target && !this.tooltipShown) {
96
+ const targetLetter = target.textContent?.trim() ?? undefined;
97
+ this.tooltipShown = true;
98
+ this.hoveredLetter = targetLetter;
99
+
100
+ await this.updateComplete;
101
+ await new Promise(resolve => {
102
+ setTimeout(resolve, 0);
103
+ });
104
+
105
+ if (this.tooltip && this.tooltip.dataset.letter === targetLetter) {
106
+ this.positionTooltip(target);
107
+ }
108
+ }
109
+ }
110
+
111
+ private handleMouseLeave() {
112
+ this.tooltipShown = false;
113
+ this.hoveredLetter = undefined;
114
+ }
115
+
116
+ private positionTooltip(targetElmt: HTMLElement) {
117
+ if (!this.tooltip) return;
118
+
119
+ // Basic positioning is just to center the whole tooltip on the letter box.
120
+ const tooltipWidth = this.tooltip.clientWidth;
121
+ const letterWidth = targetElmt.clientWidth;
122
+ let left = letterWidth / 2 - tooltipWidth / 2;
123
+
124
+ // But we also need to ensure the tooltip doesn't flow outside the viewport.
125
+ // First, calculate the full width of the document body, including margins
126
+ // (but not including any scrollbar).
127
+ const bodyStyle = getComputedStyle(document.body);
128
+ const bodyMarginLeft = parseFloat(
129
+ bodyStyle.getPropertyValue('margin-left')
130
+ );
131
+ const bodyMarginRight = parseFloat(
132
+ bodyStyle.getPropertyValue('margin-right')
133
+ );
134
+ const bodyWidthWithMargin =
135
+ document.body.clientWidth + bodyMarginLeft + bodyMarginRight;
136
+
137
+ // Calculate the positions of the tooltip's left/right edges, and determine
138
+ // how much they overflow the viewport by (if at all).
139
+ const targetRect = targetElmt.getBoundingClientRect();
140
+ const tooltipLeft = targetRect.left + left;
141
+ const tooltipRight = tooltipLeft + tooltipWidth;
142
+ const offset = 1; // How many pixels the tooltip must be offset from the left/right edges
143
+ let overflowAmt;
144
+ if (tooltipLeft < offset) {
145
+ // Tooltip overflows left edge of viewport
146
+ overflowAmt = tooltipLeft - offset;
147
+ } else if (tooltipRight > bodyWidthWithMargin - offset) {
148
+ // Tooltip overflows right edge of viewport
149
+ overflowAmt = tooltipRight - bodyWidthWithMargin + offset;
150
+ }
151
+
152
+ // Apply any needed adjustment to the tooltip and its arrow to keep it in the viewport
153
+ if (overflowAmt) {
154
+ left -= overflowAmt;
155
+ this.tooltip.style.setProperty(
156
+ '--tooltipArrowOffset',
157
+ `${overflowAmt}px`
158
+ );
159
+ }
160
+
161
+ this.tooltip.style.left = `${left}px`;
162
+ this.tooltip.classList.add('fade-in');
163
+ }
66
164
 
67
165
  static styles = css`
68
166
  h1 {
@@ -84,30 +182,56 @@ export class AlphaBar extends LitElement {
84
182
  }
85
183
 
86
184
  ul li {
185
+ position: relative;
87
186
  flex: 1;
88
187
  text-align: center;
89
188
  max-width: 2.5rem;
189
+ border-radius: 4px;
190
+ }
191
+
192
+ li:hover:not(.selected) a {
193
+ background-color: #c0c0c0;
194
+ }
195
+
196
+ a,
197
+ span {
198
+ display: flex;
199
+ justify-content: center;
200
+ align-items: center;
201
+ aspect-ratio: 1 / 1;
90
202
  }
91
203
 
92
204
  a {
93
205
  color: #333;
94
206
  text-decoration: none;
95
- padding: 0.5rem 0;
96
- display: block;
207
+ border-radius: 4px;
97
208
  }
98
209
 
99
210
  span {
100
211
  color: #aaa;
101
- padding: 0.5rem 0;
102
- display: block;
212
+ cursor: default;
103
213
  }
104
214
 
105
215
  .selected {
106
- background-color: darkgray;
216
+ background-color: #2c2c2c;
107
217
  }
108
218
 
109
219
  .selected a {
110
220
  color: white;
111
221
  }
222
+
223
+ alpha-bar-tooltip {
224
+ position: absolute;
225
+ top: 100%;
226
+ left: -9999px;
227
+ margin-top: 3px;
228
+
229
+ opacity: 0;
230
+ transition: opacity 0.2s ease;
231
+ }
232
+
233
+ alpha-bar-tooltip.fade-in {
234
+ opacity: 1;
235
+ }
112
236
  `;
113
237
  }
@@ -1,4 +1,4 @@
1
- import { css, html, LitElement, TemplateResult } from 'lit';
1
+ import { css, html, LitElement, nothing, TemplateResult } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
  import type { TileModel } from '../../models';
4
4
 
@@ -12,9 +12,12 @@ export class AccountTile extends LitElement {
12
12
 
13
13
  @property({ type: String }) baseImageUrl?: string;
14
14
 
15
+ @property({ type: Boolean }) showInfoButton = false;
16
+
15
17
  render() {
16
18
  return html`
17
19
  <div class="container">
20
+ ${this.infoButtonTemplate}
18
21
  <div class="tile-details">
19
22
  <div class="item-info">
20
23
  ${this.getAvatarTemplate} ${this.getTitleTemplate}
@@ -59,6 +62,25 @@ export class AccountTile extends LitElement {
59
62
  </tile-stats>`;
60
63
  }
61
64
 
65
+ private get infoButtonTemplate(): TemplateResult | typeof nothing {
66
+ // &#9432; is an information icon
67
+ return this.showInfoButton
68
+ ? html`<button class="info-button" @click=${this.infoButtonPressed}>
69
+ &#9432;
70
+ <span class="sr-only">More info</span>
71
+ </button>`
72
+ : nothing;
73
+ }
74
+
75
+ private infoButtonPressed(e: PointerEvent) {
76
+ e.preventDefault();
77
+ const event = new CustomEvent<{ x: number; y: number }>(
78
+ 'infoButtonPressed',
79
+ { detail: { x: e.clientX, y: e.clientY } }
80
+ );
81
+ this.dispatchEvent(event);
82
+ }
83
+
62
84
  /**
63
85
  * CSS
64
86
  */
@@ -1,4 +1,11 @@
1
- import { css, CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
1
+ import {
2
+ css,
3
+ CSSResultGroup,
4
+ html,
5
+ LitElement,
6
+ nothing,
7
+ TemplateResult,
8
+ } from 'lit';
2
9
  import { customElement, property } from 'lit/decorators.js';
3
10
  import { collectionIcon } from '../../assets/img/icons/mediatype/collection';
4
11
  import type { TileModel } from '../../models';
@@ -12,9 +19,12 @@ export class CollectionTile extends LitElement {
12
19
 
13
20
  @property({ type: String }) baseImageUrl?: string;
14
21
 
22
+ @property({ type: Boolean }) showInfoButton = false;
23
+
15
24
  render() {
16
25
  return html`
17
26
  <div class="container">
27
+ ${this.infoButtonTemplate}
18
28
  <div class="tile-details">
19
29
  <div class="item-info">
20
30
  ${this.getImageBlockTemplate} ${this.getTitleTemplate}
@@ -56,7 +66,7 @@ export class CollectionTile extends LitElement {
56
66
  }
57
67
 
58
68
  private get getItemsTemplate() {
59
- const collectionItems = this.model?.itemCount.toLocaleString();
69
+ const collectionItems = this.model?.itemCount?.toLocaleString();
60
70
 
61
71
  return html`<span id="item-count"
62
72
  >${collectionItems} item${Number(collectionItems) !== 1 ? 's' : ''}</span
@@ -71,6 +81,25 @@ export class CollectionTile extends LitElement {
71
81
  : ``;
72
82
  }
73
83
 
84
+ private get infoButtonTemplate(): TemplateResult | typeof nothing {
85
+ // &#9432; is an information icon
86
+ return this.showInfoButton
87
+ ? html`<button class="info-button" @click=${this.infoButtonPressed}>
88
+ &#9432;
89
+ <span class="sr-only">More info</span>
90
+ </button>`
91
+ : nothing;
92
+ }
93
+
94
+ private infoButtonPressed(e: PointerEvent) {
95
+ e.preventDefault();
96
+ const event = new CustomEvent<{ x: number; y: number }>(
97
+ 'infoButtonPressed',
98
+ { detail: { x: e.clientX, y: e.clientY } }
99
+ );
100
+ this.dispatchEvent(event);
101
+ }
102
+
74
103
  static get styles(): CSSResultGroup {
75
104
  const tileBorderColor = css`var(--tileBorderColor, #555555)`;
76
105
  const tileBackgroundColor = css`var(--tileBackgroundColor, #666666)`;
@@ -31,11 +31,14 @@ export class ItemTile extends LitElement {
31
31
 
32
32
  @property({ type: Object }) sortParam?: SortParam;
33
33
 
34
+ @property({ type: Boolean }) showInfoButton = false;
35
+
34
36
  render() {
35
37
  const itemTitle = this.model?.title;
36
38
 
37
39
  return html`
38
40
  <div class="container">
41
+ ${this.infoButtonTemplate}
39
42
  <div class="tile-details">
40
43
  <div class="item-info">
41
44
  ${this.imageBlockTemplate}
@@ -96,8 +99,6 @@ export class ItemTile extends LitElement {
96
99
 
97
100
  private get sortedDateInfoTemplate() {
98
101
  let sortedValue;
99
-
100
- // console.log('model: ', this.model)
101
102
  switch (this.sortParam?.field) {
102
103
  case 'date':
103
104
  sortedValue = { field: 'published', value: this.model?.datePublished };
@@ -127,6 +128,16 @@ export class ItemTile extends LitElement {
127
128
  `;
128
129
  }
129
130
 
131
+ private get infoButtonTemplate(): TemplateResult | typeof nothing {
132
+ // &#9432; is an information icon
133
+ return this.showInfoButton
134
+ ? html`<button class="info-button" @click=${this.infoButtonPressed}>
135
+ &#9432;
136
+ <span class="sr-only">More info</span>
137
+ </button>`
138
+ : nothing;
139
+ }
140
+
130
141
  private get textSnippetsTemplate(): TemplateResult | typeof nothing {
131
142
  if (!this.hasSnippets) return nothing;
132
143
 
@@ -158,6 +169,15 @@ export class ItemTile extends LitElement {
158
169
  return !!this.model?.snippets?.length;
159
170
  }
160
171
 
172
+ private infoButtonPressed(e: PointerEvent): void {
173
+ e.preventDefault();
174
+ const event = new CustomEvent<{ x: number; y: number }>(
175
+ 'infoButtonPressed',
176
+ { detail: { x: e.clientX, y: e.clientY } }
177
+ );
178
+ this.dispatchEvent(event);
179
+ }
180
+
161
181
  /**
162
182
  * CSS
163
183
  */
@@ -51,7 +51,7 @@ export const baseTileStyles = css`
51
51
  .archivist-since {
52
52
  display: flex;
53
53
  justify-content: left;
54
- align-items: flex-end; /* Important to start text from bottom */
54
+ align-items: flex-start;
55
55
  padding: 0 5px;
56
56
  }
57
57
 
@@ -97,7 +97,43 @@ export const baseTileStyles = css`
97
97
  text-decoration: underline;
98
98
  }
99
99
 
100
+ .info-button {
101
+ position: absolute;
102
+ right: 10px;
103
+ top: 10px;
104
+ margin: 0;
105
+ padding: 0;
106
+ border: none;
107
+ border-radius: 50%;
108
+ display: flex;
109
+ justify-content: center;
110
+ align-items: center;
111
+ background: rgba(220, 220, 220, 0.5);
112
+ color: white;
113
+ font-size: 2rem;
114
+ font-weight: bold;
115
+ line-height: 1;
116
+ text-shadow: black 1px 1px 3px;
117
+ overflow: visible;
118
+ aspect-ratio: 1 / 1;
119
+ z-index: 1;
120
+ }
121
+
100
122
  .hidden {
101
123
  display: none;
102
124
  }
125
+
126
+ .sr-only {
127
+ position: absolute !important;
128
+ width: 1px !important;
129
+ height: 1px !important;
130
+ margin: -1px !important;
131
+ padding: 0 !important;
132
+ border: 0 !important;
133
+ overflow: hidden !important;
134
+ white-space: nowrap !important;
135
+ clip: rect(1px, 1px, 1px, 1px) !important;
136
+ -webkit-clip-path: inset(50%) !important;
137
+ clip-path: inset(50%) !important;
138
+ }
103
139
  `;
@@ -38,6 +38,11 @@ export interface HoverPaneProviderInterface {
38
38
  getHoverPaneProps(): HoverPaneProperties;
39
39
  }
40
40
 
41
+ export interface ToggleHoverPaneOptions {
42
+ coords: { x: number; y: number };
43
+ enableTouchBackdrop?: boolean;
44
+ }
45
+
41
46
  /**
42
47
  * An interface for interacting with hover pane controllers (e.g.,
43
48
  * to retrieve their current hover pane template).
@@ -49,6 +54,14 @@ export interface HoverPaneControllerInterface extends ReactiveController {
49
54
  * pane should not currently be rendered.
50
55
  */
51
56
  getTemplate(): HTMLTemplateResult | typeof nothing;
57
+
58
+ /**
59
+ * Requests to manually toggle the state of the hover pane.
60
+ * If the hover pane is already shown, it will begin fading out and then
61
+ * subsequently be hidden and removed. If the hover pane is already fading
62
+ * out or hidden, it will fade back in and be shown.
63
+ */
64
+ toggleHoverPane(options: ToggleHoverPaneOptions): void;
52
65
  }
53
66
 
54
67
  const clamp = (val: number, min = -Infinity, max = Infinity) =>
@@ -123,6 +136,12 @@ export class HoverPaneController implements HoverPaneControllerInterface {
123
136
  /** The timer ID for recognizing a long press event */
124
137
  private longPressTimer?: number;
125
138
 
139
+ /**
140
+ * Whether the touch backdrop should currently be rendered irrespective of other touch
141
+ * interactions being enabled.
142
+ */
143
+ private forceTouchBackdrop: boolean = false;
144
+
126
145
  /** A record of the last mouse position on the host element, for positioning the hover pane */
127
146
  private lastPointerClientPos = { x: 0, y: 0 };
128
147
 
@@ -173,6 +192,18 @@ export class HoverPaneController implements HoverPaneControllerInterface {
173
192
  : nothing;
174
193
  }
175
194
 
195
+ /** @inheritdoc */
196
+ toggleHoverPane(options: ToggleHoverPaneOptions): void {
197
+ if (this.hoverPaneState === 'shown') {
198
+ this.fadeOutHoverPane();
199
+ this.forceTouchBackdrop = false;
200
+ } else {
201
+ this.lastPointerClientPos = options.coords;
202
+ this.forceTouchBackdrop = options.enableTouchBackdrop ?? false;
203
+ this.showHoverPane();
204
+ }
205
+ }
206
+
176
207
  /**
177
208
  * Produces a template for the invisible touch capture backdrop that
178
209
  * is used to cancel the hover pane on touch devices. We want any
@@ -181,7 +212,7 @@ export class HoverPaneController implements HoverPaneControllerInterface {
181
212
  * affect the state of the hover pane (e.g., fading it back in).
182
213
  */
183
214
  private get touchBackdropTemplate(): HTMLTemplateResult | typeof nothing {
184
- return this.isTouchEnabled && this.enableLongPress
215
+ return this.showTouchBackdrop
185
216
  ? html`<div
186
217
  id="touch-backdrop"
187
218
  @touchstart=${this.handleBackdropInteraction}
@@ -195,6 +226,12 @@ export class HoverPaneController implements HoverPaneControllerInterface {
195
226
  : nothing;
196
227
  }
197
228
 
229
+ private get showTouchBackdrop(): boolean {
230
+ return (
231
+ (this.isTouchEnabled && this.enableLongPress) || this.forceTouchBackdrop
232
+ );
233
+ }
234
+
198
235
  /** Whether to use the mobile layout */
199
236
  private get isMobileView(): boolean {
200
237
  return !!this.mobileBreakpoint && window.innerWidth < this.mobileBreakpoint;
@@ -140,7 +140,14 @@ export class TileList extends LitElement {
140
140
  if (!this.model?.title) {
141
141
  return nothing;
142
142
  }
143
- return html` ${this.detailsLink(this.model.identifier, this.model.title)} `;
143
+
144
+ // If the model has a server-specified href, use it
145
+ // Otherwise construct a details link using the identifier
146
+ return this.model?.href
147
+ ? html`<a href="${this.baseNavigationUrl}${this.model.href}"
148
+ >${this.model.title ?? this.model.identifier}</a
149
+ >`
150
+ : this.detailsLink(this.model.identifier, this.model.title);
144
151
  }
145
152
 
146
153
  private get itemLineTemplate() {
@@ -122,7 +122,7 @@ export class TileDispatcher
122
122
  private get linkTileTemplate() {
123
123
  return html`
124
124
  <a
125
- href="${this.baseNavigationUrl}/details/${this.model?.identifier}"
125
+ href="${this.linkTileHref}"
126
126
  title=${this.shouldPrepareHoverPane
127
127
  ? nothing // Don't show title tooltips when we have the tile info popups
128
128
  : ifDefined(this.model?.title)}
@@ -136,6 +136,14 @@ export class TileDispatcher
136
136
  `;
137
137
  }
138
138
 
139
+ private get linkTileHref() {
140
+ // Use the server-specified href if available.
141
+ // Otherwise, construct a details page URL from the item identifier.
142
+ return this.model?.href
143
+ ? `${this.baseNavigationUrl}${this.model?.href}`
144
+ : `${this.baseNavigationUrl}/details/${this.model?.identifier}`;
145
+ }
146
+
139
147
  /**
140
148
  * Whether hover pane behavior should be prepared for this tile
141
149
  * (e.g., whether mouse listeners should be attached, etc.)
@@ -148,6 +156,10 @@ export class TileDispatcher
148
156
  );
149
157
  }
150
158
 
159
+ private get isHoverEnabled(): boolean {
160
+ return window.matchMedia('(hover: hover)').matches;
161
+ }
162
+
151
163
  /** @inheritdoc */
152
164
  getHoverPane(): TileHoverPane | undefined {
153
165
  return this.hoverPane;
@@ -192,6 +204,15 @@ export class TileDispatcher
192
204
  }
193
205
  }
194
206
 
207
+ private tileInfoButtonPressed(
208
+ e: CustomEvent<{ x: number; y: number }>
209
+ ): void {
210
+ this.hoverPaneController?.toggleHoverPane({
211
+ coords: e.detail,
212
+ enableTouchBackdrop: true,
213
+ });
214
+ }
215
+
195
216
  private get tile() {
196
217
  const {
197
218
  model,
@@ -213,6 +234,8 @@ export class TileDispatcher
213
234
  .baseImageUrl=${this.baseImageUrl}
214
235
  .currentWidth=${currentWidth}
215
236
  .currentHeight=${currentHeight}
237
+ ?showInfoButton=${!this.isHoverEnabled}
238
+ @infoButtonPressed=${this.tileInfoButtonPressed}
216
239
  >
217
240
  </collection-tile>`;
218
241
  case 'account':
@@ -221,6 +244,8 @@ export class TileDispatcher
221
244
  .baseImageUrl=${this.baseImageUrl}
222
245
  .currentWidth=${currentWidth}
223
246
  .currentHeight=${currentHeight}
247
+ ?showInfoButton=${!this.isHoverEnabled}
248
+ @infoButtonPressed=${this.tileInfoButtonPressed}
224
249
  >
225
250
  </account-tile>`;
226
251
  default:
@@ -232,6 +257,8 @@ export class TileDispatcher
232
257
  .baseImageUrl=${this.baseImageUrl}
233
258
  .sortParam=${sortParam}
234
259
  .loggedIn=${this.loggedIn}
260
+ ?showInfoButton=${!this.isHoverEnabled}
261
+ @infoButtonPressed=${this.tileInfoButtonPressed}
235
262
  >
236
263
  </item-tile>`;
237
264
  }
@@ -318,7 +345,7 @@ export class TileDispatcher
318
345
  height: 100vh;
319
346
  top: 0;
320
347
  left: 0;
321
- z-index: 1;
348
+ z-index: 2;
322
349
  background: transparent;
323
350
  }
324
351
 
@@ -0,0 +1,17 @@
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import type { AlphaBarTooltip } from '../../src/sort-filter-bar/alpha-bar-tooltip';
4
+
5
+ import '../../src/sort-filter-bar/alpha-bar-tooltip';
6
+
7
+ describe('Alphabet Filter Bar Tooltips', () => {
8
+ it('renders component', async () => {
9
+ const el = await fixture<AlphaBarTooltip>(
10
+ html`<alpha-bar-tooltip .numResults=${42}></alpha-bar-tooltip>`
11
+ );
12
+
13
+ // Should render the number of results
14
+ const tooltipText = el.shadowRoot?.querySelector('#tooltip-text');
15
+ expect(tooltipText?.textContent?.trim()).to.equal('42 results');
16
+ });
17
+ });
@@ -1,8 +1,9 @@
1
- import { expect, fixture } from '@open-wc/testing';
1
+ import { aTimeout, expect, fixture } from '@open-wc/testing';
2
2
  import { html } from 'lit';
3
3
  import type { AlphaBar } from '../../src/sort-filter-bar/alpha-bar';
4
4
 
5
5
  import '../../src/sort-filter-bar/alpha-bar';
6
+ import type { AlphaBarTooltip } from '../../src/sort-filter-bar/alpha-bar-tooltip';
6
7
 
7
8
  describe('Alphabetical Filter Bar', () => {
8
9
  it('renders component', async () => {
@@ -49,4 +50,48 @@ describe('Alphabetical Filter Bar', () => {
49
50
  expect(selectedLetter).to.exist;
50
51
  expect(selectedLetter?.textContent?.trim()).to.equal('B');
51
52
  });
53
+
54
+ it('renders a tooltip when hovered and removes it when un-hovered', async () => {
55
+ const el = await fixture<AlphaBar>(html`<alpha-bar></alpha-bar>`);
56
+ const firstLetter = el.shadowRoot?.querySelector('li') as HTMLLIElement;
57
+ expect(firstLetter).to.exist;
58
+
59
+ firstLetter.dispatchEvent(new MouseEvent('mousemove'));
60
+ await el.updateComplete;
61
+
62
+ const tooltip = el.shadowRoot?.querySelector(
63
+ 'alpha-bar-tooltip'
64
+ ) as AlphaBarTooltip;
65
+ expect(tooltip).to.exist;
66
+
67
+ // Should be positioned after next tick
68
+ await aTimeout(0);
69
+ expect(tooltip.style.left).to.exist;
70
+
71
+ firstLetter.dispatchEvent(new MouseEvent('mouseleave'));
72
+ await el.updateComplete;
73
+
74
+ expect(el.shadowRoot?.querySelector('alpha-bar-tooltip')).not.to.exist;
75
+ });
76
+
77
+ it('positions tooltip correctly when it would overflow viewport', async () => {
78
+ const el = await fixture<AlphaBar>(html`<alpha-bar></alpha-bar>`);
79
+ const letters = el.shadowRoot?.querySelectorAll('li');
80
+ const lastLetter = letters?.item(letters.length - 1) as HTMLLIElement;
81
+ expect(lastLetter).to.exist;
82
+
83
+ lastLetter.dispatchEvent(new MouseEvent('mousemove'));
84
+ await el.updateComplete;
85
+
86
+ const tooltip = el.shadowRoot?.querySelector(
87
+ 'alpha-bar-tooltip'
88
+ ) as AlphaBarTooltip;
89
+ expect(tooltip).to.exist;
90
+
91
+ // Should be positioned after next tick, but not off-screen
92
+ await aTimeout(0);
93
+ expect(tooltip.getBoundingClientRect().right).to.be.lessThan(
94
+ window.innerWidth
95
+ );
96
+ });
52
97
  });