diff --git a/README.md b/README.md index aebb85b..07efa61 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,29 @@ Icons accept platform enum overrides in Blade, matching the fluent API: ``` +## Lists + +The `native:list` element supports an optional end-reached buffer for loading more items before the user reaches the end of the list. + +```blade + +``` + +A buffer of `5` starts the existing `on-end-reached` callback when the list reaches the configured buffer near the end. The default buffer is `3` when the attribute is omitted. + +The same option is available through the fluent API: + +```php +use Native\Mobile\UI\Elements\NativeList; + +NativeList::make() + ->endReachedBuffer(5) + ->onEndReached('loadMore'); +``` + ## Accessibility Every element accepts a screen-reader label and an optional hint, via Blade diff --git a/resources/android/ListRenderer.kt b/resources/android/ListRenderer.kt index ef1bb81..0a9d200 100644 --- a/resources/android/ListRenderer.kt +++ b/resources/android/ListRenderer.kt @@ -59,19 +59,20 @@ object ListRenderer { val separator = node.props.getBool("separator") 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) val scrollState = rememberLazyListState() val isRefreshing = remember { mutableStateOf(false) } val endReachedFired = remember { mutableStateOf(false) } - // Detect end reached — fire when within 3 items of the bottom + // Detect end reached — fire when within the configured number of items of the bottom if (onEndReachedCb != 0) { LaunchedEffect(scrollState) { snapshotFlow { val info = scrollState.layoutInfo val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0 val total = info.totalItemsCount - total > 0 && lastVisible >= total - 3 + total > 0 && lastVisible >= total - endReachedBuffer }.collect { nearEnd -> if (nearEnd && !endReachedFired.value) { endReachedFired.value = true diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index 3611017..647f034 100644 --- a/resources/ios/NativeUIListRenderer.swift +++ b/resources/ios/NativeUIListRenderer.swift @@ -34,6 +34,7 @@ struct NativeUIListRenderer: View { let separator = node.props.getBool("separator") 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)) let nodeId = node.id let children = node.children @@ -68,7 +69,8 @@ struct NativeUIListRenderer: View { rowView(row, separator: separator, isLastInSection: index == child.children.count - 1) .onAppear { fireEndReached(rowId: row.id, leafIndex: leafIndex, - leafCount: leafCount, cb: onEndReachedCb, nodeId: nodeId) + leafCount: leafCount, buffer: endReachedBuffer, + cb: onEndReachedCb, nodeId: nodeId) } } } header: { @@ -80,7 +82,8 @@ struct NativeUIListRenderer: View { rowView(child, separator: separator) .onAppear { fireEndReached(rowId: child.id, leafIndex: leafIndex, - leafCount: leafCount, cb: onEndReachedCb, nodeId: nodeId) + leafCount: leafCount, buffer: endReachedBuffer, + cb: onEndReachedCb, nodeId: nodeId) } } } @@ -149,11 +152,12 @@ struct NativeUIListRenderer: View { } } - /// Fire the end-reached callback when a row within the last 3 leaf rows - /// appears. Works across sections via the precomputed leaf index. - private func fireEndReached(rowId: Int, leafIndex: [Int: Int], leafCount: Int, cb: Int, nodeId: Int) { + /// 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, + buffer: Int, cb: Int, nodeId: Int) { guard cb != 0, let gi = leafIndex[rowId] else { return } - if gi >= leafCount - 3 { + if gi >= leafCount - buffer { NativeElementBridge.sendPressEvent(cb, nodeId: nodeId) } } diff --git a/src/Elements/NativeList.php b/src/Elements/NativeList.php index ad3d5da..ecac078 100644 --- a/src/Elements/NativeList.php +++ b/src/Elements/NativeList.php @@ -46,6 +46,9 @@ public function applyAttributes(array $attrs): void if (isset($attrs['on-end-reached']) || isset($attrs['onEndReached'])) { $this->onEndReached($attrs['on-end-reached'] ?? $attrs['onEndReached']); } + if (isset($attrs['end-reached-buffer']) || isset($attrs['endReachedBuffer'])) { + $this->endReachedBuffer((int) ($attrs['end-reached-buffer'] ?? $attrs['endReachedBuffer'])); + } $this->applyA11yAttributes($attrs); } @@ -114,6 +117,13 @@ public function onEndReached(string $method): static return $this; } + public function endReachedBuffer(int $items): static + { + $this->listProps['end_reached_buffer'] = max(1, $items); + + return $this; + } + protected function resolveProps(CallbackRegistry $registry): array { $props = $this->listProps; diff --git a/tests/ListTest.php b/tests/ListTest.php index ffaec08..37097d4 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -71,3 +71,21 @@ function collectList(array $attrs): array expect($opaque->toArray(new CallbackRegistry)['props']['transparent'])->toBeFalse(); }); + +it('serializes the end reached buffer from attributes', function () { + $tree = collectList(['end-reached-buffer' => 5]); + + expect($tree['props']['end_reached_buffer'])->toBe(5); +}); + +it('accepts the end reached buffer on the fluent builder', function () { + $list = NativeList::make()->endReachedBuffer(5); + + expect($list->toArray(new CallbackRegistry)['props']['end_reached_buffer'])->toBe(5); +}); + +it('clamps a negative end reached buffer to one', function () { + $list = NativeList::make()->endReachedBuffer(-1); + + expect($list->toArray(new CallbackRegistry)['props']['end_reached_buffer'])->toBe(1); +});