@abide/abide 0.43.0 → 0.43.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # abide
2
2
 
3
+ ## 0.43.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 943402b: restore reload scroll flash-free via native restoration ([`876b99c`](https://github.com/briancray/abide/commit/876b99cd6e285b6a41afe87ca053cf0631099c03))
8
+ - ed96096: abide check: quote hyphenated component prop keys (`aria-label`, `data-*`) so the props shim parses, and treat `on*` callbacks as ordinary declared props (not DOM passthrough) so a passed required `onsave`/`oncancel` no longer reads as missing while an undeclared handler is still caught
9
+
3
10
  ## 0.43.0
4
11
 
5
12
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.43.0",
3
+ "version": "0.43.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -372,12 +372,15 @@ function emitNode(node: TemplateNode, builder: Builder): void {
372
372
  case 'component': {
373
373
  /* The imported tag resolves (via the shadow host) to the child's
374
374
  `(props: Props) => …` default, so `Parameters<typeof Child>[0]` is its prop
375
- shape. `on*` / `bind:` / `attach` props are framework-handled passthrough
376
- (not part of the declared shape), so they are checked leniently — each value
377
- against its own declared key type — never flagged as excess. */
375
+ shape. `bind:` / `class:` / `style:` / `attach` and `{...spread}` are
376
+ framework directives (not part of the declared shape), so they are checked
377
+ leniently — each value against its own declared key type — never flagged as
378
+ excess. `on*` callbacks ARE ordinary declared props on a component (abide does
379
+ not auto-forward events to the DOM), so they go through the completeness check
380
+ with the rest — otherwise a required `onsave`/`oncancel` reads as missing even
381
+ when passed, and an undeclared handler slips by unflagged. */
378
382
  const handled = (prop: { name: string; spread?: boolean }): boolean =>
379
383
  prop.spread === true ||
380
- prop.name.startsWith('on') ||
381
384
  prop.name.startsWith('bind:') ||
382
385
  prop.name.startsWith('class:') ||
383
386
  prop.name.startsWith('style:') ||
@@ -427,12 +430,23 @@ function emitNode(node: TemplateNode, builder: Builder): void {
427
430
  builder.raw('\n')
428
431
  for (const prop of dataProps) {
429
432
  /* The key mapped to its source name (excess-prop errors land on the key);
430
- the value verbatim-mapped (wrong-type errors land on the value). */
433
+ the value verbatim-mapped (wrong-type errors land on the value).
434
+ Hyphenated names (`aria-label`, `data-*`) aren't valid identifiers, so
435
+ they're wrapped in raw quotes — a string-literal key the parser accepts.
436
+ The quotes are unmapped; an excess-prop error on the literal starts at the
437
+ opening quote and overlaps the mapped name, clamping back onto it. */
438
+ const needsQuoting = !/^[A-Za-z_$][\w$]*$/.test(prop.name)
439
+ if (needsQuoting) {
440
+ builder.raw('"')
441
+ }
431
442
  if (prop.nameLoc !== undefined) {
432
443
  builder.mapped(prop.name, prop.nameLoc)
433
444
  } else {
434
445
  builder.raw(prop.name)
435
446
  }
447
+ if (needsQuoting) {
448
+ builder.raw('"')
449
+ }
436
450
  builder.raw(': ')
437
451
  builder.expr(prop.code, prop.loc)
438
452
  builder.raw(',\n')
@@ -253,9 +253,16 @@ export function router(
253
253
  }
254
254
  const onPageHide = (): void => {
255
255
  /* Mirror the live scroll into the active entry's state before it unloads, so a
256
- reload can recover it — the in-memory bucket is gone and `manual` keeps the
257
- browser from restoring. */
256
+ non-hydratable reload (which tears the SSR DOM down) can recover it from the
257
+ manual bucket. */
258
258
  historyEntries.persist()
259
+ /* Re-enable native scroll restoration for the NEXT document load (a reload): the
260
+ browser restores the SSR DOM's offset before paint, flash-free — where the
261
+ manual bucket, gated behind the async chunk import, would re-apply post-paint
262
+ (a visible scroll). The first paint flips back to `manual` for in-session nav. */
263
+ if ('scrollRestoration' in history) {
264
+ history.scrollRestoration = 'auto'
265
+ }
259
266
  }
260
267
  const onClick = (event: MouseEvent): void => {
261
268
  /* Let the browser own anything that isn't a plain primary-button click:
@@ -297,12 +304,18 @@ export function router(
297
304
  navigatePath(destination.pathname + destination.search + destination.hash)
298
305
  }
299
306
  if (typeof window !== 'undefined') {
300
- /* Own scroll restoration: the browser would restore against the pre-teardown
301
- DOM. Adopt the initial entry's id (survives a reload) and stamp it onto the
302
- landing entry merging so any `scroll` a prior unload persisted into this
303
- entry's state stays put for the first-paint `restore` to recover. */
307
+ /* Scroll restoration is the browser's job on a document load (reload / first
308
+ paint): native restoration runs BEFORE paint, so a hydrating in-place adopt
309
+ returns to the reload offset flash-free (`auto` here also self-heals an entry a
310
+ prior session left `manual`). The first paint then flips to `manual` (below) so
311
+ an in-session back/forward — which tears the page down and rebuilds — restores
312
+ against the new DOM instead of letting the browser restore against the
313
+ torn-down one; `onPageHide` flips back to `auto` for the next load. Still adopt
314
+ the initial entry's id (survives a reload) and stamp it onto the landing entry —
315
+ merging so any `scroll` a prior unload persisted stays put for a non-hydratable
316
+ first paint's manual `restore` to recover. */
304
317
  if ('scrollRestoration' in history) {
305
- history.scrollRestoration = 'manual'
318
+ history.scrollRestoration = 'auto'
306
319
  }
307
320
  historyEntries.adopt(entryOf())
308
321
  const landingState = (history.state as AbideHistoryState | null) ?? {}
@@ -421,6 +434,7 @@ export function router(
421
434
  ) {
422
435
  divergence += 1
423
436
  }
437
+ const firstPaint = first
424
438
  const hydrating = first && pageView?.hydratable === true
425
439
  first = false
426
440
  /* Same page, same layout chain — only params/url differ (e.g. stepping
@@ -483,11 +497,27 @@ export function router(
483
497
  )
484
498
  /* Reapply the destination entry's scroll once its DOM exists — a
485
499
  back/forward restores its offset, a fresh nav scrolls to the `#hash`
486
- anchor (now built) or the top. Runs on the initial paint too: with
487
- `scrollRestoration='manual'` the browser does NOT restore a reload's
488
- offset, so first paint recovers it from the persisted `history.state`
489
- (a fresh load with no persisted offset falls through to hash/top). */
490
- historyEntries.restore(url.hash)
500
+ anchor (now built) or the top. SKIPPED on a hydrating first paint:
501
+ the SSR DOM is adopted in place, so the browser's native restoration
502
+ already returned the entry to its reload offset before paint — the
503
+ manual bucket, gated behind the async chunk import above, would only
504
+ re-apply post-paint (a visible scroll = the flash). A non-hydratable
505
+ first paint tore the SSR DOM down and rebuilt, so it still needs the
506
+ manual restore (recovered from the persisted `history.state`). */
507
+ if (!hydrating) {
508
+ historyEntries.restore(url.hash)
509
+ }
510
+ /* Take over scroll restoration once abide owns the DOM: a later
511
+ same-document back/forward must restore against the rebuilt page, so
512
+ the browser must not. `onPageHide` flips back to `auto` so the next
513
+ document load restores natively (flash-free) again. */
514
+ if (
515
+ firstPaint &&
516
+ typeof history !== 'undefined' &&
517
+ 'scrollRestoration' in history
518
+ ) {
519
+ history.scrollRestoration = 'manual'
520
+ }
491
521
  }
492
522
  /* Build / hydrate is the deterministic surface — a codegen defect or a
493
523
  throw in user render code fails the SAME way every load, so reloading