From 727f01cdede7ecf5c58a7880083016bfd2205aa2 Mon Sep 17 00:00:00 2001 From: Davide Mininni <101575400+DavideMininni-Fincons@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:34:32 +0100 Subject: [PATCH] refactor: remove link-button properties and create separate classes (#2364) --- CODING_STANDARDS.md | 50 +- scripts/chromatic-stories-generator.ts | 2 +- src/components/accordion/accordion.stories.ts | 1 - src/components/action-group/action-group.scss | 2 +- src/components/action-group/action-group.ts | 14 +- src/components/alert/alert/alert.ts | 19 +- src/components/alert/alert/readme.md | 28 +- src/components/autocomplete/autocomplete.ts | 7 +- src/components/autocomplete/readme.md | 2 +- .../breadcrumb-group/breadcrumb-group.ts | 17 +- .../__snapshots__/breadcrumb.spec.snap.js | 20 +- .../breadcrumb/breadcrumb/breadcrumb.scss | 4 - .../breadcrumb/breadcrumb/breadcrumb.spec.ts | 12 - .../breadcrumb/breadcrumb/breadcrumb.ts | 89 +-- .../breadcrumb/breadcrumb/readme.md | 2 +- .../button-link/button-link.e2e.ts} | 42 +- .../button/button-link/button-link.spec.ts | 75 ++ .../button/button-link/button-link.stories.ts | 196 ++++++ .../button/button-link/button-link.ts | 22 + src/components/button/button-link/index.ts | 1 + src/components/button/button-link/readme.md | 97 +++ .../button/button-static/button-static.e2e.ts | 67 ++ .../button-static/button-static.spec.ts | 103 +++ .../button-static/button-static.stories.ts | 145 ++++ .../button/button-static/button-static.ts | 22 + src/components/button/button-static/index.ts | 1 + src/components/button/button-static/readme.md | 76 +++ src/components/button/button.stories.ts | 641 ------------------ src/components/button/button.ts | 154 ----- .../button/{ => button}/button.e2e.ts | 33 +- .../button/{ => button}/button.spec.ts | 95 +-- .../button/button/button.stories.ts | 211 ++++++ src/components/button/button/button.ts | 22 + src/components/button/button/index.ts | 1 + src/components/button/button/readme.md | 90 +++ .../button/common/button-common-stories.ts | 377 ++++++++++ src/components/button/common/button-common.ts | 75 ++ .../button/{ => common}/button.scss | 8 +- src/components/button/common/index.ts | 1 + src/components/button/index.ts | 3 + src/components/button/readme.md | 104 --- src/components/calendar/calendar.e2e.ts | 1 - .../card/card-action/card-action.ts | 161 ----- src/components/card/card-action/index.ts | 1 - src/components/card/card-action/readme.md | 45 -- .../card-button.e2e.ts} | 120 +--- .../card-button.stories.ts} | 6 +- .../card/card-button/card-button.ts | 20 + src/components/card/card-button/index.ts | 1 + src/components/card/card-button/readme.md | 37 + .../card/card-link/card-link.e2e.ts | 195 ++++++ .../card/card-link/card-link.stories.ts | 28 + src/components/card/card-link/card-link.ts | 20 + src/components/card/card-link/index.ts | 1 + src/components/card/card-link/readme.md | 37 + src/components/card/card/card.stories.ts | 72 +- src/components/card/card/card.ts | 2 +- src/components/card/card/readme.md | 29 +- .../card/common/card-action-common.ts | 105 +++ .../{card-action => common}/card-action.scss | 2 +- src/components/card/common/index.ts | 1 + src/components/card/index.ts | 4 +- .../checkbox/checkbox-group/checkbox-group.ts | 7 +- .../checkbox/checkbox-group/readme.md | 2 +- .../__snapshots__/checkbox.spec.snap.js | 4 +- src/components/checkbox/checkbox/checkbox.ts | 15 +- src/components/checkbox/checkbox/readme.md | 22 +- .../action-base-element.e2e.ts | 32 + .../common-behaviors/action-base-element.ts | 20 + .../common-behaviors/action-dispatch-click.ts | 32 + .../button-base-element.e2e.ts | 91 +++ .../common-behaviors/button-base-element.ts | 110 +++ .../core/common-behaviors/disabled-mixins.ts | 52 ++ .../core/common-behaviors/icon-name-mixin.ts | 40 ++ src/components/core/common-behaviors/index.ts | 7 + .../common-behaviors/link-base-element.e2e.ts | 72 ++ .../common-behaviors/link-base-element.ts | 123 ++++ .../named-slot-list-element.ts | 211 +++--- .../core/common-behaviors/negative-mixin.ts | 23 + .../common-behaviors/slot-child-observer.ts | 10 +- .../core/eventing/action-element-handlers.ts | 160 ----- src/components/core/eventing/index.ts | 1 - src/components/core/i18n/i18n.ts | 2 +- src/components/core/interfaces/index.ts | 1 - .../interfaces/link-button-properties.spec.ts | 203 ------ .../core/interfaces/link-button-properties.ts | 182 ----- .../datepicker-next-day.ts | 63 +- .../datepicker/datepicker-next-day/readme.md | 13 +- .../datepicker-previous-day.ts | 62 +- .../datepicker-previous-day/readme.md | 13 +- .../datepicker-toggle/datepicker-toggle.ts | 7 +- src/components/dialog/dialog.e2e.ts | 1 - src/components/dialog/dialog.ts | 13 +- src/components/dialog/readme.md | 2 +- src/components/divider/divider.ts | 6 +- src/components/divider/readme.md | 2 +- .../expansion-panel-header.ts | 83 +-- .../expansion-panel-header/readme.md | 12 +- .../__snapshots__/file-selector.spec.snap.js | 10 +- src/components/file-selector/file-selector.ts | 21 +- src/components/file-selector/readme.md | 2 +- src/components/footer/footer.ts | 6 +- src/components/footer/readme.md | 2 +- src/components/form-error/form-error.ts | 8 +- .../form-field-clear/form-field-clear.ts | 48 +- .../form-field/form-field-clear/readme.md | 10 +- .../form-field/form-field/form-field.ts | 11 +- .../form-field/form-field/readme.md | 2 +- .../header/common/header-action-common.ts | 82 +++ .../header-action.scss | 8 +- src/components/header/common/index.ts | 1 + .../header-action/header-action.spec.ts | 97 --- .../header/header-action/header-action.ts | 138 ---- src/components/header/header-action/index.ts | 1 - src/components/header/header-action/readme.md | 56 -- .../header/header-button/header-button.e2e.ts | 50 ++ .../header-button/header-button.spec.ts | 53 ++ .../header-button/header-button.stories.ts | 137 ++++ .../header/header-button/header-button.ts | 22 + src/components/header/header-button/index.ts | 1 + src/components/header/header-button/readme.md | 48 ++ .../header/header-link/header-link.e2e.ts | 50 ++ .../header/header-link/header-link.spec.ts | 59 ++ .../header-link.stories.ts} | 80 +-- .../header/header-link/header-link.ts | 20 + src/components/header/header-link/index.ts | 1 + src/components/header/header-link/readme.md | 48 ++ .../header/__snapshots__/header.spec.snap.js | 6 +- src/components/header/header/header.e2e.ts | 16 +- src/components/header/header/header.scss | 6 +- src/components/header/header/header.spec.ts | 4 +- .../header/header/header.stories.ts | 41 +- src/components/header/header/readme.md | 33 +- src/components/header/index.ts | 4 +- .../journey-header/journey-header.ts | 7 +- src/components/journey-header/readme.md | 2 +- src/components/link-list/link-list.ts | 22 +- src/components/link-list/readme.md | 16 +- src/components/link/common/index.ts | 1 + .../link/common/link-common-stories.ts | 215 ++++++ src/components/link/common/link-common.ts | 68 ++ src/components/link/{ => common}/link.scss | 8 +- src/components/link/index.ts | 3 + src/components/link/link-button/index.ts | 1 + .../link/link-button/link-button.e2e.ts | 72 ++ .../link/link-button/link-button.spec.ts | 133 ++++ .../link/link-button/link-button.stories.ts | 136 ++++ .../link/link-button/link-button.ts | 22 + src/components/link/link-button/readme.md | 73 ++ src/components/link/link-static/index.ts | 1 + .../link/link-static/link-static.e2e.ts | 65 ++ .../link/link-static/link-static.spec.ts | 108 +++ .../link/link-static/link-static.stories.ts | 80 +++ .../link/link-static/link-static.ts | 22 + src/components/link/link-static/readme.md | 62 ++ src/components/link/link.spec.ts | 185 ----- src/components/link/link.stories.ts | 447 ------------ src/components/link/link.ts | 155 ----- src/components/link/link/index.ts | 1 + src/components/link/{ => link}/link.e2e.ts | 30 +- src/components/link/link/link.spec.ts | 82 +++ src/components/link/link/link.stories.ts | 142 ++++ src/components/link/link/link.ts | 22 + src/components/link/link/readme.md | 72 ++ src/components/link/readme.md | 86 --- src/components/logo/logo.ts | 6 +- src/components/logo/readme.md | 2 +- .../map-container/map-container.stories.ts | 4 +- src/components/menu/common/index.ts | 1 + .../menu/common/menu-action-common.ts | 56 ++ .../{menu-action => common}/menu-action.scss | 6 +- src/components/menu/index.ts | 4 +- src/components/menu/menu-action/index.ts | 1 - .../menu/menu-action/menu-action.spec.ts | 89 --- .../menu/menu-action/menu-action.ts | 127 ---- src/components/menu/menu-action/readme.md | 61 -- src/components/menu/menu-button/index.ts | 1 + .../menu/menu-button/menu-button.e2e.ts | 63 ++ .../menu/menu-button/menu-button.spec.ts | 72 ++ .../menu/menu-button/menu-button.stories.ts | 193 ++++++ .../menu/menu-button/menu-button.ts | 20 + src/components/menu/menu-button/readme.md | 53 ++ src/components/menu/menu-link/index.ts | 1 + .../menu-link.e2e.ts} | 45 +- .../menu/menu-link/menu-link.spec.ts | 54 ++ .../menu-link.stories.ts} | 90 +-- src/components/menu/menu-link/menu-link.ts | 20 + src/components/menu/menu-link/readme.md | 53 ++ .../menu/menu/__snapshots__/menu.spec.snap.js | 16 +- src/components/menu/menu/menu.e2e.ts | 16 +- src/components/menu/menu/menu.scss | 2 +- src/components/menu/menu/menu.spec.ts | 34 +- src/components/menu/menu/menu.stories.ts | 101 +-- src/components/menu/menu/menu.ts | 49 +- src/components/menu/menu/readme.md | 28 +- src/components/navigation/common/index.ts | 1 + .../common/navigation-action-common.ts | 91 +++ .../navigation-action.scss | 6 +- src/components/navigation/index.ts | 4 +- .../navigation/navigation-action/index.ts | 1 - .../navigation-action.e2e.ts | 78 --- .../navigation-action/navigation-action.ts | 159 ----- .../navigation/navigation-action/readme.md | 48 -- .../navigation/navigation-button/index.ts | 1 + .../navigation-button.e2e.ts | 52 ++ .../navigation-button.spec.ts} | 12 +- .../navigation-button.stories.ts | 120 ++++ .../navigation-button/navigation-button.ts | 21 + .../navigation/navigation-button/readme.md | 41 ++ .../navigation/navigation-link/index.ts | 1 + .../navigation-link/navigation-link.e2e.ts | 52 ++ .../navigation-link/navigation-link.spec.ts | 31 + .../navigation-link.stories.ts} | 52 +- .../navigation-link/navigation-link.ts | 21 + .../navigation/navigation-link/readme.md | 39 ++ .../navigation-list/navigation-list.e2e.ts | 8 +- .../navigation-list/navigation-list.spec.ts | 24 +- .../navigation-list.stories.ts | 10 +- .../navigation-list/navigation-list.ts | 22 +- .../navigation/navigation-list/readme.md | 9 +- .../navigation-marker.e2e.ts | 16 +- .../navigation-marker/navigation-marker.scss | 2 +- .../navigation-marker.stories.ts | 20 +- .../navigation-marker/navigation-marker.ts | 21 +- .../navigation/navigation-marker/readme.md | 25 +- .../navigation-section.e2e.ts | 10 +- .../navigation-section.scss | 2 +- .../navigation-section.stories.ts | 19 +- .../navigation-section/navigation-section.ts | 3 +- .../navigation/navigation-section/readme.md | 4 +- .../navigation/navigation/navigation.e2e.ts | 60 +- .../navigation/navigation/navigation.spec.ts | 12 +- .../navigation/navigation.stories.ts | 33 +- .../navigation/navigation/navigation.ts | 2 +- .../navigation/navigation/readme.md | 24 +- .../notification/notification.spec.ts | 1 - src/components/notification/notification.ts | 5 +- src/components/option/optgroup/optgroup.ts | 7 +- src/components/option/optgroup/readme.md | 8 +- src/components/option/option/option.ts | 26 +- src/components/option/option/readme.md | 4 +- .../popover-trigger/popover-trigger.spec.ts | 1 - .../popover-trigger/popover-trigger.ts | 57 +- .../popover/popover-trigger/readme.md | 15 +- .../radio-button-group/radio-button-group.ts | 9 +- .../radio-button/radio-button-group/readme.md | 2 +- src/components/select/readme.md | 4 +- src/components/select/select.ts | 12 +- src/components/skiplink-list/readme.md | 6 +- .../skiplink-list/skiplink-list.scss | 2 +- src/components/skiplink-list/skiplink-list.ts | 25 +- src/components/slider/readme.md | 2 +- src/components/slider/slider.ts | 6 +- src/components/status/readme.md | 12 +- src/components/status/status.spec.ts | 1 - src/components/status/status.ts | 31 +- .../__snapshots__/tab-title.spec.snap.js | 2 +- src/components/tabs/tab-title/readme.md | 4 +- src/components/tabs/tab-title/tab-title.ts | 26 +- src/components/tag/tag-group/tag-group.ts | 17 +- .../tag/tag/__snapshots__/tag.spec.snap.js | 2 +- src/components/tag/tag/readme.md | 19 +- src/components/tag/tag/tag.spec.ts | 17 +- src/components/tag/tag/tag.stories.ts | 1 - src/components/tag/tag/tag.ts | 76 +-- .../__snapshots__/teaser-hero.spec.snap.js | 43 +- src/components/teaser-hero/readme.md | 17 +- src/components/teaser-hero/teaser-hero.scss | 4 - .../teaser-hero/teaser-hero.spec.ts | 21 - src/components/teaser-hero/teaser-hero.ts | 109 +-- .../teaser/__snapshots__/teaser.spec.snap.js | 45 -- src/components/teaser/readme.md | 19 +- src/components/teaser/teaser.e2e.ts | 10 - src/components/teaser/teaser.scss | 4 - src/components/teaser/teaser.spec.ts | 10 - src/components/teaser/teaser.ts | 84 +-- .../timetable-occupancy-icon.e2e.snap.js | 18 +- .../timetable-occupancy-icon.ts | 7 +- .../timetable-occupancy.ts | 7 +- .../timetable-row/timetable-row.spec.ts | 8 +- src/components/timetable-row/timetable-row.ts | 4 +- src/components/title/readme.md | 2 +- src/components/title/title.ts | 6 +- .../toast/__snapshots__/toast.spec.snap.js | 4 +- src/components/toast/readme.md | 2 +- src/components/toast/toast.scss | 4 +- src/components/toast/toast.stories.ts | 8 +- src/components/toast/toast.ts | 39 +- .../__snapshots__/toggle-check.spec.snap.js | 2 +- src/components/toggle-check/readme.md | 4 +- src/components/toggle-check/toggle-check.ts | 16 +- src/components/toggle/toggle-option/readme.md | 12 +- .../toggle/toggle-option/toggle-option.ts | 15 +- .../train-formation/train-formation.e2e.ts | 1 - .../train/train-formation/train-formation.ts | 18 +- .../train/train-wagon/train-wagon.ts | 10 +- src/components/train/train/train.e2e.ts | 1 - src/components/train/train/train.spec.ts | 1 - src/components/train/train/train.ts | 20 +- src/components/visual-checkbox/readme.md | 12 +- .../visual-checkbox/visual-checkbox.ts | 10 +- .../pages/home/home--logged-in.stories.ts | 60 +- src/storybook/pages/home/home.common.ts | 126 ++-- src/storybook/pages/home/home.scss | 6 +- src/storybook/pages/home/home.stories.ts | 40 +- src/storybook/testing/chromatic.ts | 7 +- 306 files changed, 7140 insertions(+), 5560 deletions(-) rename src/components/{header/header-action/header-action.e2e.ts => button/button-link/button-link.e2e.ts} (62%) create mode 100644 src/components/button/button-link/button-link.spec.ts create mode 100644 src/components/button/button-link/button-link.stories.ts create mode 100644 src/components/button/button-link/button-link.ts create mode 100644 src/components/button/button-link/index.ts create mode 100644 src/components/button/button-link/readme.md create mode 100644 src/components/button/button-static/button-static.e2e.ts create mode 100644 src/components/button/button-static/button-static.spec.ts create mode 100644 src/components/button/button-static/button-static.stories.ts create mode 100644 src/components/button/button-static/button-static.ts create mode 100644 src/components/button/button-static/index.ts create mode 100644 src/components/button/button-static/readme.md delete mode 100644 src/components/button/button.stories.ts delete mode 100644 src/components/button/button.ts rename src/components/button/{ => button}/button.e2e.ts (68%) rename src/components/button/{ => button}/button.spec.ts (54%) create mode 100644 src/components/button/button/button.stories.ts create mode 100644 src/components/button/button/button.ts create mode 100644 src/components/button/button/index.ts create mode 100644 src/components/button/button/readme.md create mode 100644 src/components/button/common/button-common-stories.ts create mode 100644 src/components/button/common/button-common.ts rename src/components/button/{ => common}/button.scss (99%) create mode 100644 src/components/button/common/index.ts delete mode 100644 src/components/button/readme.md delete mode 100644 src/components/card/card-action/card-action.ts delete mode 100644 src/components/card/card-action/index.ts delete mode 100644 src/components/card/card-action/readme.md rename src/components/card/{card-action/card-action.e2e.ts => card-button/card-button.e2e.ts} (57%) rename src/components/card/{card-action/card-action.stories.ts => card-button/card-button.stories.ts} (76%) create mode 100644 src/components/card/card-button/card-button.ts create mode 100644 src/components/card/card-button/index.ts create mode 100644 src/components/card/card-button/readme.md create mode 100644 src/components/card/card-link/card-link.e2e.ts create mode 100644 src/components/card/card-link/card-link.stories.ts create mode 100644 src/components/card/card-link/card-link.ts create mode 100644 src/components/card/card-link/index.ts create mode 100644 src/components/card/card-link/readme.md create mode 100644 src/components/card/common/card-action-common.ts rename src/components/card/{card-action => common}/card-action.scss (95%) create mode 100644 src/components/card/common/index.ts create mode 100644 src/components/core/common-behaviors/action-base-element.e2e.ts create mode 100644 src/components/core/common-behaviors/action-base-element.ts create mode 100644 src/components/core/common-behaviors/action-dispatch-click.ts create mode 100644 src/components/core/common-behaviors/button-base-element.e2e.ts create mode 100644 src/components/core/common-behaviors/button-base-element.ts create mode 100644 src/components/core/common-behaviors/disabled-mixins.ts create mode 100644 src/components/core/common-behaviors/icon-name-mixin.ts create mode 100644 src/components/core/common-behaviors/link-base-element.e2e.ts create mode 100644 src/components/core/common-behaviors/link-base-element.ts create mode 100644 src/components/core/common-behaviors/negative-mixin.ts delete mode 100644 src/components/core/eventing/action-element-handlers.ts delete mode 100644 src/components/core/interfaces/link-button-properties.spec.ts delete mode 100644 src/components/core/interfaces/link-button-properties.ts create mode 100644 src/components/header/common/header-action-common.ts rename src/components/header/{header-action => common}/header-action.scss (97%) create mode 100644 src/components/header/common/index.ts delete mode 100644 src/components/header/header-action/header-action.spec.ts delete mode 100644 src/components/header/header-action/header-action.ts delete mode 100644 src/components/header/header-action/index.ts delete mode 100644 src/components/header/header-action/readme.md create mode 100644 src/components/header/header-button/header-button.e2e.ts create mode 100644 src/components/header/header-button/header-button.spec.ts create mode 100644 src/components/header/header-button/header-button.stories.ts create mode 100644 src/components/header/header-button/header-button.ts create mode 100644 src/components/header/header-button/index.ts create mode 100644 src/components/header/header-button/readme.md create mode 100644 src/components/header/header-link/header-link.e2e.ts create mode 100644 src/components/header/header-link/header-link.spec.ts rename src/components/header/{header-action/header-action.stories.ts => header-link/header-link.stories.ts} (64%) create mode 100644 src/components/header/header-link/header-link.ts create mode 100644 src/components/header/header-link/index.ts create mode 100644 src/components/header/header-link/readme.md create mode 100644 src/components/link/common/index.ts create mode 100644 src/components/link/common/link-common-stories.ts create mode 100644 src/components/link/common/link-common.ts rename src/components/link/{ => common}/link.scss (95%) create mode 100644 src/components/link/link-button/index.ts create mode 100644 src/components/link/link-button/link-button.e2e.ts create mode 100644 src/components/link/link-button/link-button.spec.ts create mode 100644 src/components/link/link-button/link-button.stories.ts create mode 100644 src/components/link/link-button/link-button.ts create mode 100644 src/components/link/link-button/readme.md create mode 100644 src/components/link/link-static/index.ts create mode 100644 src/components/link/link-static/link-static.e2e.ts create mode 100644 src/components/link/link-static/link-static.spec.ts create mode 100644 src/components/link/link-static/link-static.stories.ts create mode 100644 src/components/link/link-static/link-static.ts create mode 100644 src/components/link/link-static/readme.md delete mode 100644 src/components/link/link.spec.ts delete mode 100644 src/components/link/link.stories.ts delete mode 100644 src/components/link/link.ts create mode 100644 src/components/link/link/index.ts rename src/components/link/{ => link}/link.e2e.ts (71%) create mode 100644 src/components/link/link/link.spec.ts create mode 100644 src/components/link/link/link.stories.ts create mode 100644 src/components/link/link/link.ts create mode 100644 src/components/link/link/readme.md delete mode 100644 src/components/link/readme.md create mode 100644 src/components/menu/common/index.ts create mode 100644 src/components/menu/common/menu-action-common.ts rename src/components/menu/{menu-action => common}/menu-action.scss (96%) delete mode 100644 src/components/menu/menu-action/index.ts delete mode 100644 src/components/menu/menu-action/menu-action.spec.ts delete mode 100644 src/components/menu/menu-action/menu-action.ts delete mode 100644 src/components/menu/menu-action/readme.md create mode 100644 src/components/menu/menu-button/index.ts create mode 100644 src/components/menu/menu-button/menu-button.e2e.ts create mode 100644 src/components/menu/menu-button/menu-button.spec.ts create mode 100644 src/components/menu/menu-button/menu-button.stories.ts create mode 100644 src/components/menu/menu-button/menu-button.ts create mode 100644 src/components/menu/menu-button/readme.md create mode 100644 src/components/menu/menu-link/index.ts rename src/components/menu/{menu-action/menu-action.e2e.ts => menu-link/menu-link.e2e.ts} (56%) create mode 100644 src/components/menu/menu-link/menu-link.spec.ts rename src/components/menu/{menu-action/menu-action.stories.ts => menu-link/menu-link.stories.ts} (73%) create mode 100644 src/components/menu/menu-link/menu-link.ts create mode 100644 src/components/menu/menu-link/readme.md create mode 100644 src/components/navigation/common/index.ts create mode 100644 src/components/navigation/common/navigation-action-common.ts rename src/components/navigation/{navigation-action => common}/navigation-action.scss (93%) delete mode 100644 src/components/navigation/navigation-action/index.ts delete mode 100644 src/components/navigation/navigation-action/navigation-action.e2e.ts delete mode 100644 src/components/navigation/navigation-action/navigation-action.ts delete mode 100644 src/components/navigation/navigation-action/readme.md create mode 100644 src/components/navigation/navigation-button/index.ts create mode 100644 src/components/navigation/navigation-button/navigation-button.e2e.ts rename src/components/navigation/{navigation-action/navigation-action.spec.ts => navigation-button/navigation-button.spec.ts} (50%) create mode 100644 src/components/navigation/navigation-button/navigation-button.stories.ts create mode 100644 src/components/navigation/navigation-button/navigation-button.ts create mode 100644 src/components/navigation/navigation-button/readme.md create mode 100644 src/components/navigation/navigation-link/index.ts create mode 100644 src/components/navigation/navigation-link/navigation-link.e2e.ts create mode 100644 src/components/navigation/navigation-link/navigation-link.spec.ts rename src/components/navigation/{navigation-action/navigation-action.stories.ts => navigation-link/navigation-link.stories.ts} (73%) create mode 100644 src/components/navigation/navigation-link/navigation-link.ts create mode 100644 src/components/navigation/navigation-link/readme.md diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md index 2a7c8df5c3..a6d8c851c7 100644 --- a/CODING_STANDARDS.md +++ b/CODING_STANDARDS.md @@ -80,37 +80,51 @@ leave it out. This applies especially to providing two different APIs to accomplish the same thing. Always prefer sticking to a _single_ API for accomplishing something. -#### Click event handling on action elements +#### Action elements As we have to "reimplement" button and anchor functionality in order to comply with accessibility, we need to consider all native behavior of a native ``, + html` + Click me + + + + `, ); expect(element).to.have.attribute('data-has-action'); @@ -96,7 +65,7 @@ describe('sbb-card-action', () => { expect(element).to.have.attribute('data-action-role', 'button'); // Remove action from DOM - element.querySelector('sbb-card-action')!.remove(); + element.querySelector('sbb-card-button')!.remove(); await waitForLitRender(element); expect(element).not.to.have.attribute('data-has-action'); @@ -106,10 +75,12 @@ describe('sbb-card-action', () => { it('should detect added button in slotted content to update focusable elements', async () => { element = await fixture( - html`Click me`, + html` + Click me + + + + `, ); expect(document.querySelector('button')).to.have.attribute('data-card-focusable'); @@ -136,9 +107,10 @@ describe('sbb-card-action', () => { it('should detect added second element of slot to update focusable elements', async () => { element = await fixture( - html`Click me`, + html` + Click me + + `, ); // Add a button to slot @@ -153,15 +125,15 @@ describe('sbb-card-action', () => { it('should detect focusable elements when action was added at later point', async () => { element = await fixture( - html``, + html` + + `, ); - // Add a sbb-card-action + // Add a sbb-card-button document .querySelector('sbb-card')! - .appendChild(document.createElement('sbb-card-action')); + .appendChild(document.createElement('sbb-card-button')); await waitForLitRender(element); // Button should be marked as focusable @@ -169,13 +141,13 @@ describe('sbb-card-action', () => { }); describe('events', () => { - let action: SbbCardActionElement; + let action: SbbCardButtonElement; beforeEach(async () => { element = await fixture( - html`CardContent`, + html`CardContent`, ); - action = document.querySelector('sbb-card-action')!; + action = document.querySelector('sbb-card-button')!; }); it('dispatches event on click', async () => { @@ -202,26 +174,6 @@ describe('sbb-card-action', () => { expect(clickSpy.count).to.be.greaterThan(0); }); - it('should dispatch click event on pressing Enter with href', async () => { - element.setAttribute('href', 'test'); - await waitForLitRender(element); - - const clickSpy = new EventSpy('click'); - action.focus(); - await sendKeys({ press: 'Enter' }); - expect(clickSpy.count).to.be.greaterThan(0); - }); - - it('should not dispatch click event on pressing Space with href', async () => { - action.setAttribute('href', 'test'); - await waitForLitRender(element); - - const clickSpy = new EventSpy('click'); - action.focus(); - await sendKeys({ press: ' ' }); - expect(clickSpy.count).not.to.be.greaterThan(0); - }); - it('should receive focus', async () => { action.focus(); await waitForLitRender(element); diff --git a/src/components/card/card-action/card-action.stories.ts b/src/components/card/card-button/card-button.stories.ts similarity index 76% rename from src/components/card/card-action/card-action.stories.ts rename to src/components/card/card-button/card-button.stories.ts index 2197d5fb5b..4f7690c9db 100644 --- a/src/components/card/card-action/card-action.stories.ts +++ b/src/components/card/card-button/card-button.stories.ts @@ -8,11 +8,11 @@ import '../card'; const Template = (): TemplateResult => html` - 'sbb-card-action' is an invisible action element. See 'sbb-card' examples to see it in action. + 'sbb-card-button' is an invisible action element. See 'sbb-card' examples to see it in action. `; -export const SbbCardActionElement: StoryObj = { +export const SbbCardButtonElement: StoryObj = { render: Template, }; @@ -23,7 +23,7 @@ const meta: Meta = { extractComponentDescription: () => readme, }, }, - title: 'components/sbb-card/sbb-card-action', + title: 'components/sbb-card/sbb-card-button', }; export default meta; diff --git a/src/components/card/card-button/card-button.ts b/src/components/card/card-button/card-button.ts new file mode 100644 index 0000000000..a9a9458776 --- /dev/null +++ b/src/components/card/card-button/card-button.ts @@ -0,0 +1,20 @@ +import { customElement } from 'lit/decorators.js'; + +import { SbbButtonBaseElement } from '../../core/common-behaviors'; +import { SbbCardActionCommonElementMixin } from '../common/card-action-common'; + +/** + * It turns the `sbb-card` into a button element. + * + * @slot - Use the unnamed slot to add a descriptive label / title of the button (important!). + * This is relevant for SEO and screen readers. + */ +@customElement('sbb-card-button') +export class SbbCardButtonElement extends SbbCardActionCommonElementMixin(SbbButtonBaseElement) {} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-card-button': SbbCardButtonElement; + } +} diff --git a/src/components/card/card-button/index.ts b/src/components/card/card-button/index.ts new file mode 100644 index 0000000000..7218689b68 --- /dev/null +++ b/src/components/card/card-button/index.ts @@ -0,0 +1 @@ +export * from './card-button'; diff --git a/src/components/card/card-button/readme.md b/src/components/card/card-button/readme.md new file mode 100644 index 0000000000..d52af8ee81 --- /dev/null +++ b/src/components/card/card-button/readme.md @@ -0,0 +1,37 @@ +The `sbb-card-button` is the component used to turn a `sbb-card` into a button. + +```html +Buy this trip! +``` + +## Button properties + +The component is internally rendered as a button, +accepting its associated properties (`type`, `name`, `value` and `form`). + +## Accessibility + +It's **important** that a descriptive message is being slotted into the unnamed slot of `sbb-card-button` +as it is used for search engines and screen-reader users. + +```html +Buy a half-fare ticket now +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------- | --------- | ------- | ------------------------- | ------- | ------------------------------------------------ | +| `active` | `active` | public | `boolean` | `false` | Whether the card is active. | +| `type` | `type` | public | `ButtonType \| undefined` | | The type attribute to use for the button. | +| `name` | `name` | public | `string \| undefined` | | The name attribute to use for the button. | +| `value` | `value` | public | `string \| undefined` | | The value attribute to use for the button. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | + +## Slots + +| Name | Description | +| ---- | -------------------------------------------------------------------------------------------------------------------------------- | +| | Use the unnamed slot to add a descriptive label / title of the button (important!). This is relevant for SEO and screen readers. | diff --git a/src/components/card/card-link/card-link.e2e.ts b/src/components/card/card-link/card-link.e2e.ts new file mode 100644 index 0000000000..edf33588e8 --- /dev/null +++ b/src/components/card/card-link/card-link.e2e.ts @@ -0,0 +1,195 @@ +import { expect, fixture } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing'; +import type { SbbCardElement } from '../card'; + +import type { SbbCardLinkElement } from './card-link'; + +import '../card'; +import './card-link'; + +describe('sbb-card-link', () => { + let element: SbbCardElement; + + it('should render an sbb-card-link as a link opening in a new window', async () => { + element = await fixture(html` + + Follow me + Content text + + `); + + expect(element).to.have.attribute('data-has-action'); + expect(element).not.to.have.attribute('data-has-active-action'); + expect(element).to.have.attribute('data-action-role', 'link'); + + const cardAction = element.querySelector('sbb-card-link'); + + expect(cardAction).dom.to.be.equal(` + + Follow me + + `); + expect(cardAction).shadowDom.to.be.equal(` + + + + + + . Link target opens in a new window. + + + `); + }); + + it('should correctly toggle active state', async () => { + element = await fixture( + html`Click meContent`, + ); + expect(element).not.to.have.attribute('data-has-active-action'); + + element.querySelector('sbb-card-link')!.setAttribute('active', ''); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-has-active-action'); + }); + + it('should remove data properties from host', async () => { + element = await fixture( + html`Click me`, + ); + + expect(element).to.have.attribute('data-has-action'); + expect(element).to.have.attribute('data-has-active-action'); + expect(element).to.have.attribute('data-action-role', 'link'); + + // Remove action from DOM + element.querySelector('sbb-card-link')!.remove(); + await waitForLitRender(element); + + expect(element).not.to.have.attribute('data-has-action'); + expect(element).not.to.have.attribute('data-has-active-action'); + expect(element).not.to.have.attribute('data-action-role', 'button'); + }); + + it('should detect added link in slotted content to update focusable elements', async () => { + element = await fixture( + html` + Click me + + + + `, + ); + expect(document.querySelector('button')).to.have.attribute('data-card-focusable'); + + // Add a second button in content + document + .getElementById('content')! + .insertBefore(document.createElement('button'), document.querySelector('button')); + + // Both buttons should be marked as focusable + await waitForLitRender(element); + const buttons = document.querySelectorAll('button'); + expect(buttons.length).to.be.equal(2); + expect( + Array.from(buttons).every((el) => el.getAttribute('data-card-focusable') !== null), + ).to.be.equal(true); + + // Remove all buttons + buttons.forEach((el) => el.remove()); + await waitForLitRender(element); + + // Card should not have marker anymore + expect(document.querySelectorAll('button').length).to.be.equal(0); + }); + + it('should detect added second element of slot to update focusable elements', async () => { + element = await fixture( + html` + Click me + + `, + ); + + // Add a button to slot + document + .querySelector('sbb-card')! + .insertBefore(document.createElement('button'), document.getElementById('content')); + await waitForLitRender(element); + + // Button should be marked as focusable + expect(document.querySelector('button')).to.have.attribute('data-card-focusable'); + }); + + it('should detect focusable elements when action was added at later point', async () => { + element = await fixture( + html` + + + + `, + ); + + // Add a sbb-card-link + const link = document.createElement('sbb-card-link'); + link.setAttribute('href', '#'); + document.querySelector('sbb-card')!.appendChild(link); + await waitForLitRender(element); + + // Button should be marked as focusable + expect(document.querySelector('button')).to.have.attribute('data-card-focusable'); + }); + + describe('events', () => { + let action: SbbCardLinkElement; + + beforeEach(async () => { + element = await fixture(html` + + Card + Content + + `); + action = document.querySelector('sbb-card-link')!; + }); + + it('dispatches event on click', async () => { + await waitForLitRender(element); + const clickSpy = new EventSpy('click'); + + action.click(); + + await waitForCondition(() => clickSpy.events.length === 1); + expect(clickSpy.count).to.be.equal(1); + }); + + it('should dispatch click event on pressing Enter', async () => { + const clickSpy = new EventSpy('click'); + action.focus(); + await sendKeys({ press: 'Enter' }); + expect(clickSpy.count).to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Space', async () => { + const clickSpy = new EventSpy('click'); + action.focus(); + await sendKeys({ press: ' ' }); + expect(clickSpy.count).not.to.be.greaterThan(0); + }); + + it('should receive focus', async () => { + action.focus(); + await waitForLitRender(element); + + expect(document.activeElement!.id).to.be.equal('focus-id'); + }); + }); +}); diff --git a/src/components/card/card-link/card-link.stories.ts b/src/components/card/card-link/card-link.stories.ts new file mode 100644 index 0000000000..d057486167 --- /dev/null +++ b/src/components/card/card-link/card-link.stories.ts @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import { html, type TemplateResult } from 'lit'; + +import readme from './readme.md?raw'; + +import '../card'; + +const Template = (): TemplateResult => html` + + 'sbb-card-link' is an invisible action element. See 'sbb-card' examples to see it in action. + +`; + +export const SbbCardLinkElement: StoryObj = { + render: Template, +}; + +const meta: Meta = { + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'components/sbb-card/sbb-card-link', +}; + +export default meta; diff --git a/src/components/card/card-link/card-link.ts b/src/components/card/card-link/card-link.ts new file mode 100644 index 0000000000..84d007db98 --- /dev/null +++ b/src/components/card/card-link/card-link.ts @@ -0,0 +1,20 @@ +import { customElement } from 'lit/decorators.js'; + +import { SbbLinkBaseElement } from '../../core/common-behaviors'; +import { SbbCardActionCommonElementMixin } from '../common/card-action-common'; + +/** + * It turns the `sbb-card` into a link element. + * + * @slot - Use the unnamed slot to add a descriptive label / title of the link (important!). + * This is relevant for SEO and screen readers. + */ +@customElement('sbb-card-link') +export class SbbCardLinkElement extends SbbCardActionCommonElementMixin(SbbLinkBaseElement) {} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-card-link': SbbCardLinkElement; + } +} diff --git a/src/components/card/card-link/index.ts b/src/components/card/card-link/index.ts new file mode 100644 index 0000000000..e390095a59 --- /dev/null +++ b/src/components/card/card-link/index.ts @@ -0,0 +1 @@ +export * from './card-link'; diff --git a/src/components/card/card-link/readme.md b/src/components/card/card-link/readme.md new file mode 100644 index 0000000000..bbf2e2a780 --- /dev/null +++ b/src/components/card/card-link/readme.md @@ -0,0 +1,37 @@ +The `sbb-card-link` is the component used to turn a `sbb-card` into a link. + +```html +Check all the wonderful trips available. +``` + +## Link properties + +The component is internally rendered as a link, +accepting its associated properties (`href`, `target`, `rel` and `download`). + +## Accessibility + +It's **important** that a descriptive message is being slotted into the unnamed slot of `sbb-card-link` +as it is used for search engines and screen-reader users. + +```html +Buy a half-fare ticket now +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------- | ---------- | ------- | --------------------------------------- | ------- | ----------------------------------------------------------------- | +| `active` | `active` | public | `boolean` | `false` | Whether the card is active. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | + +## Slots + +| Name | Description | +| ---- | ------------------------------------------------------------------------------------------------------------------------------ | +| | Use the unnamed slot to add a descriptive label / title of the link (important!). This is relevant for SEO and screen readers. | diff --git a/src/components/card/card/card.stories.ts b/src/components/card/card/card.stories.ts index 63d1f471d7..aee0338fee 100644 --- a/src/components/card/card/card.stories.ts +++ b/src/components/card/card/card.stories.ts @@ -17,7 +17,8 @@ import { sbbSpread } from '../../core/dom'; import readme from './readme.md?raw'; import './card'; import '../card-badge'; -import '../card-action'; +import '../card-button'; +import '../card-link'; import '../../title'; const wrapperStyle = (context: StoryContext): Record => ({ @@ -58,10 +59,19 @@ const TemplateWithBadge = ({ size, color }: Args): TemplateResult => html` `; +const TemplateAction = ({ label, ...args }: Args): TemplateResult => { + if (args.href) { + ['type', 'form', 'value', 'name'].forEach((k) => delete args[k]); + return html`${label}`; + } else { + ['href', 'target', 'rel', 'download'].forEach((k) => delete args[k]); + return html`${label}`; + } +}; + const TemplateCardAction = ({ size, color, label, ...args }: Args): TemplateResult => html` - ${label} - ${Content()} + ${TemplateAction({ label, ...args })} ${Content()} `; @@ -72,14 +82,13 @@ const TemplateCardActionFixedHeight = ({ ...args }: Args): TemplateResult => html` - ${label} - ${Content()} + ${TemplateAction({ label, ...args })} ${Content()} `; const TemplateCardActionWithBadge = ({ size, color, label, ...args }: Args): TemplateResult => html` - ${label} + ${TemplateAction({ label, ...args })} % from CHF @@ -213,26 +222,32 @@ const defaultArgTypes: ArgTypes = { color, }; -const defaultArgTypesAction: ArgTypes = { +const defaultArgTypesButton: ArgTypes = { ...defaultArgTypes, active, label, - href, - download, - target, - rel, name, type, form, value, }; +const defaultArgTypesLink: ArgTypes = { + ...defaultArgTypes, + active, + label, + href, + download, + target, + rel, +}; + const defaultArgs: Args = { size: 'm', color: color.options[0], }; -const defaultArgsAction = { +const defaultArgsLink = { ...defaultArgs, active: false, label: 'Click this card to follow the action.', @@ -247,10 +262,11 @@ const defaultArgsAction = { }; const defaultArgsButton = { - ...defaultArgsAction, + ...defaultArgsLink, href: undefined, download: undefined, target: undefined, + rel: undefined, name: 'Button name', type: type.options[0], form: 'form-name', @@ -402,25 +418,25 @@ export const SizeXXXLWithBadge: StoryObj = { export const Link: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, - args: { ...defaultArgsAction }, + argTypes: defaultArgTypesLink, + args: { ...defaultArgsLink }, }; export const Button: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton }, }; export const ButtonActive: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton, active: true }, }; export const ButtonActiveMilk: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton, color: color.options[1], @@ -430,7 +446,7 @@ export const ButtonActiveMilk: StoryObj = { export const ButtonActiveTransparentBordered: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton, color: color.options[2], @@ -440,7 +456,7 @@ export const ButtonActiveTransparentBordered: StoryObj = { export const ButtonActiveTransparentBorderedDashed: StoryObj = { render: TemplateCardAction, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton, color: color.options[3], @@ -450,32 +466,32 @@ export const ButtonActiveTransparentBorderedDashed: StoryObj = { export const ButtonWithSbbBadge: StoryObj = { render: TemplateCardActionWithBadge, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton }, }; export const LinkWithSbbBadge: StoryObj = { render: TemplateCardActionWithBadge, - argTypes: defaultArgTypesAction, - args: { ...defaultArgsAction }, + argTypes: defaultArgTypesLink, + args: { ...defaultArgsLink }, }; export const LinkActiveWithSbbBadge: StoryObj = { render: TemplateCardActionWithBadge, - argTypes: defaultArgTypesAction, - args: { ...defaultArgsAction, active: true }, + argTypes: defaultArgTypesLink, + args: { ...defaultArgsLink, active: true }, }; export const FixedHeight: StoryObj = { render: TemplateCardActionFixedHeight, - argTypes: defaultArgTypesAction, + argTypes: defaultArgTypesButton, args: { ...defaultArgsButton }, }; export const Multiple: StoryObj = { render: TemplateCardActionMultipleCards, - argTypes: defaultArgTypesAction, - args: { ...defaultArgsAction }, + argTypes: defaultArgTypesLink, + args: { ...defaultArgsLink }, }; const meta: Meta = { diff --git a/src/components/card/card/card.ts b/src/components/card/card/card.ts index 274ef6508b..669553527d 100644 --- a/src/components/card/card/card.ts +++ b/src/components/card/card/card.ts @@ -9,7 +9,7 @@ import style from './card.scss?lit&inline'; * * @slot - Use the unnamed slot to add content to the card. * @slot badge - Use this slot to render a `sbb-card-badge` component. - * @slot action - Use this slot to render a `sbb-card-action` component. + * @slot action - Use this slot to render a `sbb-card-button` or a `sbb-card-link` component. */ @customElement('sbb-card') export class SbbCardElement extends LitElement { diff --git a/src/components/card/card/readme.md b/src/components/card/card/readme.md index 6749978a2b..feefccd662 100644 --- a/src/components/card/card/readme.md +++ b/src/components/card/card/readme.md @@ -7,7 +7,7 @@ The `sbb-card` component is a generic content container; its task is to contain ## Slots The content is projected in an unnamed slot. -It's possible to use the component together with the `sbb-card-badge` and the `sbb-card-action`. +It's possible to use the component together with the `sbb-card-badge` and the `sbb-card-button`/`sbb-card-link`. ### With `sbb-card-badge` @@ -26,19 +26,22 @@ For API details, see the [sbb-card-badge](/docs/components-sbb-card-sbb-card-bad ``` -### With `sbb-card-action` +### With `sbb-card-button`/`sbb-card-link` -To add an action to a card, add a `sbb-card-action` to the main slot. -With the `sbb-card-action` all the card area becomes clickable. -For API details (mainly accessibility), see the [sbb-card-action](/docs/components-sbb-card-sbb-card-action--docs) docs. +To add an action to a card, add a `sbb-card-button` or a `sbb-card-link` to the main slot. +With these components, all the card area becomes clickable. +For API details (mainly accessibility), see the [sbb-card-button](/docs/components-sbb-card-sbb-card-button--docs) or +the [sbb-card-link](/docs/components-sbb-card-sbb-card-link--docs) docs. ```html - Check all the wonderful trips available. + Check all the wonderful trips available. Buy trips + + + Buy this trip. + ``` ## Style @@ -90,8 +93,8 @@ To improve coloring, it's needed to manually define styles for Window high contr ## Slots -| Name | Description | -| -------- | ------------------------------------------------------ | -| | Use the unnamed slot to add content to the card. | -| `badge` | Use this slot to render a `sbb-card-badge` component. | -| `action` | Use this slot to render a `sbb-card-action` component. | +| Name | Description | +| -------- | --------------------------------------------------------------------------- | +| | Use the unnamed slot to add content to the card. | +| `badge` | Use this slot to render a `sbb-card-badge` component. | +| `action` | Use this slot to render a `sbb-card-button` or a `sbb-card-link` component. | diff --git a/src/components/card/common/card-action-common.ts b/src/components/card/common/card-action-common.ts new file mode 100644 index 0000000000..0ca5b726c8 --- /dev/null +++ b/src/components/card/common/card-action-common.ts @@ -0,0 +1,105 @@ +import type { CSSResultGroup, TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; +import { html } from 'lit/static-html.js'; + +import { IS_FOCUSABLE_QUERY } from '../../core/a11y'; +import type { AbstractConstructor, SbbActionBaseElement } from '../../core/common-behaviors'; +import { setAttribute, toggleDatasetEntry } from '../../core/dom'; +import { AgnosticMutationObserver } from '../../core/observers'; +import type { SbbCardElement } from '../card'; + +import style from './card-action.scss?lit&inline'; + +export declare class SbbCardActionCommonElementMixinType { + public active: boolean; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbCardActionCommonElementMixin = < + T extends AbstractConstructor, +>( + superClass: T, +): AbstractConstructor & T => { + abstract class SbbCardActionCommonElement + extends superClass + implements Partial + { + public static styles: CSSResultGroup = style; + + /** Whether the card is active. */ + @property({ reflect: true, type: Boolean }) + public set active(value: boolean) { + this._active = value; + this._onActiveChange(); + } + public get active(): boolean { + return this._active; + } + private _active: boolean = false; + + private _card: SbbCardElement | null = null; + private _cardMutationObserver = new AgnosticMutationObserver(() => + this._checkForSlottedActions(), + ); + + private _onActiveChange(): void { + if (this._card) { + toggleDatasetEntry(this._card, 'hasActiveAction', this.active); + } + } + + private _checkForSlottedActions(): void { + const cardFocusableAttributeName = 'data-card-focusable'; + + Array.from(this._card?.querySelectorAll?.(IS_FOCUSABLE_QUERY) ?? []) + .filter( + (el) => + el.tagName !== 'SBB-CARD-LINK' && + el.tagName !== 'SBB-CARD-BUTTON' && + !el.hasAttribute(cardFocusableAttributeName), + ) + .forEach((el: Element) => el.setAttribute(cardFocusableAttributeName, '')); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._card = this.closest?.('sbb-card'); + if (this._card) { + toggleDatasetEntry(this._card, 'hasAction', true); + toggleDatasetEntry(this._card, 'hasActiveAction', this.active); + this._card.dataset.actionRole = this.getAttribute('role')!; + setAttribute(this, 'slot', 'action'); + + this._checkForSlottedActions(); + this._cardMutationObserver.observe(this._card, { + childList: true, + subtree: true, + }); + } + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._card) { + toggleDatasetEntry(this._card, 'hasAction', false); + toggleDatasetEntry(this._card, 'hasActiveAction', false); + toggleDatasetEntry(this._card, 'actionRole', false); + this._card + .querySelectorAll(`[data-card-focusable]`) + .forEach((el) => el.removeAttribute('data-card-focusable')); + this._card = null; + } + this._cardMutationObserver.disconnect(); + } + + protected override renderTemplate(): TemplateResult { + return html` + + + + `; + } + } + return SbbCardActionCommonElement as unknown as AbstractConstructor & + T; +}; diff --git a/src/components/card/card-action/card-action.scss b/src/components/card/common/card-action.scss similarity index 95% rename from src/components/card/card-action/card-action.scss rename to src/components/card/common/card-action.scss index 997b90fe0f..1996215cdf 100644 --- a/src/components/card/card-action/card-action.scss +++ b/src/components/card/common/card-action.scss @@ -12,7 +12,7 @@ inset: 0; } -.sbb-card-action { +:is(.sbb-card-button, .sbb-card-link) { display: block; position: absolute; inset: 0; diff --git a/src/components/card/common/index.ts b/src/components/card/common/index.ts new file mode 100644 index 0000000000..445f7add31 --- /dev/null +++ b/src/components/card/common/index.ts @@ -0,0 +1 @@ +export * from './card-action-common'; diff --git a/src/components/card/index.ts b/src/components/card/index.ts index e56c7f0f67..3d0ed284a1 100644 --- a/src/components/card/index.ts +++ b/src/components/card/index.ts @@ -1,3 +1,5 @@ export * from './card'; -export * from './card-action'; export * from './card-badge'; +export * from './card-button'; +export * from './card-link'; +export * from './common'; diff --git a/src/components/checkbox/checkbox-group/checkbox-group.ts b/src/components/checkbox/checkbox-group/checkbox-group.ts index f055d0ca71..bd3ce36d93 100644 --- a/src/components/checkbox/checkbox-group/checkbox-group.ts +++ b/src/components/checkbox/checkbox-group/checkbox-group.ts @@ -3,7 +3,7 @@ import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { getNextElementIndex, interactivityChecker, isArrowKeyPressed } from '../../core/a11y'; -import { NamedSlotStateController } from '../../core/common-behaviors'; +import { NamedSlotStateController, SbbDisabledMixin } from '../../core/common-behaviors'; import { toggleDatasetEntry } from '../../core/dom'; import { ConnectedAbortController } from '../../core/eventing'; import type { SbbHorizontalFrom, SbbOrientation } from '../../core/interfaces'; @@ -18,12 +18,9 @@ import style from './checkbox-group.scss?lit&inline'; * @slot error - Slot used to render a `sbb-form-error` inside the `sbb-checkbox-group`. */ @customElement('sbb-checkbox-group') -export class SbbCheckboxGroupElement extends LitElement { +export class SbbCheckboxGroupElement extends SbbDisabledMixin(LitElement) { public static override styles: CSSResultGroup = style; - /** Whether the checkbox group is disabled. */ - @property({ reflect: true, type: Boolean }) public disabled = false; - /** Whether the checkbox group is required. */ @property({ reflect: true, type: Boolean }) public required = false; diff --git a/src/components/checkbox/checkbox-group/readme.md b/src/components/checkbox/checkbox-group/readme.md index 33bf0e292a..af5fe0ed24 100644 --- a/src/components/checkbox/checkbox-group/readme.md +++ b/src/components/checkbox/checkbox-group/readme.md @@ -75,12 +75,12 @@ Two values are available, `s` and `m`, which is the default | Name | Attribute | Privacy | Type | Default | Description | | ---------------- | ----------------- | ------- | -------------------------------- | -------------- | ------------------------------------------------------------------------------ | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the checkbox group is disabled. | | `required` | `required` | public | `boolean` | `false` | Whether the checkbox group is required. | | `size` | `size` | public | `SbbCheckboxSize` | `'m'` | Size variant, either m or s. | | `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| undefined` | | Overrides the behaviour of `orientation` property. | | `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Indicates the orientation of the checkboxes inside the ``. | | `checkboxes` | - | public | `SbbCheckboxElement[]` | | List of contained checkbox elements. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | ## Slots diff --git a/src/components/checkbox/checkbox/__snapshots__/checkbox.spec.snap.js b/src/components/checkbox/checkbox/__snapshots__/checkbox.spec.snap.js index 944ed5d779..312b1ebaef 100644 --- a/src/components/checkbox/checkbox/__snapshots__/checkbox.spec.snap.js +++ b/src/components/checkbox/checkbox/__snapshots__/checkbox.spec.snap.js @@ -172,7 +172,7 @@ snapshots["sbb-checkbox state unchecked disabled"] = `; /* end snapshot sbb-checkbox state unchecked disabled */ -snapshots["sbb-checkbox Unchecked - A11y tree Chrome"] = +snapshots["sbb-checkbox Unchecked - A11y tree Chrome"] = `

{ "role": "WebArea", @@ -189,7 +189,7 @@ snapshots["sbb-checkbox Unchecked - A11y tree Chrome"] = `; /* end snapshot sbb-checkbox Unchecked - A11y tree Chrome */ -snapshots["sbb-checkbox Checked - A11y tree Chrome"] = +snapshots["sbb-checkbox Checked - A11y tree Chrome"] = `

{ "role": "WebArea", diff --git a/src/components/checkbox/checkbox/checkbox.ts b/src/components/checkbox/checkbox/checkbox.ts index 24c11e84d5..0fe955871e 100644 --- a/src/components/checkbox/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox/checkbox.ts @@ -6,6 +6,7 @@ import { ref } from 'lit/directives/ref.js'; import { LanguageController, NamedSlotStateController, + SbbIconNameMixin, UpdateScheduler, } from '../../core/common-behaviors'; import { setAttributes } from '../../core/dom'; @@ -48,7 +49,7 @@ export type SbbCheckboxSize = 's' | 'm'; * @event {CustomEvent} didChange - Deprecated. used for React. Will probably be removed once React 19 is available. */ @customElement('sbb-checkbox') -export class SbbCheckboxElement extends UpdateScheduler(LitElement) { +export class SbbCheckboxElement extends UpdateScheduler(SbbIconNameMixin(LitElement)) { public static override styles: CSSResultGroup = style; public static readonly events = { didChange: 'didChange', @@ -88,12 +89,6 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { /** Whether the checkbox is indeterminate. */ @property({ reflect: true, type: Boolean }) public indeterminate = false; - /** - * The icon name we want to use, choose from the small icon variants from the ui-icons category - * from https://icons.app.sbb.ch (optional). - */ - @property({ attribute: 'icon-name', reflect: true }) public iconName?: string; - /** The label position relative to the labelIcon. Defaults to end */ @property({ attribute: 'icon-placement', reflect: true }) public iconPlacement: SbbIconPlacement = 'end'; @@ -306,11 +301,7 @@ export class SbbCheckboxElement extends UpdateScheduler(LitElement) { - - - ${this.iconName ? html`` : nothing} - - + ${this.renderIconSlot()} ${this._selectionPanelElement ? html`` : nothing} diff --git a/src/components/checkbox/checkbox/readme.md b/src/components/checkbox/checkbox/readme.md index d4efe9c322..fdaa26cc65 100644 --- a/src/components/checkbox/checkbox/readme.md +++ b/src/components/checkbox/checkbox/readme.md @@ -72,17 +72,17 @@ If you don't want the label to appear next to the checkbox, you can use `aria-la ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| --------------- | ---------------- | ------- | --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| `value` | `value` | public | `string \| undefined` | | Value of checkbox. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the checkbox is disabled. | -| `required` | `required` | public | `boolean` | `false` | Whether the checkbox is required. | -| `group` | - | public | `SbbCheckboxGroupElement \| null` | `null` | Reference to the connected checkbox group. | -| `indeterminate` | `indeterminate` | public | `boolean` | `false` | Whether the checkbox is indeterminate. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from https://icons.app.sbb.ch (optional). | -| `iconPlacement` | `icon-placement` | public | `SbbIconPlacement` | `'end'` | The label position relative to the labelIcon. Defaults to end | -| `checked` | `checked` | public | `boolean` | `false` | Whether the checkbox is checked. | -| `size` | `size` | public | `SbbCheckboxSize` | `'m'` | Label size variant, either m or s. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------- | ---------------- | ------- | --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `value` | `value` | public | `string \| undefined` | | Value of checkbox. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the checkbox is disabled. | +| `required` | `required` | public | `boolean` | `false` | Whether the checkbox is required. | +| `group` | - | public | `SbbCheckboxGroupElement \| null` | `null` | Reference to the connected checkbox group. | +| `indeterminate` | `indeterminate` | public | `boolean` | `false` | Whether the checkbox is indeterminate. | +| `iconPlacement` | `icon-placement` | public | `SbbIconPlacement` | `'end'` | The label position relative to the labelIcon. Defaults to end | +| `checked` | `checked` | public | `boolean` | `false` | Whether the checkbox is checked. | +| `size` | `size` | public | `SbbCheckboxSize` | `'m'` | Label size variant, either m or s. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | ## Events diff --git a/src/components/core/common-behaviors/action-base-element.e2e.ts b/src/components/core/common-behaviors/action-base-element.e2e.ts new file mode 100644 index 0000000000..76f2c82cf0 --- /dev/null +++ b/src/components/core/common-behaviors/action-base-element.e2e.ts @@ -0,0 +1,32 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html, type TemplateResult } from 'lit'; + +import { SbbActionBaseElement } from './action-base-element'; + +class GenericAction extends SbbActionBaseElement { + protected override renderTemplate(): TemplateResult { + return html`Action`; + } +} +customElements.define('generic-action', GenericAction); + +describe('SbbActionBaseElement', () => { + describe('template', () => { + let element: GenericAction; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('renders', async () => { + assert.instanceOf(element, GenericAction); + }); + + it('check host attributes and content', () => { + expect(element.getAttribute('dir')).to.be.equal('ltr'); + expect(element.shadowRoot!.firstElementChild!.classList.contains('generic-action')).to.be + .true; + expect(element.shadowRoot!.textContent!.trim()).to.be.equal('Action'); + }); + }); +}); diff --git a/src/components/core/common-behaviors/action-base-element.ts b/src/components/core/common-behaviors/action-base-element.ts new file mode 100644 index 0000000000..f31010aea2 --- /dev/null +++ b/src/components/core/common-behaviors/action-base-element.ts @@ -0,0 +1,20 @@ +import { html, LitElement, type TemplateResult } from 'lit'; + +import { getDocumentWritingMode } from '../dom'; + +export abstract class SbbActionBaseElement extends LitElement { + /** Override this method to render the component template. */ + protected renderTemplate(): TemplateResult { + throw new Error('Implementation needed!'); + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + this.setAttribute('dir', getDocumentWritingMode()); + return super.createRenderRoot(); + } + + /** Default render method for button-like components. */ + protected override render(): TemplateResult { + return html` ${this.renderTemplate()} `; + } +} diff --git a/src/components/core/common-behaviors/action-dispatch-click.ts b/src/components/core/common-behaviors/action-dispatch-click.ts new file mode 100644 index 0000000000..5dba27f8dd --- /dev/null +++ b/src/components/core/common-behaviors/action-dispatch-click.ts @@ -0,0 +1,32 @@ +/** + * Dispatch a `click` event. + * @param event The origin event. + */ +export const dispatchClickEvent = (event: KeyboardEvent): void => { + const { altKey, ctrlKey, metaKey, shiftKey } = event; + (event.target as Element).dispatchEvent( + new PointerEvent('click', { + bubbles: true, + cancelable: true, + composed: true, + pointerId: -1, + pointerType: '', + altKey, + ctrlKey, + metaKey, + shiftKey, + }), + ); +}; + +/** + * Dispatches a 'click' PointerEvent if the original keyboard event is a 'Enter' press. + * As verified with the native button, when 'Enter' is pressed, a 'click' event is dispatched + * after the 'keypress' event. + * @param event The origin event. + */ +export const dispatchClickEventWhenEnterKeypress = (event: KeyboardEvent): void => { + if (event.key === 'Enter' || event.key === '\n') { + dispatchClickEvent(event); + } +}; diff --git a/src/components/core/common-behaviors/button-base-element.e2e.ts b/src/components/core/common-behaviors/button-base-element.e2e.ts new file mode 100644 index 0000000000..29ca77659c --- /dev/null +++ b/src/components/core/common-behaviors/button-base-element.e2e.ts @@ -0,0 +1,91 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html, type TemplateResult } from 'lit'; + +import { EventSpy, waitForLitRender } from '../testing'; + +import { SbbButtonBaseElement } from './button-base-element'; + +class GenericButton extends SbbButtonBaseElement { + protected override renderTemplate(): TemplateResult { + return html`Button`; + } +} +customElements.define('generic-button', GenericButton); + +describe('SbbButtonBaseElement', () => { + describe('template', () => { + let element: GenericButton; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('renders', async () => { + assert.instanceOf(element, GenericButton); + }); + + it('check host attributes and content', () => { + expect(element.getAttribute('dir')).to.be.equal('ltr'); + expect(element.getAttribute('role')).to.be.equal('button'); + expect(element.getAttribute('tabindex')).to.be.equal('0'); + expect(element.shadowRoot!.firstElementChild!.classList.contains('generic-button')).to.be + .true; + expect(element.shadowRoot!.textContent!.trim()).to.be.equal('Button'); + }); + }); + + describe('events', () => { + let element: GenericButton; + + beforeEach(async () => { + element = await fixture(html` `); + }); + + it('no click dispatch if aria-disabled', async () => { + element.setAttribute('aria-disabled', 'true'); + await waitForLitRender(element); + const clickSpy = new EventSpy('click'); + element.click(); + await waitForLitRender(element); + expect(clickSpy.count).not.to.be.greaterThan(0); + }); + + it('dispatch click if type button', async () => { + element.type = 'button'; + await waitForLitRender(element); + const clickSpy = new EventSpy('click'); + element.click(); + await waitForLitRender(element); + expect(clickSpy.count).to.be.equal(1); + expect(await element.getAttribute('data-active')).to.be.equal(null); + }); + + it('space keydown and keyup', async () => { + element.focus(); + await waitForLitRender(element); + const clickSpy = new EventSpy('click'); + expect(await element.getAttribute('data-active')).to.be.equal(null); + + await sendKeys({ down: 'Space' }); + await waitForLitRender(element); + expect(await element.getAttribute('data-active')).to.be.equal(''); + + await sendKeys({ up: 'Space' }); + await waitForLitRender(element); + expect(await element.getAttribute('data-active')).to.be.equal(null); + await waitForLitRender(element); + expect(clickSpy.count).to.be.equal(1); + }); + + it('enter keydown', async () => { + element.focus(); + await waitForLitRender(element); + const clickSpy = new EventSpy('click'); + + await sendKeys({ down: 'Enter' }); + await waitForLitRender(element); + expect(clickSpy.count).to.be.equal(1); + }); + }); +}); diff --git a/src/components/core/common-behaviors/button-base-element.ts b/src/components/core/common-behaviors/button-base-element.ts new file mode 100644 index 0000000000..24aadebbb6 --- /dev/null +++ b/src/components/core/common-behaviors/button-base-element.ts @@ -0,0 +1,110 @@ +import { property } from 'lit/decorators.js'; + +import { SbbActionBaseElement } from './action-base-element'; +import { dispatchClickEvent, dispatchClickEventWhenEnterKeypress } from './action-dispatch-click'; + +/** Enumeration for type attribute in