diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e43d15620..954d29a3a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -444,6 +444,7 @@ jobs: BASE_URL=http://localhost:13581 node test-issue-1122-details-row-clamp-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1128-packets-layout-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-189-group-caret-e2e.js 2>&1 | tee -a e2e-output.txt + BASE_URL=http://localhost:13581 node test-issue-254-affinity-toggle-mobile-aria-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1128-multi-viewport-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1136-live-region-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-live-multibyte-only-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/mobile-page-actions.js b/public/mobile-page-actions.js index 2f0c08d17..2e23c802a 100644 --- a/public/mobile-page-actions.js +++ b/public/mobile-page-actions.js @@ -7,6 +7,9 @@ const SLOT_ID = 'navPageActions'; function isMobile() { return window.innerWidth <= MOBILE_BP; } + // #254: packets.js renders a group row as select-hash under this breakpoint, + // where the #1461 #7 redirect below makes activating it select. + window.MobilePageActions = { isMobile: isMobile }; function ensureSlot() { let slot = document.getElementById(SLOT_ID); diff --git a/public/nodes.js b/public/nodes.js index 516220da8..4928620e9 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -548,6 +548,38 @@ // ─── End neighbor helpers ───────────────────────────────────────────────── + // #254: the Affinity Debug card (only shown with debugAffinity). Its heading is + // a disclosure button, with caret-right when collapsed and caret-down when + // expanded (#189), and aria-expanded on the button follows the body. The click + // is handled by onFullBodyClick, delegated from #nodeFullBody. + function affinityDebugCaretHtml(expanded) { + return ''; + } + + function renderAffinityDebugCard() { + return ``; + } + + function toggleAffinityDebug(btn) { + const expanded = btn.getAttribute('aria-expanded') !== 'true'; + btn.setAttribute('aria-expanded', String(expanded)); + const body = btn.ownerDocument.getElementById(btn.getAttribute('aria-controls')); + if (body) body.hidden = !expanded; + const icon = btn.querySelector('.toggle-icon'); + if (icon) icon.innerHTML = affinityDebugCaretHtml(expanded); + return expanded; + } + + function onFullBodyClick(e) { + const btn = e.target.closest && e.target.closest('.affinity-debug-toggle'); + if (btn) toggleAffinityDebug(btn); + } + let directNode = null; // set when navigating directly to #/nodes/:pubkey let regionChangeHandler = null; @@ -570,6 +602,9 @@ `; document.getElementById('nodeBackBtn').addEventListener('click', () => { location.hash = '#/nodes'; }); + // Wired once here: loadFullNode re-renders the body (theme-refresh) and + // would stack listeners. + document.getElementById('nodeFullBody').addEventListener('click', onFullBodyClick); loadFullNode(directNode); // Escape to go back to nodes list document.addEventListener('keydown', function nodesEsc(e) { @@ -916,12 +951,7 @@
Loading neighbors…
- + ${renderAffinityDebugCard()}

Paths Through This Node

@@ -2148,6 +2178,9 @@ window._nodesRenderNodeTimestampText = renderNodeTimestampText; window._nodesGetStatusInfo = getStatusInfo; window._nodesGetStatusTooltip = getStatusTooltip; + window._nodesRenderAffinityDebugCard = renderAffinityDebugCard; + window._nodesToggleAffinityDebug = toggleAffinityDebug; + window._nodesOnFullBodyClick = onFullBodyClick; // #862: Expose search filter logic for testing window._nodesMatchesSearch = function(node, query) { diff --git a/public/packets.js b/public/packets.js index a06d8028d..826c3cd65 100644 --- a/public/packets.js +++ b/public/packets.js @@ -2404,6 +2404,29 @@ } } + // #254: mobile-page-actions.js owns the mobile breakpoint and the #1461 #7 + // redirect of a group-row click to select-hash. Without it nothing redirects. + function groupRowSelectsOnActivate() { + return !!(window.MobilePageActions && window.MobilePageActions.isMobile()); + } + + // A group row's action and aria-expanded depend on that breakpoint, so a + // resize that crosses it (e.g. a phone rotating) re-renders the visible rows. + let _groupRowsSelect = false; + let _groupRowModeTimer = null; + function _onGroupRowModeResize() { + clearTimeout(_groupRowModeTimer); + _groupRowModeTimer = setTimeout(() => { + const selects = groupRowSelectsOnActivate(); + if (selects === _groupRowsSelect) return; + _groupRowsSelect = selects; + if (!_displayGrouped) return; + _lastVisibleStart = -1; + _lastVisibleEnd = -1; + renderVisibleRows(); + }, 150); + } + // Build HTML for a single grouped packet row function buildGroupRowHtml(p, entryIdx = -1) { const isExpanded = expandedHashes.has(p.hash); @@ -2440,8 +2463,12 @@ // channels.js, network-digest.js, analytics.js and route-view.js. The row that // toggles reports its state in aria-expanded; a single-observation row cannot // expand, so it has neither. + // #254: under the mobile breakpoint activating a group row selects it (#1461 + // #7, the expand column is hidden there), so the row is select-hash and + // claims no expanded state. + const _grpToggles = !isSingle && !groupRowSelectsOnActivate(); const _grpCaret = isSingle ? '' : ''; - let html = ` + let html = ` ${_grpCaret} ${groupRegion ? `${groupRegion}` : '—'} ${renderTimestampCell(p.latest)} @@ -4257,6 +4284,8 @@ _themeRefreshHandler = () => { if (typeof renderTableRows === 'function') renderTableRows(); }; window.addEventListener('theme-refresh', _themeRefreshHandler); window.addEventListener('storage', _onStorageChange); + _groupRowsSelect = groupRowSelectsOnActivate(); + window.addEventListener('resize', _onGroupRowModeResize); var result = init(app, routeParam); // Install channel color picker on packets table (M2, #271) if (window.ChannelColorPicker) window.ChannelColorPicker.installPacketsTable(); @@ -4265,6 +4294,8 @@ destroy: function() { if (_themeRefreshHandler) { window.removeEventListener('theme-refresh', _themeRefreshHandler); _themeRefreshHandler = null; } window.removeEventListener('storage', _onStorageChange); + window.removeEventListener('resize', _onGroupRowModeResize); + clearTimeout(_groupRowModeTimer); return destroy(); } }); diff --git a/public/style.css b/public/style.css index 6c2007117..1990f320d 100644 --- a/public/style.css +++ b/public/style.css @@ -2860,6 +2860,14 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); } margin-bottom: 12px; border: 1px solid var(--border); } .node-full-card h4 { margin: 0 0 10px; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; } +/* #254: the Affinity Debug heading is a disclosure button. It keeps the h4 + * look and spans the heading, so the whole row toggles as before. */ +.node-full-card h4 .affinity-debug-toggle { + display: flex; align-items: center; gap: 6px; width: 100%; + margin: 0; padding: 0; border: 0; background: none; cursor: pointer; + font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; text-align: start; +} +@media (pointer: coarse) { .node-full-card h4 .affinity-debug-toggle { min-height: 48px; } } .node-activity-list { display: flex; flex-direction: column; gap: 6px; } .node-activity-item { display: flex; gap: 8px; font-size: 13px; padding: 6px 0; diff --git a/test-all.sh b/test-all.sh index a505c5a17..9ce3bdbcc 100755 --- a/test-all.sh +++ b/test-all.sh @@ -57,6 +57,7 @@ run test-top-routes-overlay.js run test-important-links-byte-filter.js run test-url-state.js run test-node-adverts.js +run test-issue-254-affinity-debug-toggle.js run test-perf-go-runtime.js run test-channel-psk-ux.js run test-channel-sidebar-layout.js diff --git a/test-issue-254-affinity-debug-toggle.js b/test-issue-254-affinity-debug-toggle.js new file mode 100644 index 000000000..7e388e0ae --- /dev/null +++ b/test-issue-254-affinity-debug-toggle.js @@ -0,0 +1,196 @@ +/** + * #254: the node page's Affinity Debug card (shown only with debugAffinity, e.g. + * localStorage meshcore-affinity-debug=true) folds its body in and out. + * + * Its heading used to carry an inline onclick whose value held the Phosphor + * sprite markup with unescaped double quotes, so the browser cut the attribute + * short and the handler was a syntax error: the body could never open. Its + * carets were also reversed against the #189 convention (collapsed caret-right, + * expanded caret-down). + * + * Tests the real nodes.js in a vm sandbox: + * - the card renders a disclosure