diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index 71008ae..474d98c 100644 --- a/resources/ios/NativeUIListRenderer.swift +++ b/resources/ios/NativeUIListRenderer.swift @@ -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 diff --git a/resources/ios/NativeUIVirtualListRenderer.swift b/resources/ios/NativeUIVirtualListRenderer.swift index f821483..1a36cb2 100644 --- a/resources/ios/NativeUIVirtualListRenderer.swift +++ b/resources/ios/NativeUIVirtualListRenderer.swift @@ -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 @@ -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) @@ -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) } diff --git a/src/Elements/NativeVirtualList.php b/src/Elements/NativeVirtualList.php index c5d736d..aff3ab8 100644 --- a/src/Elements/NativeVirtualList.php +++ b/src/Elements/NativeVirtualList.php @@ -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; @@ -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; diff --git a/tests/CollectorElementsTest.php b/tests/CollectorElementsTest.php index 18f4533..070ba6a 100644 --- a/tests/CollectorElementsTest.php +++ b/tests/CollectorElementsTest.php @@ -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,