feat(workspace): route platform Back gestures to the header Back (#1943)

* feat(workspace): route platform Back gestures to the header Back

The mouse Back button, Alt+Left (Cmd+[ on macOS) and, in Electron, the
window's app-command browser-backward (Windows/Linux) and trackpad swipe
(macOS) now run the workspace header's Back, one step each.

- WorkspaceBackGesturesService (shell-provided) dispatches every gesture
  through WorkspaceBackNavigationService.goBack(). A gesture is swallowed
  while a dialog, menu, the phone drawer or a fullscreen player is up, and
  left to the browser only where the header has no Back.
- The chord is a navigate-back entry in the shortcuts dialog, handled in a
  capture-phase listener of WorkspaceKeyboardShortcutsService; ignored while
  typing, repeats swallowed.
- Electron forwards app-command and swipe over the push-only channel
  WINDOW:BACK_REQUESTED (preload onWindowBackRequested), gated in the
  renderer on RuntimeCapabilitiesService.supportsWindowBackGestures.
- A mouse press that also arrives as app-command goes back once, however
  long the button is held; Electron's own Chromium history step on the
  mouse button is always prevented.
- No Forward binding.

Docs: workspace-shell (Platform gestures), portal-detail-navigation,
electron-security, UI guidelines, README shortcuts table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(workspace): drop NgZone from the Back gestures service

Master runs zoneless change detection and no longer uses NgZone; the
bridge callback calls goBack() directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(workspace): keep Back gestures where the Fullscreen API is missing

iPhone Safari has no unprefixed document.fullscreenElement; the
undefined value read as "a player is fullscreen" and blocked every
gesture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(workspace): keep a held Back chord's repeats swallowed after Back

Repeats asked for the current result, so once the first press reached a
page with no Back left they were released to the browser, which could take
another history step. Repeats now follow the chord's first press until a
key is released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(electron): one Back per press of the Linux Browser Back key

On Linux Electron raises app-command browser-backward for every
unhandled event of the Browser Back key (NativeWindowViews::
HandleKeyboardEvent), keydown and keyup alike, so one press went back
twice. The main process now consumes that key in before-input-event and
requests Back once per press; mouse Back buttons still arrive as
app-command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-15 22:15:42 +02:00
1 parent 002faebb08
commit aecb9acb8e
44 files changed
+1758 -8

No files matched your search

@@ -224,6 +224,13 @@ export interface ElectronBridgeWindowState {
isFullScreen: boolean;
}
/**
* The window event behind a Back request: `app-command` (`browser-backward`,
* Windows and Linux: mouse Back buttons and the keyboard Browser Back key)
* or `swipe` (macOS trackpad swipe).
*/
export type ElectronBridgeBackGestureSource = 'app-command' | 'swipe';
/**
* Portal credentials for an auth-gated stream, passed alongside the scoped
* header override. The main process attaches them only to requests going to
@@ -779,6 +786,14 @@ export interface ElectronBridgeApi {
onWindowStateChange: (
callback: (state: ElectronBridgeWindowState) => void
) => () => void;
/**
* Platform Back gestures the window receives, forwarded from the main
* process. Subscribe-only: the renderer runs the workspace Back (or
* ignores the request) and sends nothing back. Returns the unsubscribe.
*/
onWindowBackRequested: (
callback: (source: ElectronBridgeBackGestureSource) => void
) => () => void;
/**
* While active, the main process intercepts window close / app quit and
* pushes a close request to the renderer instead of closing. Used by the
@@ -140,6 +140,11 @@ export const WINDOW_TOGGLE_FULLSCREEN = 'WINDOW:TOGGLE_FULLSCREEN';
export const WINDOW_CLOSE = 'WINDOW:CLOSE';
export const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
export const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';
// Main → renderer only: the window received a platform Back gesture (Windows
// and Linux `app-command` `browser-backward`, macOS trackpad `swipe` left),
// carrying an `ElectronBridgeBackGestureSource`. The renderer decides
// whether Back runs; nothing flows the other way.
export const WINDOW_BACK_REQUESTED = 'WINDOW:BACK_REQUESTED';
// Synchronous preload request for the persisted app zoom level (answered
// with `event.returnValue`), and the preload's acknowledgement once it has
// applied that level with webFrame.setZoomLevel at DOMContentLoaded