From 15584993472c2a9d34349da999da74bfe450e76e Mon Sep 17 00:00:00 2001 From: Dobromira Boycheva Date: Fri, 29 Nov 2024 16:57:50 +0200 Subject: [PATCH 1/4] feat(ui5-user-menu): introduce new component (#10244) * feat(ui5-profile-menu): introduce new component --------- Co-authored-by: Adrian Bobev --- packages/fiori/README.md | 3 + packages/fiori/cypress/specs/UserMenu.cy.ts | 345 +++++++++++++++++ packages/fiori/src/UserMenu.hbs | 93 +++++ packages/fiori/src/UserMenu.ts | 351 ++++++++++++++++++ packages/fiori/src/UserMenuAccount.ts | 84 +++++ packages/fiori/src/UserMenuItem.hbs | 1 + packages/fiori/src/UserMenuItem.ts | 55 +++ packages/fiori/src/bundle.esm.ts | 3 + .../fiori/src/i18n/messagebundle.properties | 18 + packages/fiori/src/themes/UserMenu.css | 65 ++++ packages/fiori/src/themes/UserMenuItem.css | 0 packages/fiori/test/pages/UserMenu.html | 203 ++++++++++ .../fiori/UserMenu/UserMenu.mdx | 18 + .../fiori/UserMenu/UserMenuAccount.mdx | 8 + .../fiori/UserMenu/UserMenuItem.mdx | 8 + .../fiori/UserMenu/Advanced/Advanced.md | 4 + .../_samples/fiori/UserMenu/Advanced/main.js | 73 ++++ .../fiori/UserMenu/Advanced/sample.html | 64 ++++ .../_samples/fiori/UserMenu/Basic/Basic.md | 4 + .../_samples/fiori/UserMenu/Basic/main.js | 56 +++ .../_samples/fiori/UserMenu/Basic/sample.html | 51 +++ 21 files changed, 1507 insertions(+) create mode 100644 packages/fiori/cypress/specs/UserMenu.cy.ts create mode 100644 packages/fiori/src/UserMenu.hbs create mode 100644 packages/fiori/src/UserMenu.ts create mode 100644 packages/fiori/src/UserMenuAccount.ts create mode 100644 packages/fiori/src/UserMenuItem.hbs create mode 100644 packages/fiori/src/UserMenuItem.ts create mode 100644 packages/fiori/src/themes/UserMenu.css create mode 100644 packages/fiori/src/themes/UserMenuItem.css create mode 100644 packages/fiori/test/pages/UserMenu.html create mode 100644 packages/website/docs/_components_pages/fiori/UserMenu/UserMenu.mdx create mode 100644 packages/website/docs/_components_pages/fiori/UserMenu/UserMenuAccount.mdx create mode 100644 packages/website/docs/_components_pages/fiori/UserMenu/UserMenuItem.mdx create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Advanced/Advanced.md create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Advanced/main.js create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Advanced/sample.html create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Basic/Basic.md create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Basic/main.js create mode 100644 packages/website/docs/_samples/fiori/UserMenu/Basic/sample.html diff --git a/packages/fiori/README.md b/packages/fiori/README.md index b9467b3cec86..03b70a85a879 100644 --- a/packages/fiori/README.md +++ b/packages/fiori/README.md @@ -31,6 +31,9 @@ such as a common header (ShellBar). | Timeline Item | `ui5-timeline-item` | comes with `ui5-timeline` | | Upload Collection | `ui5-upload-collection` | `import "@ui5/webcomponents-fiori/dist/UploadCollection.js";` | | Upload Collection Item | `ui5-upload-collection-item` | `import "@ui5/webcomponents-fiori/dist/UploadCollectionItem.js";` | +| User Menu | `ui5-user-menu` | `import "@ui5/webcomponents-fiori/dist/UserMenu.js";` | +| User Menu Account | `ui5-user-menu-account` | `import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js";` | +| User Menu Item | `ui5-user-menu-item` | `import "@ui5/webcomponents-fiori/dist/UserMenuItem.js";` | | View Settings Dialog | `ui5-view-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/ViewSettingsDialog.js";` | | View Settings Dialog - Sort Item | `ui5-sort-item` | `import "@ui5/webcomponents-fiori/dist/SortItem.js";` | | View Settings Dialog - Filter Item | `ui5-filter-item` | `import "@ui5/webcomponents-fiori/dist/FilterItem.js";` | diff --git a/packages/fiori/cypress/specs/UserMenu.cy.ts b/packages/fiori/cypress/specs/UserMenu.cy.ts new file mode 100644 index 000000000000..165cff1150a3 --- /dev/null +++ b/packages/fiori/cypress/specs/UserMenu.cy.ts @@ -0,0 +1,345 @@ +import { html } from "lit"; +import "../../src/UserMenu.js"; +import "../../src/UserMenuAccount.js"; +import "../../src/UserMenuItem.js"; + +import "@ui5/webcomponents/dist/Avatar.js"; +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents-icons/dist/action-settings.js"; + +describe("Initial rendering", () => { + it("tests no config provided", () => { + cy.mount(html`Open User Menu + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-responsive-popover]").as("responsivePopover"); + cy.get("@responsivePopover").should("exist"); + cy.get("@responsivePopover").find("[ui5-button]").contains("Sign Out"); + cy.get("@responsivePopover").find("[ui5-button]").should("have.length", 1); + }); + + it("tests config show-manage-account", () => { + cy.mount(html`Open User Menu + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-responsive-popover]").as("responsivePopover"); + cy.get("@responsivePopover").should("exist"); + cy.get("@responsivePopover").find("[ui5-button]").contains("Manage account"); + cy.get("@responsivePopover").find("[ui5-button]").should("have.length", 2); + }); + + it("tests config show-other-accounts", () => { + cy.mount(html`Open User Menu + + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-responsive-popover]").as("responsivePopover"); + cy.get("@responsivePopover").should("exist"); + cy.get("@responsivePopover").find("[ui5-panel]").contains("Other accounts (1)"); + cy.get("@responsivePopover").find("[ui5-button]").should("have.length", 1); + }); + + it("tests config show-add-account", () => { + cy.mount(html`Open User Menu + + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-responsive-popover]").as("responsivePopover"); + cy.get("@responsivePopover").should("exist"); + cy.get("@responsivePopover").find(".ui5-pm-add-account-btn").should("exist"); + cy.get("@responsivePopover").find("[ui5-button]").should("have.length", 2); + }); +}); + +describe("Menu configuration", () => { + it("tests config items", () => { + cy.mount(html`Open User Menu + + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").find("[ui5-user-menu-item]").as("userMenuItems"); + cy.get("@userMenuItems").should("exist"); + cy.get("@userMenuItems").should("have.length", 2); + }); + + it("tests config items with submenu items", () => { + cy.mount(html`Open User Menu + + + + + + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").find("[ui5-user-menu-item]").as("userMenuItems"); + cy.get("@userMenuItems").should("exist"); + cy.get("@userMenuItems").find("[ui5-user-menu-item]").as("userSubMenuItems"); + cy.get("@userSubMenuItems").should("exist"); + cy.get("@userSubMenuItems").should("have.length", 2); + }); + + it("tests config items with icon", () => { + cy.mount(html`Open User Menu + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").find("[ui5-user-menu-item]").as("userMenuItems"); + cy.get("@userMenuItems").should("exist"); + cy.get("@userMenuItems").should("have.length", 1); + cy.get("@userMenuItems").should("have.attr", "icon", "action-settings"); + }); +}); + +describe("Avatar configuration", () => { + it("tests default", () => { + cy.mount(html`Open User Menu + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-avatar]").as("avatar"); + cy.get("@avatar").should("exist"); + cy.get("@avatar").should("have.length", 1); + cy.get("@avatar").should("have.attr", "fallback-icon", "person-placeholder"); + }); + + it("tests initials", () => { + cy.mount(html`Open User Menu + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-avatar]").as("avatar"); + cy.get("@avatar").should("exist"); + cy.get("@avatar").should("have.length", 1); + cy.get("@avatar").should("have.attr", "initials", "AC"); + }); + + it("tests image", () => { + cy.mount(html`Open User Menu + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").should("exist"); + cy.get("@userMenu").shadow().find("[ui5-avatar]").as("avatar"); + cy.get("@avatar").should("exist"); + cy.get("@avatar").should("have.length", 1); + cy.get("@avatar").find("img").as("image"); + cy.get("@image").should("have.length", 1); + cy.get("@image").should("have.attr", "src", "./../../test/pages/img/man_avatar_1.png"); + }); +}); + +describe("Events", () => { + it("tests avatar-click event", () => { + cy.mount(html`Open User Menu + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu") + .shadow() + .find("[ui5-responsive-popover]") + .find("[ui5-avatar]") + .as("avatar"); + + cy.get("@userMenu") + .then($avatar => { + $avatar.get(0).addEventListener("avatar-click", cy.stub().as("clicked")); + }); + + cy.get("@avatar").click(); + + cy.get("@clicked").should("have.been.calledOnce"); + }); + + it("tests manage-account-click event", () => { + cy.mount(html`Open User Menu + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu") + .shadow() + .find("[ui5-button]") + .eq(0) + .as("manageAccountBtn"); + + cy.get("@userMenu").then($userMenu => { + $userMenu.get(0).addEventListener("manage-account-click", cy.stub().as("clicked")); + }); + + cy.get("@manageAccountBtn").click(); + + cy.get("@clicked").should("have.been.calledOnce"); + }); + + it("tests add-account-click event", () => { + cy.mount(html`Open User Menu + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu") + .shadow() + .find(".ui5-pm-add-account-btn") + .as("addAccountBtn"); + + cy.get("@userMenu") + .then($userMenu => { + $userMenu.get(0).addEventListener("add-account-click", cy.stub().as("clicked")); + }); + + cy.get("@addAccountBtn").click(); + + cy.get("@clicked").should("have.been.calledOnce"); + }); + + it("tests change-account event", () => { + cy.mount(html`Open User Menu + + + + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu") + .shadow() + .find("[ui5-panel]") + .as("otherAccounts"); + + cy.get("@userMenu").then($userMenu => { + $userMenu.get(0).addEventListener("change-account", cy.stub().as("changedAccount")); + }); + + cy.get("@otherAccounts") + .shadow() + .find("[ui5-button]") + .click(); + + cy.get("@otherAccounts") + .find("[ui5-li-custom]") + .click(); + + cy.get("@changedAccount").should("have.been.calledOnce"); + cy.get("@changedAccount").its("args.0.0.detail.prevSelectedAccount").should("have.property", "titleText", "Alain Chevalier 1"); + cy.get("@changedAccount").its("args.0.0.detail.selectedAccount").should("have.property", "titleText", "Alain Chevalier 2"); + }); + + it("tests item-click event", () => { + cy.mount(html`Open User Menu + + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu") + .find("[ui5-user-menu-item]") + .as("userMenuItem"); + + cy.get("@userMenu") + .then($userMenu => { + $userMenu.get(0).addEventListener("item-click", cy.stub().as("clicked")); + }); + + cy.get("@userMenuItem").click(); + + cy.get("@clicked").should("have.been.calledOnce"); + cy.get("@clicked").its("args.0.0.detail.item").should("have.property", "text", "Setting"); + }); + + it("tests sign-out-click event", () => { + cy.mount(html`Open User Menu + + `); + cy.get("[ui5-user-menu]").as("userMenu"); + cy.get("@userMenu").shadow().find("[ui5-button]").as("signOutBtn"); + + cy.get("@userMenu") + .then($userMenu => { + $userMenu.get(0).addEventListener("sign-out-click", cy.stub().as("clicked")); + }); + + cy.get("@signOutBtn").click(); + + cy.get("@clicked").should("have.been.calledOnce"); + }); +}); diff --git a/packages/fiori/src/UserMenu.hbs b/packages/fiori/src/UserMenu.hbs new file mode 100644 index 000000000000..cf5fecc990bb --- /dev/null +++ b/packages/fiori/src/UserMenu.hbs @@ -0,0 +1,93 @@ + + {{#if _selectedAccount}} + + {{/if}} + + {{#if showOtherAccounts}} + + + {{#if _otherAccounts.length}} + + {{#each _otherAccounts}} + +
+ + {{#if avatarSrc}} + + {{/if}} + +
+ {{#if titleText}} + {{titleText}} + {{/if}} + {{#if subtitleText}} + {{subtitleText}} + {{/if}} + {{#if description}} + {{description}} + {{/if}} +
+
+
+ {{/each}} +
+ {{/if}} +
+ {{/if}} + + {{#if menuItems.length}} + + + + {{/if}} + + +
\ No newline at end of file diff --git a/packages/fiori/src/UserMenu.ts b/packages/fiori/src/UserMenu.ts new file mode 100644 index 000000000000..6ebbda6d8f54 --- /dev/null +++ b/packages/fiori/src/UserMenu.ts @@ -0,0 +1,351 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import { + customElement, slot, event, property, +} from "@ui5/webcomponents-base/dist/decorators.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import DOMReferenceConverter from "@ui5/webcomponents-base/dist/converters/DOMReference.js"; +import Avatar from "@ui5/webcomponents/dist/Avatar.js"; +import Title from "@ui5/webcomponents/dist/Title.js"; +import Text from "@ui5/webcomponents/dist/Text.js"; +import Button from "@ui5/webcomponents/dist/Button.js"; +import Label from "@ui5/webcomponents/dist/Label.js"; +import Panel from "@ui5/webcomponents/dist/Panel.js"; +import Icon from "@ui5/webcomponents/dist/Icon.js"; +import List, { type ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; +import ListItemCustom from "@ui5/webcomponents/dist/ListItemCustom.js"; +import Tag from "@ui5/webcomponents/dist/Tag.js"; +import ResponsivePopover from "@ui5/webcomponents/dist/ResponsivePopover.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import UserMenuAccount from "./UserMenuAccount.js"; +import UserMenuItem from "./UserMenuItem.js"; +import UserMenuTemplate from "./generated/templates/UserMenuTemplate.lit.js"; +import UserMenuCss from "./generated/themes/UserMenu.css.js"; + +// Icons +import "@ui5/webcomponents-icons/dist/add-employee.js"; +import "@ui5/webcomponents-icons/dist/edit.js"; +import "@ui5/webcomponents-icons/dist/person-placeholder.js"; +import "@ui5/webcomponents-icons/dist/log.js"; +import "@ui5/webcomponents-icons/dist/user-settings.js"; + +// Texts +import { + USER_MENU_OTHER_ACCOUNT_BUTTON_TXT, + USER_MENU_MANAGE_ACCOUNT_BUTTON_TXT, + USER_MENU_SIGN_OUT_BUTTON_TXT, + USER_MENU_POPOVER_ACCESSIBLE_NAME, + USER_MENU_EDIT_AVATAR_TXT, + USER_MENU_ADD_ACCOUNT_TXT, +} from "./generated/i18n/i18n-defaults.js"; + +type UserMenuItemClickEventDetail = { + item: UserMenuItem; +} + +type UserMenuOtherAccountClickEventDetail = { + prevSelectedAccount: UserMenuAccount; + selectedAccount: UserMenuAccount; +} + +/** + * @class + * ### Overview + * + * The `ui5-user-menu` is an SAP Fiori specific web component that is used in `ui5-shellbar` + * and allows the user to easily see information and settings for the current user and all other logged in accounts. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserMenu.js";` + * + * `import "@ui5/webcomponents-fiori/dist/UserMenuItem.js";` (for `ui5-user-menu-item`) + * + * @constructor + * @extends UI5Element + * @experimental + * @public + * @since 2.5.0 + */ + +@customElement({ + tag: "ui5-user-menu", + languageAware: true, + renderer: litRender, + template: UserMenuTemplate, + styles: [UserMenuCss], + dependencies: [ + ResponsivePopover, + Avatar, + Title, + Text, + Label, + Button, + Panel, + Icon, + List, + ListItemCustom, + Tag, + ], +}) + +/** + * Fired when the account avatar is selected. + * @public + */ +@event("avatar-click") + +/** + * Fired when the "Manage Account" button is selected. + * @public + */ +@event("manage-account-click") + +/** + * Fired when the "Add Account" button is selected. + * @public + */ +@event("add-account-click") + +/** + * Fired when the account is switched to a different one. + * @param {UserMenuAccount} prevSelectedAccount The previously selected account. + * @param {UserMenuAccount} selectedAccount The selected account. + * @public + */ +@event("change-account", { + detail: { + prevSelectedAccount: { type: UserMenuAccount }, + selectedAccount: { type: UserMenuAccount }, + }, + cancelable: true, +}) + +/** + * Fired when a menu item is selected. + * @param {UserMenuItem} item The selected `user menu item`. + * @public + */ +@event("item-click", { + detail: { + item: { type: UserMenuItem }, + }, + cancelable: true, +}) + +/** + * Fired when the "Sign Out" button is selected. + * @public + */ +@event("sign-out-click", { + cancelable: true, +}) +class UserMenu extends UI5Element { + /** + * Defines if the User Menu is opened. + * + * @default false + * @public + */ + @property({ type: Boolean }) + open = false; + + /** + * Defines the ID or DOM Reference of the element at which the user menu is shown. + * When using this attribute in a declarative way, you must only use the `id` (as a string) of the element at which you want to show the popover. + * You can only set the `opener` attribute to a DOM Reference when using JavaScript. + * @public + * @default undefined + */ + @property({ converter: DOMReferenceConverter }) + opener?: HTMLElement | string; + + /** + * Defines if the User Menu shows the Manage Account option. + * + * @default false + * @public + */ + @property({ type: Boolean }) + showManageAccount = false; + + /** + * Defines if the User Menu shows the Other Accounts option. + * + * @default false + * @public + */ + @property({ type: Boolean }) + showOtherAccounts = false; + + /** + * Defines if the User Menu shows the Add Account option. + * + * @default false + * @public + */ + @property({ type: Boolean }) + showAddAccount = false; + + /** + * Defines the menu items. + * @public + */ + @slot({ + type: HTMLElement, + "default": true, + }) + menuItems!: Array; + + /** + * Defines the user accounts. + * + * **Note:** If one item is used, it will be shown as the selected one. If more than one item is used, the first one will be shown as selected unless + * there is an item with `selected` property set to `true`. + * @public + */ + @slot({ + type: HTMLElement, + invalidateOnChildChange: { + properties: true, + slots: false, + }, + }) + accounts!: Array; + + @i18n("@ui5/webcomponents-fiori") + static i18nBundle: I18nBundle; + + /** + * @private + */ + _selectedAccount!: UserMenuAccount; + + onBeforeRendering() { + this._selectedAccount = this.accounts.find(account => account.selected) || this.accounts[0]; + } + + _handleAvatarClick() { + this.fireDecoratorEvent("avatar-click"); + } + + _handleManageAccountClick() { + this.fireDecoratorEvent("manage-account-click"); + } + + _handleAddAccountClick() { + this.fireDecoratorEvent("add-account-click"); + } + + _handleAccountSwitch(e: CustomEvent<{ item: ListItemClickEventDetail & { associatedAccount: UserMenuAccount } }>) { + const eventPrevented = !this.fireDecoratorEvent("change-account", { + prevSelectedAccount: this._selectedAccount, + selectedAccount: e.detail.item.associatedAccount, + }); + + if (eventPrevented) { + return; + } + + this._selectedAccount.selected = false; + e.detail.item.associatedAccount.selected = true; + } + + _handleSignOutClick() { + const eventPrevented = !this.fireDecoratorEvent("sign-out-click"); + + if (eventPrevented) { + return; + } + + this._closeUserMenu(); + } + + _handleMenuItemClick(e: CustomEvent) { + const item = e.detail.item; + + if (!item._popover) { + const eventPrevented = !this.fireDecoratorEvent("item-click", { + "item": item, + }); + + if (!eventPrevented) { + item.fireEvent("close-menu"); + } + } else { + this._openItemSubMenu(item); + } + } + + _handleMenuItemClose() { + this._closeUserMenu(); + } + + _handlePopoverAfterClose() { + this.open = false; + } + + _openItemSubMenu(item: UserMenuItem) { + if (!item._popover || item._popover.open) { + return; + } + + item._popover.opener = item; + item._popover.open = true; + item.selected = true; + } + + _closeItemSubMenu(item: UserMenuItem) { + if (item && item._popover) { + const openedSibling = item._menuItems.find(menuItem => menuItem._popover && menuItem._popover.open); + if (openedSibling) { + this._closeItemSubMenu(openedSibling); + } + + item._popover.open = false; + item.selected = false; + } + } + + _closeUserMenu() { + this.open = false; + } + + get _otherAccounts() { + return this.accounts.filter(account => account !== this._selectedAccount); + } + + get _manageAccountButtonText() { + return UserMenu.i18nBundle.getText(USER_MENU_MANAGE_ACCOUNT_BUTTON_TXT); + } + + get _otherAccountsButtonText() { + return UserMenu.i18nBundle.getText(USER_MENU_OTHER_ACCOUNT_BUTTON_TXT); + } + + get _signOutButtonText() { + return UserMenu.i18nBundle.getText(USER_MENU_SIGN_OUT_BUTTON_TXT); + } + + get _editAvatarTooltip() { + return UserMenu.i18nBundle.getText(USER_MENU_EDIT_AVATAR_TXT); + } + + get _addAccountTooltip() { + return UserMenu.i18nBundle.getText(USER_MENU_ADD_ACCOUNT_TXT); + } + + get accessibleNameText() { + if (!this._selectedAccount) { + return ""; + } + return `${UserMenu.i18nBundle.getText(USER_MENU_POPOVER_ACCESSIBLE_NAME)} ${this._selectedAccount.titleText}`; + } +} + +UserMenu.define(); + +export default UserMenu; +export type { + UserMenuItemClickEventDetail, + UserMenuOtherAccountClickEventDetail, +}; diff --git a/packages/fiori/src/UserMenuAccount.ts b/packages/fiori/src/UserMenuAccount.ts new file mode 100644 index 000000000000..66034cd9523c --- /dev/null +++ b/packages/fiori/src/UserMenuAccount.ts @@ -0,0 +1,84 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import { customElement, property } from "@ui5/webcomponents-base/dist/decorators.js"; + +@customElement({ + tag: "ui5-user-menu-account", +}) +/** + * @class + * ### Overview + * + * The `ui5-user-menu-account` represents an account in the `ui5-user-menu`. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js";` + * + * @constructor + * @extends UI5Element + * @experimental + * @public + * @since 2.5.0 + */ +class UserMenuAccount extends UI5Element { + /** + * Defines the avatar image url of the user. + * + * @default "" + * @public + */ + @property({ type: String }) + avatarSrc?: string; + + /** + * Defines the avatar initials of the user. + * + * @default undefined + * @public + */ + @property({ type: String }) + avatarInitials?: string; + + /** + * Defines the title text of the user. + * + * @default "" + * @public + */ + @property({ type: String }) + titleText = ""; + + /** + * Defines additional text of the user. + * + * @default "" + * @public + */ + @property({ type: String }) + subtitleText = ""; + + /** + * Defines description of the user. + * + * @default "" + * @public + */ + @property({ type: String }) + description = ""; + + /** + * Defines if the user is selected. + * + * @default false + * @public + */ + @property({ type: Boolean }) + selected = false; + + get _initials() { + return this.avatarInitials || "undefined"; + } +} + +UserMenuAccount.define(); + +export default UserMenuAccount; diff --git a/packages/fiori/src/UserMenuItem.hbs b/packages/fiori/src/UserMenuItem.hbs new file mode 100644 index 000000000000..72d8b8ae22e8 --- /dev/null +++ b/packages/fiori/src/UserMenuItem.hbs @@ -0,0 +1 @@ +{{>include "../../main/src/MenuItem.hbs"}} diff --git a/packages/fiori/src/UserMenuItem.ts b/packages/fiori/src/UserMenuItem.ts new file mode 100644 index 000000000000..5646a19a6c13 --- /dev/null +++ b/packages/fiori/src/UserMenuItem.ts @@ -0,0 +1,55 @@ +import { customElement, slot } from "@ui5/webcomponents-base/dist/decorators.js"; +import MenuItem from "@ui5/webcomponents/dist/MenuItem.js"; + +import UserMenuItemTemplate from "./generated/templates/UserMenuItemTemplate.lit.js"; + +// Styles +import userMenuItemCss from "./generated/themes/UserMenuItem.css.js"; + +/** + * @class + * + * ### Overview + * + * `ui5-user-menu-item` is the item to use inside a `ui5-user-menu`. + * An arbitrary hierarchy structure can be represented by recursively nesting menu items. + * + * ### Usage + * + * `ui5-user-menu-item` represents a node in a `ui5-user-menu`. The user menu itself is rendered as a list, + * and each `ui5-menu-item` is represented by a menu item in that menu. Therefore, you should only use + * `ui5-user-menu-item` directly in your apps. The `ui5-menu` menu item is internal for the menu, and not intended for public use. + * + * ### ES6 Module Import + * + * `import "@ui5/webcomponents-fiori/dist/UserMenuItem.js";` + * @constructor + * @extends MenuItem + * @experimental + * @public + * @since 2.5.0 + */ +@customElement({ + tag: "ui5-user-menu-item", + template: UserMenuItemTemplate, + styles: [MenuItem.styles, userMenuItemCss], + dependencies: [...MenuItem.dependencies], +}) +class UserMenuItem extends MenuItem { + /** + * Defines the items of this component. + * + * **Note:** Use `ui5-user-menu-item` for the intended design. + * @public + */ + @slot({ "default": true, type: HTMLElement, invalidateOnChildChange: true }) + declare items: Array; + + get _menuItems() { + return this.items.filter(item => !item.isSeparator); + } +} + +UserMenuItem.define(); + +export default UserMenuItem; diff --git a/packages/fiori/src/bundle.esm.ts b/packages/fiori/src/bundle.esm.ts index 0b1a4f0f3e7b..0706e94ff785 100644 --- a/packages/fiori/src/bundle.esm.ts +++ b/packages/fiori/src/bundle.esm.ts @@ -37,6 +37,9 @@ import Timeline from "./Timeline.js"; import NavigationLayout from "./NavigationLayout.js"; import UploadCollection from "./UploadCollection.js"; import UploadCollectionItem from "./UploadCollectionItem.js"; +import UserMenu from "./UserMenu.js"; +import UserMenuAccount from "./UserMenuAccount.js"; +import UserMenuItem from "./UserMenuItem.js"; import ViewSettingsDialog from "./ViewSettingsDialog.js"; import Wizard from "./Wizard.js"; diff --git a/packages/fiori/src/i18n/messagebundle.properties b/packages/fiori/src/i18n/messagebundle.properties index c06fb5686dae..c7163819b46d 100644 --- a/packages/fiori/src/i18n/messagebundle.properties +++ b/packages/fiori/src/i18n/messagebundle.properties @@ -430,3 +430,21 @@ SIDE_NAVIGATION_OVERFLOW_ACCESSIBLE_NAME=More Items #XTXT: Accessible name for the Group Header SIDE_NAVIGATION_GROUP_HEADER_DESC=Group Header + +#XTXT: User menu other accounts button +USER_MENU_OTHER_ACCOUNT_BUTTON_TXT=Other accounts + +#XTXT: User menu manage account button +USER_MENU_MANAGE_ACCOUNT_BUTTON_TXT=Manage account + +#XTXT: User menu sign out button +USER_MENU_SIGN_OUT_BUTTON_TXT=Sign Out + +#XACT: ARIA User menu edit avatar +USER_MENU_EDIT_AVATAR_TXT=Edit avatar + +#XACT: ARIA add account +USER_MENU_ADD_ACCOUNT_TXT=Add account + +#XACT: ARIA information for the user menu popover +USER_MENU_POPOVER_ACCESSIBLE_NAME=User menu for diff --git a/packages/fiori/src/themes/UserMenu.css b/packages/fiori/src/themes/UserMenu.css new file mode 100644 index 000000000000..0f833d5d11cd --- /dev/null +++ b/packages/fiori/src/themes/UserMenu.css @@ -0,0 +1,65 @@ +.ui5-pm-rp { + width: 20rem; +} + +.ui5-pm-rp::part(header), +.ui5-pm-rp::part(content), +.ui5-pm-rp::part(footer) { + padding-inline: 0.5rem; +} + +.ui5-pm-rp::part(header) { + padding-top: 1rem; + box-shadow: none; +} + +.ui5-pm-rp::part(content) { + padding-top: 0; + padding-bottom: 0.5rem; +} + +.ui5-pm-rp::part(footer) { + padding-block: 0.5rem; +} + +.ui5-pm-selected-account { + display: flex; + align-items: center; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.ui5-pm-manage-account-btn { + margin-top: 0.5rem; +} + +.ui5-pm-other-accounts { + margin-bottom: 1rem; +} + +.ui5-pm-other-accounts::part(content) { + padding: 0; +} + +.ui5-pm-other-accounts-content { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 1rem; + padding-block: 1rem; +} + +.ui5-user-menu-account-header { + display: flex; + flex: 1; + justify-content: space-between; + align-items: center; +} + +.ui5-pm-footer { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; +} \ No newline at end of file diff --git a/packages/fiori/src/themes/UserMenuItem.css b/packages/fiori/src/themes/UserMenuItem.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/fiori/test/pages/UserMenu.html b/packages/fiori/test/pages/UserMenu.html new file mode 100644 index 000000000000..4e83323c9ef0 --- /dev/null +++ b/packages/fiori/test/pages/UserMenu.html @@ -0,0 +1,203 @@ + + + + + User Menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +User menu + + + + + + + + + + + + + + + + diff --git a/packages/website/docs/_components_pages/fiori/UserMenu/UserMenu.mdx b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenu.mdx new file mode 100644 index 000000000000..82121c625b06 --- /dev/null +++ b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenu.mdx @@ -0,0 +1,18 @@ +--- +slug: ../UserMenu +sidebar_class_name: newComponentBadge +--- + +import Basic from "../../../_samples/fiori/UserMenu/Basic/Basic.md"; + +import Advanced from "../../../_samples/fiori/UserMenu/Advanced/Advanced.md"; + +<%COMPONENT_OVERVIEW%> + +## Basic Sample + + +<%COMPONENT_METADATA%> + +## More Samples + diff --git a/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuAccount.mdx b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuAccount.mdx new file mode 100644 index 000000000000..db312b6fb517 --- /dev/null +++ b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuAccount.mdx @@ -0,0 +1,8 @@ +--- +slug: ../UserMenuAccount +sidebar_class_name: newComponentBadge +--- + +<%COMPONENT_OVERVIEW%> + +<%COMPONENT_METADATA%> \ No newline at end of file diff --git a/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuItem.mdx b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuItem.mdx new file mode 100644 index 000000000000..191e8d739e12 --- /dev/null +++ b/packages/website/docs/_components_pages/fiori/UserMenu/UserMenuItem.mdx @@ -0,0 +1,8 @@ +--- +slug: ../UserMenuItem +sidebar_class_name: newComponentBadge +--- + +<%COMPONENT_OVERVIEW%> + +<%COMPONENT_METADATA%> \ No newline at end of file diff --git a/packages/website/docs/_samples/fiori/UserMenu/Advanced/Advanced.md b/packages/website/docs/_samples/fiori/UserMenu/Advanced/Advanced.md new file mode 100644 index 000000000000..e11458217b49 --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Advanced/Advanced.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/fiori/UserMenu/Advanced/main.js b/packages/website/docs/_samples/fiori/UserMenu/Advanced/main.js new file mode 100644 index 000000000000..383e5f440388 --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Advanced/main.js @@ -0,0 +1,73 @@ +import "@ui5/webcomponents-fiori/dist/UserMenu.js"; +import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js"; +import "@ui5/webcomponents-fiori/dist/UserMenuItem.js"; + +import "@ui5/webcomponents/dist/Avatar.js"; +import "@ui5/webcomponents-fiori/dist/ShellBar.js"; +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents-icons/dist/action-settings.js"; +import "@ui5/webcomponents-icons/dist/globe.js"; +import "@ui5/webcomponents-icons/dist/collaborate.js"; +import "@ui5/webcomponents-icons/dist/official-service.js"; +import "@ui5/webcomponents-icons/dist/private.js"; +import "@ui5/webcomponents-icons/dist/accelerated.js"; + +const shellbar = document.getElementById("shellbar"); +const menu = document.getElementById("userMenu"); + + + +shellbar.addEventListener("ui5-profile-click", (event) => { + menu.opener = event.detail.targetRef; + menu.open = true; +}); + +menu.addEventListener("item-click", function (event) { + const item = event.detail.item.getAttribute("data-id"); + + switch (item) { + case "setting": + console.log("Open Setting Dialog"); + break; + case "privacy-policy": + console.log("Privacy Policy"); + break; + case "terms-of-use": + console.log("Terms of Use"); + break; + case "account-action1": + console.log("Product-specific account action 1"); + break; + case "account-action2": + console.log("Product-specific account action 2"); + break; + default: + console.log("Default"); + } +}); + +menu.addEventListener("avatar-click", function () { + console.log("Avatar clicked"); +}); + +menu.addEventListener("manage-account-click", function () { + console.log("Manage account clicked"); +}); + +menu.addEventListener("add-account-click", function () { + console.log("Add account clicked"); +}); + +menu.addEventListener("change-account", function (event) { + console.log("Change account account", event.detail); +}); + +menu.addEventListener("sign-out-click", function (event) { + console.log("Sign Out clicked"); + + const result = prompt("Sign Out", "Are you sure you want to sign out?"); + if (result) { + menu.open = false; + } + event.preventDefault(); +}); \ No newline at end of file diff --git a/packages/website/docs/_samples/fiori/UserMenu/Advanced/sample.html b/packages/website/docs/_samples/fiori/UserMenu/Advanced/sample.html new file mode 100644 index 000000000000..74b2c805afe1 --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Advanced/sample.html @@ -0,0 +1,64 @@ + + + + + + + User Menu sample + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/docs/_samples/fiori/UserMenu/Basic/Basic.md b/packages/website/docs/_samples/fiori/UserMenu/Basic/Basic.md new file mode 100644 index 000000000000..e11458217b49 --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Basic/Basic.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + diff --git a/packages/website/docs/_samples/fiori/UserMenu/Basic/main.js b/packages/website/docs/_samples/fiori/UserMenu/Basic/main.js new file mode 100644 index 000000000000..a6d77a1e3e9b --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Basic/main.js @@ -0,0 +1,56 @@ +import "@ui5/webcomponents-fiori/dist/UserMenu.js"; +import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js"; +import "@ui5/webcomponents-fiori/dist/UserMenuItem.js"; + +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents-icons/dist/action-settings.js"; +import "@ui5/webcomponents-icons/dist/globe.js"; +import "@ui5/webcomponents-icons/dist/collaborate.js"; +import "@ui5/webcomponents-icons/dist/official-service.js"; +import "@ui5/webcomponents-icons/dist/private.js"; +import "@ui5/webcomponents-icons/dist/accelerated.js"; + +const button = document.getElementById("btnOpenUserMenu"); +const menu = document.getElementById("userMenu"); + +button.addEventListener("click", function () { + menu.open = true; +}); + +menu.addEventListener("item-click", function (event) { + const item = event.detail.item.getAttribute("data-id"); + + switch (item) { + case "setting": + console.log("Open Setting Dialog"); + break; + case "privacy-policy": + console.log("Privacy Policy"); + break; + case "terms-of-use": + console.log("Terms of Use"); + break; + case "account-action1": + console.log("Product-specific account action 1"); + break; + case "account-action2": + console.log("Product-specific account action 2"); + break; + default: + console.log("Default"); + } +}); + +menu.addEventListener("avatar-click", function () { + console.log("Avatar clicked"); +}); + +menu.addEventListener("sign-out-click", function (event) { + console.log("Sign Out clicked"); + + const result = prompt("Sign Out", "Are you sure you want to sign out?"); + if (result) { + menu.open = false; + } + event.preventDefault(); +}); \ No newline at end of file diff --git a/packages/website/docs/_samples/fiori/UserMenu/Basic/sample.html b/packages/website/docs/_samples/fiori/UserMenu/Basic/sample.html new file mode 100644 index 000000000000..9a47ae25a984 --- /dev/null +++ b/packages/website/docs/_samples/fiori/UserMenu/Basic/sample.html @@ -0,0 +1,51 @@ + + + + + + + User Menu sample + + + + + + + + + +User menu + + + + + + + + + + + + + + + + + From 1b2eaedf6a9effdf728d3e41b9cf56e0c2aa1c96 Mon Sep 17 00:00:00 2001 From: Siyana Todorova <72251110+s-todorova@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:57:10 +0200 Subject: [PATCH 2/4] fix(ui5-tag): correct hover background color for Set 2 Color Scheme 9 (#10229) fixes: #10164 --- packages/main/src/themes/Tag.css | 10 ++++++++++ packages/main/src/themes/base/Tag-parameters.css | 6 ++++++ .../main/src/themes/sap_fiori_3_hcb/Tag-parameters.css | 2 ++ .../main/src/themes/sap_fiori_3_hcw/Tag-parameters.css | 2 ++ .../main/src/themes/sap_horizon_hcb/Tag-parameters.css | 2 ++ .../main/src/themes/sap_horizon_hcw/Tag-parameters.css | 2 ++ 6 files changed, 24 insertions(+) diff --git a/packages/main/src/themes/Tag.css b/packages/main/src/themes/Tag.css index d4b2f1404aae..dfcd8df803d9 100644 --- a/packages/main/src/themes/Tag.css +++ b/packages/main/src/themes/Tag.css @@ -483,6 +483,16 @@ color: var(--ui5-tag-set2-color-scheme-9-color); } +:host([interactive][design="Set2"][color-scheme="9"]) .ui5-tag-root:hover { + background-color: var(--ui5-tag-set2-color-scheme-9-hover-background); +} + +:host([interactive][design="Set2"][color-scheme="9"]) .ui5-tag-root:active { + background-color: var(--ui5-tag-set2-color-scheme-9-active-background); + border-color: var(--ui5-tag-set2-color-scheme-9-active-border); + color: var(--ui5-tag-set2-color-scheme-9-active-color); +} + :host([interactive][design="Set2"][color-scheme="10"]) .ui5-tag-root:hover { background-color: var(--ui5-tag-set2-color-scheme-10-hover-background); } diff --git a/packages/main/src/themes/base/Tag-parameters.css b/packages/main/src/themes/base/Tag-parameters.css index 8a3179974910..f9d62f5adb22 100644 --- a/packages/main/src/themes/base/Tag-parameters.css +++ b/packages/main/src/themes/base/Tag-parameters.css @@ -100,6 +100,12 @@ --ui5-tag-set2-color-scheme-9-background: var(--sapIndicationColor_9b); --ui5-tag-set2-color-scheme-9-border: var(--sapIndicationColor_9b_BorderColor); + --ui5-tag-set2-color-scheme-9-hover-background: var(--sapIndicationColor_9b_Hover_Background); + + --ui5-tag-set2-color-scheme-9-active-color: var(--sapIndicationColor_9_Active_TextColor); + --ui5-tag-set2-color-scheme-9-active-background: var(--sapIndicationColor_9_Active_Background); + --ui5-tag-set2-color-scheme-9-active-border: var(--sapIndicationColor_9_Active_BorderColor); + --ui5-tag-set2-color-scheme-10-color: var(--sapIndicationColor_10); --ui5-tag-set2-color-scheme-10-background: var(--sapIndicationColor_10b); --ui5-tag-set2-color-scheme-10-border: var(--sapIndicationColor_10b_BorderColor); diff --git a/packages/main/src/themes/sap_fiori_3_hcb/Tag-parameters.css b/packages/main/src/themes/sap_fiori_3_hcb/Tag-parameters.css index 5d4161a3de16..f8958597c686 100644 --- a/packages/main/src/themes/sap_fiori_3_hcb/Tag-parameters.css +++ b/packages/main/src/themes/sap_fiori_3_hcb/Tag-parameters.css @@ -91,6 +91,8 @@ --ui5-tag-set2-color-scheme-9-background: var(--sapIndicationColor_9_Background); --ui5-tag-set2-color-scheme-9-border: var(--sapIndicationColor_9_BorderColor); + --ui5-tag-set2-color-scheme-9-hover-background: var(--sapIndicationColor_9_Hover_Background); + --ui5-tag-set2-color-scheme-10-color: var(--sapIndicationColor_10_TextColor); --ui5-tag-set2-color-scheme-10-background: var(--sapIndicationColor_10_Background); --ui5-tag-set2-color-scheme-10-border: var(--sapIndicationColor_10_BorderColor); diff --git a/packages/main/src/themes/sap_fiori_3_hcw/Tag-parameters.css b/packages/main/src/themes/sap_fiori_3_hcw/Tag-parameters.css index 5d4161a3de16..f8958597c686 100644 --- a/packages/main/src/themes/sap_fiori_3_hcw/Tag-parameters.css +++ b/packages/main/src/themes/sap_fiori_3_hcw/Tag-parameters.css @@ -91,6 +91,8 @@ --ui5-tag-set2-color-scheme-9-background: var(--sapIndicationColor_9_Background); --ui5-tag-set2-color-scheme-9-border: var(--sapIndicationColor_9_BorderColor); + --ui5-tag-set2-color-scheme-9-hover-background: var(--sapIndicationColor_9_Hover_Background); + --ui5-tag-set2-color-scheme-10-color: var(--sapIndicationColor_10_TextColor); --ui5-tag-set2-color-scheme-10-background: var(--sapIndicationColor_10_Background); --ui5-tag-set2-color-scheme-10-border: var(--sapIndicationColor_10_BorderColor); diff --git a/packages/main/src/themes/sap_horizon_hcb/Tag-parameters.css b/packages/main/src/themes/sap_horizon_hcb/Tag-parameters.css index b9627688bac5..57073946a2fc 100644 --- a/packages/main/src/themes/sap_horizon_hcb/Tag-parameters.css +++ b/packages/main/src/themes/sap_horizon_hcb/Tag-parameters.css @@ -99,6 +99,8 @@ --ui5-tag-set2-color-scheme-9-background: var(--sapIndicationColor_9_Background); --ui5-tag-set2-color-scheme-9-border: var(--sapIndicationColor_9_BorderColor); + --ui5-tag-set2-color-scheme-9-hover-background: var(--sapIndicationColor_9_Hover_Background); + --ui5-tag-set2-color-scheme-10-color: var(--sapIndicationColor_10_TextColor); --ui5-tag-set2-color-scheme-10-background: var(--sapIndicationColor_10_Background); --ui5-tag-set2-color-scheme-10-border: var(--sapIndicationColor_10_BorderColor); diff --git a/packages/main/src/themes/sap_horizon_hcw/Tag-parameters.css b/packages/main/src/themes/sap_horizon_hcw/Tag-parameters.css index cdbe98331ecd..563990aba808 100644 --- a/packages/main/src/themes/sap_horizon_hcw/Tag-parameters.css +++ b/packages/main/src/themes/sap_horizon_hcw/Tag-parameters.css @@ -100,6 +100,8 @@ --ui5-tag-set2-color-scheme-9-background: var(--sapIndicationColor_9_Background); --ui5-tag-set2-color-scheme-9-border: var(--sapIndicationColor_9_BorderColor); + --ui5-tag-set2-color-scheme-9-hover-background: var(--sapIndicationColor_9_Hover_Background); + --ui5-tag-set2-color-scheme-10-color: var(--sapIndicationColor_10_TextColor); --ui5-tag-set2-color-scheme-10-background: var(--sapIndicationColor_10_Background); --ui5-tag-set2-color-scheme-10-border: var(--sapIndicationColor_10_BorderColor); From bf4c8596611bf61435809b1bf42766db83aea002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cahit=20G=C3=BCrg=C3=BCc?= Date: Sun, 1 Dec 2024 00:34:19 +0100 Subject: [PATCH 3/4] feat(table): table virtualization (#10182) feat(ui5-table): table virtualization --- .../main/cypress/specs/TableVirtualizer.cy.ts | 246 ++++++++++++++ packages/main/src/Table.hbs | 7 +- packages/main/src/Table.ts | 29 +- packages/main/src/TableGrowing.ts | 2 +- packages/main/src/TableNavigation.ts | 8 +- packages/main/src/TableRow.ts | 12 + packages/main/src/TableUtils.ts | 11 + packages/main/src/TableVirtualizer.ts | 299 ++++++++++++++++++ packages/main/src/bundle.esm.ts | 1 + packages/main/src/themes/Table.css | 17 +- packages/main/src/themes/TableCellBase.css | 2 +- packages/main/src/themes/TableRow.css | 5 + packages/main/test/pages/Table.html | 37 +-- .../main/test/pages/TableVirtualizer.html | 99 ++++++ packages/main/test/specs/Table.spec.js | 8 +- 15 files changed, 732 insertions(+), 51 deletions(-) create mode 100644 packages/main/cypress/specs/TableVirtualizer.cy.ts create mode 100644 packages/main/src/TableVirtualizer.ts create mode 100644 packages/main/test/pages/TableVirtualizer.html diff --git a/packages/main/cypress/specs/TableVirtualizer.cy.ts b/packages/main/cypress/specs/TableVirtualizer.cy.ts new file mode 100644 index 000000000000..458833f1b4a8 --- /dev/null +++ b/packages/main/cypress/specs/TableVirtualizer.cy.ts @@ -0,0 +1,246 @@ +import { html } from "lit"; +import "../../src/Table.js"; +import "../../src/TableHeaderRow.js"; +import "../../src/TableHeaderCell.js"; +import "../../src/TableRow.js"; +import "../../src/TableCell.js"; +import "../../src/TableVirtualizer.js"; + +import type Table from "../../src/Table.js"; +import type TableVirtualizer from "../../src/TableVirtualizer.js"; +import type { RangeChangeEventDetail } from "../../src/TableVirtualizer.js"; + +describe("TableVirtualizer", () => { + function mountTable(rowHeight = 50, rowCount = 100, tableHeight = 250) { + cy.mount(html` + + + + + `); + + cy.get("[ui5-table-virtualizer]").as("virtualizer").then($virtualizer => { + $virtualizer[0].addEventListener("range-change", updateRows as EventListener); + $virtualizer[0].reset(); + }); + + cy.get("[ui5-table]").shadow().find("#table").as("innerTable"); + cy.get("[ui5-table]").children("ui5-table-row").as("rows"); + cy.get("@rows").first().as("firstRow"); + cy.get("@rows").last().as("lastRow"); + } + + function updateRows(e: CustomEvent) { + const table = document.querySelector("ui5-table") as Table; + for (let i = e.detail.first; i < e.detail.last; i++) { + const row = table.rows[i - e.detail.first]; + if (row) { + row.position = i; + row.cells[0].textContent = `${i}`; + } else { + const newRow = `${i}`; + table.insertAdjacentHTML("beforeend", newRow); + } + } + for (let i = e.detail.last; i < table.rows.length; i++) { + table.rows[i].remove(); + } + } + + function testRows(start: number, end: number) { + for (let i = start; i < end; i++) { + cy.get("[ui5-table-row]") + .eq(i - start) + .should("have.attr", "position", `${i}`) + .should("have.attr", "aria-rowindex", `${i + 1}`) + .find("ui5-table-cell") + .should("have.text", `${i}`); + } + + cy.get("[ui5-table]") + .shadow() + .find("#spacer") + .then($spacer => { + const transform = getComputedStyle($spacer[0]).transform; + return new DOMMatrix(transform); + }) + .its("f") + .should("equal", start * 50); + } + + describe("Rendering", () => { + it("should render initially 5 rows", () => { + mountTable(); + + cy.get("@innerTable") + .should("have.attr", "aria-rowcount", "100") + .should("have.css", "overflow-y", "auto") + .then($innerTable => window.getComputedStyle($innerTable[0])) + .invoke("getPropertyValue", "--row-height") + .and("equal", "50px"); + + cy.get("@innerTable") + .children("#rows") + .should("have.css", "height", "5000px"); + + cy.get("@innerTable") + .find("#spacer") + .should("have.css", "will-change", "transform"); + + testRows(0, 5); + }); + + it("should react to rowHeight changes", () => { + mountTable(); + + cy.get("@virtualizer").invoke("attr", "row-height", "60"); + + cy.get("@innerTable") + .then($innerTable => window.getComputedStyle($innerTable[0])) + .invoke("getPropertyValue", "--row-height") + .and("equal", "60px"); + + cy.get("@innerTable") + .children("#rows") + .should("have.css", "height", "6000px"); + }); + + it("should react to rowCount changes", () => { + mountTable(); + + cy.get("@virtualizer").invoke("attr", "row-count", "200"); + + cy.get("@innerTable") + .should("have.attr", "aria-rowcount", "200") + .children("#rows") + .should("have.css", "height", "10000px"); + }); + + it("should update rows on scroll", () => { + mountTable(); + + cy.get("@innerTable").scrollTo(0, 250); + testRows(5, 10); + + cy.get("@innerTable").scrollTo("bottom"); + testRows(95, 100); + + cy.get("@innerTable").scrollTo(0, 4000); + testRows(80, 85); + + cy.get("@innerTable").scrollTo("top"); + testRows(0, 5); + }); + + it("should update rows via keyboard while focus is on the row", () => { + mountTable(); + + cy.get("@firstRow").realClick(); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("PageDown"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("PageDown"); + testRows(5, 10); + + cy.realPress("ArrowDown"); + testRows(6, 11); + + cy.realPress("PageUp"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("PageUp"); + testRows(1, 6); + + cy.realPress("ArrowUp"); + testRows(0, 5); + + cy.realPress("End"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("End"); + testRows(95, 100); + + cy.realPress("Home"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("Home"); + testRows(0, 5); + }); + + it("should update rows via keyboard while focus is on the cell", () => { + mountTable(); + + cy.get("@firstRow").find("ui5-table-cell").first().as("firstRowFirstCell"); + cy.get("@lastRow").find("ui5-table-cell").first().as("lastRowFirstCell"); + + cy.get("@firstRow").realClick().realPress("ArrowRight"); + cy.get("@firstRowFirstCell").should("have.focus"); + + cy.realPress("PageDown"); + cy.get("@lastRowFirstCell").should("have.focus"); + + cy.realPress("PageDown"); + testRows(5, 10); + + cy.realPress("ArrowDown"); + testRows(6, 11); + + cy.realPress("PageUp"); + cy.get("@firstRowFirstCell").should("have.focus"); + + cy.realPress("PageUp"); + testRows(1, 6); + + cy.realPress("ArrowUp"); + testRows(0, 5); + + cy.realPress("End"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("End"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("End"); + testRows(95, 100); + + cy.get("@lastRow").realPress("ArrowRight"); + cy.get("@lastRowFirstCell").should("have.focus"); + + cy.realPress("Home"); + cy.get("@lastRow").should("have.focus"); + + cy.realPress("Home"); + cy.get("@firstRow").should("have.focus"); + + cy.realPress("Home"); + testRows(0, 5); + }); + + it("should have the reset() API", () => { + mountTable(); + + cy.get("@virtualizer").then($virtualizer => { + $virtualizer[0].addEventListener("range-change", cy.stub().as("rangeChange")); + }); + + cy.get("@innerTable").scrollTo("bottom"); + cy.get("@rangeChange").should("have.been.calledOnce"); + testRows(95, 100); + + cy.get("@virtualizer").invoke("get", 0).invoke("reset"); + cy.get("@rangeChange").should("have.been.calledTwice"); + testRows(0, 5); + + cy.get("@virtualizer").invoke("get", 0).invoke("reset"); + cy.get("@rangeChange").should("have.been.calledThrice"); + + cy.get("@virtualizer").invoke("attr", "row-count", "2"); + cy.get("@virtualizer").invoke("get", 0).invoke("reset"); + testRows(0, 2); + }); + }); +}); diff --git a/packages/main/src/Table.hbs b/packages/main/src/Table.hbs index 554bb45e9539..4688ba22e56c 100644 --- a/packages/main/src/Table.hbs +++ b/packages/main/src/Table.hbs @@ -3,10 +3,15 @@
- +
+
+ +
+
{{#unless rows.length}} diff --git a/packages/main/src/Table.ts b/packages/main/src/Table.ts index 4ef5af1314a3..f2ae4bd40654 100644 --- a/packages/main/src/Table.ts +++ b/packages/main/src/Table.ts @@ -25,6 +25,7 @@ import { import BusyIndicator from "./BusyIndicator.js"; import TableCell from "./TableCell.js"; import { findVerticalScrollContainer, scrollElementIntoView, isFeature } from "./TableUtils.js"; +import type TableVirtualizer from "./TableVirtualizer.js"; /** * Interface for components that can be slotted inside the features slot of the ui5-table. @@ -42,7 +43,7 @@ interface ITableFeature extends UI5Element { /** * Called when the table finished rendering. */ - onTableRendered?(): void; + onTableAfterRendering?(): void; } /** @@ -195,7 +196,7 @@ class Table extends UI5Element { type: HTMLElement, "default": true, invalidateOnChildChange: { - properties: ["navigated"], + properties: ["navigated", "position"], slots: false, }, }) @@ -347,13 +348,17 @@ class Table extends UI5Element { } onAfterRendering(): void { - this.features.forEach(feature => feature.onTableRendered?.()); + this.features.forEach(feature => feature.onTableAfterRendering?.()); } _getSelection(): TableSelection | undefined { return this.features.find(feature => isFeature(feature, "TableSelection")) as TableSelection; } + _getVirtualizer(): TableVirtualizer | undefined { + return this.features.find(feature => isFeature(feature, "TableVirtualizer")) as TableVirtualizer; + } + _onEvent(e: Event) { const composedPath = e.composedPath(); const eventOrigin = composedPath[0] as HTMLElement; @@ -404,6 +409,10 @@ class Table extends UI5Element { } _onfocusin(e: FocusEvent) { + if (e.target === this) { + return; + } + // Handles focus in the table, when the focus is below a sticky element scrollElementIntoView(this._scrollContainer, e.target as HTMLElement, this._stickyElements, this.effectiveDir === "rtl"); } @@ -452,7 +461,7 @@ class Table extends UI5Element { } _isFeature(feature: any) { - return Boolean(feature.onTableActivate && feature.onTableRendered); + return Boolean(feature.onTableActivate && feature.onTableAfterRendering); } _isGrowingFeature(feature: any) { @@ -464,6 +473,7 @@ class Table extends UI5Element { } get styles() { + const virtualizer = this._getVirtualizer(); const headerStyleMap = this.headerRow?.[0]?.cells?.reduce((headerStyles, headerCell) => { if (headerCell.horizontalAlign !== undefined && !headerCell._popin) { headerStyles[`--horizontal-align-${headerCell._individualSlot}`] = headerCell.horizontalAlign; @@ -473,8 +483,13 @@ class Table extends UI5Element { return { table: { "grid-template-columns": this._gridTemplateColumns, + "--row-height": virtualizer ? `${virtualizer.rowHeight}px` : "auto", ...headerStyleMap, }, + spacer: { + "transform": virtualizer?._getTransform(), + "will-change": virtualizer && "transform", + }, }; } @@ -537,6 +552,10 @@ class Table extends UI5Element { return getEffectiveAriaLabelText(this) || undefined; } + get _ariaRowCount() { + return this._getVirtualizer()?.rowCount || undefined; + } + get _ariaMultiSelectable() { const selection = this._getSelection(); return (selection?.isSelectable() && this.rows.length) ? selection.isMultiSelect() : undefined; @@ -558,7 +577,7 @@ class Table extends UI5Element { } get _scrollContainer() { - return findVerticalScrollContainer(this._tableElement); + return this._getVirtualizer() ? this._tableElement : findVerticalScrollContainer(this); } get isTable() { diff --git a/packages/main/src/TableGrowing.ts b/packages/main/src/TableGrowing.ts index 60edcf93e1b0..26c3e339e414 100644 --- a/packages/main/src/TableGrowing.ts +++ b/packages/main/src/TableGrowing.ts @@ -148,7 +148,7 @@ class TableGrowing extends UI5Element implements ITableGrowing { this._shouldFocusRow = false; } - onTableRendered(): void { + onTableAfterRendering(): void { // Focus the first row after growing, when the growing button is used if (this._shouldFocusRow) { this._shouldFocusRow = false; diff --git a/packages/main/src/TableNavigation.ts b/packages/main/src/TableNavigation.ts index afda84d60e37..e5afc6e1078c 100644 --- a/packages/main/src/TableNavigation.ts +++ b/packages/main/src/TableNavigation.ts @@ -115,7 +115,7 @@ class TableNavigation extends TableExtension { } this._ignoreFocusIn = ignoreFocusIn; - element.focus(); + element.focus({ preventScroll: element === this._table._beforeElement || element === this._table._afterElement }); if (element instanceof HTMLInputElement) { element.select(); } @@ -210,6 +210,11 @@ class TableNavigation extends TableExtension { this._gridWalker.setCurrent(eventOrigin); } + this._table._getVirtualizer()?._onKeyDown(e); + if (e.defaultPrevented) { + return; + } + const keydownHandlerName = `_handle${e.code}` as keyof TableNavigation; const keydownHandler = this[keydownHandlerName] as (e: KeyboardEvent, eventOrigin: HTMLElement) => void | false; if (typeof keydownHandler === "function" && keydownHandler.call(this, e, eventOrigin) === undefined) { @@ -284,6 +289,7 @@ class TableNavigation extends TableExtension { if (this._table.loading) { this._table._loadingElement.focus(); } else { + this._getNavigationItemsOfGrid(); this._gridWalker.setColPos(0); this._focusCurrentItem(); } diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 9a02f6043209..cfafe418a377 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -60,6 +60,15 @@ class TableRow extends TableRowBase { @property() rowKey = ""; + /** + * Defines the position of the row respect to the total number of rows within the table when the ui5-table-virtualizer feature is used. + * + * @default -1 + * @public + */ + @property({ type: Number }) + position = -1; + /** * Defines the interactive state of the row. * @@ -84,6 +93,9 @@ class TableRow extends TableRowBase { onBeforeRendering() { super.onBeforeRendering(); this.toggleAttribute("_interactive", this._isInteractive); + if (this.position !== -1) { + this.setAttribute("aria-rowindex", `${this.position + 1}`); + } if (this._renderNavigated && this.navigated) { this.setAttribute("aria-current", "true"); } else { diff --git a/packages/main/src/TableUtils.ts b/packages/main/src/TableUtils.ts index 21f8d0089928..4b93441f787a 100644 --- a/packages/main/src/TableUtils.ts +++ b/packages/main/src/TableUtils.ts @@ -77,6 +77,16 @@ const isFeature = (element: any, identifier: string): element is T => { return element.identifier === identifier; }; +const throttle = (callback: () => void) => { + let timer: number; + return () => { + cancelAnimationFrame(timer); + timer = requestAnimationFrame(() => { + callback(); + }); + }; +}; + export { isInstanceOfTable, isSelectionCheckbox, @@ -85,4 +95,5 @@ export { findVerticalScrollContainer, scrollElementIntoView, isFeature, + throttle, }; diff --git a/packages/main/src/TableVirtualizer.ts b/packages/main/src/TableVirtualizer.ts new file mode 100644 index 000000000000..12b807b3e775 --- /dev/null +++ b/packages/main/src/TableVirtualizer.ts @@ -0,0 +1,299 @@ +/* eslint-disable no-bitwise */ +import { + isUp, + isUpShift, + isDown, + isDownShift, + isPageUp, + isPageDown, + isHome, + isEnd, + isTabNext, + isTabPrevious, +} from "@ui5/webcomponents-base/dist/Keys.js"; +import UI5Element, { type InvalidationInfo } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event.js"; +import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; +import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js"; +import { throttle } from "./TableUtils.js"; +import type { ITableFeature } from "./Table.js"; +import type Table from "./Table.js"; + +enum TabBlocking { + None = 0, + Next = 1, + Previous = 2, + Released = 4, + NextReleased = Next | Released, + PreviousReleased = Previous | Released, +} + +/** + * Fired when the virtualizer is changed by user interaction e.g. on scrolling. + * @param number {first} The 0-based index of the first children currently rendered + * @param number {last} The 0-based index of the last children currently rendered + * @public + */ +type RangeChangeEventDetail = { + first: number, + last: number, +}; + +/** + * @class + * + * ### Overview + * + * The `ui5-table-virtualizer` component is used inside the `ui5-table` to virtualize the table rows, if the `overflowMode` property of the table is set to 'Scroll'. + * It is responsible for rendering only the rows that are visible in the viewport and updating them on scroll. + * This allows large numbers of rows to exist, but maintain high performance by only paying the cost for those that are currently visible. + * + * **Note:** The maximum number of virtualized rows is limited by browser constraints, specifically the maximum supported height for a DOM element. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents/dist/TableVirtualizer.js";` + * + * @constructor + * @extends UI5Element + * @public + * @experimental This component is not intended to be used in a productive enviroment. The API is under development and may be changed in the future. + */ +@customElement({ tag: "ui5-table-virtualizer" }) + +/** + * Fired when the virtualizer is changed by user interaction e.g. on scrolling. + * + * @param {number} first The 0-based index of the first children currently rendered + * @param {number} last The 0-based index of the last children currently rendered + * @public + */ +@event("range-change", { + detail: { + /** + * @public + */ + first: { type: Number }, + /** + * @public + */ + last: { type: Number }, + }, +}) + +class TableVirtualizer extends UI5Element implements ITableFeature { + /** + * Defines the height of the rows in the table. + * + * **Note:** This property is mandatory for the virtualization to work properly. + * + * @default 45 + * @public + */ + @property({ type: Number }) + rowHeight = 45; + + /** + * Defines the total count of rows in the table. + * + * **Note:** This property is mandatory for the virtualization to work properly. + * + * @default 100 + * @public + */ + @property({ type: Number }) + rowCount = 100; + + /** + * Defines the count of extra rows to be rendered at the top and bottom of the table. + * + * **Note:** This property is experimental and may be changed or deleted in the future. + * + * @default 0 + * @public + */ + @property({ type: Number }) + extraRows = 0; + + readonly identifier = "TableVirtualizer"; + + _table?: Table; + _lastRowPosition: number = 0; + _firstRowPosition: number = 0; + _visibleRowCount: number = 0; + _tabBlockingState: TabBlocking = TabBlocking.None; + _onRowInvalidateBound: (invalidationInfo: InvalidationInfo) => void; + _onScrollBound: () => void; + + constructor() { + super(); + this._onScrollBound = throttle(this._onScroll.bind(this)); + this._onRowInvalidateBound = this._onRowInvalidate.bind(this); + } + + onTableActivate(table: Table): void { + this._table = table; + this._scrollContainer.addEventListener("scroll", this._onScrollBound, { passive: true }); + this._onScroll(); + } + + onAfterRendering(): void { + this._table && this._table._invalidate++; + } + + onTableAfterRendering(): void { + if (!this._table) { + return; + } + + this._updateRowsHeight(); + if (this._tabBlockingState & TabBlocking.Released) { + const tabBlockingRow = this._table.rows.at(this._tabBlockingState & TabBlocking.Next ? -1 : 0) as HTMLElement; + const tabForwardingElement = getTabbableElements(tabBlockingRow).at(this._tabBlockingState & TabBlocking.Next ? 0 : -1); + this._tabBlockingState = TabBlocking.None; + (tabForwardingElement || tabBlockingRow).focus(); + } + } + + onExitDOM(): void { + this._scrollContainer.removeEventListener("scroll", this._onScrollBound); + this._table = undefined; + } + + /** + * Resets the virtualizer to its initial state and triggers the `range-change` event. + * @public + */ + reset(): void { + this._lastRowPosition = -1; + this._firstRowPosition = -1; + if (this._scrollContainer.scrollTop > 0) { + this._scrollContainer.scrollTop = 0; + } else { + this._onScroll(); + } + } + + get _scrollContainer() { + return this._table!._tableElement; + } + + get _rowsContainer() { + return this._table!.shadowRoot!.getElementById("rows")!; + } + + _onScroll(): void { + if (!this._table) { + return; + } + + const headerRow = this._table.headerRow[0]; + const headerHeight = headerRow.offsetHeight; + let scrollTop = this._scrollContainer.scrollTop; + let scrollableHeight = this._scrollContainer.clientHeight; + if (headerRow.sticky) { + scrollableHeight = Math.max(0, scrollableHeight - headerHeight); + } else { + scrollTop = Math.max(0, scrollTop - headerHeight); + } + + this._visibleRowCount = Math.ceil(scrollableHeight / this.rowHeight); + let firstRowPosition = Math.floor(scrollTop / this.rowHeight) - this.extraRows; + firstRowPosition = Math.max(0, firstRowPosition); + let lastRowPosition = Math.max(0, firstRowPosition + this._visibleRowCount + 2 * this.extraRows); + lastRowPosition = Math.min(lastRowPosition, this.rowCount); + + if (this._firstRowPosition === firstRowPosition && this._lastRowPosition === lastRowPosition) { + return; + } + + this._lastRowPosition = lastRowPosition; + this._firstRowPosition = firstRowPosition; + this.fireDecoratorEvent("range-change", { + first: firstRowPosition, + last: lastRowPosition, + }); + } + + _updateRowsHeight() { + const rowsHeight = this.rowCount * this.rowHeight; + this._rowsContainer.style.height = `${rowsHeight}px`; + } + + _getTransform() { + if (!this._table) { + return; + } + + const firstRow = this._table.rows[0]; + if (firstRow && firstRow.position > 0) { + const transform = firstRow.position * this.rowHeight; + return `translateY(${transform}px)`; + } + } + + _onRowInvalidate(invalidationInfo: InvalidationInfo) { + if (invalidationInfo.name === "position") { + invalidationInfo.target.detachInvalidate(this._onRowInvalidateBound); + this._tabBlockingState |= TabBlocking.Released; + } + } + + _onKeyDown(e: KeyboardEvent) { + if (!this._table) { + return; + } + + let scrollTopChange = 0; + const rows = this._table.rows; + const firstRow = rows[0]; + const lastRow = rows[rows.length - 1]; + const hasDataBeforeFirstRow = firstRow.position !== 0; + const hasDataAfterLastRow = lastRow.position !== this.rowCount - 1; + const tableNavigation = this._table._tableNavigation!; + const activeElement = getActiveElement() as HTMLElement; + + if (isTabNext(e) && hasDataAfterLastRow && getTabbableElements(this._rowsContainer).pop() === activeElement) { + this._tabBlockingState = TabBlocking.Next; + lastRow.attachInvalidate(this._onRowInvalidateBound); + scrollTopChange = this.rowHeight; + } else if (isTabPrevious(e) && hasDataBeforeFirstRow && getTabbableElements(this._rowsContainer).shift() === activeElement) { + this._tabBlockingState = TabBlocking.Previous; + firstRow.attachInvalidate(this._onRowInvalidateBound); + scrollTopChange = this.rowHeight * -1; + } else if (hasDataAfterLastRow && tableNavigation._getNavigationItemsOfRow(lastRow).includes(activeElement)) { + if (isDown(e) || isDownShift(e)) { + scrollTopChange = this.rowHeight; + } else if (isPageDown(e)) { + scrollTopChange = this._visibleRowCount * this.rowHeight; + } else if (isEnd(e) && activeElement === lastRow) { + scrollTopChange = this.rowCount * this.rowHeight; + } + } else if (hasDataBeforeFirstRow && tableNavigation._getNavigationItemsOfRow(firstRow).includes(activeElement)) { + if (isUp(e) || isUpShift(e)) { + scrollTopChange = this.rowHeight * -1; + } else if (isPageUp(e)) { + scrollTopChange = this._visibleRowCount * this.rowHeight * -1; + } else if (isHome(e) && activeElement === firstRow) { + scrollTopChange = this.rowCount * this.rowHeight * -1; + } + } + + if (scrollTopChange) { + const scrollTop = this._table.scrollTop; + this._scrollContainer.scrollTop += scrollTopChange; + if (this._scrollContainer.scrollTop !== scrollTop) { + e.preventDefault(); + } + } + } +} + +TableVirtualizer.define(); + +export default TableVirtualizer; + +export type { + RangeChangeEventDetail, +}; diff --git a/packages/main/src/bundle.esm.ts b/packages/main/src/bundle.esm.ts index 8f67245661ac..2546061c0488 100644 --- a/packages/main/src/bundle.esm.ts +++ b/packages/main/src/bundle.esm.ts @@ -56,6 +56,7 @@ import TableHeaderCell from "./TableHeaderCell.js"; import TableHeaderRow from "./TableHeaderRow.js"; import TableGrowing from "./TableGrowing.js"; import TableSelection from "./TableSelection.js"; +import TableVirtualizer from "./TableVirtualizer.js"; import Icon from "./Icon.js"; import Input from "./Input.js"; import SuggestionItemCustom from "./SuggestionItemCustom.js"; diff --git a/packages/main/src/themes/Table.css b/packages/main/src/themes/Table.css index 7bb0d299645f..2c58f8b6cc5b 100644 --- a/packages/main/src/themes/Table.css +++ b/packages/main/src/themes/Table.css @@ -11,9 +11,22 @@ #table { display: grid; - grid-auto-rows: minmax(min-content, auto); + grid-auto-rows: minmax(min-content, max-content); background: var(--sapList_Background); - height: -moz-fill, -webkit-fill-available, 100%; +} + +:host([overflow-mode="Scroll"]) #table { + overflow-x: auto; + height: 100%; + height: -webkit-fill-available; + height: fill-available; +} + +#rows, #spacer { + display: grid; + grid-template-rows: min-content; + grid-template-columns: subgrid; + grid-column: 1 / -1; } #nodata-cell { diff --git a/packages/main/src/themes/TableCellBase.css b/packages/main/src/themes/TableCellBase.css index 19345e623998..df6912f2afee 100644 --- a/packages/main/src/themes/TableCellBase.css +++ b/packages/main/src/themes/TableCellBase.css @@ -10,7 +10,7 @@ box-sizing: border-box; } -:host(:focus) { +:host([tabindex]:focus) { outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor); outline-offset: calc(-1 * var(--sapContent_FocusWidth)); } diff --git a/packages/main/src/themes/TableRow.css b/packages/main/src/themes/TableRow.css index 1d4257ffe777..a2bcd93fb63a 100644 --- a/packages/main/src/themes/TableRow.css +++ b/packages/main/src/themes/TableRow.css @@ -2,6 +2,11 @@ background: var(--sapList_Background); } +:host([position]) { + height: var(--row-height); + overflow: clip; +} + :host([aria-selected=true]) { background-color: var(--sapList_SelectionBackgroundColor); border-bottom: var(--sapList_BorderWidth) solid var(--sapList_SelectionBorderColor); diff --git a/packages/main/test/pages/Table.html b/packages/main/test/pages/Table.html index 389b196e0435..ee8426b2f66d 100644 --- a/packages/main/test/pages/Table.html +++ b/packages/main/test/pages/Table.html @@ -23,7 +23,7 @@ label-interval="0"> - + @@ -47,41 +47,6 @@ 4.5 KG 1249 EUR - - Notebook Basic 17
HT-1002
- Technocom - 32 x 21 x 4 cm - 3.7 KG - 29 EUR -
- - Notebook Basic 18
HT-1003
- Technocom - 32 x 21 x 4 cm - 3.7 KG - 29 EUR -
- - Notebook Basic 19
HT-1004
- Technocom - 32 x 21 x 4 cm - 3.7 KG - 29 EUR -
- - Notebook Basic 20
HT-1005
- Technocom - 32 x 21 x 4 cm - 3.7 KG - 29 EUR -
- - Notebook Basic 21
HT-1006
- Technocom - 32 x 21 x 4 cm - 3.7 KG - 29 EUR -
diff --git a/packages/main/test/pages/TableVirtualizer.html b/packages/main/test/pages/TableVirtualizer.html new file mode 100644 index 000000000000..aa07112cd8ea --- /dev/null +++ b/packages/main/test/pages/TableVirtualizer.html @@ -0,0 +1,99 @@ + + + + + + + Table (in development) + + + + + + + + + + + + My Selectable Products (1000) + + + + + + + + Product + Supplier + Dimensions + Weight + Price + + + + + + + + \ No newline at end of file diff --git a/packages/main/test/specs/Table.spec.js b/packages/main/test/specs/Table.spec.js index 6469745f29c7..9ffe6f8d7418 100644 --- a/packages/main/test/specs/Table.spec.js +++ b/packages/main/test/specs/Table.spec.js @@ -440,7 +440,7 @@ describe("Table - Horizontal alignment of cells", async () => { for (const row of tableRows) { const rowCells = await row.$$("ui5-table-cell"); const justifyContent = await rowCells[index].getCSSProperty("justify-content"); - + assert.equal(justifyContent.value, "normal", "justify-content correctly set."); } } @@ -563,7 +563,7 @@ describe("Table - Horizontal Scrolling", async () => { return { fixedX: row.shadowRoot.querySelector("#selection-cell").getBoundingClientRect().x, leftOffset: table.shadowRoot.querySelector("#table")?.scrollLeft || 0 - }; + }; }); assert.equal(leftOffset, 0, "Table is not scrolled horizontally"); @@ -576,8 +576,8 @@ describe("Table - Horizontal Scrolling", async () => { const row = document.getElementById("firstRow"); return { fixedX2: row.shadowRoot.querySelector("#selection-cell").getBoundingClientRect().x, - leftOffset2: table.scrollLeft || 0 - }; + leftOffset2: table._tableElement.scrollLeft || 0 + }; }); assert.ok(leftOffset2 > 0, "Table is scrolled horizontally"); From 2cca3a267da8278b4a275265dc905806d7957848 Mon Sep 17 00:00:00 2001 From: SAP LX Lab Service Account Date: Sun, 1 Dec 2024 00:50:46 -0800 Subject: [PATCH 4/4] Translation Delivery (#10266) [INTERNAL] Translation delivery: commit by LX Lab Change-Id: If40ca93435ff30d36a5ca0b4711a079290d57e90 --- packages/main/src/i18n/messagebundle_ar.properties | 2 ++ packages/main/src/i18n/messagebundle_bg.properties | 2 ++ packages/main/src/i18n/messagebundle_ca.properties | 2 ++ packages/main/src/i18n/messagebundle_cnr.properties | 2 ++ packages/main/src/i18n/messagebundle_cs.properties | 2 ++ packages/main/src/i18n/messagebundle_cy.properties | 2 ++ packages/main/src/i18n/messagebundle_da.properties | 2 ++ packages/main/src/i18n/messagebundle_de.properties | 2 ++ packages/main/src/i18n/messagebundle_el.properties | 2 ++ packages/main/src/i18n/messagebundle_en_GB.properties | 2 ++ packages/main/src/i18n/messagebundle_es.properties | 2 ++ packages/main/src/i18n/messagebundle_es_MX.properties | 2 ++ packages/main/src/i18n/messagebundle_et.properties | 2 ++ packages/main/src/i18n/messagebundle_fi.properties | 2 ++ packages/main/src/i18n/messagebundle_fr.properties | 2 ++ packages/main/src/i18n/messagebundle_fr_CA.properties | 2 ++ packages/main/src/i18n/messagebundle_hi.properties | 2 ++ packages/main/src/i18n/messagebundle_hr.properties | 2 ++ packages/main/src/i18n/messagebundle_hu.properties | 2 ++ packages/main/src/i18n/messagebundle_id.properties | 2 ++ packages/main/src/i18n/messagebundle_it.properties | 2 ++ packages/main/src/i18n/messagebundle_iw.properties | 2 ++ packages/main/src/i18n/messagebundle_ja.properties | 2 ++ packages/main/src/i18n/messagebundle_kk.properties | 2 ++ packages/main/src/i18n/messagebundle_ko.properties | 2 ++ packages/main/src/i18n/messagebundle_lt.properties | 2 ++ packages/main/src/i18n/messagebundle_lv.properties | 2 ++ packages/main/src/i18n/messagebundle_mk.properties | 2 ++ packages/main/src/i18n/messagebundle_ms.properties | 2 ++ packages/main/src/i18n/messagebundle_nl.properties | 2 ++ packages/main/src/i18n/messagebundle_no.properties | 2 ++ packages/main/src/i18n/messagebundle_pl.properties | 2 ++ packages/main/src/i18n/messagebundle_pt.properties | 2 ++ packages/main/src/i18n/messagebundle_pt_PT.properties | 2 ++ packages/main/src/i18n/messagebundle_ro.properties | 2 ++ packages/main/src/i18n/messagebundle_ru.properties | 2 ++ packages/main/src/i18n/messagebundle_sh.properties | 2 ++ packages/main/src/i18n/messagebundle_sk.properties | 2 ++ packages/main/src/i18n/messagebundle_sl.properties | 2 ++ packages/main/src/i18n/messagebundle_sr.properties | 2 ++ packages/main/src/i18n/messagebundle_sv.properties | 2 ++ packages/main/src/i18n/messagebundle_th.properties | 2 ++ packages/main/src/i18n/messagebundle_tr.properties | 2 ++ packages/main/src/i18n/messagebundle_uk.properties | 2 ++ packages/main/src/i18n/messagebundle_vi.properties | 2 ++ packages/main/src/i18n/messagebundle_zh_CN.properties | 2 ++ packages/main/src/i18n/messagebundle_zh_TW.properties | 2 ++ 47 files changed, 94 insertions(+) diff --git a/packages/main/src/i18n/messagebundle_ar.properties b/packages/main/src/i18n/messagebundle_ar.properties index 60e4605aa086..1ef6c483fef9 100644 --- a/packages/main/src/i18n/messagebundle_ar.properties +++ b/packages/main/src/i18n/messagebundle_ar.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=كل البنود TOKENIZER_SHOW_ALL_ITEMS={0} من البنود +TOKENIZER_CLEAR_ALL=مسح الكل + TREE_ITEM_ARIA_LABEL=عنصر الشجرة TREE_ITEM_EXPAND_NODE=توسيع العقدة diff --git a/packages/main/src/i18n/messagebundle_bg.properties b/packages/main/src/i18n/messagebundle_bg.properties index c60a7814d369..7f46d68ec145 100644 --- a/packages/main/src/i18n/messagebundle_bg.properties +++ b/packages/main/src/i18n/messagebundle_bg.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Всички позиции TOKENIZER_SHOW_ALL_ITEMS={0} позиции +TOKENIZER_CLEAR_ALL=Изчистване на всичко + TREE_ITEM_ARIA_LABEL=Позиция от дърво TREE_ITEM_EXPAND_NODE=Разширяване на възел diff --git a/packages/main/src/i18n/messagebundle_ca.properties b/packages/main/src/i18n/messagebundle_ca.properties index e36983fd7555..75b29277a845 100644 --- a/packages/main/src/i18n/messagebundle_ca.properties +++ b/packages/main/src/i18n/messagebundle_ca.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tots els articles TOKENIZER_SHOW_ALL_ITEMS={0} elements +TOKENIZER_CLEAR_ALL=Esborrar-ho tot + TREE_ITEM_ARIA_LABEL=Element d''arbre TREE_ITEM_EXPAND_NODE=Desplegar nodes diff --git a/packages/main/src/i18n/messagebundle_cnr.properties b/packages/main/src/i18n/messagebundle_cnr.properties index 5bdbe33c548b..f366112642d2 100644 --- a/packages/main/src/i18n/messagebundle_cnr.properties +++ b/packages/main/src/i18n/messagebundle_cnr.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Sve stavke TOKENIZER_SHOW_ALL_ITEMS={0} stavke(i) +TOKENIZER_CLEAR_ALL=Izbriši sve + TREE_ITEM_ARIA_LABEL=Stavka stabla TREE_ITEM_EXPAND_NODE=Proširi čvor diff --git a/packages/main/src/i18n/messagebundle_cs.properties b/packages/main/src/i18n/messagebundle_cs.properties index 92eaea107f29..237f222f17e9 100644 --- a/packages/main/src/i18n/messagebundle_cs.properties +++ b/packages/main/src/i18n/messagebundle_cs.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Všechny položky TOKENIZER_SHOW_ALL_ITEMS={0} položky +TOKENIZER_CLEAR_ALL=Výmaz všech + TREE_ITEM_ARIA_LABEL=Prvek stromu TREE_ITEM_EXPAND_NODE=Rozbalit uzel diff --git a/packages/main/src/i18n/messagebundle_cy.properties b/packages/main/src/i18n/messagebundle_cy.properties index 09f2bd4869ad..04c6d4b35b67 100644 --- a/packages/main/src/i18n/messagebundle_cy.properties +++ b/packages/main/src/i18n/messagebundle_cy.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Pob eitem TOKENIZER_SHOW_ALL_ITEMS={0} Eitem +TOKENIZER_CLEAR_ALL=Clirio Pob Un + TREE_ITEM_ARIA_LABEL=Eitem Coeden TREE_ITEM_EXPAND_NODE=Ehangu Nodyn diff --git a/packages/main/src/i18n/messagebundle_da.properties b/packages/main/src/i18n/messagebundle_da.properties index 852d0a299755..ee002e46f520 100644 --- a/packages/main/src/i18n/messagebundle_da.properties +++ b/packages/main/src/i18n/messagebundle_da.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Alle elementer TOKENIZER_SHOW_ALL_ITEMS={0} elementer +TOKENIZER_CLEAR_ALL=Nulstil alle + TREE_ITEM_ARIA_LABEL=Træelement TREE_ITEM_EXPAND_NODE=Ekspander knude diff --git a/packages/main/src/i18n/messagebundle_de.properties b/packages/main/src/i18n/messagebundle_de.properties index 223792c1e256..ba826250ded7 100644 --- a/packages/main/src/i18n/messagebundle_de.properties +++ b/packages/main/src/i18n/messagebundle_de.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Alle Elemente TOKENIZER_SHOW_ALL_ITEMS={0} Elemente +TOKENIZER_CLEAR_ALL=Alle löschen + TREE_ITEM_ARIA_LABEL=Baumelement TREE_ITEM_EXPAND_NODE=Knoten expandieren diff --git a/packages/main/src/i18n/messagebundle_el.properties b/packages/main/src/i18n/messagebundle_el.properties index 696f88af088f..8398713dd97d 100644 --- a/packages/main/src/i18n/messagebundle_el.properties +++ b/packages/main/src/i18n/messagebundle_el.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Όλα τα στοιχεία TOKENIZER_SHOW_ALL_ITEMS={0} Είδη +TOKENIZER_CLEAR_ALL=Εκκαθάριση Ολων + TREE_ITEM_ARIA_LABEL=Αναλ.Γραμ.Δέντρου TREE_ITEM_EXPAND_NODE=Επέκταση Κόμβου diff --git a/packages/main/src/i18n/messagebundle_en_GB.properties b/packages/main/src/i18n/messagebundle_en_GB.properties index 3b76c73b4d3f..275c2f6bdf57 100644 --- a/packages/main/src/i18n/messagebundle_en_GB.properties +++ b/packages/main/src/i18n/messagebundle_en_GB.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=All items TOKENIZER_SHOW_ALL_ITEMS={0} Items +TOKENIZER_CLEAR_ALL=Clear All + TREE_ITEM_ARIA_LABEL=Tree Item TREE_ITEM_EXPAND_NODE=Expand Node diff --git a/packages/main/src/i18n/messagebundle_es.properties b/packages/main/src/i18n/messagebundle_es.properties index 8ea35cbdd5b4..b9f04c996ff1 100644 --- a/packages/main/src/i18n/messagebundle_es.properties +++ b/packages/main/src/i18n/messagebundle_es.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Todos los artículos TOKENIZER_SHOW_ALL_ITEMS={0} posiciones +TOKENIZER_CLEAR_ALL=Borrar todo + TREE_ITEM_ARIA_LABEL=Elemento de árbol TREE_ITEM_EXPAND_NODE=Desplegar nodo diff --git a/packages/main/src/i18n/messagebundle_es_MX.properties b/packages/main/src/i18n/messagebundle_es_MX.properties index 6bf0c05c22e1..6385b043a37b 100644 --- a/packages/main/src/i18n/messagebundle_es_MX.properties +++ b/packages/main/src/i18n/messagebundle_es_MX.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Todos los elementos TOKENIZER_SHOW_ALL_ITEMS={0} elementos +TOKENIZER_CLEAR_ALL=Borrar todo + TREE_ITEM_ARIA_LABEL=Elemento de árbol TREE_ITEM_EXPAND_NODE=Expandir nodo diff --git a/packages/main/src/i18n/messagebundle_et.properties b/packages/main/src/i18n/messagebundle_et.properties index b0074429d19b..b50b607720d6 100644 --- a/packages/main/src/i18n/messagebundle_et.properties +++ b/packages/main/src/i18n/messagebundle_et.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Kõik üksused TOKENIZER_SHOW_ALL_ITEMS={0} üksust +TOKENIZER_CLEAR_ALL=Tühjenda kõik + TREE_ITEM_ARIA_LABEL=Puuelement TREE_ITEM_EXPAND_NODE=Laienda sõlme diff --git a/packages/main/src/i18n/messagebundle_fi.properties b/packages/main/src/i18n/messagebundle_fi.properties index cff69a3595c4..ffe47411b457 100644 --- a/packages/main/src/i18n/messagebundle_fi.properties +++ b/packages/main/src/i18n/messagebundle_fi.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Kaikki elementit TOKENIZER_SHOW_ALL_ITEMS={0} elementtiä +TOKENIZER_CLEAR_ALL=Tyhjennä kaikki + TREE_ITEM_ARIA_LABEL=Puuelementti TREE_ITEM_EXPAND_NODE=Laajenna solmu diff --git a/packages/main/src/i18n/messagebundle_fr.properties b/packages/main/src/i18n/messagebundle_fr.properties index e4e9e6fc3653..48620b715615 100644 --- a/packages/main/src/i18n/messagebundle_fr.properties +++ b/packages/main/src/i18n/messagebundle_fr.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tous les éléments TOKENIZER_SHOW_ALL_ITEMS={0} éléments +TOKENIZER_CLEAR_ALL=Effacer tout + TREE_ITEM_ARIA_LABEL=Élément d''arborescence TREE_ITEM_EXPAND_NODE=Développer noeud diff --git a/packages/main/src/i18n/messagebundle_fr_CA.properties b/packages/main/src/i18n/messagebundle_fr_CA.properties index 02761b51fe77..85366b5488d5 100644 --- a/packages/main/src/i18n/messagebundle_fr_CA.properties +++ b/packages/main/src/i18n/messagebundle_fr_CA.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tous les éléments TOKENIZER_SHOW_ALL_ITEMS={0} éléments +TOKENIZER_CLEAR_ALL=Réinitialiser tout + TREE_ITEM_ARIA_LABEL=Élément d''arborescence TREE_ITEM_EXPAND_NODE=Développer le nœud diff --git a/packages/main/src/i18n/messagebundle_hi.properties b/packages/main/src/i18n/messagebundle_hi.properties index 816c2b55e327..ccf92a3cf72d 100644 --- a/packages/main/src/i18n/messagebundle_hi.properties +++ b/packages/main/src/i18n/messagebundle_hi.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=सभी आइटम TOKENIZER_SHOW_ALL_ITEMS={0} आइटम +TOKENIZER_CLEAR_ALL=सब साफ करें + TREE_ITEM_ARIA_LABEL=ट्री आइटम TREE_ITEM_EXPAND_NODE=नोड विस्तृत करें diff --git a/packages/main/src/i18n/messagebundle_hr.properties b/packages/main/src/i18n/messagebundle_hr.properties index 7b1d917002c4..82ee5ba002d9 100644 --- a/packages/main/src/i18n/messagebundle_hr.properties +++ b/packages/main/src/i18n/messagebundle_hr.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Sve stavke TOKENIZER_SHOW_ALL_ITEMS={0} stavki +TOKENIZER_CLEAR_ALL=Očisti sve + TREE_ITEM_ARIA_LABEL=Element stabla TREE_ITEM_EXPAND_NODE=Proširi čvor diff --git a/packages/main/src/i18n/messagebundle_hu.properties b/packages/main/src/i18n/messagebundle_hu.properties index b4f1efa4cf40..f7adffca4688 100644 --- a/packages/main/src/i18n/messagebundle_hu.properties +++ b/packages/main/src/i18n/messagebundle_hu.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Minden elem TOKENIZER_SHOW_ALL_ITEMS={0} elem +TOKENIZER_CLEAR_ALL=Összes visszaállítása + TREE_ITEM_ARIA_LABEL=Fastruktúra eleme TREE_ITEM_EXPAND_NODE=Csomópont kibontása diff --git a/packages/main/src/i18n/messagebundle_id.properties b/packages/main/src/i18n/messagebundle_id.properties index 3e079d5b571b..63be636b2139 100644 --- a/packages/main/src/i18n/messagebundle_id.properties +++ b/packages/main/src/i18n/messagebundle_id.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Semua item TOKENIZER_SHOW_ALL_ITEMS={0} Item +TOKENIZER_CLEAR_ALL=Hapus Semua + TREE_ITEM_ARIA_LABEL=Item Pohon TREE_ITEM_EXPAND_NODE=Perluas Node diff --git a/packages/main/src/i18n/messagebundle_it.properties b/packages/main/src/i18n/messagebundle_it.properties index 8b94d83eb6f3..b3ad4620454f 100644 --- a/packages/main/src/i18n/messagebundle_it.properties +++ b/packages/main/src/i18n/messagebundle_it.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tutte le voci TOKENIZER_SHOW_ALL_ITEMS={0} voci +TOKENIZER_CLEAR_ALL=Cancella tutto + TREE_ITEM_ARIA_LABEL=Elemento albero TREE_ITEM_EXPAND_NODE=Esplodi nodo diff --git a/packages/main/src/i18n/messagebundle_iw.properties b/packages/main/src/i18n/messagebundle_iw.properties index 0e6f001c96f2..d8b6ffdaba1e 100644 --- a/packages/main/src/i18n/messagebundle_iw.properties +++ b/packages/main/src/i18n/messagebundle_iw.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=כל הפריטים TOKENIZER_SHOW_ALL_ITEMS={0} פריטים +TOKENIZER_CLEAR_ALL=נקה הכול + TREE_ITEM_ARIA_LABEL=פריט עץ TREE_ITEM_EXPAND_NODE=הרחב צומת diff --git a/packages/main/src/i18n/messagebundle_ja.properties b/packages/main/src/i18n/messagebundle_ja.properties index a41e64457c09..341b649aa16f 100644 --- a/packages/main/src/i18n/messagebundle_ja.properties +++ b/packages/main/src/i18n/messagebundle_ja.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=すべてのアイテム TOKENIZER_SHOW_ALL_ITEMS={0} アイテム +TOKENIZER_CLEAR_ALL=すべてクリア + TREE_ITEM_ARIA_LABEL=ツリーアイテム TREE_ITEM_EXPAND_NODE=ノード展開 diff --git a/packages/main/src/i18n/messagebundle_kk.properties b/packages/main/src/i18n/messagebundle_kk.properties index bd8a890e3a24..0758998007bf 100644 --- a/packages/main/src/i18n/messagebundle_kk.properties +++ b/packages/main/src/i18n/messagebundle_kk.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Барлық тармақтар TOKENIZER_SHOW_ALL_ITEMS={0} тармақ +TOKENIZER_CLEAR_ALL=Барлығын тазарту + TREE_ITEM_ARIA_LABEL=Тармақты құрылым элементі TREE_ITEM_EXPAND_NODE=Түйінді жаю diff --git a/packages/main/src/i18n/messagebundle_ko.properties b/packages/main/src/i18n/messagebundle_ko.properties index 386331f97c7a..73714dab8529 100644 --- a/packages/main/src/i18n/messagebundle_ko.properties +++ b/packages/main/src/i18n/messagebundle_ko.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=모든 항목 TOKENIZER_SHOW_ALL_ITEMS={0}개 항목 +TOKENIZER_CLEAR_ALL=모두 지우기 + TREE_ITEM_ARIA_LABEL=트리 항목 TREE_ITEM_EXPAND_NODE=노드 펼치기 diff --git a/packages/main/src/i18n/messagebundle_lt.properties b/packages/main/src/i18n/messagebundle_lt.properties index c618ce12f833..0c3c56cbccc4 100644 --- a/packages/main/src/i18n/messagebundle_lt.properties +++ b/packages/main/src/i18n/messagebundle_lt.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Visos pozicijos TOKENIZER_SHOW_ALL_ITEMS={0} pozicijos +TOKENIZER_CLEAR_ALL=Valyti viską + TREE_ITEM_ARIA_LABEL=Medžio pozicija TREE_ITEM_EXPAND_NODE=Išplėsti mazgą diff --git a/packages/main/src/i18n/messagebundle_lv.properties b/packages/main/src/i18n/messagebundle_lv.properties index 0d96b5d2de76..aba0931af70b 100644 --- a/packages/main/src/i18n/messagebundle_lv.properties +++ b/packages/main/src/i18n/messagebundle_lv.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Visi elementi TOKENIZER_SHOW_ALL_ITEMS={0} pozīcijas +TOKENIZER_CLEAR_ALL=Notīrīt visus + TREE_ITEM_ARIA_LABEL=Koka pozīcija TREE_ITEM_EXPAND_NODE=Izvērst mezglu diff --git a/packages/main/src/i18n/messagebundle_mk.properties b/packages/main/src/i18n/messagebundle_mk.properties index 02582fd832ff..27ab6a852f1f 100644 --- a/packages/main/src/i18n/messagebundle_mk.properties +++ b/packages/main/src/i18n/messagebundle_mk.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Сите ставки TOKENIZER_SHOW_ALL_ITEMS={0} ставки +TOKENIZER_CLEAR_ALL=Исчисти ги сите + TREE_ITEM_ARIA_LABEL=Ставка на дрвото TREE_ITEM_EXPAND_NODE=Прошири јазол diff --git a/packages/main/src/i18n/messagebundle_ms.properties b/packages/main/src/i18n/messagebundle_ms.properties index d67b51280246..7642fa4e9087 100644 --- a/packages/main/src/i18n/messagebundle_ms.properties +++ b/packages/main/src/i18n/messagebundle_ms.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Semua item TOKENIZER_SHOW_ALL_ITEMS={0} Item +TOKENIZER_CLEAR_ALL=Kosongkan Semua + TREE_ITEM_ARIA_LABEL=Item Pepohon TREE_ITEM_EXPAND_NODE=Kembangkan Nod diff --git a/packages/main/src/i18n/messagebundle_nl.properties b/packages/main/src/i18n/messagebundle_nl.properties index 4c7083d9b5c2..784d704f78ca 100644 --- a/packages/main/src/i18n/messagebundle_nl.properties +++ b/packages/main/src/i18n/messagebundle_nl.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Alle elementen TOKENIZER_SHOW_ALL_ITEMS={0} elementen +TOKENIZER_CLEAR_ALL=Alles wissen + TREE_ITEM_ARIA_LABEL=Structuurelement TREE_ITEM_EXPAND_NODE=Knooppunt weergeven diff --git a/packages/main/src/i18n/messagebundle_no.properties b/packages/main/src/i18n/messagebundle_no.properties index 1c94f9a8bea9..0ce010ac8779 100644 --- a/packages/main/src/i18n/messagebundle_no.properties +++ b/packages/main/src/i18n/messagebundle_no.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Alle elementer TOKENIZER_SHOW_ALL_ITEMS={0} elementer +TOKENIZER_CLEAR_ALL=Tøm alle + TREE_ITEM_ARIA_LABEL=Treelement TREE_ITEM_EXPAND_NODE=Utvid knutepunkt diff --git a/packages/main/src/i18n/messagebundle_pl.properties b/packages/main/src/i18n/messagebundle_pl.properties index 8fa581385760..fe99a865cfd4 100644 --- a/packages/main/src/i18n/messagebundle_pl.properties +++ b/packages/main/src/i18n/messagebundle_pl.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Wszystkie pozycje TOKENIZER_SHOW_ALL_ITEMS=Liczba pozycji: {0} +TOKENIZER_CLEAR_ALL=Wyczyść wszystko + TREE_ITEM_ARIA_LABEL=Element drzewa TREE_ITEM_EXPAND_NODE=Rozwiń węzeł diff --git a/packages/main/src/i18n/messagebundle_pt.properties b/packages/main/src/i18n/messagebundle_pt.properties index 2ef2969ad918..8e858c0b6da8 100644 --- a/packages/main/src/i18n/messagebundle_pt.properties +++ b/packages/main/src/i18n/messagebundle_pt.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Todos os itens TOKENIZER_SHOW_ALL_ITEMS={0} itens +TOKENIZER_CLEAR_ALL=Limpar tudo + TREE_ITEM_ARIA_LABEL=Elemento da árvore TREE_ITEM_EXPAND_NODE=Expandir nó diff --git a/packages/main/src/i18n/messagebundle_pt_PT.properties b/packages/main/src/i18n/messagebundle_pt_PT.properties index 73033f06b4b5..031aac1e0000 100644 --- a/packages/main/src/i18n/messagebundle_pt_PT.properties +++ b/packages/main/src/i18n/messagebundle_pt_PT.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Todos os itens TOKENIZER_SHOW_ALL_ITEMS={0} itens +TOKENIZER_CLEAR_ALL=Limpar tudo + TREE_ITEM_ARIA_LABEL=Item de árvore TREE_ITEM_EXPAND_NODE=Expandir nó diff --git a/packages/main/src/i18n/messagebundle_ro.properties b/packages/main/src/i18n/messagebundle_ro.properties index 5faa0edd1699..c04ce7cc1647 100644 --- a/packages/main/src/i18n/messagebundle_ro.properties +++ b/packages/main/src/i18n/messagebundle_ro.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Toate pozițiile TOKENIZER_SHOW_ALL_ITEMS={0} poziții +TOKENIZER_CLEAR_ALL=Resetare tot + TREE_ITEM_ARIA_LABEL=Element de arbore TREE_ITEM_EXPAND_NODE=Expandare nod diff --git a/packages/main/src/i18n/messagebundle_ru.properties b/packages/main/src/i18n/messagebundle_ru.properties index 40c1dccc342d..7ab0c69774d5 100644 --- a/packages/main/src/i18n/messagebundle_ru.properties +++ b/packages/main/src/i18n/messagebundle_ru.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Все позиции TOKENIZER_SHOW_ALL_ITEMS={0} поз. +TOKENIZER_CLEAR_ALL=Очистить все + TREE_ITEM_ARIA_LABEL=Элемент дерева TREE_ITEM_EXPAND_NODE=Развернуть узел diff --git a/packages/main/src/i18n/messagebundle_sh.properties b/packages/main/src/i18n/messagebundle_sh.properties index 674467190baf..38452da5a1e3 100644 --- a/packages/main/src/i18n/messagebundle_sh.properties +++ b/packages/main/src/i18n/messagebundle_sh.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Sve stavke TOKENIZER_SHOW_ALL_ITEMS={0} stavke(i) +TOKENIZER_CLEAR_ALL=Izbriši sve + TREE_ITEM_ARIA_LABEL=Stavka stabla TREE_ITEM_EXPAND_NODE=Proširi čvor diff --git a/packages/main/src/i18n/messagebundle_sk.properties b/packages/main/src/i18n/messagebundle_sk.properties index bd54b9dedd87..f3eb9db7a72a 100644 --- a/packages/main/src/i18n/messagebundle_sk.properties +++ b/packages/main/src/i18n/messagebundle_sk.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Všetky položky TOKENIZER_SHOW_ALL_ITEMS={0} prvkov +TOKENIZER_CLEAR_ALL=Vymazať všetko + TREE_ITEM_ARIA_LABEL=Prvok stromu TREE_ITEM_EXPAND_NODE=Rozbaliť uzol diff --git a/packages/main/src/i18n/messagebundle_sl.properties b/packages/main/src/i18n/messagebundle_sl.properties index 0b067c43cfbc..9cdea9dec6ee 100644 --- a/packages/main/src/i18n/messagebundle_sl.properties +++ b/packages/main/src/i18n/messagebundle_sl.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Vsi elementi TOKENIZER_SHOW_ALL_ITEMS={0} elementov +TOKENIZER_CLEAR_ALL=Počisti vse + TREE_ITEM_ARIA_LABEL=Element drevesa TREE_ITEM_EXPAND_NODE=Razširitev vozlišča diff --git a/packages/main/src/i18n/messagebundle_sr.properties b/packages/main/src/i18n/messagebundle_sr.properties index 8966bbeaa77c..138faaed112a 100644 --- a/packages/main/src/i18n/messagebundle_sr.properties +++ b/packages/main/src/i18n/messagebundle_sr.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Све ставке TOKENIZER_SHOW_ALL_ITEMS={0} ставке(и) +TOKENIZER_CLEAR_ALL=Избриши све + TREE_ITEM_ARIA_LABEL=Ставка стабла TREE_ITEM_EXPAND_NODE=Прошири чвор diff --git a/packages/main/src/i18n/messagebundle_sv.properties b/packages/main/src/i18n/messagebundle_sv.properties index 6aeff113b10e..2300f59747e0 100644 --- a/packages/main/src/i18n/messagebundle_sv.properties +++ b/packages/main/src/i18n/messagebundle_sv.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Alla element TOKENIZER_SHOW_ALL_ITEMS={0} element +TOKENIZER_CLEAR_ALL=Rensa alla + TREE_ITEM_ARIA_LABEL=Trädelement TREE_ITEM_EXPAND_NODE=Expandera nod diff --git a/packages/main/src/i18n/messagebundle_th.properties b/packages/main/src/i18n/messagebundle_th.properties index 4d510aa6c396..240401e5f287 100644 --- a/packages/main/src/i18n/messagebundle_th.properties +++ b/packages/main/src/i18n/messagebundle_th.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=ไอเท็มทั้งหมด TOKENIZER_SHOW_ALL_ITEMS={0} รายการ +TOKENIZER_CLEAR_ALL=ล้างทั้งหมด + TREE_ITEM_ARIA_LABEL=รายการทรี TREE_ITEM_EXPAND_NODE=ขยายโหนด diff --git a/packages/main/src/i18n/messagebundle_tr.properties b/packages/main/src/i18n/messagebundle_tr.properties index d913c5f53ad1..5adca8a6e326 100644 --- a/packages/main/src/i18n/messagebundle_tr.properties +++ b/packages/main/src/i18n/messagebundle_tr.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tüm öğeler TOKENIZER_SHOW_ALL_ITEMS={0} öğe +TOKENIZER_CLEAR_ALL=Tümünü temizle + TREE_ITEM_ARIA_LABEL=Ağaç öğesi TREE_ITEM_EXPAND_NODE=Düğümü genişlet diff --git a/packages/main/src/i18n/messagebundle_uk.properties b/packages/main/src/i18n/messagebundle_uk.properties index 29e6603e5e5e..3fb7c23c07b1 100644 --- a/packages/main/src/i18n/messagebundle_uk.properties +++ b/packages/main/src/i18n/messagebundle_uk.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Усі позиції TOKENIZER_SHOW_ALL_ITEMS={0} позицій +TOKENIZER_CLEAR_ALL=Очистити все + TREE_ITEM_ARIA_LABEL=Елемент дерева TREE_ITEM_EXPAND_NODE=Розгорнути вузол diff --git a/packages/main/src/i18n/messagebundle_vi.properties b/packages/main/src/i18n/messagebundle_vi.properties index d9ba68fe74bc..75a92c914521 100644 --- a/packages/main/src/i18n/messagebundle_vi.properties +++ b/packages/main/src/i18n/messagebundle_vi.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=Tất cả các mục TOKENIZER_SHOW_ALL_ITEMS={0} mục +TOKENIZER_CLEAR_ALL=Xóa tất cả + TREE_ITEM_ARIA_LABEL=Mục cây TREE_ITEM_EXPAND_NODE=Mở rộng nút diff --git a/packages/main/src/i18n/messagebundle_zh_CN.properties b/packages/main/src/i18n/messagebundle_zh_CN.properties index a523120b68e4..7c2cf775a82e 100644 --- a/packages/main/src/i18n/messagebundle_zh_CN.properties +++ b/packages/main/src/i18n/messagebundle_zh_CN.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=所有项目 TOKENIZER_SHOW_ALL_ITEMS={0} 个项目 +TOKENIZER_CLEAR_ALL=全部清除 + TREE_ITEM_ARIA_LABEL=树项目 TREE_ITEM_EXPAND_NODE=展开节点 diff --git a/packages/main/src/i18n/messagebundle_zh_TW.properties b/packages/main/src/i18n/messagebundle_zh_TW.properties index 2e225491f30d..834444275a2e 100644 --- a/packages/main/src/i18n/messagebundle_zh_TW.properties +++ b/packages/main/src/i18n/messagebundle_zh_TW.properties @@ -296,6 +296,8 @@ TOKENIZER_POPOVER_REMOVE=所有項目 TOKENIZER_SHOW_ALL_ITEMS={0} 個項目 +TOKENIZER_CLEAR_ALL=全部清除 + TREE_ITEM_ARIA_LABEL=樹狀結構項目 TREE_ITEM_EXPAND_NODE=展開節點