From 756b78b788f0e4b31591baa330dcb6bc3a1d597c Mon Sep 17 00:00:00 2001 From: ilhan orhan Date: Tue, 18 Feb 2020 13:54:17 +0200 Subject: [PATCH] feat(ui5-list): add infinite-scroll capability (#1220) - introduce infiniteScroll property, when set an event "loadMore" will be fired when user scrolls to the very bottom of the list, then the user might load new items - introduce "loadMore" event - introduce "busy" property to display a loading indicator --- packages/main/src/List.hbs | 9 +- packages/main/src/List.js | 89 +++++++++++++++++- packages/main/src/themes/List.css | 23 +++-- .../main/src/themes/base/sizes-parameters.css | 2 + packages/main/test/pages/List.html | 50 ++++++++++ packages/main/test/pages/List_test_page.html | 29 ++++++ packages/main/test/samples/List.sample.html | 92 ++++++++++++++++++- packages/main/test/specs/List.spec.js | 10 ++ 8 files changed, 289 insertions(+), 15 deletions(-) diff --git a/packages/main/src/List.hbs b/packages/main/src/List.hbs index 1f24f6d930de..db05ffb0b574 100644 --- a/packages/main/src/List.hbs +++ b/packages/main/src/List.hbs @@ -2,6 +2,7 @@ class="ui5-list-root" @focusin="{{_onfocusin}}" @keydown="{{_onkeydown}}" + @scroll="{{_onScroll}}" > {{#if header.length}} @@ -33,5 +34,11 @@ {{/if}} + {{#if showBusy}} +
+ +
+ {{/if}} +
- + \ No newline at end of file diff --git a/packages/main/src/List.js b/packages/main/src/List.js index da47e5a03601..167daed8def2 100644 --- a/packages/main/src/List.js +++ b/packages/main/src/List.js @@ -6,6 +6,7 @@ import { isTabNext } from "@ui5/webcomponents-base/dist/events/PseudoEvents.js"; import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js"; import ListMode from "./types/ListMode.js"; import ListSeparators from "./types/ListSeparators.js"; +import BusyIndicator from "./BusyIndicator.js"; // Template import ListTemplate from "./generated/templates/ListTemplate.lit.js"; @@ -13,6 +14,9 @@ import ListTemplate from "./generated/templates/ListTemplate.lit.js"; // Styles import listCss from "./generated/themes/List.css.js"; +const BUSYINDICATOR_HEIGHT = 48; // px +const INFINITE_SCROLL_DEBOUNCE_RATE = 250; // ms + /** * @public */ @@ -128,6 +132,33 @@ const metadata = { type: ListSeparators, defaultValue: ListSeparators.All, }, + + /** + * Defines if the component would fire the loadMore event, + * when the user scrolls to the bottom of the list and help achieving an "infinite scroll" effect + * by adding new items each time. + * + * @type {boolean} + * @defaultvalue false + * @public + * @since 1.0.0-rc.6 + */ + infiniteScroll: { + type: Boolean, + }, + + /** + * Defines if the component would display a loading indicator at the bottom of the list. + * It's especially useful, when combined with infiniteScroll. + * + * @type {boolean} + * @defaultvalue false + * @public + * @since 1.0.0-rc.6 + */ + busy: { + type: Boolean, + }, }, events: /** @lends sap.ui.webcomponents.main.List.prototype */ { @@ -176,6 +207,17 @@ const metadata = { selectionComponentPressed: { type: Boolean }, // protected, indicates if the user used the selection components to change the selection }, }, + + /** + * Fired when the user scrolls to the bottom of the list. + *
+ * Note: The event is fired when the infiniteScroll property is enabled. + * + * @event + * @public + * @since 1.0.0-rc.6 + */ + loadMore: {}, }, }; @@ -254,6 +296,18 @@ class List extends UI5Element { this.addEventListener("ui5-_selectionRequested", this.onSelectionRequested.bind(this)); } + get shouldRenderH1() { + return !this.header.length && this.headerText; + } + + get showNoDataText() { + return this.items.length === 0 && this.noDataText; + } + + get showBusy() { + return this.busy || this.infiniteScroll; + } + onBeforeRendering() { this.prepareListItems(); } @@ -391,6 +445,13 @@ class List extends UI5Element { } } + _onScroll(event) { + if (!this.infiniteScroll) { + return; + } + this.debounce(this.loadMore.bind(this, event.target), INFINITE_SCROLL_DEBOUNCE_RATE); + } + _onfocusin(event) { // If the focusin event does not origin from one of the 'triggers' - ignore it. if (!this.isForwardElement(this.getNormalizedTarget(event.target))) { @@ -555,12 +616,32 @@ class List extends UI5Element { return focused; } - get shouldRenderH1() { - return !this.header.length && this.headerText; + loadMore(el) { + const scrollTop = el.scrollTop; + const height = el.offsetHeight; + const scrollHeight = el.scrollHeight; + + if (this.previousScrollPosition > scrollTop) { // skip scrolling upwards + this.previousScrollPosition = scrollTop; + return; + } + this.previousScrollPosition = scrollTop; + + if (scrollHeight - BUSYINDICATOR_HEIGHT <= height + scrollTop) { + this.fireEvent("loadMore"); + } } - get showNoDataText() { - return this.items.length === 0 && this.noDataText; + debounce(fn, delay) { + clearTimeout(this.debounceInterval); + this.debounceInterval = setTimeout(() => { + this.debounceInterval = null; + fn(); + }, delay); + } + + static async onDefine() { + await BusyIndicator.define(); } } diff --git a/packages/main/src/themes/List.css b/packages/main/src/themes/List.css index 54eca5d5727c..27b8469688f7 100644 --- a/packages/main/src/themes/List.css +++ b/packages/main/src/themes/List.css @@ -16,23 +16,24 @@ height: 100%; position: relative; box-sizing: border-box; + overflow: auto; } -.ui5-list-root .ui5-list-ul { +.ui5-list-ul { list-style-type: none; padding: 0; margin: 0; } -.ui5-list-root .ui5-list-ul:focus { +.ui5-list-ul:focus { outline: none; } -.ui5-list-root .ui5-list-focusarea { +.ui5-list-focusarea { position: fixed; /* keep it in the visible viewport, so that IE does not scroll on focus */ } -.ui5-list-root .ui5-list-header { +.ui5-list-header { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -47,7 +48,7 @@ border-bottom: 1px solid var(--sapGroup_TitleBorderColor); } -.ui5-list-root .ui5-list-footer { +.ui5-list-footer { height: 2rem; box-sizing: border-box; -webkit-text-size-adjust: none; /* To improve readability Mobile Safari automatically increases the size of small text so let's disable this */ @@ -62,7 +63,7 @@ text-overflow: ellipsis; } -.ui5-list-root .ui5-list-nodata { +.ui5-list-nodata { list-style-type: none; display: -webkit-box; display: flex; @@ -78,9 +79,15 @@ font-size: var(--sapFontMediumSize); } - -.ui5-list-root .ui5-list-nodata-text { +.ui5-list-nodata-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} + +.ui5-list-busy-row { + display: flex; + align-items: center; + height: var(--_ui5_list_busy_row_height); + justify-content: center; } \ No newline at end of file diff --git a/packages/main/src/themes/base/sizes-parameters.css b/packages/main/src/themes/base/sizes-parameters.css index a4e1f8ef83ca..f24c36026cd1 100644 --- a/packages/main/src/themes/base/sizes-parameters.css +++ b/packages/main/src/themes/base/sizes-parameters.css @@ -19,6 +19,7 @@ --_ui5_list_item_img_size: 2rem; --_ui5_list_item_img_margin: 0.5rem 0.75rem 0.5rem 0rem; --_ui5_list_item_base_height: 3rem; + --_ui5_list_busy_row_height: 3rem; --_ui5_month_picker_item_height: 3rem; --_ui5_year_picker_item_height: 3rem; --_ui5_tokenizer_root_padding: 0.1875rem; @@ -107,6 +108,7 @@ --_ui5_list_item_img_size: 1.75rem; --_ui5_list_item_img_margin: 0.55rem 0.75rem 0.5rem 0rem; --_ui5_list_item_base_height: 2rem; + --_ui5_list_busy_row_height: 2rem; --_ui5_month_picker_item_height: 2rem; --_ui5_panel_header_height: 2rem; diff --git a/packages/main/test/pages/List.html b/packages/main/test/pages/List.html index 60601476687e..cdf7b535d6b8 100644 --- a/packages/main/test/pages/List.html +++ b/packages/main/test/pages/List.html @@ -13,6 +13,27 @@ + + List "infinite-scroll" + + + New Items + Voluptate do eu cupidatat elit est culpa. Reprehenderit eiusmod voluptate ex est dolor nostrud Lorem Lorem do nisi laborum veniam. Sint do non culpa aute occaecat labore ipsum veniam minim tempor est. Duis pariatur aute culpa irure ad excepteur pariatur culpa culpa ea duis occaecat aute irure. Ipsum velit culpa non exercitation ex laboris deserunt in eu non officia in. Laborum sunt aliqua labore cupidatat sunt labore. + Laptop Lenovo + IPhone 3 + + Discounted Items + HP Monitor 24 + Audio cabel + DVD set + + Discounted Items + HP Monitor 24 + Audio cabel + DVD set + + +

ui5-list

@@ -294,6 +315,35 @@

Items 3/3

var selectedItems = event.detail.selectedItems; lblSelectionChange2.innerHTML = "Event] selectionChange :: " + selectedItems.map(item => item.id).join("/"); }); + + function template(i) { + var li = document.createElement("ui5-li"); + li.textContent = "Title"; + li.description = "my description goes here " + i; + li.info = "Available"; + return li; + } + + function insertItems(el, num) { + for(var i= 0; i { + var el = infiniteScrollEx; + el.busy = true; + + setTimeout(() => { + insertItems(el, 3); + el.busy = false; + result.textContent = (++loadedCount) + " times " + (itemsLoadedTotal += itemsToLoad); + }, 200); + }); + diff --git a/packages/main/test/pages/List_test_page.html b/packages/main/test/pages/List_test_page.html index 3ca8a1806f6b..a7c34bd28c4b 100644 --- a/packages/main/test/pages/List_test_page.html +++ b/packages/main/test/pages/List_test_page.html @@ -12,6 +12,20 @@ + List "infinite-scroll" + scroll + + Laptop Lenovo + IPhone 3 + HP Monitor 24 + Audio cabel + DVD set + HP Monitor 24 + Audio cabel + Last Item + + +
Argentina Bulgaria @@ -129,6 +143,21 @@ listMultiSel.addEventListener("ui5-selectionChange", function(event) { fieldMultiSelResult.value = event.detail.selectionComponentPressed; }) + + btnTrigger.addEventListener("click", (e) => { + const scrollableContiner = infiniteScrollEx.shadowRoot.querySelector(".ui5-list-root"); + scrollableContiner.scroll(0, scrollableContiner.scrollHeight); + }); + + var loadMoreCounter = 0; + infiniteScrollEx.addEventListener("ui5-loadMore", (e) => { + for(var i = 0; i < 3; i++) { + var li = document.createElement("ui5-li"); + li.textContent = "Title" + i; + infiniteScrollEx.appendChild(li); + } + loadMoreResult.value= ++loadMoreCounter; + }); diff --git a/packages/main/test/samples/List.sample.html b/packages/main/test/samples/List.sample.html index 59b196a245b4..0fd0d6180a92 100644 --- a/packages/main/test/samples/List.sample.html +++ b/packages/main/test/samples/List.sample.html @@ -6,6 +6,7 @@

List

<ui5-list>
+

Basic List

@@ -26,6 +27,82 @@

Basic List

+ +
+

List with infinite-scroll

+
+ + Pineapple + Orange + Banana + Mango + Apple + Peach + Pomelo + Pear + +
+

+<ui5-list id="infiniteScrollEx" style="height: 300px" infinite-scroll>
+	<ui5-li icon="nutrition-activity" description="Tropical plant with an edible fruit" info="In-stock" info-state="Success">Pineapple</ui5-li>
+	<ui5-li icon="nutrition-activity" description="Occurs between red and yellow" info="Expires" info-state="Warning">Orange</ui5-li>
+	<ui5-li icon="nutrition-activity" description="The yellow lengthy fruit" info="Re-stock" info-state="Error">Banana</ui5-li>
+	<ui5-li icon="nutrition-activity" description="The tropical stone fruit" info="Re-stock" info-state="Error">Mango</ui5-li>
+	<ui5-li icon="nutrition-activity" description="An apple is a sweet, edible fruit produced by an apple tree " info="In-stock" info-state="Success">Apple</ui5-li>
+	<ui5-li icon="nutrition-activity" description="The peach (Prunus persica) is a deciduous tree native to the region of Northwest China" info="Expires" info-state="Warning">Peach</ui5-li>
+	<ui5-li icon="nutrition-activity" description="The pomelo is the largest citrus fruit from the family Rutaceae and the principal ancestor of the grapefruit" info="Re-stock" info-state="Error">Pomelo</ui5-li>
+	<ui5-li icon="nutrition-activity" description="The pear (/ˈpɛər/) tree and shrub are a species of genus Pyrus, bearing the pomaceous fruit of the same name." info="Re-stock" info-state="Error">Pear</ui5-li>
+</ui5-list>
+
+<script>
+	const infiniteScrollEx = document.getElementById("infiniteScrollEx");
+
+	infiniteScrollEx.addEventListener("loadMore", (e) => {
+		infiniteScrollEx.busy = true;
+
+		setTimeout(() => {
+			for(let i = 0; i < 5; i++) {
+				const li = document.createElement("ui5-li");
+				li.textContent = "Fruit name" + i;
+				infiniteScrollEx.appendChild(li);
+			}
+
+			infiniteScrollEx.busy = false;
+		}, 200);
+	});
+</script>
+	
+ + +
+ +

List in Single-selection Mode

@@ -62,7 +139,7 @@

List in Single-selection Mode

- +

List in Multi-selection Mode

@@ -83,7 +160,18 @@

List in Multi-selection Mode

- + +
+

Busy List

+
+ +
+

+<ui5-list header-text="Fetching data ..." busy></ui5-list>
+	
+
+ +

List With GroupHeaders

diff --git a/packages/main/test/specs/List.spec.js b/packages/main/test/specs/List.spec.js index 2e2dc900fd2e..8c048fa927be 100644 --- a/packages/main/test/specs/List.spec.js +++ b/packages/main/test/specs/List.spec.js @@ -205,4 +205,14 @@ describe("List Tests", () => { assert.ok(firstListItem.isFocused(), "First item remains focussed"); }); + + it("tests 'loadMore' event fired upon infinite scroll", () => { + const btn = $("#btnTrigger"); + const loadMoreResult = $("#loadMoreResult"); + + btn.click(); + browser.pause(1000); + + assert.strictEqual(loadMoreResult.getAttribute("value"), "1", "The event loadMore is fired."); + }); });