Events
Events follow the DOM and React. A handler is a function that receives the
event by value; the event’s methods control what happens next. Handlers
are props of package h: OnClick, OnKeyDown, OnDrag…
func Toggle() tela.Node { open, setOpen := tela.UseState(false)
return Div(Class("rounded-xl border p-4"), OnClick(func(e tela.MouseEvent) { e.StopPropagation() // an ancestor's OnClick does not run setOpen.Set(!open) }), IfElse(open, Text("Open"), Text("Closed")), )}Every handler prop panics when its handler is nil, with a message that shows
the right call.
To stop an event from reaching the ancestors’ handlers without reading it, wrap
the function in tela.StopPropagation, which every handler prop accepts:
Div(OnClick(openRow), Button(OnClick(tela.StopPropagation(remove)), Text("Delete")), // openRow does not run)Div(OnKeyDown(tela.StopPropagation(closeDialog)))It calls e.StopPropagation() and then the function. A handler that stops
the event only sometimes takes the event and calls e.StopPropagation()
itself. (The press hook of the aria package stops propagation by
default, as React Aria does.)
How events travel
Section titled “How events travel”- A pointer event’s target is the topmost element under the pointer as
painted, inside the clips and transforms of its ancestors, skipping
pointer-events-noneand invisible elements. - Events bubble from the target to the root, calling each ancestor’s
handler.
StopPropagation()stops that.OnMouseEnter,OnMouseLeave,OnPointerEnter,OnPointerLeave,OnLongPress, the drag handlers andOnScrolldo not bubble. - A
Disabledelement and its descendants receive no pointer events. PreventDefault()cancels Tela’s default action for the event (the focus move of Tab, the scroll of a wheel notch, the text a key types). Which default each event has is listed on its handler.- After every discrete event (presses, releases, clicks, drag start and end, long press), pending state updates are applied and components rebuilt before the next event, so two clicks before a frame see each other’s updates. Continuous events (moves, drag moves) are batched: state set there is applied once per frame.
Handlers and their events
Section titled “Handlers and their events”| Area | Handlers | Event |
|---|---|---|
| Mouse | OnClick OnDoubleClick OnContextMenu OnMouseDown OnMouseUp OnMouseMove OnMouseEnter OnMouseLeave |
tela.MouseEvent |
| Pointer (mouse, touch, pen) | OnPointerDown OnPointerMove OnPointerUp OnPointerCancel OnPointerEnter OnPointerLeave OnLongPress |
tela.PointerEvent |
| Drag | OnDragStart OnDrag OnDragEnd |
tela.DragEvent |
| Keyboard | OnKeyDown OnKeyUp |
tela.KeyboardEvent |
| Focus | OnFocus OnBlur |
tela.FocusEvent |
| Wheel and scroll | OnWheel OnScroll |
tela.WheelEvent, tela.ScrollEvent |
| Text fields and forms | OnChange OnBeforeInput OnCompositionStart OnCompositionUpdate OnCompositionEnd OnSelect OnSubmit |
see Forms and Text Input |
Every handler takes a func() or a func of its event type, so write the
event only when the handler reads it (about four handlers in five do not):
Button(OnClick(func() { setCount.Update(func(n int) int { return n + 1 }) }), Text("+1"))Div(OnMouseMove(func(e tela.MouseEvent) { pos.Set(e.LocalX) }))A nil handler panics, naming both shapes. tela.Toggle(setOpen) is a
ready-made func() that flips a bool state with Update, so two clicks
that land in one frame both count: OnClick(tela.Toggle(setOpen)).
The fields and methods of the event types are listed in API-Reference-tela; the common ones are below.
Mouse and pointer
Section titled “Mouse and pointer”Pointer events fire for every pointer: mouse, touch and pen. Mouse events are the compatibility view of the primary pointer, in the DOM order: pointerdown, mousedown, pointerup, mouseup, click.
| Field | Meaning |
|---|---|
X, Y |
Position in the window, in logical pixels |
LocalX, LocalY |
Position relative to the border box of the element whose handler runs |
Button, Buttons |
The button that changed (tela.MouseButtonLeft, tela.MouseButtonRight, tela.MouseButtonMiddle…) and the set held; e.Buttons.Has(tela.MouseButtonLeft) |
Shift, Ctrl, Alt, Meta |
Modifier keys (Meta is the Windows, Command or Super key) |
ClickCount |
1 for a click, 2 for the second click of a double click |
Kind, PointerID, Pressure, IsPrimary |
On a PointerEvent: tela.PointerMouse, tela.PointerTouch or tela.PointerPen, and the pointer’s identity |
func Tab(title string, closeTab func()) tela.Node { return Div(Class("rounded-t-lg px-4 py-2 hover:bg-slate-100"), OnMouseDown(func(e tela.MouseEvent) { if e.Button == tela.MouseButtonMiddle { closeTab() } }), OnContextMenu(func(e tela.MouseEvent) { openMenu(e.X, e.Y) }), Text(title), )}
func openMenu(x, y float32) {}OnDoubleClick runs on the second click of a double click (two clicks within
the OS’s double-click time and 5 px: the Windows setting, 500 ms where the OS
does not report one; 300 ms and 100 px for touch); the first click is not
delayed, so OnClick runs for both.
Keyboard
Section titled “Keyboard”Key events go to the focused element and bubble; with nothing focused they go
to the outermost element of the window’s content. A tela.KeyboardEvent has:
Key, the logical key: what it means with the active layout and modifiers. Atela.KeyNameis either a named key (tela.KeyEnter,tela.KeyEscape,tela.KeyArrowDown…) or the character the key types ("a","A"," ").Ctrldoes not change it: Ctrl+S has Key"s".Code, the physical key (atela.KeyCode), whatever the layout:tela.CodeKeyS,tela.CodeKeyW… Use it for positions (WASD) andKeyfor meanings (Ctrl+Z).Location(tela.KeyLocationLeft,tela.KeyLocationRight,tela.KeyLocationNumpad),Repeat, andShift,Ctrl,Alt,Meta.
func Editor(save, closeEditor func()) tela.Node { return Div(TabIndex(0), Class("p-4 outline-none"), OnKeyDown(func(e tela.KeyboardEvent) { switch { case e.Key == tela.KeyEscape: closeEditor() case e.Ctrl && e.Code == tela.CodeKeyS: e.PreventDefault() save() } }), Text("Press Ctrl+S to save, Esc to close"), )}After the handlers, Tela runs the default actions unless one called
PreventDefault: Tab and Shift+Tab move the focus through the focusable
elements; Enter on a focused Button clicks it on keydown, Space on keyup.
Typed text reaches text fields, which report it with OnChange.
A Button is focusable. Any other element becomes focusable with TabIndex:
TabIndex(0) puts it in the Tab order in tree order, TabIndex(-1) makes it
focusable by click and by code only. Disabled and hidden elements are not
focusable. AutoFocus focuses an element when it first appears. A press of the
primary button focuses the nearest focusable element under the pointer, and
blurs the focused one otherwise.
OnFocus and OnBlur bubble, like React’s: a container’s OnFocus also
runs when a descendant gains the focus, which makes “focus within” logic easy.
The focus:, focus-visible: and focus-within: variants
style the focused element.
func Combo() tela.Node { editing, setEditing := tela.UseState(false) return Div( OnFocus(func(tela.FocusEvent) { setEditing.Set(true) }), OnBlur(func(tela.FocusEvent) { setEditing.Set(false) }), Class("rounded-lg border p-2 focus-within:ring-2 focus-within:ring-indigo-500"), Input(AriaLabel("Name")), If(editing, Span(Class("text-xs text-slate-500"), Text("editing"))), )}To move the focus from code, attach an tela.ElementRef with Ref and call
Focus(); see Hooks.
A press that moves farther than a few pixels (4 for a mouse, 18 for touch and
pen) before a long press becomes a drag, and does not click. Drag events go to
the innermost element with drag handlers. A tela.DragEvent carries DeltaX and
DeltaY (since the previous event), OffsetX and OffsetY (since the press,
so an element moved by them follows the pointer exactly) and VelocityX,
VelocityY in pixels per second (meaningful at the end, for flings).
Moving an element by repainting only is the job of tela.TranslateFn with a
UseValue; no component builds while the
card follows the pointer:
type Offset struct{ X, Y float32 }
func DraggableCard() tela.Node { offset := tela.UseValue(Offset{}) start := tela.UseRef(Offset{})
return Div(Class("w-48 rounded-xl bg-white p-6 shadow-lg"), Style(tela.TranslateFn(func() (x, y float32) { p := offset.Get() return p.X, p.Y })), OnDragStart(func(tela.DragEvent) { start.Current = offset.Latest() }), OnDrag(func(e tela.DragEvent) { offset.Set(Offset{start.Current.X + e.OffsetX, start.Current.Y + e.OffsetY}) }), OnDragEnd(func(e tela.DragEvent) { if e.VelocityX > 800 { // a fling to the right } }), Text("Drag me"), )}The examples/board kanban board uses this to move cards between columns.
Long press
Section titled “Long press”OnLongPress runs when the primary button (or a touch) stays pressed for
500 ms without moving more than a few pixels (4 for a mouse, 18 for touch and
pen). The press then does not click or drag. It goes to the innermost element
with the handler and does not bubble.
func Item(name string) tela.Node { return Div(OnLongPress(func(e tela.PointerEvent) { showMenu(e.X, e.Y) }), Text(name))}
func showMenu(x, y float32) {}In a test, hold the button with telatest.Delay or advance the manual clock;
see Testing.
OnWheel observes a wheel notch or a touchpad scroll (DOM signs: positive
DeltaY scrolls down; a notch is 53 pixels, and Shift turns a vertical notch
horizontal). Unless a handler calls PreventDefault(), the nearest scrolling
element scrolls:
func Canvas(zoom *tela.Value[float32]) tela.Node { return Div(OnWheel(func(e tela.WheelEvent) { if e.Ctrl { // Ctrl+wheel zooms instead of scrolling the page e.PreventDefault() zoom.Update(func(z float32) float32 { return z * (1 - e.DeltaY/500) }) } }))}OnScroll is covered in Scrolling and VirtualList.
See also
Section titled “See also”- Accessibility: the same elements, seen by assistive technologies and tests.
telatestsimulates all of these with real events:App.Click,App.Hover,App.Drag,App.Press.