Skip to content
Merged
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,29 @@ Icons accept platform enum overrides in Blade, matching the fluent API:
<native:icon :ios="Ios::House" :android="Android::Home" :size="24" />
```

## 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
<native:list
on-end-reached="loadMore"
:end-reached-buffer="5"
/>
```

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
Expand Down
5 changes: 3 additions & 2 deletions resources/android/ListRenderer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions resources/ios/NativeUIListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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: {
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/Elements/NativeList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions tests/ListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading