mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(ui): right rail strip never scrolls: tabs shrink in proportion and clip their labels
A lane too short for the tab strip, even after the rail has climbed toward the HUD corner, shrank the strip into a scrollbar. The tabs now give way in proportion to their labels (flex 0 1 auto, min-height 0) and clip them at the end with a short fade; the join keeps corners no larger than half of a shrunken tab. The strip's scroll handling in placeJoin, the scroll listener, railJoinPath's tab-less path and the scroller marker are gone with the scrolling. The expanded Radio Enable's reveal of the directory and Play lands its smooth scroll on scrollend when a rail layout pass, measuring the panels mid-scroll, puts the Context scroller's offset back and cancels it; the Radio browser gate stopped there since the strip-fit commit. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
bff1d0eb29
commit
84c204b7c4
7 files changed
+95
-157
No files matched your search
@@ -2,8 +2,7 @@
|
|||||||
Imagery while shown) is a vertical tab strip with sideways labels instead of
|
Imagery while shown) is a vertical tab strip with sideways labels instead of
|
||||||
an accordion of collapsed launcher cards: at most one panel is open in every
|
an accordion of collapsed launcher cards: at most one panel is open in every
|
||||||
desktop HUD layout, and the open tab and its panel share one surface and
|
desktop HUD layout, and the open tab and its panel share one surface and
|
||||||
outline, like a folder tab. On a short window the rail rises toward the HUD
|
outline, like a folder tab. On a short window the rail rises toward the HUD corner above it and the tabs shrink, clipping their labels, rather than scroll. A first-time visitor on a
|
||||||
corner above it so the whole strip stays in view. A first-time visitor on a
|
|
||||||
desktop finds Display open; a stored choice still wins. The panels' own
|
desktop finds Display open; a stored choice still wins. The panels' own
|
||||||
collapse arrows are gone on desktop: the tab opens and closes a panel, and
|
collapse arrows are gone on desktop: the tab opens and closes a panel, and
|
||||||
Escape inside one closes it and returns focus to its tab; the strip is a
|
Escape inside one closes it and returns focus to its tab; the strip is a
|
||||||
|
|||||||
@@ -1487,7 +1487,9 @@ gives every owner-collapsed panel back. Stable Display allocation avoids
|
|||||||
unnecessary style writes.
|
unnecessary style writes.
|
||||||
The compact Radio player is independent of Context: its launcher toggles it
|
The compact Radio player is independent of Context: its launcher toggles it
|
||||||
whether Context is collapsed or open, and only an outside pointer, Escape, its
|
whether Context is collapsed or open, and only an outside pointer, Escape, its
|
||||||
close button or its Details button closes it, never a layout pass.
|
close button or its Details button closes it, never a layout pass. The
|
||||||
|
expanded Enable's reveal of the directory and Play lands its smooth scroll
|
||||||
|
even when a rail layout pass, measuring the panels, cuts it short.
|
||||||
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
The right rail skips panels hidden by other CSS (computed `display: none`), but
|
||||||
keeps the collapsed panels its own exclusive presentation hides, so the owner
|
keeps the collapsed panels its own exclusive presentation hides, so the owner
|
||||||
rule can still give them back.
|
rule can still give them back.
|
||||||
@@ -1511,8 +1513,8 @@ the DOM so Tab from the open tab reaches the tool slot and then the panel.
|
|||||||
Labels are the panels' own translated header titles. The rail's lane shares
|
Labels are the panels' own translated header titles. The rail's lane shares
|
||||||
the left rail's top; when it is too short for the strip, the rail climbs toward
|
the left rail's top; when it is too short for the strip, the rail climbs toward
|
||||||
the HUD corner above as far as the strip needs (`src/ui/rightPanelRail.js`),
|
the HUD corner above as far as the strip needs (`src/ui/rightPanelRail.js`),
|
||||||
and only a lane still shorter scrolls the strip, with a thin scrollbar and the
|
and a lane still shorter shrinks the tabs in proportion, clipping their
|
||||||
open tab kept in view. Under the tabs the strip's
|
labels at the end; the strip never scrolls. Under the tabs the strip's
|
||||||
column keeps a tool slot (`#right-rail-tools`) that holds the compact Radio
|
column keeps a tool slot (`#right-rail-tools`) that holds the compact Radio
|
||||||
launcher on desktop: `src/ui/radioPanelPlacement.js` moves it there from
|
launcher on desktop: `src/ui/radioPanelPlacement.js` moves it there from
|
||||||
Context's header, and back for the phone sheet, so the compact player stays one
|
Context's header, and back for the phone sheet, so the compact player stays one
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ test('only the expanded Radio Enable gesture requests the contained post-enable
|
|||||||
assert.match(radioControls, /if \(revealAfterEnable\)\s*await this\._revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
assert.match(radioControls, /if \(revealAfterEnable\)\s*await this\._revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
||||||
assert.equal((radioBindings.match(/_revealRadioControlsAfterExplicitEnable\(trigger\)/g) || []).length, 1);
|
assert.equal((radioBindings.match(/_revealRadioControlsAfterExplicitEnable\(trigger\)/g) || []).length, 1);
|
||||||
assert.match(radioControlsSource, /async _revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
assert.match(radioControlsSource, /async _revealRadioControlsAfterExplicitEnable\(trigger\)/);
|
||||||
|
// A rail layout pass measuring the panels mid-scroll cancels the smooth
|
||||||
|
// reveal; the helper lands it at the target when the scroll ends short.
|
||||||
|
assert.match(
|
||||||
|
radioControlsSource,
|
||||||
|
/scroller\.addEventListener\(\s*'scrollend',[\s\S]*?Math\.abs\(scroller\.scrollTop - next\) >= 1\)\s*scroller\.scrollTop = next;/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('right-rail context entry is transactional: activation result gates the mode', () => {
|
test('right-rail context entry is transactional: activation result gates the mode', () => {
|
||||||
|
|||||||
@@ -106,6 +106,18 @@ export class RadioControls {
|
|||||||
top: next,
|
top: next,
|
||||||
behavior: reducedMotion ? 'auto' : 'smooth',
|
behavior: reducedMotion ? 'auto' : 'smooth',
|
||||||
});
|
});
|
||||||
|
// A rail layout pass that measures the panels mid-scroll puts the scroll
|
||||||
|
// offset back where it was, which cancels the smooth scroll: land it when
|
||||||
|
// the scroll ends short.
|
||||||
|
if (!reducedMotion)
|
||||||
|
scroller.addEventListener(
|
||||||
|
'scrollend',
|
||||||
|
() => {
|
||||||
|
if (!this.destroyed && Math.abs(scroller.scrollTop - next) >= 1)
|
||||||
|
scroller.scrollTop = next;
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-85
@@ -75,9 +75,8 @@ export function railTabPanels(rail) {
|
|||||||
*
|
*
|
||||||
* @param {object} g
|
* @param {object} g
|
||||||
* @param {{x0: number, y0: number, x1: number, y1: number}} g.panel
|
* @param {{x0: number, y0: number, x1: number, y1: number}} g.panel
|
||||||
* @param {{top: number, bottom: number, right: number}|null} g.tab The tab's
|
* @param {{top: number, bottom: number, right: number}} g.tab The tab's
|
||||||
* extent; its left edge is the panel's right edge. Null draws the panel
|
* extent; its left edge is the panel's right edge.
|
||||||
* alone.
|
|
||||||
* @param {number} [g.corner] Panel corner radius, or the chamfer size.
|
* @param {number} [g.corner] Panel corner radius, or the chamfer size.
|
||||||
* @param {number} [g.tabRadius] The tab's outer corner radius.
|
* @param {number} [g.tabRadius] The tab's outer corner radius.
|
||||||
* @param {number} [g.fillet] Concave fillet radius at the junction.
|
* @param {number} [g.fillet] Concave fillet radius at the junction.
|
||||||
@@ -100,24 +99,21 @@ export function railJoinPath({
|
|||||||
const y0 = panel.y0 + inset;
|
const y0 = panel.y0 + inset;
|
||||||
const x1 = panel.x1 - inset;
|
const x1 = panel.x1 - inset;
|
||||||
const y1 = panel.y1 - inset;
|
const y1 = panel.y1 - inset;
|
||||||
// No tab (it is scrolled out of the strip): the panel alone.
|
const ty0 = tab.top + inset;
|
||||||
const ty0 = tab ? tab.top + inset : NaN;
|
const ty1 = tab.bottom - inset;
|
||||||
const ty1 = tab ? tab.bottom - inset : NaN;
|
const tx1 = tab.right - inset;
|
||||||
const tx1 = tab ? tab.right - inset : NaN;
|
|
||||||
if (style === 'chamfer') {
|
if (style === 'chamfer') {
|
||||||
const c = corner;
|
const c = corner;
|
||||||
const cx = (panel.x0 + panel.x1) / 2;
|
const cx = (panel.x0 + panel.x1) / 2;
|
||||||
// The tab's edges along the panel stay between the chamfers.
|
// The tab's edges along the panel stay between the chamfers.
|
||||||
const tyA = Math.max(ty0, y0 + c);
|
const tyA = Math.max(ty0, y0 + c);
|
||||||
const tyB = Math.max(tyA, Math.min(ty1, y1 - c));
|
const tyB = Math.max(tyA, Math.min(ty1, y1 - c));
|
||||||
const edge = tab
|
const edge = [
|
||||||
? [
|
`L ${P(x1, tyA)}`,
|
||||||
`L ${P(x1, tyA)}`,
|
`L ${P(tx1, tyA)}`,
|
||||||
`L ${P(tx1, tyA)}`,
|
`L ${P(tx1, tyB)}`,
|
||||||
`L ${P(tx1, tyB)}`,
|
`L ${P(x1, tyB)}`,
|
||||||
`L ${P(x1, tyB)}`,
|
];
|
||||||
]
|
|
||||||
: [];
|
|
||||||
return [
|
return [
|
||||||
`M ${P(x0, y0 + c)}`,
|
`M ${P(x0, y0 + c)}`,
|
||||||
`L ${P(x0 + c, y0)}`,
|
`L ${P(x0 + c, y0)}`,
|
||||||
@@ -165,35 +161,28 @@ export function railJoinPath({
|
|||||||
return { fillet, corner: Math.min(R, Math.max(0, room - fillet)) };
|
return { fillet, corner: Math.min(R, Math.max(0, room - fillet)) };
|
||||||
};
|
};
|
||||||
move(x0 + R, y0);
|
move(x0 + R, y0);
|
||||||
if (!tab) {
|
const first = tab.top - panel.y0 < 0.5;
|
||||||
line(x1 - R, y0);
|
const last = panel.y1 - tab.bottom < 0.5;
|
||||||
arc(R, 1, x1, y0 + R);
|
const above = fit(ty0 - y0);
|
||||||
line(x1, y1 - R);
|
const below = fit(y1 - ty1);
|
||||||
arc(R, 1, x1 - R, y1);
|
if (first) {
|
||||||
|
line(tx1 - r, y0);
|
||||||
|
arc(r, 1, tx1, y0 + r);
|
||||||
} else {
|
} else {
|
||||||
const first = tab.top - panel.y0 < 0.5;
|
line(x1 - above.corner, y0);
|
||||||
const last = panel.y1 - tab.bottom < 0.5;
|
arc(above.corner, 1, x1, y0 + above.corner);
|
||||||
const above = fit(ty0 - y0);
|
line(x1, ty0 - above.fillet);
|
||||||
const below = fit(y1 - ty1);
|
arc(above.fillet, 0, x1 + above.fillet, ty0);
|
||||||
if (first) {
|
line(tx1 - r, ty0);
|
||||||
line(tx1 - r, y0);
|
arc(r, 1, tx1, ty0 + r);
|
||||||
arc(r, 1, tx1, y0 + r);
|
}
|
||||||
} else {
|
line(tx1, ty1 - r);
|
||||||
line(x1 - above.corner, y0);
|
arc(r, 1, tx1 - r, ty1);
|
||||||
arc(above.corner, 1, x1, y0 + above.corner);
|
if (!last) {
|
||||||
line(x1, ty0 - above.fillet);
|
line(x1 + below.fillet, ty1);
|
||||||
arc(above.fillet, 0, x1 + above.fillet, ty0);
|
arc(below.fillet, 0, x1, ty1 + below.fillet);
|
||||||
line(tx1 - r, ty0);
|
line(x1, y1 - below.corner);
|
||||||
arc(r, 1, tx1, ty0 + r);
|
arc(below.corner, 1, x1 - below.corner, y1);
|
||||||
}
|
|
||||||
line(tx1, ty1 - r);
|
|
||||||
arc(r, 1, tx1 - r, ty1);
|
|
||||||
if (!last) {
|
|
||||||
line(x1 + below.fillet, ty1);
|
|
||||||
arc(below.fillet, 0, x1, ty1 + below.fillet);
|
|
||||||
line(x1, y1 - below.corner);
|
|
||||||
arc(below.corner, 1, x1 - below.corner, y1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
line(x0 + R, y1);
|
line(x0 + R, y1);
|
||||||
arc(R, 1, x0, y1 - R);
|
arc(R, 1, x0, y1 - R);
|
||||||
@@ -239,10 +228,6 @@ export function createRightRailTabs({
|
|||||||
strip.setAttribute('role', 'tablist');
|
strip.setAttribute('role', 'tablist');
|
||||||
strip.setAttribute('aria-orientation', 'vertical');
|
strip.setAttribute('aria-orientation', 'vertical');
|
||||||
strip.setAttribute('aria-label', t('Panels'));
|
strip.setAttribute('aria-label', t('Panels'));
|
||||||
// The rail's measuring pass (rightPanelRail.js) lifts the column's lane
|
|
||||||
// clamp, which would reset a scrolled strip; the marker has it put the
|
|
||||||
// offset back.
|
|
||||||
strip.setAttribute('data-rail-scroller', '');
|
|
||||||
const tools = documentRef.createElement('div');
|
const tools = documentRef.createElement('div');
|
||||||
tools.id = RIGHT_RAIL_TOOLS_ID;
|
tools.id = RIGHT_RAIL_TOOLS_ID;
|
||||||
column.append(strip, tools);
|
column.append(strip, tools);
|
||||||
@@ -296,7 +281,7 @@ export function createRightRailTabs({
|
|||||||
typeof Observer === 'function' ? new Observer(() => sync()) : null;
|
typeof Observer === 'function' ? new Observer(() => sync()) : null;
|
||||||
// Tab heights follow their labels (fonts, locale) and panel heights their
|
// Tab heights follow their labels (fonts, locale) and panel heights their
|
||||||
// content and allocation: re-measure the join when the strip or a panel
|
// content and allocation: re-measure the join when the strip or a panel
|
||||||
// changes size and on strip scroll.
|
// changes size.
|
||||||
const Resize = documentRef.defaultView?.ResizeObserver;
|
const Resize = documentRef.defaultView?.ResizeObserver;
|
||||||
const resizer =
|
const resizer =
|
||||||
typeof Resize === 'function' ? new Resize(() => sync()) : null;
|
typeof Resize === 'function' ? new Resize(() => sync()) : null;
|
||||||
@@ -325,28 +310,13 @@ export function createRightRailTabs({
|
|||||||
* extended down to cover the tab if that is shorter, but never past the
|
* extended down to cover the tab if that is shorter, but never past the
|
||||||
* rail's lane (--right-stack-max-height).
|
* rail's lane (--right-stack-max-height).
|
||||||
*/
|
*/
|
||||||
let shownTab = null;
|
|
||||||
function placeJoin(tab, panel) {
|
function placeJoin(tab, panel) {
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
rail.style.removeProperty('--rail-tab-bottom');
|
rail.style.removeProperty('--rail-tab-bottom');
|
||||||
join.hidden = true;
|
join.hidden = true;
|
||||||
shownTab = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A short lane makes the strip scroll. A newly opened tab is brought into
|
const top = tab.offsetTop;
|
||||||
// view once; after that the strip scrolls as the user wishes and the join
|
|
||||||
// follows the visible part of the tab, or the panel alone.
|
|
||||||
const viewHeight = strip.clientHeight;
|
|
||||||
const scrolls = Number.isFinite(viewHeight) && viewHeight > 0;
|
|
||||||
if (scrolls && tab !== shownTab) {
|
|
||||||
const above = tab.offsetTop - strip.scrollTop;
|
|
||||||
const below =
|
|
||||||
tab.offsetTop + tab.offsetHeight - (strip.scrollTop + viewHeight);
|
|
||||||
if (above < 0) strip.scrollTop += above;
|
|
||||||
else if (below > 0) strip.scrollTop += below;
|
|
||||||
}
|
|
||||||
shownTab = tab;
|
|
||||||
const top = tab.offsetTop - (strip.scrollTop || 0);
|
|
||||||
rail.style.setProperty('--rail-tab-bottom', `${top + tab.offsetHeight}px`);
|
rail.style.setProperty('--rail-tab-bottom', `${top + tab.offsetHeight}px`);
|
||||||
const computed = documentRef.defaultView?.getComputedStyle?.(rail);
|
const computed = documentRef.defaultView?.getComputedStyle?.(rail);
|
||||||
const corner = token(computed, '--right-rail-corner', 16);
|
const corner = token(computed, '--right-rail-corner', 16);
|
||||||
@@ -360,13 +330,8 @@ export function createRightRailTabs({
|
|||||||
// column's to the rail.
|
// column's to the rail.
|
||||||
const stripTop = (column.offsetTop || 0) + (strip.offsetTop || 0);
|
const stripTop = (column.offsetTop || 0) + (strip.offsetTop || 0);
|
||||||
const stripLeft = (column.offsetLeft || 0) + (strip.offsetLeft || 0);
|
const stripLeft = (column.offsetLeft || 0) + (strip.offsetLeft || 0);
|
||||||
let tabTop = stripTop + top;
|
const tabTop = stripTop + top;
|
||||||
let tabBottom = tabTop + tab.offsetHeight;
|
const tabBottom = tabTop + tab.offsetHeight;
|
||||||
if (scrolls) {
|
|
||||||
tabTop = Math.max(tabTop, stripTop);
|
|
||||||
tabBottom = Math.min(tabBottom, stripTop + viewHeight);
|
|
||||||
}
|
|
||||||
const visible = tabBottom - tabTop >= 1;
|
|
||||||
const geometry = {
|
const geometry = {
|
||||||
panel: {
|
panel: {
|
||||||
x0: panel.offsetLeft,
|
x0: panel.offsetLeft,
|
||||||
@@ -374,20 +339,17 @@ export function createRightRailTabs({
|
|||||||
x1: panel.offsetLeft + panel.offsetWidth,
|
x1: panel.offsetLeft + panel.offsetWidth,
|
||||||
y1: Math.max(
|
y1: Math.max(
|
||||||
panel.offsetTop + panel.offsetHeight,
|
panel.offsetTop + panel.offsetHeight,
|
||||||
visible ? Math.min(lane, tabBottom + fillet + 4 + corner) : 0,
|
Math.min(lane, tabBottom + fillet + 4 + corner),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
tab: visible
|
tab: {
|
||||||
? {
|
top: tabTop,
|
||||||
top: tabTop,
|
bottom: tabBottom,
|
||||||
bottom: tabBottom,
|
right: stripLeft + tab.offsetLeft + tab.offsetWidth,
|
||||||
right: stripLeft + tab.offsetLeft + tab.offsetWidth,
|
},
|
||||||
}
|
|
||||||
: null,
|
|
||||||
corner,
|
corner,
|
||||||
tabRadius: visible
|
// A tab shrunk by a short strip keeps corners no larger than half of it.
|
||||||
? Math.min(tabRadius, (tabBottom - tabTop) / 2)
|
tabRadius: Math.min(tabRadius, (tabBottom - tabTop) / 2),
|
||||||
: tabRadius,
|
|
||||||
fillet,
|
fillet,
|
||||||
style: chamfer ? 'chamfer' : 'round',
|
style: chamfer ? 'chamfer' : 'round',
|
||||||
};
|
};
|
||||||
@@ -465,10 +427,8 @@ export function createRightRailTabs({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
visible[next].focus();
|
visible[next].focus();
|
||||||
};
|
};
|
||||||
const onScroll = () => sync();
|
|
||||||
strip.addEventListener('click', onClick);
|
strip.addEventListener('click', onClick);
|
||||||
strip.addEventListener('keydown', onKeydown);
|
strip.addEventListener('keydown', onKeydown);
|
||||||
strip.addEventListener('scroll', onScroll, { passive: true });
|
|
||||||
observer?.observe(rail, { childList: true });
|
observer?.observe(rail, { childList: true });
|
||||||
rail.prepend(column);
|
rail.prepend(column);
|
||||||
rail.append(join);
|
rail.append(join);
|
||||||
@@ -487,7 +447,6 @@ export function createRightRailTabs({
|
|||||||
resizer?.disconnect();
|
resizer?.disconnect();
|
||||||
strip.removeEventListener('click', onClick);
|
strip.removeEventListener('click', onClick);
|
||||||
strip.removeEventListener('keydown', onKeydown);
|
strip.removeEventListener('keydown', onKeydown);
|
||||||
strip.removeEventListener('scroll', onScroll);
|
|
||||||
column.remove();
|
column.remove();
|
||||||
join.remove();
|
join.remove();
|
||||||
rail.removeAttribute(RIGHT_RAIL_TABS_ATTRIBUTE);
|
rail.removeAttribute(RIGHT_RAIL_TABS_ATTRIBUTE);
|
||||||
|
|||||||
@@ -213,11 +213,6 @@ test('builds one tab per visible rail panel, in rail order, labelled from the pa
|
|||||||
assert.deepEqual(f.tabs.tools.children, [], 'the slot starts empty');
|
assert.deepEqual(f.tabs.tools.children, [], 'the slot starts empty');
|
||||||
assert.equal(f.strip.id, RIGHT_RAIL_TABS_ID);
|
assert.equal(f.strip.id, RIGHT_RAIL_TABS_ID);
|
||||||
assert.equal(f.strip.getAttribute('role'), 'tablist');
|
assert.equal(f.strip.getAttribute('role'), 'tablist');
|
||||||
assert.equal(
|
|
||||||
f.strip.getAttribute('data-rail-scroller'),
|
|
||||||
'',
|
|
||||||
'the rail layout pass keeps the strip scroll offset across measuring',
|
|
||||||
);
|
|
||||||
assert.equal(f.strip.getAttribute('aria-orientation'), 'vertical');
|
assert.equal(f.strip.getAttribute('aria-orientation'), 'vertical');
|
||||||
assert.equal(f.rail.getAttribute(RIGHT_RAIL_TABS_ATTRIBUTE), 'vertical');
|
assert.equal(f.rail.getAttribute(RIGHT_RAIL_TABS_ATTRIBUTE), 'vertical');
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -372,7 +367,6 @@ test('publishes the open tab extent on the rail and keeps a join element under t
|
|||||||
offsetHeight: 300,
|
offsetHeight: 300,
|
||||||
offsetWidth: 330,
|
offsetWidth: 330,
|
||||||
});
|
});
|
||||||
f.strip.scrollTop = 0;
|
|
||||||
f.cctv.classList.remove('collapsed');
|
f.cctv.classList.remove('collapsed');
|
||||||
f.tabs.sync();
|
f.tabs.sync();
|
||||||
assert.deepEqual([...props], [['--rail-tab-bottom', '142px']]);
|
assert.deepEqual([...props], [['--rail-tab-bottom', '142px']]);
|
||||||
@@ -382,13 +376,6 @@ test('publishes the open tab extent on the rail and keeps a join element under t
|
|||||||
glass.style.clipPath,
|
glass.style.clipPath,
|
||||||
/^path\('M 16 0 L 314 0 A 16 16 0 0 1 330 16 L 330 78 /,
|
/^path\('M 16 0 L 314 0 A 16 16 0 0 1 330 16 L 330 78 /,
|
||||||
);
|
);
|
||||||
f.strip.scrollTop = 20;
|
|
||||||
f.tabs.sync();
|
|
||||||
assert.equal(
|
|
||||||
props.get('--rail-tab-bottom'),
|
|
||||||
'122px',
|
|
||||||
'strip scroll is subtracted',
|
|
||||||
);
|
|
||||||
f.cctv.classList.add('collapsed');
|
f.cctv.classList.add('collapsed');
|
||||||
f.tabs.sync();
|
f.tabs.sync();
|
||||||
assert.deepEqual([...props], [], 'no open panel, no join');
|
assert.deepEqual([...props], [], 'no open panel, no join');
|
||||||
@@ -517,16 +504,16 @@ test('a rail panel hidden at mount gets its tab when its owner unhides it, from
|
|||||||
tabs.destroy();
|
tabs.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a newly opened tab is scrolled into the strip window once; the join follows the visible part', () => {
|
test('a tab shrunk by a short strip joins with corners no larger than half its height', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
// The strip's offsets are relative to its column, the column's to the rail.
|
// The strip's offsets are relative to its column, the column's to the rail.
|
||||||
Object.assign(f.tabs.column, { offsetTop: 0, offsetLeft: 320 });
|
Object.assign(f.tabs.column, { offsetTop: 0, offsetLeft: 320 });
|
||||||
Object.assign(f.strip, { clientHeight: 100, scrollTop: 0, offsetLeft: 10 });
|
Object.assign(f.strip, { offsetLeft: 10 });
|
||||||
const tab = f.tabFor('global-context-panel');
|
const tab = f.tabFor('global-context-panel');
|
||||||
Object.assign(tab, {
|
Object.assign(tab, {
|
||||||
offsetTop: 150,
|
offsetTop: 150,
|
||||||
offsetLeft: 0,
|
offsetLeft: 0,
|
||||||
offsetHeight: 56,
|
offsetHeight: 12,
|
||||||
offsetWidth: 42,
|
offsetWidth: 42,
|
||||||
});
|
});
|
||||||
Object.assign(f.context, {
|
Object.assign(f.context, {
|
||||||
@@ -537,49 +524,13 @@ test('a newly opened tab is scrolled into the strip window once; the join follow
|
|||||||
});
|
});
|
||||||
f.context.classList.remove('collapsed');
|
f.context.classList.remove('collapsed');
|
||||||
f.tabs.sync();
|
f.tabs.sync();
|
||||||
assert.equal(
|
assert.equal(f.rail.style.props.get('--rail-tab-bottom'), '162px');
|
||||||
f.strip.scrollTop,
|
|
||||||
106,
|
|
||||||
'opened: scrolled so the tab bottom meets the window',
|
|
||||||
);
|
|
||||||
assert.equal(f.rail.style.props.get('--rail-tab-bottom'), '100px');
|
|
||||||
const [, glass] = f.tabs.join.children;
|
const [, glass] = f.tabs.join.children;
|
||||||
f.strip.scrollTop = 220;
|
|
||||||
f.tabs.sync();
|
|
||||||
assert.equal(
|
|
||||||
f.strip.scrollTop,
|
|
||||||
220,
|
|
||||||
'a sync after the user scrolled leaves the strip alone',
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
glass.style.clipPath,
|
|
||||||
"path('M 16 0 L 314 0 A 16 16 0 0 1 330 16 L 330 284 A 16 16 0 0 1 314 300 L 16 300 A 16 16 0 0 1 0 284 L 0 16 A 16 16 0 0 1 16 0 Z')",
|
|
||||||
'the tab is out of view: the join is the panel alone',
|
|
||||||
);
|
|
||||||
f.strip.scrollTop = 190;
|
|
||||||
f.tabs.sync();
|
|
||||||
assert.match(
|
assert.match(
|
||||||
glass.style.clipPath,
|
glass.style.clipPath,
|
||||||
/^path\('M 16 0 L 364 0 A 8 8 0 0 1 372 8 A 8 8 0 0 1 364 16 L 338 16 A 8 8 0 0 0 330 24 L 330 284 /,
|
/L 366 150 A 6 6 0 0 1 372 156 A 6 6 0 0 1 366 162 L 338 162 /,
|
||||||
'a tab half in view joins with the visible 16px and smaller corners',
|
'a 12px tab gets 6px corners',
|
||||||
);
|
);
|
||||||
const cctvTab = f.tabFor('cctv-panel');
|
|
||||||
Object.assign(cctvTab, {
|
|
||||||
offsetTop: 86,
|
|
||||||
offsetLeft: 0,
|
|
||||||
offsetHeight: 56,
|
|
||||||
offsetWidth: 42,
|
|
||||||
});
|
|
||||||
Object.assign(f.cctv, {
|
|
||||||
offsetTop: 0,
|
|
||||||
offsetLeft: 0,
|
|
||||||
offsetHeight: 300,
|
|
||||||
offsetWidth: 330,
|
|
||||||
});
|
|
||||||
f.context.classList.add('collapsed');
|
|
||||||
f.cctv.classList.remove('collapsed');
|
|
||||||
f.tabs.sync();
|
|
||||||
assert.equal(f.strip.scrollTop, 86, 'another tab opens: brought into view');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('railJoinPath shrinks the fillet and corner to the room left, down to a shared edge', () => {
|
test('railJoinPath shrinks the fillet and corner to the room left, down to a shared edge', () => {
|
||||||
@@ -630,7 +581,7 @@ test('railJoinPath shrinks the fillet and corner to the room left, down to a sha
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the stylesheet keeps phones on border-box, folds the strip into narrow desktops, shows a scrollbar and draws the focus ring inside', () => {
|
test('the stylesheet keeps phones on border-box, folds the strip into narrow desktops, shrinks tabs instead of scrolling and draws the focus ring inside', () => {
|
||||||
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../../style.css', import.meta.url));
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
@@ -640,8 +591,16 @@ test('the stylesheet keeps phones on border-box, folds the strip into narrow des
|
|||||||
css,
|
css,
|
||||||
/@media \(min-width: 721px\) and \(max-width: 760px\) \{\s*#right-context-rail\[data-rail-tabs\] \{\s*width: calc\(330px - var\(--right-rail-tabs-inset\)\);/,
|
/@media \(min-width: 721px\) and \(max-width: 760px\) \{\s*#right-context-rail\[data-rail-tabs\] \{\s*width: calc\(330px - var\(--right-rail-tabs-inset\)\);/,
|
||||||
);
|
);
|
||||||
assert.match(css, /#right-rail-tabs \{[^}]*scrollbar-width: thin;/);
|
assert.match(css, /#right-rail-tabs \{[^}]*overflow: hidden;/);
|
||||||
assert.doesNotMatch(css, /#right-rail-tabs::-webkit-scrollbar/);
|
assert.doesNotMatch(
|
||||||
|
css,
|
||||||
|
/#right-rail-tabs \{[^}]*(overflow: [^;]*auto|scrollbar-width)/,
|
||||||
|
);
|
||||||
|
// The tabs give way in proportion 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\);/,
|
||||||
|
);
|
||||||
// The rail layout pass measures the column's natural height to climb above
|
// The rail layout pass measures the column's natural height to climb above
|
||||||
// its baseline before the strip has to scroll.
|
// its baseline before the strip has to scroll.
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -88,12 +88,9 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
/* Room for the open tab's bridge on the left. */
|
/* Room for the open tab's bridge on the left. */
|
||||||
padding-left: var(--right-rail-tabs-gap);
|
padding-left: var(--right-rail-tabs-gap);
|
||||||
overflow: hidden auto;
|
|
||||||
/* The rail climbs above its baseline to fit the column (rightPanelRail.js);
|
/* The rail climbs above its baseline to fit the column (rightPanelRail.js);
|
||||||
a lane still shorter overflows the strip: a thin scrollbar says there is
|
a lane still shorter shrinks the tabs. The strip never scrolls. */
|
||||||
more, and the strip keeps the open tab in view (rightRailTabs.js). */
|
overflow: hidden;
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#right-rail-tools {
|
#right-rail-tools {
|
||||||
@@ -172,16 +169,20 @@ body:not(.mobile-shell)
|
|||||||
outline-offset: -3px;
|
outline-offset: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A tab shrinks in proportion to its label when the strip is short, and
|
||||||
|
clips the label at its end, faded over the bottom padding. */
|
||||||
.right-rail-tab {
|
.right-rail-tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 1 auto;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: var(--right-rail-tabs-width);
|
width: var(--right-rail-tabs-width);
|
||||||
min-height: 40px;
|
min-height: 0;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, #000 calc(100% - 6px), transparent);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--right-rail-tab-radius);
|
border-radius: var(--right-rail-tab-radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -228,9 +229,9 @@ body:not(.mobile-shell)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-rail-tab {
|
.right-rail-tab {
|
||||||
min-height: 36px;
|
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
letter-spacing: 1.3px;
|
letter-spacing: 1.3px;
|
||||||
|
mask-image: linear-gradient(to bottom, #000 calc(100% - 4px), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#right-rail-tools {
|
#right-rail-tools {
|
||||||
|
|||||||
Reference in new issue
Block a user