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
+32 -4
View File
@@ -102,7 +102,9 @@ Panel {
property int headerIndex: 0
readonly property bool canDisconnect: !!connectedWifiNetwork
readonly property bool headerHasDisconnect: false
readonly property int headerActionCount: 0
readonly property int headerActionCount: networkManagerAvailable ? 1 : 0
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
readonly property int heroRingPad: Style.space(6)
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
property int dnsIndex: 0
@@ -118,8 +120,20 @@ Panel {
headerIndex = Math.max(0, Math.min(headerActionCount - 1, headerIndex + delta))
}
function toggleNetwork() {
if (!networkManagerAvailable) return
Networking.wifiEnabled = !Networking.wifiEnabled
Qt.callLater(function() { root.refresh(true) })
}
function activateHeader() {
if (headerHasDisconnect && headerIndex === 0 && !busy) disconnect(connectedWifiNetwork)
toggleNetwork()
}
function setHeaderCursor() {
cursorActive = true
focusSection = "header"
headerIndex = 0
}
function selectDnsByDelta(delta) {
@@ -756,7 +770,7 @@ Panel {
// one; otherwise stays put. j drops into the wifi list if there's
// anywhere to land.
if (dy < 0) {
if (root.headerHasDisconnect) {
if (root.headerActionCount > 0) {
root.focusSection = "header"
root.headerIndex = 0
}
@@ -791,6 +805,7 @@ Panel {
onTabRequested: function(direction) { root.switchPanel(direction) }
onTextKey: function(t) {
if (t === "r" || t === "R") root.refresh()
else if (t === "w" || t === "W") root.toggleNetwork()
}
Column {
@@ -803,7 +818,18 @@ Panel {
// ---------- Hero: network icon · SSID + state · actions ----------
Item {
width: parent.width
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight)
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight) + root.heroRingPad * 2
// Keyboard focus ring around the hero Wi-Fi toggle. heroIcon is inset
// by heroRingPad so this ring stays inside the panel's clip box.
BorderSurface {
anchors.fill: heroIcon
anchors.margins: -root.heroRingPad
color: "transparent"
radius: Style.cornerRadius
visible: root.headerHasCursor
borderSpec: Border.controlSpec("hover-cursor", root.bar.foreground, Color.accent)
}
Text {
id: heroIcon
@@ -813,6 +839,7 @@ Panel {
font.pixelSize: Style.font.display
opacity: root.networkManagerAvailable ? 1.0 : 0.5
anchors.left: parent.left
anchors.leftMargin: root.heroRingPad
anchors.verticalCenter: parent.verticalCenter
MouseArea {
@@ -821,6 +848,7 @@ Panel {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.networkManagerAvailable
onContainsMouseChanged: if (containsMouse) root.setHeaderCursor()
onClicked: {
Networking.wifiEnabled = !Networking.wifiEnabled
Qt.callLater(function() { root.refresh(true) })