Skip to content

Commit 4c1a506

Browse files
feat(ui5-li-group): introduced wrappingType prop (#12022)
### Description The list item group `ui5-li-group` now supports content wrapping through a publicly available `wrappingType` property. When set to "Normal", long text content (header text) will wrap to multiple lines instead of truncating with an ellipsis. #### Key features: - Responsive behavior based on screen size: - On mobile (size S): Text truncates after 100 characters - On tablets/desktop (size M/L): Text truncates after 300 characters - "Show More/Less" functionality for very long content using `ui5-expandable-text` - Works for header text fields - CSS styling to ensure proper layout in different states #### Implementation details: - Made `wrappingType` property public available with options "None" (default) and "Normal" - Conditionally render content using `ui5-expandable-text` component when `wrappingType` is "Normal" - Added `_maxCharacters` getter to determine truncation point based on media range - Updated ListItemGroupHeaderTemplate to handle wrapped content rendering - Updated CSS selectors to use `wrapping-type` attribute - Updated HTML test page with examples #### Documentation: - Updated section in List.mdx explaining the wrapping behavior - Updated sample in WrappingBehavior folder - Added JSDoc descriptions for the new property #### Tests: - Added desktop test for 300 character limit - Added mobile-specific test for 100 character limit Fixes: #11655
1 parent 5c8bc5d commit 4c1a506

File tree

11 files changed

+201
-5
lines changed

11 files changed

+201
-5
lines changed

packages/main/cypress/specs/List.cy.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type UI5Element from "@ui5/webcomponents-base";
22
import List from "../../src/List.js";
33
import ListItemStandard from "../../src/ListItemStandard.js";
4+
import ListItemGroup from "../../src/ListItemGroup.js";
45

56
describe("List Tests", () => {
67
it("tests 'loadMore' event fired upon infinite scroll", () => {
@@ -216,6 +217,9 @@ describe("List - Wrapping Behavior", () => {
216217
cy.mount(
217218
<List>
218219
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText} description={longDescription}></ListItemStandard>
220+
<ListItemGroup id="lig" wrapping-type="Normal" header-text={longText}>
221+
<ListItemStandard>1. Bulgaria</ListItemStandard>
222+
</ListItemGroup>
219223
</List>
220224
);
221225

@@ -229,6 +233,17 @@ describe("List - Wrapping Behavior", () => {
229233
.find("ui5-expandable-text")
230234
.should("exist")
231235
.and("have.length", 2);
236+
237+
cy.get("#lig")
238+
.should("have.attr", "wrapping-type", "Normal")
239+
.shadow()
240+
.find("ui5-li-group-header")
241+
.should("have.attr", "wrapping-type", "Normal")
242+
.shadow()
243+
.find("ui5-expandable-text")
244+
.should("exist")
245+
.and("have.length", 1);
246+
232247
});
233248

234249
it("uses maxCharacters of 300 on desktop viewport for wrapping list items", () => {
@@ -237,6 +252,9 @@ describe("List - Wrapping Behavior", () => {
237252
cy.mount(
238253
<List>
239254
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText}></ListItemStandard>
255+
<ListItemGroup id="lig" wrapping-type="Normal" header-text={longText}>
256+
<ListItemStandard>1. Bulgaria</ListItemStandard>
257+
</ListItemGroup>
240258
</List>
241259
);
242260

@@ -247,6 +265,14 @@ describe("List - Wrapping Behavior", () => {
247265
.first()
248266
.invoke('prop', 'maxCharacters')
249267
.should('eq', 300);
268+
269+
cy.get("#lig")
270+
.shadow()
271+
.find("ui5-li-group-header")
272+
.shadow()
273+
.find("ui5-expandable-text")
274+
.invoke('prop', 'maxCharacters')
275+
.should('eq', 300);
250276
});
251277

252278
it("should render different nodes based on wrappingType prop", () => {

packages/main/cypress/specs/List.mobile.cy.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import List from "../../src/List.js";
2+
import ListItemGroup from "../../src/ListItemGroup.js";
23
import ListItemStandard from "../../src/ListItemStandard.js";
34

45
describe("List Mobile Tests", () => {
@@ -12,6 +13,9 @@ describe("List Mobile Tests", () => {
1213
cy.mount(
1314
<List>
1415
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText}></ListItemStandard>
16+
<ListItemGroup id="lig" wrapping-type="Normal" header-text={longText}>
17+
<ListItemStandard>1. Bulgaria</ListItemStandard>
18+
</ListItemGroup>
1519
</List>
1620
);
1721

@@ -20,12 +24,27 @@ describe("List Mobile Tests", () => {
2024
.invoke('prop', 'mediaRange')
2125
.should('eq', 'S');
2226

27+
cy.get("#lig")
28+
.shadow()
29+
.find("ui5-li-group-header")
30+
.invoke('prop', 'mediaRange')
31+
.should('eq', 'S');
32+
2333
// Check that ExpandableText is created with maxCharacters prop of 100
2434
cy.get("#wrapping-item")
2535
.shadow()
2636
.find("ui5-expandable-text")
2737
.first()
2838
.invoke('prop', 'maxCharacters')
2939
.should('eq', 100);
40+
41+
cy.get("#lig")
42+
.shadow()
43+
.find("ui5-li-group-header")
44+
.shadow()
45+
.find("ui5-expandable-text")
46+
.should("exist")
47+
.invoke('prop', 'maxCharacters')
48+
.should('eq', 100);
3049
});
3150
});

packages/main/src/ListItemGroup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ListItemGroupTemplate from "./ListItemGroupTemplate.js";
1717
// Styles
1818
import ListItemGroupCss from "./generated/themes/ListItemGroup.css.js";
1919
import type ListItemGroupHeader from "./ListItemGroupHeader.js";
20+
import type WrappingType from "./types/WrappingType.js";
2021

2122
type ListItemGroupMoveEventDetail = {
2223
source: {
@@ -111,6 +112,26 @@ class ListItemGroup extends UI5Element {
111112
})
112113
items!: Array<ListItemBase>;
113114

115+
/**
116+
* Defines if the text of the component should wrap when it's too long.
117+
* When set to "Normal", the content (title, description) will be wrapped
118+
* using the `ui5-expandable-text` component.<br/>
119+
*
120+
* The text can wrap up to 100 characters on small screens (size S) and
121+
* up to 300 characters on larger screens (size M and above). When text exceeds
122+
* these limits, it truncates with an ellipsis followed by a text expansion trigger.
123+
*
124+
* Available options are:
125+
* - `None` (default) - The text will truncate with an ellipsis.
126+
* - `Normal` - The text will wrap (without truncation).
127+
*
128+
* @default "None"
129+
* @public
130+
* @since 2.15.0
131+
*/
132+
@property()
133+
wrappingType: `${WrappingType}` = "None";
134+
114135
/**
115136
* Indicates whether the header is focused
116137
* @private

packages/main/src/ListItemGroupHeader.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
44
import type { AriaRole } from "@ui5/webcomponents-base/dist/types.js";
55
import toLowercaseEnumValue from "@ui5/webcomponents-base/dist/util/toLowercaseEnumValue.js";
66
import ListItemBase from "./ListItemBase.js";
7+
import type { ExpandableTextTemplateParams } from "./types/ExpandableTextTemplateParams.js";
78

89
import { GROUP_HEADER_TEXT } from "./generated/i18n/i18n-defaults.js";
910

@@ -13,6 +14,22 @@ import ListItemGroupHeaderTemplate from "./ListItemGroupHeaderTemplate.js";
1314
// Styles
1415
import ListItemGroupHeaderCss from "./generated/themes/ListItemGroupHeader.css.js";
1516
import ListItemAccessibleRole from "./types/ListItemAccessibleRole.js";
17+
import type WrappingType from "./types/WrappingType.js";
18+
19+
/**
20+
* Maximum number of characters to display for small screens (Size S)
21+
* @private
22+
*/
23+
const MAX_CHARACTERS_SIZE_S = 100;
24+
25+
/**
26+
* Maximum number of characters to display for medium and larger screens (Size M and above)
27+
* @private
28+
*/
29+
const MAX_CHARACTERS_SIZE_M = 300;
30+
31+
// Specific template type for expandable text
32+
type ExpandableTextTemplate = (this: ListItemGroupHeader, params: ExpandableTextTemplateParams) => JSX.Element;
1633

1734
/**
1835
* @class
@@ -45,6 +62,41 @@ class ListItemGroupHeader extends ListItemBase {
4562
@property()
4663
accessibleRole: `${ListItemAccessibleRole}` = ListItemAccessibleRole.ListItem;
4764

65+
/**
66+
* Defines if the text of the component should wrap when it's too long.
67+
* When set to "Normal", the content (title, description) will be wrapped
68+
* using the `ui5-expandable-text` component.<br/>
69+
*
70+
* The text can wrap up to 100 characters on small screens (size S) and
71+
* up to 300 characters on larger screens (size M and above). When text exceeds
72+
* these limits, it truncates with an ellipsis followed by a text expansion trigger.
73+
*
74+
* Available options are:
75+
* - `None` (default) - The text will truncate with an ellipsis.
76+
* - `Normal` - The text will wrap (without truncation).
77+
*
78+
* @default "None"
79+
* @public
80+
* @since 2.15.0
81+
*/
82+
@property()
83+
wrappingType: `${WrappingType}` = "None";
84+
85+
/**
86+
* Defines the current media query size.
87+
* @default "S"
88+
* @private
89+
*/
90+
@property()
91+
mediaRange = "S";
92+
93+
/**
94+
* The expandableText template.
95+
* @private
96+
*/
97+
@property({ noAttribute: true })
98+
expandableTextTemplate?: ExpandableTextTemplate;
99+
48100
@slot()
49101
subItems!: Array<HTMLElement>;
50102

@@ -67,13 +119,54 @@ class ListItemGroupHeader extends ListItemBase {
67119
return ListItemGroupHeader.i18nBundle.getText(GROUP_HEADER_TEXT);
68120
}
69121

122+
get defaultSlotText(): string {
123+
return this.textContent!;
124+
}
125+
70126
get ariaLabelText() {
71127
return [this.textContent, this.accessibleName].filter(Boolean).join(" ");
72128
}
73129

74130
get hasSubItems() {
75131
return this.subItems.length > 0;
76132
}
133+
134+
onBeforeRendering() {
135+
super.onBeforeRendering();
136+
137+
// Only load ExpandableText if "Normal" wrapping is used
138+
if (this.wrappingType === "Normal") {
139+
// If feature is already loaded (preloaded by the user via importing ListItemGroupHeaderExpandableText.js), the template is already available
140+
if (ListItemGroupHeader.ExpandableTextTemplate) {
141+
this.expandableTextTemplate = ListItemGroupHeader.ExpandableTextTemplate;
142+
// If feature is not preloaded, load the template dynamically
143+
} else {
144+
import("./features/ListItemStandardExpandableTextTemplate.js").then(module => {
145+
this.expandableTextTemplate = module.default;
146+
});
147+
}
148+
}
149+
}
150+
151+
/**
152+
* Determines the maximum characters to display based on the current media range.
153+
* - Size S: 100 characters
154+
* - Size M and larger: 300 characters
155+
* @private
156+
*/
157+
get _maxCharacters(): number {
158+
return this.mediaRange === "S" ? MAX_CHARACTERS_SIZE_S : MAX_CHARACTERS_SIZE_M;
159+
}
160+
161+
/**
162+
* Returns the content text, either from text property or from the default slot
163+
* @private
164+
*/
165+
get _textContent(): string {
166+
return this.defaultSlotText || this.groupHeaderText || "";
167+
}
168+
169+
static ExpandableTextTemplate?: ExpandableTextTemplate;
77170
}
78171

79172
ListItemGroupHeader.define();

packages/main/src/ListItemGroupHeaderTemplate.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type ListItemGroupHeader from "./ListItemGroupHeader.js";
2+
import WrappingType from "./types/WrappingType.js";
23

34
export default function ListItemGroupHeaderTemplate(this: ListItemGroupHeader) {
45
return (
@@ -16,10 +17,27 @@ export default function ListItemGroupHeaderTemplate(this: ListItemGroupHeader) {
1617
onKeyDown={this._onkeydown}
1718
>
1819
<div id={`${this._id}-content`} class="ui5-li-content">
19-
<span class="ui5-ghli-title"><slot></slot></span>
20+
{renderTitle.call(this)}
2021
</div>
2122

2223
{this.hasSubItems && <slot name="subItems"></slot>}
2324
</div>
2425
);
2526
}
27+
28+
function renderTitle(this: ListItemGroupHeader) {
29+
if (this.wrappingType === WrappingType.Normal) {
30+
return this.expandableTextTemplate?.call(this, {
31+
className: "ui5-ghli-title",
32+
text: this._textContent,
33+
maxCharacters: this._maxCharacters,
34+
part: "title",
35+
});
36+
}
37+
38+
return (
39+
<span part="title" class="ui5-ghli-title">
40+
<slot></slot>
41+
</span>
42+
);
43+
}

packages/main/src/ListItemGroupTemplate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function ListItemGroupTemplate(this: ListItemGroup) {
77
return (
88
<>
99
{this.hasHeader &&
10-
<ListItemGroupHeader focused={this.focused} part="header" accessibleRole={ListItemAccessibleRole.ListItem}>
10+
<ListItemGroupHeader wrappingType={this.wrappingType} focused={this.focused} part="header" accessibleRole={ListItemAccessibleRole.ListItem}>
1111
{ this.hasFormattedHeader ? <slot name="header"></slot> : this.headerText }
1212
<div
1313
role="list"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import ListItemStandard from "../ListItemStandard.js";
2+
import ListItemGroupHeader from "../ListItemGroupHeader.js";
23
import ListItemStandardExpandableTextTemplate from "./ListItemStandardExpandableTextTemplate.js";
34

45
// Assigns the template to the component to be used when the feature is preloaded
56
ListItemStandard.ExpandableTextTemplate = ListItemStandardExpandableTextTemplate;
7+
ListItemGroupHeader.ExpandableTextTemplate = ListItemStandardExpandableTextTemplate;

packages/main/src/features/ListItemStandardExpandableTextTemplate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ExpandableText from "../ExpandableText.js";
2-
import type ListItemStandard from "../ListItemStandard.js";
2+
import type ListItemBase from "../ListItemBase.js";
33
import type { ExpandableTextTemplateParams } from "../types/ExpandableTextTemplateParams.js";
44

55
/**
@@ -10,7 +10,7 @@ import type { ExpandableTextTemplateParams } from "../types/ExpandableTextTempla
1010
* @returns {JSX.Element} The rendered ExpandableText component
1111
*/
1212
export default function ListItemStandardExpandableTextTemplate(
13-
this: ListItemStandard,
13+
this: ListItemBase,
1414
injectedProps: ExpandableTextTemplateParams
1515
): JSX.Element {
1616
const {

packages/main/src/themes/ListItemGroupHeader.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
color: var(--sapList_TableGroupHeaderTextColor);
77
}
88

9+
:host([wrapping-type="Normal"]) {
10+
height: auto;
11+
}
12+
913
:host([has-border]) {
1014
border-bottom: var(--sapList_BorderWidth) solid var(--sapList_GroupHeaderBorderColor);
1115
}

packages/website/docs/_components_pages/main/List/List.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ You can select and drag multiple items at once when using <b>selection-mode="Mul
6464
<MultipleDrag />
6565

6666
### Wrapping Behavior
67-
The standard list item `<ui5-li>` supports text wrapping through the <b>wrappingType</b> property. When set to "Normal", long text content (title and description) will wrap to multiple lines instead of truncating with an ellipsis. For very long content, the text is displayed with a "Show More/Show Less" mechanism.
67+
The standard list item `<ui5-li>` and the list item group `<ui5-li-group>` supports text wrapping through the <b>wrappingType</b> property. When set to "Normal", long text content (title and description) will wrap to multiple lines instead of truncating with an ellipsis. For very long content, the text is displayed with a "Show More/Show Less" mechanism.
6868

6969
The wrapping behavior is responsive:
7070
- On mobile devices (size S), text is truncated after 100 characters

0 commit comments

Comments
 (0)