From 0d5dc53651061533bf332404dc525f543e309614 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Wed, 9 Sep 2026 17:56:26 +0900 Subject: [PATCH] fix: allow nested content in Ionic item groups --- docs/rules/require-ion-item-group.md | 4 ++-- src/rules/require-ion-item-group.ts | 10 +++++----- tests/rules/require-ion-item-group.ts | 20 +++++++++++++++----- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/rules/require-ion-item-group.md b/docs/rules/require-ion-item-group.md index 1e63147..b458d79 100644 --- a/docs/rules/require-ion-item-group.md +++ b/docs/rules/require-ion-item-group.md @@ -9,14 +9,14 @@ Ionic's iOS 26 and Material Design 3 list styling expects list items to be organ ## Rule Details -An `ion-item` within `ion-list` must use exactly one of these structures: +An `ion-item` within `ion-list` must have a supported group between itself and its nearest enclosing list. Common structures include: - `ion-list > ion-item-group > ion-item` - `ion-list > ion-reorder-group > ion-item` - `ion-list > ion-accordion-group > ion-accordion > ion-item` - `ion-list > ion-radio-group > ion-item` -Angular control-flow blocks such as `@if`, `@for`, `@empty`, `@switch`, and `@defer` are transparent for this structural check because they do not render an element. `ng-container` and `ng-template` are also transparent. Rendered HTML or Angular elements are not transparent: inserting a `div` between the list, group, or item is reported. +Angular control-flow blocks such as `@if`, `@for`, `@empty`, `@switch`, and `@defer` are transparent for this structural check because they do not render an element. `ng-container` and `ng-template` are also transparent. Rendered wrappers and nested groups are allowed. For example, `ion-list > ion-item-group > ion-accordion-group > ion-accordion > ion-item` is valid. This rule detects missing groups, not exact layout depth. An outer list’s group does not satisfy the requirement for items inside a nested `ion-list`. The rule only checks `ion-item` elements contained by `ion-list`. An `ion-item` outside a list is not reported, and `.spec.html` files are ignored. diff --git a/src/rules/require-ion-item-group.ts b/src/rules/require-ion-item-group.ts index df5dd0a..8ee8abc 100644 --- a/src/rules/require-ion-item-group.ts +++ b/src/rules/require-ion-item-group.ts @@ -2,7 +2,7 @@ import { TSESLint } from '@typescript-eslint/utils'; import type { TSESTree } from '@typescript-eslint/utils'; import { isRenderedElement, isRenderedText, isTransparentTemplateStructure, type TemplateAstNode, visitTemplateChildren } from './template-ast-utils'; -const DIRECT_ITEM_GROUPS = new Set(['ion-item-group', 'ion-reorder-group', 'ion-radio-group']); +const ITEM_GROUPS = new Set(['ion-item-group', 'ion-reorder-group', 'ion-radio-group']); type MessageIds = 'requireIonItemGroup' | 'wrapIonItemGroup'; @@ -97,10 +97,10 @@ const rule: TSESLint.RuleModule = { if (listIndex >= 0) { const nearestList = ancestors[listIndex]; const elementsAfterList = ancestors.slice(listIndex + 1).map((ancestor) => ancestor.name); - const hasDirectItemGroup = elementsAfterList.length === 1 && DIRECT_ITEM_GROUPS.has(elementsAfterList[0] ?? ''); - const hasAccordionGroup = - elementsAfterList.length === 2 && elementsAfterList[0] === 'ion-accordion-group' && elementsAfterList[1] === 'ion-accordion'; - const hasRequiredStructure = hasDirectItemGroup || hasAccordionGroup; + const hasItemGroup = elementsAfterList.some((name) => ITEM_GROUPS.has(name ?? '')); + const accordionGroupIndex = elementsAfterList.indexOf('ion-accordion-group'); + const hasAccordionGroup = accordionGroupIndex >= 0 && elementsAfterList.slice(accordionGroupIndex + 1).includes('ion-accordion'); + const hasRequiredStructure = hasItemGroup || hasAccordionGroup; if (!hasRequiredStructure) { const reportNode = node as unknown as TSESTree.Node; diff --git a/tests/rules/require-ion-item-group.ts b/tests/rules/require-ion-item-group.ts index a118b6b..780ad4c 100644 --- a/tests/rules/require-ion-item-group.ts +++ b/tests/rules/require-ion-item-group.ts @@ -10,6 +10,15 @@ const ruleTester = new RuleTester({ ruleTester.run('require-ion-item-group', rule, { valid: [ + { code: '
', filename: 'template.html' }, + { code: '
', filename: 'template.html' }, + { + code: '@if (visible) { Date }Other', + filename: 'template.html', + }, + { code: '
Choice
', filename: 'template.html' }, + { code: '
Row
', filename: 'template.html' }, + { code: '', filename: 'template.html' }, { code: '', filename: 'template.html' }, { @@ -85,6 +94,12 @@ ruleTester.run('require-ion-item-group', rule, { }, ], invalid: [ + { + code: '
Ungrouped inner row
', + filename: 'template.html', + errors: [{ messageId: 'requireIonItemGroup', suggestions: null }], + }, + { code: '', filename: 'template.html', @@ -106,11 +121,6 @@ ruleTester.run('require-ion-item-group', rule, { output: null, errors: [{ messageId: 'requireIonItemGroup' }], }, - { - code: '
', - filename: 'template.html', - errors: [{ messageId: 'requireIonItemGroup' }], - }, { code: '', filename: 'template.html',