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
4 changes: 2 additions & 2 deletions docs/rules/require-ion-item-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 5 additions & 5 deletions src/rules/require-ion-item-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -97,10 +97,10 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
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;
Expand Down
20 changes: 15 additions & 5 deletions tests/rules/require-ion-item-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ const ruleTester = new RuleTester({

ruleTester.run('require-ion-item-group', rule, {
valid: [
{ code: '<ion-list><ion-item-group><div><ion-item></ion-item></div></ion-item-group></ion-list>', filename: 'template.html' },
{ code: '<ion-list><div><ion-item-group><ion-item></ion-item></ion-item-group></div></ion-list>', filename: 'template.html' },
{
code: '<ion-list><ion-item-group>@if (visible) { <ion-accordion-group><ion-accordion><ion-item slot="header">Date</ion-item></ion-accordion></ion-accordion-group> }<ion-item>Other</ion-item></ion-item-group></ion-list>',
filename: 'template.html',
},
{ code: '<ion-list><ion-radio-group><div><ion-item>Choice</ion-item></div></ion-radio-group></ion-list>', filename: 'template.html' },
{ code: '<ion-list><ion-reorder-group><div><ion-item>Row</ion-item></div></ion-reorder-group></ion-list>', filename: 'template.html' },

{ code: '<ion-item></ion-item>', filename: 'template.html' },
{ code: '<ion-list></ion-list>', filename: 'template.html' },
{
Expand Down Expand Up @@ -85,6 +94,12 @@ ruleTester.run('require-ion-item-group', rule, {
},
],
invalid: [
{
code: '<ion-list><ion-item-group><ion-list><div><ion-item>Ungrouped inner row</ion-item></div></ion-list></ion-item-group></ion-list>',
filename: 'template.html',
errors: [{ messageId: 'requireIonItemGroup', suggestions: null }],
},

{
code: '<ion-list><ion-item></ion-item></ion-list>',
filename: 'template.html',
Expand All @@ -106,11 +121,6 @@ ruleTester.run('require-ion-item-group', rule, {
output: null,
errors: [{ messageId: 'requireIonItemGroup' }],
},
{
code: '<ion-list><ion-item-group><div><ion-item></ion-item></div></ion-item-group></ion-list>',
filename: 'template.html',
errors: [{ messageId: 'requireIonItemGroup' }],
},
{
code: '<ion-list><app-item-group><ion-item></ion-item></app-item-group></ion-list>',
filename: 'template.html',
Expand Down