diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 26a0b98..58c7d0b 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -358,6 +358,20 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me 'aria-expanded', '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(); // 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); 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. const page_sideways = await page.evaluate( () => document.documentElement.scrollWidth - document.documentElement.clientWidth @@ -1827,6 +1849,58 @@ test('sidebjælken kan ikke scrolles vandret', async ({ page }) => { 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. * diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte index 8894065..9ffe437 100644 --- a/web/src/lib/components/Sidebar.svelte +++ b/web/src/lib/components/Sidebar.svelte @@ -639,301 +639,313 @@ {/snippet} -
- - -
{ - if (!carries(e, PROJECT)) return; - accept(e); - overGroup = ''; - overId = null; - }} - ondragleave={() => (overGroup = null)} - ondrop={(e) => onDropInGroup(e, '')} - > - {@render foldHeading('projects', t('sidebar.projects'))} - - -
+ + 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. --> +
+
+
onGroupDragStart(e, group)} - ondragend={clearDrag} - ondragover={(e) => onGroupDragOver(e, group)} - ondragleave={() => { - overGroup = null; + class="group-head" + class:over={overGroup === ''} + ondragover={(e) => { + if (!carries(e, PROJECT)) return; + accept(e); + overGroup = ''; overId = null; }} - ondrop={(e) => onGroupDrop(e, group)} - ondblclick={() => startRename(group)} - onkeydown={(e) => { - if (e.key === 'F2') { - e.preventDefault(); - startRename(group); - } - }} + ondragleave={() => (overGroup = null)} + ondrop={(e) => onDropInGroup(e, '')} > - {#if renamingGroup === group.id} - -
renameGroup(e, group)} - onfocusout={(e) => { - if (!e.currentTarget.contains(e.relatedTarget)) renamingGroup = null; - }} - > - e.key === 'Escape' && (renamingGroup = null)} - /> -
- {#each COLORS as color (color.id)} - - {/each} -
-
- {:else} - - - -

- - { - e.preventDefault(); - clearTimeout(navTimer); - if (e.detail > 1) return; - navTimer = setTimeout(() => { - onnavigate(); - goto(`/gruppe/${group.id}`); - }, 200); - }} - > - - {group.name} - -

- {inside.length} - {/if} + {@render foldHeading('projects', t('sidebar.projects'))} + +
- {#if !group.collapsed} - {#each inside as project (project.id)} + {#if adding} +
+ !newName.trim() && (adding = false)} + onkeydown={(e) => e.key === 'Escape' && (adding = false)} + /> +
+ {/if} + + {#if addingGroup} +
+ !newGroupName.trim() && (addingGroup = false)} + onkeydown={(e) => e.key === 'Escape' && (addingGroup = false)} + /> +
+ {/if} + + {#if !app.sectionCollapsed('projects')} + {#each ungrouped as project (project.id)} {@render projectRow(project, true)} {/each} - {#if !inside.length} -

{t('sidebar.emptyGroup')}

+ + {#if own.length === 0 && !adding} +

{t('sidebar.noProjects')}

{/if} {/if}
- {/each} - {#if shared.length} -
-
{@render foldHeading('shared', t('sidebar.shared'))}
- {#if !app.sectionCollapsed('shared')} - {#each shared as project (project.id)} - {@render projectRow(project, false)} - {/each} - {/if} -
- {/if} + {#each groups as group (group.id)} + {@const inside = inGroup(group.id)} +
+ + +
onGroupDragStart(e, group)} + ondragend={clearDrag} + ondragover={(e) => onGroupDragOver(e, group)} + ondragleave={() => { + overGroup = null; + overId = null; + }} + ondrop={(e) => onGroupDrop(e, group)} + ondblclick={() => startRename(group)} + onkeydown={(e) => { + if (e.key === 'F2') { + e.preventDefault(); + startRename(group); + } + }} + > + {#if renamingGroup === group.id} + +
renameGroup(e, group)} + onfocusout={(e) => { + if (!e.currentTarget.contains(e.relatedTarget)) renamingGroup = null; + }} + > + e.key === 'Escape' && (renamingGroup = null)} + /> +
+ {#each COLORS as color (color.id)} + + {/each} +
+
+ {:else} + + + +

+ + { + e.preventDefault(); + clearTimeout(navTimer); + if (e.detail > 1) return; + navTimer = setTimeout(() => { + onnavigate(); + goto(`/gruppe/${group.id}`); + }, 200); + }} + > + + {group.name} + +

+ {inside.length} + {/if} +
- {#if labels.length} -
-
{@render foldHeading('labels', t('sidebar.labels'))}
- {#if !app.sectionCollapsed('labels')} - {#each labels.filter((l) => l.task_count > 0) as label (label.id)} - - - {label.name} - {label.task_count} - - {/each} - {/if} -
- {/if} + {#if !group.collapsed} + {#each inside as project (project.id)} + {@render projectRow(project, true)} + {/each} + {#if !inside.length} +

{t('sidebar.emptyGroup')}

+ {/if} + {/if} +
+ {/each} -
- - - {t('nav.settings')} - - - - {#if !app.connected} - {t('nav.offline')} + {#if shared.length} +
+
{@render foldHeading('shared', t('sidebar.shared'))}
+ {#if !app.sectionCollapsed('shared')} + {#each shared as project (project.id)} + {@render projectRow(project, false)} + {/each} + {/if} +
{/if} - + + {#if filters.length} +
+
{@render foldHeading('filters', t('sidebar.filters'))}
+ {#if !app.sectionCollapsed('filters')} + {#each filters as filter (filter.id)} + + + {filter.name} + + {/each} + {/if} +
+ {/if} + + {#if labels.length} +
+
{@render foldHeading('labels', t('sidebar.labels'))}
+ {#if !app.sectionCollapsed('labels')} + {#each labels.filter((l) => l.task_count > 0) as label (label.id)} + + + {label.name} + {label.task_count} + + {/each} + {/if} +
+ {/if} + +
+ + + {t('nav.settings')} + + + + {#if !app.connected} + {t('nav.offline')} + {/if} + +