Skip to content

RFC: mouseRelay capability — relay mouse back/forward buttons from app iframes to the host #3077

Description

@YuanboXue-Amber

Summary

A new TeamsJS capability, mouseRelay, lets an app inside a Teams iframe send mouse back (X1) / forward (X2) presses to the host for Teams history navigation — the mouse analog of shortcutRelay.

Motivation

Mouse back/forward (X1/X2) already works on the web client: the browser handles the buttons as a user-agent action over the tab's joint session history, so they navigate correctly even when focus is inside an app iframe. Web behavior is not changing.

The goal is to make the desktop client (WebView2) behave the same way. Today it doesn't: when focus is inside an app iframe, the X1/X2 event never reaches the host, so it can't drive Teams history navigation. mouseRelay lets the iframe send the button to the host, bringing desktop to parity with web.

Proposed public API

New module src/public/mouseRelay.ts (used as import * as mouseRelay from '@microsoft/teams-js'):

export type NavigationDirection = "back" | "forward";

export function isSupported(): boolean;
export function enableMouseRelayCapability(): Promise<void>;
export function resetIsMouseRelayCapabilityEnabled(): void;

Behavior (iframe)

enableMouseRelayCapability() attaches 3 capture-phase listeners on document.

  • It posts a message on mouseup to match the browser's native release timing (so press-and-hold does nothing until release, exactly like web);
  • mousedown and auxclick exist only to cancel the iframe's own native X1/X2 navigation and the synthetic click.
// 3 = back (X1), 4 = forward (X2); every other button is ignored.
const directionFor = (b: number) =>
  b === 3 ? "back" : b === 4 ? "forward" : undefined;

function enableMouseRelayCapability(): Promise<void> {
  if (!isSupported()) throw errorNotSupportedOnPlatform;
  document.addEventListener("mousedown", suppressX1X2, { capture: true }); // suppress (early)
  document.addEventListener("mouseup", onMouseUp, { capture: true }); // postMessage on release
  document.addEventListener("auxclick", suppressX1X2, { capture: true }); // suppress synthetic click
}

function onMouseUp(e: MouseEvent): void {
  const direction = directionFor(e.button);
  if (!direction) {
    return; // not X1/X2 → do nothing
  }

  e.preventDefault();
  e.stopImmediatePropagation(); // cancel the iframe's own native nav on release

  callFunctionInHost("mouseRelay.navigateHistory", [{ direction }]); // postMessage once, on release
}

function suppressX1X2(e: MouseEvent): void {
  if (directionFor(e.button) !== undefined) {
    e.preventDefault();
    e.stopImmediatePropagation(); // suppress only 
  }
}

Call sequence for one physical X1/X2 press (Chromium dispatch order):

pointerdown → mousedown → pointerup → mouseup → auxclick
                  │                      │          │
              suppress              postMessage      suppress
                  │                 {direction}     │
            cancel native         1 msg to host  cancel native + click

One mouseRelay.navigateHistory message per press, fired on mouse release to match how the browser navigates on web; the iframe performs no navigation of its own.

Host responsibilities

The host implements a service that maps the received direction onto its existing history navigation.

Teams knows the platform type, so it advertises this capability (runtime.supports.mouseRelay) on the desktop client only, keeping the browser's native back/forward behavior untouched on web. Apps therefore see isSupported() as true only on desktop.

The iframe is agnostic to this decision: an app only calls isSupported() / enableMouseRelayCapability() and never reasons about platform. So if Teams later chooses to support the capability on web too (or with new behavior), it simply advertises it there — nothing more is required from iframes.

Why a new capability (not extending shortcutRelay)

  • Clean negotiation: a dedicated isSupported() distinguishes hosts that understand mouse from those that don't. Overloading shortcutRelay would need a version flag anyway.
  • Simpler: mouse needs no getHostShortcuts handshake.

Implementation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions