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
5 changes: 4 additions & 1 deletion resources/ios/NativeUIListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,10 @@ private struct GroupedOrPlainListStyle: ViewModifier {
/// - `transparent` — the list sits directly on the screen's background
/// (a gradient, an image, the background layer). `bg-transparent`
/// can't express this: it packs to the same 0 as "no colour".
private struct ListBackgroundModifier: ViewModifier {
///
/// Shared with `NativeUIVirtualListRenderer`, so both lists follow one set
/// of background rules.
struct ListBackgroundModifier: ViewModifier {
let node: NativeUINode
@Environment(\.colorScheme) private var colorScheme

Expand Down
17 changes: 14 additions & 3 deletions resources/ios/NativeUIVirtualListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import Combine

/// Windowed list. The native side renders `count` logical slots in a
/// lazy `List`; PHP only ships the rows inside `window_from..window_to`.
/// Slots outside the window display a fixed-height skeleton so SwiftUI's
/// Slots outside the window display a fixed-height placeholder so SwiftUI's
/// lazy machinery can compute total scroll extent without allocating a
/// real subtree per row.
///
/// The background follows `native:list`'s rules (`ListBackgroundModifier`):
/// a `bg-*` colour, a gradient or `transparent` replaces the system
/// background, and rows are clear so it shows through them.
///
/// As the visible range moves, the tracker debounces onAppear/onDisappear
/// events and (with hysteresis) fires `on_window_change(from, to)` to
/// PHP only when the visible range is approaching the edge of the data
Expand Down Expand Up @@ -47,6 +51,7 @@ struct NativeUIVirtualListRenderer: View {
nodeId: nodeId,
rowByIndex: rowByIndex
)
.modifier(ListBackgroundModifier(node: node))
// Environment-propagating, so it reaches the List inside the body
// without threading another property through it.
.scrollIndicators(node.props.getBool("shows_indicators", default: true) ? .automatic : .hidden)
Expand Down Expand Up @@ -79,13 +84,19 @@ private struct VirtualListBody: View {
if let child = rowByIndex[index] {
NodeView(node: child).equatable()
} else {
// Placeholder skeleton — meaningless to VoiceOver.
Color(.systemGray6)
// Clear, like `native:list`'s placeholder rows: the
// list's own background shows through, so a fast
// scroll past the window never flashes a fill that
// isn't the screen's. Meaningless to VoiceOver.
Color.clear
.frame(height: estimatedRowHeight)
.accessibilityHidden(true)
}
}
.listRowInsets(EdgeInsets())
// Clear, as in a plain `native:list`: SwiftUI's opaque
// system row fill would paint over the list's background.
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.onAppear { tracker.markVisible(index) }
.onDisappear { tracker.markHidden(index) }
Expand Down
16 changes: 16 additions & 0 deletions src/Elements/NativeVirtualList.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public function applyAttributes(array $attrs): void
if (isset($attrs['showsIndicators']) || isset($attrs['shows-indicators'])) {
$this->showsIndicators((bool) ($attrs['showsIndicators'] ?? $attrs['shows-indicators']));
}
if (! empty($attrs['transparent'])) {
$this->transparent();
}
$cb = $attrs['on_window_change'] ?? $attrs['onWindowChange'] ?? $attrs['on-window-change'] ?? null;
if ($cb !== null) {
$this->windowCallback = $cb;
Expand All @@ -72,6 +75,19 @@ public function showsIndicators(bool $value = true): static
return $this;
}

/**
* Let the screen behind the list show through, as on `native:list`:
* SwiftUI's `List` paints the system background, and `bg-transparent`
* packs to the same value as "no colour". iOS-only in effect:
* Compose's LazyColumn draws no background of its own.
*/
public function transparent(bool $value = true): static
{
$this->listProps['transparent'] = $value;

return $this;
}

protected function resolveProps(CallbackRegistry $registry): array
{
$props = $this->listProps;
Expand Down
10 changes: 10 additions & 0 deletions tests/CollectorElementsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@
expect($tree['props']['shows_indicators'])->toBeFalse();
});

it('marks a virtual list transparent like a list', function () {
NativeElementCollector::leaf('virtual_list', ['count' => 100]);
expect(NativeElementCollector::collect()->toArray(new CallbackRegistry)['props'])->not->toHaveKey('transparent');

NativeElementCollector::reset();
NativeElementCollector::leaf('virtual_list', ['count' => 100, 'transparent' => true]);
expect(NativeElementCollector::collect()->toArray(new CallbackRegistry)['props']['transparent'])->toBeTrue()
->and(NativeVirtualList::make()->transparent()->toArray(new CallbackRegistry)['props']['transparent'])->toBeTrue();
});

it('applies checkbox props', function () {
NativeElementCollector::leaf('checkbox', [
'value' => true,
Expand Down
Loading