From 43bae1ba973b096e88801d5bc8337f605f733d5d Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Sat, 18 Apr 2026 21:41:17 -0700 Subject: [PATCH 1/2] fix(listview): use section-aware template lookup in sectioned ListView When using a sectioned ListView with multiple item templates, the template returned by _getItemTemplate could be wrong because it calls _getDataItem which does not consider the sectioned data structure. This means the itemTemplateSelector receives the section object (e.g. { title, items }) instead of the actual row data item, causing it to resolve the wrong template. Fix by adding _getItemTemplateInSection(section, index) to ListViewBase that uses _getDataItemInSection and _getItemsInSection to correctly resolve the data item and its section's items array before passing them to the selector. On iOS, tableViewCellForRowAtIndexPath, tableViewHeightForRowAtIndexPath, and _prepareCell are updated to call _getItemTemplateInSection when sectioned is enabled. The broken absoluteIndex workaround in _prepareCell is removed. On Android, getItemViewType and _createItemView are updated to call _getItemTemplateInSection when sectioned is enabled. Fixes #11133 --- packages/core/ui/list-view/index.android.ts | 8 ++++---- packages/core/ui/list-view/index.ios.ts | 12 +++--------- packages/core/ui/list-view/list-view-common.ts | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/core/ui/list-view/index.android.ts b/packages/core/ui/list-view/index.android.ts index 34cf29a854..c89c64e136 100644 --- a/packages/core/ui/list-view/index.android.ts +++ b/packages/core/ui/list-view/index.android.ts @@ -996,8 +996,8 @@ function ensureListViewAdapterClass() { // Header view type is the last index (after all item template types) return this.owner._itemTemplatesInternal.length; } else { - // Get template for the actual item - const template = this.owner._getItemTemplate(positionInfo.itemIndex); + // Get template for the actual item using section-aware lookup + const template = this.owner._getItemTemplateInSection(positionInfo.section, positionInfo.itemIndex); return this.owner._itemTemplatesInternal.indexOf(template); } } else { @@ -1124,8 +1124,8 @@ function ensureListViewAdapterClass() { } private _createItemView(section: number, itemIndex: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View { - // Use existing item creation logic but with sectioned data - const template = this.owner._getItemTemplate(itemIndex); + // Use section-aware template lookup when in sectioned mode, flat lookup otherwise + const template = section >= 0 && this.owner.sectioned ? this.owner._getItemTemplateInSection(section, itemIndex) : this.owner._getItemTemplate(itemIndex); let view: View; // convertView is of the wrong type diff --git a/packages/core/ui/list-view/index.ios.ts b/packages/core/ui/list-view/index.ios.ts index 3dff3a77c3..8a3393cc14 100644 --- a/packages/core/ui/list-view/index.ios.ts +++ b/packages/core/ui/list-view/index.ios.ts @@ -162,7 +162,7 @@ class DataSource extends NSObject implements UITableViewDataSource { const owner = this._owner?.deref(); let cell: ListViewCell; if (owner) { - const template = owner._getItemTemplate(indexPath.row); + const template = owner.sectioned ? owner._getItemTemplateInSection(indexPath.section, indexPath.row) : owner._getItemTemplate(indexPath.row); cell = (tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground()); owner._prepareCell(cell, indexPath); @@ -235,7 +235,7 @@ class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate { let height = owner.getHeight(indexPath.row); if (height === undefined) { // in iOS8+ after call to scrollToRowAtIndexPath:atScrollPosition:animated: this method is called before tableViewCellForRowAtIndexPath so we need fake cell to measure its content. - const template = owner._getItemTemplate(indexPath.row); + const template = owner.sectioned ? owner._getItemTemplateInSection(indexPath.section, indexPath.row) : owner._getItemTemplate(indexPath.row); let cell = this._measureCellMap.get(template.key); if (!cell) { cell = tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground(); @@ -794,13 +794,7 @@ export class ListView extends ListViewBase { let view: ItemView = cell.view; if (!view) { 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(); + view = this._getItemTemplateInSection(indexPath.section, indexPath.row).createView(); } else { view = this._getItemTemplate(indexPath.row).createView(); } diff --git a/packages/core/ui/list-view/list-view-common.ts b/packages/core/ui/list-view/list-view-common.ts index f005b17629..05ca423868 100644 --- a/packages/core/ui/list-view/list-view-common.ts +++ b/packages/core/ui/list-view/list-view-common.ts @@ -129,6 +129,24 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi return this._itemTemplatesInternal[0]; } + public _getItemTemplateInSection(section: number, index: number): KeyedTemplate { + let templateKey = 'default'; + if (this.itemTemplateSelector) { + const dataItem = this._getDataItemInSection(section, index); + const sectionItems = this._getItemsInSection(section); + templateKey = this._itemTemplateSelector(dataItem, index, sectionItems); + } + + for (let i = 0, length = this._itemTemplatesInternal.length; i < length; i++) { + if (this._itemTemplatesInternal[i].key === templateKey) { + return this._itemTemplatesInternal[i]; + } + } + + // This is the default template + return this._itemTemplatesInternal[0]; + } + public _prepareItem(item: View, index: number) { if (item) { item.bindingContext = this._getDataItem(index); From 97ab92a371b3bb51c1de4f11918d8d7c5fee5d4d Mon Sep 17 00:00:00 2001 From: VeinDevTtv Date: Sat, 18 Apr 2026 21:48:54 -0700 Subject: [PATCH 2/2] test(listview): add unit tests for sectioned ListView template selection (fix #11133) --- .../src/ui/list-view/list-view-tests.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/apps/automated/src/ui/list-view/list-view-tests.ts b/apps/automated/src/ui/list-view/list-view-tests.ts index 7305117e96..223f16ea3b 100644 --- a/apps/automated/src/ui/list-view/list-view-tests.ts +++ b/apps/automated/src/ui/list-view/list-view-tests.ts @@ -1095,6 +1095,69 @@ export class ListViewTest extends UITest { } } + // Sectioned ListView + multiple item templates tests (fix for #11133) + public test_SectionedListView_ItemTemplateSelector_CorrectTemplatePerSection() { + // Verifies that _getItemTemplateInSection resolves the template using the + // actual row data item (not the section wrapper object). + const listView = this.testView; + listView.sectioned = true; + + // Section 0 items have age=0 (even → 'red'), section 1 items have age=1 (odd → 'green') + listView.items = [ + { title: 'Section A', items: [{ age: 0 }, { age: 2 }] }, + { title: 'Section B', items: [{ age: 1 }, { age: 3 }] }, + ]; + listView.itemTemplates = this._itemTemplatesString; + listView.itemTemplateSelector = (item: any) => (item.age % 2 === 0 ? 'red' : 'green'); + + // Section 0, row 0: age=0 → 'red' + const template00 = listView._getItemTemplateInSection(0, 0); + TKUnit.assertEqual(template00.key, 'red', 'section 0 row 0 should use red template'); + + // Section 0, row 1: age=2 → 'red' + const template01 = listView._getItemTemplateInSection(0, 1); + TKUnit.assertEqual(template01.key, 'red', 'section 0 row 1 should use red template'); + + // Section 1, row 0: age=1 → 'green' + const template10 = listView._getItemTemplateInSection(1, 0); + TKUnit.assertEqual(template10.key, 'green', 'section 1 row 0 should use green template'); + + // Section 1, row 1: age=3 → 'green' + const template11 = listView._getItemTemplateInSection(1, 1); + TKUnit.assertEqual(template11.key, 'green', 'section 1 row 1 should use green template'); + } + + public test_SectionedListView_ItemTemplateSelector_DifferentTemplatesWithinSameSection() { + // Verifies mixed templates within a single section are resolved correctly. + const listView = this.testView; + listView.sectioned = true; + + listView.items = [{ title: 'Mixed', items: [{ age: 0 }, { age: 1 }, { age: 2 }] }]; + listView.itemTemplates = this._itemTemplatesString; + listView.itemTemplateSelector = (item: any) => (item.age % 2 === 0 ? 'red' : 'green'); + + TKUnit.assertEqual(listView._getItemTemplateInSection(0, 0).key, 'red', 'row 0 (age=0) → red'); + TKUnit.assertEqual(listView._getItemTemplateInSection(0, 1).key, 'green', 'row 1 (age=1) → green'); + TKUnit.assertEqual(listView._getItemTemplateInSection(0, 2).key, 'red', 'row 2 (age=2) → red'); + } + + public test_SectionedListView_ItemTemplateSelector_UnknownKeyFallsBackToDefault() { + // Mirrors test_ItemTemplateSelector_WhenWrongTemplateKeyIsSpecified_TheDefaultTemplateIsUsed + // but for the sectioned path. + const listView = this.testView; + listView.sectioned = true; + + listView.items = [{ title: 'Section A', items: [{ age: 0 }] }]; + listView.itemTemplate = "