diff --git a/tests/e2e/switch.test.e2e.ts b/tests/e2e/switch.test.e2e.ts new file mode 100644 index 0000000000..217a6576b9 --- /dev/null +++ b/tests/e2e/switch.test.e2e.ts @@ -0,0 +1,74 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/css/modules/switch.css +// web_src/css/themes +// @watch end + +import {expect} from '@playwright/test'; +import {test} from './utils_e2e.ts'; + +test('Switch CSS properties', async ({browser}) => { + // This test doesn't need JS and runs a little faster without it + const context = await browser.newContext({javaScriptEnabled: false}); + const page = await context.newPage(); + + const noBg = 'rgba(0, 0, 0, 0)'; + const activeBg = 'rgb(226, 226, 229)'; + + const normalMargin = '0px'; + const normalPadding = '15.75px'; + + const specialLeftMargin = '-4px'; + const specialPadding = '19.75px'; + + async function evaluateSwitchItem(page, selector, isActive, background, marginLeft, marginRight, paddingLeft, paddingRight) { + const item = page.locator(selector); + if (isActive) { + await expect(item).toHaveClass(/active/); + } else { + await expect(item).not.toHaveClass(/active/); + } + const cs = await item.evaluate((el) => { + // In Firefox getComputedStyle is undefined if returned from evaluate + const s = getComputedStyle(el); + return { + backgroundColor: s.backgroundColor, + marginLeft: s.marginLeft, + marginRight: s.marginRight, + paddingLeft: s.paddingLeft, + paddingRight: s.paddingRight, + }; + }); + expect(cs.backgroundColor).toBe(background); + expect(cs.marginLeft).toBe(marginLeft); + expect(cs.marginRight).toBe(marginRight); + expect(cs.paddingLeft).toBe(paddingLeft); + expect(cs.paddingRight).toBe(paddingRight); + } + + await page.goto('/user2/repo1/pulls'); + + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(1)', true, activeBg, normalMargin, normalMargin, normalPadding, normalPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(2)', false, noBg, specialLeftMargin, normalMargin, specialPadding, normalPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(3)', false, noBg, normalMargin, normalMargin, normalPadding, normalPadding); + + await page.goto('/user2/repo1/pulls?state=closed'); + + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(1)', false, noBg, normalMargin, specialLeftMargin, normalPadding, specialPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(2)', true, activeBg, normalMargin, normalMargin, normalPadding, normalPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(3)', false, noBg, specialLeftMargin, normalMargin, specialPadding, normalPadding); + + await page.goto('/user2/repo1/pulls?state=all'); + + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(1)', false, noBg, normalMargin, normalMargin, normalPadding, normalPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(2)', false, noBg, normalMargin, specialLeftMargin, normalPadding, specialPadding); + await evaluateSwitchItem(page, '#issue-filters .switch > .item:nth-child(3)', true, activeBg, normalMargin, normalMargin, normalPadding, normalPadding); + + // E2E already runs clients with both fine and coarse pointer simulated + // This test will verify that coarse-related CSS is working as intended + const itemHeight = await page.evaluate(() => window.matchMedia('(pointer: coarse)').matches) ? 41 : 34; + // In Firefox Math.round is needed because .height is 33.99998474121094 + expect(Math.round((await page.locator('#issue-filters .switch > .item:nth-child(1)').boundingBox()).height)).toBe(itemHeight); +}); diff --git a/web_src/css/modules/switch.css b/web_src/css/modules/switch.css index 7780155787..6777c32fc8 100644 --- a/web_src/css/modules/switch.css +++ b/web_src/css/modules/switch.css @@ -1,3 +1,17 @@ +/* Copyright 2025 The Forgejo Authors. All rights reserved. +SPDX-License-Identifier: GPL-3.0-or-later */ + +:root .switch { + --switch-padding-y: .5em; + --switch-padding-x: 1.125em; +} + +@media (pointer: coarse) { + :root .switch { + --switch-padding-y: .75em; + } +} + .switch { display: grid; grid-auto-flow: column; @@ -9,27 +23,39 @@ border-radius: var(--border-radius); } -.switch .item { +.switch > .item { display: flex; gap: 0.5rem; align-items: center; - padding: .5em 1.125em; + padding: var(--switch-padding-y) var(--switch-padding-x); color: var(--color-text); border-radius: var(--border-radius); text-wrap: nowrap; + transition: background-color 0.1s ease; } -.switch .active.item { +.switch > .item:hover { + background: var(--color-hover); +} + +/* Item that has to crawl under it's active neighbor, so when it is hovered, +there are no ugly unpainted v/^ shapes between them */ +.switch > .item:has(+ .active.item) { /* Active neighbor is next item */ + margin-right: calc(-1 * var(--border-radius)); + padding-right: calc(var(--switch-padding-x) + var(--border-radius)); +} +.switch > .active.item + .item { /* Active neighbor is previous item */ + margin-left: calc(-1 * var(--border-radius)); + padding-left: calc(var(--switch-padding-x) + var(--border-radius)); +} + +.switch > .active.item { + z-index: 2; + padding-left: var(--switch-padding-x); background: var(--color-active); outline: 1px solid var(--color-input-border); } -@media (pointer: coarse) { - .switch .item { - padding: .75em 1.125em; - } -} - -.switch button.item { +.switch > button.item { background: transparent; } diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css index 37ddc2a413..44b997b39c 100644 --- a/web_src/css/themes/theme-forgejo-light.css +++ b/web_src/css/themes/theme-forgejo-light.css @@ -240,7 +240,7 @@ --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #0000001d; --color-hover: #e4e4e4aa; - --color-active: #d4d4d8aa; + --color-active: #e2e2e5; --color-menu: var(--zinc-100); --color-card: var(--zinc-50); --fancy-card-bg: var(--zinc-100); diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 7ba428f0b7..84183a9e63 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -205,7 +205,7 @@ --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #e8e8ff28; --color-hover: #e8e8ff19; - --color-active: #e8e8ff24; + --color-active: #393b43; --color-menu: #151a1e; --color-card: #151a1e; --fancy-card-bg: #14171a; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 8ad89f44a4..aee47dc814 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -205,7 +205,7 @@ --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #0000171d; --color-hover: #00001708; - --color-active: #00001714; + --color-active: #ebebed; --color-menu: #f8f9fb; --color-card: #f8f9fb; --fancy-card-bg: #ffffff;