feat(ios): ListView sticky headers options

This commit is contained in:
Nathan Walker
2025-08-02 17:51:04 -07:00
parent c934d898b4
commit 783f8ed691
6 changed files with 1991 additions and 53 deletions

View File

@@ -99,6 +99,46 @@ export class ListView extends View {
*/
iosEstimatedRowHeight: CoreTypes.LengthType;
/**
* Gets or sets a value indicating whether the ListView should display sticky headers.
* When enabled, headers will remain visible at the top while scrolling through sections.
*
* @nsProperty
*/
stickyHeader: boolean;
/**
* Gets or sets the template for sticky headers.
*
* @nsProperty
*/
stickyHeaderTemplate: string | Template;
/**
* Gets or sets the height of sticky headers.
*
* @nsProperty
*/
stickyHeaderHeight: CoreTypes.LengthType;
/**
* Gets or sets a value indicating whether the ListView should show default top padding above section headers.
* When set to false (default), removes iOS default spacing for a tighter layout.
* When set to true, preserves iOS default ~4-5px spacing above section headers.
*
* @nsProperty
*/
stickyHeaderTopPadding: boolean;
/**
* Gets or sets a value indicating whether the ListView should treat items as sectioned data.
* When enabled, items array should contain objects with 'items' property for section content.
* Each section will have its own sticky header.
*
* @nsProperty
*/
sectioned: boolean;
/**
* Forces the ListView to reload all its items.
*/
@@ -230,3 +270,28 @@ export const iosEstimatedRowHeightProperty: Property<ListView, CoreTypes.LengthT
* Backing property for separator color property.
*/
export const separatorColorProperty: CssProperty<Style, Color>;
/**
* Represents the observable property backing the stickyHeader property of each ListView instance.
*/
export const stickyHeaderProperty: Property<ListView, boolean>;
/**
* Represents the sticky header template property of each ListView instance.
*/
export const stickyHeaderTemplateProperty: Property<ListView, string | Template>;
/**
* Represents the observable property backing the stickyHeaderHeight property of each ListView instance.
*/
export const stickyHeaderHeightProperty: Property<ListView, CoreTypes.LengthType>;
/**
* Represents the observable property backing the stickyHeaderTopPadding property of each ListView instance.
*/
export const stickyHeaderTopPaddingProperty: Property<ListView, boolean>;
/**
* Represents the observable property backing the sectioned property of each ListView instance.
*/
export const sectionedProperty: Property<ListView, boolean>;

View File

@@ -1,7 +1,7 @@
import { ItemEventData } from '.';
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty } from './list-view-common';
import { ListViewBase, separatorColorProperty, itemTemplatesProperty, iosEstimatedRowHeightProperty, stickyHeaderProperty, stickyHeaderTemplateProperty, stickyHeaderHeightProperty, sectionedProperty } from './list-view-common';
import { CoreTypes } from '../../core-types';
import { View, KeyedTemplate } from '../core/view';
import { View, KeyedTemplate, Template } from '../core/view';
import { Length } from '../styling/length-shared';
import { Observable, EventData } from '../../data/observable';
import { Color } from '../../color';
@@ -10,6 +10,9 @@ import { StackLayout } from '../layouts/stack-layout';
import { ProxyViewContainer } from '../proxy-view-container';
import { profile } from '../../profiling';
import { Trace } from '../../trace';
import { Builder } from '../builder';
import { Label } from '../label';
import { isFunction } from '../../utils/types';
export * from './list-view-common';
@@ -61,6 +64,41 @@ class ListViewCell extends UITableViewCell {
public owner: WeakRef<View>;
}
@NativeClass
class ListViewHeaderCell extends UITableViewHeaderFooterView {
public static initWithEmptyBackground(): ListViewHeaderCell {
const cell = <ListViewHeaderCell>ListViewHeaderCell.new();
// Clear background by default - this will make headers transparent
cell.backgroundColor = UIColor.clearColor;
return cell;
}
initWithReuseIdentifier(reuseIdentifier: string): this {
const cell = <this>super.initWithReuseIdentifier(reuseIdentifier);
// Clear background by default - this will make headers transparent
cell.backgroundColor = UIColor.clearColor;
return cell;
}
public willMoveToSuperview(newSuperview: UIView): void {
const parent = <ListView>(this.view ? this.view.parent : null);
// When inside ListView and there is no newSuperview this header is
// removed from native visual tree so we remove it from our tree too.
if (parent && !newSuperview) {
parent._removeHeaderContainer(this);
}
}
public get view(): View {
return this.owner ? this.owner.deref() : null;
}
public owner: WeakRef<View>;
}
function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eventName: string, indexPath: NSIndexPath) {
const args = <ItemEventData>{
eventName: eventName,
@@ -88,10 +126,33 @@ class DataSource extends NSObject implements UITableViewDataSource {
return dataSource;
}
public numberOfSectionsInTableView(tableView: UITableView): number {
const owner = this._owner?.deref();
if (!owner) {
return 1;
}
const sections = owner._getSectionCount();
if (Trace.isEnabled()) {
Trace.write(`ListView: numberOfSections = ${sections} (sectioned: ${owner.sectioned})`, Trace.categories.Debug);
}
return sections;
}
public tableViewNumberOfRowsInSection(tableView: UITableView, section: number) {
const owner = this._owner?.deref();
return owner && owner.items ? owner.items.length : 0;
if (!owner) {
return 0;
}
const sectionItems = owner._getItemsInSection(section);
const rowCount = sectionItems ? sectionItems.length : 0;
if (Trace.isEnabled()) {
Trace.write(`ListView: numberOfRows in section ${section} = ${rowCount}`, Trace.categories.Debug);
}
return rowCount;
}
public tableViewCellForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): UITableViewCell {
@@ -184,6 +245,55 @@ class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
return layout.toDeviceIndependentPixels(height);
}
public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView {
const owner = this._owner?.deref();
if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) {
if (Trace.isEnabled()) {
Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug);
}
return null;
}
if (Trace.isEnabled()) {
Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug);
}
const headerReuseIdentifier = 'stickyHeader';
let headerCell = <ListViewHeaderCell>tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier);
if (!headerCell) {
// Use proper iOS initialization for registered header cells
headerCell = <ListViewHeaderCell>ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier);
headerCell.backgroundColor = UIColor.clearColor;
}
owner._prepareHeader(headerCell, section);
return headerCell;
}
public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number {
const owner = this._owner?.deref();
if (!owner || !owner.stickyHeader) {
return 0;
}
let height: number;
if (owner.stickyHeaderHeight === 'auto') {
height = 44;
} else {
height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44));
}
if (Trace.isEnabled()) {
Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug);
}
return height;
}
}
@NativeClass
@@ -233,6 +343,54 @@ class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDe
return layout.toDeviceIndependentPixels(owner._effectiveRowHeight);
}
public tableViewViewForHeaderInSection(tableView: UITableView, section: number): UIView {
const owner = this._owner?.deref();
if (!owner || !owner.stickyHeader || !owner.stickyHeaderTemplate) {
if (Trace.isEnabled()) {
Trace.write(`ListView: No sticky header (stickyHeader: ${owner?.stickyHeader}, hasTemplate: ${!!owner?.stickyHeaderTemplate})`, Trace.categories.Debug);
}
return null;
}
if (Trace.isEnabled()) {
Trace.write(`ListView: Creating sticky header`, Trace.categories.Debug);
}
const headerReuseIdentifier = 'stickyHeader';
let headerCell = <ListViewHeaderCell>tableView.dequeueReusableHeaderFooterViewWithIdentifier(headerReuseIdentifier);
if (!headerCell) {
headerCell = <ListViewHeaderCell>ListViewHeaderCell.alloc().initWithReuseIdentifier(headerReuseIdentifier);
headerCell.backgroundColor = UIColor.clearColor;
}
owner._prepareHeader(headerCell, section);
return headerCell;
}
public tableViewHeightForHeaderInSection(tableView: UITableView, section: number): number {
const owner = this._owner?.deref();
if (!owner || !owner.stickyHeader) {
return 0;
}
let height: number;
if (owner.stickyHeaderHeight === 'auto') {
height = 44;
} else {
height = layout.toDeviceIndependentPixels(Length.toDevicePixels(owner.stickyHeaderHeight, 44));
}
if (Trace.isEnabled()) {
Trace.write(`ListView: Sticky header height: ${height}`, Trace.categories.Debug);
}
return height;
}
}
export class ListView extends ListViewBase {
@@ -244,11 +402,15 @@ export class ListView extends ListViewBase {
private _preparingCell: boolean;
private _isDataDirty: boolean;
private _map: Map<ListViewCell, ItemView>;
private _headerMap: Map<ListViewHeaderCell, View>;
private _preparingHeader: boolean;
private _headerTemplateCache: View;
widthMeasureSpec = 0;
constructor() {
super();
this._map = new Map<ListViewCell, ItemView>();
this._headerMap = new Map<ListViewHeaderCell, View>();
this._heights = new Array<number>();
}
@@ -260,10 +422,20 @@ export class ListView extends ListViewBase {
super.initNativeView();
const nativeView = this.nativeViewProtected;
nativeView.registerClassForCellReuseIdentifier(ListViewCell.class(), this._defaultTemplate.key);
nativeView.registerClassForHeaderFooterViewReuseIdentifier(ListViewHeaderCell.class(), 'stickyHeader');
nativeView.estimatedRowHeight = DEFAULT_HEIGHT;
nativeView.rowHeight = UITableViewAutomaticDimension;
nativeView.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this));
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
// Control section header top padding (iOS 15+)
if (nativeView.respondsToSelector('setSectionHeaderTopPadding:')) {
if (!this.stickyHeaderTopPadding) {
nativeView.sectionHeaderTopPadding = 0;
}
// When stickyHeaderTopPadding is true, don't set the property to use iOS default
}
this._setNativeClipToBounds();
}
@@ -296,13 +468,16 @@ export class ListView extends ListViewBase {
}
get _childrenCount(): number {
return this._map.size;
return this._map.size + this._headerMap.size;
}
public eachChildView(callback: (child: View) => boolean): void {
this._map.forEach((view, key) => {
callback(view);
});
this._headerMap.forEach((view, key) => {
callback(view);
});
}
public scrollToIndex(index: number) {
@@ -340,6 +515,11 @@ export class ListView extends ListViewBase {
view.bindingContext = null;
}
});
this._headerMap.forEach((view, nativeView, map) => {
if (!(view.bindingContext instanceof Observable)) {
view.bindingContext = null;
}
});
if (this.isLoaded) {
this.nativeViewProtected.reloadData();
@@ -385,8 +565,8 @@ export class ListView extends ListViewBase {
}
public requestLayout(): void {
// When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed.
if (!this._preparingCell) {
// When preparing cell or header don't call super - no need to invalidate our measure when cell/header desiredSize is changed.
if (!this._preparingCell && !this._preparingHeader) {
super.requestLayout();
}
}
@@ -409,6 +589,9 @@ export class ListView extends ListViewBase {
this._map.forEach((childView, listViewCell) => {
View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec);
});
this._headerMap.forEach((childView, listViewHeaderCell) => {
View.measureChild(this, childView, childView._currentWidthMeasureSpec, childView._currentHeightMeasureSpec);
});
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
@@ -423,6 +606,12 @@ export class ListView extends ListViewBase {
View.layoutChild(this, childView, 0, 0, width, cellHeight);
}
});
this._headerMap.forEach((childView, listViewHeaderCell) => {
const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44);
const width = layout.getMeasureSpecSize(this.widthMeasureSpec);
childView.iosOverflowSafeAreaEnabled = false;
View.layoutChild(this, childView, 0, 0, width, headerHeight);
});
}
private _layoutCell(cellView: View, indexPath: NSIndexPath): number {
@@ -445,11 +634,21 @@ export class ListView extends ListViewBase {
this._preparingCell = true;
let view: ItemView = cell.view;
if (!view) {
view = this._getItemTemplate(indexPath.row).createView();
if (this.sectioned) {
// For sectioned data, we need to calculate the absolute index for template selection
let absoluteIndex = 0;
for (let i = 0; i < indexPath.section; i++) {
absoluteIndex += this._getItemsInSection(i).length;
}
absoluteIndex += indexPath.row;
view = this._getItemTemplate(absoluteIndex).createView();
} else {
view = this._getItemTemplate(indexPath.row).createView();
}
}
const args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath);
view = args.view || this._getDefaultItemContent(indexPath.row);
view = args.view || this._getDefaultItemContent(this.sectioned ? indexPath.row : indexPath.row);
// Proxy containers should not get treated as layouts.
// Wrap them in a real layout as well.
@@ -469,8 +668,14 @@ export class ListView extends ListViewBase {
cell.owner = new WeakRef(view);
}
this._prepareItem(view, indexPath.row);
view._listViewItemIndex = indexPath.row;
if (this.sectioned) {
this._prepareItemInSection(view, indexPath.section, indexPath.row);
view._listViewItemIndex = indexPath.row; // Keep row index for compatibility
(view as any)._listViewSectionIndex = indexPath.section;
} else {
this._prepareItem(view, indexPath.row);
view._listViewItemIndex = indexPath.row;
}
this._map.set(cell, view);
// We expect that views returned from itemLoading are new (e.g. not reused).
@@ -503,6 +708,163 @@ export class ListView extends ListViewBase {
this._map.delete(cell);
}
public _prepareHeader(headerCell: ListViewHeaderCell, section: number): number {
let headerHeight: number;
try {
this._preparingHeader = true;
let view: View = headerCell.view;
if (!view) {
view = this._getHeaderTemplate();
if (!view) {
if (Trace.isEnabled()) {
Trace.write(`ListView: Failed to create header view for section ${section}`, Trace.categories.Debug);
}
// Create a fallback view
const lbl = new Label();
lbl.text = `Section ${section}`;
view = lbl;
}
}
// Handle header cell reuse
if (!headerCell.view) {
headerCell.owner = new WeakRef(view);
} else if (headerCell.view !== view) {
// Remove old view and set new one
(<UIView>headerCell.view.nativeViewProtected)?.removeFromSuperview();
this._removeHeaderContainer(headerCell);
headerCell.owner = new WeakRef(view);
}
// Clear existing binding context and set new one
if (view.bindingContext) {
view.bindingContext = null;
}
if (this.sectioned) {
const sectionData = this._getSectionData(section);
if (sectionData) {
view.bindingContext = sectionData;
} else {
// Fallback if section data is missing
view.bindingContext = { title: `Section ${section}`, section: section };
}
} else {
view.bindingContext = this.bindingContext;
}
// Force immediate binding context evaluation
if (view && typeof (view as any)._onBindingContextChanged === 'function') {
(view as any)._onBindingContextChanged(null, view.bindingContext);
// Also trigger for child views
// @ts-ignore
if (view._childrenCount) {
view.eachChildView((child) => {
if (typeof (child as any)._onBindingContextChanged === 'function') {
(child as any)._onBindingContextChanged(null, view.bindingContext);
}
return true;
});
}
}
this._headerMap.set(headerCell, view);
// Add new header view to the cell
if (view && !view.parent) {
this._addView(view);
headerCell.contentView.addSubview(view.nativeViewProtected);
}
// Request layout and measure/layout the header
if (view && view.bindingContext) {
view.requestLayout();
}
headerHeight = this._layoutHeader(view);
} finally {
this._preparingHeader = false;
}
return headerHeight;
}
private _layoutHeader(headerView: View): number {
if (headerView) {
const headerHeight = this.stickyHeaderHeight === 'auto' ? 44 : Length.toDevicePixels(this.stickyHeaderHeight, 44);
const heightMeasureSpec: number = layout.makeMeasureSpec(headerHeight, layout.EXACTLY);
const measuredSize = View.measureChild(this, headerView, this.widthMeasureSpec, heightMeasureSpec);
// Layout the header with the measured size
View.layoutChild(this, headerView, 0, 0, measuredSize.measuredWidth, measuredSize.measuredHeight);
return measuredSize.measuredHeight;
}
return 44;
}
private _getHeaderTemplate(): View {
if (this.stickyHeaderTemplate) {
if (__UI_USE_EXTERNAL_RENDERER__) {
if (isFunction(this.stickyHeaderTemplate)) {
return (<Template>this.stickyHeaderTemplate)();
}
} else {
if (typeof this.stickyHeaderTemplate === 'string') {
try {
const parsed = Builder.parse(this.stickyHeaderTemplate, this);
if (!parsed) {
// Create a simple fallback
const fallbackLabel = new Label();
fallbackLabel.text = 'Parse Failed';
return fallbackLabel;
}
return parsed;
} catch (error) {
if (Trace.isEnabled()) {
Trace.write(`ListView: Template parsing error: ${error}`, Trace.categories.Debug);
}
// Create a simple fallback
const errorLabel = new Label();
errorLabel.text = 'Template Error';
return errorLabel;
}
} else {
const view = (<Template>this.stickyHeaderTemplate)();
if (Trace.isEnabled()) {
Trace.write(`ListView: Created header view from template function: ${!!view} (type: ${view?.constructor?.name})`, Trace.categories.Debug);
}
return view;
}
}
}
if (Trace.isEnabled()) {
Trace.write(`ListView: No sticky header template, creating default`, Trace.categories.Debug);
}
// Return a default header if no template is provided
const lbl = new Label();
lbl.text = 'Default Header';
return lbl;
}
public _removeHeaderContainer(headerCell: ListViewHeaderCell): void {
const view: View = headerCell.view;
// This is to clear the StackLayout that is used to wrap ProxyViewContainer instances.
if (!(view.parent instanceof ListView)) {
this._removeView(view.parent);
}
// No need to request layout when we are removing headers.
const preparing = this._preparingHeader;
this._preparingHeader = true;
view.parent._removeView(view);
this._preparingHeader = preparing;
this._headerMap.delete(headerCell);
}
[separatorColorProperty.getDefault](): UIColor {
return this.nativeViewProtected.separatorColor;
}
@@ -533,4 +895,58 @@ export class ListView extends ListViewBase {
const estimatedHeight = layout.toDeviceIndependentPixels(Length.toDevicePixels(value, 0));
nativeView.estimatedRowHeight = estimatedHeight < 0 ? DEFAULT_HEIGHT : estimatedHeight;
}
[stickyHeaderProperty.getDefault](): boolean {
return false;
}
[stickyHeaderProperty.setNative](value: boolean) {
if (Trace.isEnabled()) {
Trace.write(`ListView: stickyHeader set to ${value}`, Trace.categories.Debug);
}
// Immediately refresh to apply changes
if (this.isLoaded) {
this.refresh();
}
}
[stickyHeaderTemplateProperty.getDefault](): string | Template {
return null;
}
[stickyHeaderTemplateProperty.setNative](value: string | Template) {
if (Trace.isEnabled()) {
Trace.write(`ListView: stickyHeaderTemplate set: ${typeof value} ${value ? '(has value)' : '(null)'}`, Trace.categories.Debug);
}
// Clear any cached template
this._headerTemplateCache = null;
// Immediately refresh to apply changes
if (this.isLoaded) {
this.refresh();
}
}
[stickyHeaderHeightProperty.getDefault](): CoreTypes.LengthType {
return 'auto';
}
[stickyHeaderHeightProperty.setNative](value: CoreTypes.LengthType) {
if (Trace.isEnabled()) {
Trace.write(`ListView: stickyHeaderHeight set to ${value}`, Trace.categories.Debug);
}
// Immediately refresh to apply changes
if (this.isLoaded) {
this.refresh();
}
}
[sectionedProperty.getDefault](): boolean {
return false;
}
[sectionedProperty.setNative](value: boolean) {
if (Trace.isEnabled()) {
Trace.write(`ListView: sectioned set to ${value}`, Trace.categories.Debug);
}
// Immediately refresh to apply changes
if (this.isLoaded) {
this.refresh();
}
}
}

View File

@@ -11,6 +11,8 @@ import { ObservableArray, ChangedData } from '../../data/observable-array';
import { addWeakEventListener, removeWeakEventListener } from '../core/weak-event-listener';
import { CoreTypes } from '../../core-types';
import { isFunction } from '../../utils/types';
import { Trace } from '../../trace';
import { booleanConverter } from '../core/view-base';
const autoEffectiveRowHeight = -1;
@@ -49,6 +51,11 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
public items: any[] | ItemsSource;
public itemTemplate: string | Template;
public itemTemplates: string | Array<KeyedTemplate>;
public stickyHeader: boolean;
public stickyHeaderTemplate: string | Template;
public stickyHeaderHeight: CoreTypes.LengthType;
public stickyHeaderTopPadding: boolean;
public sectioned: boolean;
get separatorColor(): Color {
return this.style.separatorColor;
@@ -125,12 +132,58 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
}
}
public _prepareItemInSection(item: View, section: number, index: number) {
if (item) {
item.bindingContext = this._getDataItemInSection(section, index);
}
}
private _getDataItem(index: number): any {
const thisItems = <ItemsSource>this.items;
return thisItems.getItem ? thisItems.getItem(index) : thisItems[index];
}
public _getSectionCount(): number {
if (!this.sectioned || !this.items) {
return 1;
}
return this.items.length;
}
public _getItemsInSection(section: number): any[] | ItemsSource {
if (!this.sectioned || !this.items) {
return this.items;
}
const sectionData = this.items[section];
return sectionData?.items || [];
}
public _getSectionData(section: number): any {
if (!this.sectioned || !this.items || !Array.isArray(this.items)) {
return null;
}
if (section < 0 || section >= this.items.length) {
if (Trace.isEnabled()) {
Trace.write(`ListView: Section ${section} out of bounds (total sections: ${this.items.length})`, Trace.categories.Debug);
}
return null;
}
const sectionData = this.items[section];
if (Trace.isEnabled() && !sectionData) {
Trace.write(`ListView: Section ${section} data is null/undefined`, Trace.categories.Debug);
}
return sectionData;
}
public _getDataItemInSection(section: number, index: number): any {
const sectionItems = this._getItemsInSection(section);
return (sectionItems as ItemsSource).getItem ? (sectionItems as ItemsSource).getItem(index) : sectionItems[index];
}
public _getDefaultItemContent(index: number): View {
const lbl = new Label();
lbl.bind({
@@ -249,3 +302,40 @@ export const separatorColorProperty = new CssProperty<Style, Color>({
valueConverter: (v) => new Color(v),
});
separatorColorProperty.register(Style);
export const stickyHeaderProperty = new Property<ListViewBase, boolean>({
name: 'stickyHeader',
defaultValue: false,
valueConverter: booleanConverter,
});
stickyHeaderProperty.register(ListViewBase);
export const stickyHeaderTemplateProperty = new Property<ListViewBase, string | Template>({
name: 'stickyHeaderTemplate',
valueChanged: (target) => {
target.refresh();
},
});
stickyHeaderTemplateProperty.register(ListViewBase);
export const stickyHeaderHeightProperty = new Property<ListViewBase, CoreTypes.LengthType>({
name: 'stickyHeaderHeight',
defaultValue: 'auto',
equalityComparer: Length.equals,
valueConverter: Length.parse,
});
stickyHeaderHeightProperty.register(ListViewBase);
export const stickyHeaderTopPaddingProperty = new Property<ListViewBase, boolean>({
name: 'stickyHeaderTopPadding',
defaultValue: false,
valueConverter: booleanConverter,
});
stickyHeaderTopPaddingProperty.register(ListViewBase);
export const sectionedProperty = new Property<ListViewBase, boolean>({
name: 'sectioned',
defaultValue: false,
valueConverter: (v) => !!v,
});
sectionedProperty.register(ListViewBase);