Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions resources/android/ListRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ object ListRenderer {
fun Render(node: NativeUINode, modifier: Modifier) {
val horizontal = node.props.getBool("horizontal")
val separator = node.props.getBool("separator")
val rowColor = node.props.getColor("row_color", 0)
val rowBackground = if (rowColor != 0) Color(rowColor) else null
val onRefreshCb = node.props.getCallbackId("on_refresh")
val onEndReachedCb = node.props.getCallbackId("on_end_reached")
val endReachedBuffer = node.props.getInt("end_reached_buffer", 3).coerceAtLeast(1)
Expand Down Expand Up @@ -133,9 +135,10 @@ object ListRenderer {
SectionRow(
isFirst = i == 0,
isLast = i == child.children.size - 1,
background = rowBackground,
) { ListRow(row) }
} else {
ListRow(row)
ListRow(row, rowBackground)
if (separator && i < child.children.size - 1) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
Expand All @@ -147,7 +150,7 @@ object ListRenderer {
}
} else {
item(key = child.id) {
ListRow(child)
ListRow(child, rowBackground)
if (separator && index < node.children.size - 1) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
}
Expand Down Expand Up @@ -192,7 +195,7 @@ object ListRenderer {
* destructive trailing action when no structured actions are present.
*/
@Composable
private fun ListRow(child: NativeUINode) {
private fun ListRow(child: NativeUINode, background: Color? = null) {
val legacyDeleteCb = child.props.getCallbackId("on_swipe_delete")
val leading = decodeSwipeActions(child.props.getString("leading_actions_json", ""))
val trailing = decodeSwipeActions(child.props.getString("trailing_actions_json", ""))
Expand All @@ -208,8 +211,18 @@ private fun ListRow(child: NativeUINode) {
trailing = effectiveTrailing,
onAction = { cb -> NativeElementBridge.sendPressEvent(cb, child.id) }
) {
NodeView(node = child)
RowContent(child, background)
}
} else {
RowContent(child, background)
}
}

/** The row's node, on the list's row colour when it declares one. */
@Composable
private fun RowContent(child: NativeUINode, background: Color?) {
if (background != null) {
Box(Modifier.fillMaxWidth().background(background)) { NodeView(node = child) }
} else {
NodeView(node = child)
}
Expand Down Expand Up @@ -258,7 +271,7 @@ private fun SectionGap() {
* look SwiftUI's `.insetGrouped` gives for free.
*/
@Composable
private fun SectionRow(isFirst: Boolean, isLast: Boolean, content: @Composable () -> Unit) {
private fun SectionRow(isFirst: Boolean, isLast: Boolean, background: Color? = null, content: @Composable () -> Unit) {
val radius = 12.dp
val shape = RoundedCornerShape(
topStart = if (isFirst) radius else 0.dp,
Expand All @@ -271,7 +284,7 @@ private fun SectionRow(isFirst: Boolean, isLast: Boolean, content: @Composable (
Modifier
.fillMaxWidth()
.clip(shape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.background(background ?: MaterialTheme.colorScheme.surfaceVariant)
) {
content()
if (!isLast) {
Expand Down
25 changes: 17 additions & 8 deletions resources/ios/NativeUIListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct NativeUIListRenderer: View {
let horizontal = node.props.getBool("horizontal")
let showsIndicators = node.props.getBool("shows_indicators", default: true)
let separator = node.props.getBool("separator")
let rowColor = node.props.getColor("row_color", default: 0)
let onRefreshCb = node.props.getCallbackId("on_refresh")
let onEndReachedCb = node.props.getCallbackId("on_end_reached")
let endReachedBuffer = max(1, node.props.getInt("end_reached_buffer", default: 3))
Expand Down Expand Up @@ -66,7 +67,7 @@ struct NativeUIListRenderer: View {
let footer = child.props.getString("footer", default: "")
Section {
ForEach(Array(child.children.enumerated()), id: \.element.id) { index, row in
rowView(row, separator: separator, isLastInSection: index == child.children.count - 1)
rowView(row, separator: separator, rowColor: rowColor, grouped: grouped, isLastInSection: index == child.children.count - 1)
.onAppear {
fireEndReached(rowId: row.id, leafIndex: leafIndex,
leafCount: leafCount, buffer: endReachedBuffer,
Expand All @@ -79,7 +80,7 @@ struct NativeUIListRenderer: View {
if !footer.isEmpty { Text(footer).nuiScaledFont(size: 13) }
}
} else {
rowView(child, separator: separator)
rowView(child, separator: separator, rowColor: rowColor, grouped: grouped)
.onAppear {
fireEndReached(rowId: child.id, leafIndex: leafIndex,
leafCount: leafCount, buffer: endReachedBuffer,
Expand All @@ -106,7 +107,7 @@ struct NativeUIListRenderer: View {
/// One list row: the node plus its leading/trailing swipe actions and
/// row-level styling. Shared by both flat rows and section children.
@ViewBuilder
private func rowView(_ child: NativeUINode, separator: Bool, isLastInSection: Bool = false) -> some View {
private func rowView(_ child: NativeUINode, separator: Bool, rowColor: Int = 0, grouped: Bool = false, isLastInSection: Bool = false) -> some View {
// Legacy single-action API.
let legacyDeleteCb = child.props.getCallbackId("on_swipe_delete")
// New multi-action API.
Expand All @@ -117,11 +118,12 @@ struct NativeUIListRenderer: View {
.equatable()
.frame(maxWidth: .infinity, alignment: .leading)
.listRowInsets(EdgeInsets())
// Rows carry SwiftUI's opaque system fill, which paints over the
// list's own background — so hiding the scroll background via
// ListBackgroundModifier still left a white sheet on a themed
// screen. Clear the row and let its content decide.
.listRowBackground(Color.clear)
// Plain rows are cleared: SwiftUI's opaque system fill paints
// over the list's own background, leaving a white sheet on a
// themed screen. Grouped rows keep a card — the system grouped
// cell colour, which is what draws the rounded inset-grouped
// sections — and `row_color` themes either.
.listRowBackground(Self.rowBackground(rowColor: rowColor, grouped: grouped))
// Drive dividers from the bottom edge only; always hide the top
// edge. The top separator renders solely on a section's first row,
// so hiding it removes the stray full-width line that otherwise
Expand Down Expand Up @@ -152,6 +154,13 @@ struct NativeUIListRenderer: View {
}
}

/// A row's fill: the list's `row_color` when set, else the system card
/// colour in the grouped style and nothing in the plain one.
private static func rowBackground(rowColor: Int, grouped: Bool) -> Color {
if rowColor != 0 { return Color(argb: rowColor) }
return grouped ? Color(uiColor: .secondarySystemGroupedBackground) : .clear
}

/// Fire the end-reached callback when a row within the configured number
/// of leaf rows appears. Works across sections via the precomputed leaf index.
private func fireEndReached(rowId: Int, leafIndex: [Int: Int], leafCount: Int,
Expand Down
20 changes: 20 additions & 0 deletions src/Elements/NativeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use Native\Mobile\Edge\CallbackRegistry;
use Native\Mobile\Edge\Element;
use Native\Mobile\UI\Concerns\ResolvesColorValues;

class NativeList extends Element
{
use ResolvesColorValues;

protected string $type = 'list';

protected array $listProps = [];
Expand Down Expand Up @@ -40,6 +43,10 @@ public function applyAttributes(array $attrs): void
if (! empty($attrs['transparent'])) {
$this->transparent();
}
$rowColor = $attrs['row-color'] ?? $attrs['rowColor'] ?? null;
if (is_string($rowColor) && $rowColor !== '') {
$this->rowColor($rowColor);
}
if (isset($attrs['on-refresh']) || isset($attrs['onRefresh'])) {
$this->onRefresh($attrs['on-refresh'] ?? $attrs['onRefresh']);
}
Expand Down Expand Up @@ -103,6 +110,19 @@ public function transparent(bool $value = true): static
return $this;
}

/**
* Fill every row with one colour: the white cards of an inset-grouped
* settings list (iOS Settings) or opaque rows on a plain one. Rows are
* otherwise clear, so the screen background shows through them. Takes
* a hex value or a theme token (`theme('surface')`).
*/
public function rowColor(string $color): static
{
$this->listProps['row_color'] = $this->resolveColorValue($color);

return $this;
}

public function onRefresh(string $method): static
{
$this->refreshCallback = $method;
Expand Down
15 changes: 15 additions & 0 deletions tests/ListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ function collectList(array $attrs): array
->and($tree['props']['separator'])->toBeTrue();
});

it('paints rows with a row colour', function () {
$tree = collectList(['row-color' => '#FFFFFF']);

expect($tree['props']['row_color'])->toBe('#FFFFFF');
});

it('accepts the camelCase rowColor spelling and the fluent builder', function () {
expect(collectList(['rowColor' => '#FFF8EE'])['props']['row_color'])->toBe('#FFF8EE')
->and(NativeList::make()->rowColor('#123456')->toArray(new CallbackRegistry)['props']['row_color'])->toBe('#123456');
});

it('leaves rows clear without a row colour', function () {
expect(collectList(['row-color' => ''])['props'] ?? [])->not->toHaveKey('row_color');
});

it('exposes transparent on the fluent builder', function () {
$list = NativeList::make()->transparent();

Expand Down
Loading