@abreen/tada 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,9 +71,11 @@ A static site generator. The successor to Presto.
71
71
  * Special heading subtitles with `## Heading # A subtitle here`
72
72
  * `{{{ _partial.md }}}` syntax for including partials
73
73
 
74
- - Extremely fast client-side navigation
75
- * A miniature SPA without the bloat of a framework
76
- * Highly efficient, thoroughly covered by cross-platform browser tests
74
+ - Extremely efficient client-side navigation
75
+ * A miniature SPA that uses internal links tagged at build time
76
+ * Get the benefits of DOM tree replacement without an entire bloated framework
77
+ * Thoroughly covered by browser tests across multiple engines
78
+ * Clear loading indicators for slow requests
77
79
  * Also detects a new version of the current page and supplies a reload button
78
80
 
79
81
  ### Experimental features
package/bin/tada.ts CHANGED
@@ -50,7 +50,7 @@ function parsePortArg(name: string): number | undefined {
50
50
  const idx = process.argv.indexOf(name);
51
51
  if (idx !== -1 && process.argv[idx + 1]) {
52
52
  const val = parseInt(process.argv[idx + 1], 10);
53
- if (val > 0 && val < 65536) {
53
+ if (val >= 0 && val < 65536) {
54
54
  return val;
55
55
  }
56
56
  }
@@ -2,59 +2,24 @@ import path from 'path';
2
2
  import { normalizeOutputPath } from './util';
3
3
  import type { HtmlOutputAnalysis } from './types';
4
4
 
5
- function normalizeInternalTarget(target: string): string {
6
- return normalizeOutputPath(target);
7
- }
8
-
5
+ /** HTML candidates for an already normalized target without a non-HTML extension. */
9
6
  function toCandidateHtmlAssetPaths(target: string): string[] {
10
- const normalizedTarget = normalizeInternalTarget(target);
11
-
12
- if (normalizedTarget === '/') {
7
+ if (target === '/') {
13
8
  return ['index.html'];
14
9
  }
15
10
 
16
- if (normalizedTarget.endsWith('.html')) {
17
- return [normalizedTarget.slice(1)];
18
- }
19
-
20
- if (path.posix.extname(normalizedTarget)) {
21
- return [];
11
+ if (target.endsWith('.html')) {
12
+ return [target.slice(1)];
22
13
  }
23
14
 
24
- const withoutLeadingSlash = normalizedTarget.slice(1);
25
- if (normalizedTarget.endsWith('/')) {
15
+ const withoutLeadingSlash = target.slice(1);
16
+ if (target.endsWith('/')) {
26
17
  return [`${withoutLeadingSlash}index.html`];
27
18
  }
28
19
 
29
20
  return [`${withoutLeadingSlash}/index.html`, `${withoutLeadingSlash}.html`];
30
21
  }
31
22
 
32
- function resolveTargetToAssetTarget(target: string): string | null {
33
- const normalizedTarget = normalizeInternalTarget(target);
34
- if (normalizedTarget === '/' || normalizedTarget.endsWith('.html')) {
35
- return null;
36
- }
37
- return path.posix.extname(normalizedTarget) ? normalizedTarget : null;
38
- }
39
-
40
- function collectOutgoingHtmlAssetPaths(
41
- analysis: HtmlOutputAnalysis,
42
- knownHtmlAssetPaths: Set<string>,
43
- ): string[] {
44
- const htmlAssetPaths = new Set<string>();
45
-
46
- for (const target of analysis.outgoingTargets) {
47
- for (const candidate of toCandidateHtmlAssetPaths(target)) {
48
- if (knownHtmlAssetPaths.has(candidate)) {
49
- htmlAssetPaths.add(candidate);
50
- break;
51
- }
52
- }
53
- }
54
-
55
- return [...htmlAssetPaths].sort();
56
- }
57
-
58
23
  interface CollectReachableOptions {
59
24
  htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>;
60
25
  knownAssetTargets?: Set<string>;
@@ -75,7 +40,6 @@ export function collectReachableSiteAssets({
75
40
  throw new Error(`Pagefind reachability root not found: ${rootPath}`);
76
41
  }
77
42
 
78
- const knownHtmlAssetPaths = new Set(htmlAnalysisByPath.keys());
79
43
  const reachableHtmlPaths = new Set<string>();
80
44
  const reachableAssetTargets = new Set<string>();
81
45
  const pending: string[] = [rootPath];
@@ -89,19 +53,19 @@ export function collectReachableSiteAssets({
89
53
 
90
54
  const analysis = htmlAnalysisByPath.get(currentPath)!;
91
55
 
92
- for (const targetPath of collectOutgoingHtmlAssetPaths(
93
- analysis,
94
- knownHtmlAssetPaths,
95
- )) {
96
- if (!reachableHtmlPaths.has(targetPath)) {
97
- pending.push(targetPath);
56
+ for (const outgoingTarget of analysis.outgoingTargets) {
57
+ const target = normalizeOutputPath(outgoingTarget);
58
+ if (!target.endsWith('.html') && path.posix.extname(target)) {
59
+ if (knownAssetTargets.has(target)) {
60
+ reachableAssetTargets.add(target);
61
+ }
62
+ continue;
98
63
  }
99
- }
100
-
101
- for (const target of analysis.outgoingTargets) {
102
- const assetTarget = resolveTargetToAssetTarget(target);
103
- if (assetTarget && knownAssetTargets.has(assetTarget)) {
104
- reachableAssetTargets.add(assetTarget);
64
+ const targetPath = toCandidateHtmlAssetPaths(target).find(candidate =>
65
+ htmlAnalysisByPath.has(candidate),
66
+ );
67
+ if (targetPath && !reachableHtmlPaths.has(targetPath)) {
68
+ pending.push(targetPath);
105
69
  }
106
70
  }
107
71
  }
@@ -426,7 +426,7 @@ export function renderCopiedContentAsset({
426
426
  const rawSource = fs.readFileSync(filePath, 'utf-8');
427
427
  const templated = applySourceTemplate(rawSource, siteVariables, filePath);
428
428
 
429
- if (filePath.endsWith('.java')) {
429
+ if (path.extname(filePath).toLowerCase() === '.java') {
430
430
  const pageDirPath = toPosix(
431
431
  path.relative(contentDir, path.dirname(filePath)),
432
432
  );
@@ -568,7 +568,7 @@ function renderPlainTextContent(
568
568
  } {
569
569
  const applyBasePath = createApplyBasePath(siteVariables);
570
570
 
571
- const ext = path.extname(filePath);
571
+ const ext = path.extname(filePath).toLowerCase();
572
572
  const raw = fs.readFileSync(filePath, 'utf-8');
573
573
  const { pageVariables: rawPageVariables, content } =
574
574
  parseFrontMatterAndContent(raw, ext);
@@ -34,18 +34,13 @@ const MIN_ARRAY_CELL_WIDTH = 48;
34
34
 
35
35
  /** Information collected about a heap object across all trace steps. */
36
36
  interface ObjectInfo {
37
- id: string;
38
37
  type: string;
39
38
  firstStep: number;
40
39
  lastStep: number;
41
- /** Set of object IDs this object ever references (across all steps). */
42
- references: Set<string>;
43
40
  /** Step indices where this object is actually present. */
44
41
  activeSteps: Set<number>;
45
42
  /** Per-field reference targets (field name -> set of referenced object IDs). */
46
43
  fieldRefs: Map<string, Set<string>>;
47
- /** Max number of fields seen on this object (for field objects). */
48
- maxFields: number;
49
44
  /** Max number of elements seen on this object (for arrays). */
50
45
  maxElements: number;
51
46
  /** Scalar value (for heap objects represented by a single value). */
@@ -70,7 +65,7 @@ interface TreeNode {
70
65
 
71
66
  /**
72
67
  * Walk every step, collect every heap object's type, lifetime (first/last step),
73
- * all references it ever makes, max fields seen, whether it's an array or string.
68
+ * reference targets by field, display widths, and whether it's an array or string.
74
69
  */
75
70
  export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
76
71
  const objects = new Map<string, ObjectInfo>();
@@ -81,14 +76,11 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
81
76
  let info = objects.get(id);
82
77
  if (!info) {
83
78
  info = {
84
- id,
85
79
  type: obj.type,
86
80
  firstStep: stepIndex,
87
81
  lastStep: stepIndex,
88
- references: new Set(),
89
82
  activeSteps: new Set(),
90
83
  fieldRefs: new Map(),
91
- maxFields: 0,
92
84
  maxElements: 0,
93
85
  value: null,
94
86
  hasValue: false,
@@ -115,18 +107,14 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
115
107
 
116
108
  for (const elem of obj.elements) {
117
109
  if (elem.type === 'ref') {
118
- info.references.add(elem.id);
119
110
  elemRefs.add(elem.id);
120
111
  }
121
112
  }
122
113
  } else if ('fields' in obj) {
123
- const fieldEntries = Object.entries(obj.fields);
124
- info.maxFields = Math.max(info.maxFields, fieldEntries.length);
125
- for (const [fieldName, val] of fieldEntries) {
114
+ for (const [fieldName, val] of Object.entries(obj.fields)) {
126
115
  info.fieldNames.add(fieldName);
127
116
 
128
117
  if (val.type === 'ref') {
129
- info.references.add(val.id);
130
118
  if (!info.fieldRefs.has(fieldName)) {
131
119
  info.fieldRefs.set(fieldName, new Set());
132
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -25,6 +25,8 @@ const PERSISTENT_COMPONENTS: Record<
25
25
  let startTime = -1;
26
26
 
27
27
  document.addEventListener('DOMContentLoaded', async () => {
28
+ const initialHref = window.location.href;
29
+ const initialHash = window.location.hash;
28
30
  startTime = window.performance.now();
29
31
 
30
32
  const failed: Record<string, string> = {};
@@ -54,8 +56,8 @@ document.addEventListener('DOMContentLoaded', async () => {
54
56
  // fragment scroll. Do it ourselves once per-page components have
55
57
  // finished mutating the DOM. On a fresh cold load the browser already
56
58
  // scrolled, so this is a harmless re-align.
57
- if (window.location.hash) {
58
- getHashTarget(window.document, window.location.hash)?.scrollIntoView();
59
+ if (initialHash && window.location.href === initialHref) {
60
+ getHashTarget(window.document, initialHash)?.scrollIntoView();
59
61
  }
60
62
 
61
63
  for (const [name, reason] of Object.entries(failed)) {
@@ -7,6 +7,11 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
7
7
  --page-transition-movement-easing: #{$movement-easing};
8
8
  }
9
9
 
10
+ html.navigation-loading,
11
+ html.navigation-loading * {
12
+ cursor: progress !important;
13
+ }
14
+
10
15
  @keyframes page-fade-out {
11
16
  to {
12
17
  opacity: 0;
@@ -9,6 +9,8 @@ import { swapHeaderTitle } from '../header';
9
9
 
10
10
  export const NAVIGATION_EVENT = 'tada:navigation';
11
11
 
12
+ const LOADING_CURSOR_DELAY = 400;
13
+
12
14
  let currentAbortController: AbortController | null = null;
13
15
  let historyIndex = 0;
14
16
  let currentPath = '';
@@ -235,15 +237,28 @@ export async function navigateToUrl(
235
237
 
236
238
  const header = document.querySelector('header');
237
239
  header?.classList.add('loading');
240
+ const loadingCursorTimeout = window.setTimeout(() => {
241
+ if (isActiveNavigation(controller)) {
242
+ document.documentElement.classList.add('navigation-loading');
243
+ }
244
+ }, LOADING_CURSOR_DELAY);
245
+ const stopLoading = (): void => {
246
+ window.clearTimeout(loadingCursorTimeout);
247
+ if (!isActiveNavigation(controller)) {
248
+ return;
249
+ }
250
+ header?.classList.remove('loading');
251
+ document.documentElement.classList.remove('navigation-loading');
252
+ };
238
253
 
239
254
  let response: Response;
240
255
  try {
241
256
  response = await globals.fetch(url, { signal: controller.signal });
242
257
  } catch (err: unknown) {
258
+ stopLoading();
243
259
  if (!isActiveNavigation(controller)) {
244
260
  return;
245
261
  }
246
- header?.classList.remove('loading');
247
262
  if (isAbortError(err)) {
248
263
  return;
249
264
  }
@@ -252,11 +267,12 @@ export async function navigateToUrl(
252
267
  }
253
268
 
254
269
  if (!isActiveNavigation(controller)) {
270
+ stopLoading();
255
271
  return;
256
272
  }
257
273
 
258
274
  if (!response.ok) {
259
- header?.classList.remove('loading');
275
+ stopLoading();
260
276
  globals.setLocationHref(window, url);
261
277
  return;
262
278
  }
@@ -265,10 +281,10 @@ export async function navigateToUrl(
265
281
  try {
266
282
  html = await response.text();
267
283
  } catch (err: unknown) {
284
+ stopLoading();
268
285
  if (!isActiveNavigation(controller)) {
269
286
  return;
270
287
  }
271
- header?.classList.remove('loading');
272
288
  if (isAbortError(err)) {
273
289
  return;
274
290
  }
@@ -277,10 +293,11 @@ export async function navigateToUrl(
277
293
  }
278
294
 
279
295
  if (!isActiveNavigation(controller)) {
296
+ stopLoading();
280
297
  return;
281
298
  }
282
299
 
283
- header?.classList.remove('loading');
300
+ stopLoading();
284
301
 
285
302
  const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
286
303
  .DOMParser;
@@ -62,12 +62,19 @@ export default (window: Window) => {
62
62
  const bodies =
63
63
  window.document.querySelectorAll<HTMLElement>('.question-a-body');
64
64
  bodies.forEach(el => {
65
+ const content = el.querySelector<HTMLElement>('.question-a-content');
66
+ if (content) {
67
+ content.inert = !el.hasAttribute('data-revealed');
68
+ }
65
69
  el.setAttribute('role', 'button');
66
70
  el.setAttribute('tabindex', '0');
67
71
  el.setAttribute('aria-label', 'Click to reveal answer');
68
72
 
69
73
  const reveal = () => {
70
74
  el.setAttribute('data-revealed', '');
75
+ if (content) {
76
+ content.inert = false;
77
+ }
71
78
  el.removeAttribute('role');
72
79
  el.removeAttribute('tabindex');
73
80
  el.removeAttribute('aria-label');
@@ -94,25 +94,7 @@ async function doSearch(query: string, window: Window): Promise<SearchState> {
94
94
  return { totalResults: grouped.length, results: grouped };
95
95
  }
96
96
 
97
- function applyHighlight(
98
- resultsContainer: HTMLElement,
99
- focusedEl: HTMLElement | null,
100
- ) {
101
- const options = Array.from(
102
- resultsContainer.querySelectorAll('[role="option"]'),
103
- ) as HTMLElement[];
104
- options.forEach(opt => {
105
- const selected = focusedEl !== null && opt.contains(focusedEl);
106
- opt.setAttribute('aria-selected', selected ? 'true' : 'false');
107
- });
108
- }
109
-
110
- function render(
111
- input: HTMLInputElement,
112
- resultsContainer: HTMLElement,
113
- state: State,
114
- loading = false,
115
- ) {
97
+ function render(resultsContainer: HTMLElement, state: State, loading = false) {
116
98
  const doc = resultsContainer.ownerDocument;
117
99
 
118
100
  if (state.showResults) {
@@ -129,16 +111,10 @@ function render(
129
111
  }
130
112
 
131
113
  const ol = doc.createElement('ol');
132
- ol.id = `${input.name}-results`;
133
- ol.role = 'listbox';
134
- ol.tabIndex = -1;
135
114
  ol.setAttribute('aria-label', 'Search results');
136
115
 
137
- const totalVisible = state.results.length;
138
-
139
- state.results.forEach((result, i) => {
116
+ state.results.forEach(result => {
140
117
  const a = doc.createElement('a');
141
- a.id = `result-${i}`;
142
118
  a.className = 'result';
143
119
  a.href = result.url;
144
120
  const isGeneratedPage = Boolean(result.template);
@@ -148,7 +124,6 @@ function render(
148
124
  a.tabIndex = 0;
149
125
 
150
126
  const titleEl = doc.createElement('div');
151
- titleEl.id = `title-${i}`;
152
127
  titleEl.className = 'title';
153
128
  if (result.template === 'code') {
154
129
  titleEl.classList.add('code-page');
@@ -166,12 +141,6 @@ function render(
166
141
  a.appendChild(excerpt);
167
142
 
168
143
  const li = doc.createElement('li');
169
- li.id = `option-${i}`;
170
- li.setAttribute('role', 'option');
171
- li.setAttribute('aria-selected', 'false');
172
- li.setAttribute('aria-setsize', String(totalVisible));
173
- li.setAttribute('aria-posinset', String(i + 1));
174
- li.setAttribute('aria-labelledby', `title-${i}`);
175
144
  li.appendChild(a);
176
145
 
177
146
  const subsToShow = result.subResults;
@@ -265,9 +234,6 @@ function render(
265
234
  resultsContainer.setAttribute('aria-hidden', 'true');
266
235
  resultsContainer.setAttribute('inert', '');
267
236
  }
268
-
269
- input.setAttribute('aria-expanded', String(state.showResults));
270
- input.setAttribute('aria-controls', ol.id);
271
237
  }
272
238
 
273
239
  export default (window: Window) => {
@@ -335,7 +301,7 @@ export default (window: Window) => {
335
301
  pagefindLoadGeneration += 1;
336
302
  console.log(`failed to load Pagefind: ${err}`);
337
303
  if (state.showResults) {
338
- render(input!, resultsContainer, state, false);
304
+ render(resultsContainer, state, false);
339
305
  }
340
306
  }
341
307
  return false;
@@ -362,7 +328,7 @@ export default (window: Window) => {
362
328
 
363
329
  async function update(updateId: number) {
364
330
  if (state.showResults) {
365
- render(input!, resultsContainer, state, true);
331
+ render(resultsContainer, state, true);
366
332
  }
367
333
  const query = state.value;
368
334
  if (!(await loadPagefind()) || updateId !== latestUpdateId) {
@@ -380,7 +346,7 @@ export default (window: Window) => {
380
346
  if (!state.showResults) {
381
347
  return;
382
348
  }
383
- render(input!, resultsContainer, state, false);
349
+ render(resultsContainer, state, false);
384
350
  }
385
351
 
386
352
  function queueUpdate() {
@@ -402,7 +368,7 @@ export default (window: Window) => {
402
368
  .catch(err => {
403
369
  console.log(`failed to load Pagefind: ${err}`);
404
370
  if (state.showResults) {
405
- render(input!, resultsContainer, state, false);
371
+ render(resultsContainer, state, false);
406
372
  }
407
373
  });
408
374
  }
@@ -418,7 +384,7 @@ export default (window: Window) => {
418
384
  const stalePagefind = pagefind;
419
385
  invalidatePagefind();
420
386
  if (state.showResults) {
421
- render(input!, resultsContainer, state, true);
387
+ render(resultsContainer, state, true);
422
388
  }
423
389
  if (stalePagefind) {
424
390
  try {
@@ -434,7 +400,7 @@ export default (window: Window) => {
434
400
  reloadPagefindAfterPageUpdateRefresh().catch(err => {
435
401
  console.log(`failed to reload Pagefind: ${err}`);
436
402
  if (state.showResults) {
437
- render(input!, resultsContainer, state, false);
403
+ render(resultsContainer, state, false);
438
404
  }
439
405
  });
440
406
  }
@@ -446,7 +412,7 @@ export default (window: Window) => {
446
412
  return;
447
413
  }
448
414
  state.showResults = false;
449
- render(input!, resultsContainer, state);
415
+ render(resultsContainer, state);
450
416
  }
451
417
 
452
418
  function handleInput(e: Event) {
@@ -573,23 +539,11 @@ export default (window: Window) => {
573
539
  }
574
540
  } else if (e.key === 'Escape') {
575
541
  e.preventDefault();
576
- hide();
577
542
  input!.focus();
543
+ hide();
578
544
  }
579
545
  }
580
546
 
581
- function handleResultsFocusIn(e: FocusEvent) {
582
- applyHighlight(resultsContainer, e.target as HTMLElement);
583
- }
584
-
585
- function handleResultsFocusOut(e: FocusEvent) {
586
- const related = e.relatedTarget as HTMLElement | null;
587
- if (related && (resultsContainer.contains(related) || related === input)) {
588
- return;
589
- }
590
- applyHighlight(resultsContainer, null);
591
- }
592
-
593
547
  function handleWindowPointerDown(e: PointerEvent) {
594
548
  const target = e.target as Node;
595
549
  if (!input!.contains(target) && !resultsContainer.contains(target)) {
@@ -616,8 +570,6 @@ export default (window: Window) => {
616
570
  resultsContainer.addEventListener('pointerdown', handlePointerDown);
617
571
  resultsContainer.addEventListener('click', handleResultClick);
618
572
  resultsContainer.addEventListener('keydown', handleResultsKeyDown);
619
- resultsContainer.addEventListener('focusin', handleResultsFocusIn);
620
- resultsContainer.addEventListener('focusout', handleResultsFocusOut);
621
573
  window.addEventListener('pointerup', handleWindowPointerUp);
622
574
  window.addEventListener('pointerdown', handleWindowPointerDown);
623
575
  window.addEventListener('pointermove', handleWindowPointerMove);
@@ -637,8 +589,6 @@ export default (window: Window) => {
637
589
  window.removeEventListener('pointermove', handleWindowPointerMove);
638
590
  window.removeEventListener('pointerdown', handleWindowPointerDown);
639
591
  window.removeEventListener('pointerup', handleWindowPointerUp);
640
- resultsContainer.removeEventListener('focusout', handleResultsFocusOut);
641
- resultsContainer.removeEventListener('focusin', handleResultsFocusIn);
642
592
  resultsContainer.removeEventListener('keydown', handleResultsKeyDown);
643
593
  resultsContainer.removeEventListener('click', handleResultClick);
644
594
  resultsContainer.removeEventListener('pointerdown', handlePointerDown);
package/src/top/README.md CHANGED
@@ -1,4 +1,9 @@
1
1
  # The `top` component
2
2
 
3
- Show a floating "back to top" button once the page is scrolled more than 250px,
4
- and keep it visible until the page is back near the top.
3
+ Show a floating "back to top" button on long pages once the reader has scrolled
4
+ well down the page, and keep it visible until they are back above that point.
5
+
6
+ - The page must be at least 3 viewports tall; shorter pages never show it.
7
+ - It shows once the scroll position passes 1.5 viewports or 25% of the
8
+ scrollable distance, whichever is farther.
9
+ - Visibility is recalculated on scroll and on resize.
package/src/top/index.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import { debounce, removeClass } from '../util';
2
2
  import { globals } from '../globals';
3
3
 
4
- /** Show the "Back to top" button once the user is past this scroll position */
5
- const SHOW_THRESHOLD_PX = 250;
4
+ /** Only show the "Back to top" button on pages at least this many viewports tall */
5
+ const MIN_PAGE_SCREENS = 3;
6
+
7
+ /** Show the button once the user is this many viewports down the page... */
8
+ const SHOW_AFTER_SCREENS = 1.5;
9
+
10
+ /** ...or this fraction of the scrollable distance, whichever is farther */
11
+ const SHOW_AFTER_FRACTION = 0.25;
6
12
 
7
13
  /** Debounce time (maximum amount of time to wait before updates) */
8
14
  const LATENCY_MS = 50;
@@ -60,8 +66,22 @@ export default (window: Window) => {
60
66
  isShowing = false;
61
67
  }
62
68
 
69
+ function shouldShow(): boolean {
70
+ const viewportHeight = window.innerHeight;
71
+ const maxScroll =
72
+ window.document.documentElement.scrollHeight - viewportHeight;
73
+ if (maxScroll < (MIN_PAGE_SCREENS - 1) * viewportHeight) {
74
+ return false;
75
+ }
76
+ const threshold = Math.max(
77
+ SHOW_AFTER_SCREENS * viewportHeight,
78
+ SHOW_AFTER_FRACTION * maxScroll,
79
+ );
80
+ return window.scrollY > threshold;
81
+ }
82
+
63
83
  function updateVisibility() {
64
- if (window.scrollY > SHOW_THRESHOLD_PX) {
84
+ if (shouldShow()) {
65
85
  show(link);
66
86
  } else {
67
87
  hide(link);
@@ -70,9 +90,11 @@ export default (window: Window) => {
70
90
 
71
91
  const debounced = debounce(window, updateVisibility, LATENCY_MS);
72
92
  window.addEventListener('scroll', debounced, { passive: true });
93
+ window.addEventListener('resize', debounced, { passive: true });
73
94
  updateVisibility();
74
95
 
75
96
  return () => {
76
97
  window.removeEventListener('scroll', debounced);
98
+ window.removeEventListener('resize', debounced);
77
99
  };
78
100
  };
@@ -192,20 +192,17 @@
192
192
  <div class="header-controls">
193
193
  <div id="to-top-container" class="to-top-container"></div>
194
194
  <% if (site.features.search) { %>
195
- <div class="search-controls">
195
+ <div class="search-controls" role="search">
196
196
  <input type="search" class="search quick-search"
197
197
  autocomplete="off" name="quick-search"
198
198
  placeholder="Search"
199
- role="combobox"
200
- aria-haspopup="listbox"
201
- aria-autocomplete="list"
202
- aria-expanded="false"
199
+ aria-label="Search"
203
200
  aria-controls="quick-search-results"
204
201
  disabled />
205
202
  </div>
206
203
  <% } %>
207
204
  </div>
208
- <div class="results-container" role="region"
205
+ <div id="quick-search-results" class="results-container" role="region"
209
206
  aria-label="Search results"
210
207
  aria-live="polite" aria-hidden="true" inert>
211
208
  <div class="results" style="display:none"></div>