mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user