Give every hero toggle a keyboard focus ring

Each panel's hero on/off toggle (Tailscale, Dropbox Wi-Fi, Bluetooth,
network, audio mute) now has a reachable "header" cursor section, a focus
ring around the hero icon, Enter/Space activation, a letter shortcut, and
hover parity. The hero is inset by heroRingPad so the ring stays inside
the Flickable/ScrollView clip box instead of being cut off.

Bluetooth and audio get a virtual "header" section above their device
sections, so the adapter/mute can be toggled by keyboard even when no
device rows exist. Network repurposes its previously dead header section.

For the PanelHero-based panels (Tailscale, Dropbox), route panel state
through the wrapper's `header` id: inside a PanelHero iconComponent `root`
resolves to PanelHero, not the Panel, so `root.headerHasCursor` and
`root.setHeaderCursor()` silently referenced the wrong object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-20 15:50:05 -07:00
co-authored by Claude Opus 4.8
parent 835181f112
commit 228b5ac3ba
5 changed files with 224 additions and 45 deletions
+84 -31
View File
@@ -38,6 +38,8 @@ Panel {
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim
readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55)
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
readonly property int heroRingPad: Style.space(6)
function ensureCursor() {
if (!dropbox.authenticated) {
@@ -50,7 +52,7 @@ Panel {
fileIndex = 0
return
}
if (focusSection !== "files") focusSection = "files"
if (focusSection !== "files" && focusSection !== "header") focusSection = "files"
if (fileIndex >= dropbox.files.length) fileIndex = Math.max(0, dropbox.files.length - 1)
if (fileIndex < 0) fileIndex = 0
}
@@ -58,15 +60,39 @@ Panel {
function moveCursor(dx, dy) {
cursorActive = true
ensureCursor()
if (focusSection === "files" && dy !== 0) {
if (dy === 0) return
if (focusSection === "header") {
if (dy > 0 && dropbox.files.length > 0) {
focusSection = "files"
fileIndex = 0
scrollCursorIntoView()
}
return
}
if (focusSection === "files") {
if (dy < 0 && fileIndex === 0) {
setHeaderCursor()
return
}
fileIndex = Math.max(0, Math.min(dropbox.files.length - 1, fileIndex + dy))
scrollCursorIntoView()
}
}
function setHeaderCursor() {
cursorActive = true
focusSection = "header"
if (panelFlick) panelFlick.contentY = 0
}
function toggleRunning() {
if (dropbox.installed && !dropbox.busy) dropbox.toggleRunning()
}
function activateCursor() {
ensureCursor()
if (focusSection === "login") dropbox.login()
else if (focusSection === "header") toggleRunning()
else if (focusSection === "files") dropbox.openFile(selectedFile())
}
@@ -183,6 +209,7 @@ Panel {
onTextKey: function(t) {
if (t === "r" || t === "R") dropbox.refresh()
else if (t === "l" || t === "L") dropbox.login()
else if (t === "p" || t === "P") root.toggleRunning()
}
Flickable {
@@ -201,40 +228,66 @@ Panel {
width: panelFlick.width
spacing: Style.space(12)
PanelHero {
id: hero
Item {
id: header
visible: dropbox.authenticated
width: parent.width
title: "Dropbox"
meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
foreground: root.foreground
fontFamily: root.fontFamily
iconOpacity: dropbox.active ? 1.0 : 0.5
iconComponent: Component {
Item {
implicitWidth: heroIcon.implicitWidth
implicitHeight: heroIcon.implicitHeight
implicitHeight: hero.implicitHeight + root.heroRingPad
// Exposed for the hero's iconComponent, whose `root` resolves to
// PanelHero (not this Panel) — reach panel state via `header`.
readonly property bool ringVisible: root.headerHasCursor
readonly property int ringPad: root.heroRingPad
function focusHero() { root.setHeaderCursor() }
DropboxIcon {
id: heroIcon
iconSize: Style.font.display
color: root.iconColor
anchors.centerIn: parent
}
PanelHero {
id: hero
x: root.heroRingPad
y: root.heroRingPad
width: parent.width - root.heroRingPad
title: "Dropbox"
meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
foreground: root.foreground
fontFamily: root.fontFamily
iconOpacity: dropbox.active ? 1.0 : 0.5
iconComponent: Component {
Item {
implicitWidth: heroIcon.implicitWidth
implicitHeight: heroIcon.implicitHeight
MouseArea {
id: heroMouse
anchors.fill: parent
hoverEnabled: true
enabled: dropbox.installed && !dropbox.busy
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: dropbox.toggleRunning()
}
// Keyboard focus ring around the hero toggle. The hero is
// inset by heroRingPad so this ring stays inside the
// Flickable's clip box instead of being cut off.
BorderSurface {
anchors.fill: heroIcon
anchors.margins: -header.ringPad
color: "transparent"
radius: Style.cornerRadius
visible: header.ringVisible
borderSpec: Border.controlSpec("hover-cursor", hero.foreground, Color.accent)
}
PanelToolTip {
visible: heroMouse.containsMouse
text: dropbox.active ? "Pause syncing" : "Resume syncing"
fontFamily: root.fontFamily
DropboxIcon {
id: heroIcon
iconSize: Style.font.display
color: root.iconColor
anchors.centerIn: parent
}
MouseArea {
id: heroMouse
anchors.fill: parent
hoverEnabled: true
enabled: dropbox.installed && !dropbox.busy
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onContainsMouseChanged: if (containsMouse) header.focusHero()
onClicked: dropbox.toggleRunning()
}
PanelToolTip {
visible: heroMouse.containsMouse
text: dropbox.active ? "Pause syncing" : "Resume syncing"
fontFamily: root.fontFamily
}
}
}
}