Summary#
onPointerEnter and onPointerLeave run an action when the pointer enters or leaves an element.
They exist because hover had only ever been a look. inside Card.Hover { Bg = Accent; } lowers to a :hover
rule, so it can restyle an element and cannot tell the app anything — which left "am I over this drop target?"
with no spelling at all.
Signature#
// WHICH element — no coordinates.
Box(onPointerEnter: <action>, onPointerLeave: <action>)
// WHERE on the element — each action takes a `Point`.
Canvas(onPointerDown: <action>, onPointerMove: <action>, onPointerUp: <action>)
class Point { public double X; public double Y; } // element-relative, in CSS pixelsDescription#
Neither carries coordinates, and that is the point. The element already knows the pointer is over it — that is what a pointer event is. Answering "which lane am I over?" with coordinates plus hit-testing is a much larger surface, and it is the wrong tool for a question the DOM has already answered.
Neither bubbles. They lower to pointerenter/pointerleave, the non-bubbling pair, deliberately: the bubbling
alternative fires on every ancestor of whatever the pointer is really over, so a lane containing a card would report
itself entered when the pointer merely crossed the card — and telling the two apart would need the coordinates back.
They are not activations. They never spin a control's busy affordance and never take part in the double-submit guard. A pointer crossing an element is not a submission, and a drop target that greyed itself out when you hovered it would be the worse failure.
Reach for a Hover variant when only the LOOK changes. It is CSS, so it costs no round trip and no re-render,
and it keeps working when the app is busy. Reach for these when the app must know — a drop target that has to
record which lane is active, a row that loads a preview, a chart that reports what is under the pointer.
Where was the pointer? — element-local coordinates#
onPointerDown / onPointerMove / onPointerUp each hand their action a Point — where the pointer is, in
the element's own coordinates. (0, 0) is the element's top-left corner, whatever the page has done around it.
That is the space every consumer actually wants: a canvas has no elements to hit-test, so "where in THIS canvas" is
the only question there is, and Draw.Circle(at.X, at.Y, 4, ink) needs no conversion. A viewport coordinate would be the
raw browser answer and useless alone — turning it into an element coordinate needs the element's own origin, which
would then have to be kept in sync with every scroll and reflow.
All three phases, because a gesture has three. A brush, a zoom or a lasso anchors on down, stretches on move
and commits on up. onPointerMove alone cannot express a drag at all: there is no way to learn when it started or
that it ended.
onPointerMove is coalesced to one run per animation frame, keeping the newest position. A pointer fires at
60-120Hz while a canvas redraws once a frame, so the extra runs are waste — and the alternative default, firing
everything and expecting a debounce:, makes the expensive case the one you get by forgetting. down and up are
discrete and always fire immediately: a coalesced press is a lost click.
Reach for enter/leave when the DOM already knows the answer. "Am I over this lane?" needs no coordinates, and answering it by hit-testing a
Pointre-derives something the browser has already computed.
They fire for touch and pen too, since they are pointer events rather than mouse events. On a touch screen "enter" arrives with the tap, so an interaction that is only reachable by hovering is unreachable there — give it a tap or keyboard path as well.
Right-click and long-press — onContextMenu#
onContextMenu runs an action on the secondary click — right-click with a mouse, the long-press menu on touch.
Pressable(onClick: Reveal, onContextMenu: Flag) { Text(face); }It suppresses the browser's own menu, and that is the point rather than a convenience. The native menu cannot be prevented any other way, so an element that drew its own would get both, stacked. Binding the prop is the statement that this element owns its secondary action — bind it only where you mean to replace the browser's.
Like onClick, the innermost binding wins: a right-click on a card inside a row runs the card's action and not
the row's. Unlike onClick, it is not an activation — it never spins a control's busy affordance, because
opening a menu is not a submission.
⚑ There is no onRightClick, onAuxClick, onLongPress, onMouseDown or onDoubleClick. One event covers
the gesture on both pointer and touch, and the pointer props above cover everything else — the platform has no
ambient for "which button is down", so onPointerDown cannot tell a left press from a right one.
Examples#
[Page("/lanes")]
[Render(CSR)]
[AllowAnonymous]
component Lanes() {
var over = "none";
action EnterTodo() { over = "todo"; }
action EnterDone() { over = "done"; }
action Clear() { over = "none"; }
render {
Stack(gap: 2) {
Text($"over={over}");
Box(p: 3, onPointerEnter: EnterTodo, onPointerLeave: Clear) { Text("To do"); }
Box(p: 3, onPointerEnter: EnterDone, onPointerLeave: Clear) { Text("Done"); }
}
}
}[Page("/paint")]
[Render(CSR)]
[AllowAnonymous]
component Paint() {
var drawing = false;
action Start(Point at) { drawing = true; }
action Stroke(Point at) { if (drawing) { Draw.Circle(at.X, at.Y, 4.0, "#3b82f6"); } }
action Stop(Point at) { drawing = false; }
render {
Canvas(w: 400, h: 300, onPointerDown: Start, onPointerMove: Stroke, onPointerUp: Stop);
}
}See also#
- style props —
inside X.Hover { … }, the CSS-only form to prefer when only the look changes - keys — the keyboard counterpart: declaring the keys an element owns, and reading held state
- drag — dragging an element, which is a separate binding and not built on these
- component — where an
actionis declared