From f7445eb2eb571a98aeafd2fae7422e1ad427cffb Mon Sep 17 00:00:00 2001 From: Marco D'Auria Date: Mon, 22 Dec 2025 12:40:22 +0100 Subject: [PATCH 1/2] feat(launchpad): align to new style and responsive design Align the launchpad to match iX/Element styling, improve responsiveness, animations and accessibility. BREAKING CHANGE: removed default subtitle text from launchpad The `subtitleText` input no longer shows "Access all your apps" by default. To maintain the previous behavior, explicitly set the input. --- .../application-header/index.api.md | 12 +- api-goldens/element-ng/translate/index.api.md | 4 +- .../e2e/element-examples/si-launchpad.spec.ts | 2 +- ...e-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- ...bar--si-navbar-launchpad--medium-size.yaml | 6 +- ...s-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- ...avbar--si-navbar-launchpad-categories.yaml | 8 +- ...d-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- .../si-navbar--si-navbar-launchpad.yaml | 6 +- ...e-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- ...lication-header--si-launchpad--mobile.yaml | 54 +++++++-- ...e-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- ...on-header--si-launchpad--new-favorite.yaml | 52 +++++++-- ...d-element-examples-chromium-dark-linux.png | 4 +- ...-element-examples-chromium-light-linux.png | 4 +- .../si-application-header--si-launchpad.yaml | 54 +++++++-- .../launchpad/si-launchpad-app.component.html | 24 ++-- .../launchpad/si-launchpad-app.component.scss | 61 ++++++++-- .../launchpad/si-launchpad-app.component.ts | 15 ++- .../si-launchpad-factory.component.html | 46 +++++--- .../si-launchpad-factory.component.scss | 42 +++++-- .../si-launchpad-factory.component.ts | 20 ++-- .../launchpad/si-launchpad.spec.ts | 2 +- .../si-application-header.component.html | 7 +- .../si-application-header.component.scss | 36 ++++++ .../si-application-header.component.ts | 3 - .../testing/si-launchpad-category.harness.ts | 2 +- .../testing/si-launchpad.harness.ts | 2 +- .../si-translatable-keys.interface.ts | 2 +- .../components/_application-header.scss | 5 +- .../src/styles/variables/_zindex.scss | 2 +- .../si-application-header/si-launchpad.html | 90 ++++++++++++++- .../si-application-header/si-launchpad.ts | 109 +++++++++++++++++- 38 files changed, 558 insertions(+), 156 deletions(-) diff --git a/api-goldens/element-ng/application-header/index.api.md b/api-goldens/element-ng/application-header/index.api.md index a316bc714..53167d835 100644 --- a/api-goldens/element-ng/application-header/index.api.md +++ b/api-goldens/element-ng/application-header/index.api.md @@ -135,15 +135,15 @@ export class SiHeaderSiemensLogoComponent extends SiHeaderLogoDirective { // @public (undocumented) export class SiLaunchpadFactoryComponent { readonly apps: _angular_core.InputSignal; - readonly closeText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; + readonly closeText: _angular_core.InputSignal; readonly enableFavorites: _angular_core.InputSignalWithTransform; - readonly favoriteAppsText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; + readonly favoriteAppsText: _angular_core.InputSignal; // (undocumented) readonly favoriteChange: _angular_core.OutputEmitterRef; - readonly showLessAppsText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; - readonly showMoreAppsText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; - readonly subtitleText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; - readonly titleText: _angular_core.InputSignal<_siemens_element_translate_ng_translate.TranslatableString>; + readonly showLessAppsText: _angular_core.InputSignal; + readonly showMoreAppsText: _angular_core.InputSignal; + readonly subtitleText: _angular_core.InputSignal; + readonly titleText: _angular_core.InputSignal; } // (No @packageDocumentation comment for this package) diff --git a/api-goldens/element-ng/translate/index.api.md b/api-goldens/element-ng/translate/index.api.md index d7e5013e3..308e5ff6e 100644 --- a/api-goldens/element-ng/translate/index.api.md +++ b/api-goldens/element-ng/translate/index.api.md @@ -296,6 +296,8 @@ export interface SiTranslatableKeys { // (undocumented) 'SI_LAUNCHPAD.DEFAULT_CATEGORY_TITLE'?: string; // (undocumented) + 'SI_LAUNCHPAD.EXTERNAL_LINK'?: string; + // (undocumented) 'SI_LAUNCHPAD.FAVORITE_APPS'?: string; // (undocumented) 'SI_LAUNCHPAD.SHOW_LESS'?: string; @@ -304,8 +306,6 @@ export interface SiTranslatableKeys { // (undocumented) 'SI_LAUNCHPAD.SUB_TITLE'?: string; // (undocumented) - 'SI_LAUNCHPAD.SUBTITLE'?: string; - // (undocumented) 'SI_LAUNCHPAD.TITLE'?: string; // (undocumented) 'SI_LIST_DETAILS.BACK'?: string; diff --git a/playwright/e2e/element-examples/si-launchpad.spec.ts b/playwright/e2e/element-examples/si-launchpad.spec.ts index 84307f0c0..0d1de305c 100644 --- a/playwright/e2e/element-examples/si-launchpad.spec.ts +++ b/playwright/e2e/element-examples/si-launchpad.spec.ts @@ -13,7 +13,7 @@ test.describe('launchpad', () => { await page.getByText('Show more').click(); await si.runVisualAndA11yTests(); - await page.getByRole('link', { name: 'Rocket' }).locator('.favorite-icon').click(); + await page.getByRole('link', { name: 'Fischbach' }).first().locator('.favorite-icon').click(); await si.runVisualAndA11yTests('new favorite'); }); diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-dark-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-dark-linux.png index aeb365589..5c398d8cf 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e7d11df6c94a349ab52ac0586ae893d2134b640f4c3981b9b2689c49713b766 -size 20700 +oid sha256:df75ee30ca86465f9230e09d2f2b3d89e222b336c66e6f6f31544e090bc92d90 +size 23750 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-light-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-light-linux.png index 22fab506e..182436c39 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c5fa0fb3fc93a53bd7d26fcde9d3359e105bae8cbd25404e4a9708db5305f39 -size 19956 +oid sha256:325b5f10332ed5c8f7ec50b8372a040851fbd24a80207eabcf985042e7975f17 +size 22825 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size.yaml b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size.yaml index ecb5d869b..bd66cd558 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size.yaml +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad--medium-size.yaml @@ -8,8 +8,9 @@ - /url: "#/viewer/viewer/item2" - button "Help" - button "Jane Smith" -- paragraph: Launchpad +- paragraph: Switch applications - paragraph: Access all your apps +- button "Close launchpad" - link "Assets": - /url: . - link "Water": @@ -19,5 +20,4 @@ - link "Statistics": - /url: . - link "Add more": - - /url: https://example.org -- button "Close launchpad" \ No newline at end of file + - /url: https://example.org \ No newline at end of file diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-dark-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-dark-linux.png index a45e8287a..06bcc374c 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48d086f14afc36b3b6f1c32889e70fd011134b5be8e750c7e5d882a8a879c6e3 -size 32411 +oid sha256:a667f8a3115ea5b3b57a55180ed71c7c18869d422ff452dbcbb9210a539a1ea6 +size 30158 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-light-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-light-linux.png index 0a468220b..f37d1342a 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:784861dfe1a5eaeeb325dc6cd6b1d757a6e05f1b05f0ed481a882c4b3a0eb33b -size 31441 +oid sha256:6f7d9e3d12591f67fb9795bbf5b6d794262dc28b29b887ec2cc989e02a04df42 +size 29251 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories.yaml b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories.yaml index 1a3ace920..169c35542 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories.yaml +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-categories.yaml @@ -10,9 +10,10 @@ - /url: "#/viewer/viewer/item2" - button "Help" - button "Jane Smith" -- paragraph: Launchpad +- paragraph: Switch applications - paragraph: Access all your apps -- text: Favorite apps +- button "Close launchpad" +- text: Favorites - link "Water": - /url: . - button "Show less" @@ -27,5 +28,4 @@ - link "Statistics": - /url: . - link "Add more": - - /url: https://example.org -- button "Close launchpad" \ No newline at end of file + - /url: https://example.org \ No newline at end of file diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-dark-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-dark-linux.png index be3b34914..0b178d578 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c0873818c060d6714c0e12ec1dd78c1d74e2b951ae3a15e3a49f3725c3e054f -size 23002 +oid sha256:9ffdff6331a200bcebd52e472156e87cbac463368149a0b09a0cd9461fdb9b81 +size 24436 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-light-linux.png b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-light-linux.png index 9d1440174..d6b61153b 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29c14f6e86a2276a335571454d2f1f48731c534553404a1b62bcf056bdfd047d -size 22175 +oid sha256:d9de655d4b558b605c297fef29a4da3092420a61e7e3c88ce15c04f37e9ae9de +size 23749 diff --git a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad.yaml b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad.yaml index ed09e6bf1..d83fd1510 100644 --- a/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad.yaml +++ b/playwright/snapshots/navbar-launchpad.spec.ts-snapshots/si-navbar--si-navbar-launchpad.yaml @@ -10,8 +10,9 @@ - /url: "#/viewer/viewer/item2" - button "Help" - button "Jane Smith" -- paragraph: Launchpad +- paragraph: Switch applications - paragraph: Access all your apps +- button "Close launchpad" - link "Assets": - /url: . - link "Water": @@ -21,5 +22,4 @@ - link "Statistics": - /url: . - link "Add more": - - /url: https://example.org -- button "Close launchpad" \ No newline at end of file + - /url: https://example.org \ No newline at end of file diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-dark-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-dark-linux.png index 1f5479b22..ce373217f 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56989f38b4e64a0e5180e347bb6b668a13c771cd4db3d1e19073e0f35efc0584 -size 21377 +oid sha256:b1ce840fa589cfdc282799dcc1ab9c2cea1c66cb575d7cc69054deaa7e28634b +size 31782 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-light-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-light-linux.png index 7a3f6aaf8..dc46ddf90 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27ac44fe8531b5f27f87004bc6fab04f3edce8ca0964b124be543676a6d22f25 -size 20654 +oid sha256:0ff965e05cda61e9821ea6bc86f73a9a48e25d787cde919f216ba166e9ddcfa8 +size 31052 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile.yaml b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile.yaml index c698b8391..9077dbe3d 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile.yaml +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--mobile.yaml @@ -2,18 +2,52 @@ - button "Launchpad" [expanded] - link "Siemens logo": - /url: "#/" -- paragraph: Launchpad -- paragraph: Access all your apps -- text: Favorite apps -- link "Fischbach": +- paragraph: Switch applications +- button "Close launchpad" +- text: Favorites +- link "Assets System name": - /url: . -- button "Show less" -- link "Assets": +- link "Fischbach System name": + - /url: . +- link "Statistics System name": + - /url: "#/viewer/viewer/stats" +- link "Rocket System name": - /url: . -- link "Fischbach": +- button "Show less" +- link "Assets System name": - /url: . -- link "Rocket": +- link "Fischbach System name": - /url: . -- link "Statistics": +- link "Statistics System name": - /url: "#/viewer/viewer/stats" -- button "Close launchpad" \ No newline at end of file +- link "Rocket System name": + - /url: . +- link "App name This is a really long name": + - /url: . +- link "App name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "This is a really long name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- heading "Launchpad Configuration" [level=2] +- paragraph: Configure your launchpad settings and test different display modes. +- text: Enable Favorites Show favorite apps at the top +- switch "Enable Favorites Show favorite apps at the top" [checked] +- text: Enable Categories Group apps into organized categories +- switch "Enable Categories Group apps into organized categories" +- paragraph: + - strong: "Demo note:" + - text: Click the header icon to open the launchpad and test your settings \ No newline at end of file diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-dark-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-dark-linux.png index 4cfb77646..f43d01c09 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e981b58c90a6aad54bb519136823252946d17453b3cc47ebaa36e38ec347cc7 -size 25077 +oid sha256:343acdb557d33d838dd9557d9a2ca6fd3b1b28814bc97b4ddf62ab1583f1b118 +size 38528 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-light-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-light-linux.png index 89f345391..460f10bca 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52b21044da053c9ac62cb9298ed2b54477e8ea137a87ea521bb08e1367d85a6b -size 24480 +oid sha256:91e4f4d4e57e620e876d8a7de99528b1dccb980b4eb85e091d2fa1f578c36f29 +size 37672 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite.yaml b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite.yaml index d89abcb8d..3190c3ad3 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite.yaml +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad--new-favorite.yaml @@ -2,20 +2,50 @@ - button "Launchpad" [expanded] - link "Siemens logo": - /url: "#/" -- paragraph: Launchpad -- paragraph: Access all your apps -- text: Favorite apps -- link "Fischbach": +- paragraph: Switch applications +- button "Close launchpad" +- text: Favorites +- link "Assets System name": - /url: . -- link "Rocket": +- link "Statistics System name": + - /url: "#/viewer/viewer/stats" +- link "Rocket System name": - /url: . - button "Show less" -- link "Assets": - - /url: . -- link "Fischbach": +- link "Assets System name": - /url: . -- link "Rocket": +- link "Fischbach System name": - /url: . -- link "Statistics": +- link "Statistics System name": - /url: "#/viewer/viewer/stats" -- button "Close launchpad" \ No newline at end of file +- link "Rocket System name": + - /url: . +- link "App name This is a really long name": + - /url: . +- link "App name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "This is a really long name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- heading "Launchpad Configuration" [level=2] +- paragraph: Configure your launchpad settings and test different display modes. +- text: Enable Favorites Show favorite apps at the top +- switch "Enable Favorites Show favorite apps at the top" [checked] +- text: Enable Categories Group apps into organized categories +- switch "Enable Categories Group apps into organized categories" +- paragraph: + - strong: "Demo note:" + - text: Click the header icon to open the launchpad and test your settings \ No newline at end of file diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-dark-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-dark-linux.png index 99f53002e..ce15dcb61 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-dark-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-dark-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:038f8a475cfd2a28b25e878bf5da3016e373752be85a750b698667e09fea70c9 -size 22076 +oid sha256:709cb82c24df842e882946c1f4e0df24c66cccc8bc1d07d7299477f054f401ba +size 40135 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-light-linux.png b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-light-linux.png index f3f430dba..9fb8d2fa9 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-light-linux.png +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad-element-examples-chromium-light-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:930e7ebe86f3ea01f8c586e03423b94700634434796888e1dd2a72f254e5b603 -size 21517 +oid sha256:434ca6544c022e6711a7e825d90d061a5b5ad46464a04035ee5c7721949db2cc +size 39365 diff --git a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad.yaml b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad.yaml index c698b8391..9077dbe3d 100644 --- a/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad.yaml +++ b/playwright/snapshots/si-launchpad.spec.ts-snapshots/si-application-header--si-launchpad.yaml @@ -2,18 +2,52 @@ - button "Launchpad" [expanded] - link "Siemens logo": - /url: "#/" -- paragraph: Launchpad -- paragraph: Access all your apps -- text: Favorite apps -- link "Fischbach": +- paragraph: Switch applications +- button "Close launchpad" +- text: Favorites +- link "Assets System name": - /url: . -- button "Show less" -- link "Assets": +- link "Fischbach System name": + - /url: . +- link "Statistics System name": + - /url: "#/viewer/viewer/stats" +- link "Rocket System name": - /url: . -- link "Fischbach": +- button "Show less" +- link "Assets System name": - /url: . -- link "Rocket": +- link "Fischbach System name": - /url: . -- link "Statistics": +- link "Statistics System name": - /url: "#/viewer/viewer/stats" -- button "Close launchpad" \ No newline at end of file +- link "Rocket System name": + - /url: . +- link "App name This is a really long name": + - /url: . +- link "App name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "This is a really long name Opens in a new tab System name": + - /url: . + - text: "" +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- link "App name System name": + - /url: . +- heading "Launchpad Configuration" [level=2] +- paragraph: Configure your launchpad settings and test different display modes. +- text: Enable Favorites Show favorite apps at the top +- switch "Enable Favorites Show favorite apps at the top" [checked] +- text: Enable Categories Group apps into organized categories +- switch "Enable Categories Group apps into organized categories" +- paragraph: + - strong: "Demo note:" + - text: Click the header icon to open the launchpad and test your settings \ No newline at end of file diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-app.component.html b/projects/element-ng/application-header/launchpad/si-launchpad-app.component.html index 54ee6766b..9c63f5713 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-app.component.html +++ b/projects/element-ng/application-header/launchpad/si-launchpad-app.component.html @@ -3,14 +3,22 @@ } @else if (iconClass()) { } -
- - @if (external()) { - - } -
-
- +
+
+ + + + @if (external()) { + + } +
+
+ +
@if (enableFavoriteToggle()) { (); readonly iconClass = input(); + /** + * Aria-label for the external link icon. + * + * @defaultValue + * ``` + * t(() => $localize`:@@SI_LAUNCHPAD.EXTERNAL_LINK:Opens in a new tab`) + * ``` + */ + readonly externalLinkText = input( + t(() => $localize`:@@SI_LAUNCHPAD.EXTERNAL_LINK:Opens in a new tab`) + ); protected readonly icons = addIcons({ elementExport, elementFavorites, elementFavoritesFilled }); diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html index f2484cb22..a9319a26b 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html +++ b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html @@ -1,27 +1,43 @@
@if (titleText()) { -

{{ titleText() | translate }}

+
+ +

{{ titleText() | translate }}

+ @if (subtitleText()) { +

{{ subtitleText() | translate }}

+ } +
+ +
} - @if (subtitleText()) { -

{{ subtitleText() | translate }}

- } -
+
@for (category of categories(); track category; let first = $first) { @if (!enableFavorites() || !hasFavorites() || first || showAllApps) { -
+
@if (category.name) { - +
{{ category.name | translate }} - +
} -
+
@for (app of category.apps; track app) { @switch (app.type) { @case ('router-link') { @@ -76,7 +92,7 @@ @if (enableFavorites() && first && hasFavorites()) {
-
diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.scss b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.scss index 300485b84..e6f704726 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.scss +++ b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.scss @@ -1,26 +1,50 @@ @use 'sass:map'; -@use '@siemens/element-theme/src/styles/variables'; +@use '@siemens/element-theme/src/styles/all-variables'; .app-switcher { - position: fixed; + position: absolute; + pointer-events: all; inset-block-start: calc( - variables.$si-application-header-height + variables.$si-titlebar-spacing + - variables.$si-system-banner-spacing + all-variables.$si-application-header-height + 1px /* header border compensation */ + + all-variables.$si-titlebar-spacing + all-variables.$si-system-banner-spacing ); inset-inline: 0; min-block-size: 200px; max-block-size: calc( - 100vh - variables.$si-application-header-height - variables.$si-titlebar-spacing - - variables.$si-system-banner-spacing + 100vh - all-variables.$si-application-header-height - all-variables.$si-titlebar-spacing - + all-variables.$si-system-banner-spacing ); + padding-block: map.get(all-variables.$spacers, 6); + padding-inline: map.get(all-variables.$spacers, 8); display: flex; flex-direction: column; - z-index: variables.$zindex-launchpad; - background-color: variables.$element-base-1; - box-shadow: variables.$element-elevation-inset-1; + background-color: all-variables.$element-base-1; + + @include all-variables.media-breakpoint-down(md) { + padding: map.get(all-variables.$spacers, 6); + } +} + +.apps-header { + display: flex; + align-items: center; + justify-content: space-between; } .apps-scroll { overflow-y: auto; } + +.btn-show-all { + margin-block-start: map.get(all-variables.$spacers, 6); + margin-block-end: map.get(all-variables.$spacers, 4); + + &.show { + margin-block-end: map.get(all-variables.$spacers, 8); + } + + &.show:has(+ div .launchpad-category-title) { + margin-block-end: 0; + } +} diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts index 9175e6180..37df08d58 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts +++ b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts @@ -15,7 +15,7 @@ import { import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router'; import { addIcons, elementCancel, elementDown2, SiIconComponent } from '@siemens/element-ng/icon'; import { SiLinkModule } from '@siemens/element-ng/link'; -import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; +import { SiTranslatePipe, t, TranslatableString } from '@siemens/element-translate-ng/translate'; import { SiApplicationHeaderComponent } from '../si-application-header.component'; import { SiLaunchpadAppComponent } from './si-launchpad-app.component'; @@ -57,20 +57,16 @@ export class SiLaunchpadFactoryComponent { * * @defaultValue * ``` - * t(() => $localize`:@@SI_LAUNCHPAD.TITLE:Launchpad`) + * t(() => $localize`:@@SI_LAUNCHPAD.TITLE:Switch applications`) * ``` */ - readonly titleText = input(t(() => $localize`:@@SI_LAUNCHPAD.TITLE:Launchpad`)); + readonly titleText = input(t(() => $localize`:@@SI_LAUNCHPAD.TITLE:Switch applications`)); /** * Subtitle of the launchpad. - * - * @defaultValue - * ``` - * t(() => $localize`:@@SI_LAUNCHPAD.SUBTITLE:Access all your apps`) - * ``` + * When not provided, no subtitle is displayed. */ - readonly subtitleText = input(t(() => $localize`:@@SI_LAUNCHPAD.SUBTITLE:Access all your apps`)); + readonly subtitleText = input(); /** All app items shown in the launchpad. */ readonly apps = input.required(); @@ -87,12 +83,10 @@ export class SiLaunchpadFactoryComponent { * * @defaultValue * ``` - * t(() => $localize`:@@SI_LAUNCHPAD.FAVORITE_APPS:Favorite apps`) + * t(() => $localize`:@@SI_LAUNCHPAD.FAVORITE_APPS:Favorites`) * ``` */ - readonly favoriteAppsText = input( - t(() => $localize`:@@SI_LAUNCHPAD.FAVORITE_APPS:Favorite apps`) - ); + readonly favoriteAppsText = input(t(() => $localize`:@@SI_LAUNCHPAD.FAVORITE_APPS:Favorites`)); /** * Title of the show more apps button. diff --git a/projects/element-ng/application-header/launchpad/si-launchpad.spec.ts b/projects/element-ng/application-header/launchpad/si-launchpad.spec.ts index 49d7492f7..538bb808b 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad.spec.ts +++ b/projects/element-ng/application-header/launchpad/si-launchpad.spec.ts @@ -109,7 +109,7 @@ describe('SiLaunchpad', () => { await harness.toggleMore(); const categories = await harness.getCategories(); expect(categories).toHaveSize(2); - expect(await categories[0].getName()).toBe('Favorite apps'); + expect(await categories[0].getName()).toBe('Favorites'); expect(await categories[1].getName()).toBe(null); expect(await harness.getApp('A-1').then(app => app.isFavorite())).toBeTrue(); expect(await harness.getFavoriteCategory().then(category => category.getApps())).toHaveSize( diff --git a/projects/element-ng/application-header/si-application-header.component.html b/projects/element-ng/application-header/si-application-header.component.html index 70773c72c..75e8c30d7 100644 --- a/projects/element-ng/application-header/si-application-header.component.html +++ b/projects/element-ng/application-header/si-application-header.component.html @@ -51,13 +51,16 @@ @if (launchpadOpen() && launchpad()) { -
- +
+
+ +
} @if (openDropdownCount() || launchpadOpen()) {
diff --git a/projects/element-ng/application-header/si-application-header.component.scss b/projects/element-ng/application-header/si-application-header.component.scss index fbc12f68e..1ac2af066 100644 --- a/projects/element-ng/application-header/si-application-header.component.scss +++ b/projects/element-ng/application-header/si-application-header.component.scss @@ -2,4 +2,40 @@ .modal-backdrop { z-index: variables.$zindex-application-header-backdrop; + opacity: 1; + transition: opacity 0.15s linear; + + @starting-style { + opacity: 0; + } + + &.backdrop-leave { + opacity: 0; + transition: opacity 0.15s linear; + } +} + +.launchpad-container { + position: fixed; + inset: 0; + pointer-events: none; + z-index: variables.$zindex-launchpad; + opacity: 1; + transform: translateY(0); + transition: + opacity 0.5s ease, + transform 0.5s ease; + + @starting-style { + opacity: 0; + transform: translateY(-120px); + } + + &.expand-leave { + opacity: 0; + transform: translateY(-120px); + transition: + opacity 0.25s ease, + transform 0.25s ease; + } } diff --git a/projects/element-ng/application-header/si-application-header.component.ts b/projects/element-ng/application-header/si-application-header.component.ts index 318ff2b44..818ae9468 100644 --- a/projects/element-ng/application-header/si-application-header.component.ts +++ b/projects/element-ng/application-header/si-application-header.component.ts @@ -148,9 +148,6 @@ export class SiApplicationHeaderComponent implements HeaderWithDropdowns, OnDest this.dropdownOpened(); this.closeMobileMenus.next(); this.launchpadOpen.set(true); - this.inlineDropdown - .pipe(skip(1), takeUntil(this.closeMobileMenus)) - .subscribe(() => this.closeMobileMenus.next()); } } diff --git a/projects/element-ng/application-header/testing/si-launchpad-category.harness.ts b/projects/element-ng/application-header/testing/si-launchpad-category.harness.ts index c29f75ccc..e23fe36b1 100644 --- a/projects/element-ng/application-header/testing/si-launchpad-category.harness.ts +++ b/projects/element-ng/application-header/testing/si-launchpad-category.harness.ts @@ -15,7 +15,7 @@ export class SiLaunchpadCategoryHarness extends ComponentHarness { ); } - private name = this.locatorForOptional('.si-h4'); + private name = this.locatorForOptional('.launchpad-category-title'); async getName(): Promise { return this.name().then(name => name?.text() ?? null); diff --git a/projects/element-ng/application-header/testing/si-launchpad.harness.ts b/projects/element-ng/application-header/testing/si-launchpad.harness.ts index 0eaebf7bb..7b5da7e83 100644 --- a/projects/element-ng/application-header/testing/si-launchpad.harness.ts +++ b/projects/element-ng/application-header/testing/si-launchpad.harness.ts @@ -17,7 +17,7 @@ export class SiLaunchpadHarness extends ComponentHarness { } async getFavoriteCategory(): Promise { - return this.getCategory('Favorite apps'); + return this.getCategory('Favorites'); } async getCategories(): Promise { diff --git a/projects/element-ng/translate/si-translatable-keys.interface.ts b/projects/element-ng/translate/si-translatable-keys.interface.ts index f5ce83212..65788a261 100644 --- a/projects/element-ng/translate/si-translatable-keys.interface.ts +++ b/projects/element-ng/translate/si-translatable-keys.interface.ts @@ -146,10 +146,10 @@ export interface SiTranslatableKeys { 'SI_LANGUAGE_SWITCHER.LABEL'?: string; 'SI_LAUNCHPAD.CLOSE'?: string; 'SI_LAUNCHPAD.DEFAULT_CATEGORY_TITLE'?: string; + 'SI_LAUNCHPAD.EXTERNAL_LINK'?: string; 'SI_LAUNCHPAD.FAVORITE_APPS'?: string; 'SI_LAUNCHPAD.SHOW_LESS'?: string; 'SI_LAUNCHPAD.SHOW_MORE'?: string; - 'SI_LAUNCHPAD.SUBTITLE'?: string; 'SI_LAUNCHPAD.SUB_TITLE'?: string; 'SI_LAUNCHPAD.TITLE'?: string; 'SI_LIST_DETAILS.BACK'?: string; diff --git a/projects/element-theme/src/styles/components/_application-header.scss b/projects/element-theme/src/styles/components/_application-header.scss index 0ddf7e5a1..23e5878ba 100644 --- a/projects/element-theme/src/styles/components/_application-header.scss +++ b/projects/element-theme/src/styles/components/_application-header.scss @@ -31,8 +31,9 @@ align-items: stretch; border-block-end: 1px solid semantic-tokens.$element-ui-4; - // hide border when mobile styles are applied and either the dropdown-menu or header-toggle is shown - &:has(.dropdown-menu.show, .header-toggle.show) { + // hide border when mobile styles are applied and either the dropdown-menu is shown or navigation is expanded + &:has(.dropdown-menu.show), + &.show-navigation { border-block-end: none; } diff --git a/projects/element-theme/src/styles/variables/_zindex.scss b/projects/element-theme/src/styles/variables/_zindex.scss index 6054721c5..451c36e20 100644 --- a/projects/element-theme/src/styles/variables/_zindex.scss +++ b/projects/element-theme/src/styles/variables/_zindex.scss @@ -16,5 +16,5 @@ $zindex-sidepanel-responsive: $zindex-fixed + 1; // above vertical-nav $zindex-vertical-nav: $zindex-fixed + 1; $zindex-vertical-nav-collapsed: $zindex-fixed; $zindex-application-header: $zindex-fixed + 3; -$zindex-launchpad: $zindex-application-header; +$zindex-launchpad: $zindex-application-header - 1; $zindex-application-header-backdrop: $zindex-launchpad - 1; diff --git a/src/app/examples/si-application-header/si-launchpad.html b/src/app/examples/si-application-header/si-launchpad.html index 12917de0b..c884d7dd7 100644 --- a/src/app/examples/si-application-header/si-launchpad.html +++ b/src/app/examples/si-application-header/si-launchpad.html @@ -1,9 +1,87 @@ - - - - - +
+ + + + + + +
+
+
+
+
+

Launchpad Configuration

+

+ Configure your launchpad settings and test different display modes. +

+ +
+
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+

+ + Demo note: Click the header icon to open the launchpad and test + your settings + +

+
+
+
+
+
+
+
- + diff --git a/src/app/examples/si-application-header/si-launchpad.ts b/src/app/examples/si-application-header/si-launchpad.ts index b0e23bd59..4ae18e22e 100644 --- a/src/app/examples/si-application-header/si-launchpad.ts +++ b/src/app/examples/si-application-header/si-launchpad.ts @@ -2,10 +2,11 @@ * Copyright (c) Siemens 2016 - 2025 * SPDX-License-Identifier: MIT */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { App, + AppCategory, SiApplicationHeaderComponent, SiHeaderBrandDirective, SiHeaderLogoDirective, @@ -25,33 +26,131 @@ import { changeDetection: ChangeDetectionStrategy.OnPush }) export class SampleComponent { - apps: App[] = [ + readonly enableFavorites = signal(true); + readonly enableCategories = signal(false); + + private readonly baseApps = signal([ { name: 'Assets', + systemName: 'System name', iconUrl: './assets/app-icons/assets.svg', + favorite: true, href: '.' }, { name: 'Fischbach', + systemName: 'System name', iconUrl: './assets/app-icons/fischbach.svg', favorite: true, + active: true, href: '.' }, { name: 'Rocket', + systemName: 'System name', iconUrl: './assets/app-icons/rocket.svg', + favorite: true, href: '.' }, { name: 'Statistics', + systemName: 'System name', iconUrl: './assets/app-icons/statistics.svg', + favorite: true, type: 'router-link', routerLink: 'stats' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + external: true, + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'This is a really long name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + external: true, + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'App name', + systemName: 'This is a really long name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + }, + { + name: 'App name', + systemName: 'System name', + iconUrl: './assets/app-icons/assets.svg', + href: '.' + } + ]); + + readonly apps = computed((): App[] | AppCategory[] => { + const businessApps = this.baseApps().filter(app => + ['Assets', 'Fischbach', 'Statistics'].includes(app.name) + ); + const toolApps = this.baseApps().filter( + app => app.name === 'Rocket' || app.systemName === 'This is a really long name' + ); + const otherApps = this.baseApps().filter( + app => !businessApps.includes(app) && !toolApps.includes(app) + ); + + if (!this.enableCategories()) { + return [...businessApps, ...toolApps, ...otherApps]; } - ]; + + return [ + { name: 'Business Applications', apps: businessApps }, + { name: 'System Tools', apps: toolApps }, + { name: 'Other Applications', apps: otherApps } + ].filter(category => category.apps.length > 0); + }); updateFavorite({ app, favorite }: { app: App; favorite: boolean }): void { - app.favorite = favorite; - this.apps = [...this.apps]; // Trigger change detection + this.baseApps.set(this.baseApps().map(a => (a === app ? { ...a, favorite } : a))); + } + + toggleFavorites(event: Event): void { + const target = event.target as HTMLInputElement; + this.enableFavorites.set(target.checked); + } + + toggleCategories(event: Event): void { + const target = event.target as HTMLInputElement; + this.enableCategories.set(target.checked); } } From 7262aeca6e4820216084e9c5830a5a2cae43ee6b Mon Sep 17 00:00:00 2001 From: Marco D'Auria Date: Tue, 20 Jan 2026 20:42:12 +0100 Subject: [PATCH 2/2] feat(launchpad): implement spatial arrow navigation --- .../si-launchpad-factory.component.html | 2 + .../si-launchpad-factory.component.ts | 120 ++++++++++++++++ .../launchpad/si-launchpad-navigation.spec.ts | 135 ++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 projects/element-ng/application-header/launchpad/si-launchpad-navigation.spec.ts diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html index a9319a26b..39342bc25 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html +++ b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.html @@ -59,6 +59,7 @@ [preserveFragment]="app.extras?.preserveFragment" [skipLocationChange]="app.extras?.skipLocationChange" [replaceUrl]="app.extras?.replaceUrl" + (keydown)="handleAppsNavigation($event, $event.currentTarget)" (favoriteChange)="toggleFavorite(app, $event)" > {{ app.name | translate }} @@ -77,6 +78,7 @@ [external]="app.external" [iconUrl]="app.iconUrl" [iconClass]="app.iconClass" + (keydown)="handleAppsNavigation($event, $event.currentTarget)" (favoriteChange)="toggleFavorite(app, $event)" > {{ app.name | translate }} diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts index 37df08d58..131611e8e 100644 --- a/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts +++ b/projects/element-ng/application-header/launchpad/si-launchpad-factory.component.ts @@ -13,6 +13,7 @@ import { output } from '@angular/core'; import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router'; +import { correctKeyRTL } from '@siemens/element-ng/common'; import { addIcons, elementCancel, elementDown2, SiIconComponent } from '@siemens/element-ng/icon'; import { SiLinkModule } from '@siemens/element-ng/link'; import { SiTranslatePipe, t, TranslatableString } from '@siemens/element-translate-ng/translate'; @@ -139,6 +140,10 @@ export class SiLaunchpadFactoryComponent { protected readonly activatedRoute = inject(ActivatedRoute, { optional: true }); private header = inject(SiApplicationHeaderComponent); + // Navigation constants for keyboard arrow navigation + private readonly rowTolerance = 10; + private readonly leftTolerance = 20; + protected closeLaunchpad(): void { this.header.closeLaunchpad(); } @@ -154,4 +159,119 @@ export class SiLaunchpadFactoryComponent { protected isCategories(items: App[] | AppCategory[]): items is AppCategory[] { return items.some(item => 'apps' in item); } + + protected handleAppsNavigation(event: KeyboardEvent, element: EventTarget | null): void { + const correctedKey = correctKeyRTL(event.key); + + if ( + !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(correctedKey) || + !element || + !(element instanceof HTMLElement) + ) { + return; + } + + const appContainer = element.closest('.d-flex'); + if (!appContainer) return; + + const enabledApps = Array.from( + appContainer.querySelectorAll(':scope > a[si-launchpad-app]') + ) as HTMLElement[]; + + const currentIndex = enabledApps.indexOf(element); + if (currentIndex === -1) return; + + let targetIndex: number; + + if (correctedKey === 'ArrowLeft' || correctedKey === 'ArrowRight') { + // Horizontal navigation within the same row + const direction = correctedKey === 'ArrowLeft' ? -1 : 1; + targetIndex = (currentIndex + direction + enabledApps.length) % enabledApps.length; + } else { + // Vertical navigation between rows + targetIndex = this.getVerticalTargetIndex( + enabledApps, + currentIndex, + correctedKey === 'ArrowUp' + ); + } + + enabledApps[targetIndex]?.focus(); + event.preventDefault(); + } + + private getVerticalTargetIndex(apps: HTMLElement[], currentIndex: number, isUp: boolean): number { + // Cache all bounding rects to avoid multiple expensive DOM calls + const appRects = apps.map(app => ({ + element: app, + rect: app.getBoundingClientRect() + })); + + const currentRect = appRects[currentIndex].rect; + + // Check if layout is single column (mobile/narrow screen) + const alignedApps = appRects.filter( + ({ rect }) => Math.abs(rect.left - currentRect.left) <= this.leftTolerance + ); + + // Single column: use sequential navigation + if (alignedApps.length >= apps.length) { + const direction = isUp ? -1 : 1; + const targetIndex = currentIndex + direction; + + return targetIndex < 0 ? apps.length - 1 : targetIndex >= apps.length ? 0 : targetIndex; + } + + // Grid layout: use spatial navigation + const currentRowKey = Math.round(currentRect.top / this.rowTolerance) * this.rowTolerance; + + // Group apps by rows (excluding current app for target selection) + const rowGroups = new Map(); + let hasMultipleRows = false; + + appRects.forEach((appRect, index) => { + const rowKey = Math.round(appRect.rect.top / this.rowTolerance) * this.rowTolerance; + + if (rowKey !== currentRowKey) { + hasMultipleRows = true; + } + + if (index !== currentIndex) { + if (!rowGroups.has(rowKey)) { + rowGroups.set(rowKey, []); + } + rowGroups.get(rowKey)!.push(appRect); + } + }); + + // Single row: no vertical movement + if (!hasMultipleRows) { + return currentIndex; + } + + // Find target row and closest app + const sortedRowKeys = Array.from(rowGroups.keys()).sort((a, b) => a - b); + const currentRowIndex = sortedRowKeys.indexOf(currentRowKey); + + const targetRowKey = isUp + ? currentRowIndex > 0 + ? sortedRowKeys[currentRowIndex - 1] + : sortedRowKeys[sortedRowKeys.length - 1] + : currentRowIndex < sortedRowKeys.length - 1 + ? sortedRowKeys[currentRowIndex + 1] + : sortedRowKeys[0]; + + const targetRowApps = rowGroups.get(targetRowKey) ?? []; + + if (targetRowApps.length === 0) return currentIndex; + + // Find closest app horizontally in target row + const closestApp = targetRowApps.reduce((closest, app) => + Math.abs(app.rect.left - currentRect.left) < Math.abs(closest.rect.left - currentRect.left) + ? app + : closest + ); + + return apps.indexOf(closestApp.element); + } } diff --git a/projects/element-ng/application-header/launchpad/si-launchpad-navigation.spec.ts b/projects/element-ng/application-header/launchpad/si-launchpad-navigation.spec.ts new file mode 100644 index 000000000..04262f03c --- /dev/null +++ b/projects/element-ng/application-header/launchpad/si-launchpad-navigation.spec.ts @@ -0,0 +1,135 @@ +/** + * Copyright (c) Siemens 2016 - 2025 + * SPDX-License-Identifier: MIT + */ +import { Component, provideZonelessChangeDetection } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SiApplicationHeaderComponent } from '../si-application-header.component'; +import { SiLaunchpadFactoryComponent } from './si-launchpad-factory.component'; + +describe('SiLaunchpadFactory - Keyboard Navigation', () => { + @Component({ + imports: [SiLaunchpadFactoryComponent], + template: `` + }) + class TestHostComponent {} + + let fixture: ComponentFixture; + let component: SiLaunchpadFactoryComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestHostComponent], + providers: [ + { provide: SiApplicationHeaderComponent, useValue: {} }, + provideZonelessChangeDetection() + ] + }); + + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.debugElement.children[0].componentInstance; + }); + + const createMockApps = (positions: { top: number; left: number }[]): HTMLElement[] => { + return positions.map(({ top, left }) => { + const app = document.createElement('a'); + spyOn(app, 'getBoundingClientRect').and.returnValue({ + top, + left, + width: 100, + height: 60 + } as DOMRect); + return app; + }); + }; + + describe('getVerticalTargetIndex', () => { + describe('single column layout', () => { + const testCases: [number, boolean, number, string][] = [ + [2, false, 3, 'down to next'], + [2, true, 1, 'up to previous'], + [5, false, 0, 'down wrap to first'], + [0, true, 5, 'up wrap to last'] + ]; + + testCases.forEach(([from, isUp, expected, description]) => { + it(`should navigate from ${from} ${isUp ? 'up' : 'down'} (${description})`, () => { + const mockApps = createMockApps([ + { top: 0, left: 10 }, + { top: 80, left: 10 }, + { top: 160, left: 10 }, + { top: 240, left: 10 }, + { top: 320, left: 10 }, + { top: 400, left: 10 } + ]); + + const result = (component as any).getVerticalTargetIndex(mockApps, from, isUp); + expect(result).toBe(expected); + }); + }); + }); + + describe('grid layout (2x3)', () => { + const gridPositions = [ + { top: 50, left: 10 }, + { top: 50, left: 130 }, + { top: 50, left: 250 }, // Row 0 + { top: 130, left: 10 }, + { top: 130, left: 130 }, + { top: 130, left: 250 } // Row 1 + ]; + + const gridTestCases: [number, boolean, number, string][] = [ + [4, true, 1, 'up from row 1 to row 0'], + [1, false, 4, 'down from row 0 to row 1'], + [2, false, 5, 'down with exact alignment'], + [1, true, 4, 'up wrap to bottom row'], + [4, false, 1, 'down wrap to top row'] + ]; + + gridTestCases.forEach(([from, isUp, expected, description]) => { + it(`should navigate from ${from} ${isUp ? 'up' : 'down'} (${description})`, () => { + const mockApps = createMockApps(gridPositions); + const result = (component as any).getVerticalTargetIndex(mockApps, from, isUp); + expect(result).toBe(expected); + }); + }); + + it('should stay in place for single row', () => { + const singleRowApps = createMockApps([ + { top: 50, left: 10 }, + { top: 50, left: 130 }, + { top: 50, left: 250 } + ]); + + const result = (component as any).getVerticalTargetIndex(singleRowApps, 1, false); + expect(result).toBe(1); + }); + }); + + describe('tolerance handling', () => { + it('should detect single column within leftTolerance (20px)', () => { + const mockApps = createMockApps([ + { top: 0, left: 10 }, + { top: 80, left: 25 }, + { top: 160, left: 15 } + ]); + + const result = (component as any).getVerticalTargetIndex(mockApps, 1, false); + expect(result).toBe(2); // Sequential navigation + }); + + it('should detect same row within rowTolerance (10px)', () => { + const mockApps = createMockApps([ + { top: 50, left: 10 }, + { top: 50, left: 130 }, + { top: 50, left: 250 } + ]); + + const result = (component as any).getVerticalTargetIndex(mockApps, 1, false); + expect(result).toBe(1); // No movement + }); + }); + }); +});