diff --git a/src/ui/rightRailTabs.js b/src/ui/rightRailTabs.js index 11670f57f..15687b83c 100644 --- a/src/ui/rightRailTabs.js +++ b/src/ui/rightRailTabs.js @@ -296,69 +296,12 @@ export function createRightRailTabs({ }); }; - /** A length from a computed style, in pixels. */ + /** A join token from the stylesheet (--right-rail-*), in pixels. */ const token = (computed, name, fallback) => { const value = parseFloat(computed?.getPropertyValue?.(name)); return Number.isFinite(value) ? value : fallback; }; - /** - * Share a strip shorter than its tabs. The lane (--right-stack-max-height) - * less the tool slot and the strip's gaps is divided equally; a tab shorter - * than its share keeps its height and the rest is shared again, so the long - * labels end up the same height and clip alike (styles/right-rail-tabs.css). - * Tabs that fit keep their natural height. - */ - const heights = new WeakMap(); - const setHeight = (tab, value) => { - if (heights.get(tab) === value) return; - heights.set(tab, value); - if (value) tab.style.setProperty('height', value); - else tab.style.removeProperty('height'); - }; - function fitTabs(tabs) { - const styleOf = (el) => documentRef.defaultView?.getComputedStyle?.(el); - const stripStyle = styleOf(strip); - const lane = token(styleOf(rail), '--right-stack-max-height', NaN); - const chrome = - token(stripStyle, 'row-gap', 0) * Math.max(0, tabs.length - 1) + - token(stripStyle, 'padding-top', 0) + - token(stripStyle, 'padding-bottom', 0); - const overhead = (column.offsetHeight || 0) - (strip.offsetHeight || 0); - const room = lane - overhead - chrome; - const entries = tabs.map((tab) => { - const tabStyle = styleOf(tab); - return { - tab, - natural: - (tab.firstElementChild?.offsetHeight || 0) + - token(tabStyle, 'padding-top', 0) + - token(tabStyle, 'padding-bottom', 0), - }; - }); - const natural = entries.reduce((sum, entry) => sum + entry.natural, 0); - if ( - !(room > 0) || - natural <= room + 0.5 || - entries.some((entry) => !(entry.natural > 0)) - ) { - for (const entry of entries) setHeight(entry.tab, ''); - return; - } - let remaining = room; - let count = entries.length; - for (const entry of [...entries].sort((a, b) => a.natural - b.natural)) { - const share = remaining / count; - if (entry.natural <= share) { - remaining -= entry.natural; - count -= 1; - setHeight(entry.tab, ''); - } else { - setHeight(entry.tab, `${Math.round(share * 100) / 100}px`); - } - } - } - /** * Publish the join: the open tab's bottom on the rail (--rail-tab-bottom, * rail pixels; the stylesheet sets the panel's minimum height from it) and @@ -442,10 +385,8 @@ export function createRightRailTabs({ // would drop keyboard focus on every class change. let cursor = strip.firstElementChild; let open = null; - const tabs = []; for (const panel of panels) { const { tab, label } = entryFor(panel); - tabs.push(tab); if (tab !== cursor) strip.insertBefore(tab, cursor); else cursor = cursor.nextElementSibling; const collapsed = panel.classList.contains('collapsed'); @@ -464,7 +405,6 @@ export function createRightRailTabs({ entry.tab.remove(); entries.delete(id); } - fitTabs(tabs); placeJoin(open?.tab, open?.panel); } @@ -490,19 +430,6 @@ export function createRightRailTabs({ strip.addEventListener('click', onClick); strip.addEventListener('keydown', onKeydown); observer?.observe(rail, { childList: true }); - // Tabs holding shared heights do not resize with the lane: watch the rail's - // style for a new lane. The join's own writes to that style change no lane. - let seenLane = rail.style?.getPropertyValue?.('--right-stack-max-height'); - const laneObserver = - typeof Observer === 'function' - ? new Observer(() => { - const lane = rail.style.getPropertyValue('--right-stack-max-height'); - if (lane === seenLane) return; - seenLane = lane; - sync(); - }) - : null; - laneObserver?.observe(rail, { attributes: true, attributeFilter: ['style'] }); rail.prepend(column); rail.append(join); sync(); @@ -517,7 +444,6 @@ export function createRightRailTabs({ if (destroyed) return; destroyed = true; observer?.disconnect(); - laneObserver?.disconnect(); resizer?.disconnect(); strip.removeEventListener('click', onClick); strip.removeEventListener('keydown', onKeydown); diff --git a/src/ui/rightRailTabs.test.mjs b/src/ui/rightRailTabs.test.mjs index 2b713ab7c..972e298b4 100644 --- a/src/ui/rightRailTabs.test.mjs +++ b/src/ui/rightRailTabs.test.mjs @@ -504,46 +504,6 @@ test('a rail panel hidden at mount gets its tab when its owner unhides it, from tabs.destroy(); }); -test('a strip shorter than its tabs is shared: short labels keep their height, the long ones get equal heights', () => { - const f = fixture(); - f.doc.defaultView = { - getComputedStyle: (el) => ({ - getPropertyValue: (name) => el.style.props.get(name) ?? '', - }), - }; - f.strip.style.setProperty('row-gap', '4px'); - const labels = { - 'pp-toggles': 49, - 'cctv-panel': 28, - 'global-context-panel': 49, - }; - for (const [id, height] of Object.entries(labels)) { - const tab = f.tabFor(id); - tab.style.setProperty('padding-top', '6px'); - tab.style.setProperty('padding-bottom', '6px'); - tab.children[0].offsetHeight = height; - } - // The column holds the strip (160px) and a 40px tool slot; the lane is 200. - Object.assign(f.tabs.column, { offsetHeight: 200 }); - Object.assign(f.strip, { offsetHeight: 160 }); - f.rail.style.setProperty('--right-stack-max-height', '200px'); - f.tabs.sync(); - // Room 152 for naturals 61, 40, 61: CCTV keeps 40, the other two share 112. - assert.equal(f.tabFor('cctv-panel').style.props.has('height'), false); - assert.equal(f.tabFor('pp-toggles').style.props.get('height'), '56px'); - assert.equal( - f.tabFor('global-context-panel').style.props.get('height'), - '56px', - ); - f.rail.style.setProperty('--right-stack-max-height', '400px'); - f.tabs.sync(); - assert.equal( - f.strip.children.some((tab) => tab.style.props.has('height')), - false, - 'a lane with room gives every tab its natural height back', - ); -}); - test('a tab shrunk by a short strip joins with corners no larger than half its height', () => { const f = fixture(); // The strip's offsets are relative to its column, the column's to the rail. @@ -639,18 +599,25 @@ test('the stylesheet keeps phones on border-box, folds the strip into narrow des // The tabs give way to a short strip and clip their labels at the end. assert.match( css, - /\.right-rail-tab \{[^}]*flex: 0 1 auto;[^}]*align-items: flex-start;[^}]*min-height: 0;[^}]*overflow: hidden;[^}]*mask-image: linear-gradient\(to bottom, #000 calc\(100% - 6px\), transparent\);/, + /\.right-rail-tab \{[^}]*flex: 1 1 0%;[^}]*align-items: flex-start;[^}]*min-height: 0;[^}]*max-height: max-content;[^}]*overflow: hidden;[^}]*mask-image: linear-gradient\(to bottom, #000 calc\(100% - 6px\), transparent\);/, + ); + // The column stands the lane's full height so the strip's size is definite + // and a zero basis shares it; only the strip and the slot take the pointer. + assert.match( + css, + /#right-rail-column \{[^}]*height: var\(--right-stack-max-height, 70vh\);[^}]*pointer-events: none;/, + ); + assert.match(css, /#right-rail-tabs \{[^}]*pointer-events: auto;/); + assert.match(css, /#right-rail-tools \{[^}]*pointer-events: auto;/); + assert.doesNotMatch( + css, + /#right-rail-column \{[^}]*max-height: var\(--right-stack-max-height/, ); // The rail layout pass measures the column's natural height to climb above // its baseline before the strip has to scroll. assert.match( css, - /#right-context-rail\[data-rail-measuring\] > #right-rail-column \{\s*max-height: none !important;/, - ); - assert.match( - css, - /#right-context-rail\[data-rail-measuring\] #right-rail-tabs > \.right-rail-tab \{\s*height: auto !important;/, - 'shared tab heights are lifted for the measurement, or the climb would see a column that already fits', + /#right-context-rail\[data-rail-measuring\] > #right-rail-column \{\s*height: auto !important;/, ); assert.match( css, diff --git a/src/ui/styles/right-rail-tabs.css b/src/ui/styles/right-rail-tabs.css index 86f44bd9f..534ecf06b 100644 --- a/src/ui/styles/right-rail-tabs.css +++ b/src/ui/styles/right-rail-tabs.css @@ -58,7 +58,9 @@ } /* The strip's column: the tablist, then a tool slot that holds the compact - Radio launcher on desktop (radioPanelPlacement.js). */ + Radio launcher on desktop (radioPanelPlacement.js). It stands the lane's + full height, which makes the strip's size definite so its tabs can share + it (see .right-rail-tab); only the strip and the slot take the pointer. */ #right-rail-column { position: absolute; top: 0; @@ -68,18 +70,13 @@ flex-direction: column; box-sizing: border-box; width: var(--right-rail-tabs-inset); - max-height: var(--right-stack-max-height, 70vh); - pointer-events: auto; + height: var(--right-stack-max-height, 70vh); + pointer-events: none; } -/* The layout pass reads the column's natural height with the lane clamp and - the shared tab heights lifted, to climb when the lane is too short for it - (rightPanelRail.js). */ +/* The layout pass reads the column's natural height, to climb when the lane + is too short for it (rightPanelRail.js). */ #right-context-rail[data-rail-measuring] > #right-rail-column { - max-height: none !important; -} - -#right-context-rail[data-rail-measuring] #right-rail-tabs > .right-rail-tab { height: auto !important; } @@ -96,6 +93,7 @@ /* The rail climbs above its baseline to fit the column (rightPanelRail.js); a lane still shorter shrinks the tabs. The strip never scrolls. */ overflow: hidden; + pointer-events: auto; } #right-rail-tools { @@ -106,6 +104,7 @@ box-sizing: border-box; margin-top: 6px; padding-left: var(--right-rail-tabs-gap); + pointer-events: auto; } #right-rail-tools:empty { @@ -174,18 +173,22 @@ body:not(.mobile-shell) outline-offset: -3px; } -/* A short strip is shared between the tabs (rightRailTabs.js): the long - labels end up the same height. A tab clips its label at the end, faded over - the bottom padding. */ +/* A short strip is shared between the tabs: a zero basis hands the strip's + height out in equal parts, a tab's own length caps its part, and what a + short label leaves goes to the longer ones, which end up the same height. A + strip with room gives every tab its own length. The basis is 0%, not 0: + the strip's content size, which sizes the column, comes from the labels. A + tab clips its label at the end, faded over the bottom padding. */ .right-rail-tab { position: relative; display: flex; - flex: 0 1 auto; + flex: 1 1 0%; align-items: flex-start; justify-content: center; box-sizing: border-box; width: var(--right-rail-tabs-width); min-height: 0; + max-height: max-content; padding: 6px 0; overflow: hidden; mask-image: linear-gradient(to bottom, #000 calc(100% - 6px), transparent);