Skip to content

Commit 22d7d2f

Browse files
authored
feat(ui5-li): add text wrapping support (#11108)
The standard list item ui5-li now supports content wrapping through a publicly available wrappingType property. When set to "Normal", long text content (title and description) will wrap to multiple lines instead of truncating with an ellipsis.
1 parent 7d6fdeb commit 22d7d2f

File tree

18 files changed

+599
-48
lines changed

18 files changed

+599
-48
lines changed

docs/2-advanced/05-using-features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Currently, only a few components offer additional features:
2929
| Package | Affected Components | Feature Import | Description |
3030
|----------------|---------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
3131
| `main` | `ui5-color-palette` | dynamically loaded if `showMoreColors` is set to `true` (to pre-load: `import "@ui5/webcomponents/dist/features/ColorPaletteMoreColors.js"` ) | Adds support for a "more colors" dialog in the color palette component allowing users to choose specific colors not present in the predefined range. |
32+
| `main` | `ui5-li` | dynamically loaded if `wrappingType="Normal"` is set (to pre-load: `import "@ui5/webcomponents/dist/features/ListItemStandardExpandableText.js"` ) | Adds support for expandable text in list items when wrapping type is set to "Normal" mode. |
3233
| `main` | `ui5-input` | dynamically loaded if `showSuggestions` is set to `true`(to pre-load: `import "@ui5/webcomponents/dist/features/InputSuggestions.js"` ) | Adds support for input suggestions while typing |
3334
| `main` | Multiple (e.g., `ui5-input`, `ui5-date-picker`) | `@ui5/webcomponents/dist/features/InputElementsFormSupport.js` | Adds support for the use of input components within forms |
3435
| `localization` | Multiple (e.g., `ui5-date-picker`) | `@ui5/webcomponents-localization/dist/features/calendar/Buddhist.js` | Adds support for the Buddhist calendars |

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("List Tests", () => {
2020

2121
cy.get<List>("@list")
2222
.then(list => {
23-
list.get(0).addEventListener("ui5-load-more", cy.stub().as("loadMore"));
23+
list.get(0)?.addEventListener("ui5-load-more", cy.stub().as("loadMore"));
2424
})
2525
.shadow()
2626
.find(".ui5-list-scroll-container")
@@ -177,3 +177,82 @@ describe("List - Accessibility", () => {
177177
});
178178
});
179179
});
180+
181+
describe("List - Wrapping Behavior", () => {
182+
it("renders list items with wrapping functionality", () => {
183+
const longText = "This is a very long text that should demonstrate the wrapping functionality of ListItemStandard components; This is a very long text that should demonstrate the wrapping functionality of ListItemStandard components; This is a very long text that should demonstrate the wrapping functionality of ListItemStandard components; This is a very long text that should demonstrate the wrapping functionality of ListItemStandard components; This is a very long text that should demonstrate the wrapping functionality of ListItemStandard components";
184+
const longDescription = "This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well; This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well; This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well; This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well; This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well; This is an even longer description text to verify that wrapping works correctly for the description part of the list item as well";
185+
186+
cy.mount(
187+
<List>
188+
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText} description={longDescription}></ListItemStandard>
189+
</List>
190+
);
191+
192+
// Check wrapping attributes are set correctly
193+
cy.get("#wrapping-item")
194+
.should("have.attr", "wrapping-type", "Normal");
195+
196+
// Check that ExpandableText components are present in the wrapping item
197+
cy.get("#wrapping-item")
198+
.shadow()
199+
.find("ui5-expandable-text")
200+
.should("exist")
201+
.and("have.length", 2);
202+
});
203+
204+
it("uses maxCharacters of 300 on desktop viewport for wrapping list items", () => {
205+
const longText = "This is a very long text that exceeds 100 characters but is less than 300 characters. This sentence is just to add more text to ensure we pass the 100 character threshold. And now we're adding even more text to be extra certain that we have enough content to demonstrate the behavior properly. And now we're adding even more text to be extra certain that we have enough content to demonstrate the behavior properly. And now we're adding even more text to be extra certain that we have enough content to demonstrate the behavior properly.";
206+
207+
cy.mount(
208+
<List>
209+
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText}></ListItemStandard>
210+
</List>
211+
);
212+
213+
// Check that ExpandableText is created with maxCharacters prop of 300
214+
cy.get("#wrapping-item")
215+
.shadow()
216+
.find("ui5-expandable-text")
217+
.first()
218+
.invoke('prop', 'maxCharacters')
219+
.should('eq', 300);
220+
});
221+
222+
it("should render different nodes based on wrappingType prop", () => {
223+
const longText = "This is a very long text that should be wrapped when the wrapping prop is enabled, and truncated when it's disabled. This is a very long text that should be wrapped when the wrapping prop is enabled, and truncated when it's disabled. This is a very long text that should be wrapped when the wrapping prop is enabled, and truncated when it's disabled. This is a very long text that should be wrapped when the wrapping prop is enabled, and truncated when it's disabled. This is a very long text that should be wrapped when the wrapping prop is enabled, and truncated when it's disabled. And now we're adding even more text to be extra certain that we have enough content to demonstrate the behavior properly.";
224+
225+
// First render with wrapping enabled
226+
cy.mount(
227+
<List>
228+
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText}></ListItemStandard>
229+
</List>
230+
);
231+
232+
// Check that wrapping-type attribute is set to Normal
233+
cy.get("#wrapping-item")
234+
.should("have.attr", "wrapping-type", "Normal");
235+
236+
// Should have expandable text component when wrapping is enabled
237+
cy.get("#wrapping-item")
238+
.shadow()
239+
.find("ui5-expandable-text")
240+
.should("exist");
241+
242+
// Set wrappingType to None
243+
cy.get("#wrapping-item")
244+
.then($el => {
245+
$el[0].setAttribute("wrapping-type", "None");
246+
});
247+
248+
// Check that wrapping-type attribute is set to None
249+
cy.get("#wrapping-item")
250+
.should("have.attr", "wrapping-type", "None");
251+
252+
// Should not have expandable text component when wrapping is disabled
253+
cy.get("#wrapping-item")
254+
.shadow()
255+
.find("ui5-expandable-text")
256+
.should("not.exist");
257+
});
258+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import List from "../../src/List.js";
2+
import ListItemStandard from "../../src/ListItemStandard.js";
3+
4+
describe("List Mobile Tests", () => {
5+
before(() => {
6+
cy.ui5SimulateDevice("phone");
7+
});
8+
9+
it("adjusts maxCharacters based on viewport size for wrapping list items", () => {
10+
const longText = "This is a very long text that exceeds 100 characters but is less than 300 characters. This sentence is just to add more text to ensure we pass the 100 character threshold. And now we're adding even more text to be extra certain.";
11+
12+
cy.mount(
13+
<List>
14+
<ListItemStandard id="wrapping-item" wrappingType="Normal" text={longText}></ListItemStandard>
15+
</List>
16+
);
17+
18+
// Get the list item and check its media range
19+
cy.get("#wrapping-item")
20+
.invoke('prop', 'mediaRange')
21+
.should('eq', 'S');
22+
23+
// Check that ExpandableText is created with maxCharacters prop of 100
24+
cy.get("#wrapping-item")
25+
.shadow()
26+
.find("ui5-expandable-text")
27+
.first()
28+
.invoke('prop', 'maxCharacters')
29+
.should('eq', 100);
30+
});
31+
});

packages/main/src/List.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
SelectionRequestEventDetail,
5656
} from "./ListItem.js";
5757
import ListSeparator from "./types/ListSeparator.js";
58+
import MediaRange from "@ui5/webcomponents-base/dist/MediaRange.js";
5859

5960
// Template
6061
import ListTemplate from "./ListTemplate.js";
@@ -464,6 +465,14 @@ class List extends UI5Element {
464465
@property({ type: Boolean })
465466
_loadMoreActive = false;
466467

468+
/**
469+
* Defines the current media query size.
470+
* @default "S"
471+
* @private
472+
*/
473+
@property()
474+
mediaRange = "S";
475+
467476
/**
468477
* Defines the items of the component.
469478
*
@@ -491,9 +500,8 @@ class List extends UI5Element {
491500
static i18nBundle: I18nBundle;
492501
_previouslyFocusedItem: ListItemBase | null;
493502
_forwardingFocus: boolean;
494-
resizeListenerAttached: boolean;
495503
listEndObserved: boolean;
496-
_handleResize: ResizeObserverCallback;
504+
_handleResizeCallback: ResizeObserverCallback;
497505
initialIntersection: boolean;
498506
_selectionRequested?: boolean;
499507
_groupCount: number;
@@ -516,9 +524,6 @@ class List extends UI5Element {
516524
// Indicates that the List is forwarding the focus before or after the internal ul.
517525
this._forwardingFocus = false;
518526

519-
// Indicates that the List has already subscribed for resize.
520-
this.resizeListenerAttached = false;
521-
522527
// Indicates if the IntersectionObserver started observing the List
523528
this.listEndObserved = false;
524529

@@ -528,9 +533,7 @@ class List extends UI5Element {
528533
getItemsCallback: () => this.getEnabledItems(),
529534
});
530535

531-
this._handleResize = this.checkListInViewport.bind(this);
532-
533-
this._handleResize = this.checkListInViewport.bind(this);
536+
this._handleResizeCallback = this._handleResize.bind(this);
534537

535538
// Indicates the List bottom most part has been detected by the IntersectionObserver
536539
// for the first time.
@@ -562,13 +565,13 @@ class List extends UI5Element {
562565
onEnterDOM() {
563566
registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this));
564567
DragRegistry.subscribe(this);
568+
ResizeHandler.register(this.getDomRef()!, this._handleResizeCallback);
565569
}
566570

567571
onExitDOM() {
568572
deregisterUI5Element(this);
569573
this.unobserveListEnd();
570-
this.resizeListenerAttached = false;
571-
ResizeHandler.deregister(this.getDomRef()!, this._handleResize);
574+
ResizeHandler.deregister(this.getDomRef()!, this._handleResizeCallback);
572575
DragRegistry.unsubscribe(this);
573576
}
574577

@@ -587,7 +590,6 @@ class List extends UI5Element {
587590

588591
if (this.grows) {
589592
this.checkListInViewport();
590-
this.attachForResize();
591593
}
592594
}
593595

@@ -613,13 +615,6 @@ class List extends UI5Element {
613615
});
614616
}
615617

616-
attachForResize() {
617-
if (!this.resizeListenerAttached) {
618-
this.resizeListenerAttached = true;
619-
ResizeHandler.register(this.getDomRef()!, this._handleResize);
620-
}
621-
}
622-
623618
get shouldRenderH1() {
624619
return !this.header.length && this.headerText;
625620
}
@@ -776,6 +771,8 @@ class List extends UI5Element {
776771
(item as ListItem)._selectionMode = this.selectionMode;
777772
}
778773
item.hasBorder = showBottomBorder;
774+
775+
(item as ListItem).mediaRange = this.mediaRange;
779776
});
780777
}
781778

@@ -1065,6 +1062,13 @@ class List extends UI5Element {
10651062
}
10661063
}
10671064

1065+
_handleResize() {
1066+
this.checkListInViewport();
1067+
1068+
const width = this.getBoundingClientRect().width;
1069+
this.mediaRange = MediaRange.getCurrentRange(MediaRange.RANGESETS.RANGE_4STEPS, width);
1070+
}
1071+
10681072
/*
10691073
* KEYBOARD SUPPORT
10701074
*/

packages/main/src/ListItem.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ abstract class ListItem extends ListItemBase {
191191
@property()
192192
_selectionMode: `${ListSelectionMode}` = "None";
193193

194+
/**
195+
* Defines the current media query size.
196+
* @default "S"
197+
* @private
198+
*/
199+
@property()
200+
mediaRange = "S";
201+
194202
/**
195203
* Defines the delete button, displayed in "Delete" mode.
196204
* **Note:** While the slot allows custom buttons, to match

0 commit comments

Comments
 (0)