fix(sidebar): refine search and tag controls
- commit
- 789ddb7d1566f4fb4b4b07f4c57739b8e994c80e
- author
- brice · · 2026-08-12 06:36 UTC
- parent
- 700b5d6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +33 -12
M internal/ui/component/favorites_sidebar_keyboard.go +4 -4
| @@ -17,10 +17,10 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() { | |||
| 17 | 17 | if keyController == nil { | |
| 18 | 18 | return | |
| 19 | 19 | } | |
| 20 | - | // Let focused child widgets process their own keys before sidebar commands. | |
| 21 | - | // In particular, GtkEntry consumes printable text during the target phase, so | |
| 22 | - | // its content cannot reach the global command switch below. | |
| 23 | - | keyController.SetPropagationPhase(gtk.PhaseBubbleValue) | |
| 20 | + | // Directional controls must run before GtkSearchEntry and GtkListBox consume | |
| 21 | + | // arrows. Explicit text-input focus tracking below still defers printable | |
| 22 | + | // keys to their focused entry. | |
| 23 | + | keyController.SetPropagationPhase(gtk.PhaseCaptureValue) | |
| 24 | 24 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | |
| 25 | 25 | fs.mu.RLock() | |
| 26 | 26 | if fs.destroyed { | |
| @@ -17,10 +17,10 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() { | |||||
| 17 | if keyController == nil { | 17 | if keyController == nil { | ||
| 18 | return | 18 | return | ||
| 19 | } | 19 | } | ||
| 20 | - | // Let focused child widgets process their own keys before sidebar commands. | 20 | + | // Directional controls must run before GtkSearchEntry and GtkListBox consume |
| 21 | - | // In particular, GtkEntry consumes printable text during the target phase, so | 21 | + | // arrows. Explicit text-input focus tracking below still defers printable |
| 22 | - | // its content cannot reach the global command switch below. | 22 | + | // keys to their focused entry. |
| 23 | - | keyController.SetPropagationPhase(gtk.PhaseBubbleValue) | 23 | + | keyController.SetPropagationPhase(gtk.PhaseCaptureValue) |
| 24 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | 24 | keyPressedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, state gdk.ModifierType) bool { | ||
| 25 | fs.mu.RLock() | 25 | fs.mu.RLock() | ||
| 26 | if fs.destroyed { | 26 | if fs.destroyed { | ||
M internal/ui/component/favorites_sidebar_rows.go +1 -1
| @@ -126,7 +126,7 @@ func (fs *FavoritesSidebar) appendTagFilter( | |||
| 126 | 126 | } | |
| 127 | 127 | button.AddCssClass("favorites-sidebar-tag-filter") | |
| 128 | 128 | if selected { | |
| 129 | - | button.AddCssClass("suggested-action") | |
| 129 | + | button.AddCssClass("favorites-sidebar-tag-filter-active") | |
| 130 | 130 | } | |
| 131 | 131 | callback := func(_ gtk.Button) { action() } | |
| 132 | 132 | *callbacks = append(*callbacks, callback) | |
| @@ -126,7 +126,7 @@ func (fs *FavoritesSidebar) appendTagFilter( | |||||
| 126 | } | 126 | } | ||
| 127 | button.AddCssClass("favorites-sidebar-tag-filter") | 127 | button.AddCssClass("favorites-sidebar-tag-filter") | ||
| 128 | if selected { | 128 | if selected { | ||
| 129 | - | button.AddCssClass("suggested-action") | 129 | + | button.AddCssClass("favorites-sidebar-tag-filter-active") |
| 130 | } | 130 | } | ||
| 131 | callback := func(_ gtk.Button) { action() } | 131 | callback := func(_ gtk.Button) { action() } | ||
| 132 | *callbacks = append(*callbacks, callback) | 132 | *callbacks = append(*callbacks, callback) | ||
M internal/ui/component/favorites_sidebar_test.go +14 -3
| @@ -433,9 +433,10 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | |||
| 433 | 433 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | |
| 434 | 434 | } | |
| 435 | 435 | ||
| 436 | - | func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) { | |
| 437 | - | fs := newFavoritesSidebarHarness(nil, nil) | |
| 438 | - | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} | |
| 436 | + | func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { | |
| 437 | + | fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} | |
| 438 | + | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | |
| 439 | + | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} | |
| 439 | 440 | fs.focusZone = favoritesSidebarFocusSearch | |
| 440 | 441 | ||
| 441 | 442 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | |
| @@ -444,6 +445,16 @@ func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) { | |||
| 444 | 445 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | |
| 445 | 446 | } | |
| 446 | 447 | ||
| 448 | + | func TestFavoritesSidebarDirectionalFocusConsumesArrowsOnTagControls(t *testing.T) { | |
| 449 | + | fs := newFavoritesSidebarHarness(nil, nil) | |
| 450 | + | fs.focusZone = favoritesSidebarFocusTags | |
| 451 | + | ||
| 452 | + | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | |
| 453 | + | assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) | |
| 454 | + | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) | |
| 455 | + | assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) | |
| 456 | + | } | |
| 457 | + | ||
| 447 | 458 | func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) { | |
| 448 | 459 | fav := &entity.Favorite{ID: 5, URL: "https://go.dev"} | |
| 449 | 460 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | |
| @@ -433,9 +433,10 @@ func TestFavoritesSidebarFormTagSelectionAndEditSynchronization(t *testing.T) { | |||||
| 433 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | 433 | assert.Equal(t, dev.ID, uc.untagged[0].tagID) | ||
| 434 | } | 434 | } | ||
| 435 | 435 | ||||
| 436 | - | func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) { | 436 | + | func TestFavoritesSidebarDirectionalFocusMovesDirectlyBetweenSearchAndFirstFavorite(t *testing.T) { |
| 437 | - | fs := newFavoritesSidebarHarness(nil, nil) | 437 | + | fav := &entity.Favorite{ID: 1, URL: "https://go.dev"} |
| 438 | - | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: 1, Favorite: &entity.Favorite{ID: 1}, Selectable: true}} | 438 | + | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) |
| 439 | + | fs.displayRows = []favoriteSidebarDisplayRow{{FavoriteID: fav.ID, Favorite: fav, URL: fav.URL, Selectable: true}} | |||
| 439 | fs.focusZone = favoritesSidebarFocusSearch | 440 | fs.focusZone = favoritesSidebarFocusSearch | ||
| 440 | 441 | ||||
| 441 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | 442 | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | ||
| @@ -444,6 +445,16 @@ func TestFavoritesSidebarDirectionalFocusSkipsTagControls(t *testing.T) { | |||||
| 444 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | 445 | assert.Equal(t, favoritesSidebarFocusSearch, fs.focusZone) | ||
| 445 | } | 446 | } | ||
| 446 | 447 | ||||
| 448 | + | func TestFavoritesSidebarDirectionalFocusConsumesArrowsOnTagControls(t *testing.T) { | |||
| 449 | + | fs := newFavoritesSidebarHarness(nil, nil) | |||
| 450 | + | fs.focusZone = favoritesSidebarFocusTags | |||
| 451 | + | ||||
| 452 | + | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Down))) | |||
| 453 | + | assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) | |||
| 454 | + | assert.True(t, fs.routeDirectionalFocus(uint(gdk.KEY_Up))) | |||
| 455 | + | assert.Equal(t, favoritesSidebarFocusTags, fs.focusZone) | |||
| 456 | + | } | |||
| 457 | + | ||||
| 447 | func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) { | 458 | func TestFavoritesSidebarEnterConfirmsDelete(t *testing.T) { | ||
| 448 | fav := &entity.Favorite{ID: 5, URL: "https://go.dev"} | 459 | fav := &entity.Favorite{ID: 5, URL: "https://go.dev"} | ||
| 449 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | 460 | fs := newFavoritesSidebarHarness([]*entity.Favorite{fav}, nil) | ||
M internal/ui/theme/favorites_sidebar_css.go +1 -1
| @@ -28,7 +28,7 @@ func generateFavoritesSidebarCSS(_ Palette) string { | |||
| 28 | 28 | background-color: alpha(var(--accent), 0.10); | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | - | .favorites-sidebar-tag-filter.suggested-action { | |
| 31 | + | .favorites-sidebar-tag-filter-active { | |
| 32 | 32 | color: var(--accent); | |
| 33 | 33 | background-color: alpha(var(--accent), 0.16); | |
| 34 | 34 | border-color: alpha(var(--accent), 0.5); | |
| @@ -28,7 +28,7 @@ func generateFavoritesSidebarCSS(_ Palette) string { | |||||
| 28 | background-color: alpha(var(--accent), 0.10); | 28 | background-color: alpha(var(--accent), 0.10); | ||
| 29 | } | 29 | } | ||
| 30 | 30 | ||||
| 31 | - | .favorites-sidebar-tag-filter.suggested-action { | 31 | + | .favorites-sidebar-tag-filter-active { |
| 32 | color: var(--accent); | 32 | color: var(--accent); | ||
| 33 | background-color: alpha(var(--accent), 0.16); | 33 | background-color: alpha(var(--accent), 0.16); | ||
| 34 | border-color: alpha(var(--accent), 0.5); | 34 | border-color: alpha(var(--accent), 0.5); | ||
M internal/ui/theme/favorites_sidebar_css_test.go +3 -1
| @@ -13,7 +13,7 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T) | |||
| 13 | 13 | for _, selector := range []string{ | |
| 14 | 14 | ".favorites-sidebar-tags", | |
| 15 | 15 | ".favorites-sidebar-tag-filter", | |
| 16 | - | ".favorites-sidebar-tag-filter.suggested-action", | |
| 16 | + | ".favorites-sidebar-tag-filter-active", | |
| 17 | 17 | ".favorites-sidebar-tag-add", | |
| 18 | 18 | ".favorites-sidebar-tag-prompt", | |
| 19 | 19 | ".favorites-sidebar-tag-binding", | |
| @@ -25,6 +25,8 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T) | |||
| 25 | 25 | } { | |
| 26 | 26 | assert.Contains(t, css, selector+" {") | |
| 27 | 27 | } | |
| 28 | + | assert.Contains(t, css, ".favorites-sidebar-tag-filter-active {", "active filters use sidebar-owned styling") | |
| 29 | + | assert.NotContains(t, css, ".suggested-action", "native suggested-action geometry must not affect tag filters") | |
| 28 | 30 | assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact") | |
| 29 | 31 | assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list") | |
| 30 | 32 | assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list") | |
| @@ -13,7 +13,7 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T) | |||||
| 13 | for _, selector := range []string{ | 13 | for _, selector := range []string{ | ||
| 14 | ".favorites-sidebar-tags", | 14 | ".favorites-sidebar-tags", | ||
| 15 | ".favorites-sidebar-tag-filter", | 15 | ".favorites-sidebar-tag-filter", | ||
| 16 | - | ".favorites-sidebar-tag-filter.suggested-action", | 16 | + | ".favorites-sidebar-tag-filter-active", |
| 17 | ".favorites-sidebar-tag-add", | 17 | ".favorites-sidebar-tag-add", | ||
| 18 | ".favorites-sidebar-tag-prompt", | 18 | ".favorites-sidebar-tag-prompt", | ||
| 19 | ".favorites-sidebar-tag-binding", | 19 | ".favorites-sidebar-tag-binding", | ||
| @@ -25,6 +25,8 @@ func TestGenerateFavoritesSidebarCSS_ContainsCompactControlStyles(t *testing.T) | |||||
| 25 | } { | 25 | } { | ||
| 26 | assert.Contains(t, css, selector+" {") | 26 | assert.Contains(t, css, selector+" {") | ||
| 27 | } | 27 | } | ||
| 28 | + | assert.Contains(t, css, ".favorites-sidebar-tag-filter-active {", "active filters use sidebar-owned styling") | |||
| 29 | + | assert.NotContains(t, css, ".suggested-action", "native suggested-action geometry must not affect tag filters") | |||
| 28 | assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact") | 30 | assert.Contains(t, css, "font-size: 0.66em;", "tags and shortcut badges stay compact") | ||
| 29 | assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list") | 31 | assert.Contains(t, css, "border-bottom: 0.0625em solid var(--border);", "filters are separated from the list") | ||
| 30 | assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list") | 32 | assert.Contains(t, css, "border-top: 0.0625em solid var(--border);", "the form is separated from the list") | ||
M internal/ui/theme/history_sidebar_css_test.go +7 -0
| @@ -116,6 +116,13 @@ func TestGenerateSidebarCSS_CustomPaletteValuesInterpolated(t *testing.T) { | |||
| 116 | 116 | assert.Contains(t, css, ".sidebar-empty {") | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | + | func TestGenerateSidebarCSS_UsesCompactBalancedSearchChrome(t *testing.T) { | |
| 120 | + | css := generateSidebarCSS(DefaultDarkPalette()) | |
| 121 | + | ||
| 122 | + | assert.Contains(t, css, ".sidebar-search-box {\n\tpadding: 0.5em 0.5em 0.375em;") | |
| 123 | + | assert.Contains(t, css, ".sidebar-search {\n\tmin-height: 0;\n\tpadding: 0.0625em 0.375em;") | |
| 124 | + | } | |
| 125 | + | ||
| 119 | 126 | func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) { | |
| 120 | 127 | css := generateSidebarCSS(DefaultDarkPalette()) | |
| 121 | 128 | ||
| @@ -116,6 +116,13 @@ func TestGenerateSidebarCSS_CustomPaletteValuesInterpolated(t *testing.T) { | |||||
| 116 | assert.Contains(t, css, ".sidebar-empty {") | 116 | assert.Contains(t, css, ".sidebar-empty {") | ||
| 117 | } | 117 | } | ||
| 118 | 118 | ||||
| 119 | + | func TestGenerateSidebarCSS_UsesCompactBalancedSearchChrome(t *testing.T) { | |||
| 120 | + | css := generateSidebarCSS(DefaultDarkPalette()) | |||
| 121 | + | ||||
| 122 | + | assert.Contains(t, css, ".sidebar-search-box {\n\tpadding: 0.5em 0.5em 0.375em;") | |||
| 123 | + | assert.Contains(t, css, ".sidebar-search {\n\tmin-height: 0;\n\tpadding: 0.0625em 0.375em;") | |||
| 124 | + | } | |||
| 125 | + | ||||
| 119 | func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) { | 126 | func TestGenerateSidebarCSS_ContainsTransition(t *testing.T) { | ||
| 120 | css := generateSidebarCSS(DefaultDarkPalette()) | 127 | css := generateSidebarCSS(DefaultDarkPalette()) | ||
| 121 | 128 | ||||
M internal/ui/theme/sidebar_css.go +3 -2
| @@ -12,13 +12,14 @@ func generateSidebarCSS(_ Palette) string { | |||
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | .sidebar-search-box { | |
| 15 | - | padding: 0.375em 0.5em; | |
| 15 | + | padding: 0.5em 0.5em 0.375em; | |
| 16 | 16 | border-bottom: 0.0625em solid var(--border); | |
| 17 | 17 | background-color: var(--surface); | |
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | 20 | .sidebar-search { | |
| 21 | - | padding: 0.125em 0.375em; | |
| 21 | + | min-height: 0; | |
| 22 | + | padding: 0.0625em 0.375em; | |
| 22 | 23 | font-size: 0.85em; | |
| 23 | 24 | } | |
| 24 | 25 | ||
| @@ -12,13 +12,14 @@ func generateSidebarCSS(_ Palette) string { | |||||
| 12 | } | 12 | } | ||
| 13 | 13 | ||||
| 14 | .sidebar-search-box { | 14 | .sidebar-search-box { | ||
| 15 | - | padding: 0.375em 0.5em; | 15 | + | padding: 0.5em 0.5em 0.375em; |
| 16 | border-bottom: 0.0625em solid var(--border); | 16 | border-bottom: 0.0625em solid var(--border); | ||
| 17 | background-color: var(--surface); | 17 | background-color: var(--surface); | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | .sidebar-search { | 20 | .sidebar-search { | ||
| 21 | - | padding: 0.125em 0.375em; | 21 | + | min-height: 0; |
| 22 | + | padding: 0.0625em 0.375em; | |||
| 22 | font-size: 0.85em; | 23 | font-size: 0.85em; | ||
| 23 | } | 24 | } | ||
| 24 | 25 | ||||