Skip to content

Commit f8e15fd

Browse files
authored
feat(ListView): sticky headers, sectioned data, optional search bar with auto-hide (#10790)
1 parent 65c8163 commit f8e15fd

File tree

14 files changed

+3385
-186
lines changed

14 files changed

+3385
-186
lines changed

apps/automated/src/ui/list-view/list-view-tests.ts

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as TKUnit from '../../tk-unit';
22
import * as helper from '../../ui-helper';
33
import { UITest } from '../../ui-test';
4-
import { isAndroid, Page, View, KeyedTemplate, Utils, Observable, EventData, ObservableArray, Label, Application, ListView, ItemEventData } from '@nativescript/core';
4+
import { isAndroid, Page, View, KeyedTemplate, Utils, Observable, EventData, ObservableArray, Label, Application, ListView, ItemEventData, StackLayout } from '@nativescript/core';
55
import { MyButton, MyStackLayout } from '../layouts/layout-helper';
66

77
// >> article-item-tap
@@ -369,9 +369,10 @@ export class ListViewTest extends UITest<ListView> {
369369
}
370370

371371
public test_loadMoreItems_raised_when_showing_few_items() {
372-
var listView = this.testView;
372+
this.setUp();
373+
const listView = this.testView;
373374

374-
var loadMoreItemsCount = 0;
375+
let loadMoreItemsCount = 0;
375376
listView.items = FEW_ITEMS;
376377
listView.on(ListView.itemLoadingEvent, this.loadViewWithItemNumber);
377378
// >> article-loadmoreitems-event
@@ -751,10 +752,136 @@ export class ListViewTest extends UITest<ListView> {
751752
listView.scrollToIndex(10000);
752753
}
753754

755+
// Sticky header sanity tests
756+
public test_stickyHeader_iOS_sectioned_headers_basic() {
757+
if (!__APPLE__) {
758+
return;
759+
}
760+
761+
this.setUp();
762+
763+
const listView = this.testView;
764+
listView.sectioned = true;
765+
listView.stickyHeader = true;
766+
listView.stickyHeaderTemplate = "<Label id='headerLabel' text='{{ title }}' />";
767+
768+
const items = [
769+
{ title: 'Section A', items: [1, 2, 3] },
770+
{ title: 'Section B', items: [4, 5] },
771+
];
772+
listView.items = items;
773+
774+
// Ensure layout
775+
this.waitUntilTestElementIsLoaded();
776+
this.waitUntilTestElementLayoutIsValid();
777+
778+
const table = <UITableView>listView.ios;
779+
TKUnit.assertEqual(table.numberOfSections, 2, 'iOS sticky headers should use sections');
780+
781+
// Default auto height is ~44; ensure > 0
782+
const rect0 = table.rectForHeaderInSection(0);
783+
TKUnit.assert(rect0.size.height > 0, 'header height > 0');
784+
785+
// Template binding sanity: force-create header view via delegate and read label text
786+
const header0 = (<any>table.delegate).tableViewViewForHeaderInSection(table, 0);
787+
const headerText0 = this.getTextFromNativeHeaderForSection(listView, header0);
788+
TKUnit.assertEqual(headerText0, 'Section A', 'iOS header 0 text');
789+
790+
// Respect explicit stickyHeaderHeight
791+
listView.stickyHeaderHeight = 60;
792+
listView.refresh();
793+
TKUnit.wait(0.05);
794+
const rect0b = table.rectForHeaderInSection(0);
795+
// iOS reports in points; allow small variance
796+
TKUnit.assert(Math.abs(rect0b.size.height - 60) <= 1, 'explicit header height ~60');
797+
}
798+
799+
public test_stickyHeader_Android_header_updates_and_padding() {
800+
if (!isAndroid) {
801+
return;
802+
}
803+
804+
this.setUp();
805+
806+
const listView = this.testView;
807+
listView.sectioned = true;
808+
listView.stickyHeader = true;
809+
listView.stickyHeaderTemplate = "<Label id='headerLabel' text='{{ title }}' />";
810+
listView.items = [
811+
{ title: 'First', items: ['a', 'b', 'c'] },
812+
{ title: 'Second', items: ['d', 'e'] },
813+
];
814+
815+
this.waitUntilTestElementIsLoaded();
816+
this.waitUntilTestElementLayoutIsValid();
817+
TKUnit.waitUntilReady(() => !!(<any>listView)._stickyHeaderView);
818+
819+
// Sticky header view exists and binds
820+
const sticky = (<any>listView)._stickyHeaderView;
821+
TKUnit.assert(!!sticky, 'sticky header view exists');
822+
const text0 = this.getStickyHeaderTextAndroid(listView);
823+
TKUnit.assertEqual(text0, 'First', 'Android sticky header initial text');
824+
825+
// ListView should have top padding to avoid content under header
826+
const topPad = (<android.widget.ListView>listView.android).getPaddingTop();
827+
TKUnit.assert(topPad > 0, 'ListView has top padding for sticky header');
828+
829+
// Update header to next section (simulate scroll)
830+
if ((<any>listView)._updateStickyHeader) {
831+
(<any>listView)._updateStickyHeader(1);
832+
TKUnit.wait(0.05);
833+
const text1 = this.getStickyHeaderTextAndroid(listView);
834+
TKUnit.assertEqual(text1, 'Second', 'Android sticky header updated text');
835+
}
836+
}
837+
754838
private checkItemVisibleAtIndex(listView: ListView, index: number): boolean {
755839
return listView.isItemAtIndexVisible(index);
756840
}
757841

842+
private getTextFromNativeHeaderForSection(listView: ListView, headerView: any): string {
843+
if (__APPLE__ && headerView && headerView.contentView && headerView.contentView.subviews) {
844+
// subviews can be function or array-like depending on runtime bridge
845+
try {
846+
if (Utils.isFunction(headerView.contentView.subviews)) {
847+
const sv = headerView.contentView.subviews();
848+
return sv && sv.length ? sv[0].text + '' : '';
849+
} else {
850+
return headerView.contentView.subviews[0].text + '';
851+
}
852+
} catch (e) {
853+
return '';
854+
}
855+
}
856+
857+
return '';
858+
}
859+
860+
private getStickyHeaderTextAndroid(listView: ListView): string {
861+
if (isAndroid) {
862+
const headerView = (<any>listView)._stickyHeaderView as StackLayout;
863+
if (!headerView) {
864+
return '';
865+
}
866+
if (headerView instanceof Label) {
867+
return headerView.text + '';
868+
}
869+
if (headerView.getChildAt) {
870+
const child = headerView.getChildAt(0) as StackLayout;
871+
if (child instanceof Label) {
872+
return child.text + '';
873+
}
874+
if (child?.getChildAt) {
875+
const gchild = child.getChildAt(0);
876+
if (gchild instanceof Label) {
877+
return gchild.text + '';
878+
}
879+
}
880+
}
881+
}
882+
return '';
883+
}
884+
758885
private assertNoMemoryLeak(weakRef: WeakRef<ListView>) {
759886
this.tearDown();
760887
//

apps/toolbox/src/main-page.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page" androidOverflowEdge="bottom" statusBarStyle="dark">
22
<Page.actionBar>
3-
<ActionBar title="Dev Toolbox" icon="" class="action-bar" iosLargeTitle="true" iosShadow="false">
3+
<ActionBar title="Dev Toolbox" icon="" class="action-bar">
44
</ActionBar>
55
</Page.actionBar>
66
<StackLayout>
@@ -18,6 +18,7 @@
1818
<Button text="image-handling" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
1919
<Button text="labels" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2020
<Button text="list-page" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
21+
<Button text="list-page-sticky" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2122
<Button text="multiple-scenes" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2223
<Button text="root-layout" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
2324
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />

0 commit comments

Comments
 (0)