fix(look): mærket og de faste visninger fulgte med op, når listen rullede

Hele sidebjælken rullede, så runen og I dag forsvandt over kanten, så snart der
var projekter nok — og de er dem, man er på vej hen til oftest. Alt under de
faste visninger ligger i en rulleboks nu, så rullebjælken begynder under dem.

En boks indeni frem for `position: sticky` udenom: et klæbende hoved efterlader
rullebjælken i fuld højde ved siden af sig, og det var netop den, der skulle
starte længere nede. Snippet-erklæringerne bliver liggende udenfor — de tegner
ikke noget selv, men de kaldes oppe fra .views, og en snippet erklæret indeni
hører kun til indeni.

Chevronen for en mappe dækkede gruppens prik med et felt i --surface. På den
sænkede bjælke er det en lys firkant, og en foldet gruppe har sin chevron fremme
permanent, så klodsen stod der hele tiden. Prikken tones ud i stedet.

Det kostede en fælde: en `opacity` under 1 laver en stakkontekst og maler
elementet, som var det placeret med `z-index: 0`. Prikken lagde sig dermed oven
på knappen, den lige var forsvundet for, og tog klikket — usynlig og stadig i
vejen, så gruppen ikke kunne foldes. Ét lag op på knappen, og rækkefølgen er
som før.

Prøven for vandret rul måler nu begge bokse. Bjælken selv er `overflow: hidden`,
så en for bred række ville blive klippet dér og aldrig nå at tælle med — den
ville bestå af den forkerte grund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-20 14:56:01 +02:00
co-authored by Claude Opus 5
parent 6c3d66108b
commit 98bfa216d6
3 changed files with 406 additions and 281 deletions
+74
View File
@@ -358,6 +358,20 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
'aria-expanded', 'aria-expanded',
'false' 'false'
); );
// En foldet gruppe står med sin chevron fremme permanent, og den lå oven i
// prikken med et felt i --surface bag sig — en lys firkant ud for navnet på
// den sænkede bjælke, hele tiden. Nu tones prikken ud i stedet, så der er
// ingenting at male henover den med.
const chevron = await sidebar
.locator('.folder-head .chevron-button')
.evaluate((el) => ({
background: getComputedStyle(el).backgroundColor,
dot: getComputedStyle(el.closest('.folder-head').querySelector('.group-dot')).opacity
}));
expect(chevron.background, 'chevronen har en firkant bag sig').toBe('rgba(0, 0, 0, 0)');
expect(chevron.dot, 'prikken ligger stadig under chevronen').toBe('0');
await sidebar.getByRole('button', { name: /^Fold Arbejde/ }).click(); await sidebar.getByRole('button', { name: /^Fold Arbejde/ }).click();
// Colour, which is chosen while renaming rather than from the row. Both are // Colour, which is chosen while renaming rather than from the row. Both are
@@ -1814,6 +1828,14 @@ test('sidebjælken kan ikke scrolles vandret', async ({ page }) => {
const sideways = await sidebar.evaluate((el) => el.scrollWidth - el.clientWidth); const sideways = await sidebar.evaluate((el) => el.scrollWidth - el.clientWidth);
expect(sideways, 'sidebjælken kan scrolles vandret').toBeLessThanOrEqual(1); expect(sideways, 'sidebjælken kan scrolles vandret').toBeLessThanOrEqual(1);
// Og rulleboksen indeni, som er den, der faktisk ruller nu. Bjælken selv er
// `overflow: hidden`, så en for bred række ville blive klippet dér og aldrig
// nå at tælle med ovenfor — målingen ville stå og bestå af den forkerte grund.
const inner = await sidebar
.locator('.scroller')
.evaluate((el) => el.scrollWidth - el.clientWidth);
expect(inner, 'rulleboksen kan scrolles vandret').toBeLessThanOrEqual(1);
// And the whole page, for the same reason at a different scale. // And the whole page, for the same reason at a different scale.
const page_sideways = await page.evaluate( const page_sideways = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth () => document.documentElement.scrollWidth - document.documentElement.clientWidth
@@ -1827,6 +1849,58 @@ test('sidebjælken kan ikke scrolles vandret', async ({ page }) => {
expect(trouble).toEqual([]); expect(trouble).toEqual([]);
}); });
/**
* Mærket og de faste visninger bliver stående, når resten ruller.
*
* Rullebjælken gik før hele vejen op: runen og I dag forsvandt op over kanten, så
* snart der var projekter nok — og de er dem, man er på vej hen til oftest.
*
* Målt i et lavt vindue, for i et højt er der ikke noget at rulle, og en prøve,
* der ikke ruller, består uanset hvad man laver om. Bredden holdes over
* telefongrænsen, hvor bjælken bliver til en skuffe og er en anden ting.
*/
test('sidebjælkens hoved bliver stående, når listen ruller', async ({ page }) => {
const trouble = watchForTrouble(page);
await page.goto('/');
await page.setViewportSize({ width: 1000, height: 420 });
const sidebar = page.getByRole('navigation', { name: 'Hovedmenu' });
const scroller = sidebar.locator('.scroller');
await expect(scroller).toBeVisible();
// Rullebjælken begynder under de faste visninger og ikke bag dem.
const geometry = await sidebar.evaluate((el) => {
const box = (sel) => el.querySelector(sel).getBoundingClientRect().toJSON();
const scroll = el.querySelector('.scroller');
return {
views: box('.views'),
scroller: box('.scroller'),
overflows: scroll.scrollHeight - scroll.clientHeight
};
});
expect(geometry.overflows, 'der er ikke noget at rulle — prøven beviser intet').toBeGreaterThan(
0
);
expect(
geometry.scroller.top,
'rulleboksen begynder oppe i de faste visninger'
).toBeGreaterThanOrEqual(geometry.views.bottom - 1);
// Og hovedet bliver, hvor det er, hele vejen ned.
const brand = sidebar.locator('.brand');
const before = await brand.boundingBox();
await scroller.evaluate((el) => el.scrollTo(0, el.scrollHeight));
await expect
.poll(async () => scroller.evaluate((el) => el.scrollTop))
.toBeGreaterThan(0);
const after = await brand.boundingBox();
expect(Math.abs(after.y - before.y), 'mærket fulgte med op').toBeLessThanOrEqual(1);
await expect(sidebar.getByRole('link', { name: 'I dag' })).toBeInViewport();
expect(trouble).toEqual([]);
});
/** /**
* A project can have more than one section. * A project can have more than one section.
* *
+55 -3
View File
@@ -639,6 +639,17 @@
</h2> </h2>
{/snippet} {/snippet}
<!-- Alt herunder ruller; mærket og de faste visninger gør ikke.
Hele sidebjælken rullede før, så runen og I dag forsvandt op over kanten,
så snart der var projekter nok — og de er dem, man er på vej hen til
oftest. En rulleboks indeni frem for `position: sticky` udenom, fordi et
klæbende hoved efterlader rullebjælken i fuld højde ved siden af sig: den
skal begynde under hovedet, ikke bag det.
Snippet-erklæringerne bliver liggende udenfor. De tegner ikke noget selv,
men de kaldes oppe fra .views, og en snippet erklæret herinde hører kun
til herinde. -->
<div class="scroller">
<div class="group"> <div class="group">
<!-- The heading is also the drop target for "no group": without it, a project <!-- The heading is also the drop target for "no group": without it, a project
dragged into a group could only be got out again by emptying the group, dragged into a group could only be got out again by emptying the group,
@@ -935,6 +946,7 @@
<span class="signout">{t('nav.signOut')}</span> <span class="signout">{t('nav.signOut')}</span>
</button> </button>
</div> </div>
</div>
<!-- Also a slider for the keyboard: arrow keys move it, and Home puts it back. <!-- Also a slider for the keyboard: arrow keys move it, and Home puts it back.
A drag handle with no keyboard equivalent is a setting some people simply A drag handle with no keyboard equivalent is a setting some people simply
cannot reach. --> cannot reach. -->
@@ -970,11 +982,31 @@
padding: var(--s4) var(--s3); padding: var(--s4) var(--s3);
background: var(--surface-sunken); background: var(--surface-sunken);
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
overflow-y: auto; /* Selve bjælken ruller ikke længere — .scroller gør. `hidden` frem for
ingenting, fordi den stadig er den yderste kant: løber en række over,
skal den klippes her og ikke skubbe kolonnen bredere. */
overflow: hidden;
padding-left: max(var(--s3), env(safe-area-inset-left)); padding-left: max(var(--s3), env(safe-area-inset-left));
position: relative; position: relative;
} }
/* `min-height: 0`, ellers nægter et flex-element at blive lavere end sit
indhold, og boksen ville vokse ned gennem bunden af skærmen i stedet for at
rulle — samme fælde som `min-width: 0` på kontonavnet længere nede.
Ikke `overflow-x: hidden` her: `overflow-y: auto` gør den anden akse til
`auto` af sig selv, og en vandret rullebjælke under menuen er en fejl i en
række, ikke i boksen. At klippe den ville skjule fejlen; prøven måler begge
bokse. */
.scroller {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--s5);
}
/* Wider than it looks: a 1px target is a target you miss. The visible line stays /* Wider than it looks: a 1px target is a target you miss. The visible line stays
the border; this only widens what the pointer hits. the border; this only widens what the pointer hits.
@@ -1213,6 +1245,11 @@
The dot is the resting state; the chevron takes over on hover or focus, and The dot is the resting state; the chevron takes over on hover or focus, and
stays put while the group is folded, so a folded group still shows the way stays put while the group is folded, so a folded group still shows the way
back open. */ back open. */
/* Uden baggrund. Knappen dækkede prikken med et felt i --surface, og på den
sænkede bjælke er det en lys firkant: en foldet gruppe stod med en hvid
klods ud for navnet permanent, fordi chevronen bliver stående, når gruppen
er lukket. Prikken tones ud i stedet for at blive dækket — det er samme
skifte, uden noget at male henover den med. */
.chevron-button { .chevron-button {
position: absolute; position: absolute;
left: calc(var(--s2) - 5px); left: calc(var(--s2) - 5px);
@@ -1223,7 +1260,13 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: inherit; color: inherit;
opacity: 0; opacity: 0;
background: var(--surface); /* Fordi prikken herunder tones ud frem for at blive dækket. En `opacity`
under 1 laver en stakkontekst og maler elementet, som var det placeret
med `z-index: 0` — så prikken, der står senere i DOM'en, lagde sig oven
på knappen i det øjeblik den forsvandt. Den var usynlig og tog stadig
klikket: prøven brugte tredive sekunder på at folde en gruppe, der ikke
ville foldes. Ét lag op, og rækkefølgen er den samme som før. */
z-index: 1;
} }
.folder-head:hover .chevron-button, .folder-head:hover .chevron-button,
@@ -1232,9 +1275,18 @@
opacity: 1; opacity: 1;
} }
/* Præcis de tre tilfælde ovenfor, læst nedad i stedet for opad: står chevronen
der, er prikken væk. `opacity` og ikke `display`, for de to ligger oven i
hinanden — forsvandt prikken ud af flowet, ville navnet rykke sig hver gang
pilen kom frem. */
.folder-head:hover .group-dot,
.folder-head:has(.chevron-button:focus-visible) .group-dot,
.folder-head:has(.chevron-button[aria-expanded='false']) .group-dot {
opacity: 0;
}
.chevron-button:hover { .chevron-button:hover {
color: var(--ink); color: var(--ink);
background: var(--surface);
} }
.fold a, .fold a,
-1
View File
@@ -437,7 +437,6 @@
padding-left: max(var(--s3), env(safe-area-inset-left)); padding-left: max(var(--s3), env(safe-area-inset-left));
padding-right: var(--s3); padding-right: var(--s3);
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
overflow-y: auto;
} }
} }
</style> </style>