mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	feat(ui): implement hover for switch (#9057)
Followup to https://codeberg.org/forgejo/forgejo/issues/6459 Preview: - https://codeberg.org/attachments/cc12a227-c6eb-4ab5-91ad-0ef967f88c7a - https://codeberg.org/attachments/a13c8cb6-f654-430c-a6c6-990a2a7f0911 Changes: - hovered items now have background painted with transition - added E2E testing: mostly for CSS logic introduced in this PR but also a bit for existing functionality - variable --color-active is now consistently non-transparent: it already was in forgejo-dark but is now in all themes - some code improvements in switch.css Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9057 Reviewed-by: Beowulf <beowulf@beocode.eu>
This commit is contained in:
		
					parent
					
						
							
								9ed225b100
							
						
					
				
			
			
				commit
				
					
						18cd9b5efa
					
				
			
		
					 5 changed files with 113 additions and 13 deletions
				
			
		
							
								
								
									
										74
									
								
								tests/e2e/switch.test.e2e.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								tests/e2e/switch.test.e2e.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||||
|  | }); | ||||||
|  | @ -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 { | .switch { | ||||||
|   display: grid; |   display: grid; | ||||||
|   grid-auto-flow: column; |   grid-auto-flow: column; | ||||||
|  | @ -9,27 +23,39 @@ | ||||||
|   border-radius: var(--border-radius); |   border-radius: var(--border-radius); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .switch .item { | .switch > .item { | ||||||
|   display: flex; |   display: flex; | ||||||
|   gap: 0.5rem; |   gap: 0.5rem; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   padding: .5em 1.125em; |   padding: var(--switch-padding-y) var(--switch-padding-x); | ||||||
|   color: var(--color-text); |   color: var(--color-text); | ||||||
|   border-radius: var(--border-radius); |   border-radius: var(--border-radius); | ||||||
|   text-wrap: nowrap; |   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); |   background: var(--color-active); | ||||||
|   outline: 1px solid var(--color-input-border); |   outline: 1px solid var(--color-input-border); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (pointer: coarse) { | .switch > button.item { | ||||||
|   .switch .item { |  | ||||||
|     padding: .75em 1.125em; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .switch button.item { |  | ||||||
|   background: transparent; |   background: transparent; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -240,7 +240,7 @@ | ||||||
|   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); |   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); | ||||||
|   --color-light-border: #0000001d; |   --color-light-border: #0000001d; | ||||||
|   --color-hover: #e4e4e4aa; |   --color-hover: #e4e4e4aa; | ||||||
|   --color-active: #d4d4d8aa; |   --color-active: #e2e2e5; | ||||||
|   --color-menu: var(--zinc-100); |   --color-menu: var(--zinc-100); | ||||||
|   --color-card: var(--zinc-50); |   --color-card: var(--zinc-50); | ||||||
|   --fancy-card-bg: var(--zinc-100); |   --fancy-card-bg: var(--zinc-100); | ||||||
|  |  | ||||||
|  | @ -205,7 +205,7 @@ | ||||||
|   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); |   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); | ||||||
|   --color-light-border: #e8e8ff28; |   --color-light-border: #e8e8ff28; | ||||||
|   --color-hover: #e8e8ff19; |   --color-hover: #e8e8ff19; | ||||||
|   --color-active: #e8e8ff24; |   --color-active: #393b43; | ||||||
|   --color-menu: #151a1e; |   --color-menu: #151a1e; | ||||||
|   --color-card: #151a1e; |   --color-card: #151a1e; | ||||||
|   --fancy-card-bg: #14171a; |   --fancy-card-bg: #14171a; | ||||||
|  |  | ||||||
|  | @ -205,7 +205,7 @@ | ||||||
|   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); |   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); | ||||||
|   --color-light-border: #0000171d; |   --color-light-border: #0000171d; | ||||||
|   --color-hover: #00001708; |   --color-hover: #00001708; | ||||||
|   --color-active: #00001714; |   --color-active: #ebebed; | ||||||
|   --color-menu: #f8f9fb; |   --color-menu: #f8f9fb; | ||||||
|   --color-card: #f8f9fb; |   --color-card: #f8f9fb; | ||||||
|   --fancy-card-bg: #ffffff; |   --fancy-card-bg: #ffffff; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue