<< All versions

Skill v1.0.1

currentTrusted Publisher100/100
microsoft/microsoft-ui-reactor/reactor-input

Build(deps): bump js-yaml in /tests/startup_perf/BlankRNW (#1200)

──Details
PublishedSeptember 11, 2026 at 08:22 AM
Content Hashsha256:f9646372a485a884...
Git SHA72b8fc85f8c3
Compare with v1.0.0
──Files
Files (1 file, 15.0 KB)
SKILL.md15.0 KBactive
SKILL.md · 451 lines · 15.0 KB

version: "1.0.1" 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

Quick reference

APIPurpose
.OnPointerEntered/Exited/Pressed/Released/MovedPointer 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. <!-- /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)
.OnGotFocus((s, e) => ShowSuggestions())
.OnLostFocus((s, e) => HideSuggestions())

Access keys (keyboard shortcuts)

csharp
Button("Save", onSave).AccessKey("S") // Alt+S

Tab order

csharp
TextBox(name, setName).IsTabStop(true).TabIndex(1)
TextBox(email, setEmail).IsTabStop(true).TabIndex(2)
Button("Submit", onSubmit).IsTabStop(true).TabIndex(3)

7. Drag and drop

Reactor's drag-drop system supports typed in-process payloads (via a transfer registry) and standard formats for cross-process drops.

Basic drag source + drop target

csharp
// Drag source — provide a DragData factory
Border(TextBlock(item.Name))
.OnDragStart(
getData: () => DragData.Typed(item),
allowedOperations: DragOperations.Copy | DragOperations.Move)
// Drop target — typed overload auto-extracts payload
Border(TextBlock("Drop here"))
.OnDrop<BorderElement, Item>(
onDrop: (droppedItem) => HandleDrop(droppedItem))

Typed in-process payloads

For type-safe object transfer within the same app, use the typed OnDragStart<T, TPayload> and OnDrop<T, TPayload> overloads:

csharp
// Drag source — typed payload via getPayload factory
Border(TextBlock(item.Name))
.OnDragStart<BorderElement, Item>(
getPayload: () => item,
allowedOperations: DragOperations.Move,
onEnd: (ctx) =>
{
if (ctx.CompletedOperation == DragOperations.Move && !ctx.WasCancelled)
RemoveItem(item); // only remove after confirmed drop
})
// Drop target — typed extraction
Border(TextBlock("Drop here"))
.OnDrop<BorderElement, Item>(
onDrop: (droppedItem) => MoveItem(droppedItem),
acceptedOps: DragOperations.Move)

Untyped drop target (raw DragTargetArgs)

For cross-process drops or custom handling:

csharp
Border(TextBlock("Drop here"))
.OnDragOver((args) =>
{
args.UIOverride.Caption = "Copy here";
args.UIOverride.IsCaptionVisible = true;
})
.OnDrop((args) =>
{
if (args.Data.TryGetTypedPayload<Item>(out var item))
HandleDrop(item);
})

DragUI customization

Via args.UIOverride on drag-over handlers:

csharp
.OnDragOver((args) =>
{
args.UIOverride.IsCaptionVisible = true;
args.UIOverride.IsContentVisible = true;
args.UIOverride.IsGlyphVisible = false;
args.UIOverride.Caption = "Move to folder";
})

Critical gotchas

  1. CANNOT combine `OnPan` and `OnTapped` on the same element. They

conflict via ManipulationMode. If you need both, put OnTapped on a child and OnPan on the parent.

  1. 60Hz pan pattern — for smooth drag UIs, write Translation directly

via ref during onChanged, and only setState in onEnded.

  1. Gesture phase lifecycle — always handle both onEnded and

onCancelled to avoid stuck state.

  1. `AllowDrop` auto-enables — just attaching .OnDrop() sets the flag.

Don't set it manually.

  1. Typed payloads are in-process only — TryGetTypedPayload<T> returns

false for cross-app drops. Always check the return value.

  1. Use `e.Handled = true` on keyboard events to prevent bubbling when

you've handled the key.

  1. Mouse long-press needs `enableMouseEmulation: true` — touch/pen have

native hold detection, but mouse does not.

← v1.0.0All versions