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:
co-authored by
Claude Opus 4.8
parent
ffa12802f7
commit
1ad707b81e
@@ -48,6 +48,8 @@ Panel {
|
|||||||
readonly property var exitNodes: displayExitNodes()
|
readonly property var exitNodes: displayExitNodes()
|
||||||
readonly property bool showExitNodes: tailscale.active && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
readonly property bool showExitNodes: tailscale.active && (exitNodes.length > 0 || tailscale.mullvadRegions.length > 0)
|
||||||
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
readonly property var filteredMullvadRegions: filteredMullvadRegionNodes()
|
||||||
|
readonly property bool headerHasCursor: cursorActive && focusSection === "header"
|
||||||
|
readonly property int heroRingPad: Style.space(6)
|
||||||
readonly property color iconColor: tailscale.active ? foreground : dim
|
readonly property color iconColor: tailscale.active ? foreground : dim
|
||||||
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
readonly property color barIconColor: tailscale.active ? barForeground : Qt.darker(barForeground, 1.55)
|
||||||
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||||
@@ -317,6 +319,12 @@ Panel {
|
|||||||
focusSection = "auth"
|
focusSection = "auth"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHeaderCursor() {
|
||||||
|
cursorActive = true
|
||||||
|
focusSection = "header"
|
||||||
|
headerIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: button.implicitWidth
|
implicitWidth: button.implicitWidth
|
||||||
implicitHeight: button.implicitHeight
|
implicitHeight: button.implicitHeight
|
||||||
|
|
||||||
@@ -429,11 +437,18 @@ Panel {
|
|||||||
Item {
|
Item {
|
||||||
id: header
|
id: header
|
||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: hero.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() }
|
||||||
|
|
||||||
PanelHero {
|
PanelHero {
|
||||||
id: hero
|
id: hero
|
||||||
width: parent.width
|
x: root.heroRingPad
|
||||||
|
y: root.heroRingPad
|
||||||
|
width: parent.width - root.heroRingPad
|
||||||
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
title: tailscale.installed ? (tailscale.selfName || "Tailscale") : "Tailscale"
|
||||||
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
meta: tailscale.active ? root.heroPhraseText : "Tailscale is disconnected"
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
@@ -444,6 +459,18 @@ Panel {
|
|||||||
implicitWidth: icon.implicitWidth
|
implicitWidth: icon.implicitWidth
|
||||||
implicitHeight: icon.implicitHeight
|
implicitHeight: icon.implicitHeight
|
||||||
|
|
||||||
|
// 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: icon
|
||||||
|
anchors.margins: -header.ringPad
|
||||||
|
color: "transparent"
|
||||||
|
radius: Style.cornerRadius
|
||||||
|
visible: header.ringVisible
|
||||||
|
borderSpec: Border.controlSpec("hover-cursor", hero.foreground, Color.accent)
|
||||||
|
}
|
||||||
|
|
||||||
TailscaleIcon {
|
TailscaleIcon {
|
||||||
id: icon
|
id: icon
|
||||||
iconSize: Style.font.display
|
iconSize: Style.font.display
|
||||||
@@ -460,10 +487,7 @@ Panel {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
enabled: tailscale.installed && !tailscale.busy
|
enabled: tailscale.installed && !tailscale.busy
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
onContainsMouseChanged: if (containsMouse) {
|
onContainsMouseChanged: if (containsMouse) header.focusHero()
|
||||||
root.focusSection = "header"
|
|
||||||
root.headerIndex = 0
|
|
||||||
}
|
|
||||||
onClicked: tailscale.toggleTailscale()
|
onClicked: tailscale.toggleTailscale()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user