Refactor: RadioButtonGroup focus behavior and orientation-aware navigation

- Radio button indicator no longer pulses, stays static (selected=accent, unselected=border)
- Only focused item shows indicator, not all items
- Vertical orientation: Up/Down arrows navigate, Left/Right ignored
- Horizontal orientation: Left/Right arrows navigate, Up/Down ignored
- Enter/Space always select regardless of orientation
- Pass orientation to RadioButtonGroupHandler for navigation logic
- Updated all handler tests to include orientation parameter
- All 591 tests passing
This commit is contained in:
phranck
2026-02-06 18:06:08 +01:00
parent ce772ea6f3
commit 6fee5aa671
2 changed files with 30 additions and 10 deletions
+18 -6
View File
@@ -164,6 +164,7 @@ final class RadioButtonGroupHandler: Focusable {
let focusID: String
let selection: Binding<AnyHashable>
let itemValues: [AnyHashable]
let orientation: RadioButtonOrientation
var canBeFocused: Bool
var focusedIndex: Int = 0
@@ -171,11 +172,13 @@ final class RadioButtonGroupHandler: Focusable {
focusID: String,
selection: Binding<AnyHashable>,
itemValues: [AnyHashable],
orientation: RadioButtonOrientation,
canBeFocused: Bool
) {
self.focusID = focusID
self.selection = selection
self.itemValues = itemValues
self.orientation = orientation
self.canBeFocused = canBeFocused
// Find current focused index based on selection
@@ -191,6 +194,8 @@ extension RadioButtonGroupHandler {
func handleKeyEvent(_ event: KeyEvent) -> Bool {
switch event.key {
case .up:
// Up/Down only work for vertical orientation
guard orientation == .vertical else { return false }
if focusedIndex > 0 {
focusedIndex -= 1
selection.wrappedValue = itemValues[focusedIndex]
@@ -199,6 +204,8 @@ extension RadioButtonGroupHandler {
return false
case .down:
// Up/Down only work for vertical orientation
guard orientation == .vertical else { return false }
if focusedIndex < itemValues.count - 1 {
focusedIndex += 1
selection.wrappedValue = itemValues[focusedIndex]
@@ -207,6 +214,8 @@ extension RadioButtonGroupHandler {
return false
case .left:
// Left/Right only work for horizontal orientation
guard orientation == .horizontal else { return false }
if focusedIndex > 0 {
focusedIndex -= 1
selection.wrappedValue = itemValues[focusedIndex]
@@ -215,6 +224,8 @@ extension RadioButtonGroupHandler {
return false
case .right:
// Left/Right only work for horizontal orientation
guard orientation == .horizontal else { return false }
if focusedIndex < itemValues.count - 1 {
focusedIndex += 1
selection.wrappedValue = itemValues[focusedIndex]
@@ -256,6 +267,7 @@ extension RadioButtonGroup: Renderable {
focusID: focusID,
selection: erasedSelection,
itemValues: itemValues,
orientation: orientation,
canBeFocused: !isDisabled
)
focusManager.register(handler, inSection: context.activeFocusSectionID)
@@ -321,19 +333,19 @@ extension RadioButtonGroup: Renderable {
// Radio indicator: ● if selected, ◯ if not
let indicator = isSelected ? "●" : "◯"
// Determine indicator color
// Determine indicator color (static, no pulsing)
let indicatorColor: Color
if isDisabled {
indicatorColor = palette.foregroundTertiary
} else if isFocused {
// Subtle pulse on focus
let dimAccent = palette.accent.opacity(0.35)
indicatorColor = Color.lerp(dimAccent, palette.accent, phase: context.pulsePhase)
} else if isSelected {
// Selected indicator uses accent color
indicatorColor = palette.accent
} else {
// Unselected indicator uses border color
indicatorColor = palette.border
}
let styledIndicator = ANSIRenderer.colorize(indicator, foreground: indicatorColor, bold: isFocused && !isDisabled)
let styledIndicator = ANSIRenderer.colorize(indicator, foreground: indicatorColor, bold: isSelected && !isDisabled)
// Render label
let labelView = item.labelBuilder()
+12 -4
View File
@@ -235,7 +235,7 @@ struct RadioButtonGroupTests {
@Suite("RadioButtonGroupHandler Tests")
struct RadioButtonGroupHandlerTests {
@Test("Handler handles arrow down to navigate items")
@Test("Handler handles arrow down to navigate items (vertical)")
func handleArrowDown() {
var selection = AnyHashable("opt1")
let binding = Binding(
@@ -248,6 +248,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: true
)
@@ -259,7 +260,7 @@ struct RadioButtonGroupHandlerTests {
#expect(selection == AnyHashable("opt2"))
}
@Test("Handler handles arrow up to navigate items")
@Test("Handler handles arrow up to navigate items (vertical)")
func handleArrowUp() {
var selection = AnyHashable("opt2")
let binding = Binding(
@@ -272,6 +273,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: true
)
handler.focusedIndex = 1
@@ -283,7 +285,7 @@ struct RadioButtonGroupHandlerTests {
#expect(handler.focusedIndex == 0)
}
@Test("Handler handles arrow right (horizontal navigation)")
@Test("Handler handles arrow right (horizontal only)")
func handleArrowRight() {
var selection = AnyHashable("a")
let binding = Binding(
@@ -296,6 +298,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .horizontal,
canBeFocused: true
)
@@ -307,7 +310,7 @@ struct RadioButtonGroupHandlerTests {
#expect(selection == AnyHashable("b"))
}
@Test("Handler handles arrow left (horizontal navigation)")
@Test("Handler handles arrow left (horizontal only)")
func handleArrowLeft() {
var selection = AnyHashable("b")
let binding = Binding(
@@ -320,6 +323,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .horizontal,
canBeFocused: true
)
handler.focusedIndex = 1
@@ -345,6 +349,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: true
)
handler.focusedIndex = 1
@@ -369,6 +374,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: true
)
handler.focusedIndex = 1
@@ -393,6 +399,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: true
)
@@ -417,6 +424,7 @@ struct RadioButtonGroupHandlerTests {
focusID: "test",
selection: binding,
itemValues: itemValues,
orientation: .vertical,
canBeFocused: false
)