stx-router 0.2.456 → 0.2.458

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 (2) hide show
  1. package/dist/client.js +76 -18
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -253,10 +253,34 @@ function routerSource() {
253
253
  // started the navigation, so programmatic and history navigations keep it.
254
254
  var instantNext=false;
255
255
  var instantNav=false;
256
+ // The one element a transition names stx-content: the routed container.
257
+ // The stylesheet used to name every main, #app-content and
258
+ // [data-stx-content], and a View Transition with two elements of one name
259
+ // aborts ("Unexpected duplicate view-transition-name: stx-content"). HQ's
260
+ // public profile has a <main> inside its layout's <main>, so every
261
+ // navigation away from it skipped its transition with that error, and a
262
+ // layout pairing a [data-stx-content] with a <main> did the same. So the
263
+ // name goes on the container alone, before the old page is captured and
264
+ // again once the swap has written the new one.
265
+ var VT_NAME='data-stx-vt-content';
266
+ function nameTransitionContainer(){
267
+ var named=getContainer();
268
+ qsa('['+VT_NAME+']').forEach(function(el){if(el!==named)el.removeAttribute(VT_NAME)});
269
+ if(named&&!named.hasAttribute(VT_NAME))named.setAttribute(VT_NAME,'');
270
+ }
271
+ function namedTransition(callback){
272
+ nameTransitionContainer();
273
+ return document.startViewTransition(function(){
274
+ var out=callback();
275
+ nameTransitionContainer();
276
+ if(out&&typeof out.then==='function')return out.then(function(v){nameTransitionContainer();return v});
277
+ return out;
278
+ });
279
+ }
256
280
  function runViewTransition(callback){
257
281
  if(instantNav||!o.viewTransitions||!document.startViewTransition)return false;
258
282
  try{
259
- var transition=document.startViewTransition(callback);
283
+ var transition=namedTransition(callback);
260
284
  var onAbort=function(err){log('[router] view transition aborted:',err&&err.message?err.message:err)};
261
285
  if(transition&&transition.ready&&transition.ready.catch)transition.ready.catch(onAbort);
262
286
  if(transition&&transition.finished&&transition.finished.catch)transition.finished.catch(onAbort);
@@ -326,7 +350,7 @@ function routerSource() {
326
350
  root.classList.add('stx-instant');
327
351
  var clear=function(){root.classList.remove('stx-instant')};
328
352
  try{
329
- var transition=document.startViewTransition(function(){
353
+ var transition=namedTransition(function(){
330
354
  scriptsPending=0;
331
355
  // The runtime binds the page on stx:load after a short debounce; run
332
356
  // it now, so the frame this reveals is bound rather than showing its
@@ -1341,15 +1365,10 @@ else {
1341
1365
  return el;
1342
1366
  }
1343
1367
 
1344
- function announceRoute(){
1345
- if(o.announceRoute===false)return;
1346
- var c=activeRoot();
1347
- var h=c&&c.querySelector?c.querySelector('h1'):null;
1348
- // The heading names the destination better than the title, which often
1349
- // carries a site-name suffix. Falls back to the title, then gives up
1350
- // rather than announcing an empty string.
1351
- var label=(h&&(h.textContent||'').trim())||document.title||'';
1352
- if(!label)return;
1368
+ // Text the runtime has not bound yet: an interpolation or an @{ } block as
1369
+ // the server sent it. Never worth saying aloud.
1370
+ function unrendered(text){return text.indexOf('{{')!==-1||text.indexOf('}}')!==-1||text.indexOf('@{')!==-1}
1371
+ function speakRoute(label){
1353
1372
  var el=routeAnnouncer();
1354
1373
  // Cleared first: several screen readers ignore a live-region update whose
1355
1374
  // text is identical to what is already there, so navigating between two
@@ -1357,6 +1376,42 @@ else {
1357
1376
  el.textContent='';
1358
1377
  el.textContent=label;
1359
1378
  }
1379
+ // The heading names the destination better than the title, which often
1380
+ // carries a site-name suffix. It is read once the page has bound: read at
1381
+ // the swap, a heading built from the page's state was still the server's
1382
+ // template, and screen readers said "{{ greeting }}, {{ firstName }}" while
1383
+ // the screen showed "Good morning, Chris". So a swap that binds its page
1384
+ // (awaitBind) waits for stx:load and the runtime's bind after it, and a
1385
+ // heading still holding template text is given until ANNOUNCE_WAIT to fill
1386
+ // in. Past that, or with no heading, the title; neither when it too is
1387
+ // unrendered, rather than announcing an expression or an empty string.
1388
+ var ANNOUNCE_WAIT=1500;
1389
+ var announceSeq=0;
1390
+ function announceRoute(awaitBind){
1391
+ if(o.announceRoute===false)return;
1392
+ var seq=++announceSeq,started=Date.now(),bound=!awaitBind,timer=null;
1393
+ function onLoad(){bound=true;schedule(20)}
1394
+ function stop(){if(timer)clearTimeout(timer);timer=null;window.removeEventListener('stx:load',onLoad)}
1395
+ function schedule(ms){if(timer)clearTimeout(timer);timer=setTimeout(attempt,ms)}
1396
+ function attempt(){
1397
+ timer=null;
1398
+ if(seq!==announceSeq){stop();return}
1399
+ var expired=Date.now()-started>=ANNOUNCE_WAIT;
1400
+ if(!bound&&!expired){schedule(50);return}
1401
+ var c=activeRoot();
1402
+ var h=c&&c.querySelector?c.querySelector('h1'):null;
1403
+ var heading=h?(h.textContent||'').trim():'';
1404
+ if(heading&&!unrendered(heading)){stop();speakRoute(heading);return}
1405
+ if(heading&&!expired){schedule(50);return}
1406
+ stop();
1407
+ var title=(document.title||'').trim();
1408
+ if(title&&!unrendered(title))speakRoute(title);
1409
+ }
1410
+ if(awaitBind)window.addEventListener('stx:load',onLoad);
1411
+ // Deferred a tick regardless: the title is applied by the caller after
1412
+ // the swap resolves, and the announcement should read the new one.
1413
+ schedule(0);
1414
+ }
1360
1415
 
1361
1416
  function focusAfterNavigation(hash){
1362
1417
  if(o.routeFocus===false)return;
@@ -1628,9 +1683,8 @@ else {
1628
1683
  // Before page scripts run, so a page that focuses its own control on
1629
1684
  // mount still wins \u2014 its script executes after this.
1630
1685
  focusAfterNavigation(hash);
1631
- // Deferred a tick because the title is applied by the caller after this
1632
- // swap resolves, and the announcement should read the new one.
1633
- setTimeout(announceRoute,0);
1686
+ // Read once the page's scripts have run and the runtime has bound it.
1687
+ announceRoute(true);
1634
1688
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
1635
1689
  log('[router] frag scripts:', fragScripts.length);
1636
1690
  qsa('script[data-stx-page]').forEach(function(s){s.remove()});
@@ -2136,6 +2190,8 @@ else {
2136
2190
  }
2137
2191
 
2138
2192
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url,direction:ctx.direction}}));
2193
+ // A layout change is a navigation too, and said nothing at all.
2194
+ announceRoute(true);
2139
2195
 
2140
2196
  // Execute page scripts FIRST \u2014 they define setup functions and set _latestSetup
2141
2197
  function execScripts(){
@@ -2532,7 +2588,8 @@ else {
2532
2588
  refreshCurrentLinks();
2533
2589
  window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:to.url,direction:dir,retained:true,params:to.params||{}}}));
2534
2590
  focusAfterNavigation('');
2535
- setTimeout(announceRoute,0);
2591
+ // A retained screen is bound already.
2592
+ announceRoute(false);
2536
2593
  window.dispatchEvent(new CustomEvent('stx:screen-shown',{detail:{url:to.url,direction:dir}}));
2537
2594
  revalidate(to.key,to.url);
2538
2595
  var keep=dir==='push'||dir==='tab';
@@ -3561,13 +3618,14 @@ else {
3561
3618
  // One View Transitions block. There used to be two, and the second
3562
3619
  // (injected after this one) silently overrode the first: the page stays
3563
3620
  // put and only the routed content fades, which is what this now says
3564
- // once. The retained screen inside it is not named, so the container
3565
- // and the screen never carry the same name.
3621
+ // once. Only the routed container carries the name, set by
3622
+ // nameTransitionContainer as a transition starts: never a retained
3623
+ // screen inside it, a nested <main>, or a second content element.
3566
3624
  if(o.viewTransitions&&'startViewTransition' in document){
3567
3625
  var dur=(o.viewTransitionDuration||220)+'ms';
3568
3626
  var ease=o.viewTransitionEasing||'cubic-bezier(0.16, 1, 0.3, 1)';
3569
3627
  css+='::view-transition-old(root),::view-transition-new(root){animation:none}::view-transition{background:transparent}';
3570
- css+='main,#app-content,[data-stx-content]:not([data-stx-screen]){view-transition-name:stx-content}::view-transition-group(stx-content){overflow:hidden}';
3628
+ css+='[data-stx-vt-content]{view-transition-name:stx-content}::view-transition-group(stx-content){overflow:hidden}';
3571
3629
  css+='::view-transition-old(stx-content){animation:stx-r-fade-out '+dur+' '+ease+' both}::view-transition-new(stx-content){animation:stx-r-fade-in '+dur+' '+ease+' both}';
3572
3630
  css+='@keyframes stx-r-fade-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-4px)}}';
3573
3631
  css+='@keyframes stx-r-fade-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.456",
4
+ "version": "0.2.458",
5
5
  "description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",