Refactor: Remove maxVisibleRows from List and Table (not in SwiftUI API)

List and Table now always fill available height like SwiftUI.
The TUI-specific maxVisibleRows parameter was removed for API parity.
This commit is contained in:
phranck
2026-02-10 12:46:09 +01:00
parent e2acc069fc
commit 0dedddca59
5 changed files with 41 additions and 55 deletions
+25 -30
View File
@@ -101,9 +101,6 @@ public struct List<SelectionValue: Hashable & Sendable, Content: View, Footer: V
/// Whether the list is disabled.
var isDisabled: Bool
/// The maximum number of visible rows (nil = use available height).
let maxVisibleRows: Int?
/// The placeholder text shown when the list is empty.
let emptyPlaceholder: String
@@ -120,7 +117,6 @@ public struct List<SelectionValue: Hashable & Sendable, Content: View, Footer: V
selectionMode: selectionMode,
focusID: focusID,
isDisabled: isDisabled,
maxVisibleRows: maxVisibleRows,
emptyPlaceholder: emptyPlaceholder,
showFooterSeparator: showFooterSeparator
)
@@ -136,7 +132,7 @@ extension List {
/// - title: The title displayed in the border.
/// - selection: A binding to the selected item's ID (nil = no selection).
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - showFooterSeparator: Whether to show separator before footer (default: true).
/// - content: A ViewBuilder that defines the list content.
@@ -145,7 +141,7 @@ extension List {
_ title: String,
selection: Binding<SelectionValue?>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
showFooterSeparator: Bool = true,
@ViewBuilder content: () -> Content,
@@ -158,7 +154,7 @@ extension List {
self.multiSelection = nil
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = showFooterSeparator
}
@@ -168,7 +164,7 @@ extension List {
/// - Parameters:
/// - selection: A binding to the selected item's ID (nil = no selection).
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - showFooterSeparator: Whether to show separator before footer (default: true).
/// - content: A ViewBuilder that defines the list content.
@@ -176,7 +172,7 @@ extension List {
public init(
selection: Binding<SelectionValue?>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
showFooterSeparator: Bool = true,
@ViewBuilder content: () -> Content,
@@ -189,7 +185,7 @@ extension List {
self.multiSelection = nil
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = showFooterSeparator
}
@@ -204,14 +200,14 @@ extension List where Footer == EmptyView {
/// - title: The title displayed in the border.
/// - selection: A binding to the selected item's ID (nil = no selection).
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - content: A ViewBuilder that defines the list content.
public init(
_ title: String,
selection: Binding<SelectionValue?>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
@ViewBuilder content: () -> Content
) {
@@ -222,7 +218,7 @@ extension List where Footer == EmptyView {
self.multiSelection = nil
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = false
}
@@ -232,13 +228,13 @@ extension List where Footer == EmptyView {
/// - Parameters:
/// - selection: A binding to the selected item's ID (nil = no selection).
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - content: A ViewBuilder that defines the list content.
public init(
selection: Binding<SelectionValue?>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
@ViewBuilder content: () -> Content
) {
@@ -249,7 +245,7 @@ extension List where Footer == EmptyView {
self.multiSelection = nil
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = false
}
@@ -264,7 +260,7 @@ extension List {
/// - title: The title displayed in the border.
/// - selection: A binding to the set of selected item IDs.
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - showFooterSeparator: Whether to show separator before footer (default: true).
/// - content: A ViewBuilder that defines the list content.
@@ -273,7 +269,7 @@ extension List {
_ title: String,
selection: Binding<Set<SelectionValue>>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
showFooterSeparator: Bool = true,
@ViewBuilder content: () -> Content,
@@ -286,7 +282,7 @@ extension List {
self.multiSelection = selection
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = showFooterSeparator
}
@@ -296,7 +292,7 @@ extension List {
/// - Parameters:
/// - selection: A binding to the set of selected item IDs.
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - showFooterSeparator: Whether to show separator before footer (default: true).
/// - content: A ViewBuilder that defines the list content.
@@ -304,7 +300,7 @@ extension List {
public init(
selection: Binding<Set<SelectionValue>>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
showFooterSeparator: Bool = true,
@ViewBuilder content: () -> Content,
@@ -317,7 +313,7 @@ extension List {
self.multiSelection = selection
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = showFooterSeparator
}
@@ -332,14 +328,14 @@ extension List where Footer == EmptyView {
/// - title: The title displayed in the border.
/// - selection: A binding to the set of selected item IDs.
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - content: A ViewBuilder that defines the list content.
public init(
_ title: String,
selection: Binding<Set<SelectionValue>>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
@ViewBuilder content: () -> Content
) {
@@ -350,7 +346,7 @@ extension List where Footer == EmptyView {
self.multiSelection = selection
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = false
}
@@ -360,13 +356,13 @@ extension List where Footer == EmptyView {
/// - Parameters:
/// - selection: A binding to the set of selected item IDs.
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - content: A ViewBuilder that defines the list content.
public init(
selection: Binding<Set<SelectionValue>>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
emptyPlaceholder: String = "No items",
@ViewBuilder content: () -> Content
) {
@@ -377,7 +373,7 @@ extension List where Footer == EmptyView {
self.multiSelection = selection
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.emptyPlaceholder = emptyPlaceholder
self.showFooterSeparator = false
}
@@ -409,7 +405,6 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
let selectionMode: SelectionMode
let focusID: String?
let isDisabled: Bool
let maxVisibleRows: Int?
let emptyPlaceholder: String
let showFooterSeparator: Bool
@@ -436,7 +431,7 @@ private struct _ListCore<SelectionValue: Hashable & Sendable, Content: View, Foo
} else {
// Calculate viewport height (reserve space for scroll indicators if needed)
let availableHeight = context.availableHeight
let viewportHeight = maxVisibleRows ?? max(1, availableHeight - 4) // Reserve for border + indicators
let viewportHeight = max(1, availableHeight - 4) // Reserve for border + indicators
// Get or create persistent focusID
let focusIDKey = StateStorage.StateKey(identity: context.identity, propertyIndex: 1)
+7 -12
View File
@@ -69,9 +69,6 @@ public struct Table<Value: Identifiable & Sendable>: View where Value.ID: Hashab
/// Whether the table is disabled.
var isDisabled: Bool
/// The maximum number of visible rows (nil = use available height).
let maxVisibleRows: Int?
/// The placeholder text shown when the table is empty.
let emptyPlaceholder: String
@@ -87,7 +84,6 @@ public struct Table<Value: Identifiable & Sendable>: View where Value.ID: Hashab
selectionMode: selectionMode,
focusID: focusID,
isDisabled: isDisabled,
maxVisibleRows: maxVisibleRows,
emptyPlaceholder: emptyPlaceholder,
columnSpacing: columnSpacing
)
@@ -103,7 +99,7 @@ extension Table {
/// - data: The data items to display.
/// - selection: A binding to the selected item's ID (nil = no selection).
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - columnSpacing: Spacing between columns (default: 2).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - columns: A builder that defines the table columns.
@@ -111,7 +107,7 @@ extension Table {
_ data: [Value],
selection: Binding<Value.ID?>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
columnSpacing: Int = 2,
emptyPlaceholder: String = "No items",
@TableColumnBuilder<Value> columns: () -> [TableColumn<Value>]
@@ -122,7 +118,7 @@ extension Table {
self.multiSelection = nil
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.columnSpacing = columnSpacing
self.emptyPlaceholder = emptyPlaceholder
}
@@ -137,7 +133,7 @@ extension Table {
/// - data: The data items to display.
/// - selection: A binding to the set of selected item IDs.
/// - focusID: The unique focus identifier (default: auto-generated).
/// - maxVisibleRows: Maximum visible rows (default: nil = available height).
/// - columnSpacing: Spacing between columns (default: 2).
/// - emptyPlaceholder: Placeholder text when empty (default: "No items").
/// - columns: A builder that defines the table columns.
@@ -145,7 +141,7 @@ extension Table {
_ data: [Value],
selection: Binding<Set<Value.ID>>,
focusID: String? = nil,
maxVisibleRows: Int? = nil,
columnSpacing: Int = 2,
emptyPlaceholder: String = "No items",
@TableColumnBuilder<Value> columns: () -> [TableColumn<Value>]
@@ -156,7 +152,7 @@ extension Table {
self.multiSelection = selection
self.focusID = focusID
self.isDisabled = false
self.maxVisibleRows = maxVisibleRows
self.columnSpacing = columnSpacing
self.emptyPlaceholder = emptyPlaceholder
}
@@ -187,7 +183,6 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
let selectionMode: SelectionMode
let focusID: String?
let isDisabled: Bool
let maxVisibleRows: Int?
let emptyPlaceholder: String
let columnSpacing: Int
@@ -220,7 +215,7 @@ private struct _TableCore<Value: Identifiable & Sendable>: View, Renderable wher
} else {
// Calculate viewport height
let availableHeight = context.availableHeight
let viewportHeight = maxVisibleRows ?? max(1, availableHeight - 6) // Reserve for border + header + indicators
let viewportHeight = max(1, availableHeight - 6) // Reserve for border + header + indicators
// Get or create persistent focusID
let focusIDKey = StateStorage.StateKey(identity: context.identity, propertyIndex: 1)
+2 -4
View File
@@ -51,8 +51,7 @@ struct ListPage: View {
HStack(spacing: 2) {
List(
"Single Selection",
selection: $singleSelection,
maxVisibleRows: 6
selection: $singleSelection
) {
ForEach(FileItem.sampleFiles) { file in
HStack(spacing: 1) {
@@ -64,8 +63,7 @@ struct ListPage: View {
List(
"Multi Selection",
selection: $multiSelection,
maxVisibleRows: 6
selection: $multiSelection
) {
ForEach(FileItem.sampleFiles) { file in
HStack(spacing: 1) {
+2 -4
View File
@@ -54,8 +54,7 @@ struct TablePage: View {
.foregroundStyle(.palette.foregroundSecondary)
Table(
FileEntry.sampleFiles,
selection: $singleSelection,
maxVisibleRows: 6
selection: $singleSelection
) {
TableColumn("Name", value: \FileEntry.name)
TableColumn("Size", value: \FileEntry.size)
@@ -71,8 +70,7 @@ struct TablePage: View {
.foregroundStyle(.palette.foregroundSecondary)
Table(
FileEntry.sampleFiles,
selection: $multiSelection,
maxVisibleRows: 4
selection: $multiSelection
) {
TableColumn("Name", value: \FileEntry.name)
TableColumn("Type", value: \FileEntry.type)
+5 -5
View File
@@ -132,12 +132,11 @@ struct ListRenderingTests {
@Test("Scroll indicators appear when needed")
func scrollIndicatorsAppear() {
let context = createTestContext(height: 5)
struct Item: Identifiable {
let id: Int
let name: String
}
// Create list with more items than will fit in available height
let items = (0..<20).map { Item(id: $0, name: "Item \($0)") }
var selection: Int?
@@ -145,18 +144,19 @@ struct ListRenderingTests {
selection: Binding(
get: { selection },
set: { selection = $0 }
),
maxVisibleRows: 3
)
) {
ForEach(items) { item in
Text(item.name)
}
}
// Use a small height context so scrolling is triggered
let context = createTestContext(width: 40, height: 8)
let buffer = renderToBuffer(list, context: context)
let content = buffer.lines.joined()
// Should have "more below" indicator
// Should have "more below" indicator since we have 20 items in height 8
#expect(content.contains("") || content.contains("more below"))
}