Keyboard and focus
Many people use an app from the keyboard alone, and screen-reader users mostly do. Every control they need should be reachable with Tab and operable with keys.
The focus order
On desktop, Tab moves focus to the next widget that accepts it, and Shift+Tab to the previous one. At either end, focus wraps around.
// The focus order is the order in the tree: name, email, notes, then the
// button. The disabled check is skipped. Tab stops at notes: a multi-line
// entry keeps Tab and Shift+Tab, so the keyboard cannot move focus past it.
name := widget.NewEntry()
email := widget.NewEntry()
notes := widget.NewMultiLineEntry() // takes Tab itself, to insert a tab
later := widget.NewCheck("Remind me later", nil)
later.Disable()
send := widget.NewButton("Send", func() {})
w.SetContent(container.NewVBox(name, email, notes, later, send))- The order is the order of the widget tree: the order of objects in their containers, depth first. To change the order, change the layout.
- Disabled widgets are skipped.
- Under a right-to-left layout, Bellui reorders focus to follow the screen: rows from top to bottom, and each row from right to left. A single column keeps its order.
- Escape closes the topmost pop-up, menu or dialog, unless the focused widget uses Escape itself.
Buttons are activated with Space when they have focus.
Set the first focused widget
For a window whose purpose is to type something, focus the first field when you build it, so the user can start typing straight away:
w.Canvas().Focus(name) // start typing straight awayThe canvas also has FocusNext, FocusPrevious, Unfocus and Focused.
Widgets that keep Tab
A widget that implements
event.Tabbable
and returns true from AcceptsTab receives Tab itself, instead of
moving focus. A multi-line entry does this so that Tab inserts a tab
character. It also keeps Shift+Tab, and Bellui gives it no other key that
moves focus out, so a keyboard user who reaches one cannot Tab past it.
If you write such a widget, give users another key that leaves it, and move
focus on yourself with the canvas’s FocusNext. A widget that keeps Tab
with no way out traps keyboard users.
Make a custom widget focusable
A widget receives focus and keys by implementing
event.Focusable:
var _ event.Focusable = (*Rating)(nil)
func (r *Rating) FocusGained() { r.focused = true; r.update() }
func (r *Rating) FocusLost() { r.focused = false; r.update() }
func (r *Rating) TypedRune(rune) {}
// TypedKey gives keyboard users the same control as the pointer.
func (r *Rating) TypedKey(ev *event.KeyEvent) {
switch ev.Name {
case event.KeyRight, event.KeyUp:
r.set(r.Value + 1)
case event.KeyLeft, event.KeyDown:
r.set(r.Value - 1)
case event.KeyHome:
r.set(0)
}
}FocusGainedandFocusLosttell the widget when it gains and loses focus. Show it: users must be able to see where focus is. The rating widget puts brackets around its stars.TypedKeyreceives keys such as the arrows, Home and Space.TypedRunereceives typed characters.
Give keyboard users the same control a pointer user has. The rating widget uses the arrow keys for what a tap does.
A focusable widget that a key activates, as Space activates a button, can
also implement
event.Activatable.
ActivatedByKey reports which keys activate it. Bellui then reports that
key press as an activation, the same signal a tap sends. The widget still
handles the key itself in TypedKey.
The full program:
// Keyboard focus: where it starts, and the order Tab follows.
package main
import (
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Keyboard")
// The focus order is the order in the tree: name, email, notes, then the
// button. The disabled check is skipped. Tab stops at notes: a multi-line
// entry keeps Tab and Shift+Tab, so the keyboard cannot move focus past it.
name := widget.NewEntry()
email := widget.NewEntry()
notes := widget.NewMultiLineEntry() // takes Tab itself, to insert a tab
later := widget.NewCheck("Remind me later", nil)
later.Disable()
send := widget.NewButton("Send", func() {})
w.SetContent(container.NewVBox(name, email, notes, later, send))
w.Canvas().Focus(name) // start typing straight away
bellui.ShowAndRun(w)
}