From 7eb7a7fd7b303d145567f490263d80079a170cf3 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:30:51 +0530 Subject: [PATCH 01/14] Add configurable list end reached threshold --- src/Elements/NativeList.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Elements/NativeList.php b/src/Elements/NativeList.php index ad3d5da..c3ef55f 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-threshold']) || isset($attrs['endReachedThreshold'])) { + $this->endReachedThreshold((int) ($attrs['end-reached-threshold'] ?? $attrs['endReachedThreshold'])); + } $this->applyA11yAttributes($attrs); } From cb9ebc2b0f1700f5f80fa3a6ce61f606a9389c3e Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:30:57 +0530 Subject: [PATCH 02/14] Add configurable list end reached threshold --- resources/android/ListRenderer.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/android/ListRenderer.kt b/resources/android/ListRenderer.kt index ef1bb81..f033129 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 endReachedThreshold = node.props.getInt("end_reached_threshold", 3).coerceAtLeast(0) 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 - endReachedThreshold }.collect { nearEnd -> if (nearEnd && !endReachedFired.value) { endReachedFired.value = true From 2a8daecddc5df239276e7b09d6ae0e63d7a5f0e9 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:31:01 +0530 Subject: [PATCH 03/14] Add configurable list end reached threshold --- resources/ios/NativeUIListRenderer.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index 3611017..662baac 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 endReachedThreshold = max(0, node.props.getInt("end_reached_threshold", 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, threshold: endReachedThreshold, + 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, threshold: endReachedThreshold, + 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, + threshold: Int, cb: Int, nodeId: Int) { guard cb != 0, let gi = leafIndex[rowId] else { return } - if gi >= leafCount - 3 { + if gi >= leafCount - threshold { NativeElementBridge.sendPressEvent(cb, nodeId: nodeId) } } From 53290daf04523719ff24ddf515c8b0732ed9b138 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:31:05 +0530 Subject: [PATCH 04/14] Test list end reached threshold --- tests/ListTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/ListTest.php b/tests/ListTest.php index ffaec08..ecc1aaf 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -71,3 +71,22 @@ function collectList(array $attrs): array expect($opaque->toArray(new CallbackRegistry)['props']['transparent'])->toBeFalse(); }); + + +it('serializes the end reached threshold from attributes', function () { + $tree = collectList(['end-reached-threshold' => 5]); + + expect($tree['props']['end_reached_threshold'])->toBe(5); +}); + +it('accepts the end reached threshold on the fluent builder', function () { + $list = NativeList::make()->endReachedThreshold(5); + + expect($list->toArray(new CallbackRegistry)['props']['end_reached_threshold'])->toBe(5); +}); + +it('clamps a negative end reached threshold to zero', function () { + $list = NativeList::make()->endReachedThreshold(-1); + + expect($list->toArray(new CallbackRegistry)['props']['end_reached_threshold'])->toBe(0); +}); From 5213ae3a9e34ae077fe318ee215e3e10cdd7bf1f Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:57:39 +0530 Subject: [PATCH 05/14] Add configurable list end reached buffer --- src/Elements/NativeList.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Elements/NativeList.php b/src/Elements/NativeList.php index c3ef55f..ecac078 100644 --- a/src/Elements/NativeList.php +++ b/src/Elements/NativeList.php @@ -46,8 +46,8 @@ 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-threshold']) || isset($attrs['endReachedThreshold'])) { - $this->endReachedThreshold((int) ($attrs['end-reached-threshold'] ?? $attrs['endReachedThreshold'])); + if (isset($attrs['end-reached-buffer']) || isset($attrs['endReachedBuffer'])) { + $this->endReachedBuffer((int) ($attrs['end-reached-buffer'] ?? $attrs['endReachedBuffer'])); } $this->applyA11yAttributes($attrs); @@ -117,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; From 4b850f0aedeaa586be40c32699b0b194d65c05bd Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:57:43 +0530 Subject: [PATCH 06/14] Rename list end reached threshold to buffer --- resources/android/ListRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/android/ListRenderer.kt b/resources/android/ListRenderer.kt index f033129..4ab702a 100644 --- a/resources/android/ListRenderer.kt +++ b/resources/android/ListRenderer.kt @@ -59,7 +59,7 @@ object ListRenderer { val separator = node.props.getBool("separator") val onRefreshCb = node.props.getCallbackId("on_refresh") val onEndReachedCb = node.props.getCallbackId("on_end_reached") - val endReachedThreshold = node.props.getInt("end_reached_threshold", 3).coerceAtLeast(0) + val endReachedBuffer = node.props.getInt("end_reached_buffer", 3).coerceAtLeast(1) val scrollState = rememberLazyListState() val isRefreshing = remember { mutableStateOf(false) } From fdb8b40fecfec12792c24dc6f918648a687c5e07 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:57:47 +0530 Subject: [PATCH 07/14] Rename list end reached threshold to buffer --- resources/ios/NativeUIListRenderer.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index 662baac..928a4d2 100644 --- a/resources/ios/NativeUIListRenderer.swift +++ b/resources/ios/NativeUIListRenderer.swift @@ -34,7 +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 endReachedThreshold = max(0, node.props.getInt("end_reached_threshold", default: 3)) + let endReachedBuffer = max(1, node.props.getInt("end_reached_buffer", default: 3)) let nodeId = node.id let children = node.children @@ -155,9 +155,9 @@ struct NativeUIListRenderer: View { /// 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, - threshold: Int, cb: Int, nodeId: Int) { + buffer: Int, cb: Int, nodeId: Int) { guard cb != 0, let gi = leafIndex[rowId] else { return } - if gi >= leafCount - threshold { + if gi >= leafCount - buffer { NativeElementBridge.sendPressEvent(cb, nodeId: nodeId) } } From 5a3b154420009df2b5476ea13f6214e8405450e7 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:57:52 +0530 Subject: [PATCH 08/14] Test list end reached buffer --- tests/ListTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ListTest.php b/tests/ListTest.php index ecc1aaf..0328d78 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -73,20 +73,20 @@ function collectList(array $attrs): array }); -it('serializes the end reached threshold from attributes', function () { - $tree = collectList(['end-reached-threshold' => 5]); +it('serializes the end reached buffer from attributes', function () { + $tree = collectList(['end-reached-buffer' => 5]); - expect($tree['props']['end_reached_threshold'])->toBe(5); + expect($tree['props']['end_reached_buffer'])->toBe(5); }); -it('accepts the end reached threshold on the fluent builder', function () { - $list = NativeList::make()->endReachedThreshold(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_threshold'])->toBe(5); + expect($list->toArray(new CallbackRegistry)['props']['end_reached_buffer'])->toBe(5); }); -it('clamps a negative end reached threshold to zero', function () { - $list = NativeList::make()->endReachedThreshold(-1); +it('clamps a negative end reached buffer to one', function () { + $list = NativeList::make()->endReachedBuffer(-1); - expect($list->toArray(new CallbackRegistry)['props']['end_reached_threshold'])->toBe(0); + expect($list->toArray(new CallbackRegistry)['props']['end_reached_threshold'})->toBe(1); }); From 3fbc069b5fd3d05f90c79f53e934c992c4c48c79 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:58:15 +0530 Subject: [PATCH 09/14] Fix Android list end reached buffer --- resources/android/ListRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/android/ListRenderer.kt b/resources/android/ListRenderer.kt index 4ab702a..0a9d200 100644 --- a/resources/android/ListRenderer.kt +++ b/resources/android/ListRenderer.kt @@ -72,7 +72,7 @@ object ListRenderer { val info = scrollState.layoutInfo val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: 0 val total = info.totalItemsCount - total > 0 && lastVisible >= total - endReachedThreshold + total > 0 && lastVisible >= total - endReachedBuffer }.collect { nearEnd -> if (nearEnd && !endReachedFired.value) { endReachedFired.value = true From 24224893f8014083b559e6952e0632dcb7f00662 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:58:19 +0530 Subject: [PATCH 10/14] Fix iOS list end reached buffer --- resources/ios/NativeUIListRenderer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index 928a4d2..e84fced 100644 --- a/resources/ios/NativeUIListRenderer.swift +++ b/resources/ios/NativeUIListRenderer.swift @@ -69,7 +69,7 @@ struct NativeUIListRenderer: View { rowView(row, separator: separator, isLastInSection: index == child.children.count - 1) .onAppear { fireEndReached(rowId: row.id, leafIndex: leafIndex, - leafCount: leafCount, threshold: endReachedThreshold, + leafCount: leafCount, buffer: endReachedBuffer, cb: onEndReachedCb, nodeId: nodeId) } } From 2dc94ab981323fea750a26fdbfcfd55130097fc2 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:58:23 +0530 Subject: [PATCH 11/14] Fix list end reached buffer test --- tests/ListTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ListTest.php b/tests/ListTest.php index 0328d78..782857c 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -88,5 +88,5 @@ function collectList(array $attrs): array it('clamps a negative end reached buffer to one', function () { $list = NativeList::make()->endReachedBuffer(-1); - expect($list->toArray(new CallbackRegistry)['props']['end_reached_threshold'})->toBe(1); + expect($list->toArray(new CallbackRegistry)['props']['end_reached_buffer'])->toBe(1); }); From 8aef7d32758f423f646b4a3650f8982975fedd96 Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:06:17 +0530 Subject: [PATCH 12/14] Document list end reached buffer --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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 From ae66314f7a90db8034cadb41b5217a6b136c73db Mon Sep 17 00:00:00 2001 From: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:06:30 +0530 Subject: [PATCH 13/14] Fix iOS list end reached buffer --- resources/ios/NativeUIListRenderer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/ios/NativeUIListRenderer.swift b/resources/ios/NativeUIListRenderer.swift index e84fced..647f034 100644 --- a/resources/ios/NativeUIListRenderer.swift +++ b/resources/ios/NativeUIListRenderer.swift @@ -82,7 +82,7 @@ struct NativeUIListRenderer: View { rowView(child, separator: separator) .onAppear { fireEndReached(rowId: child.id, leafIndex: leafIndex, - leafCount: leafCount, threshold: endReachedThreshold, + leafCount: leafCount, buffer: endReachedBuffer, cb: onEndReachedCb, nodeId: nodeId) } } From ee12c0e2ac3d6fcd228e3de5298bc7281bd7b0cb Mon Sep 17 00:00:00 2001 From: Punyapal Shah Date: Tue, 22 Sep 2026 22:51:29 +0530 Subject: [PATCH 14/14] lint --- tests/ListTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ListTest.php b/tests/ListTest.php index 782857c..37097d4 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -72,7 +72,6 @@ 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]);