Skip to content

Commit

Permalink
feat(ui5-table): table loading indicator (#9214)
Browse files Browse the repository at this point in the history
* feat(ui5-table): table loading indicator

* feat(ui5-table): table loading indicator

* feat(ui5-table): table loading indicator

The loading/loadingDelay properties are added to show a busy indicator component on the table which blocks the interaction while it is shown.
  • Loading branch information
aborjinik authored Jun 18, 2024
1 parent 19380fe commit fabc911
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 15 deletions.
16 changes: 5 additions & 11 deletions packages/main/src/Table.hbs
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
<div id="before" role="none" tabindex="0" ui5-table-dummy-focus-area></div>

{{#if loading}}
<!-- TODO: Change to loading indicator -->
<ui5-busy-indicator
id="busy-indicator"
delay="50"
class="ui5-table-busy-ind"
active
data-sap-focus-ref
></ui5-busy-indicator>
{{/if}}

<div id="table" role="grid"
style="{{styles.table}}"
aria-label="{{_ariaLabel}}"
Expand Down Expand Up @@ -38,6 +27,11 @@

{{> tableEndRow}}
</div>

{{#if loading}}
<ui5-busy-indicator id="loading" delay="{{loadingDelay}}" active data-sap-focus-ref></ui5-busy-indicator>
{{/if}}

<div id="after" role="none" tabindex="0" ui5-table-dummy-focus-area></div>

{{#*inline "growingRow"}}
Expand Down
12 changes: 12 additions & 0 deletions packages/main/src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ class Table extends UI5Element {
@property({ type: Boolean })
loading!: boolean;

/**
* Defines the delay in milliseconds, after which the loading indicator will show up for this component.
* @default 1000
* @public
*/
@property({ validator: Integer, defaultValue: 1000 })
loadingDelay!: number;

/**
* Defines the sticky top offset of the table, if other sticky elements outside of the table exist.
*/
Expand Down Expand Up @@ -507,6 +515,10 @@ class Table extends UI5Element {
return this.shadowRoot!.getElementById("table") as HTMLElement;
}

get _loadingElement() {
return this.shadowRoot!.getElementById("loading") as HTMLElement;
}

get _effectiveNoDataText() {
return this.noDataText || Table.i18nBundle.getText(TABLE_NO_DATA);
}
Expand Down
27 changes: 25 additions & 2 deletions packages/main/src/TableNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
isPageDown,
isHome,
isEnd,
isTabNext,
isTabPrevious,
} from "@ui5/webcomponents-base/dist/Keys.js";
import isElementClickable from "@ui5/webcomponents-base/dist/util/isElementClickable.js";
import isElementHidden from "@ui5/webcomponents-base/dist/util/isElementHidden.js";
Expand All @@ -31,12 +33,18 @@ class TableNavigation extends TableExtension {
_tabPosition: number = 0;
_ignoreFocusIn?: boolean;
_lastFocusedItem?: HTMLElement;
_onKeyDownCaptureBound: (e: KeyboardEvent) => void;

constructor(table: Table) {
super();
this._table = table;
this._gridWalker = new GridWalker();
this._gridWalker.setGrid(this._getNavigationItemsOfGrid());
this._onKeyDownCaptureBound = this._onKeyDownCapture.bind(this);

// we register the keydown handler on the table element at the capturing phase since the
// busy indicator stops the propagation of the keydown event and it never reaches the table
this._table.addEventListener("keydown", this._onKeyDownCaptureBound, { capture: true });
}

_getNavigationItemsOfRow(row: TableRowBase) {
Expand Down Expand Up @@ -272,12 +280,27 @@ class TableNavigation extends TableExtension {
}

if (eventOrigin === this._table._beforeElement || eventOrigin === this._table._afterElement) {
this._gridWalker.setColPos(0);
this._focusCurrentItem();
if (this._table.loading) {
this._table._loadingElement.focus();
} else {
this._gridWalker.setColPos(0);
this._focusCurrentItem();
}
} else if (eventOrigin !== this._lastFocusedItem && this._getNavigationItemsOfGrid().flat().includes(eventOrigin)) {
this._lastFocusedItem = eventOrigin;
}
}

_onKeyDownCapture(e: KeyboardEvent) {
if (!this._table.loading) {
return;
}

if (isTabNext(e) || isTabPrevious(e)) {
this._focusElement(e.shiftKey ? this._table._beforeElement : this._table._afterElement);
e.stopImmediatePropagation();
}
}
}

export default TableNavigation;
5 changes: 3 additions & 2 deletions packages/main/src/themes/Table.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
:host {
display: block;
position: relative;
color: var(--sapList_TextColor);
font: var(--sapFontSize) var(--sapFontFamily);
}
Expand Down Expand Up @@ -33,9 +34,9 @@
box-sizing: border-box;
}

#busy-indicator {
#loading {
position: absolute;
z-index: 1;
inset: 0;
height: 100%;
z-index: 2;
}
24 changes: 24 additions & 0 deletions packages/main/test/pages/TableLoading.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test Page - Table Navigation</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
</head>
<body>
<div class="section">
<input id="before-table1">
<ui5-table id="table1" loading loadingDelay="0">
<ui5-table-header-row slot="headerRow">
<ui5-table-header-cell id="colA"><span>ColumnA</span></ui5-table-header-cell>
</ui5-table-header-row>
<ui5-table-row>
<ui5-table-cell><ui5-label>Cell A</ui5-label></ui5-table-cell>
</ui5-table-row>
</ui5-table>
<input id="after-table1">
</div>
</body>
</html>
28 changes: 28 additions & 0 deletions packages/main/test/specs/TableLoading.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { assert } from "chai";

describe("Table - loading", async () => {
before(async () => {
await browser.url(`test/pages/TableLoading.html`);
});

it("tests busy indicator is displayed", async () => {
const before = await browser.$("#before-table1");
const table = await browser.$("#table1");
const after = await browser.$("#after-table1");
const loading = await table.shadow$("#loading");

assert.ok(await loading.isExisting(), "The busy indicator is displayed");

await before.click();
assert.ok(await before.isFocused(), "The input before the table1 is focused.");

await before.keys("Tab");
let res = await browser.executeAsync(done => {
done(document.getElementById("table1").shadowRoot.querySelector("#loading").matches(":focus"));
});
assert.ok(res, "Busy indicator is focused");

await browser.keys("Tab");
assert.ok(await after.isFocused(), "The input after the table1 is focused.");
});
});

0 comments on commit fabc911

Please sign in to comment.