feat(ui): enable webview accessibility on vim mode entry
- commit
- d3c8f2e0967da7ca3889bb99a5730fd69e37ac22
- author
- brice · · 2026-08-14 07:44 UTC
- committer
- brice · · 2026-08-14 09:38 UTC
- parent
- 2affa97
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +106 -0
M internal/ui/app.go +24 -0
| @@ -2160,6 +2160,26 @@ func (a *App) activeWebViewForBrowserWindow(bw *browserWindow) (entity.PaneID, p | |||
| 2160 | 2160 | return paneID, a.contentCoord.GetWebView(paneID) | |
| 2161 | 2161 | } | |
| 2162 | 2162 | ||
| 2163 | + | func (a *App) enableAccessibilityForVimMode(ctx context.Context, bw *browserWindow) { | |
| 2164 | + | paneID, wv := a.activeWebViewForBrowserWindow(bw) | |
| 2165 | + | if wv == nil { | |
| 2166 | + | return | |
| 2167 | + | } | |
| 2168 | + | enabler, ok := wv.(port.AccessibilityEnabler) | |
| 2169 | + | if !ok { | |
| 2170 | + | return | |
| 2171 | + | } | |
| 2172 | + | enabler.EnableAccessibility() | |
| 2173 | + | windowID := "" | |
| 2174 | + | if bw != nil { | |
| 2175 | + | windowID = bw.id | |
| 2176 | + | } | |
| 2177 | + | logging.FromContext(ctx).Debug(). | |
| 2178 | + | Str("window_id", windowID). | |
| 2179 | + | Str("pane_id", string(paneID)). | |
| 2180 | + | Msg("webview accessibility enabled for vim mode") | |
| 2181 | + | } | |
| 2182 | + | ||
| 2163 | 2183 | // omniboxNavigateForBrowserWindow returns an omnibox OnNavigate callback that routes | |
| 2164 | 2184 | // navigation through navigate for the supplied browser window. The closure captures | |
| 2165 | 2185 | // bw at creation time, so navigation targets the owning window even when another | |
| @@ -3861,6 +3881,10 @@ func (a *App) handleModeChange(ctx context.Context, bw *browserWindow, from, to | |||
| 3861 | 3881 | a.applyResizeModeBorder(ctx, a.activeWorkspace()) | |
| 3862 | 3882 | } | |
| 3863 | 3883 | ||
| 3884 | + | if to == input.ModeVim && from != input.ModeVim { | |
| 3885 | + | a.enableAccessibilityForVimMode(ctx, bw) | |
| 3886 | + | } | |
| 3887 | + | ||
| 3864 | 3888 | // Handle pane-local Vim Mode visual ownership. | |
| 3865 | 3889 | // Entering Vim Mode accents the active pane; leaving removes the accent. | |
| 3866 | 3890 | a.handleVimModeOwnership(ctx, bw, to, from) | |
| @@ -2160,6 +2160,26 @@ func (a *App) activeWebViewForBrowserWindow(bw *browserWindow) (entity.PaneID, p | |||||
| 2160 | return paneID, a.contentCoord.GetWebView(paneID) | 2160 | return paneID, a.contentCoord.GetWebView(paneID) | ||
| 2161 | } | 2161 | } | ||
| 2162 | 2162 | ||||
| 2163 | + | func (a *App) enableAccessibilityForVimMode(ctx context.Context, bw *browserWindow) { | |||
| 2164 | + | paneID, wv := a.activeWebViewForBrowserWindow(bw) | |||
| 2165 | + | if wv == nil { | |||
| 2166 | + | return | |||
| 2167 | + | } | |||
| 2168 | + | enabler, ok := wv.(port.AccessibilityEnabler) | |||
| 2169 | + | if !ok { | |||
| 2170 | + | return | |||
| 2171 | + | } | |||
| 2172 | + | enabler.EnableAccessibility() | |||
| 2173 | + | windowID := "" | |||
| 2174 | + | if bw != nil { | |||
| 2175 | + | windowID = bw.id | |||
| 2176 | + | } | |||
| 2177 | + | logging.FromContext(ctx).Debug(). | |||
| 2178 | + | Str("window_id", windowID). | |||
| 2179 | + | Str("pane_id", string(paneID)). | |||
| 2180 | + | Msg("webview accessibility enabled for vim mode") | |||
| 2181 | + | } | |||
| 2182 | + | ||||
| 2163 | // omniboxNavigateForBrowserWindow returns an omnibox OnNavigate callback that routes | 2183 | // omniboxNavigateForBrowserWindow returns an omnibox OnNavigate callback that routes | ||
| 2164 | // navigation through navigate for the supplied browser window. The closure captures | 2184 | // navigation through navigate for the supplied browser window. The closure captures | ||
| 2165 | // bw at creation time, so navigation targets the owning window even when another | 2185 | // bw at creation time, so navigation targets the owning window even when another | ||
| @@ -3861,6 +3881,10 @@ func (a *App) handleModeChange(ctx context.Context, bw *browserWindow, from, to | |||||
| 3861 | a.applyResizeModeBorder(ctx, a.activeWorkspace()) | 3881 | a.applyResizeModeBorder(ctx, a.activeWorkspace()) | ||
| 3862 | } | 3882 | } | ||
| 3863 | 3883 | ||||
| 3884 | + | if to == input.ModeVim && from != input.ModeVim { | |||
| 3885 | + | a.enableAccessibilityForVimMode(ctx, bw) | |||
| 3886 | + | } | |||
| 3887 | + | ||||
| 3864 | // Handle pane-local Vim Mode visual ownership. | 3888 | // Handle pane-local Vim Mode visual ownership. | ||
| 3865 | // Entering Vim Mode accents the active pane; leaving removes the accent. | 3889 | // Entering Vim Mode accents the active pane; leaving removes the accent. | ||
| 3866 | a.handleVimModeOwnership(ctx, bw, to, from) | 3890 | a.handleVimModeOwnership(ctx, bw, to, from) | ||
M internal/ui/app_vim_mode_test.go +82 -0
| @@ -10,9 +10,12 @@ import ( | |||
| 10 | 10 | "github.com/stretchr/testify/mock" | |
| 11 | 11 | "github.com/stretchr/testify/require" | |
| 12 | 12 | ||
| 13 | + | "github.com/bnema/dumber/internal/application/port" | |
| 14 | + | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 13 | 15 | "github.com/bnema/dumber/internal/application/usecase" | |
| 14 | 16 | "github.com/bnema/dumber/internal/domain/entity" | |
| 15 | 17 | "github.com/bnema/dumber/internal/ui/component" | |
| 18 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |
| 16 | 19 | "github.com/bnema/dumber/internal/ui/focus" | |
| 17 | 20 | "github.com/bnema/dumber/internal/ui/input" | |
| 18 | 21 | "github.com/bnema/dumber/internal/ui/layout" | |
| @@ -154,6 +157,43 @@ func expectVimModeAccentHidden(overlay *mocks.MockOverlayWidget) { | |||
| 154 | 157 | overlay.EXPECT().RemoveCssClass("vim-mode-active").Once() | |
| 155 | 158 | } | |
| 156 | 159 | ||
| 160 | + | type accessibilityEnablingWebView struct { | |
| 161 | + | *portmocks.MockWebView | |
| 162 | + | enableAccessibilityCalls int | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | func newAccessibilityEnablingWebView(t *testing.T, id port.WebViewID) *accessibilityEnablingWebView { | |
| 166 | + | t.Helper() | |
| 167 | + | wv := portmocks.NewMockWebView(t) | |
| 168 | + | wv.EXPECT().ID().Return(id).Once() | |
| 169 | + | return &accessibilityEnablingWebView{MockWebView: wv} | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func (wv *accessibilityEnablingWebView) EnableAccessibility() { | |
| 173 | + | wv.enableAccessibilityCalls++ | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | func newVimModeAccessibilityFixture(t *testing.T) (*App, *browserWindow, entity.PaneID) { | |
| 177 | + | t.Helper() | |
| 178 | + | paneID := entity.PaneID("pane-a") | |
| 179 | + | ws := &entity.Workspace{ | |
| 180 | + | ID: "ws-1", | |
| 181 | + | ActivePaneID: paneID, | |
| 182 | + | Root: &entity.PaneNode{ | |
| 183 | + | ID: "ws-1-root", | |
| 184 | + | Pane: entity.NewPane(paneID), | |
| 185 | + | }, | |
| 186 | + | } | |
| 187 | + | tab := &entity.Tab{ID: "tab-1", Workspace: ws} | |
| 188 | + | tabs := entity.NewTabList() | |
| 189 | + | tabs.Add(tab) | |
| 190 | + | tabs.SetActive(tab.ID) | |
| 191 | + | bw := &browserWindow{id: "win-1", tabs: tabs} | |
| 192 | + | return &App{ | |
| 193 | + | contentCoord: contentcoord.NewCoordinator(context.Background(), nil, nil, nil, nil, nil, nil, nil), | |
| 194 | + | }, bw, paneID | |
| 195 | + | } | |
| 196 | + | ||
| 157 | 197 | // setupWorkspaceViewMocks creates a mock-backed WorkspaceView. | |
| 158 | 198 | func setupWorkspaceViewMocks( | |
| 159 | 199 | t *testing.T, | |
| @@ -351,6 +391,48 @@ func TestVimMode_Enter_MarksCorrectWindow(t *testing.T) { | |||
| 351 | 391 | assert.False(t, f.pv1A.IsVimMode()) | |
| 352 | 392 | } | |
| 353 | 393 | ||
| 394 | + | func TestVimMode_Enter_EnablesAccessibilityForSupportedActiveWebView(t *testing.T) { | |
| 395 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |
| 396 | + | wv := newAccessibilityEnablingWebView(t, 1) | |
| 397 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |
| 398 | + | ||
| 399 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |
| 400 | + | ||
| 401 | + | assert.Equal(t, 1, wv.enableAccessibilityCalls) | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | func TestVimMode_Enter_AccessibilityNoopsForUnsupportedActiveWebView(t *testing.T) { | |
| 405 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |
| 406 | + | wv := portmocks.NewMockWebView(t) | |
| 407 | + | wv.EXPECT().ID().Return(port.WebViewID(2)).Once() | |
| 408 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |
| 409 | + | ||
| 410 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | func TestVimMode_Enter_AccessibilityNoopsWhenActiveWebViewMissing(t *testing.T) { | |
| 414 | + | app, bw, _ := newVimModeAccessibilityFixture(t) | |
| 415 | + | ||
| 416 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | func TestVimMode_AccessibilityOnlyEnabledOnEnteringVimMode(t *testing.T) { | |
| 420 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |
| 421 | + | wv := newAccessibilityEnablingWebView(t, 3) | |
| 422 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |
| 423 | + | ||
| 424 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeTab) | |
| 425 | + | app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeNormal) | |
| 426 | + | app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeVim) | |
| 427 | + | assert.Equal(t, 0, wv.enableAccessibilityCalls) | |
| 428 | + | ||
| 429 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |
| 430 | + | assert.Equal(t, 1, wv.enableAccessibilityCalls) | |
| 431 | + | ||
| 432 | + | app.handleModeChange(context.Background(), bw, input.ModePane, input.ModeVim) | |
| 433 | + | assert.Equal(t, 2, wv.enableAccessibilityCalls) | |
| 434 | + | } | |
| 435 | + | ||
| 354 | 436 | // ============================================================================ | |
| 355 | 437 | // 2. Leaving Vim mode clears ownership | |
| 356 | 438 | // ============================================================================ | |
| @@ -10,9 +10,12 @@ import ( | |||||
| 10 | "github.com/stretchr/testify/mock" | 10 | "github.com/stretchr/testify/mock" | ||
| 11 | "github.com/stretchr/testify/require" | 11 | "github.com/stretchr/testify/require" | ||
| 12 | 12 | ||||
| 13 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 14 | + | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 13 | "github.com/bnema/dumber/internal/application/usecase" | 15 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 14 | "github.com/bnema/dumber/internal/domain/entity" | 16 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 15 | "github.com/bnema/dumber/internal/ui/component" | 17 | "github.com/bnema/dumber/internal/ui/component" | ||
| 18 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |||
| 16 | "github.com/bnema/dumber/internal/ui/focus" | 19 | "github.com/bnema/dumber/internal/ui/focus" | ||
| 17 | "github.com/bnema/dumber/internal/ui/input" | 20 | "github.com/bnema/dumber/internal/ui/input" | ||
| 18 | "github.com/bnema/dumber/internal/ui/layout" | 21 | "github.com/bnema/dumber/internal/ui/layout" | ||
| @@ -154,6 +157,43 @@ func expectVimModeAccentHidden(overlay *mocks.MockOverlayWidget) { | |||||
| 154 | overlay.EXPECT().RemoveCssClass("vim-mode-active").Once() | 157 | overlay.EXPECT().RemoveCssClass("vim-mode-active").Once() | ||
| 155 | } | 158 | } | ||
| 156 | 159 | ||||
| 160 | + | type accessibilityEnablingWebView struct { | |||
| 161 | + | *portmocks.MockWebView | |||
| 162 | + | enableAccessibilityCalls int | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | func newAccessibilityEnablingWebView(t *testing.T, id port.WebViewID) *accessibilityEnablingWebView { | |||
| 166 | + | t.Helper() | |||
| 167 | + | wv := portmocks.NewMockWebView(t) | |||
| 168 | + | wv.EXPECT().ID().Return(id).Once() | |||
| 169 | + | return &accessibilityEnablingWebView{MockWebView: wv} | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func (wv *accessibilityEnablingWebView) EnableAccessibility() { | |||
| 173 | + | wv.enableAccessibilityCalls++ | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | func newVimModeAccessibilityFixture(t *testing.T) (*App, *browserWindow, entity.PaneID) { | |||
| 177 | + | t.Helper() | |||
| 178 | + | paneID := entity.PaneID("pane-a") | |||
| 179 | + | ws := &entity.Workspace{ | |||
| 180 | + | ID: "ws-1", | |||
| 181 | + | ActivePaneID: paneID, | |||
| 182 | + | Root: &entity.PaneNode{ | |||
| 183 | + | ID: "ws-1-root", | |||
| 184 | + | Pane: entity.NewPane(paneID), | |||
| 185 | + | }, | |||
| 186 | + | } | |||
| 187 | + | tab := &entity.Tab{ID: "tab-1", Workspace: ws} | |||
| 188 | + | tabs := entity.NewTabList() | |||
| 189 | + | tabs.Add(tab) | |||
| 190 | + | tabs.SetActive(tab.ID) | |||
| 191 | + | bw := &browserWindow{id: "win-1", tabs: tabs} | |||
| 192 | + | return &App{ | |||
| 193 | + | contentCoord: contentcoord.NewCoordinator(context.Background(), nil, nil, nil, nil, nil, nil, nil), | |||
| 194 | + | }, bw, paneID | |||
| 195 | + | } | |||
| 196 | + | ||||
| 157 | // setupWorkspaceViewMocks creates a mock-backed WorkspaceView. | 197 | // setupWorkspaceViewMocks creates a mock-backed WorkspaceView. | ||
| 158 | func setupWorkspaceViewMocks( | 198 | func setupWorkspaceViewMocks( | ||
| 159 | t *testing.T, | 199 | t *testing.T, | ||
| @@ -351,6 +391,48 @@ func TestVimMode_Enter_MarksCorrectWindow(t *testing.T) { | |||||
| 351 | assert.False(t, f.pv1A.IsVimMode()) | 391 | assert.False(t, f.pv1A.IsVimMode()) | ||
| 352 | } | 392 | } | ||
| 353 | 393 | ||||
| 394 | + | func TestVimMode_Enter_EnablesAccessibilityForSupportedActiveWebView(t *testing.T) { | |||
| 395 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |||
| 396 | + | wv := newAccessibilityEnablingWebView(t, 1) | |||
| 397 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |||
| 398 | + | ||||
| 399 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |||
| 400 | + | ||||
| 401 | + | assert.Equal(t, 1, wv.enableAccessibilityCalls) | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | func TestVimMode_Enter_AccessibilityNoopsForUnsupportedActiveWebView(t *testing.T) { | |||
| 405 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |||
| 406 | + | wv := portmocks.NewMockWebView(t) | |||
| 407 | + | wv.EXPECT().ID().Return(port.WebViewID(2)).Once() | |||
| 408 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |||
| 409 | + | ||||
| 410 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |||
| 411 | + | } | |||
| 412 | + | ||||
| 413 | + | func TestVimMode_Enter_AccessibilityNoopsWhenActiveWebViewMissing(t *testing.T) { | |||
| 414 | + | app, bw, _ := newVimModeAccessibilityFixture(t) | |||
| 415 | + | ||||
| 416 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | func TestVimMode_AccessibilityOnlyEnabledOnEnteringVimMode(t *testing.T) { | |||
| 420 | + | app, bw, paneID := newVimModeAccessibilityFixture(t) | |||
| 421 | + | wv := newAccessibilityEnablingWebView(t, 3) | |||
| 422 | + | app.contentCoord.RegisterPopupWebView(paneID, wv) | |||
| 423 | + | ||||
| 424 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeTab) | |||
| 425 | + | app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeNormal) | |||
| 426 | + | app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeVim) | |||
| 427 | + | assert.Equal(t, 0, wv.enableAccessibilityCalls) | |||
| 428 | + | ||||
| 429 | + | app.handleModeChange(context.Background(), bw, input.ModeNormal, input.ModeVim) | |||
| 430 | + | assert.Equal(t, 1, wv.enableAccessibilityCalls) | |||
| 431 | + | ||||
| 432 | + | app.handleModeChange(context.Background(), bw, input.ModePane, input.ModeVim) | |||
| 433 | + | assert.Equal(t, 2, wv.enableAccessibilityCalls) | |||
| 434 | + | } | |||
| 435 | + | ||||
| 354 | // ============================================================================ | 436 | // ============================================================================ | ||
| 355 | // 2. Leaving Vim mode clears ownership | 437 | // 2. Leaving Vim mode clears ownership | ||
| 356 | // ============================================================================ | 438 | // ============================================================================ | ||