Skip to main content

reactor-input

Reactor input — gestures (`OnPan`, `OnPinch`, `OnRotate`), pointer events, drag-and-drop (`OnDragStarting`), focus management (`UseElementFocus`, `UseFocus`, `UseFocusTrap`). Use when implementing direct manipulation or custom focus behavior.

Source facts

Repository
microsoft/microsoft-ui-reactor
Last source activity
September 24, 2026 at 20:38
Detected SKILL.md language
English
Stars
663
Forks
57

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
reactor-input
description
Reactor input — gestures (`OnPan`, `OnPinch`, `OnRotate`), pointer events, drag-and-drop (`OnDragStarting`), focus management (`UseElementFocus`, `UseFocus`, `UseFocusTrap`). Use when implementing direct manipulation or custom focus behavior.
# Input and Gestures in Reactor Reactor exposes input via **trampoline-dispatched `.On*` modifiers**. Events auto-enable their underlying WinUI flags (`ManipulationMode`, `AllowDrop`, etc.) when you attach a handler, so you never need to set those manually. > **Controlled prop note:** input factories keep the ergonomic > `(value, setter)` shape. The underlying controlled element properties are > `Optional<T>` so `Unset` can mean "control-owned"; direct reads need > `.Value` or `.GetValueOrDefault(...)`. See > [`migration/050-optional-t.md`](../../../../docs/guide/migration/050-optional-t.md). ## Quick reference | API | Purpose | |-----|---------| | `.OnPointerEntered/Exited/Pressed/Released/Moved` | Pointer events | | `.IsHitTestVisible(bool)` | Opt an element + its subtree out of hit-testing | | `.OnTapped()` / `.OnDoubleTapped()` / `.OnRightTapped()` | Tap events | | `.OnKeyDown()` / `.OnKeyUp()` | Keyboard events | | `.OnPan(...)` | Pan gesture (drag with inertia) | | `.OnPinch(...)` | Pinch-to-zoom gesture | | `.OnRotate(...)` | Rotation gesture | | `.OnLongPress(...)` | Press-and-hold | | `.OnGotFocus()` / `.OnLostFocus()` | Focus change events | | `UseElementFocus()` | Untyped ref + dispatcher-scheduled focus | | `UseElementRef<T>()` | Typed element ref — `.Current` is `T` (no cast) | | `.XYFocusUp/Down/Left/Right(ref)` | Directional focus reference props | | `.AccessKey("S")` | Alt+key keyboard shortcut | | `.OnDragStart(...)` / `.OnDrop(...)` | Drag-and-drop | ## 1. Pointer events <!-- index:pointer-input --> Pointer handlers attach like any other modifier; Reactor installs one stable trampoline per element, so re-rendering with a fresh closure costs no re-subscription. ```csharp var (isHovered, setIsHovered) = UseState(false); return Border(child) .OnPointerEntered((s, e) => setIsHovered(true)) .OnPointerExited((s, e) => setIsHovered(false)) .Background(isHovered ? Theme.SubtleFill : Colors.Transparent); ``` Events: `OnPointerEntered`, `OnPointerExited`, `OnPointerPressed`, `OnPointerReleased`, `OnPointerMoved`, `OnPointerCanceled`. The `(s, e)` signature gives you the sender and `PointerRoutedEventArgs`. ### Opting out of hit-testing `.IsHitTestVisible(false)` sets `UIElement.IsHitTestVisible` — the element **and its subtree** stop being pointer targets, so decorative or overlay layers let pointer events pass through to whatever is underneath. ```csharp Border(overlayGlow).IsHitTestVisible(false) ``` Unlike `.IsEnabled(false)`, it does not grey the control out or block focus — reach for `.IsEnabled(false)` when you want a disabled affordance, and `.IsHitTestVisible(false)` when you only want clicks to fall through. ## 2. Tap events `Border` and other non-control elements are not tab stops by default: a `Border` with `.OnTapped(...)` answers the mouse but is skipped by the keyboard. Add `.IsTabStop(true)` and an `.OnKeyDown` Enter/Space handler, or use a `Button`, whenever the gesture is a real command. ```csharp Border(child) .OnTapped((s, e) => HandleClick()) .OnDoubleTapped((s, e) => HandleDoubleClick()) .OnRightTapped((s, e) => ShowContextMenu()) ``` <!-- /index:pointer-input --> ## 3. Keyboard events <!-- index:keyboard-input --> Attach `.OnKeyDown` / `.OnKeyUp` to any element. The handler receives `(sender, KeyRoutedEventArgs)`; set `e.Handled = true` to stop the event bubbling. `.OnPreviewKeyDown` / `.OnPreviewKeyUp` run first, during the tunnelling phase, and can swallow a key before the focused control sees it. ```csharp TextBox(text, setText) .OnKeyDown((s, e) => { if (e.Key == VirtualKey.Enter) { Submit(); e.Handled = true; } }) ``` **`e.Key` is `Windows.System.VirtualKey` — WinRT, not WPF.** It is not `System.Windows.Input.Key`, and the member names differ: `VirtualKey` has **no** `OemPeriod`, `OemPlus`, `OemMinus` or `Equal`. Punctuation and OEM keys simply have no named member. Two ways to handle them: ```csharp // 1. Compare the numeric virtual-key code for OEM keys. .OnKeyDown((s, e) => { if ((int)e.Key == 190) AppendDecimalPoint(); // VK_OEM_PERIOD '.' if ((int)e.Key == 187) Evaluate(); // VK_OEM_PLUS '=' }) // 2. Better for text: let WinUI resolve the character for you. .OnCharacterReceived((s, e) => { if (e.Character == '.') AppendDecimalPoint(); if (e.Character == '=') Evaluate(); }) ``` Named members you *can* rely on are the letters (`VirtualKey.A`), digits (`VirtualKey.Number0`, `VirtualKey.NumberPad0`), the numpad operators (`Add`, `Subtract`, `Multiply`, `Divide`, `Decimal`), and the editing and navigation keys (`Enter`, `Space`, `Back`, `Escape`, `Tab`, `Delete`, `Left`, `Right`, `Up`, `Down`). **Ctrl/Alt chords do not belong on `.OnKeyDown`.** A handler on a `TextBox` only fires while that field has focus, so an app-wide shortcut written that way silently does nothing elsewhere. Use a `Command` with an `Accelerator`, which registers with WinUI's accelerator infrastructure. The `REACTOR_INPUT_001` analyzer flags the mistake. ```csharp new Command { Label = "Save", Execute = Save, Accelerator = Accelerator(VirtualKey.S, VirtualKeyModifiers.Control), } ``` Mind the **scope**, though: an accelerator fires when the surface carrying it is in the focused element's ancestor chain, so `Button(save)` on its own covers the button, not the window. `CommandHost([save], subtree)` widens it to a whole subtree, and a `MenuBar` or `CommandBar` at the window root is what makes a chord genuinely window-wide — those attach to the window's `KeyboardAccelerators` collection. See [commanding](../../../../docs/guide/commanding.md). <!-- /index:keyboard-input --> ## 4. Continuous gestures (Pan, Pinch, Rotate) <!-- index:pointer-input --> Gestures follow a **phase lifecycle**: `Began → Changed (repeat) → Ended | Cancelled`. Attaching a gesture handler auto-enables the underlying `ManipulationMode`, so you never set that yourself. ### Pan gesture ```csharp var (offset, setOffset) = UseState(new Point(0, 0)); return Border(child) .OnPan( minimumDistance: 10, // pixels before gesture starts axis: PanAxis.Both, // or Horizontal, Vertical withInertia: true, // momentum after release onBegan: (e) => { /* pan began */ }, onChanged: (e) => { // PanGesture.Delta is the per-event movement, a Point — Reactor flattens // WinUI's ManipulationDelta, so there is no e.Delta.Translation here. setOffset(new Point(offset.X + e.Delta.X, offset.Y + e.Delta.Y)); }, onEnded: (e) => { /* pan ended */ }) .Translation((float)offset.X, (float)offset.Y, 0); ``` ### 60Hz pan pattern (performance-critical) For smooth dragging, write `Translation` directly via ref during the gesture and only `setState` at the end. The cell must be an **`ElementRef`** from `UseElementRef<T>` — that is what the reconciler populates. A `UseRef` box is not accepted by `.Ref(...)` and would never point at anything. ```csharp var itemRef = this.UseElementRef<FrameworkElement>(); var (finalPos, setFinalPos) = UseState(new Point(0, 0)); return Border(child) .Ref(itemRef) .OnPan( onChanged: (e) => { // Direct property write — no re-render per frame if (itemRef.Current is { } el) { var t = el.Translation; el.Translation = new System.Numerics.Vector3( t.X + (float)e.Delta.X, t.Y + (float)e.Delta.Y, t.Z); } }, onEnded: (e) => { // Sync state once at end if (itemRef.Current is { } el) setFinalPos(new Point(el.Translation.X, el.Translation.Y)); }); ``` ### Pinch and Rotate Both gestures carry an absolute value and a per-event delta as flat doubles — `Scale`/`ScaleDelta` and `Angle`/`AngleDelta`. There is no nested `e.Delta` object on these two. ```csharp var (scale, setScale) = UseState(1.0f); return Border(child) .OnPinch( onChanged: (e) => setScale(scale * (float)e.ScaleDelta), onEnded: (e) => { }) .Scale(scale); // Rotation: var (angle, setAngle) = UseState(0f); return Border(child) .OnRotate( onChanged: (e) => setAngle(angle + (float)e.AngleDelta), onEnded: (e) => { }) .Rotation(angle); ``` ## 5. Long press Touch/pen has built-in long press; mouse requires emulation: ```csharp Border(child) .OnLongPress( onTriggered: (e) => ShowEditMode(), enableMouseEmulation: true) // also trigger on mouse press-and-hold ``` <!-- /index:pointer-input --> ## 6. Focus management <!-- index:element-refs --> An `ElementRef` is the bridge from the declarative tree to a realized WinUI element. Attach it with `.Ref(...)`, then read `.Current` — it is `null` until the element mounts, survives re-renders, and clears on unmount, so the same cell reliably points at whatever is mounted now. ### UseElementFocus ```csharp var focusRef = this.UseElementFocus(); return VStack(12, TextBox(text, setText).Ref(focusRef.Ref), Button("Focus the field", () => focusRef.RequestFocus()) ); ``` `UseElementFocus` returns a handle with `.Ref` (attach to the element) and `.RequestFocus()` (imperatively focus it). It is a named tuple, so the deconstructed form is equivalent and is what the guide uses — `RequestFocus` schedules the focus on the UI dispatcher, so it is safe to call from an effect that runs before the element has mounted: ```csharp var (inputRef, requestFocus) = this.UseElementFocus(); UseEffect(() => requestFocus(), Array.Empty<object>()); // focus on mount return TextBox(name, setName).AutomationName("Name").Ref(inputRef); ``` For a synchronous attempt, or one whose success you need to observe, call `Microsoft.UI.Reactor.Input.FocusManager.Focus(inputRef)` or `FocusManager.FocusAsync(inputRef)` instead. ### UseElementRef\<T\> — typed refs When you actually need to call methods on the underlying control (`SelectAll()` on a `TextBox`, `Focus(FocusState.Programmatic)`, etc.), use the typed variant. `.Current` is already typed as `T` — no cast at the call site: ```csharp var inputRef = this.UseElementRef<TextBox>(); UseEffect(() => inputRef.Current?.SelectAll(), Array.Empty<object>()); return TextBox(query, setQuery).Ref(inputRef); ``` The constraint is `T : FrameworkElement`. In `DEBUG` builds Reactor asserts the actual mounted element is a `T`; in release the mismatch is silent and `.Current` returns `null`. Spec 033 §3. ### XYFocus reference props Use XYFocus refs for gamepad/keyboard directional navigation graphs. The target can mount before or after the referrer; Reactor rewrites the WinUI `UIElement.XYFocus*` property when the referenced element becomes available and clears it when that element unmounts. ```csharp var left = this.UseElementRef<FrameworkElement>(); var right = this.UseElementRef<FrameworkElement>(); return HStack(8, Button("Left").Ref(left).XYFocusRight(right), Button("Right").Ref(right).XYFocusLeft(left)); ``` For cyclic focus rings, declare both directions. Do not assign `button.XYFocusRight = right.Current` in a handler; that snapshot will not survive late mount or recreation. The same rule covers every reference prop — `TeachingTip(..., target: ref)`, `.LabeledBy(ref)`, `.DescribedBy(refs)`, `.FlowsTo(refs)` — and `REACTOR_REF_001` flags reading `.Current` to set one. <!-- /index:element-refs --> ### Focus events ```csharp TextBox(text, setText)
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub