From c6013278a45885ebdfaceebf3cbc0a60176665d8 Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Mon, 18 Sep 2023 18:21:57 +0200 Subject: [PATCH] refactor: migrate library to Lit --- .eslintignore | 3 +- .eslintrc.json | 46 +- .../continuous-integration-secure.yml | 8 +- .github/workflows/continuous-integration.yml | 39 +- .github/workflows/release.yml | 53 +- .gitignore | 6 - .husky/commit-msg | 2 +- .nvmrc | 2 +- .prettierignore | 11 +- .storybook/main.ts | 40 +- .storybook/preview-head.html | 22 + .storybook/preview.js | 80 - .storybook/preview.ts | 61 + .stylelintrc.json | 2 +- CODING_STANDARDS.md | 64 +- DEVELOPER.md | 9 +- Dockerfile | 2 +- README.md | 2 +- TODOS.md | 19 +- _index.scss | 2 - ci/bundleStories.js | 59 - ci/chromatic-stories-generator.ts | 111 +- ...ook-files.js => clean-storybook-files.cjs} | 4 +- ci/docs_generate.ts | 140 + ci/inline-events.mts | 121 + ci/lit-migration.mts | 152 +- commitlint.config.js | 2 +- config/custom-elements-manifest.config.js | 20 + .../boilerplate/component.custom.d.ts | 3 - .../boilerplate/component.e2e.ts | 13 - .../boilerplate/component.events.ts | 3 - .../boilerplate/component.spec.ts | 19 - .../boilerplate/component.stories.tsx | 40 - .../boilerplate/component.tsx | 37 - convenience/generate-component/index.js | 117 - docs/CICD.md | 5 +- docs/DEVELOP.md | 152 +- jestAssetsTransformer.js | 7 - package.json | 161 +- react-library/package.json | 27 - react-library/src/components/index.ts | 1 - .../src/components/stencil-generated/index.ts | 103 - .../react-component-lib/createComponent.tsx | 106 - .../createOverlayComponent.tsx | 142 - .../react-component-lib/index.ts | 2 - .../react-component-lib/interfaces.ts | 34 - .../react-component-lib/utils/attachProps.ts | 125 - .../react-component-lib/utils/case.ts | 7 - .../react-component-lib/utils/dev.ts | 14 - .../react-component-lib/utils/index.tsx | 50 - react-library/src/index.ts | 1 - react-library/tsconfig.json | 26 - renovate.json | 15 +- src/components.d.ts | 5163 ------------ src/components/_index.scss | 2 + src/components/accordion/accordion.e2e.ts | 206 + .../accordion.scss} | 2 +- src/components/accordion/accordion.spec.ts | 70 + .../accordion.stories.tsx} | 27 +- src/components/accordion/accordion.ts | 120 + src/components/accordion/index.ts | 1 + src/components/accordion/readme.md | 56 + .../action-group/action-group.e2e.ts | 62 + .../action-group.scss} | 2 +- .../action-group/action-group.spec.ts | 111 + .../action-group.stories.tsx} | 12 +- src/components/action-group/action-group.ts | 85 + src/components/action-group/index.ts | 1 + src/components/action-group/readme.md | 135 + .../alert/alert-group/alert-group.e2e.ts | 104 + .../alert-group/alert-group.scss} | 2 +- .../alert/alert-group/alert-group.spec.ts | 78 + .../alert-group/alert-group.stories.tsx} | 15 +- .../alert/alert-group/alert-group.ts | 117 + src/components/alert/alert-group/index.ts | 1 + src/components/alert/alert-group/readme.md | 64 + src/components/alert/alert/alert.e2e.ts | 27 + .../sbb-alert.scss => alert/alert/alert.scss} | 2 +- src/components/alert/alert/alert.spec.ts | 120 + .../alert/alert.stories.tsx} | 17 +- src/components/alert/alert/alert.ts | 260 + src/components/alert/alert/index.ts | 1 + src/components/alert/alert/readme.md | 112 + src/components/alert/index.ts | 2 + .../autocomplete/autocomplete.e2e.ts | 179 + .../autocomplete.scss} | 14 +- .../autocomplete/autocomplete.spec.ts | 112 + .../autocomplete.stories.tsx} | 48 +- .../autocomplete.ts} | 314 +- src/components/autocomplete/index.ts | 1 + .../readme.md | 85 +- .../breadcrumb-group/breadcrumb-group.e2e.ts | 170 + .../breadcrumb-group/breadcrumb-group.scss} | 2 +- .../breadcrumb-group/breadcrumb-group.spec.ts | 46 + .../breadcrumb-group.stories.tsx} | 11 +- .../breadcrumb-group/breadcrumb-group.ts} | 179 +- .../breadcrumb/breadcrumb-group/index.ts | 1 + .../breadcrumb-group}/readme.md | 43 +- .../breadcrumb/breadcrumb/breadcrumb.e2e.ts | 33 + .../breadcrumb/breadcrumb.scss} | 2 +- .../breadcrumb/breadcrumb/breadcrumb.spec.ts | 96 + .../breadcrumb/breadcrumb.stories.tsx} | 12 +- .../breadcrumb/breadcrumb/breadcrumb.ts | 143 + src/components/breadcrumb/breadcrumb/index.ts | 1 + .../breadcrumb/breadcrumb/readme.md | 55 + src/components/breadcrumb/index.ts | 2 + src/components/button/button.e2e.ts | 103 + .../sbb-button.scss => button/button.scss} | 28 +- src/components/button/button.spec.ts | 179 + .../button.stories.tsx} | 11 +- src/components/button/button.ts | 181 + src/components/button/index.ts | 1 + src/components/button/readme.md | 103 + src/components/calendar/calendar.e2e.ts | 475 ++ .../calendar.scss} | 4 +- .../calendar.spec.ts} | 87 +- .../calendar.stories.tsx} | 13 +- .../sbb-calendar.tsx => calendar/calendar.ts} | 723 +- src/components/calendar/index.ts | 1 + .../{sbb-calendar => calendar}/readme.md | 73 +- .../card/card-action/card-action.e2e.ts | 230 + .../card-action/card-action.scss} | 2 +- .../card-action/card-action.stories.tsx} | 8 +- .../card/card-action/card-action.ts | 178 + src/components/card/card-action/index.ts | 1 + src/components/card/card-action/readme.md | 45 + .../card/card-badge/card-badge.e2e.ts | 13 + .../card-badge/card-badge.scss} | 2 +- .../card/card-badge/card-badge.spec.ts | 28 + .../card-badge/card-badge.stories.tsx} | 10 +- src/components/card/card-badge/card-badge.ts | 58 + src/components/card/card-badge/index.ts | 1 + src/components/card/card-badge/readme.md | 36 + src/components/card/card/card.e2e.ts | 73 + .../sbb-card.scss => card/card/card.scss} | 2 +- .../card/card.spec.ts} | 18 +- .../card/card.stories.tsx} | 15 +- src/components/card/card/card.ts | 58 + src/components/card/card/index.ts | 1 + .../{sbb-card => card/card}/readme.md | 52 +- src/components/card/index.ts | 3 + .../checkbox-group/checkbox-group.e2e.ts | 94 + .../checkbox-group/checkbox-group.scss} | 2 +- .../checkbox-group/checkbox-group.spec.ts | 31 + .../checkbox-group.stories.tsx} | 12 +- .../checkbox/checkbox-group/checkbox-group.ts | 155 + .../checkbox/checkbox-group/index.ts | 1 + .../checkbox/checkbox-group/readme.md | 89 + .../checkbox/checkbox/checkbox.e2e.ts | 85 + .../checkbox/checkbox.scss} | 4 +- .../checkbox/checkbox/checkbox.spec.ts | 148 + .../checkbox/checkbox.stories.tsx} | 10 +- src/components/checkbox/checkbox/checkbox.ts | 341 + src/components/checkbox/checkbox/index.ts | 1 + src/components/checkbox/checkbox/readme.md | 99 + src/components/checkbox/index.ts | 2 + src/components/chip/chip.e2e.ts | 11 + .../sbb-chip.scss => chip/chip.scss} | 2 +- src/components/chip/chip.spec.ts | 19 + .../chip.stories.tsx} | 11 +- src/components/chip/chip.ts | 39 + src/components/chip/index.ts | 1 + src/components/chip/readme.md | 33 + .../assets/sbb_clock_face.svg | 0 .../assets/sbb_clock_hours.svg | 0 .../assets/sbb_clock_minutes.svg | 0 .../assets/sbb_clock_seconds.svg | 0 .../sbb-clock.scss => clock/clock.scss} | 2 +- src/components/clock/clock.spec.ts | 64 + .../clock.stories.tsx} | 15 +- .../sbb-clock.tsx => clock/clock.ts} | 145 +- src/components/clock/index.ts | 1 + src/components/{sbb-clock => clock}/readme.md | 7 +- .../core}/a11y/arrow-navigation.spec.ts | 88 +- .../core}/a11y/arrow-navigation.ts | 0 .../core}/a11y/assign-id.ts | 0 .../core}/a11y/fake-event-detection.ts | 0 src/{global => components/core}/a11y/focus.ts | 2 +- src/{global => components/core}/a11y/index.ts | 0 .../core}/a11y/input-modality-detector.ts | 9 +- .../core/a11y/interactivity-checker.spec.ts | 71 + .../core}/a11y/interactivity-checker.ts | 0 .../core/config}/config.ts | 2 +- src/components/core/config/index.ts | 1 + .../core}/datetime/date-adapter.ts | 0 .../core}/datetime/date-helper.spec.ts | 23 +- .../core}/datetime/date-helper.ts | 3 +- .../core}/datetime/index.ts | 0 .../datetime/native-date-adapter.spec.ts | 222 +- .../core}/datetime/native-date-adapter.ts | 6 +- .../core}/dom/breakpoint.ts | 0 .../core}/dom/dataset.ts | 0 .../core}/dom/find-referenced-element.ts | 0 .../core}/dom/get-document-writing-mode.ts | 0 .../core}/dom/host-context.spec.ts | 6 +- .../core}/dom/host-context.ts | 0 src/{global => components/core}/dom/index.ts | 1 - .../core}/dom/input-element.ts | 0 .../core}/dom/is-valid-attribute.ts | 11 +- .../core}/dom/platform.ts | 0 src/{global => components/core}/dom/scroll.ts | 0 .../core}/eventing/action-element-handlers.ts | 1 + .../eventing/composed-path-has-attribute.ts | 0 .../eventing/connected-abort-controller.ts | 0 .../core}/eventing/event-emitter.ts | 12 +- .../core}/eventing/event-target.ts | 0 .../core}/eventing/form-element-handlers.ts | 0 .../core}/eventing/forward-event.ts | 0 .../core}/eventing/handler-repository.ts | 0 .../core}/eventing/index.ts | 0 .../core}/eventing/is-event-prevented.ts | 0 .../eventing/language-change-handler.e2e.ts | 62 + .../core}/eventing/language-change-handler.ts | 1 + .../eventing/named-slot-change-handler.ts | 1 + .../core}/eventing/throttle.ts | 0 src/{global => components/core/i18n}/i18n.ts | 0 src/components/core/i18n/index.ts | 1 + src/{global => components/core}/images.ts | 0 .../core}/interfaces/index.ts | 1 + .../interfaces/link-button-properties.spec.ts | 52 +- .../interfaces/link-button-properties.ts | 6 +- src/components/core/interfaces/types.ts | 30 + .../core}/interfaces/validation-change.ts | 0 .../core}/observers/index.ts | 0 .../core}/observers/intersection-observer.ts | 0 .../core}/observers/mutation-observer.ts | 0 .../core}/observers/resize-observer.ts | 0 .../core}/overlay/index.ts | 0 .../core}/overlay/overlay-option-panel.ts | 0 .../overlay/overlay-trigger-attributes.ts | 0 .../core/overlay/overlay.ts} | 13 +- .../core}/overlay/position.spec.ts | 44 +- .../core}/overlay/position.ts | 0 .../core}/styles/_index.scss | 0 .../styles/core/component-utilities.scss | 0 .../core}/styles/core/font-face.scss | 0 .../core}/styles/core/functions.scss | 0 .../core}/styles/core/mediaqueries.scss | 0 .../core}/styles/core/variables.scss | 0 src/components/core/styles/global.scss | 9 + .../core}/styles/mixins/a11y.scss | 0 .../core}/styles/mixins/badge.scss | 0 .../core}/styles/mixins/buttons.scss | 4 +- .../core}/styles/mixins/card.scss | 0 .../core}/styles/mixins/chip.scss | 0 .../core}/styles/mixins/dev_and_debug.scss | 0 .../core}/styles/mixins/helpers.scss | 0 .../core}/styles/mixins/inputs.scss | 0 .../core}/styles/mixins/layout.scss | 0 .../core}/styles/mixins/link.scss | 0 .../core}/styles/mixins/lists.scss | 0 .../core}/styles/mixins/overlay.scss | 0 .../core}/styles/mixins/panel.scss | 0 .../styles/mixins/pearl-chain-bullet.scss | 0 .../core}/styles/mixins/scrollbar.scss | 0 .../core}/styles/mixins/shadows.scss | 0 .../core}/styles/mixins/typo.scss | 0 .../core}/styles/normalize.scss | 0 .../core}/styles/typography.scss | 6 +- .../core}/testing/event-spy.ts | 16 +- src/components/core/testing/index.ts | 4 + src/components/core/testing/scroll.ts | 11 + .../core/testing/test-setup.ts} | 9 +- .../core/testing/wait-for-condition.ts | 24 + .../core/testing/wait-for-render.ts | 26 + .../core}/timetable/access-leg-helper.spec.ts | 16 +- .../core/timetable/access-leg-helper.ts} | 113 +- .../core}/timetable/icons.json | 0 .../core}/timetable/index.ts | 0 .../core/timetable/timetable-helper.spec.ts | 25 + .../timetable/timetable-helper.ts} | 14 +- .../core}/timetable/timetable-properties.ts | 2 +- .../datepicker-next-day.e2e.ts | 115 + .../datepicker-next-day.scss} | 2 +- .../datepicker-next-day.spec.ts | 53 + .../datepicker-next-day.stories.tsx} | 16 +- .../datepicker-next-day.ts} | 186 +- .../datepicker/datepicker-next-day/index.ts | 1 + .../datepicker-next-day}/readme.md | 36 +- .../datepicker-previous-day.e2e.ts | 108 + .../datepicker-previous-day.scss} | 2 +- .../datepicker-previous-day.spec.ts | 52 + .../datepicker-previous-day.stories.tsx} | 14 +- .../datepicker-previous-day.ts} | 182 +- .../datepicker-previous-day/index.ts | 1 + .../datepicker-previous-day}/readme.md | 38 +- .../datepicker-toggle.e2e.ts | 107 + .../datepicker-toggle/datepicker-toggle.scss} | 2 +- .../datepicker-toggle.spec.ts | 154 + .../datepicker-toggle.stories.tsx} | 32 +- .../datepicker-toggle/datepicker-toggle.ts | 246 + .../datepicker/datepicker-toggle/index.ts | 1 + .../datepicker/datepicker-toggle/readme.md | 49 + .../datepicker/datepicker/datepicker.e2e.ts | 347 + .../datepicker/datepicker.scss} | 2 +- .../datepicker/datepicker.spec.ts} | 148 +- .../datepicker/datepicker.stories.tsx} | 44 +- .../datepicker/datepicker/datepicker.ts | 495 ++ src/components/datepicker/datepicker/index.ts | 1 + .../datepicker}/readme.md | 107 +- src/components/datepicker/index.ts | 4 + src/components/dialog/dialog.e2e.ts | 339 + .../sbb-dialog.scss => dialog/dialog.scss} | 14 +- src/components/dialog/dialog.spec.ts | 42 + .../dialog.stories.tsx} | 41 +- .../sbb-dialog.tsx => dialog/dialog.ts} | 344 +- src/components/dialog/index.ts | 1 + src/components/dialog/readme.md | 119 + src/components/divider/divider.e2e.ts | 11 + .../sbb-divider.scss => divider/divider.scss} | 4 +- src/components/divider/divider.spec.ts | 34 + .../divider.stories.tsx} | 12 +- src/components/divider/divider.ts | 35 + src/components/divider/index.ts | 1 + src/components/divider/readme.md | 22 + .../expansion-panel-content.e2e.ts | 15 + .../expansion-panel-content.scss} | 2 +- .../expansion-panel-content.spec.ts | 47 + .../expansion-panel-content.stories.tsx} | 8 +- .../expansion-panel-content.ts | 34 + .../expansion-panel-content/index.ts | 1 + .../expansion-panel-content}/readme.md | 22 +- .../expansion-panel-header.e2e.ts | 33 + .../expansion-panel-header.scss} | 4 +- .../expansion-panel-header.spec.ts | 129 + .../expansion-panel-header.stories.tsx} | 8 +- .../expansion-panel-header.ts | 128 + .../expansion-panel-header/index.ts | 1 + .../expansion-panel-header/readme.md | 54 + .../expansion-panel/expansion-panel.e2e.ts | 106 + .../expansion-panel/expansion-panel.scss} | 16 +- .../expansion-panel/expansion-panel.spec.ts | 69 + .../expansion-panel.stories.tsx} | 29 +- .../expansion-panel/expansion-panel.ts | 266 + .../expansion-panel/expansion-panel/index.ts | 1 + .../expansion-panel/expansion-panel/readme.md | 96 + src/components/expansion-panel/index.ts | 3 + .../file-selector/file-selector.e2e.ts | 131 + .../file-selector.scss} | 4 +- .../file-selector/file-selector.spec.ts | 60 + .../file-selector.stories.tsx} | 15 +- .../file-selector.ts} | 226 +- src/components/file-selector/index.ts | 1 + src/components/file-selector/readme.md | 112 + src/components/footer/footer.e2e.ts | 13 + .../sbb-footer.scss => footer/footer.scss} | 9 +- src/components/footer/footer.spec.ts | 30 + .../footer.stories.tsx} | 18 +- src/components/footer/footer.ts | 65 + src/components/footer/index.ts | 1 + .../{sbb-footer => footer}/readme.md | 32 +- src/components/form-error/form-error.e2e.ts | 15 + .../form-error.scss} | 4 +- src/components/form-error/form-error.spec.ts | 37 + .../form-error.stories.tsx} | 16 +- src/components/form-error/form-error.ts | 65 + src/components/form-error/index.ts | 1 + src/components/form-error/readme.md | 33 + .../form-field-clear/form-field-clear.e2e.ts | 56 + .../form-field-clear/form-field-clear.scss} | 2 +- .../form-field-clear/form-field-clear.spec.ts | 56 + .../form-field-clear.stories.tsx} | 22 +- .../form-field-clear/form-field-clear.ts | 89 + .../form-field/form-field-clear/index.ts | 1 + .../form-field/form-field-clear/readme.md | 19 + .../form-field/form-field/form-field.e2e.ts | 328 + .../form-field/form-field.scss} | 32 +- .../form-field/form-field/form-field.spec.ts | 280 + .../form-field/form-field.stories.tsx} | 24 +- .../form-field/form-field.ts} | 294 +- src/components/form-field/form-field/index.ts | 1 + .../form-field/form-field/readme.md | 147 + src/components/form-field/index.ts | 2 + .../header/header-action/header-action.e2e.ts | 70 + .../header-action/header-action.scss} | 2 +- .../header-action/header-action.spec.ts} | 87 +- .../header-action/header-action.stories.tsx} | 10 +- .../header/header-action/header-action.ts | 148 + src/components/header/header-action/index.ts | 1 + src/components/header/header-action/readme.md | 56 + src/components/header/header/header.e2e.ts | 123 + .../header/header.scss} | 6 +- .../header/header.spec.ts} | 73 +- .../header/header.stories.tsx} | 27 +- .../header/header.ts} | 120 +- src/components/header/header/index.ts | 1 + .../{sbb-header => header/header}/readme.md | 66 +- src/components/header/index.ts | 2 + .../icon-request.ts} | 5 +- .../icon-validate.spec.ts} | 18 +- .../icon-validate.ts} | 0 src/components/icon/icon.e2e.ts | 14 + .../sbb-icon.scss => icon/icon.scss} | 2 +- src/components/icon/icon.spec.ts | 125 + .../icon.stories.tsx} | 10 +- src/components/icon/icon.ts | 135 + src/components/icon/index.ts | 1 + src/components/icon/readme.md | 39 + src/components/image/image.e2e.ts | 43 + .../image.helper.ts} | 51 +- .../sbb-image.scss => image/image.scss} | 4 +- .../image.stories.tsx} | 36 +- .../sbb-image.tsx => image/image.ts} | 206 +- src/components/image/index.ts | 1 + src/components/image/readme.md | 29 + src/components/journey-header/index.ts | 1 + .../journey-header/journey-header.e2e.ts | 16 + .../journey-header.scss} | 2 +- .../journey-header/journey-header.spec.ts | 75 + .../journey-header.stories.tsx} | 12 +- .../journey-header/journey-header.ts | 97 + src/components/journey-header/readme.md | 56 + src/components/journey-summary/index.ts | 1 + .../journey-summary/journey-summary.e2e.ts | 11 + .../journey-summary.scss} | 5 +- .../journey-summary/journey-summary.spec.ts | 180 + .../journey-summary.stories.tsx} | 18 +- .../journey-summary/journey-summary.ts | 183 + src/components/journey-summary/readme.md | 35 + src/components/link-list/index.ts | 1 + src/components/link-list/link-list.e2e.ts | 66 + .../link-list.scss} | 2 +- .../link-list.spec.ts} | 189 +- .../link-list.stories.tsx} | 15 +- src/components/link-list/link-list.ts | 152 + src/components/link-list/readme.md | 81 + src/components/link/index.ts | 1 + src/components/link/link.e2e.ts | 103 + .../sbb-link.scss => link/link.scss} | 28 +- src/components/link/link.spec.ts | 168 + .../link.stories.tsx} | 17 +- src/components/link/link.ts | 166 + src/components/link/readme.md | 85 + src/components/loading-indicator/index.ts | 1 + .../loading-indicator.e2e.ts | 13 + .../loading-indicator.scss} | 5 +- .../loading-indicator.spec.ts | 156 + .../loading-indicator.stories.tsx} | 19 +- .../loading-indicator/loading-indicator.ts | 57 + src/components/loading-indicator/readme.md | 57 + src/components/logo/index.ts | 1 + .../sbb-logo.scss => logo/logo.scss} | 4 +- .../logo.stories.tsx} | 12 +- .../{sbb-logo/sbb-logo.tsx => logo/logo.ts} | 49 +- src/components/logo/readme.md | 36 + src/components/map-container/index.ts | 1 + .../map-container/map-container.e2e.ts | 49 + .../map-container.scss} | 2 +- .../map-container/map-container.spec.ts | 66 + .../map-container.stories.tsx} | 14 +- src/components/map-container/map-container.ts | 130 + src/components/map-container/readme.md | 35 + src/components/menu/index.ts | 2 + src/components/menu/menu-action/index.ts | 1 + .../menu/menu-action/menu-action.e2e.ts | 90 + .../menu-action/menu-action.scss} | 8 +- .../menu/menu-action/menu-action.spec.ts | 84 + .../menu-action/menu-action.stories.tsx} | 13 +- .../menu/menu-action/menu-action.ts | 136 + src/components/menu/menu-action/readme.md | 61 + src/components/menu/menu/index.ts | 1 + src/components/menu/menu/menu.e2e.ts | 246 + .../sbb-menu.scss => menu/menu/menu.scss} | 6 +- .../menu/menu.spec.ts} | 113 +- .../menu/menu.stories.tsx} | 44 +- .../sbb-menu.tsx => menu/menu/menu.ts} | 275 +- .../{sbb-menu => menu/menu}/readme.md | 80 +- src/components/message/index.ts | 1 + src/components/message/message.e2e.ts | 13 + .../sbb-message.scss => message/message.scss} | 2 +- src/components/message/message.spec.ts | 81 + .../message.stories.tsx} | 16 +- src/components/message/message.ts | 48 + src/components/message/readme.md | 45 + src/components/navigation/index.ts | 5 + .../navigation/navigation-action/index.ts | 1 + .../navigation-action.e2e.ts | 77 + .../navigation-action/navigation-action.scss} | 4 +- .../navigation-action.spec.ts | 23 + .../navigation-action.stories.tsx} | 10 +- .../navigation-action/navigation-action.ts | 166 + .../navigation/navigation-action/readme.md | 48 + .../navigation/navigation-list/index.ts | 1 + .../navigation-list/navigation-list.e2e.ts | 34 + .../navigation-list/navigation-list.scss} | 2 +- .../navigation-list/navigation-list.spec.ts | 56 + .../navigation-list.stories.tsx} | 11 +- .../navigation-list/navigation-list.ts | 104 + .../navigation/navigation-list/readme.md | 26 + .../navigation/navigation-marker/index.ts | 1 + .../navigation-marker.e2e.ts | 57 + .../navigation-marker/navigation-marker.scss} | 2 +- .../navigation-marker.spec.ts | 19 + .../navigation-marker.stories.tsx} | 11 +- .../navigation-marker/navigation-marker.ts | 132 + .../navigation/navigation-marker/readme.md | 44 + .../navigation/navigation-section/index.ts | 1 + .../navigation-section.e2e.ts | 55 + .../navigation-section.scss} | 6 +- .../navigation-section.spec.ts | 30 + .../navigation-section.stories.tsx} | 25 +- .../navigation-section/navigation-section.ts} | 229 +- .../navigation/navigation-section/readme.md | 48 + src/components/navigation/navigation/index.ts | 1 + .../navigation/navigation/navigation.e2e.ts | 326 + .../navigation/navigation.scss} | 6 +- .../navigation/navigation/navigation.spec.ts | 66 + .../navigation/navigation.stories.tsx} | 36 +- .../navigation/navigation.ts} | 259 +- .../navigation/navigation/readme.md | 91 + src/components/notification/index.ts | 1 + .../notification/notification.e2e.ts | 75 + .../notification.scss} | 6 +- .../notification/notification.spec.ts | 146 + .../notification.stories.tsx} | 23 +- src/components/notification/notification.ts | 250 + src/components/notification/readme.md | 93 + src/components/option/index.ts | 2 + src/components/option/optgroup/index.ts | 1 + .../option/optgroup/optgroup.e2e.ts | 76 + .../optgroup/optgroup.scss} | 2 +- .../option/optgroup/optgroup.spec.ts | 78 + .../optgroup/optgroup.stories.tsx} | 18 +- src/components/option/optgroup/optgroup.ts | 127 + .../optgroup}/readme.md | 36 +- src/components/option/option/index.ts | 1 + src/components/option/option/option.e2e.ts | 79 + .../option/option.scss} | 16 +- src/components/option/option/option.spec.ts | 68 + .../option/option.stories.tsx} | 20 +- src/components/option/option/option.ts | 325 + src/components/option/option/readme.md | 88 + src/components/package.json | 19 + src/components/pearl-chain-time/index.ts | 1 + .../pearl-chain-time.scss} | 6 +- .../pearl-chain-time/pearl-chain-time.spec.ts | 237 + .../pearl-chain-time.stories.tsx} | 18 +- .../pearl-chain-time/pearl-chain-time.ts | 127 + src/components/pearl-chain-time/readme.md | 64 + .../pearl-chain-vertical-item/index.ts | 1 + .../pearl-chain-vertical-item.scss} | 4 +- .../pearl-chain-vertical-item.spec.ts | 243 + .../pearl-chain-vertical-item.stories.tsx} | 11 +- .../pearl-chain-vertical-item.ts | 87 + .../pearl-chain-vertical-item/readme.md | 49 + src/components/pearl-chain-vertical/index.ts | 1 + .../pearl-chain-vertical.e2e.ts | 13 + .../pearl-chain-vertical.scss} | 2 +- .../pearl-chain-vertical.spec.ts | 23 + .../pearl-chain-vertical.stories.tsx} | 14 +- .../pearl-chain-vertical.ts | 29 + .../readme.md | 23 +- src/components/pearl-chain/index.ts | 1 + src/components/pearl-chain/pearl-chain.e2e.ts | 13 + .../pearl-chain/pearl-chain.sample-data.ts | 107 + .../pearl-chain.scss} | 2 +- .../pearl-chain/pearl-chain.spec.ts | 167 + .../pearl-chain.stories.tsx} | 18 +- .../pearl-chain.ts} | 90 +- .../readme.md | 34 +- src/components/radio-button/index.ts | 2 + .../radio-button/radio-button-group/index.ts | 1 + .../radio-button-group.e2e.ts | 160 + .../radio-button-group.scss} | 2 +- .../radio-button-group.spec.ts | 23 + .../radio-button-group.stories.tsx} | 17 +- .../radio-button-group/radio-button-group.ts | 326 + .../radio-button/radio-button-group/readme.md | 95 + .../radio-button/radio-button/index.ts | 1 + .../radio-button/radio-button.e2e.ts | 67 + .../radio-button/radio-button.scss} | 6 +- .../radio-button/radio-button.spec.ts | 26 + .../radio-button/radio-button.stories.tsx} | 10 +- .../radio-button/radio-button/radio-button.ts | 294 + .../radio-button/radio-button/readme.md | 66 + src/components/sbb-accordion/readme.md | 65 - .../sbb-accordion/sbb-accordion.e2e.ts | 217 - .../sbb-accordion/sbb-accordion.spec.ts | 40 - .../sbb-accordion/sbb-accordion.tsx | 86 - src/components/sbb-action-group/readme.md | 139 - .../sbb-action-group.custom.d.ts | 5 - .../sbb-action-group/sbb-action-group.e2e.ts | 53 - .../sbb-action-group/sbb-action-group.spec.ts | 125 - .../sbb-action-group/sbb-action-group.tsx | 73 - src/components/sbb-alert-group/readme.md | 72 - .../sbb-alert-group.custom.d.ts | 3 - .../sbb-alert-group/sbb-alert-group.e2e.ts | 82 - .../sbb-alert-group/sbb-alert-group.events.ts | 8 - .../sbb-alert-group/sbb-alert-group.spec.ts | 62 - .../sbb-alert-group/sbb-alert-group.tsx | 107 - src/components/sbb-alert/readme.md | 147 - .../sbb-alert/sbb-alert.custom.d.ts | 3 - src/components/sbb-alert/sbb-alert.e2e.ts | 32 - src/components/sbb-alert/sbb-alert.events.ts | 9 - src/components/sbb-alert/sbb-alert.spec.ts | 89 - src/components/sbb-alert/sbb-alert.tsx | 253 - .../sbb-autocomplete/sbb-autocomplete.e2e.ts | 190 - .../sbb-autocomplete.events.ts | 10 - .../sbb-autocomplete/sbb-autocomplete.spec.ts | 109 - .../sbb-breadcrumb-group.e2e.ts | 176 - .../sbb-breadcrumb-group.spec.ts | 48 - src/components/sbb-breadcrumb/readme.md | 75 - .../sbb-breadcrumb/sbb-breadcrumb.e2e.ts | 34 - .../sbb-breadcrumb/sbb-breadcrumb.spec.ts | 100 - .../sbb-breadcrumb/sbb-breadcrumb.tsx | 114 - src/components/sbb-button/readme.md | 150 - .../sbb-button/sbb-button.custom.d.ts | 4 - src/components/sbb-button/sbb-button.e2e.ts | 97 - src/components/sbb-button/sbb-button.spec.ts | 184 - src/components/sbb-button/sbb-button.tsx | 157 - .../sbb-calendar/sbb-calendar.custom.d.ts | 19 - .../sbb-calendar/sbb-calendar.e2e.ts | 494 -- .../sbb-calendar/sbb-calendar.events.ts | 7 - src/components/sbb-card-action/readme.md | 64 - .../sbb-card-action/sbb-card-action.e2e.ts | 222 - .../sbb-card-action/sbb-card-action.tsx | 167 - src/components/sbb-card-badge/readme.md | 56 - .../sbb-card-badge/sbb-card-badge.custom.d.ts | 3 - .../sbb-card-badge/sbb-card-badge.e2e.ts | 13 - .../sbb-card-badge/sbb-card-badge.spec.ts | 26 - .../sbb-card-badge/sbb-card-badge.tsx | 52 - src/components/sbb-card/sbb-card.custom.d.ts | 4 - src/components/sbb-card/sbb-card.e2e.ts | 83 - src/components/sbb-card/sbb-card.tsx | 45 - src/components/sbb-checkbox-group/readme.md | 104 - .../sbb-checkbox-group.custom.ts | 5 - .../sbb-checkbox-group.e2e.ts | 73 - .../sbb-checkbox-group.spec.ts | 30 - .../sbb-checkbox-group/sbb-checkbox-group.tsx | 162 - src/components/sbb-checkbox/readme.md | 121 - .../sbb-checkbox/sbb-checkbox.custom.d.ts | 16 - .../sbb-checkbox/sbb-checkbox.e2e.ts | 88 - .../sbb-checkbox/sbb-checkbox.events.ts | 9 - .../sbb-checkbox/sbb-checkbox.spec.ts | 154 - src/components/sbb-checkbox/sbb-checkbox.tsx | 301 - src/components/sbb-chip/readme.md | 40 - src/components/sbb-chip/sbb-chip.custom.d.ts | 4 - src/components/sbb-chip/sbb-chip.e2e.ts | 13 - src/components/sbb-chip/sbb-chip.spec.ts | 24 - src/components/sbb-chip/sbb-chip.tsx | 30 - src/components/sbb-clock/sbb-clock.e2e.ts | 47 - src/components/sbb-clock/sbb-clock.spec.ts | 24 - .../sbb-datepicker-next-day.e2e.ts | 111 - .../sbb-datepicker-next-day.spec.ts | 57 - .../sbb-datepicker-previous-day.e2e.ts | 108 - .../sbb-datepicker-previous-day.spec.ts | 59 - .../sbb-datepicker-toggle/readme.md | 81 - .../sbb-datepicker-toggle.e2e.ts | 111 - .../sbb-datepicker-toggle.spec.ts | 101 - .../sbb-datepicker-toggle.tsx | 220 - .../sbb-datepicker/sbb-datepicker.e2e.ts | 338 - .../sbb-datepicker/sbb-datepicker.events.ts | 11 - .../sbb-datepicker/sbb-datepicker.helper.ts | 139 - .../sbb-datepicker/sbb-datepicker.spec.ts | 19 - .../sbb-datepicker/sbb-datepicker.tsx | 281 - src/components/sbb-dialog/readme.md | 164 - src/components/sbb-dialog/sbb-dialog.e2e.ts | 458 -- .../sbb-dialog/sbb-dialog.events.ts | 11 - src/components/sbb-dialog/sbb-dialog.spec.ts | 40 - src/components/sbb-divider/readme.md | 51 - .../sbb-divider/sbb-divider.custom.d.ts | 4 - src/components/sbb-divider/sbb-divider.e2e.ts | 13 - .../sbb-divider/sbb-divider.spec.ts | 49 - src/components/sbb-divider/sbb-divider.tsx | 24 - .../sbb-expansion-panel-content.e2e.ts | 13 - .../sbb-expansion-panel-content.spec.ts | 40 - .../sbb-expansion-panel-content.tsx | 21 - .../sbb-expansion-panel-header/readme.md | 77 - .../sbb-expansion-panel-header.e2e.ts | 32 - .../sbb-expansion-panel-header.events.ts | 7 - .../sbb-expansion-panel-header.spec.ts | 88 - .../sbb-expansion-panel-header.tsx | 114 - src/components/sbb-expansion-panel/readme.md | 111 - .../sbb-expansion-panel.custom.d.ts | 3 - .../sbb-expansion-panel.e2e.ts | 106 - .../sbb-expansion-panel.events.ts | 10 - .../sbb-expansion-panel.spec.ts | 66 - .../sbb-expansion-panel.tsx | 237 - src/components/sbb-file-selector/readme.md | 141 - .../sbb-file-selector.custom.d.ts | 4 - .../sbb-file-selector.e2e.ts | 154 - .../sbb-file-selector.events.ts | 7 - .../sbb-file-selector.spec.ts | 65 - .../sbb-footer/sbb-footer.custom.d.ts | 3 - src/components/sbb-footer/sbb-footer.e2e.ts | 13 - src/components/sbb-footer/sbb-footer.spec.ts | 24 - src/components/sbb-footer/sbb-footer.tsx | 47 - src/components/sbb-form-error/readme.md | 34 - .../sbb-form-error/sbb-form-error.e2e.ts | 13 - .../sbb-form-error/sbb-form-error.spec.ts | 39 - .../sbb-form-error/sbb-form-error.tsx | 52 - src/components/sbb-form-field-clear/readme.md | 39 - .../sbb-form-field-clear.e2e.ts | 72 - .../sbb-form-field-clear.spec.ts | 52 - .../sbb-form-field-clear.tsx | 80 - src/components/sbb-form-field/readme.md | 191 - .../sbb-form-field/sbb-form-field.custom.d.ts | 4 - .../sbb-form-field/sbb-form-field.e2e.ts | 361 - .../sbb-form-field/sbb-form-field.spec.ts | 270 - src/components/sbb-header-action/readme.md | 76 - .../sbb-header-action.custom.d.ts | 3 - .../sbb-header-action.e2e.ts | 62 - .../sbb-header-action/sbb-header-action.tsx | 130 - src/components/sbb-header/sbb-header.e2e.ts | 116 - src/components/sbb-icon/readme.md | 112 - src/components/sbb-icon/sbb-icon.e2e.ts | 13 - src/components/sbb-icon/sbb-icon.spec.ts | 126 - src/components/sbb-icon/sbb-icon.tsx | 125 - src/components/sbb-image/readme.md | 48 - .../sbb-image/sbb-image.custom.d.ts | 41 - src/components/sbb-image/sbb-image.e2e.ts | 39 - src/components/sbb-journey-header/readme.md | 82 - .../sbb-journey-header.custom.d.ts | 3 - .../sbb-journey-header.e2e.ts | 16 - .../sbb-journey-header.spec.ts | 69 - .../sbb-journey-header/sbb-journey-header.tsx | 85 - src/components/sbb-journey-summary/readme.md | 53 - .../sbb-journey-summary.custom.d.ts | 13 - .../sbb-journey-summary.e2e.ts | 11 - .../sbb-journey-summary.spec.ts | 179 - .../sbb-journey-summary.tsx | 163 - src/components/sbb-link-list/readme.md | 83 - .../sbb-link-list/sbb-link-list.custom.d.ts | 4 - .../sbb-link-list/sbb-link-list.e2e.ts | 53 - .../sbb-link-list/sbb-link-list.tsx | 133 - src/components/sbb-link/readme.md | 114 - src/components/sbb-link/sbb-link.custom.d.ts | 5 - src/components/sbb-link/sbb-link.e2e.ts | 94 - src/components/sbb-link/sbb-link.spec.ts | 179 - src/components/sbb-link/sbb-link.tsx | 138 - .../sbb-loading-indicator/readme.md | 59 - .../sbb-loading-indicator.custom.d.ts | 5 - .../sbb-loading-indicator.e2e.ts | 13 - .../sbb-loading-indicator.spec.ts | 136 - .../sbb-loading-indicator.tsx | 44 - src/components/sbb-logo/readme.md | 55 - src/components/sbb-logo/sbb-logo.custom.d.ts | 3 - src/components/sbb-map-container/readme.md | 56 - .../sbb-map-container.e2e.ts | 42 - .../sbb-map-container.spec.ts | 55 - .../sbb-map-container/sbb-map-container.tsx | 117 - src/components/sbb-menu-action/readme.md | 81 - .../sbb-menu-action/sbb-menu-action.e2e.ts | 84 - .../sbb-menu-action/sbb-menu-action.spec.ts | 70 - .../sbb-menu-action/sbb-menu-action.tsx | 113 - src/components/sbb-menu/sbb-menu.e2e.ts | 257 - src/components/sbb-menu/sbb-menu.events.ts | 10 - src/components/sbb-message/readme.md | 65 - src/components/sbb-message/sbb-message.e2e.ts | 13 - .../sbb-message/sbb-message.spec.ts | 74 - src/components/sbb-message/sbb-message.tsx | 36 - .../sbb-navigation-action/readme.md | 55 - .../sbb-navigation-action.e2e.ts | 77 - .../sbb-navigation-action.spec.ts | 21 - .../sbb-navigation-action.tsx | 159 - src/components/sbb-navigation-list/readme.md | 33 - .../sbb-navigation-list.e2e.ts | 34 - .../sbb-navigation-list.spec.ts | 53 - .../sbb-navigation-list.tsx | 88 - .../sbb-navigation-marker/readme.md | 65 - .../sbb-navigation-marker.e2e.ts | 52 - .../sbb-navigation-marker.spec.ts | 22 - .../sbb-navigation-marker.tsx | 127 - .../sbb-navigation-section/readme.md | 87 - .../sbb-navigation-section.e2e.ts | 52 - .../sbb-navigation-section.spec.ts | 28 - src/components/sbb-navigation/readme.md | 129 - .../sbb-navigation/sbb-navigation.e2e.ts | 309 - .../sbb-navigation/sbb-navigation.events.ts | 10 - .../sbb-navigation/sbb-navigation.spec.ts | 58 - src/components/sbb-notification/readme.md | 128 - .../sbb-notification.custom.d.ts | 3 - .../sbb-notification/sbb-notification.e2e.ts | 97 - .../sbb-notification.events.ts | 10 - .../sbb-notification/sbb-notification.spec.ts | 120 - .../sbb-notification/sbb-notification.tsx | 255 - .../sbb-optgroup/sbb-optgroup.e2e.ts | 61 - .../sbb-optgroup/sbb-optgroup.spec.ts | 68 - src/components/sbb-optgroup/sbb-optgroup.tsx | 131 - src/components/sbb-option/readme.md | 118 - .../sbb-option/sbb-option.custom.d.ts | 1 - src/components/sbb-option/sbb-option.e2e.ts | 73 - .../sbb-option/sbb-option.events.ts | 8 - src/components/sbb-option/sbb-option.spec.ts | 64 - src/components/sbb-option/sbb-option.tsx | 319 - src/components/sbb-pearl-chain-time/readme.md | 90 - .../sbb-pearl-chain-time.helper.spec.tsx | 23 - .../sbb-pearl-chain-time.spec.ts | 214 - .../sbb-pearl-chain-time.tsx | 116 - .../sbb-pearl-chain-vertical-item/readme.md | 54 - .../sbb-pearl-chain-vertical-item.custom.d.ts | 17 - .../sbb-pearl-chain-vertical-item.spec.ts | 276 - .../sbb-pearl-chain-vertical-item.tsx | 60 - .../sbb-pearl-chain-vertical.e2e.ts | 13 - .../sbb-pearl-chain-vertical.spec.ts | 24 - .../sbb-pearl-chain-vertical.tsx | 19 - .../sbb-pearl-chain/sbb-pearl-chain.e2e.ts | 13 - .../sbb-pearl-chain.sample-data.js | 199 - .../sbb-pearl-chain/sbb-pearl-chain.spec.ts | 196 - .../sbb-radio-button-group/readme.md | 105 - .../sbb-radio-button-group.custom.d.ts | 5 - .../sbb-radio-button-group.e2e.ts | 178 - .../sbb-radio-button-group.events.ts | 9 - .../sbb-radio-button-group.spec.ts | 21 - .../sbb-radio-button-group.tsx | 287 - src/components/sbb-radio-button/readme.md | 89 - .../sbb-radio-button.custom.d.ts | 15 - .../sbb-radio-button/sbb-radio-button.e2e.ts | 67 - .../sbb-radio-button.events.ts | 8 - .../sbb-radio-button/sbb-radio-button.spec.ts | 24 - .../sbb-radio-button/sbb-radio-button.tsx | 270 - .../sbb-select/sbb-select.custom.d.ts | 4 - src/components/sbb-select/sbb-select.e2e.ts | 353 - .../sbb-select/sbb-select.events.ts | 14 - src/components/sbb-select/sbb-select.spec.ts | 86 - src/components/sbb-selection-panel/readme.md | 131 - .../sbb-selection-panel.custom.d.ts | 3 - .../sbb-selection-panel.e2e.ts | 634 -- .../sbb-selection-panel.events.ts | 10 - .../sbb-selection-panel.spec.ts | 60 - .../sbb-selection-panel.tsx | 239 - src/components/sbb-signet/readme.md | 36 - .../sbb-signet/sbb-signet.custom.d.ts | 3 - src/components/sbb-signet/sbb-signet.tsx | 33 - src/components/sbb-skiplink-list/readme.md | 55 - .../sbb-skiplink-list.e2e.ts | 66 - .../sbb-skiplink-list.spec.ts | 87 - .../sbb-skiplink-list/sbb-skiplink-list.tsx | 108 - src/components/sbb-slider/readme.md | 118 - src/components/sbb-slider/sbb-slider.e2e.ts | 115 - .../sbb-slider/sbb-slider.events.ts | 7 - src/components/sbb-slider/sbb-slider.spec.ts | 73 - src/components/sbb-slider/sbb-slider.tsx | 210 - src/components/sbb-tab-group/readme.md | 102 - .../sbb-tab-group/sbb-tab-group.custom.d.ts | 17 - .../sbb-tab-group/sbb-tab-group.e2e.ts | 127 - .../sbb-tab-group/sbb-tab-group.events.ts | 7 - .../sbb-tab-group/sbb-tab-group.spec.ts | 84 - src/components/sbb-tab-title/readme.md | 78 - .../sbb-tab-title/sbb-tab-title.e2e.ts | 13 - .../sbb-tab-title/sbb-tab-title.spec.ts | 73 - .../sbb-tab-title/sbb-tab-title.tsx | 84 - .../sbb-tag-group/sbb-tag-group.e2e.ts | 637 -- src/components/sbb-tag/readme.md | 98 - src/components/sbb-tag/sbb-tag.custom.d.ts | 11 - src/components/sbb-tag/sbb-tag.e2e.ts | 75 - src/components/sbb-tag/sbb-tag.events.ts | 10 - src/components/sbb-tag/sbb-tag.spec.ts | 112 - src/components/sbb-tag/sbb-tag.tsx | 155 - src/components/sbb-teaser-hero/readme.md | 70 - .../sbb-teaser-hero/sbb-teaser-hero.e2e.ts | 29 - .../sbb-teaser-hero/sbb-teaser-hero.spec.ts | 124 - .../sbb-teaser-hero/sbb-teaser-hero.tsx | 102 - src/components/sbb-teaser/readme.md | 63 - src/components/sbb-teaser/sbb-teaser.e2e.ts | 16 - src/components/sbb-teaser/sbb-teaser.spec.ts | 56 - src/components/sbb-teaser/sbb-teaser.tsx | 99 - .../sbb-time-input/sbb-time-input.e2e.ts | 273 - .../sbb-time-input/sbb-time-input.events.ts | 8 - .../sbb-time-input/sbb-time-input.spec.ts | 22 - .../sbb-timetable-barrier-free/readme.md | 15 - .../sbb-timetable-barrier-free.e2e.ts | 18 - .../sbb-timetable-barrier-free.tsx | 58 - .../sbb-timetable-duration/readme.md | 16 - .../sbb-timetable-duration.e2e.ts | 16 - .../sbb-timetable-occupancy/readme.md | 16 - .../sbb-timetable-occupancy.e2e.ts | 16 - .../sbb-timetable-occupancy.tsx | 73 - .../sbb-timetable-park-and-rail/readme.md | 17 - .../sbb-timetable-park-and-rail.custom.d.ts | 3 - .../sbb-timetable-park-and-rail.e2e.ts | 18 - .../sbb-timetable-park-and-rail.tsx | 86 - .../readme.md | 16 - .../sbb-timetable-row-column-headers.e2e.ts | 18 - .../sbb-timetable-row-column-headers.tsx | 28 - .../sbb-timetable-row-day-change/readme.md | 16 - .../sbb-timetable-row-day-change.e2e.ts | 18 - .../sbb-timetable-row-day-change.tsx | 78 - .../sbb-timetable-row-header/readme.md | 16 - .../sbb-timetable-row-header.e2e.ts | 18 - .../sbb-timetable-row-header.tsx | 27 - src/components/sbb-timetable-row/readme.md | 87 - .../sbb-timetable-row.custom.d.ts | 17 - .../sbb-timetable-row.e2e.ts | 28 - .../sbb-timetable-row.helper.spec.ts | 108 - .../sbb-timetable-row.helper.tsx | 231 - .../sbb-timetable-row.spec.ts | 175 - .../sbb-timetable-row/sbb-timetable-row.tsx | 478 -- .../readme.md | 17 - ...imetable-transportation-number.custom.d.ts | 3 - ...sbb-timetable-transportation-number.e2e.ts | 18 - .../sbb-timetable-transportation-number.tsx | 69 - .../readme.md | 17 - ...-timetable-transportation-time.custom.d.ts | 3 - .../sbb-timetable-transportation-time.e2e.ts | 18 - .../sbb-timetable-transportation-time.tsx | 69 - .../sbb-timetable-travel-hints/readme.md | 17 - .../sbb-timetable-travel-hints.custom.d.ts | 3 - .../sbb-timetable-travel-hints.e2e.ts | 18 - .../sbb-timetable-travel-hints.tsx | 81 - src/components/sbb-title/readme.md | 76 - .../sbb-title/sbb-title.custom.d.ts | 6 - src/components/sbb-title/sbb-title.e2e.ts | 12 - src/components/sbb-title/sbb-title.spec.ts | 20 - src/components/sbb-title/sbb-title.tsx | 37 - src/components/sbb-toast/readme.md | 176 - .../sbb-toast/sbb-toast.custom.d.ts | 7 - src/components/sbb-toast/sbb-toast.e2e.ts | 168 - src/components/sbb-toast/sbb-toast.events.ts | 10 - src/components/sbb-toast/sbb-toast.spec.ts | 44 - src/components/sbb-toast/sbb-toast.tsx | 295 - src/components/sbb-toggle-check/readme.md | 99 - .../sbb-toggle-check.custom.d.ts | 9 - .../sbb-toggle-check/sbb-toggle-check.e2e.ts | 69 - .../sbb-toggle-check.events.ts | 7 - .../sbb-toggle-check/sbb-toggle-check.spec.ts | 171 - .../sbb-toggle-check/sbb-toggle-check.tsx | 157 - src/components/sbb-toggle-option/readme.md | 73 - .../sbb-toggle-option.custom.d.ts | 11 - .../sbb-toggle-option.e2e.ts | 46 - .../sbb-toggle-option.events.ts | 7 - .../sbb-toggle-option.spec.ts | 54 - .../sbb-toggle-option/sbb-toggle-option.tsx | 169 - src/components/sbb-toggle/readme.md | 68 - .../sbb-toggle/sbb-toggle.custom.d.ts | 3 - src/components/sbb-toggle/sbb-toggle.e2e.ts | 129 - .../sbb-toggle/sbb-toggle.events.ts | 8 - src/components/sbb-toggle/sbb-toggle.spec.ts | 212 - src/components/sbb-toggle/sbb-toggle.tsx | 221 - src/components/sbb-tooltip-trigger/readme.md | 110 - .../sbb-tooltip-trigger.e2e.ts | 103 - .../sbb-tooltip-trigger.spec.ts | 43 - .../sbb-tooltip-trigger.tsx | 62 - src/components/sbb-tooltip/readme.md | 175 - src/components/sbb-tooltip/sbb-tooltip.e2e.ts | 451 -- .../sbb-tooltip/sbb-tooltip.events.ts | 10 - .../sbb-tooltip/sbb-tooltip.spec.ts | 33 - .../sbb-train-blocked-passage.e2e.ts | 13 - .../sbb-train-blocked-passage.spec.ts | 23 - .../sbb-train-blocked-passage.tsx | 18 - .../sbb-train-formation.spec.ts | 89 - .../sbb-train-formation.tsx | 172 - src/components/sbb-train-wagon/readme.md | 76 - .../sbb-train-wagon.custom.d.ts | 5 - .../sbb-train-wagon/sbb-train-wagon.e2e.ts | 52 - .../sbb-train-wagon/sbb-train-wagon.events.ts | 7 - .../sbb-train-wagon/sbb-train-wagon.spec.ts | 277 - .../sbb-train-wagon/sbb-train-wagon.tsx | 245 - src/components/sbb-train/readme.md | 64 - .../sbb-train/sbb-train.custom.d.ts | 3 - src/components/sbb-train/sbb-train.e2e.ts | 33 - src/components/sbb-train/sbb-train.events.ts | 7 - src/components/sbb-train/sbb-train.spec.ts | 121 - src/components/sbb-train/sbb-train.tsx | 155 - src/components/sbb-visual-checkbox/readme.md | 44 - .../sbb-visual-checkbox.e2e.ts | 13 - .../sbb-visual-checkbox.spec.ts | 86 - .../sbb-visual-checkbox.tsx | 45 - src/components/select/index.ts | 1 + .../{sbb-select => select}/readme.md | 133 +- src/components/select/select.e2e.ts | 357 + .../sbb-select.scss => select/select.scss} | 14 +- src/components/select/select.spec.ts | 86 + .../select.stories.tsx} | 49 +- .../sbb-select.tsx => select/select.ts} | 401 +- src/components/selection-panel/index.ts | 1 + src/components/selection-panel/readme.md | 102 + .../selection-panel/selection-panel.e2e.ts | 526 ++ .../selection-panel.scss} | 8 +- .../selection-panel/selection-panel.spec.ts | 57 + .../selection-panel.stories.tsx} | 47 +- .../selection-panel/selection-panel.ts | 234 + src/components/signet/readme.md | 29 + .../sbb-signet.scss => signet/signet.scss} | 2 +- .../signet.stories.tsx} | 12 +- src/components/signet/signet.ts | 44 + src/components/skiplink-list/index.ts | 1 + src/components/skiplink-list/readme.md | 35 + .../skiplink-list/skiplink-list.e2e.ts | 61 + .../skiplink-list.scss} | 2 +- .../skiplink-list/skiplink-list.spec.ts | 92 + .../skiplink-list.stories.tsx} | 19 +- src/components/skiplink-list/skiplink-list.ts | 123 + src/components/slider/index.ts | 1 + src/components/slider/readme.md | 97 + src/components/slider/slider.e2e.ts | 80 + .../sbb-slider.scss => slider/slider.scss} | 14 +- src/components/slider/slider.spec.ts | 77 + .../slider.stories.tsx} | 16 +- src/components/slider/slider.ts | 227 + src/components/tabs/index.ts | 2 + src/components/tabs/tab-group/index.ts | 1 + src/components/tabs/tab-group/readme.md | 67 + .../tabs/tab-group/tab-group.e2e.ts | 126 + .../tab-group/tab-group.scss} | 8 +- .../tabs/tab-group/tab-group.spec.ts | 84 + .../tab-group/tab-group.stories.tsx} | 17 +- .../tab-group/tab-group.ts} | 171 +- src/components/tabs/tab-title/index.ts | 1 + src/components/tabs/tab-title/readme.md | 54 + .../tabs/tab-title/tab-title.e2e.ts | 13 + .../tab-title/tab-title.scss} | 8 +- .../tabs/tab-title/tab-title.spec.ts | 71 + .../tab-title/tab-title.stories.tsx} | 13 +- src/components/tabs/tab-title/tab-title.ts | 104 + src/components/tag/index.ts | 2 + src/components/tag/tag-group/index.ts | 1 + .../tag-group}/readme.md | 46 +- src/components/tag/tag-group/tag-group.e2e.ts | 546 ++ .../tag-group/tag-group.scss} | 2 +- .../tag/tag-group/tag-group.spec.ts | 56 + .../tag-group/tag-group.stories.tsx} | 11 +- .../tag-group/tag-group.ts} | 146 +- src/components/tag/tag/index.ts | 1 + src/components/tag/tag/readme.md | 74 + src/components/tag/tag/tag.e2e.ts | 77 + .../sbb-tag.scss => tag/tag/tag.scss} | 14 +- src/components/tag/tag/tag.spec.ts | 126 + .../tag/tag.stories.tsx} | 13 +- src/components/tag/tag/tag.ts | 197 + src/components/teaser-hero/index.ts | 1 + src/components/teaser-hero/readme.md | 52 + src/components/teaser-hero/teaser-hero.e2e.ts | 32 + .../teaser-hero.scss} | 2 +- .../teaser-hero/teaser-hero.spec.ts | 165 + .../teaser-hero.stories.tsx} | 15 +- src/components/teaser-hero/teaser-hero.ts | 130 + src/components/teaser/index.ts | 1 + src/components/teaser/readme.md | 43 + .../stories/placeholder.png | Bin src/components/teaser/teaser.e2e.ts | 20 + .../sbb-teaser.scss => teaser/teaser.scss} | 4 +- src/components/teaser/teaser.spec.ts | 71 + .../teaser.stories.tsx} | 16 +- src/components/teaser/teaser.ts | 118 + src/components/time-input/index.ts | 1 + .../{sbb-time-input => time-input}/readme.md | 64 +- src/components/time-input/time-input.e2e.ts | 274 + .../time-input.scss} | 2 +- src/components/time-input/time-input.spec.ts | 26 + .../time-input.stories.tsx} | 34 +- .../time-input.ts} | 184 +- .../timetable-barrier-free/readme.md | 9 + .../timetable-barrier-free.e2e.ts | 19 + .../timetable-barrier-free.sample-data.js} | 0 .../timetable-barrier-free.scss} | 2 +- .../timetable-barrier-free.spec.ts} | 30 +- .../timetable-barrier-free.stories.tsx} | 12 +- .../timetable-barrier-free.ts | 65 + src/components/timetable-duration/readme.md | 10 + .../timetable-duration.e2e.ts | 18 + .../timetable-duration.sample-data.js} | 0 .../timetable-duration.scss} | 2 +- .../timetable-duration.spec.ts} | 31 +- .../timetable-duration.stories.tsx} | 12 +- .../timetable-duration.ts} | 62 +- src/components/timetable-occupancy/readme.md | 10 + .../timetable-occupancy.e2e.ts | 18 + .../timetable-occupancy.sample-data.js} | 6 +- .../timetable-occupancy.scss} | 2 +- .../timetable-occupancy.spec.ts} | 30 +- .../timetable-occupancy.stories.tsx} | 12 +- .../timetable-occupancy.ts | 79 + .../timetable-park-and-rail/readme.md | 11 + .../timetable-park-and-rail.e2e.ts | 18 + .../timetable-park-and-rail.sample-data.js} | 0 .../timetable-park-and-rail.scss} | 2 +- .../timetable-park-and-rail.spec.ts} | 30 +- .../timetable-park-and-rail.stories.tsx} | 12 +- .../timetable-park-and-rail.ts | 89 + .../timetable-row-column-headers/readme.md | 10 + .../timetable-row-column-headers.e2e.ts | 20 + ...metable-row-column-headers.sample-data.js} | 0 .../timetable-row-column-headers.scss} | 2 +- .../timetable-row-column-headers.spec.ts} | 32 +- .../timetable-row-column-headers.stories.tsx} | 11 +- .../timetable-row-column-headers.ts | 39 + .../timetable-row-day-change/readme.md | 10 + .../timetable-row-day-change.e2e.ts | 18 + .../timetable-row-day-change.sample-data.js} | 0 .../timetable-row-day-change.scss} | 2 +- .../timetable-row-day-change.spec.ts} | 30 +- .../timetable-row-day-change.stories.tsx} | 12 +- .../timetable-row-day-change.ts | 82 + src/components/timetable-row-header/readme.md | 10 + .../timetable-row-header.e2e.ts | 19 + .../timetable-row-header.sample-data.js} | 4 +- .../timetable-row-header.scss} | 2 +- .../timetable-row-header.spec.ts} | 33 +- .../timetable-row-header.stories.tsx} | 12 +- .../timetable-row-header.ts | 34 + src/components/timetable-row/index.ts | 1 + src/components/timetable-row/readme.md | 61 + .../timetable-row/timetable-row.e2e.ts | 29 + .../timetable-row.sample-data.ts} | 5 +- .../timetable-row.scss} | 8 +- .../timetable-row/timetable-row.spec.ts | 301 + .../timetable-row.stories.tsx} | 18 +- src/components/timetable-row/timetable-row.ts | 731 ++ .../timetable-transportation-number/readme.md | 11 + .../timetable-transportation-number.e2e.ts | 20 + ...able-transportation-number.sample-data.js} | 0 .../timetable-transportation-number.scss} | 2 +- .../timetable-transportation-number.spec.ts} | 32 +- ...metable-transportation-number.stories.tsx} | 12 +- .../timetable-transportation-number.ts | 77 + .../timetable-transportation-time/readme.md | 11 + .../timetable-transportation-time.e2e.ts | 20 + ...etable-transportation-time.sample-data.js} | 0 .../timetable-transportation-time.scss} | 2 +- .../timetable-transportation-time.spec.ts} | 32 +- ...timetable-transportation-time.stories.tsx} | 12 +- .../timetable-transportation-time.ts | 74 + .../timetable-travel-hints/readme.md | 11 + .../timetable-travel-hints.e2e.ts | 18 + .../timetable-travel-hints.sample-data.js} | 0 .../timetable-travel-hints.scss} | 2 +- .../timetable-travel-hints.spec.ts} | 33 +- .../timetable-travel-hints.stories.tsx} | 12 +- .../timetable-travel-hints.ts | 86 + src/components/title/index.ts | 1 + src/components/title/readme.md | 49 + src/components/title/title.e2e.ts | 14 + .../sbb-title.scss => title/title.scss} | 44 +- src/components/title/title.spec.ts | 21 + .../title.stories.tsx} | 10 +- src/components/title/title.ts | 58 + src/components/toast/index.ts | 1 + src/components/toast/readme.md | 132 + src/components/toast/toast.e2e.ts | 165 + .../sbb-toast.scss => toast/toast.scss} | 6 +- src/components/toast/toast.spec.ts | 48 + .../toast.stories.tsx} | 30 +- src/components/toast/toast.ts | 314 + src/components/toggle-check/index.ts | 1 + src/components/toggle-check/readme.md | 76 + .../toggle-check/toggle-check.e2e.ts | 74 + .../toggle-check.scss} | 8 +- .../toggle-check/toggle-check.spec.ts | 189 + .../toggle-check.stories.tsx} | 8 +- src/components/toggle-check/toggle-check.ts | 168 + src/components/toggle/index.ts | 2 + src/components/toggle/toggle-option/index.ts | 1 + src/components/toggle/toggle-option/readme.md | 46 + .../toggle/toggle-option/toggle-option.e2e.ts | 47 + .../toggle-option/toggle-option.scss} | 6 +- .../toggle-option/toggle-option.spec.ts | 65 + .../toggle-option/toggle-option.stories.tsx} | 10 +- .../toggle/toggle-option/toggle-option.ts | 205 + src/components/toggle/toggle/index.ts | 1 + src/components/toggle/toggle/readme.md | 56 + src/components/toggle/toggle/toggle.e2e.ts | 162 + .../toggle/toggle.scss} | 10 +- src/components/toggle/toggle/toggle.spec.ts | 167 + .../toggle/toggle.stories.tsx} | 13 +- src/components/toggle/toggle/toggle.ts | 250 + src/components/tooltip/index.ts | 2 + .../tooltip/tooltip-trigger/index.ts | 1 + .../tooltip/tooltip-trigger/readme.md | 82 + .../tooltip-trigger/tooltip-trigger.e2e.ts | 95 + .../tooltip-trigger/tooltip-trigger.scss} | 16 +- .../tooltip-trigger/tooltip-trigger.spec.ts | 49 + .../tooltip-trigger.stories.tsx} | 12 +- .../tooltip-trigger/tooltip-trigger.ts | 72 + src/components/tooltip/tooltip/index.ts | 1 + src/components/tooltip/tooltip/readme.md | 142 + src/components/tooltip/tooltip/tooltip.e2e.ts | 376 + .../tooltip/tooltip.scss} | 6 +- .../tooltip/tooltip/tooltip.spec.ts | 48 + .../tooltip/tooltip.stories.tsx} | 29 +- .../tooltip/tooltip.ts} | 287 +- src/components/train/index.ts | 4 + .../train/train-blocked-passage/index.ts | 1 + .../train-blocked-passage}/readme.md | 8 +- .../train-blocked-passage.e2e.ts | 13 + .../train-blocked-passage.scss} | 2 +- .../train-blocked-passage.spec.ts | 25 + .../train-blocked-passage.stories.tsx} | 10 +- .../train-blocked-passage.ts | 29 + src/components/train/train-formation/index.ts | 1 + .../train-formation}/readme.md | 30 +- .../train-formation/train-formation.e2e.ts} | 189 +- .../train-formation/train-formation.scss} | 4 +- .../train-formation/train-formation.spec.ts | 104 + .../train-formation.stories.tsx} | 28 +- .../train/train-formation/train-formation.ts | 190 + src/components/train/train-wagon/index.ts | 1 + src/components/train/train-wagon/readme.md | 60 + .../train/train-wagon/train-wagon.e2e.ts | 49 + .../train-wagon/train-wagon.scss} | 2 +- .../train/train-wagon/train-wagon.spec.ts | 322 + .../train-wagon/train-wagon.stories.tsx} | 10 +- .../train/train-wagon/train-wagon.ts | 261 + src/components/train/train/index.ts | 1 + src/components/train/train/readme.md | 44 + src/components/train/train/train.e2e.ts | 32 + .../sbb-train.scss => train/train/train.scss} | 2 +- src/components/train/train/train.spec.ts | 121 + .../train/train.stories.tsx} | 10 +- src/components/train/train/train.ts | 173 + src/components/tsconfig.json | 13 + src/components/tsconfig.spec.json | 4 + src/components/visual-checkbox/index.ts | 1 + src/components/visual-checkbox/readme.md | 24 + .../visual-checkbox/visual-checkbox.e2e.ts | 13 + .../visual-checkbox.scss} | 18 +- .../visual-checkbox/visual-checkbox.spec.ts | 63 + .../visual-checkbox.stories.tsx} | 10 +- .../visual-checkbox/visual-checkbox.ts | 56 + src/components/vite.config.ts | 100 + src/global/a11y/interactivity-checker.e2e.ts | 71 - src/global/dom/component-on-ready.spec.ts | 55 - src/global/dom/request-animation-frame.ts | 28 - .../eventing/language-change-handler.e2e.ts | 48 - src/global/styles/global.scss | 23 - src/global/testing/index.ts | 6 - src/global/testing/slotchange-events.ts | 57 - src/global/testing/wait-for-condition.ts | 14 - src/global/timetable/timetable-helper.ts | 13 - src/index.ts | 1 - src/react/.gitignore | 1 + src/react/package.json | 19 + src/react/tsconfig.json | 13 + src/react/vite.config.ts | 147 + .../pages/home/home--logged-in.stories.tsx | 51 +- src/storybook/pages/home/home.common.tsx | 27 +- src/storybook/pages/home/home.scss | 2 +- src/storybook/pages/home/home.stories.tsx | 32 +- src/storybook/styles/layout/layout.scss | 2 +- .../styles/layout/layout.stories.tsx | 19 +- src/storybook/styles/list/list.stories.tsx | 9 +- .../styles/scrollbar/scrollbar-internal.scss | 2 +- .../styles/scrollbar/scrollbar.stories.tsx | 8 +- .../styles/typography/typo-internal.scss | 2 +- .../styles/typography/typography.stories.tsx | 11 +- .../testing}/chromatic.tsx | 8 +- .../testing/wait-for-components-ready.ts | 2 +- .../testing/wait-for-stable-position.ts | 0 src/types.d.ts | 22 - src/vite-env.d.ts | 391 + stencil.config.ts | 190 - tools/eslint/index.ts | 15 + tools/eslint/link/index.js | 4 + tools/eslint/link/package.json | 5 + .../missing-component-documentation-rule.ts | 231 + tools/eslint/tsconfig.json | 10 + .../boilerplate/component.e2e.ts | 23 + .../boilerplate/component.scss | 2 +- .../boilerplate/component.spec.ts | 17 + .../boilerplate/component.stories.tsx | 55 + .../boilerplate/component.ts | 65 + .../generate-component/boilerplate/readme.md | 6 +- tools/generate-component/index.mts | 121 + tsconfig.json | 66 +- vite.config.ts | 108 + web-test-runner.config.js | 182 + yarn.lock | 7061 ++++++++--------- 1257 files changed, 41999 insertions(+), 47730 deletions(-) delete mode 100644 .storybook/preview.js create mode 100644 .storybook/preview.ts delete mode 100644 _index.scss delete mode 100644 ci/bundleStories.js rename ci/{clean-storybook-files.js => clean-storybook-files.cjs} (88%) create mode 100644 ci/docs_generate.ts create mode 100644 ci/inline-events.mts create mode 100644 config/custom-elements-manifest.config.js delete mode 100644 convenience/generate-component/boilerplate/component.custom.d.ts delete mode 100644 convenience/generate-component/boilerplate/component.e2e.ts delete mode 100644 convenience/generate-component/boilerplate/component.events.ts delete mode 100644 convenience/generate-component/boilerplate/component.spec.ts delete mode 100644 convenience/generate-component/boilerplate/component.stories.tsx delete mode 100644 convenience/generate-component/boilerplate/component.tsx delete mode 100644 convenience/generate-component/index.js delete mode 100644 jestAssetsTransformer.js delete mode 100644 react-library/package.json delete mode 100644 react-library/src/components/index.ts delete mode 100644 react-library/src/components/stencil-generated/index.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/createComponent.tsx delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/createOverlayComponent.tsx delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/index.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/interfaces.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/utils/attachProps.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/utils/case.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/utils/dev.ts delete mode 100644 react-library/src/components/stencil-generated/react-component-lib/utils/index.tsx delete mode 100644 react-library/src/index.ts delete mode 100644 react-library/tsconfig.json delete mode 100644 src/components.d.ts create mode 100644 src/components/_index.scss create mode 100644 src/components/accordion/accordion.e2e.ts rename src/components/{sbb-accordion/sbb-accordion.scss => accordion/accordion.scss} (83%) create mode 100644 src/components/accordion/accordion.spec.ts rename src/components/{sbb-accordion/sbb-accordion.stories.tsx => accordion/accordion.stories.tsx} (91%) create mode 100644 src/components/accordion/accordion.ts create mode 100644 src/components/accordion/index.ts create mode 100644 src/components/accordion/readme.md create mode 100644 src/components/action-group/action-group.e2e.ts rename src/components/{sbb-action-group/sbb-action-group.scss => action-group/action-group.scss} (98%) create mode 100644 src/components/action-group/action-group.spec.ts rename src/components/{sbb-action-group/sbb-action-group.stories.tsx => action-group/action-group.stories.tsx} (97%) create mode 100644 src/components/action-group/action-group.ts create mode 100644 src/components/action-group/index.ts create mode 100644 src/components/action-group/readme.md create mode 100644 src/components/alert/alert-group/alert-group.e2e.ts rename src/components/{sbb-alert-group/sbb-alert-group.scss => alert/alert-group/alert-group.scss} (94%) create mode 100644 src/components/alert/alert-group/alert-group.spec.ts rename src/components/{sbb-alert-group/sbb-alert-group.stories.tsx => alert/alert-group/alert-group.stories.tsx} (88%) create mode 100644 src/components/alert/alert-group/alert-group.ts create mode 100644 src/components/alert/alert-group/index.ts create mode 100644 src/components/alert/alert-group/readme.md create mode 100644 src/components/alert/alert/alert.e2e.ts rename src/components/{sbb-alert/sbb-alert.scss => alert/alert/alert.scss} (98%) create mode 100644 src/components/alert/alert/alert.spec.ts rename src/components/{sbb-alert/sbb-alert.stories.tsx => alert/alert/alert.stories.tsx} (94%) create mode 100644 src/components/alert/alert/alert.ts create mode 100644 src/components/alert/alert/index.ts create mode 100644 src/components/alert/alert/readme.md create mode 100644 src/components/alert/index.ts create mode 100644 src/components/autocomplete/autocomplete.e2e.ts rename src/components/{sbb-autocomplete/sbb-autocomplete.scss => autocomplete/autocomplete.scss} (90%) create mode 100644 src/components/autocomplete/autocomplete.spec.ts rename src/components/{sbb-autocomplete/sbb-autocomplete.stories.tsx => autocomplete/autocomplete.stories.tsx} (93%) rename src/components/{sbb-autocomplete/sbb-autocomplete.tsx => autocomplete/autocomplete.ts} (62%) create mode 100644 src/components/autocomplete/index.ts rename src/components/{sbb-autocomplete => autocomplete}/readme.md (57%) create mode 100644 src/components/breadcrumb/breadcrumb-group/breadcrumb-group.e2e.ts rename src/components/{sbb-breadcrumb-group/sbb-breadcrumb-group.scss => breadcrumb/breadcrumb-group/breadcrumb-group.scss} (98%) create mode 100644 src/components/breadcrumb/breadcrumb-group/breadcrumb-group.spec.ts rename src/components/{sbb-breadcrumb-group/sbb-breadcrumb-group.stories.tsx => breadcrumb/breadcrumb-group/breadcrumb-group.stories.tsx} (94%) rename src/components/{sbb-breadcrumb-group/sbb-breadcrumb-group.tsx => breadcrumb/breadcrumb-group/breadcrumb-group.ts} (52%) create mode 100644 src/components/breadcrumb/breadcrumb-group/index.ts rename src/components/{sbb-breadcrumb-group => breadcrumb/breadcrumb-group}/readme.md (54%) create mode 100644 src/components/breadcrumb/breadcrumb/breadcrumb.e2e.ts rename src/components/{sbb-breadcrumb/sbb-breadcrumb.scss => breadcrumb/breadcrumb/breadcrumb.scss} (97%) create mode 100644 src/components/breadcrumb/breadcrumb/breadcrumb.spec.ts rename src/components/{sbb-breadcrumb/sbb-breadcrumb.stories.tsx => breadcrumb/breadcrumb/breadcrumb.stories.tsx} (94%) create mode 100644 src/components/breadcrumb/breadcrumb/breadcrumb.ts create mode 100644 src/components/breadcrumb/breadcrumb/index.ts create mode 100644 src/components/breadcrumb/breadcrumb/readme.md create mode 100644 src/components/breadcrumb/index.ts create mode 100644 src/components/button/button.e2e.ts rename src/components/{sbb-button/sbb-button.scss => button/button.scss} (93%) create mode 100644 src/components/button/button.spec.ts rename src/components/{sbb-button/sbb-button.stories.tsx => button/button.stories.tsx} (98%) create mode 100644 src/components/button/button.ts create mode 100644 src/components/button/index.ts create mode 100644 src/components/button/readme.md create mode 100644 src/components/calendar/calendar.e2e.ts rename src/components/{sbb-calendar/sbb-calendar.scss => calendar/calendar.scss} (99%) rename src/components/{sbb-calendar/sbb-calendar.spec.ts => calendar/calendar.spec.ts} (83%) rename src/components/{sbb-calendar/sbb-calendar.stories.tsx => calendar/calendar.stories.tsx} (95%) rename src/components/{sbb-calendar/sbb-calendar.tsx => calendar/calendar.ts} (67%) create mode 100644 src/components/calendar/index.ts rename src/components/{sbb-calendar => calendar}/readme.md (53%) create mode 100644 src/components/card/card-action/card-action.e2e.ts rename src/components/{sbb-card-action/sbb-card-action.scss => card/card-action/card-action.scss} (95%) rename src/components/{sbb-card-action/sbb-card-action.stories.tsx => card/card-action/card-action.stories.tsx} (75%) create mode 100644 src/components/card/card-action/card-action.ts create mode 100644 src/components/card/card-action/index.ts create mode 100644 src/components/card/card-action/readme.md create mode 100644 src/components/card/card-badge/card-badge.e2e.ts rename src/components/{sbb-card-badge/sbb-card-badge.scss => card/card-badge/card-badge.scss} (98%) create mode 100644 src/components/card/card-badge/card-badge.spec.ts rename src/components/{sbb-card-badge/sbb-card-badge.stories.tsx => card/card-badge/card-badge.stories.tsx} (91%) create mode 100644 src/components/card/card-badge/card-badge.ts create mode 100644 src/components/card/card-badge/index.ts create mode 100644 src/components/card/card-badge/readme.md create mode 100644 src/components/card/card/card.e2e.ts rename src/components/{sbb-card/sbb-card.scss => card/card/card.scss} (98%) rename src/components/{sbb-card/sbb-card.spec.ts => card/card/card.spec.ts} (62%) rename src/components/{sbb-card/sbb-card.stories.tsx => card/card/card.stories.tsx} (97%) create mode 100644 src/components/card/card/card.ts create mode 100644 src/components/card/card/index.ts rename src/components/{sbb-card => card/card}/readme.md (61%) create mode 100644 src/components/card/index.ts create mode 100644 src/components/checkbox/checkbox-group/checkbox-group.e2e.ts rename src/components/{sbb-checkbox-group/sbb-checkbox-group.scss => checkbox/checkbox-group/checkbox-group.scss} (98%) create mode 100644 src/components/checkbox/checkbox-group/checkbox-group.spec.ts rename src/components/{sbb-checkbox-group/sbb-checkbox-group.stories.tsx => checkbox/checkbox-group/checkbox-group.stories.tsx} (97%) create mode 100644 src/components/checkbox/checkbox-group/checkbox-group.ts create mode 100644 src/components/checkbox/checkbox-group/index.ts create mode 100644 src/components/checkbox/checkbox-group/readme.md create mode 100644 src/components/checkbox/checkbox/checkbox.e2e.ts rename src/components/{sbb-checkbox/sbb-checkbox.scss => checkbox/checkbox/checkbox.scss} (97%) create mode 100644 src/components/checkbox/checkbox/checkbox.spec.ts rename src/components/{sbb-checkbox/sbb-checkbox.stories.tsx => checkbox/checkbox/checkbox.stories.tsx} (96%) create mode 100644 src/components/checkbox/checkbox/checkbox.ts create mode 100644 src/components/checkbox/checkbox/index.ts create mode 100644 src/components/checkbox/checkbox/readme.md create mode 100644 src/components/checkbox/index.ts create mode 100644 src/components/chip/chip.e2e.ts rename src/components/{sbb-chip/sbb-chip.scss => chip/chip.scss} (95%) create mode 100644 src/components/chip/chip.spec.ts rename src/components/{sbb-chip/sbb-chip.stories.tsx => chip/chip.stories.tsx} (94%) create mode 100644 src/components/chip/chip.ts create mode 100644 src/components/chip/index.ts create mode 100644 src/components/chip/readme.md rename src/components/{sbb-clock => clock}/assets/sbb_clock_face.svg (100%) rename src/components/{sbb-clock => clock}/assets/sbb_clock_hours.svg (100%) rename src/components/{sbb-clock => clock}/assets/sbb_clock_minutes.svg (100%) rename src/components/{sbb-clock => clock}/assets/sbb_clock_seconds.svg (100%) rename src/components/{sbb-clock/sbb-clock.scss => clock/clock.scss} (98%) create mode 100644 src/components/clock/clock.spec.ts rename src/components/{sbb-clock/sbb-clock.stories.tsx => clock/clock.stories.tsx} (78%) rename src/components/{sbb-clock/sbb-clock.tsx => clock/clock.ts} (71%) create mode 100644 src/components/clock/index.ts rename src/components/{sbb-clock => clock}/readme.md (87%) rename src/{global => components/core}/a11y/arrow-navigation.spec.ts (55%) rename src/{global => components/core}/a11y/arrow-navigation.ts (100%) rename src/{global => components/core}/a11y/assign-id.ts (100%) rename src/{global => components/core}/a11y/fake-event-detection.ts (100%) rename src/{global => components/core}/a11y/focus.ts (98%) rename src/{global => components/core}/a11y/index.ts (100%) rename src/{global => components/core}/a11y/input-modality-detector.ts (98%) create mode 100644 src/components/core/a11y/interactivity-checker.spec.ts rename src/{global => components/core}/a11y/interactivity-checker.ts (100%) rename src/{global => components/core/config}/config.ts (92%) create mode 100644 src/components/core/config/index.ts rename src/{global => components/core}/datetime/date-adapter.ts (100%) rename src/{global => components/core}/datetime/date-helper.spec.ts (68%) rename src/{global => components/core}/datetime/date-helper.ts (98%) rename src/{global => components/core}/datetime/index.ts (100%) rename src/{global => components/core}/datetime/native-date-adapter.spec.ts (51%) rename src/{global => components/core}/datetime/native-date-adapter.ts (99%) rename src/{global => components/core}/dom/breakpoint.ts (100%) rename src/{global => components/core}/dom/dataset.ts (100%) rename src/{global => components/core}/dom/find-referenced-element.ts (100%) rename src/{global => components/core}/dom/get-document-writing-mode.ts (100%) rename src/{global => components/core}/dom/host-context.spec.ts (81%) rename src/{global => components/core}/dom/host-context.ts (100%) rename src/{global => components/core}/dom/index.ts (87%) rename src/{global => components/core}/dom/input-element.ts (100%) rename src/{global => components/core}/dom/is-valid-attribute.ts (77%) rename src/{global => components/core}/dom/platform.ts (100%) rename src/{global => components/core}/dom/scroll.ts (100%) rename src/{global => components/core}/eventing/action-element-handlers.ts (99%) rename src/{global => components/core}/eventing/composed-path-has-attribute.ts (100%) rename src/{global => components/core}/eventing/connected-abort-controller.ts (100%) rename src/{global => components/core}/eventing/event-emitter.ts (58%) rename src/{global => components/core}/eventing/event-target.ts (100%) rename src/{global => components/core}/eventing/form-element-handlers.ts (100%) rename src/{global => components/core}/eventing/forward-event.ts (100%) rename src/{global => components/core}/eventing/handler-repository.ts (100%) rename src/{global => components/core}/eventing/index.ts (100%) rename src/{global => components/core}/eventing/is-event-prevented.ts (100%) create mode 100644 src/components/core/eventing/language-change-handler.e2e.ts rename src/{global => components/core}/eventing/language-change-handler.ts (99%) rename src/{global => components/core}/eventing/named-slot-change-handler.ts (99%) rename src/{global => components/core}/eventing/throttle.ts (100%) rename src/{global => components/core/i18n}/i18n.ts (100%) create mode 100644 src/components/core/i18n/index.ts rename src/{global => components/core}/images.ts (100%) rename src/{global => components/core}/interfaces/index.ts (75%) rename src/{global => components/core}/interfaces/link-button-properties.spec.ts (79%) rename src/{global => components/core}/interfaces/link-button-properties.ts (96%) create mode 100644 src/components/core/interfaces/types.ts rename src/{global => components/core}/interfaces/validation-change.ts (100%) rename src/{global => components/core}/observers/index.ts (100%) rename src/{global => components/core}/observers/intersection-observer.ts (100%) rename src/{global => components/core}/observers/mutation-observer.ts (100%) rename src/{global => components/core}/observers/resize-observer.ts (100%) rename src/{global => components/core}/overlay/index.ts (100%) rename src/{global => components/core}/overlay/overlay-option-panel.ts (100%) rename src/{global => components/core}/overlay/overlay-trigger-attributes.ts (100%) rename src/{global/overlay/overlay.tsx => components/core/overlay/overlay.ts} (95%) rename src/{global => components/core}/overlay/position.spec.ts (81%) rename src/{global => components/core}/overlay/position.ts (100%) rename src/{global => components/core}/styles/_index.scss (100%) rename src/{global => components/core}/styles/core/component-utilities.scss (100%) rename src/{global => components/core}/styles/core/font-face.scss (100%) rename src/{global => components/core}/styles/core/functions.scss (100%) rename src/{global => components/core}/styles/core/mediaqueries.scss (100%) rename src/{global => components/core}/styles/core/variables.scss (100%) create mode 100644 src/components/core/styles/global.scss rename src/{global => components/core}/styles/mixins/a11y.scss (100%) rename src/{global => components/core}/styles/mixins/badge.scss (100%) rename src/{global => components/core}/styles/mixins/buttons.scss (98%) rename src/{global => components/core}/styles/mixins/card.scss (100%) rename src/{global => components/core}/styles/mixins/chip.scss (100%) rename src/{global => components/core}/styles/mixins/dev_and_debug.scss (100%) rename src/{global => components/core}/styles/mixins/helpers.scss (100%) rename src/{global => components/core}/styles/mixins/inputs.scss (100%) rename src/{global => components/core}/styles/mixins/layout.scss (100%) rename src/{global => components/core}/styles/mixins/link.scss (100%) rename src/{global => components/core}/styles/mixins/lists.scss (100%) rename src/{global => components/core}/styles/mixins/overlay.scss (100%) rename src/{global => components/core}/styles/mixins/panel.scss (100%) rename src/{global => components/core}/styles/mixins/pearl-chain-bullet.scss (100%) rename src/{global => components/core}/styles/mixins/scrollbar.scss (100%) rename src/{global => components/core}/styles/mixins/shadows.scss (100%) rename src/{global => components/core}/styles/mixins/typo.scss (100%) rename src/{global => components/core}/styles/normalize.scss (100%) rename src/{global => components/core}/styles/typography.scss (95%) rename src/{global => components/core}/testing/event-spy.ts (67%) create mode 100644 src/components/core/testing/index.ts create mode 100644 src/components/core/testing/scroll.ts rename src/{global/testing/jest-setup.ts => components/core/testing/test-setup.ts} (75%) create mode 100644 src/components/core/testing/wait-for-condition.ts create mode 100644 src/components/core/testing/wait-for-render.ts rename src/{global => components/core}/timetable/access-leg-helper.spec.ts (80%) rename src/{global/timetable/access-leg-helper.tsx => components/core/timetable/access-leg-helper.ts} (75%) rename src/{global => components/core}/timetable/icons.json (100%) rename src/{global => components/core}/timetable/index.ts (100%) create mode 100644 src/components/core/timetable/timetable-helper.spec.ts rename src/components/{sbb-pearl-chain-time/sbb-pearl-chain-time.helper.tsx => core/timetable/timetable-helper.ts} (54%) rename src/{global => components/core}/timetable/timetable-properties.ts (99%) create mode 100644 src/components/datepicker/datepicker-next-day/datepicker-next-day.e2e.ts rename src/components/{sbb-datepicker-next-day/sbb-datepicker-next-day.scss => datepicker/datepicker-next-day/datepicker-next-day.scss} (93%) create mode 100644 src/components/datepicker/datepicker-next-day/datepicker-next-day.spec.ts rename src/components/{sbb-datepicker-next-day/sbb-datepicker-next-day.stories.tsx => datepicker/datepicker-next-day/datepicker-next-day.stories.tsx} (89%) rename src/components/{sbb-datepicker-next-day/sbb-datepicker-next-day.tsx => datepicker/datepicker-next-day/datepicker-next-day.ts} (50%) create mode 100644 src/components/datepicker/datepicker-next-day/index.ts rename src/components/{sbb-datepicker-next-day => datepicker/datepicker-next-day}/readme.md (57%) create mode 100644 src/components/datepicker/datepicker-previous-day/datepicker-previous-day.e2e.ts rename src/components/{sbb-datepicker-previous-day/sbb-datepicker-previous-day.scss => datepicker/datepicker-previous-day/datepicker-previous-day.scss} (93%) create mode 100644 src/components/datepicker/datepicker-previous-day/datepicker-previous-day.spec.ts rename src/components/{sbb-datepicker-previous-day/sbb-datepicker-previous-day.stories.tsx => datepicker/datepicker-previous-day/datepicker-previous-day.stories.tsx} (90%) rename src/components/{sbb-datepicker-previous-day/sbb-datepicker-previous-day.tsx => datepicker/datepicker-previous-day/datepicker-previous-day.ts} (51%) create mode 100644 src/components/datepicker/datepicker-previous-day/index.ts rename src/components/{sbb-datepicker-previous-day => datepicker/datepicker-previous-day}/readme.md (59%) create mode 100644 src/components/datepicker/datepicker-toggle/datepicker-toggle.e2e.ts rename src/components/{sbb-datepicker-toggle/sbb-datepicker-toggle.scss => datepicker/datepicker-toggle/datepicker-toggle.scss} (94%) create mode 100644 src/components/datepicker/datepicker-toggle/datepicker-toggle.spec.ts rename src/components/{sbb-datepicker-toggle/sbb-datepicker-toggle.stories.tsx => datepicker/datepicker-toggle/datepicker-toggle.stories.tsx} (84%) create mode 100644 src/components/datepicker/datepicker-toggle/datepicker-toggle.ts create mode 100644 src/components/datepicker/datepicker-toggle/index.ts create mode 100644 src/components/datepicker/datepicker-toggle/readme.md create mode 100644 src/components/datepicker/datepicker/datepicker.e2e.ts rename src/components/{sbb-datepicker/sbb-datepicker.scss => datepicker/datepicker/datepicker.scss} (56%) rename src/components/{sbb-datepicker/sbb-datepicker.helper.spec.ts => datepicker/datepicker/datepicker.spec.ts} (60%) rename src/components/{sbb-datepicker/sbb-datepicker.stories.tsx => datepicker/datepicker/datepicker.stories.tsx} (90%) create mode 100644 src/components/datepicker/datepicker/datepicker.ts create mode 100644 src/components/datepicker/datepicker/index.ts rename src/components/{sbb-datepicker => datepicker/datepicker}/readme.md (51%) create mode 100644 src/components/datepicker/index.ts create mode 100644 src/components/dialog/dialog.e2e.ts rename src/components/{sbb-dialog/sbb-dialog.scss => dialog/dialog.scss} (94%) create mode 100644 src/components/dialog/dialog.spec.ts rename src/components/{sbb-dialog/sbb-dialog.stories.tsx => dialog/dialog.stories.tsx} (94%) rename src/components/{sbb-dialog/sbb-dialog.tsx => dialog/dialog.ts} (53%) create mode 100644 src/components/dialog/index.ts create mode 100644 src/components/dialog/readme.md create mode 100644 src/components/divider/divider.e2e.ts rename src/components/{sbb-divider/sbb-divider.scss => divider/divider.scss} (89%) create mode 100644 src/components/divider/divider.spec.ts rename src/components/{sbb-divider/sbb-divider.stories.tsx => divider/divider.stories.tsx} (89%) create mode 100644 src/components/divider/divider.ts create mode 100644 src/components/divider/index.ts create mode 100644 src/components/divider/readme.md create mode 100644 src/components/expansion-panel/expansion-panel-content/expansion-panel-content.e2e.ts rename src/components/{sbb-expansion-panel-content/sbb-expansion-panel-content.scss => expansion-panel/expansion-panel-content/expansion-panel-content.scss} (97%) create mode 100644 src/components/expansion-panel/expansion-panel-content/expansion-panel-content.spec.ts rename src/components/{sbb-expansion-panel-content/sbb-expansion-panel-content.stories.tsx => expansion-panel/expansion-panel-content/expansion-panel-content.stories.tsx} (77%) create mode 100644 src/components/expansion-panel/expansion-panel-content/expansion-panel-content.ts create mode 100644 src/components/expansion-panel/expansion-panel-content/index.ts rename src/components/{sbb-expansion-panel-content => expansion-panel/expansion-panel-content}/readme.md (58%) create mode 100644 src/components/expansion-panel/expansion-panel-header/expansion-panel-header.e2e.ts rename src/components/{sbb-expansion-panel-header/sbb-expansion-panel-header.scss => expansion-panel/expansion-panel-header/expansion-panel-header.scss} (96%) create mode 100644 src/components/expansion-panel/expansion-panel-header/expansion-panel-header.spec.ts rename src/components/{sbb-expansion-panel-header/sbb-expansion-panel-header.stories.tsx => expansion-panel/expansion-panel-header/expansion-panel-header.stories.tsx} (77%) create mode 100644 src/components/expansion-panel/expansion-panel-header/expansion-panel-header.ts create mode 100644 src/components/expansion-panel/expansion-panel-header/index.ts create mode 100644 src/components/expansion-panel/expansion-panel-header/readme.md create mode 100644 src/components/expansion-panel/expansion-panel/expansion-panel.e2e.ts rename src/components/{sbb-expansion-panel/sbb-expansion-panel.scss => expansion-panel/expansion-panel/expansion-panel.scss} (91%) create mode 100644 src/components/expansion-panel/expansion-panel/expansion-panel.spec.ts rename src/components/{sbb-expansion-panel/sbb-expansion-panel.stories.tsx => expansion-panel/expansion-panel/expansion-panel.stories.tsx} (89%) create mode 100644 src/components/expansion-panel/expansion-panel/expansion-panel.ts create mode 100644 src/components/expansion-panel/expansion-panel/index.ts create mode 100644 src/components/expansion-panel/expansion-panel/readme.md create mode 100644 src/components/expansion-panel/index.ts create mode 100644 src/components/file-selector/file-selector.e2e.ts rename src/components/{sbb-file-selector/sbb-file-selector.scss => file-selector/file-selector.scss} (97%) create mode 100644 src/components/file-selector/file-selector.spec.ts rename src/components/{sbb-file-selector/sbb-file-selector.stories.tsx => file-selector/file-selector.stories.tsx} (93%) rename src/components/{sbb-file-selector/sbb-file-selector.tsx => file-selector/file-selector.ts} (58%) create mode 100644 src/components/file-selector/index.ts create mode 100644 src/components/file-selector/readme.md create mode 100644 src/components/footer/footer.e2e.ts rename src/components/{sbb-footer/sbb-footer.scss => footer/footer.scss} (93%) create mode 100644 src/components/footer/footer.spec.ts rename src/components/{sbb-footer/sbb-footer.stories.tsx => footer/footer.stories.tsx} (96%) create mode 100644 src/components/footer/footer.ts create mode 100644 src/components/footer/index.ts rename src/components/{sbb-footer => footer}/readme.md (57%) create mode 100644 src/components/form-error/form-error.e2e.ts rename src/components/{sbb-form-error/sbb-form-error.scss => form-error/form-error.scss} (93%) create mode 100644 src/components/form-error/form-error.spec.ts rename src/components/{sbb-form-error/sbb-form-error.stories.tsx => form-error/form-error.stories.tsx} (88%) create mode 100644 src/components/form-error/form-error.ts create mode 100644 src/components/form-error/index.ts create mode 100644 src/components/form-error/readme.md create mode 100644 src/components/form-field/form-field-clear/form-field-clear.e2e.ts rename src/components/{sbb-form-field-clear/sbb-form-field-clear.scss => form-field/form-field-clear/form-field-clear.scss} (92%) create mode 100644 src/components/form-field/form-field-clear/form-field-clear.spec.ts rename src/components/{sbb-form-field-clear/sbb-form-field-clear.stories.tsx => form-field/form-field-clear/form-field-clear.stories.tsx} (86%) create mode 100644 src/components/form-field/form-field-clear/form-field-clear.ts create mode 100644 src/components/form-field/form-field-clear/index.ts create mode 100644 src/components/form-field/form-field-clear/readme.md create mode 100644 src/components/form-field/form-field/form-field.e2e.ts rename src/components/{sbb-form-field/sbb-form-field.scss => form-field/form-field/form-field.scss} (92%) create mode 100644 src/components/form-field/form-field/form-field.spec.ts rename src/components/{sbb-form-field/sbb-form-field.stories.tsx => form-field/form-field/form-field.stories.tsx} (97%) rename src/components/{sbb-form-field/sbb-form-field.tsx => form-field/form-field/form-field.ts} (60%) create mode 100644 src/components/form-field/form-field/index.ts create mode 100644 src/components/form-field/form-field/readme.md create mode 100644 src/components/form-field/index.ts create mode 100644 src/components/header/header-action/header-action.e2e.ts rename src/components/{sbb-header-action/sbb-header-action.scss => header/header-action/header-action.scss} (99%) rename src/components/{sbb-header-action/sbb-header-action.spec.ts => header/header-action/header-action.spec.ts} (51%) rename src/components/{sbb-header-action/sbb-header-action.stories.tsx => header/header-action/header-action.stories.tsx} (96%) create mode 100644 src/components/header/header-action/header-action.ts create mode 100644 src/components/header/header-action/index.ts create mode 100644 src/components/header/header-action/readme.md create mode 100644 src/components/header/header/header.e2e.ts rename src/components/{sbb-header/sbb-header.scss => header/header/header.scss} (96%) rename src/components/{sbb-header/sbb-header.spec.ts => header/header/header.spec.ts} (58%) rename src/components/{sbb-header/sbb-header.stories.tsx => header/header/header.stories.tsx} (92%) rename src/components/{sbb-header/sbb-header.tsx => header/header/header.ts} (59%) create mode 100644 src/components/header/header/index.ts rename src/components/{sbb-header => header/header}/readme.md (64%) create mode 100644 src/components/header/index.ts rename src/components/{sbb-icon/sbb-icon-request.ts => icon/icon-request.ts} (93%) rename src/components/{sbb-icon/sbb-icon-validate.spec.ts => icon/icon-validate.spec.ts} (75%) rename src/components/{sbb-icon/sbb-icon-validate.ts => icon/icon-validate.ts} (100%) create mode 100644 src/components/icon/icon.e2e.ts rename src/components/{sbb-icon/sbb-icon.scss => icon/icon.scss} (97%) create mode 100644 src/components/icon/icon.spec.ts rename src/components/{sbb-icon/sbb-icon.stories.tsx => icon/icon.stories.tsx} (81%) create mode 100644 src/components/icon/icon.ts create mode 100644 src/components/icon/index.ts create mode 100644 src/components/icon/readme.md create mode 100644 src/components/image/image.e2e.ts rename src/components/{sbb-image/sbb-image.helper.tsx => image/image.helper.ts} (60%) rename src/components/{sbb-image/sbb-image.scss => image/image.scss} (95%) rename src/components/{sbb-image/sbb-image.stories.tsx => image/image.stories.tsx} (87%) rename src/components/{sbb-image/sbb-image.tsx => image/image.ts} (73%) create mode 100644 src/components/image/index.ts create mode 100644 src/components/image/readme.md create mode 100644 src/components/journey-header/index.ts create mode 100644 src/components/journey-header/journey-header.e2e.ts rename src/components/{sbb-journey-header/sbb-journey-header.scss => journey-header/journey-header.scss} (95%) create mode 100644 src/components/journey-header/journey-header.spec.ts rename src/components/{sbb-journey-header/sbb-journey-header.stories.tsx => journey-header/journey-header.stories.tsx} (92%) create mode 100644 src/components/journey-header/journey-header.ts create mode 100644 src/components/journey-header/readme.md create mode 100644 src/components/journey-summary/index.ts create mode 100644 src/components/journey-summary/journey-summary.e2e.ts rename src/components/{sbb-journey-summary/sbb-journey-summary.scss => journey-summary/journey-summary.scss} (93%) create mode 100644 src/components/journey-summary/journey-summary.spec.ts rename src/components/{sbb-journey-summary/sbb-journey-summary.stories.tsx => journey-summary/journey-summary.stories.tsx} (95%) create mode 100644 src/components/journey-summary/journey-summary.ts create mode 100644 src/components/journey-summary/readme.md create mode 100644 src/components/link-list/index.ts create mode 100644 src/components/link-list/link-list.e2e.ts rename src/components/{sbb-link-list/sbb-link-list.scss => link-list/link-list.scss} (97%) rename src/components/{sbb-link-list/sbb-link-list.spec.ts => link-list/link-list.spec.ts} (65%) rename src/components/{sbb-link-list/sbb-link-list.stories.tsx => link-list/link-list.stories.tsx} (92%) create mode 100644 src/components/link-list/link-list.ts create mode 100644 src/components/link-list/readme.md create mode 100644 src/components/link/index.ts create mode 100644 src/components/link/link.e2e.ts rename src/components/{sbb-link/sbb-link.scss => link/link.scss} (87%) create mode 100644 src/components/link/link.spec.ts rename src/components/{sbb-link/sbb-link.stories.tsx => link/link.stories.tsx} (97%) create mode 100644 src/components/link/link.ts create mode 100644 src/components/link/readme.md create mode 100644 src/components/loading-indicator/index.ts create mode 100644 src/components/loading-indicator/loading-indicator.e2e.ts rename src/components/{sbb-loading-indicator/sbb-loading-indicator.scss => loading-indicator/loading-indicator.scss} (97%) create mode 100644 src/components/loading-indicator/loading-indicator.spec.ts rename src/components/{sbb-loading-indicator/sbb-loading-indicator.stories.tsx => loading-indicator/loading-indicator.stories.tsx} (94%) create mode 100644 src/components/loading-indicator/loading-indicator.ts create mode 100644 src/components/loading-indicator/readme.md create mode 100644 src/components/logo/index.ts rename src/components/{sbb-logo/sbb-logo.scss => logo/logo.scss} (95%) rename src/components/{sbb-logo/sbb-logo.stories.tsx => logo/logo.stories.tsx} (89%) rename src/components/{sbb-logo/sbb-logo.tsx => logo/logo.ts} (67%) create mode 100644 src/components/logo/readme.md create mode 100644 src/components/map-container/index.ts create mode 100644 src/components/map-container/map-container.e2e.ts rename src/components/{sbb-map-container/sbb-map-container.scss => map-container/map-container.scss} (98%) create mode 100644 src/components/map-container/map-container.spec.ts rename src/components/{sbb-map-container/sbb-map-container.stories.tsx => map-container/map-container.stories.tsx} (91%) create mode 100644 src/components/map-container/map-container.ts create mode 100644 src/components/map-container/readme.md create mode 100644 src/components/menu/index.ts create mode 100644 src/components/menu/menu-action/index.ts create mode 100644 src/components/menu/menu-action/menu-action.e2e.ts rename src/components/{sbb-menu-action/sbb-menu-action.scss => menu/menu-action/menu-action.scss} (94%) create mode 100644 src/components/menu/menu-action/menu-action.spec.ts rename src/components/{sbb-menu-action/sbb-menu-action.stories.tsx => menu/menu-action/menu-action.stories.tsx} (94%) create mode 100644 src/components/menu/menu-action/menu-action.ts create mode 100644 src/components/menu/menu-action/readme.md create mode 100644 src/components/menu/menu/index.ts create mode 100644 src/components/menu/menu/menu.e2e.ts rename src/components/{sbb-menu/sbb-menu.scss => menu/menu/menu.scss} (97%) rename src/components/{sbb-menu/sbb-menu.spec.ts => menu/menu/menu.spec.ts} (67%) rename src/components/{sbb-menu/sbb-menu.stories.tsx => menu/menu/menu.stories.tsx} (91%) rename src/components/{sbb-menu/sbb-menu.tsx => menu/menu/menu.ts} (59%) rename src/components/{sbb-menu => menu/menu}/readme.md (50%) create mode 100644 src/components/message/index.ts create mode 100644 src/components/message/message.e2e.ts rename src/components/{sbb-message/sbb-message.scss => message/message.scss} (96%) create mode 100644 src/components/message/message.spec.ts rename src/components/{sbb-message/sbb-message.stories.tsx => message/message.stories.tsx} (93%) create mode 100644 src/components/message/message.ts create mode 100644 src/components/message/readme.md create mode 100644 src/components/navigation/index.ts create mode 100644 src/components/navigation/navigation-action/index.ts create mode 100644 src/components/navigation/navigation-action/navigation-action.e2e.ts rename src/components/{sbb-navigation-action/sbb-navigation-action.scss => navigation/navigation-action/navigation-action.scss} (95%) create mode 100644 src/components/navigation/navigation-action/navigation-action.spec.ts rename src/components/{sbb-navigation-action/sbb-navigation-action.stories.tsx => navigation/navigation-action/navigation-action.stories.tsx} (92%) create mode 100644 src/components/navigation/navigation-action/navigation-action.ts create mode 100644 src/components/navigation/navigation-action/readme.md create mode 100644 src/components/navigation/navigation-list/index.ts create mode 100644 src/components/navigation/navigation-list/navigation-list.e2e.ts rename src/components/{sbb-navigation-list/sbb-navigation-list.scss => navigation/navigation-list/navigation-list.scss} (94%) create mode 100644 src/components/navigation/navigation-list/navigation-list.spec.ts rename src/components/{sbb-navigation-list/sbb-navigation-list.stories.tsx => navigation/navigation-list/navigation-list.stories.tsx} (90%) create mode 100644 src/components/navigation/navigation-list/navigation-list.ts create mode 100644 src/components/navigation/navigation-list/readme.md create mode 100644 src/components/navigation/navigation-marker/index.ts create mode 100644 src/components/navigation/navigation-marker/navigation-marker.e2e.ts rename src/components/{sbb-navigation-marker/sbb-navigation-marker.scss => navigation/navigation-marker/navigation-marker.scss} (98%) create mode 100644 src/components/navigation/navigation-marker/navigation-marker.spec.ts rename src/components/{sbb-navigation-marker/sbb-navigation-marker.stories.tsx => navigation/navigation-marker/navigation-marker.stories.tsx} (93%) create mode 100644 src/components/navigation/navigation-marker/navigation-marker.ts create mode 100644 src/components/navigation/navigation-marker/readme.md create mode 100644 src/components/navigation/navigation-section/index.ts create mode 100644 src/components/navigation/navigation-section/navigation-section.e2e.ts rename src/components/{sbb-navigation-section/sbb-navigation-section.scss => navigation/navigation-section/navigation-section.scss} (97%) create mode 100644 src/components/navigation/navigation-section/navigation-section.spec.ts rename src/components/{sbb-navigation-section/sbb-navigation-section.stories.tsx => navigation/navigation-section/navigation-section.stories.tsx} (90%) rename src/components/{sbb-navigation-section/sbb-navigation-section.tsx => navigation/navigation-section/navigation-section.ts} (63%) create mode 100644 src/components/navigation/navigation-section/readme.md create mode 100644 src/components/navigation/navigation/index.ts create mode 100644 src/components/navigation/navigation/navigation.e2e.ts rename src/components/{sbb-navigation/sbb-navigation.scss => navigation/navigation/navigation.scss} (97%) create mode 100644 src/components/navigation/navigation/navigation.spec.ts rename src/components/{sbb-navigation/sbb-navigation.stories.tsx => navigation/navigation/navigation.stories.tsx} (91%) rename src/components/{sbb-navigation/sbb-navigation.tsx => navigation/navigation/navigation.ts} (54%) create mode 100644 src/components/navigation/navigation/readme.md create mode 100644 src/components/notification/index.ts create mode 100644 src/components/notification/notification.e2e.ts rename src/components/{sbb-notification/sbb-notification.scss => notification/notification.scss} (97%) create mode 100644 src/components/notification/notification.spec.ts rename src/components/{sbb-notification/sbb-notification.stories.tsx => notification/notification.stories.tsx} (92%) create mode 100644 src/components/notification/notification.ts create mode 100644 src/components/notification/readme.md create mode 100644 src/components/option/index.ts create mode 100644 src/components/option/optgroup/index.ts create mode 100644 src/components/option/optgroup/optgroup.e2e.ts rename src/components/{sbb-optgroup/sbb-optgroup.scss => option/optgroup/optgroup.scss} (98%) create mode 100644 src/components/option/optgroup/optgroup.spec.ts rename src/components/{sbb-optgroup/sbb-optgroup.stories.tsx => option/optgroup/optgroup.stories.tsx} (92%) create mode 100644 src/components/option/optgroup/optgroup.ts rename src/components/{sbb-optgroup => option/optgroup}/readme.md (58%) create mode 100644 src/components/option/option/index.ts create mode 100644 src/components/option/option/option.e2e.ts rename src/components/{sbb-option/sbb-option.scss => option/option/option.scss} (88%) create mode 100644 src/components/option/option/option.spec.ts rename src/components/{sbb-option/sbb-option.stories.tsx => option/option/option.stories.tsx} (90%) create mode 100644 src/components/option/option/option.ts create mode 100644 src/components/option/option/readme.md create mode 100644 src/components/package.json create mode 100644 src/components/pearl-chain-time/index.ts rename src/components/{sbb-pearl-chain-time/sbb-pearl-chain-time.scss => pearl-chain-time/pearl-chain-time.scss} (94%) create mode 100644 src/components/pearl-chain-time/pearl-chain-time.spec.ts rename src/components/{sbb-pearl-chain-time/sbb-pearl-chain-time.stories.tsx => pearl-chain-time/pearl-chain-time.stories.tsx} (88%) create mode 100644 src/components/pearl-chain-time/pearl-chain-time.ts create mode 100644 src/components/pearl-chain-time/readme.md create mode 100644 src/components/pearl-chain-vertical-item/index.ts rename src/components/{sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.scss => pearl-chain-vertical-item/pearl-chain-vertical-item.scss} (98%) create mode 100644 src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.spec.ts rename src/components/{sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.stories.tsx => pearl-chain-vertical-item/pearl-chain-vertical-item.stories.tsx} (88%) create mode 100644 src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.ts create mode 100644 src/components/pearl-chain-vertical-item/readme.md create mode 100644 src/components/pearl-chain-vertical/index.ts create mode 100644 src/components/pearl-chain-vertical/pearl-chain-vertical.e2e.ts rename src/components/{sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.scss => pearl-chain-vertical/pearl-chain-vertical.scss} (90%) create mode 100644 src/components/pearl-chain-vertical/pearl-chain-vertical.spec.ts rename src/components/{sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.stories.tsx => pearl-chain-vertical/pearl-chain-vertical.stories.tsx} (98%) create mode 100644 src/components/pearl-chain-vertical/pearl-chain-vertical.ts rename src/components/{sbb-pearl-chain-vertical => pearl-chain-vertical}/readme.md (52%) create mode 100644 src/components/pearl-chain/index.ts create mode 100644 src/components/pearl-chain/pearl-chain.e2e.ts create mode 100644 src/components/pearl-chain/pearl-chain.sample-data.ts rename src/components/{sbb-pearl-chain/sbb-pearl-chain.scss => pearl-chain/pearl-chain.scss} (99%) create mode 100644 src/components/pearl-chain/pearl-chain.spec.ts rename src/components/{sbb-pearl-chain/sbb-pearl-chain.stories.tsx => pearl-chain/pearl-chain.stories.tsx} (93%) rename src/components/{sbb-pearl-chain/sbb-pearl-chain.tsx => pearl-chain/pearl-chain.ts} (75%) rename src/components/{sbb-pearl-chain => pearl-chain}/readme.md (54%) create mode 100644 src/components/radio-button/index.ts create mode 100644 src/components/radio-button/radio-button-group/index.ts create mode 100644 src/components/radio-button/radio-button-group/radio-button-group.e2e.ts rename src/components/{sbb-radio-button-group/sbb-radio-button-group.scss => radio-button/radio-button-group/radio-button-group.scss} (97%) create mode 100644 src/components/radio-button/radio-button-group/radio-button-group.spec.ts rename src/components/{sbb-radio-button-group/sbb-radio-button-group.stories.tsx => radio-button/radio-button-group/radio-button-group.stories.tsx} (92%) create mode 100644 src/components/radio-button/radio-button-group/radio-button-group.ts create mode 100644 src/components/radio-button/radio-button-group/readme.md create mode 100644 src/components/radio-button/radio-button/index.ts create mode 100644 src/components/radio-button/radio-button/radio-button.e2e.ts rename src/components/{sbb-radio-button/sbb-radio-button.scss => radio-button/radio-button/radio-button.scss} (97%) create mode 100644 src/components/radio-button/radio-button/radio-button.spec.ts rename src/components/{sbb-radio-button/sbb-radio-button.stories.tsx => radio-button/radio-button/radio-button.stories.tsx} (94%) create mode 100644 src/components/radio-button/radio-button/radio-button.ts create mode 100644 src/components/radio-button/radio-button/readme.md delete mode 100644 src/components/sbb-accordion/readme.md delete mode 100644 src/components/sbb-accordion/sbb-accordion.e2e.ts delete mode 100644 src/components/sbb-accordion/sbb-accordion.spec.ts delete mode 100644 src/components/sbb-accordion/sbb-accordion.tsx delete mode 100644 src/components/sbb-action-group/readme.md delete mode 100644 src/components/sbb-action-group/sbb-action-group.custom.d.ts delete mode 100644 src/components/sbb-action-group/sbb-action-group.e2e.ts delete mode 100644 src/components/sbb-action-group/sbb-action-group.spec.ts delete mode 100644 src/components/sbb-action-group/sbb-action-group.tsx delete mode 100644 src/components/sbb-alert-group/readme.md delete mode 100644 src/components/sbb-alert-group/sbb-alert-group.custom.d.ts delete mode 100644 src/components/sbb-alert-group/sbb-alert-group.e2e.ts delete mode 100644 src/components/sbb-alert-group/sbb-alert-group.events.ts delete mode 100644 src/components/sbb-alert-group/sbb-alert-group.spec.ts delete mode 100644 src/components/sbb-alert-group/sbb-alert-group.tsx delete mode 100644 src/components/sbb-alert/readme.md delete mode 100644 src/components/sbb-alert/sbb-alert.custom.d.ts delete mode 100644 src/components/sbb-alert/sbb-alert.e2e.ts delete mode 100644 src/components/sbb-alert/sbb-alert.events.ts delete mode 100644 src/components/sbb-alert/sbb-alert.spec.ts delete mode 100644 src/components/sbb-alert/sbb-alert.tsx delete mode 100644 src/components/sbb-autocomplete/sbb-autocomplete.e2e.ts delete mode 100644 src/components/sbb-autocomplete/sbb-autocomplete.events.ts delete mode 100644 src/components/sbb-autocomplete/sbb-autocomplete.spec.ts delete mode 100644 src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.e2e.ts delete mode 100644 src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.spec.ts delete mode 100644 src/components/sbb-breadcrumb/readme.md delete mode 100644 src/components/sbb-breadcrumb/sbb-breadcrumb.e2e.ts delete mode 100644 src/components/sbb-breadcrumb/sbb-breadcrumb.spec.ts delete mode 100644 src/components/sbb-breadcrumb/sbb-breadcrumb.tsx delete mode 100644 src/components/sbb-button/readme.md delete mode 100644 src/components/sbb-button/sbb-button.custom.d.ts delete mode 100644 src/components/sbb-button/sbb-button.e2e.ts delete mode 100644 src/components/sbb-button/sbb-button.spec.ts delete mode 100644 src/components/sbb-button/sbb-button.tsx delete mode 100644 src/components/sbb-calendar/sbb-calendar.custom.d.ts delete mode 100644 src/components/sbb-calendar/sbb-calendar.e2e.ts delete mode 100644 src/components/sbb-calendar/sbb-calendar.events.ts delete mode 100644 src/components/sbb-card-action/readme.md delete mode 100644 src/components/sbb-card-action/sbb-card-action.e2e.ts delete mode 100644 src/components/sbb-card-action/sbb-card-action.tsx delete mode 100644 src/components/sbb-card-badge/readme.md delete mode 100644 src/components/sbb-card-badge/sbb-card-badge.custom.d.ts delete mode 100644 src/components/sbb-card-badge/sbb-card-badge.e2e.ts delete mode 100644 src/components/sbb-card-badge/sbb-card-badge.spec.ts delete mode 100644 src/components/sbb-card-badge/sbb-card-badge.tsx delete mode 100644 src/components/sbb-card/sbb-card.custom.d.ts delete mode 100644 src/components/sbb-card/sbb-card.e2e.ts delete mode 100644 src/components/sbb-card/sbb-card.tsx delete mode 100644 src/components/sbb-checkbox-group/readme.md delete mode 100644 src/components/sbb-checkbox-group/sbb-checkbox-group.custom.ts delete mode 100644 src/components/sbb-checkbox-group/sbb-checkbox-group.e2e.ts delete mode 100644 src/components/sbb-checkbox-group/sbb-checkbox-group.spec.ts delete mode 100644 src/components/sbb-checkbox-group/sbb-checkbox-group.tsx delete mode 100644 src/components/sbb-checkbox/readme.md delete mode 100644 src/components/sbb-checkbox/sbb-checkbox.custom.d.ts delete mode 100644 src/components/sbb-checkbox/sbb-checkbox.e2e.ts delete mode 100644 src/components/sbb-checkbox/sbb-checkbox.events.ts delete mode 100644 src/components/sbb-checkbox/sbb-checkbox.spec.ts delete mode 100644 src/components/sbb-checkbox/sbb-checkbox.tsx delete mode 100644 src/components/sbb-chip/readme.md delete mode 100644 src/components/sbb-chip/sbb-chip.custom.d.ts delete mode 100644 src/components/sbb-chip/sbb-chip.e2e.ts delete mode 100644 src/components/sbb-chip/sbb-chip.spec.ts delete mode 100644 src/components/sbb-chip/sbb-chip.tsx delete mode 100644 src/components/sbb-clock/sbb-clock.e2e.ts delete mode 100644 src/components/sbb-clock/sbb-clock.spec.ts delete mode 100644 src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.e2e.ts delete mode 100644 src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.spec.ts delete mode 100644 src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.e2e.ts delete mode 100644 src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.spec.ts delete mode 100644 src/components/sbb-datepicker-toggle/readme.md delete mode 100644 src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.e2e.ts delete mode 100644 src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.spec.ts delete mode 100644 src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.tsx delete mode 100644 src/components/sbb-datepicker/sbb-datepicker.e2e.ts delete mode 100644 src/components/sbb-datepicker/sbb-datepicker.events.ts delete mode 100644 src/components/sbb-datepicker/sbb-datepicker.helper.ts delete mode 100644 src/components/sbb-datepicker/sbb-datepicker.spec.ts delete mode 100644 src/components/sbb-datepicker/sbb-datepicker.tsx delete mode 100644 src/components/sbb-dialog/readme.md delete mode 100644 src/components/sbb-dialog/sbb-dialog.e2e.ts delete mode 100644 src/components/sbb-dialog/sbb-dialog.events.ts delete mode 100644 src/components/sbb-dialog/sbb-dialog.spec.ts delete mode 100644 src/components/sbb-divider/readme.md delete mode 100644 src/components/sbb-divider/sbb-divider.custom.d.ts delete mode 100644 src/components/sbb-divider/sbb-divider.e2e.ts delete mode 100644 src/components/sbb-divider/sbb-divider.spec.ts delete mode 100644 src/components/sbb-divider/sbb-divider.tsx delete mode 100644 src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.e2e.ts delete mode 100644 src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.spec.ts delete mode 100644 src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.tsx delete mode 100644 src/components/sbb-expansion-panel-header/readme.md delete mode 100644 src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.e2e.ts delete mode 100644 src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.events.ts delete mode 100644 src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.spec.ts delete mode 100644 src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.tsx delete mode 100644 src/components/sbb-expansion-panel/readme.md delete mode 100644 src/components/sbb-expansion-panel/sbb-expansion-panel.custom.d.ts delete mode 100644 src/components/sbb-expansion-panel/sbb-expansion-panel.e2e.ts delete mode 100644 src/components/sbb-expansion-panel/sbb-expansion-panel.events.ts delete mode 100644 src/components/sbb-expansion-panel/sbb-expansion-panel.spec.ts delete mode 100644 src/components/sbb-expansion-panel/sbb-expansion-panel.tsx delete mode 100644 src/components/sbb-file-selector/readme.md delete mode 100644 src/components/sbb-file-selector/sbb-file-selector.custom.d.ts delete mode 100644 src/components/sbb-file-selector/sbb-file-selector.e2e.ts delete mode 100644 src/components/sbb-file-selector/sbb-file-selector.events.ts delete mode 100644 src/components/sbb-file-selector/sbb-file-selector.spec.ts delete mode 100644 src/components/sbb-footer/sbb-footer.custom.d.ts delete mode 100644 src/components/sbb-footer/sbb-footer.e2e.ts delete mode 100644 src/components/sbb-footer/sbb-footer.spec.ts delete mode 100644 src/components/sbb-footer/sbb-footer.tsx delete mode 100644 src/components/sbb-form-error/readme.md delete mode 100644 src/components/sbb-form-error/sbb-form-error.e2e.ts delete mode 100644 src/components/sbb-form-error/sbb-form-error.spec.ts delete mode 100644 src/components/sbb-form-error/sbb-form-error.tsx delete mode 100644 src/components/sbb-form-field-clear/readme.md delete mode 100644 src/components/sbb-form-field-clear/sbb-form-field-clear.e2e.ts delete mode 100644 src/components/sbb-form-field-clear/sbb-form-field-clear.spec.ts delete mode 100644 src/components/sbb-form-field-clear/sbb-form-field-clear.tsx delete mode 100644 src/components/sbb-form-field/readme.md delete mode 100644 src/components/sbb-form-field/sbb-form-field.custom.d.ts delete mode 100644 src/components/sbb-form-field/sbb-form-field.e2e.ts delete mode 100644 src/components/sbb-form-field/sbb-form-field.spec.ts delete mode 100644 src/components/sbb-header-action/readme.md delete mode 100644 src/components/sbb-header-action/sbb-header-action.custom.d.ts delete mode 100644 src/components/sbb-header-action/sbb-header-action.e2e.ts delete mode 100644 src/components/sbb-header-action/sbb-header-action.tsx delete mode 100644 src/components/sbb-header/sbb-header.e2e.ts delete mode 100644 src/components/sbb-icon/readme.md delete mode 100644 src/components/sbb-icon/sbb-icon.e2e.ts delete mode 100644 src/components/sbb-icon/sbb-icon.spec.ts delete mode 100644 src/components/sbb-icon/sbb-icon.tsx delete mode 100644 src/components/sbb-image/readme.md delete mode 100644 src/components/sbb-image/sbb-image.custom.d.ts delete mode 100644 src/components/sbb-image/sbb-image.e2e.ts delete mode 100644 src/components/sbb-journey-header/readme.md delete mode 100644 src/components/sbb-journey-header/sbb-journey-header.custom.d.ts delete mode 100644 src/components/sbb-journey-header/sbb-journey-header.e2e.ts delete mode 100644 src/components/sbb-journey-header/sbb-journey-header.spec.ts delete mode 100644 src/components/sbb-journey-header/sbb-journey-header.tsx delete mode 100644 src/components/sbb-journey-summary/readme.md delete mode 100644 src/components/sbb-journey-summary/sbb-journey-summary.custom.d.ts delete mode 100644 src/components/sbb-journey-summary/sbb-journey-summary.e2e.ts delete mode 100644 src/components/sbb-journey-summary/sbb-journey-summary.spec.ts delete mode 100644 src/components/sbb-journey-summary/sbb-journey-summary.tsx delete mode 100644 src/components/sbb-link-list/readme.md delete mode 100644 src/components/sbb-link-list/sbb-link-list.custom.d.ts delete mode 100644 src/components/sbb-link-list/sbb-link-list.e2e.ts delete mode 100644 src/components/sbb-link-list/sbb-link-list.tsx delete mode 100644 src/components/sbb-link/readme.md delete mode 100644 src/components/sbb-link/sbb-link.custom.d.ts delete mode 100644 src/components/sbb-link/sbb-link.e2e.ts delete mode 100644 src/components/sbb-link/sbb-link.spec.ts delete mode 100644 src/components/sbb-link/sbb-link.tsx delete mode 100644 src/components/sbb-loading-indicator/readme.md delete mode 100644 src/components/sbb-loading-indicator/sbb-loading-indicator.custom.d.ts delete mode 100644 src/components/sbb-loading-indicator/sbb-loading-indicator.e2e.ts delete mode 100644 src/components/sbb-loading-indicator/sbb-loading-indicator.spec.ts delete mode 100644 src/components/sbb-loading-indicator/sbb-loading-indicator.tsx delete mode 100644 src/components/sbb-logo/readme.md delete mode 100644 src/components/sbb-logo/sbb-logo.custom.d.ts delete mode 100644 src/components/sbb-map-container/readme.md delete mode 100644 src/components/sbb-map-container/sbb-map-container.e2e.ts delete mode 100644 src/components/sbb-map-container/sbb-map-container.spec.ts delete mode 100644 src/components/sbb-map-container/sbb-map-container.tsx delete mode 100644 src/components/sbb-menu-action/readme.md delete mode 100644 src/components/sbb-menu-action/sbb-menu-action.e2e.ts delete mode 100644 src/components/sbb-menu-action/sbb-menu-action.spec.ts delete mode 100644 src/components/sbb-menu-action/sbb-menu-action.tsx delete mode 100644 src/components/sbb-menu/sbb-menu.e2e.ts delete mode 100644 src/components/sbb-menu/sbb-menu.events.ts delete mode 100644 src/components/sbb-message/readme.md delete mode 100644 src/components/sbb-message/sbb-message.e2e.ts delete mode 100644 src/components/sbb-message/sbb-message.spec.ts delete mode 100644 src/components/sbb-message/sbb-message.tsx delete mode 100644 src/components/sbb-navigation-action/readme.md delete mode 100644 src/components/sbb-navigation-action/sbb-navigation-action.e2e.ts delete mode 100644 src/components/sbb-navigation-action/sbb-navigation-action.spec.ts delete mode 100644 src/components/sbb-navigation-action/sbb-navigation-action.tsx delete mode 100644 src/components/sbb-navigation-list/readme.md delete mode 100644 src/components/sbb-navigation-list/sbb-navigation-list.e2e.ts delete mode 100644 src/components/sbb-navigation-list/sbb-navigation-list.spec.ts delete mode 100644 src/components/sbb-navigation-list/sbb-navigation-list.tsx delete mode 100644 src/components/sbb-navigation-marker/readme.md delete mode 100644 src/components/sbb-navigation-marker/sbb-navigation-marker.e2e.ts delete mode 100644 src/components/sbb-navigation-marker/sbb-navigation-marker.spec.ts delete mode 100644 src/components/sbb-navigation-marker/sbb-navigation-marker.tsx delete mode 100644 src/components/sbb-navigation-section/readme.md delete mode 100644 src/components/sbb-navigation-section/sbb-navigation-section.e2e.ts delete mode 100644 src/components/sbb-navigation-section/sbb-navigation-section.spec.ts delete mode 100644 src/components/sbb-navigation/readme.md delete mode 100644 src/components/sbb-navigation/sbb-navigation.e2e.ts delete mode 100644 src/components/sbb-navigation/sbb-navigation.events.ts delete mode 100644 src/components/sbb-navigation/sbb-navigation.spec.ts delete mode 100644 src/components/sbb-notification/readme.md delete mode 100644 src/components/sbb-notification/sbb-notification.custom.d.ts delete mode 100644 src/components/sbb-notification/sbb-notification.e2e.ts delete mode 100644 src/components/sbb-notification/sbb-notification.events.ts delete mode 100644 src/components/sbb-notification/sbb-notification.spec.ts delete mode 100644 src/components/sbb-notification/sbb-notification.tsx delete mode 100644 src/components/sbb-optgroup/sbb-optgroup.e2e.ts delete mode 100644 src/components/sbb-optgroup/sbb-optgroup.spec.ts delete mode 100644 src/components/sbb-optgroup/sbb-optgroup.tsx delete mode 100644 src/components/sbb-option/readme.md delete mode 100644 src/components/sbb-option/sbb-option.custom.d.ts delete mode 100644 src/components/sbb-option/sbb-option.e2e.ts delete mode 100644 src/components/sbb-option/sbb-option.events.ts delete mode 100644 src/components/sbb-option/sbb-option.spec.ts delete mode 100644 src/components/sbb-option/sbb-option.tsx delete mode 100644 src/components/sbb-pearl-chain-time/readme.md delete mode 100644 src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.helper.spec.tsx delete mode 100644 src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.spec.ts delete mode 100644 src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.tsx delete mode 100644 src/components/sbb-pearl-chain-vertical-item/readme.md delete mode 100644 src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.custom.d.ts delete mode 100644 src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.spec.ts delete mode 100644 src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.tsx delete mode 100644 src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.e2e.ts delete mode 100644 src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.spec.ts delete mode 100644 src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.tsx delete mode 100644 src/components/sbb-pearl-chain/sbb-pearl-chain.e2e.ts delete mode 100644 src/components/sbb-pearl-chain/sbb-pearl-chain.sample-data.js delete mode 100644 src/components/sbb-pearl-chain/sbb-pearl-chain.spec.ts delete mode 100644 src/components/sbb-radio-button-group/readme.md delete mode 100644 src/components/sbb-radio-button-group/sbb-radio-button-group.custom.d.ts delete mode 100644 src/components/sbb-radio-button-group/sbb-radio-button-group.e2e.ts delete mode 100644 src/components/sbb-radio-button-group/sbb-radio-button-group.events.ts delete mode 100644 src/components/sbb-radio-button-group/sbb-radio-button-group.spec.ts delete mode 100644 src/components/sbb-radio-button-group/sbb-radio-button-group.tsx delete mode 100644 src/components/sbb-radio-button/readme.md delete mode 100644 src/components/sbb-radio-button/sbb-radio-button.custom.d.ts delete mode 100644 src/components/sbb-radio-button/sbb-radio-button.e2e.ts delete mode 100644 src/components/sbb-radio-button/sbb-radio-button.events.ts delete mode 100644 src/components/sbb-radio-button/sbb-radio-button.spec.ts delete mode 100644 src/components/sbb-radio-button/sbb-radio-button.tsx delete mode 100644 src/components/sbb-select/sbb-select.custom.d.ts delete mode 100644 src/components/sbb-select/sbb-select.e2e.ts delete mode 100644 src/components/sbb-select/sbb-select.events.ts delete mode 100644 src/components/sbb-select/sbb-select.spec.ts delete mode 100644 src/components/sbb-selection-panel/readme.md delete mode 100644 src/components/sbb-selection-panel/sbb-selection-panel.custom.d.ts delete mode 100644 src/components/sbb-selection-panel/sbb-selection-panel.e2e.ts delete mode 100644 src/components/sbb-selection-panel/sbb-selection-panel.events.ts delete mode 100644 src/components/sbb-selection-panel/sbb-selection-panel.spec.ts delete mode 100644 src/components/sbb-selection-panel/sbb-selection-panel.tsx delete mode 100644 src/components/sbb-signet/readme.md delete mode 100644 src/components/sbb-signet/sbb-signet.custom.d.ts delete mode 100644 src/components/sbb-signet/sbb-signet.tsx delete mode 100644 src/components/sbb-skiplink-list/readme.md delete mode 100644 src/components/sbb-skiplink-list/sbb-skiplink-list.e2e.ts delete mode 100644 src/components/sbb-skiplink-list/sbb-skiplink-list.spec.ts delete mode 100644 src/components/sbb-skiplink-list/sbb-skiplink-list.tsx delete mode 100644 src/components/sbb-slider/readme.md delete mode 100644 src/components/sbb-slider/sbb-slider.e2e.ts delete mode 100644 src/components/sbb-slider/sbb-slider.events.ts delete mode 100644 src/components/sbb-slider/sbb-slider.spec.ts delete mode 100644 src/components/sbb-slider/sbb-slider.tsx delete mode 100644 src/components/sbb-tab-group/readme.md delete mode 100644 src/components/sbb-tab-group/sbb-tab-group.custom.d.ts delete mode 100644 src/components/sbb-tab-group/sbb-tab-group.e2e.ts delete mode 100644 src/components/sbb-tab-group/sbb-tab-group.events.ts delete mode 100644 src/components/sbb-tab-group/sbb-tab-group.spec.ts delete mode 100644 src/components/sbb-tab-title/readme.md delete mode 100644 src/components/sbb-tab-title/sbb-tab-title.e2e.ts delete mode 100644 src/components/sbb-tab-title/sbb-tab-title.spec.ts delete mode 100644 src/components/sbb-tab-title/sbb-tab-title.tsx delete mode 100644 src/components/sbb-tag-group/sbb-tag-group.e2e.ts delete mode 100644 src/components/sbb-tag/readme.md delete mode 100644 src/components/sbb-tag/sbb-tag.custom.d.ts delete mode 100644 src/components/sbb-tag/sbb-tag.e2e.ts delete mode 100644 src/components/sbb-tag/sbb-tag.events.ts delete mode 100644 src/components/sbb-tag/sbb-tag.spec.ts delete mode 100644 src/components/sbb-tag/sbb-tag.tsx delete mode 100644 src/components/sbb-teaser-hero/readme.md delete mode 100644 src/components/sbb-teaser-hero/sbb-teaser-hero.e2e.ts delete mode 100644 src/components/sbb-teaser-hero/sbb-teaser-hero.spec.ts delete mode 100644 src/components/sbb-teaser-hero/sbb-teaser-hero.tsx delete mode 100644 src/components/sbb-teaser/readme.md delete mode 100644 src/components/sbb-teaser/sbb-teaser.e2e.ts delete mode 100644 src/components/sbb-teaser/sbb-teaser.spec.ts delete mode 100644 src/components/sbb-teaser/sbb-teaser.tsx delete mode 100644 src/components/sbb-time-input/sbb-time-input.e2e.ts delete mode 100644 src/components/sbb-time-input/sbb-time-input.events.ts delete mode 100644 src/components/sbb-time-input/sbb-time-input.spec.ts delete mode 100644 src/components/sbb-timetable-barrier-free/readme.md delete mode 100644 src/components/sbb-timetable-barrier-free/sbb-timetable-barrier-free.e2e.ts delete mode 100644 src/components/sbb-timetable-barrier-free/sbb-timetable-barrier-free.tsx delete mode 100644 src/components/sbb-timetable-duration/readme.md delete mode 100644 src/components/sbb-timetable-duration/sbb-timetable-duration.e2e.ts delete mode 100644 src/components/sbb-timetable-occupancy/readme.md delete mode 100644 src/components/sbb-timetable-occupancy/sbb-timetable-occupancy.e2e.ts delete mode 100644 src/components/sbb-timetable-occupancy/sbb-timetable-occupancy.tsx delete mode 100644 src/components/sbb-timetable-park-and-rail/readme.md delete mode 100644 src/components/sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.custom.d.ts delete mode 100644 src/components/sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.e2e.ts delete mode 100644 src/components/sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.tsx delete mode 100644 src/components/sbb-timetable-row-column-headers/readme.md delete mode 100644 src/components/sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.e2e.ts delete mode 100644 src/components/sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.tsx delete mode 100644 src/components/sbb-timetable-row-day-change/readme.md delete mode 100644 src/components/sbb-timetable-row-day-change/sbb-timetable-row-day-change.e2e.ts delete mode 100644 src/components/sbb-timetable-row-day-change/sbb-timetable-row-day-change.tsx delete mode 100644 src/components/sbb-timetable-row-header/readme.md delete mode 100644 src/components/sbb-timetable-row-header/sbb-timetable-row-header.e2e.ts delete mode 100644 src/components/sbb-timetable-row-header/sbb-timetable-row-header.tsx delete mode 100644 src/components/sbb-timetable-row/readme.md delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.custom.d.ts delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.e2e.ts delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.helper.spec.ts delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.helper.tsx delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.spec.ts delete mode 100644 src/components/sbb-timetable-row/sbb-timetable-row.tsx delete mode 100644 src/components/sbb-timetable-transportation-number/readme.md delete mode 100644 src/components/sbb-timetable-transportation-number/sbb-timetable-transportation-number.custom.d.ts delete mode 100644 src/components/sbb-timetable-transportation-number/sbb-timetable-transportation-number.e2e.ts delete mode 100644 src/components/sbb-timetable-transportation-number/sbb-timetable-transportation-number.tsx delete mode 100644 src/components/sbb-timetable-transportation-time/readme.md delete mode 100644 src/components/sbb-timetable-transportation-time/sbb-timetable-transportation-time.custom.d.ts delete mode 100644 src/components/sbb-timetable-transportation-time/sbb-timetable-transportation-time.e2e.ts delete mode 100644 src/components/sbb-timetable-transportation-time/sbb-timetable-transportation-time.tsx delete mode 100644 src/components/sbb-timetable-travel-hints/readme.md delete mode 100644 src/components/sbb-timetable-travel-hints/sbb-timetable-travel-hints.custom.d.ts delete mode 100644 src/components/sbb-timetable-travel-hints/sbb-timetable-travel-hints.e2e.ts delete mode 100644 src/components/sbb-timetable-travel-hints/sbb-timetable-travel-hints.tsx delete mode 100644 src/components/sbb-title/readme.md delete mode 100644 src/components/sbb-title/sbb-title.custom.d.ts delete mode 100644 src/components/sbb-title/sbb-title.e2e.ts delete mode 100644 src/components/sbb-title/sbb-title.spec.ts delete mode 100644 src/components/sbb-title/sbb-title.tsx delete mode 100644 src/components/sbb-toast/readme.md delete mode 100644 src/components/sbb-toast/sbb-toast.custom.d.ts delete mode 100644 src/components/sbb-toast/sbb-toast.e2e.ts delete mode 100644 src/components/sbb-toast/sbb-toast.events.ts delete mode 100644 src/components/sbb-toast/sbb-toast.spec.ts delete mode 100644 src/components/sbb-toast/sbb-toast.tsx delete mode 100644 src/components/sbb-toggle-check/readme.md delete mode 100644 src/components/sbb-toggle-check/sbb-toggle-check.custom.d.ts delete mode 100644 src/components/sbb-toggle-check/sbb-toggle-check.e2e.ts delete mode 100644 src/components/sbb-toggle-check/sbb-toggle-check.events.ts delete mode 100644 src/components/sbb-toggle-check/sbb-toggle-check.spec.ts delete mode 100644 src/components/sbb-toggle-check/sbb-toggle-check.tsx delete mode 100644 src/components/sbb-toggle-option/readme.md delete mode 100644 src/components/sbb-toggle-option/sbb-toggle-option.custom.d.ts delete mode 100644 src/components/sbb-toggle-option/sbb-toggle-option.e2e.ts delete mode 100644 src/components/sbb-toggle-option/sbb-toggle-option.events.ts delete mode 100644 src/components/sbb-toggle-option/sbb-toggle-option.spec.ts delete mode 100644 src/components/sbb-toggle-option/sbb-toggle-option.tsx delete mode 100644 src/components/sbb-toggle/readme.md delete mode 100644 src/components/sbb-toggle/sbb-toggle.custom.d.ts delete mode 100644 src/components/sbb-toggle/sbb-toggle.e2e.ts delete mode 100644 src/components/sbb-toggle/sbb-toggle.events.ts delete mode 100644 src/components/sbb-toggle/sbb-toggle.spec.ts delete mode 100644 src/components/sbb-toggle/sbb-toggle.tsx delete mode 100644 src/components/sbb-tooltip-trigger/readme.md delete mode 100644 src/components/sbb-tooltip-trigger/sbb-tooltip-trigger.e2e.ts delete mode 100644 src/components/sbb-tooltip-trigger/sbb-tooltip-trigger.spec.ts delete mode 100644 src/components/sbb-tooltip-trigger/sbb-tooltip-trigger.tsx delete mode 100644 src/components/sbb-tooltip/readme.md delete mode 100644 src/components/sbb-tooltip/sbb-tooltip.e2e.ts delete mode 100644 src/components/sbb-tooltip/sbb-tooltip.events.ts delete mode 100644 src/components/sbb-tooltip/sbb-tooltip.spec.ts delete mode 100644 src/components/sbb-train-blocked-passage/sbb-train-blocked-passage.e2e.ts delete mode 100644 src/components/sbb-train-blocked-passage/sbb-train-blocked-passage.spec.ts delete mode 100644 src/components/sbb-train-blocked-passage/sbb-train-blocked-passage.tsx delete mode 100644 src/components/sbb-train-formation/sbb-train-formation.spec.ts delete mode 100644 src/components/sbb-train-formation/sbb-train-formation.tsx delete mode 100644 src/components/sbb-train-wagon/readme.md delete mode 100644 src/components/sbb-train-wagon/sbb-train-wagon.custom.d.ts delete mode 100644 src/components/sbb-train-wagon/sbb-train-wagon.e2e.ts delete mode 100644 src/components/sbb-train-wagon/sbb-train-wagon.events.ts delete mode 100644 src/components/sbb-train-wagon/sbb-train-wagon.spec.ts delete mode 100644 src/components/sbb-train-wagon/sbb-train-wagon.tsx delete mode 100644 src/components/sbb-train/readme.md delete mode 100644 src/components/sbb-train/sbb-train.custom.d.ts delete mode 100644 src/components/sbb-train/sbb-train.e2e.ts delete mode 100644 src/components/sbb-train/sbb-train.events.ts delete mode 100644 src/components/sbb-train/sbb-train.spec.ts delete mode 100644 src/components/sbb-train/sbb-train.tsx delete mode 100644 src/components/sbb-visual-checkbox/readme.md delete mode 100644 src/components/sbb-visual-checkbox/sbb-visual-checkbox.e2e.ts delete mode 100644 src/components/sbb-visual-checkbox/sbb-visual-checkbox.spec.ts delete mode 100644 src/components/sbb-visual-checkbox/sbb-visual-checkbox.tsx create mode 100644 src/components/select/index.ts rename src/components/{sbb-select => select}/readme.md (52%) create mode 100644 src/components/select/select.e2e.ts rename src/components/{sbb-select/sbb-select.scss => select/select.scss} (91%) create mode 100644 src/components/select/select.spec.ts rename src/components/{sbb-select/sbb-select.stories.tsx => select/select.stories.tsx} (95%) rename src/components/{sbb-select/sbb-select.tsx => select/select.ts} (61%) create mode 100644 src/components/selection-panel/index.ts create mode 100644 src/components/selection-panel/readme.md create mode 100644 src/components/selection-panel/selection-panel.e2e.ts rename src/components/{sbb-selection-panel/sbb-selection-panel.scss => selection-panel/selection-panel.scss} (94%) create mode 100644 src/components/selection-panel/selection-panel.spec.ts rename src/components/{sbb-selection-panel/sbb-selection-panel.stories.tsx => selection-panel/selection-panel.stories.tsx} (94%) create mode 100644 src/components/selection-panel/selection-panel.ts create mode 100644 src/components/signet/readme.md rename src/components/{sbb-signet/sbb-signet.scss => signet/signet.scss} (97%) rename src/components/{sbb-signet/sbb-signet.stories.tsx => signet/signet.stories.tsx} (86%) create mode 100644 src/components/signet/signet.ts create mode 100644 src/components/skiplink-list/index.ts create mode 100644 src/components/skiplink-list/readme.md create mode 100644 src/components/skiplink-list/skiplink-list.e2e.ts rename src/components/{sbb-skiplink-list/sbb-skiplink-list.scss => skiplink-list/skiplink-list.scss} (97%) create mode 100644 src/components/skiplink-list/skiplink-list.spec.ts rename src/components/{sbb-skiplink-list/sbb-skiplink-list.stories.tsx => skiplink-list/skiplink-list.stories.tsx} (90%) create mode 100644 src/components/skiplink-list/skiplink-list.ts create mode 100644 src/components/slider/index.ts create mode 100644 src/components/slider/readme.md create mode 100644 src/components/slider/slider.e2e.ts rename src/components/{sbb-slider/sbb-slider.scss => slider/slider.scss} (92%) create mode 100644 src/components/slider/slider.spec.ts rename src/components/{sbb-slider/sbb-slider.stories.tsx => slider/slider.stories.tsx} (93%) create mode 100644 src/components/slider/slider.ts create mode 100644 src/components/tabs/index.ts create mode 100644 src/components/tabs/tab-group/index.ts create mode 100644 src/components/tabs/tab-group/readme.md create mode 100644 src/components/tabs/tab-group/tab-group.e2e.ts rename src/components/{sbb-tab-group/sbb-tab-group.scss => tabs/tab-group/tab-group.scss} (81%) create mode 100644 src/components/tabs/tab-group/tab-group.spec.ts rename src/components/{sbb-tab-group/sbb-tab-group.stories.tsx => tabs/tab-group/tab-group.stories.tsx} (95%) rename src/components/{sbb-tab-group/sbb-tab-group.tsx => tabs/tab-group/tab-group.ts} (71%) create mode 100644 src/components/tabs/tab-title/index.ts create mode 100644 src/components/tabs/tab-title/readme.md create mode 100644 src/components/tabs/tab-title/tab-title.e2e.ts rename src/components/{sbb-tab-title/sbb-tab-title.scss => tabs/tab-title/tab-title.scss} (96%) create mode 100644 src/components/tabs/tab-title/tab-title.spec.ts rename src/components/{sbb-tab-title/sbb-tab-title.stories.tsx => tabs/tab-title/tab-title.stories.tsx} (94%) create mode 100644 src/components/tabs/tab-title/tab-title.ts create mode 100644 src/components/tag/index.ts create mode 100644 src/components/tag/tag-group/index.ts rename src/components/{sbb-tag-group => tag/tag-group}/readme.md (60%) create mode 100644 src/components/tag/tag-group/tag-group.e2e.ts rename src/components/{sbb-tag-group/sbb-tag-group.scss => tag/tag-group/tag-group.scss} (91%) create mode 100644 src/components/tag/tag-group/tag-group.spec.ts rename src/components/{sbb-tag-group/sbb-tag-group.stories.tsx => tag/tag-group/tag-group.stories.tsx} (95%) rename src/components/{sbb-tag-group/sbb-tag-group.tsx => tag/tag-group/tag-group.ts} (50%) create mode 100644 src/components/tag/tag/index.ts create mode 100644 src/components/tag/tag/readme.md create mode 100644 src/components/tag/tag/tag.e2e.ts rename src/components/{sbb-tag/sbb-tag.scss => tag/tag/tag.scss} (91%) create mode 100644 src/components/tag/tag/tag.spec.ts rename src/components/{sbb-tag/sbb-tag.stories.tsx => tag/tag/tag.stories.tsx} (93%) create mode 100644 src/components/tag/tag/tag.ts create mode 100644 src/components/teaser-hero/index.ts create mode 100644 src/components/teaser-hero/readme.md create mode 100644 src/components/teaser-hero/teaser-hero.e2e.ts rename src/components/{sbb-teaser-hero/sbb-teaser-hero.scss => teaser-hero/teaser-hero.scss} (97%) create mode 100644 src/components/teaser-hero/teaser-hero.spec.ts rename src/components/{sbb-teaser-hero/sbb-teaser-hero.stories.tsx => teaser-hero/teaser-hero.stories.tsx} (91%) create mode 100644 src/components/teaser-hero/teaser-hero.ts create mode 100644 src/components/teaser/index.ts create mode 100644 src/components/teaser/readme.md rename src/components/{sbb-teaser => teaser}/stories/placeholder.png (100%) create mode 100644 src/components/teaser/teaser.e2e.ts rename src/components/{sbb-teaser/sbb-teaser.scss => teaser/teaser.scss} (97%) create mode 100644 src/components/teaser/teaser.spec.ts rename src/components/{sbb-teaser/sbb-teaser.stories.tsx => teaser/teaser.stories.tsx} (94%) create mode 100644 src/components/teaser/teaser.ts create mode 100644 src/components/time-input/index.ts rename src/components/{sbb-time-input => time-input}/readme.md (50%) create mode 100644 src/components/time-input/time-input.e2e.ts rename src/components/{sbb-time-input/sbb-time-input.scss => time-input/time-input.scss} (65%) create mode 100644 src/components/time-input/time-input.spec.ts rename src/components/{sbb-time-input/sbb-time-input.stories.tsx => time-input/time-input.stories.tsx} (90%) rename src/components/{sbb-time-input/sbb-time-input.tsx => time-input/time-input.ts} (70%) create mode 100644 src/components/timetable-barrier-free/readme.md create mode 100644 src/components/timetable-barrier-free/timetable-barrier-free.e2e.ts rename src/components/{sbb-timetable-barrier-free/sbb-timetable-barrier-free.sample-data.js => timetable-barrier-free/timetable-barrier-free.sample-data.js} (100%) rename src/components/{sbb-timetable-barrier-free/sbb-timetable-barrier-free.scss => timetable-barrier-free/timetable-barrier-free.scss} (96%) rename src/components/{sbb-timetable-barrier-free/sbb-timetable-barrier-free.spec.ts => timetable-barrier-free/timetable-barrier-free.spec.ts} (79%) rename src/components/{sbb-timetable-barrier-free/sbb-timetable-barrier-free.stories.tsx => timetable-barrier-free/timetable-barrier-free.stories.tsx} (82%) create mode 100644 src/components/timetable-barrier-free/timetable-barrier-free.ts create mode 100644 src/components/timetable-duration/readme.md create mode 100644 src/components/timetable-duration/timetable-duration.e2e.ts rename src/components/{sbb-timetable-duration/sbb-timetable-duration.sample-data.js => timetable-duration/timetable-duration.sample-data.js} (100%) rename src/components/{sbb-timetable-duration/sbb-timetable-duration.scss => timetable-duration/timetable-duration.scss} (81%) rename src/components/{sbb-timetable-duration/sbb-timetable-duration.spec.ts => timetable-duration/timetable-duration.spec.ts} (63%) rename src/components/{sbb-timetable-duration/sbb-timetable-duration.stories.tsx => timetable-duration/timetable-duration.stories.tsx} (78%) rename src/components/{sbb-timetable-duration/sbb-timetable-duration.tsx => timetable-duration/timetable-duration.ts} (53%) create mode 100644 src/components/timetable-occupancy/readme.md create mode 100644 src/components/timetable-occupancy/timetable-occupancy.e2e.ts rename src/components/{sbb-timetable-occupancy/sbb-timetable-occupancy.sample-data.js => timetable-occupancy/timetable-occupancy.sample-data.js} (95%) rename src/components/{sbb-timetable-occupancy/sbb-timetable-occupancy.scss => timetable-occupancy/timetable-occupancy.scss} (96%) rename src/components/{sbb-timetable-occupancy/sbb-timetable-occupancy.spec.ts => timetable-occupancy/timetable-occupancy.spec.ts} (91%) rename src/components/{sbb-timetable-occupancy/sbb-timetable-occupancy.stories.tsx => timetable-occupancy/timetable-occupancy.stories.tsx} (86%) create mode 100644 src/components/timetable-occupancy/timetable-occupancy.ts create mode 100644 src/components/timetable-park-and-rail/readme.md create mode 100644 src/components/timetable-park-and-rail/timetable-park-and-rail.e2e.ts rename src/components/{sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.sample-data.js => timetable-park-and-rail/timetable-park-and-rail.sample-data.js} (100%) rename src/components/{sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.scss => timetable-park-and-rail/timetable-park-and-rail.scss} (93%) rename src/components/{sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.spec.ts => timetable-park-and-rail/timetable-park-and-rail.spec.ts} (81%) rename src/components/{sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.stories.tsx => timetable-park-and-rail/timetable-park-and-rail.stories.tsx} (82%) create mode 100644 src/components/timetable-park-and-rail/timetable-park-and-rail.ts create mode 100644 src/components/timetable-row-column-headers/readme.md create mode 100644 src/components/timetable-row-column-headers/timetable-row-column-headers.e2e.ts rename src/components/{sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.sample-data.js => timetable-row-column-headers/timetable-row-column-headers.sample-data.js} (100%) rename src/components/{sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.scss => timetable-row-column-headers/timetable-row-column-headers.scss} (61%) rename src/components/{sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.spec.ts => timetable-row-column-headers/timetable-row-column-headers.spec.ts} (66%) rename src/components/{sbb-timetable-row-column-headers/sbb-timetable-row-column-headers.stories.tsx => timetable-row-column-headers/timetable-row-column-headers.stories.tsx} (73%) create mode 100644 src/components/timetable-row-column-headers/timetable-row-column-headers.ts create mode 100644 src/components/timetable-row-day-change/readme.md create mode 100644 src/components/timetable-row-day-change/timetable-row-day-change.e2e.ts rename src/components/{sbb-timetable-row-day-change/sbb-timetable-row-day-change.sample-data.js => timetable-row-day-change/timetable-row-day-change.sample-data.js} (100%) rename src/components/{sbb-timetable-row-day-change/sbb-timetable-row-day-change.scss => timetable-row-day-change/timetable-row-day-change.scss} (90%) rename src/components/{sbb-timetable-row-day-change/sbb-timetable-row-day-change.spec.ts => timetable-row-day-change/timetable-row-day-change.spec.ts} (71%) rename src/components/{sbb-timetable-row-day-change/sbb-timetable-row-day-change.stories.tsx => timetable-row-day-change/timetable-row-day-change.stories.tsx} (78%) create mode 100644 src/components/timetable-row-day-change/timetable-row-day-change.ts create mode 100644 src/components/timetable-row-header/readme.md create mode 100644 src/components/timetable-row-header/timetable-row-header.e2e.ts rename src/components/{sbb-timetable-row-header/sbb-timetable-row-header.sample-data.js => timetable-row-header/timetable-row-header.sample-data.js} (70%) rename src/components/{sbb-timetable-row-header/sbb-timetable-row-header.scss => timetable-row-header/timetable-row-header.scss} (59%) rename src/components/{sbb-timetable-row-header/sbb-timetable-row-header.spec.ts => timetable-row-header/timetable-row-header.spec.ts} (52%) rename src/components/{sbb-timetable-row-header/sbb-timetable-row-header.stories.tsx => timetable-row-header/timetable-row-header.stories.tsx} (73%) create mode 100644 src/components/timetable-row-header/timetable-row-header.ts create mode 100644 src/components/timetable-row/index.ts create mode 100644 src/components/timetable-row/readme.md create mode 100644 src/components/timetable-row/timetable-row.e2e.ts rename src/components/{sbb-timetable-row/sbb-timetable-row.sample-data.ts => timetable-row/timetable-row.sample-data.ts} (99%) rename src/components/{sbb-timetable-row/sbb-timetable-row.scss => timetable-row/timetable-row.scss} (97%) create mode 100644 src/components/timetable-row/timetable-row.spec.ts rename src/components/{sbb-timetable-row/sbb-timetable-row.stories.tsx => timetable-row/timetable-row.stories.tsx} (97%) create mode 100644 src/components/timetable-row/timetable-row.ts create mode 100644 src/components/timetable-transportation-number/readme.md create mode 100644 src/components/timetable-transportation-number/timetable-transportation-number.e2e.ts rename src/components/{sbb-timetable-transportation-number/sbb-timetable-transportation-number.sample-data.js => timetable-transportation-number/timetable-transportation-number.sample-data.js} (100%) rename src/components/{sbb-timetable-transportation-number/sbb-timetable-transportation-number.scss => timetable-transportation-number/timetable-transportation-number.scss} (98%) rename src/components/{sbb-timetable-transportation-number/sbb-timetable-transportation-number.spec.ts => timetable-transportation-number/timetable-transportation-number.spec.ts} (86%) rename src/components/{sbb-timetable-transportation-number/sbb-timetable-transportation-number.stories.tsx => timetable-transportation-number/timetable-transportation-number.stories.tsx} (90%) create mode 100644 src/components/timetable-transportation-number/timetable-transportation-number.ts create mode 100644 src/components/timetable-transportation-time/readme.md create mode 100644 src/components/timetable-transportation-time/timetable-transportation-time.e2e.ts rename src/components/{sbb-timetable-transportation-time/sbb-timetable-transportation-time.sample-data.js => timetable-transportation-time/timetable-transportation-time.sample-data.js} (100%) rename src/components/{sbb-timetable-transportation-time/sbb-timetable-transportation-time.scss => timetable-transportation-time/timetable-transportation-time.scss} (96%) rename src/components/{sbb-timetable-transportation-time/sbb-timetable-transportation-time.spec.ts => timetable-transportation-time/timetable-transportation-time.spec.ts} (64%) rename src/components/{sbb-timetable-transportation-time/sbb-timetable-transportation-time.stories.tsx => timetable-transportation-time/timetable-transportation-time.stories.tsx} (87%) create mode 100644 src/components/timetable-transportation-time/timetable-transportation-time.ts create mode 100644 src/components/timetable-travel-hints/readme.md create mode 100644 src/components/timetable-travel-hints/timetable-travel-hints.e2e.ts rename src/components/{sbb-timetable-travel-hints/sbb-timetable-travel-hints.sample-data.js => timetable-travel-hints/timetable-travel-hints.sample-data.js} (100%) rename src/components/{sbb-timetable-travel-hints/sbb-timetable-travel-hints.scss => timetable-travel-hints/timetable-travel-hints.scss} (99%) rename src/components/{sbb-timetable-travel-hints/sbb-timetable-travel-hints.spec.ts => timetable-travel-hints/timetable-travel-hints.spec.ts} (89%) rename src/components/{sbb-timetable-travel-hints/sbb-timetable-travel-hints.stories.tsx => timetable-travel-hints/timetable-travel-hints.stories.tsx} (86%) create mode 100644 src/components/timetable-travel-hints/timetable-travel-hints.ts create mode 100644 src/components/title/index.ts create mode 100644 src/components/title/readme.md create mode 100644 src/components/title/title.e2e.ts rename src/components/{sbb-title/sbb-title.scss => title/title.scss} (65%) create mode 100644 src/components/title/title.spec.ts rename src/components/{sbb-title/sbb-title.stories.tsx => title/title.stories.tsx} (94%) create mode 100644 src/components/title/title.ts create mode 100644 src/components/toast/index.ts create mode 100644 src/components/toast/readme.md create mode 100644 src/components/toast/toast.e2e.ts rename src/components/{sbb-toast/sbb-toast.scss => toast/toast.scss} (96%) create mode 100644 src/components/toast/toast.spec.ts rename src/components/{sbb-toast/sbb-toast.stories.tsx => toast/toast.stories.tsx} (87%) create mode 100644 src/components/toast/toast.ts create mode 100644 src/components/toggle-check/index.ts create mode 100644 src/components/toggle-check/readme.md create mode 100644 src/components/toggle-check/toggle-check.e2e.ts rename src/components/{sbb-toggle-check/sbb-toggle-check.scss => toggle-check/toggle-check.scss} (96%) create mode 100644 src/components/toggle-check/toggle-check.spec.ts rename src/components/{sbb-toggle-check/sbb-toggle-check.stories.tsx => toggle-check/toggle-check.stories.tsx} (97%) create mode 100644 src/components/toggle-check/toggle-check.ts create mode 100644 src/components/toggle/index.ts create mode 100644 src/components/toggle/toggle-option/index.ts create mode 100644 src/components/toggle/toggle-option/readme.md create mode 100644 src/components/toggle/toggle-option/toggle-option.e2e.ts rename src/components/{sbb-toggle-option/sbb-toggle-option.scss => toggle/toggle-option/toggle-option.scss} (95%) create mode 100644 src/components/toggle/toggle-option/toggle-option.spec.ts rename src/components/{sbb-toggle-option/sbb-toggle-option.stories.tsx => toggle/toggle-option/toggle-option.stories.tsx} (94%) create mode 100644 src/components/toggle/toggle-option/toggle-option.ts create mode 100644 src/components/toggle/toggle/index.ts create mode 100644 src/components/toggle/toggle/readme.md create mode 100644 src/components/toggle/toggle/toggle.e2e.ts rename src/components/{sbb-toggle/sbb-toggle.scss => toggle/toggle/toggle.scss} (93%) create mode 100644 src/components/toggle/toggle/toggle.spec.ts rename src/components/{sbb-toggle/sbb-toggle.stories.tsx => toggle/toggle/toggle.stories.tsx} (97%) create mode 100644 src/components/toggle/toggle/toggle.ts create mode 100644 src/components/tooltip/index.ts create mode 100644 src/components/tooltip/tooltip-trigger/index.ts create mode 100644 src/components/tooltip/tooltip-trigger/readme.md create mode 100644 src/components/tooltip/tooltip-trigger/tooltip-trigger.e2e.ts rename src/components/{sbb-tooltip-trigger/sbb-tooltip-trigger.scss => tooltip/tooltip-trigger/tooltip-trigger.scss} (81%) create mode 100644 src/components/tooltip/tooltip-trigger/tooltip-trigger.spec.ts rename src/components/{sbb-tooltip-trigger/sbb-tooltip-trigger.stories.tsx => tooltip/tooltip-trigger/tooltip-trigger.stories.tsx} (95%) create mode 100644 src/components/tooltip/tooltip-trigger/tooltip-trigger.ts create mode 100644 src/components/tooltip/tooltip/index.ts create mode 100644 src/components/tooltip/tooltip/readme.md create mode 100644 src/components/tooltip/tooltip/tooltip.e2e.ts rename src/components/{sbb-tooltip/sbb-tooltip.scss => tooltip/tooltip/tooltip.scss} (96%) create mode 100644 src/components/tooltip/tooltip/tooltip.spec.ts rename src/components/{sbb-tooltip/sbb-tooltip.stories.tsx => tooltip/tooltip/tooltip.stories.tsx} (91%) rename src/components/{sbb-tooltip/sbb-tooltip.tsx => tooltip/tooltip/tooltip.ts} (64%) create mode 100644 src/components/train/index.ts create mode 100644 src/components/train/train-blocked-passage/index.ts rename src/components/{sbb-train-blocked-passage => train/train-blocked-passage}/readme.md (76%) create mode 100644 src/components/train/train-blocked-passage/train-blocked-passage.e2e.ts rename src/components/{sbb-train-blocked-passage/sbb-train-blocked-passage.scss => train/train-blocked-passage/train-blocked-passage.scss} (98%) create mode 100644 src/components/train/train-blocked-passage/train-blocked-passage.spec.ts rename src/components/{sbb-train-blocked-passage/sbb-train-blocked-passage.stories.tsx => train/train-blocked-passage/train-blocked-passage.stories.tsx} (72%) create mode 100644 src/components/train/train-blocked-passage/train-blocked-passage.ts create mode 100644 src/components/train/train-formation/index.ts rename src/components/{sbb-train-formation => train/train-formation}/readme.md (66%) rename src/components/{sbb-train-formation/sbb-train-formation.e2e.ts => train/train-formation/train-formation.e2e.ts} (56%) rename src/components/{sbb-train-formation/sbb-train-formation.scss => train/train-formation/train-formation.scss} (96%) create mode 100644 src/components/train/train-formation/train-formation.spec.ts rename src/components/{sbb-train-formation/sbb-train-formation.stories.tsx => train/train-formation/train-formation.stories.tsx} (94%) create mode 100644 src/components/train/train-formation/train-formation.ts create mode 100644 src/components/train/train-wagon/index.ts create mode 100644 src/components/train/train-wagon/readme.md create mode 100644 src/components/train/train-wagon/train-wagon.e2e.ts rename src/components/{sbb-train-wagon/sbb-train-wagon.scss => train/train-wagon/train-wagon.scss} (98%) create mode 100644 src/components/train/train-wagon/train-wagon.spec.ts rename src/components/{sbb-train-wagon/sbb-train-wagon.stories.tsx => train/train-wagon/train-wagon.stories.tsx} (96%) create mode 100644 src/components/train/train-wagon/train-wagon.ts create mode 100644 src/components/train/train/index.ts create mode 100644 src/components/train/train/readme.md create mode 100644 src/components/train/train/train.e2e.ts rename src/components/{sbb-train/sbb-train.scss => train/train/train.scss} (99%) create mode 100644 src/components/train/train/train.spec.ts rename src/components/{sbb-train/sbb-train.stories.tsx => train/train/train.stories.tsx} (93%) create mode 100644 src/components/train/train/train.ts create mode 100644 src/components/tsconfig.json create mode 100644 src/components/tsconfig.spec.json create mode 100644 src/components/visual-checkbox/index.ts create mode 100644 src/components/visual-checkbox/readme.md create mode 100644 src/components/visual-checkbox/visual-checkbox.e2e.ts rename src/components/{sbb-visual-checkbox/sbb-visual-checkbox.scss => visual-checkbox/visual-checkbox.scss} (85%) create mode 100644 src/components/visual-checkbox/visual-checkbox.spec.ts rename src/components/{sbb-visual-checkbox/sbb-visual-checkbox.stories.tsx => visual-checkbox/visual-checkbox.stories.tsx} (95%) create mode 100644 src/components/visual-checkbox/visual-checkbox.ts create mode 100644 src/components/vite.config.ts delete mode 100644 src/global/a11y/interactivity-checker.e2e.ts delete mode 100644 src/global/dom/component-on-ready.spec.ts delete mode 100644 src/global/dom/request-animation-frame.ts delete mode 100644 src/global/eventing/language-change-handler.e2e.ts delete mode 100644 src/global/styles/global.scss delete mode 100644 src/global/testing/index.ts delete mode 100644 src/global/testing/slotchange-events.ts delete mode 100644 src/global/testing/wait-for-condition.ts delete mode 100644 src/global/timetable/timetable-helper.ts delete mode 100644 src/index.ts create mode 100644 src/react/.gitignore create mode 100644 src/react/package.json create mode 100644 src/react/tsconfig.json create mode 100644 src/react/vite.config.ts rename src/{global => storybook/testing}/chromatic.tsx (94%) rename src/{global => storybook}/testing/wait-for-components-ready.ts (100%) rename src/{global => storybook}/testing/wait-for-stable-position.ts (100%) delete mode 100644 src/types.d.ts create mode 100644 src/vite-env.d.ts delete mode 100644 stencil.config.ts create mode 100644 tools/eslint/index.ts create mode 100644 tools/eslint/link/index.js create mode 100644 tools/eslint/link/package.json create mode 100644 tools/eslint/missing-component-documentation-rule.ts create mode 100644 tools/eslint/tsconfig.json create mode 100644 tools/generate-component/boilerplate/component.e2e.ts rename {convenience => tools}/generate-component/boilerplate/component.scss (89%) create mode 100644 tools/generate-component/boilerplate/component.spec.ts create mode 100644 tools/generate-component/boilerplate/component.stories.tsx create mode 100644 tools/generate-component/boilerplate/component.ts rename {convenience => tools}/generate-component/boilerplate/readme.md (88%) create mode 100644 tools/generate-component/index.mts create mode 100644 vite.config.ts create mode 100644 web-test-runner.config.js diff --git a/.eslintignore b/.eslintignore index f839c94645..856bb61f3f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,9 +2,8 @@ dist/ loader/ hydrate/ src/components.d.ts -storybook-static/ www/ -convenience/generate-component/boilerplate/ +tools/generate-component/boilerplate/ # not ignored folders/files !.github/ diff --git a/.eslintrc.json b/.eslintrc.json index 5947a02c46..406aa7858f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,6 @@ "jsx": true } }, - "ignorePatterns": ["**/react-library/**/*"], "overrides": [ { "files": ["*.js", "*.jsx"], @@ -30,7 +29,14 @@ }, { "files": ["*.ts", "*.tsx"], - "extends": ["plugin:@typescript-eslint/recommended", "prettier"], + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + "plugin:lit/recommended", + "plugin:lyne/all", + "prettier" + ], "rules": { "@typescript-eslint/array-type": "error", "@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }], @@ -82,7 +88,33 @@ // TODO: Remove this after fixing issues "@typescript-eslint/no-var-requires": "off", // TODO: Evaluate this rule - "@typescript-eslint/semi": ["error"], + "@typescript-eslint/semi": "error", + "import/first": "error", + "import/no-absolute-path": "error", + "import/no-cycle": "error", + "import/no-self-import": "error", + "import/no-unresolved": [ + "error", + { + "ignore": [ + "\\.md\\?raw$", + "\\.svg\\?raw$", + "\\.scss\\?lit\\&inline", + // Broken. Maybe due to commonjs? + "@storybook/addon-actions/decorator" + ] + } + ], + "import/no-useless-path-segments": "error", + "import/order": [ + "error", + { + "alphabetize": { "order": "asc", "caseInsensitive": true }, + "newlines-between": "always" + } + ], + // TODO Discuss this with the team + "lit/no-invalid-html": "off", "camelcase": "off" } }, @@ -90,14 +122,6 @@ "files": ["*.yaml", "*.yml"], "plugins": ["yaml"] }, - { - "files": ["*e2e.ts", "*spec.ts"], - "env": { - "jest/globals": true - }, - "extends": ["plugin:jest/recommended"], - "plugins": ["jest"] - }, { "files": ["*.tsx", "*.jsx"], "extends": ["plugin:jsx-a11y/recommended"], diff --git a/.github/workflows/continuous-integration-secure.yml b/.github/workflows/continuous-integration-secure.yml index 8042fb534e..21d954c9da 100644 --- a/.github/workflows/continuous-integration-secure.yml +++ b/.github/workflows/continuous-integration-secure.yml @@ -32,10 +32,10 @@ jobs: uses: ./.github/actions/download-artifacts-from-workflow with: artifacts: 'storybook' - - run: mkdir storybook-static - - run: unzip storybook.zip -d storybook-static + - run: mkdir -p dist/storybook + - run: unzip storybook.zip -d dist/storybook - name: Remove files with forbidden extensions - run: node ./ci/clean-storybook-files.js + run: node ./ci/clean-storybook-files.cjs - name: Create GitHub Deployment id: tag-name @@ -100,6 +100,6 @@ jobs: directory: coverage override_branch: ${{ github.event.workflow_run.head_branch }} override_commit: ${{ github.event.workflow_run.head_commit.id }} - override_pr: ${{ steps.yarn-cache-dir-path.outputs.pr }} + override_pr: ${{ steps.pr-number.outputs.pr }} fail_ci_if_error: true verbose: true diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6e2f60f466..9ac41487bb 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -39,11 +39,6 @@ jobs: test: runs-on: ubuntu-latest steps: - # Attempt to prevent "The operation was canceled" error - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 5 - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: @@ -51,11 +46,12 @@ jobs: cache: 'yarn' - run: yarn install --frozen-lockfile --non-interactive + - name: Install browser dependencies + run: yarn playwright install-deps - name: Run tests - # Try yarn test:prod up to three times, if it fails and exit with the exit code from the last execution - run: for i in $(seq 1 3); do [ $i -gt 1 ] && sleep 5; yarn test:prod && s=0 && break || s=$?; done; (exit $s) + run: yarn test env: - NODE_OPTIONS: '--max-old-space-size=6144' + NODE_ENV: production - name: Store coverage if: github.event_name == 'pull_request' uses: actions/upload-artifact@v3 @@ -85,21 +81,14 @@ jobs: - run: yarn install --frozen-lockfile --non-interactive - name: Run build - run: STORYBOOK_COMPONENTS_VERSION=$GITHUB_SHA yarn build - - name: Store stencil artifacts - uses: actions/upload-artifact@v3 - with: - name: stencil - path: | - dist/ - hydrate/ - loader/ - react-library/dist/ + run: yarn build + env: + STORYBOOK_COMPONENTS_VERSION: ${{ github.event.pull_request.head.sha || github.sha }} - name: Store storybook artifacts uses: actions/upload-artifact@v3 with: name: storybook - path: storybook-static/ + path: dist/storybook/ chromatic: runs-on: ubuntu-latest @@ -114,18 +103,16 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' - run: yarn install --frozen-lockfile --non-interactive - - name: Restore stencil artifacts - uses: actions/download-artifact@v3 - with: - name: stencil - path: . - name: Run build - run: yarn build:chromatic-stories && yarn build:storybook + run: yarn generate:chromatic-stories && yarn build:storybook + env: + CHROMATIC: true - name: Publish to Chromatic id: chromatic-publish uses: chromaui/action@v1 with: projectToken: ${{ secrets.CHROMATIC_TOKEN }} - storybookBuildDir: storybook-static + storybookBuildDir: dist/storybook exitOnceUploaded: true exitZeroOnChanges: true + zip: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f79cf22cb..38b8a7c779 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,11 +48,6 @@ jobs: registry-url: 'https://registry.npmjs.org' scope: sbb-esta - run: yarn install --frozen-lockfile --non-interactive - - name: Run build - run: yarn build:chromatic-stories && yarn build - - - name: Bundle stories - run: node ./ci/bundleStories.js - name: 'Release: Set git user' run: | @@ -65,45 +60,22 @@ jobs: run: echo "value=$(jq --raw-output .version ./package.json)" >> $GITHUB_OUTPUT - name: 'Release: Push release to repository' run: git push --follow-tags origin master + - name: Run build + run: STORYBOOK_COMPONENTS_VERSION=${{ steps.version.outputs.value }} yarn build - name: 'Release: Determine npm tag' id: npm_tag run: echo "npm_tag=$([[ "${{ steps.version.outputs.value }}" == *"-"* ]] && echo "next" || echo "latest")" >> $GITHUB_OUTPUT - name: 'Release: Publish @sbb-esta/lyne-components' - run: yarn publish --tag ${{ steps.npm_tag.outputs.npm_tag }} + run: yarn publish dist/components --tag ${{ steps.npm_tag.outputs.npm_tag }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: 'Release: Assign current dependency version' - uses: actions/github-script@v6 - with: - script: | - const fs = require('fs'); - const path = `${process.env.GITHUB_WORKSPACE}/react-library/package.json`; - const pkgJson = fs.readFileSync(path, 'utf8'); - fs.writeFileSync(path, pkgJson.replace(/0.0.0-PLACEHOLDER/g, '${{ steps.version.outputs.value }}'), 'utf8'); - name: 'Release: Publish @sbb-esta/lyne-components-react' - run: yarn publish react-library --tag ${{ steps.npm_tag.outputs.npm_tag }} + run: yarn publish dist/react --tag ${{ steps.npm_tag.outputs.npm_tag }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create versioned storybook for chromatic - run: STORYBOOK_COMPONENTS_VERSION=${{ steps.version.outputs.value }} yarn build:storybook - # Send storybook to chromatic. These snapshots should be accepted as new - # baseline in storybook. - - name: Publish to Chromatic - uses: chromaui/action@v1 - with: - projectToken: ${{ secrets.CHROMATIC_TOKEN }} - storybookBuildDir: storybook-static - autoAcceptChanges: true - exitZeroOnChanges: true - - - name: Remove chromatic stories - run: cd src && git clean -f -X - - name: Create versioned storybook for image - run: STORYBOOK_COMPONENTS_VERSION=${{ steps.version.outputs.value }} yarn build:storybook - name: Remove files with forbidden extensions - run: node ./ci/clean-storybook-files.js - + run: node ./ci/clean-storybook-files.cjs - name: 'Container: Login to GitHub Container Repository' run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io --username ${{ github.actor }} --password-stdin - name: 'Container: Build image' @@ -114,3 +86,18 @@ jobs: run: docker push $IMAGE_REPO:${{ steps.version.outputs.value }} - name: 'Container: Publish image as latest' run: docker push $IMAGE_REPO:latest + + - name: Generate chromatic stories + run: yarn generate:chromatic-stories + - name: Create versioned storybook for chromatic + run: STORYBOOK_COMPONENTS_VERSION=${{ steps.version.outputs.value }} yarn build:storybook + # Send storybook to chromatic. These snapshots should be accepted as new + # baseline in storybook. + - name: Publish to Chromatic + uses: chromaui/action@v1 + with: + projectToken: ${{ secrets.CHROMATIC_TOKEN }} + storybookBuildDir: dist/storybook + autoAcceptChanges: true + exitZeroOnChanges: true + zip: true diff --git a/.gitignore b/.gitignore index 03fa1b25e9..9bbefc3ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ /dist -/www -/loader -/hydrate -/storybook-static /coverage -/react-library/dist *~ *.sw[mnpcod] @@ -18,7 +13,6 @@ log.txt **/*.chromatic.stories.tsx package-lock.json -.stencil/ .idea/ .vscode/ .sass-cache/ diff --git a/.husky/commit-msg b/.husky/commit-msg index ee8c7d54da..db9f148c19 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -3,4 +3,4 @@ . "$(dirname "$0")/_/husky.sh" -npx --no-install commitlint --edit $1 +yarn commitlint --edit $1 diff --git a/.nvmrc b/.nvmrc index 3876fd4986..f3f52b42d3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.16.1 +20.9.0 diff --git a/.prettierignore b/.prettierignore index c519c7a155..03f6318114 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,10 +1,5 @@ coverage dist -hydrate -loader -storybook-static -src/components.d.ts -src/components/*/readme.md -src/global/core/components/*/readme.md -convenience/generate-component/boilerplate/readme.md -react-library + +# needed for apexes in `HTMLElementTagNameMap`, which otherwise would be stripped +tools/generate-component/boilerplate/component.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 56aae74bda..6335d0c2c5 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,25 +1,35 @@ -import type { StorybookConfig } from '@storybook/html-webpack5'; +import type { StorybookConfig } from '@storybook/web-components-vite'; +import { BuildOptions, UserConfig, mergeConfig } from 'vite'; const config: StorybookConfig = { - stories: ['../src/**/*.stories.tsx', '../src/**/*.stories.mdx'], - addons: [ - '@storybook/addon-essentials', - '@storybook/addon-a11y', - '@storybook/addon-interactions', - '@storybook/preset-scss', - '@storybook/addon-mdx-gfm', - ], - features: {}, - typescript: { - check: false, - }, + stories: ['../src/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-a11y', '@storybook/addon-interactions'], framework: { - name: '@storybook/html-webpack5', + name: '@storybook/web-components-vite', options: {}, }, docs: { autodocs: true, }, -}; + async viteFinal(config) { + let build: BuildOptions = {}; + if (process.env.CHROMATIC) { + build = { + sourcemap: false, + rollupOptions: { + output: { + manualChunks(id) { + return 'main'; + }, + }, + }, + }; + } + return mergeConfig(config, { + assetsInclude: ['src/**/*.md'], + build, + }); + }, +}; export default config; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index cf1d1e879a..8b11de558a 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -3,6 +3,28 @@ --> + + + + + +``` + +## Style + +The component has a negative variant which can be set using the `negative` property. + +```html + +``` + +The aspect ratio of the logo can be changed using the `protectiveRoom` property. +Possible values are `ideal` (default), `minimal` and `none`. + +```html + +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------------------- | --------------------- | ------- | -------------------------------- | --------- | ------------------------------------------------------------ | +| `negative` | `negative` | public | `boolean` | `false` | Variants of the logo. | +| `protectiveRoom` | `protective-room` | public | `SbbProtectiveRoom \| undefined` | `'ideal'` | Visual protective room around logo. | +| `accessibilityLabel` | `accessibility-label` | public | `string` | `'Logo'` | Accessibility label which will be forwarded to the SVG logo. | diff --git a/src/components/map-container/index.ts b/src/components/map-container/index.ts new file mode 100644 index 0000000000..761112cb4f --- /dev/null +++ b/src/components/map-container/index.ts @@ -0,0 +1 @@ +export * from './map-container'; diff --git a/src/components/map-container/map-container.e2e.ts b/src/components/map-container/map-container.e2e.ts new file mode 100644 index 0000000000..bab4f0602a --- /dev/null +++ b/src/components/map-container/map-container.e2e.ts @@ -0,0 +1,49 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { setViewport } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import { waitForCondition } from '../core/testing'; + +import { SbbMapContainer } from './map-container'; +import '.'; + +describe('sbb-map-container', () => { + let element: SbbMapContainer; + + it('should react to scrolling', async () => { + await setViewport({ width: 320, height: 600 }); + + await fixture( + html` +
+ Operations & Disruptions + ${[...Array(10).keys()].map( + (value) => + html`
+

Situation ${value}

+
`, + )} +
+
+
map
+
+
`, + ); + element = document.querySelector('sbb-map-container'); + assert.instanceOf(element, SbbMapContainer); + + function getInert(): boolean { + return element.shadowRoot.querySelector('sbb-button').hasAttribute('inert'); + } + + expect(element).not.to.have.attribute('data-scroll-up-button-visible'); + expect(getInert()).to.be.equal(true); + + // Scroll down + window.scrollTo(0, 400); + await waitForCondition(async () => !getInert()); + + expect(element).to.have.attribute('data-scroll-up-button-visible'); + expect(getInert()).to.be.equal(false); + }); +}); diff --git a/src/components/sbb-map-container/sbb-map-container.scss b/src/components/map-container/map-container.scss similarity index 98% rename from src/components/sbb-map-container/sbb-map-container.scss rename to src/components/map-container/map-container.scss index 97a538f60b..eac3b0b048 100644 --- a/src/components/sbb-map-container/sbb-map-container.scss +++ b/src/components/map-container/map-container.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/map-container/map-container.spec.ts b/src/components/map-container/map-container.spec.ts new file mode 100644 index 0000000000..a6cf44f664 --- /dev/null +++ b/src/components/map-container/map-container.spec.ts @@ -0,0 +1,66 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbMapContainer } from './map-container'; +import '.'; + +describe('sbb-map-container', () => { + let element: SbbMapContainer; + + it('renders the container with button', async () => { + element = await fixture(html``); + + expect(element).dom.to.be.equal( + ` + + + `, + ); + expect(element).shadowDom.to.be.equal( + ` +
+
+ + + Show map +
+
+ +
+
+ `, + ); + }); + it('renders the container without button', async () => { + element = await fixture(html``); + + expect(element).dom.to.be.equal( + ` + + + `, + ); + expect(element).shadowDom.to.be.equal( + ` +
+
+ +
+
+ +
+
+ `, + ); + }); +}); diff --git a/src/components/sbb-map-container/sbb-map-container.stories.tsx b/src/components/map-container/map-container.stories.tsx similarity index 91% rename from src/components/sbb-map-container/sbb-map-container.stories.tsx rename to src/components/map-container/map-container.stories.tsx index e1a20ae319..419d40d1eb 100644 --- a/src/components/sbb-map-container/sbb-map-container.stories.tsx +++ b/src/components/map-container/map-container.stories.tsx @@ -1,8 +1,14 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './map-container'; +import '../form-field'; +import '../icon'; +import '../title'; +import '../header'; const hideScrollUpButton: InputType = { control: { @@ -77,7 +83,7 @@ const meta: Meta = { Menu - + ), ], diff --git a/src/components/map-container/map-container.ts b/src/components/map-container/map-container.ts new file mode 100644 index 0000000000..5210a75401 --- /dev/null +++ b/src/components/map-container/map-container.ts @@ -0,0 +1,130 @@ +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref.js'; + +import { toggleDatasetEntry } from '../core/dom'; +import { documentLanguage, HandlerRepository, languageChangeHandlerAspect } from '../core/eventing'; +import { i18nMapContainerButtonLabel } from '../core/i18n'; +import { AgnosticIntersectionObserver } from '../core/observers'; + +import style from './map-container.scss?lit&inline'; +import '../button'; + +/** + * It can be used as a container for maps. + * + * @slot - Use the unnamed slot to add content to the sidebar. + * @slot map - Used for slotting the map. + */ +@customElement('sbb-map-container') +export class SbbMapContainer extends LitElement { + public static override styles: CSSResult = style; + + /** Flag to show/hide the scroll up button inside the sidebar on mobile. */ + @property({ attribute: 'hide-scroll-up-button', reflect: true, type: Boolean }) + public hideScrollUpButton = false; + + @state() private _scrollUpButtonVisible = false; + + /** Current document language used for translation of the button label. */ + @state() private _currentLanguage = documentLanguage(); + + private _handlerRepository = new HandlerRepository( + this, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + ); + + private _intersector: HTMLSpanElement; + private _observer = new AgnosticIntersectionObserver((entries) => + this._toggleButtonVisibilityOnIntersect(entries), + ); + + /** + * Button click callback to trigger the scroll to container top + * @private + */ + private _onScrollButtonClick(): void { + this.scrollIntoView({ behavior: 'smooth' }); + } + /** + * Intersection callback. Toggles the visibility. + * @param entries + * @private + */ + private _toggleButtonVisibilityOnIntersect(entries: IntersectionObserverEntry[]): void { + entries.forEach((entry) => { + const mapIsHidden = !entry.isIntersecting; + toggleDatasetEntry(this, 'scrollUpButtonVisible', mapIsHidden); + this._scrollUpButtonVisible = mapIsHidden; + }); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._handlerRepository.connect(); + this._updateIntersectionObserver(); + } + + private _updateIntersectionObserver(): void { + this._observer.disconnect(); + if (!this.hideScrollUpButton && this._intersector) { + this._observer.observe(this._intersector); + } + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.connect(); + this._observer.disconnect(); + } + + protected override render(): TemplateResult { + return html` +
+
+ ${!this.hideScrollUpButton + ? html` { + if (this._intersector === el) { + return; + } + this._intersector = el; + this._updateIntersectionObserver(); + })} + >` + : nothing} + + + + ${!this.hideScrollUpButton + ? html` { + if (ref) { + ref.inert = !this._scrollUpButtonVisible; + } + })} + variant="tertiary" + size="l" + icon-name="location-pin-map-small" + type="button" + @click=${() => this._onScrollButtonClick()} + > + ${i18nMapContainerButtonLabel[this._currentLanguage]} + ` + : nothing} +
+
+ +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-map-container': SbbMapContainer; + } +} diff --git a/src/components/map-container/readme.md b/src/components/map-container/readme.md new file mode 100644 index 0000000000..6f48e9d46c --- /dev/null +++ b/src/components/map-container/readme.md @@ -0,0 +1,35 @@ +This component is the layout container for the disruption map, the level 3 navigation and the future ATLAS. + +## Slots + +It provides two slots: one unnamed slot for the sidebar content, and one named `map` for the map. + +```html + +
Content
+
Here comes the map.
+
+``` + +On mobile, the map is sticky above the sidebar, and the sidebar content is scrolling over the map. +On desktop, the sidebar and the map are shown in a two column layout side by side. + +## Style + +The component comes along with a height calculation that subtracts the height of the header. +The header height can be overridden setting the variable `--sbb-map-container-margin-start`, if needed. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------------------- | ----------------------- | ------- | --------- | ------- | -------------------------------------------------------------------- | +| `hideScrollUpButton` | `hide-scroll-up-button` | public | `boolean` | `false` | Flag to show/hide the scroll up button inside the sidebar on mobile. | + +## Slots + +| Name | Description | +| ----- | --------------------------------------------------- | +| | Use the unnamed slot to add content to the sidebar. | +| `map` | Used for slotting the map. | diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts new file mode 100644 index 0000000000..b025d13966 --- /dev/null +++ b/src/components/menu/index.ts @@ -0,0 +1,2 @@ +export * from './menu'; +export * from './menu-action'; diff --git a/src/components/menu/menu-action/index.ts b/src/components/menu/menu-action/index.ts new file mode 100644 index 0000000000..5779d4f7cd --- /dev/null +++ b/src/components/menu/menu-action/index.ts @@ -0,0 +1 @@ +export * from './menu-action'; diff --git a/src/components/menu/menu-action/menu-action.e2e.ts b/src/components/menu/menu-action/menu-action.e2e.ts new file mode 100644 index 0000000000..76d87ed35f --- /dev/null +++ b/src/components/menu/menu-action/menu-action.e2e.ts @@ -0,0 +1,90 @@ +import { assert, 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 { SbbMenuAction } from './menu-action'; + +describe('sbb-menu-action', () => { + let element: SbbMenuAction; + + beforeEach(async () => { + element = await fixture(html`Menu Action`); + }); + + describe('events', () => { + it('dispatches event on click', async () => { + const changeSpy = new EventSpy('click'); + + element.click(); + await waitForCondition(() => changeSpy.events.length === 1); + expect(changeSpy.count).to.be.equal(1); + }); + + it('should not dispatch event on click if disabled', async () => { + element.setAttribute('disabled', 'true'); + + await waitForLitRender(element); + + const clickSpy = new EventSpy('click'); + + element.dispatchEvent( + new CustomEvent('click', { bubbles: true, cancelable: true, composed: true }), + ); + expect(clickSpy.count).not.to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Enter', async () => { + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: 'Enter' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Space', async () => { + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: ' ' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Enter with href', async () => { + element.setAttribute('href', '#'); + await waitForLitRender(element); + + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: 'Enter' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should not dispatch click event on pressing Space with href', async () => { + element.setAttribute('href', '#'); + await waitForLitRender(element); + + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: ' ' }); + expect(changeSpy.count).not.to.be.greaterThan(0); + }); + + it('should receive focus', async () => { + element.focus(); + await waitForLitRender(element); + + expect(document.activeElement.id).to.be.equal('focus-id'); + }); + }); + + it('renders as a button and triggers click event', async () => { + element = await fixture(html``); + + assert.instanceOf(element, SbbMenuAction); + + const clickedSpy = new EventSpy('click'); + element.click(); + await waitForCondition(() => clickedSpy.events.length === 1); + expect(clickedSpy.count).to.be.equal(1); + }); +}); diff --git a/src/components/sbb-menu-action/sbb-menu-action.scss b/src/components/menu/menu-action/menu-action.scss similarity index 94% rename from src/components/sbb-menu-action/sbb-menu-action.scss rename to src/components/menu/menu-action/menu-action.scss index 19d58b7811..731a15a8ee 100644 --- a/src/components/sbb-menu-action/sbb-menu-action.scss +++ b/src/components/menu/menu-action/menu-action.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -17,14 +17,14 @@ --sbb-menu-action-forced-color-border-color: CanvasText; } -:host(:hover:not([disabled]:not([disabled='false']))) { +:host(:hover:not([disabled])) { @include sbb.hover-mq($hover: true) { --sbb-menu-background-color: var(--sbb-color-iron-default); --sbb-menu-action-forced-color-border-color: Highlight; } } -:host([disabled]:not([disabled='false'])) { +:host([disabled]) { --sbb-menu-action-cursor: default; --sbb-menu-action-color: var(--sbb-color-graphite-default); --sbb-menu-action-forced-color-border-color: GrayText; @@ -85,7 +85,7 @@ .sbb-menu-action__label { @include sbb.ellipsis; - :host([disabled]:not([disabled='false'])) & { + :host([disabled]) & { text-decoration: line-through; } } diff --git a/src/components/menu/menu-action/menu-action.spec.ts b/src/components/menu/menu-action/menu-action.spec.ts new file mode 100644 index 0000000000..1c41cb958f --- /dev/null +++ b/src/components/menu/menu-action/menu-action.spec.ts @@ -0,0 +1,84 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './menu-action'; + +describe('sbb-menu-action', () => { + it('renders component as button', async () => { + const root = await fixture(html` + + Action + + `); + + expect(root).dom.to.be.equal( + ` + + Action + + `, + ); + expect(root).shadowDom.to.be.equal( + ` + + + + + + + + + + + `, + ); + }); + + it('renders component as link with icon and amount', async () => { + const root = await fixture(html` + + Action + + `); + + expect(root).dom.to.be.equal( + ` + + Action + + `, + ); + expect(root).shadowDom.to.be.equal( + ` + + + + + + + + + + + + 123456 + + + + . Link target opens in new window. + + + `, + ); + }); +}); diff --git a/src/components/sbb-menu-action/sbb-menu-action.stories.tsx b/src/components/menu/menu-action/menu-action.stories.tsx similarity index 94% rename from src/components/sbb-menu-action/sbb-menu-action.stories.tsx rename to src/components/menu/menu-action/menu-action.stories.tsx index 7430ebdd31..990a2dea42 100644 --- a/src/components/sbb-menu-action/sbb-menu-action.stories.tsx +++ b/src/components/menu/menu-action/menu-action.stories.tsx @@ -1,14 +1,17 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './menu-action'; +import '../../icon'; const getBasicTemplate = ({ text, ...args }, id, iconSlot = false): JSX.Element => ( {text} {id} - {iconSlot && } + {iconSlot && } ); @@ -232,7 +235,7 @@ const meta: Meta = { decorators: [ (Story) => (
- +
), withActions as Decorator, diff --git a/src/components/menu/menu-action/menu-action.ts b/src/components/menu/menu-action/menu-action.ts new file mode 100644 index 0000000000..23cd47df23 --- /dev/null +++ b/src/components/menu/menu-action/menu-action.ts @@ -0,0 +1,136 @@ +import { spread } from '@open-wc/lit-helpers'; +import { CSSResult, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { html, unsafeStatic } from 'lit/static-html.js'; + +import { setAttributes } from '../../core/dom'; +import { + documentLanguage, + HandlerRepository, + actionElementHandlerAspect, + languageChangeHandlerAspect, +} from '../../core/eventing'; +import { i18nTargetOpensInNewWindow } from '../../core/i18n'; +import { + ButtonType, + LinkButtonProperties, + LinkButtonRenderVariables, + LinkTargetType, + resolveRenderVariables, + targetsNewWindow, +} from '../../core/interfaces'; + +import style from './menu-action.scss?lit&inline'; +import '../../icon'; + +/** + * It displays an action element that can be used in the `sbb-menu` component. + * + * @slot - Use the unnamed slot to add content to the `sbb-menu-action`. + * @slot icon - Use this slot to provide an icon. If `icon-name` is set, a `sbb-icon` will be used. + */ +@customElement('sbb-menu-action') +export class SbbMenuAction extends LitElement implements LinkButtonProperties { + public static override styles: CSSResult = style; + + /** + * The name of the icon, choose from the small icon variants + * from the ui-icons category from here + * https://icons.app.sbb.ch. + */ + @property({ attribute: 'icon-name' }) public iconName?: string | undefined; + + /** Value shown as badge at component end. */ + @property() public amount?: string | undefined; + + /** The href value you want to link to (if it is not present menu action becomes a button). */ + @property() public href: string | undefined; + + /** Where to display the linked URL. */ + @property() public target?: LinkTargetType | string | undefined; + + /** The relationship of the linked URL as space-separated link types. */ + @property() public rel?: string | undefined; + + /** Whether the browser will show the download dialog on click. */ + @property({ type: Boolean }) public download?: boolean; + + /** The type attribute to use for the button. */ + @property() public type: ButtonType | undefined; + + /** Whether the button is disabled. */ + @property({ reflect: true, type: Boolean }) public disabled = false; + + /** The name attribute to use for the button. */ + @property({ reflect: true }) public name: string | undefined; + + /** The value attribute to use for the button. */ + @property() public value?: string; + + /** The
element to associate the button with. */ + @property() public form?: string; + + @state() private _currentLanguage = documentLanguage(); + + private _handlerRepository = new HandlerRepository( + this, + actionElementHandlerAspect, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + ); + + public override connectedCallback(): void { + super.connectedCallback(); + this._handlerRepository.connect(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + } + + protected override render(): TemplateResult { + const { + tagName: TAG_NAME, + hostAttributes, + attributes, + }: LinkButtonRenderVariables = resolveRenderVariables(this); + + setAttributes(this, hostAttributes); + + /* eslint-disable lit/binding-positions */ + return html` + <${unsafeStatic(TAG_NAME)} class="sbb-menu-action" ${spread(attributes)}> + + + ${this.iconName ? html`` : nothing} + + + + + ${ + this.amount && !this.disabled + ? html`${this.amount}` + : nothing + } + + ${ + targetsNewWindow(this) + ? html` + . ${i18nTargetOpensInNewWindow[this._currentLanguage]} + ` + : nothing + } + + `; + /* eslint-disable lit/binding-positions */ + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-menu-action': SbbMenuAction; + } +} diff --git a/src/components/menu/menu-action/readme.md b/src/components/menu/menu-action/readme.md new file mode 100644 index 0000000000..f0ad4f863d --- /dev/null +++ b/src/components/menu/menu-action/readme.md @@ -0,0 +1,61 @@ +The component represents an action element contained by the [sbb-menu](/docs/components-sbb-menu-sbb-menu--docs) component. + +## Slots + +It is possible to provide a label via an unnamed slot; the component can optionally display a `sbb-icon` +at the component start using the `iconName` property or via custom content using the `icon` slot. + +```html +Text + +Another text +``` + +An amount can be rendered at the end of the action element as white text in a red circle via the `amount` property. + +```html +Amount text +``` + +## Link / button properties + +As the [sbb-link](/docs/components-sbb-link--docs) and the [sbb-button](/docs/components-sbb-button--docs), +the component can be internally rendered as a button or as a link, +depending on the value of the `href` property, so the associated properties are available +(`href`, `target`, `rel` and `download` for link; `type`, `name`, `value` and `form` for button). + +```html +Link + +Button +``` + +## Style + +For cases where smaller outer paddings are needed, +you can set the css variable `--sbb-menu-action-outer-horizontal-padding` to your desired outer padding. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------- | ----------- | ------- | ---------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ | +| `iconName` | `icon-name` | public | `string \| undefined \| undefined` | | The name of the icon, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `amount` | `amount` | public | `string \| undefined \| undefined` | | Value shown as badge at component end. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to (if it is not present menu action becomes a button). | +| `target` | `target` | public | `LinkTargetType \| string \| undefined \| undefined` | | Where to display the linked URL. | +| `rel` | `rel` | public | `string \| undefined \| 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. | +| `type` | `type` | public | `ButtonType \| undefined` | | The type attribute to use for the button. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the button is disabled. | +| `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 content to the `sbb-menu-action`. | +| `icon` | Use this slot to provide an icon. If `icon-name` is set, a `sbb-icon` will be used. | diff --git a/src/components/menu/menu/index.ts b/src/components/menu/menu/index.ts new file mode 100644 index 0000000000..8267df70b3 --- /dev/null +++ b/src/components/menu/menu/index.ts @@ -0,0 +1 @@ +export * from './menu'; diff --git a/src/components/menu/menu/menu.e2e.ts b/src/components/menu/menu/menu.e2e.ts new file mode 100644 index 0000000000..8d3ea9b820 --- /dev/null +++ b/src/components/menu/menu/menu.e2e.ts @@ -0,0 +1,246 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { sendKeys, setViewport } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing'; + +import { SbbMenu } from './menu'; +import '../../button'; +import '../menu-action'; +import '../../link'; +import '../../divider'; + +describe('sbb-menu', () => { + let element: SbbMenu, trigger: HTMLElement; + + beforeEach(async () => { + await fixture(html` + Menu trigger + + Profile + View + Edit + Details + + Cancel + + `); + trigger = document.querySelector('sbb-button'); + element = document.querySelector('sbb-menu'); + }); + + it('renders', () => { + assert.instanceOf(element, SbbMenu); + }); + + it('opens on trigger click', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + + trigger.click(); + await waitForLitRender(element); + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + + await waitForLitRender(element); + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + + await waitForLitRender(element); + expect(element).to.have.attribute('data-state', 'opened'); + }); + + it('closes on Esc keypress', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbMenu.events.willClose); + const didCloseEventSpy = new EventSpy(SbbMenu.events.didClose); + + trigger.click(); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + await sendKeys({ down: 'Tab' }); + await waitForLitRender(element); + + await sendKeys({ down: 'Escape' }); + await waitForLitRender(element); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('closes on menu action click', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbMenu.events.willClose); + const didCloseEventSpy = new EventSpy(SbbMenu.events.didClose); + const menuAction = document.querySelector('sbb-menu > sbb-menu-action') as HTMLElement; + + trigger.click(); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(menuAction).not.to.be.null; + + menuAction.click(); + await waitForLitRender(element); + await waitForCondition(() => willCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + + await waitForLitRender(element); + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + + await waitForLitRender(element); + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('closes on interactive element click', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + const willCloseEventSpy = new EventSpy(SbbMenu.events.willClose); + const didCloseEventSpy = new EventSpy(SbbMenu.events.didClose); + const menuLink = document.querySelector('sbb-menu > sbb-link') as HTMLElement; + + trigger.click(); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(menuLink).not.to.be.null; + + menuLink.click(); + await waitForLitRender(element); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('is correctly positioned on desktop', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + await setViewport({ width: 1200, height: 800 }); + const menu: HTMLElement = element.shadowRoot.querySelector('.sbb-menu'); + + trigger.click(); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + const buttonHeight = getComputedStyle(document.documentElement).getPropertyValue( + `--sbb-size-button-l-min-height-large`, + ); + expect(buttonHeight.trim()).to.be.equal('3.5rem'); + + const buttonHeightPx = parseFloat(buttonHeight) * 16; + expect(trigger.offsetHeight).to.be.equal(buttonHeightPx); + expect(trigger.offsetTop).to.be.equal(0); + expect(trigger.offsetLeft).to.be.equal(0); + + // Expect menu offsetTop to be equal to the trigger height + the menu offset (8px) + expect(menu.offsetTop).to.be.equal(buttonHeightPx + 8); + expect(menu.offsetLeft).to.be.equal(0); + }); + + it('is correctly positioned on mobile', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + + await setViewport({ width: 800, height: 600 }); + const menu: HTMLElement = element.shadowRoot.querySelector('.sbb-menu'); + + trigger.click(); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + const menuOffsetTop = menu.offsetTop; + const menuHeight = menu.offsetHeight; + const pageHeight = window.innerHeight; + + expect(menuOffsetTop).to.be.equal(pageHeight - menuHeight); + }); + + it('sets the focus to the first focusable element when the menu is opened by keyboard', async () => { + const willOpenEventSpy = new EventSpy(SbbMenu.events.willOpen); + const didOpenEventSpy = new EventSpy(SbbMenu.events.didOpen); + + await sendKeys({ down: 'Tab' }); + await waitForLitRender(element); + + await sendKeys({ down: 'Enter' }); + await waitForLitRender(element); + + await waitForCondition(() => willOpenEventSpy.events.length === 1); + expect(willOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + await waitForLitRender(element); + expect(document.activeElement.id).to.be.equal('menu-link'); + }); +}); diff --git a/src/components/sbb-menu/sbb-menu.scss b/src/components/menu/menu/menu.scss similarity index 97% rename from src/components/sbb-menu/sbb-menu.scss rename to src/components/menu/menu/menu.scss index 9ecc94d309..d95140effb 100644 --- a/src/components/sbb-menu/sbb-menu.scss +++ b/src/components/menu/menu/menu.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -53,8 +53,8 @@ --sbb-menu-inset: 0; } -:host([disable-animation]:not([disable-animation='false'])) { - --sbb-menu-animation-duration: 0s; +:host([disable-animation]) { + --sbb-menu-animation-duration: 0.1ms; } ::slotted(:not(sbb-menu-action, sbb-divider)) { diff --git a/src/components/sbb-menu/sbb-menu.spec.ts b/src/components/menu/menu/menu.spec.ts similarity index 67% rename from src/components/sbb-menu/sbb-menu.spec.ts rename to src/components/menu/menu/menu.spec.ts index 8033755a62..ce10f8cbda 100644 --- a/src/components/sbb-menu/sbb-menu.spec.ts +++ b/src/components/menu/menu/menu.spec.ts @@ -1,33 +1,25 @@ -import { SbbMenu } from './sbb-menu'; -import { newSpecPage } from '@stencil/core/testing'; +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './menu'; describe('sbb-menu', () => { it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbMenu], - html: ` + await fixture(html` Menu trigger Profile View Edit Details - + Cancel - `, - }); + + `); + const menu = document.querySelector('sbb-menu'); - expect(root).toEqualHtml(` + expect(menu).dom.to.be.equal( + ` - -
-
-
- -
-
-
-
Profile @@ -40,31 +32,59 @@ describe('sbb-menu', () => { Details - - - Cancel - - + + + Cancel +
- `); + `, + ); + expect(menu).shadowDom.to.be.equal( + ` +
+
+
+ +
+
+
+ `, + ); }); it('renders with list', async () => { - const { root } = await newSpecPage({ - components: [SbbMenu], - html: ` - Menu trigger - - View - Edit - Details - Cancel - `, - }); + await fixture( + html` Menu trigger + + View + Edit + Details + Cancel + `, + ); + const menu = document.querySelector('sbb-menu'); + + expect(menu).dom.to.be.equal( + ` + - expect(root).toEqualHtml(` - - + + View + + + Edit + + + Details + + + Cancel + + + `, + ); + expect(menu).shadowDom.to.be.equal( + `
@@ -88,20 +108,7 @@ describe('sbb-menu', () => {
- - - View - - - Edit - - - Details - - - Cancel - -
- `); + `, + ); }); }); diff --git a/src/components/sbb-menu/sbb-menu.stories.tsx b/src/components/menu/menu/menu.stories.tsx similarity index 91% rename from src/components/sbb-menu/sbb-menu.stories.tsx rename to src/components/menu/menu/menu.stories.tsx index 914b446871..aec883b238 100644 --- a/src/components/sbb-menu/sbb-menu.stories.tsx +++ b/src/components/menu/menu/menu.stories.tsx @@ -1,14 +1,21 @@ /** @jsx h */ -import events from './sbb-menu.events'; -import { Fragment, h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import isChromatic from 'chromatic'; -import { userEvent, within } from '@storybook/testing-library'; -import { waitForComponentsReady } from '../../global/testing/wait-for-components-ready'; -import { waitForStablePosition } from '../../global/testing'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; +import { userEvent, within } from '@storybook/testing-library'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import { waitForComponentsReady } from '../../../storybook/testing/wait-for-components-ready'; +import { waitForStablePosition } from '../../../storybook/testing/wait-for-stable-position'; + +import { SbbMenu } from './menu'; +import readme from './readme.md?raw'; + +import '../../button'; +import '../../divider'; +import '../../link'; +import '../menu-action'; // Story interaction executed after the story renders const playStory = async ({ canvasElement }): Promise => { @@ -104,7 +111,7 @@ const DefaultTemplate = (args): JSX.Element => ( Details - + Cancel @@ -145,7 +152,7 @@ const CustomContentTemplate = (args): JSX.Element => ( Profile - + View @@ -155,7 +162,7 @@ const CustomContentTemplate = (args): JSX.Element => ( Cart - + Log Out @@ -194,7 +201,7 @@ const LongContentTemplate = (args): JSX.Element => ( Dansk Nederlands Suomi - + Cancel @@ -213,7 +220,7 @@ const EllipsisTemplate = (args): JSX.Element => ( Profile - + View @@ -224,7 +231,7 @@ const EllipsisTemplate = (args): JSX.Element => ( Very long label that exceeds the maximum width of the menu, very long label that exceeds the maximum width of the menu, very long label that exceeds the maximum width of the menu - + Cancel @@ -269,7 +276,7 @@ const meta: Meta = { decorators: [ (Story) => (
- +
), withActions as Decorator, @@ -277,7 +284,12 @@ const meta: Meta = { parameters: { chromatic: { disableSnapshot: false }, actions: { - handles: [events.willOpen, events.didOpen, events.didClose, events.willClose], + handles: [ + SbbMenu.events.willOpen, + SbbMenu.events.didOpen, + SbbMenu.events.didClose, + SbbMenu.events.willClose, + ], }, backgrounds: { disable: true, diff --git a/src/components/sbb-menu/sbb-menu.tsx b/src/components/menu/menu/menu.ts similarity index 59% rename from src/components/sbb-menu/sbb-menu.tsx rename to src/components/menu/menu/menu.ts index 293caa8698..51a50b00e7 100644 --- a/src/components/sbb-menu/sbb-menu.tsx +++ b/src/components/menu/menu/menu.ts @@ -1,27 +1,7 @@ -import { - Component, - ComponentInterface, - Element, - Event, - EventEmitter, - h, - Host, - JSX, - Listen, - Method, - Prop, - State, - Watch, -} from '@stencil/core'; -import { - applyInertMechanism, - getElementPosition, - isEventOnElement, - removeAriaOverlayTriggerAttributes, - removeInertMechanism, - SbbOverlayState, - setAriaOverlayTriggerAttributes, -} from '../../global/overlay'; +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref.js'; + import { assignId, FocusTrap, @@ -30,13 +10,27 @@ import { IS_FOCUSABLE_QUERY, isArrowKeyPressed, setModalityOnNextFocus, -} from '../../global/a11y'; +} from '../../core/a11y'; import { findReferencedElement, isBreakpoint, isValidAttribute, ScrollHandler, -} from '../../global/dom'; + setAttribute, +} from '../../core/dom'; +import { EventEmitter, ConnectedAbortController } from '../../core/eventing'; +import { + applyInertMechanism, + getElementPosition, + isEventOnElement, + removeAriaOverlayTriggerAttributes, + removeInertMechanism, + SbbOverlayState, + setAriaOverlayTriggerAttributes, +} from '../../core/overlay'; +import type { SbbMenuAction } from '../menu-action'; + +import style from './menu.scss?lit&inline'; const MENU_OFFSET = 8; const INTERACTIVE_ELEMENTS = ['A', 'BUTTON', 'SBB-BUTTON', 'SBB-LINK']; @@ -44,78 +38,82 @@ const INTERACTIVE_ELEMENTS = ['A', 'BUTTON', 'SBB-BUTTON', 'SBB-LINK']; let nextId = 0; /** - * @slot unnamed - Use this slot to project any content inside the menu. + * It displays a contextual menu with one or more action element. + * + * @slot - Use the unnamed slot to add `sbb-menu-action` or other elements to the menu. + * @event {CustomEvent} will-open - Emits whenever the `sbb-menu` starts the opening transition. + * @event {CustomEvent} did-open - Emits whenever the `sbb-menu` is opened. + * @event {CustomEvent} will-close - Emits whenever the `sbb-menu` begins the closing transition. + * @event {CustomEvent} did-close - Emits whenever the `sbb-menu` is closed. */ -@Component({ - shadow: true, - styleUrl: 'sbb-menu.scss', - tag: 'sbb-menu', -}) -export class SbbMenu implements ComponentInterface { +@customElement('sbb-menu') +export class SbbMenu extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + willOpen: 'will-open', + didOpen: 'did-open', + willClose: 'will-close', + didClose: 'did-close', + } as const; + /** * The element that will trigger the menu overlay. * Accepts both a string (id of an element) or an HTML element. */ - @Prop() public trigger: string | HTMLElement; + @property() + public set trigger(value: string | HTMLElement) { + const oldValue = this._trigger; + this._trigger = value; + this._removeTriggerClickListener(this._trigger, oldValue); + } + public get trigger(): string | HTMLElement { + return this._trigger; + } + private _trigger: string | HTMLElement = null; /** * Whether the animation is enabled. */ - @Prop({ reflect: true }) public disableAnimation = false; + @property({ attribute: 'disable-animation', reflect: true, type: Boolean }) + public disableAnimation = false; /** * This will be forwarded as aria-label to the inner list. * Used only if the menu automatically renders the actions inside as a list. */ - @Prop() public listAccessibilityLabel?: string; + @property({ attribute: 'list-accessibility-label' }) public listAccessibilityLabel?: string; /** * The state of the menu. */ - @State() private _state: SbbOverlayState = 'closed'; + @state() private _state: SbbOverlayState = 'closed'; /** Sbb-Link elements */ - @State() private _actions: HTMLSbbMenuActionElement[]; + @state() private _actions: SbbMenuAction[]; - /** - * Emits whenever the menu starts the opening transition. - */ - @Event({ + /** Emits whenever the `sbb-menu` starts the opening transition. */ + private _willOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.willOpen, { bubbles: true, composed: true, - eventName: 'will-open', - }) - public willOpen: EventEmitter; + }); - /** - * Emits whenever the menu is opened. - */ - @Event({ + /** Emits whenever the `sbb-menu` is opened. */ + private _didOpen: EventEmitter = new EventEmitter(this, SbbMenu.events.didOpen, { bubbles: true, composed: true, - eventName: 'did-open', - }) - public didOpen: EventEmitter; + }); - /** - * Emits whenever the menu begins the closing transition. - */ - @Event({ + /** Emits whenever the `sbb-menu` begins the closing transition. */ + private _willClose: EventEmitter = new EventEmitter(this, SbbMenu.events.willClose, { bubbles: true, composed: true, - eventName: 'will-close', - }) - public willClose: EventEmitter; + }); - /** - * Emits whenever the menu is closed. - */ - @Event({ + /** Emits whenever the `sbb-menu` is closed. */ + private _didClose: EventEmitter = new EventEmitter(this, SbbMenu.events.didClose, { bubbles: true, composed: true, - eventName: 'did-close', - }) - public didClose: EventEmitter; + }); private _menu: HTMLDivElement; private _triggerElement: HTMLElement; @@ -123,22 +121,20 @@ export class SbbMenu implements ComponentInterface { private _isPointerDownEventOnMenu: boolean; private _menuController: AbortController; private _windowEventsController: AbortController; + private _abort = new ConnectedAbortController(this); private _focusTrap = new FocusTrap(); private _scrollHandler = new ScrollHandler(); private _menuId = `sbb-menu-${++nextId}`; - @Element() private _element!: HTMLElement; - /** * Opens the menu on trigger click. */ - @Method() - public async open(): Promise { + public open(): void { if (this._state === 'closing' || !this._menu) { return; } - this.willOpen.emit(); + this._willOpen.emit(); this._state = 'opening'; this._setMenuPosition(); this._triggerElement?.setAttribute('aria-expanded', 'true'); @@ -152,13 +148,12 @@ export class SbbMenu implements ComponentInterface { /** * Closes the menu. */ - @Method() - public async close(): Promise { + public close(): void { if (this._state === 'opening') { return; } - this.willClose.emit(); + this._willClose.emit(); this._state = 'closing'; this._triggerElement?.setAttribute('aria-expanded', 'false'); } @@ -166,24 +161,22 @@ export class SbbMenu implements ComponentInterface { /** * Handles click and checks if its target is a sbb-menu-action. */ - @Listen('click') - public async onClick(event: Event): Promise { + private _onClick(event: Event): void { const target = event.target as HTMLElement | undefined; if (target?.tagName === 'SBB-MENU-ACTION') { - await this.close(); + this.close(); } } - @Listen('keydown') - public handleKeyDown(evt: KeyboardEvent): void { + private _handleKeyDown(evt: KeyboardEvent): void { if (!isArrowKeyPressed(evt)) { return; } evt.preventDefault(); - const enabledActions: Element[] = Array.from( - this._element.querySelectorAll('SBB-MENU-ACTION'), - ).filter((el: HTMLElement) => el.tabIndex === 0 && interactivityChecker.isVisible(el)); + const enabledActions: Element[] = Array.from(this.querySelectorAll('sbb-menu-action')).filter( + (el: HTMLElement) => el.tabIndex === 0 && interactivityChecker.isVisible(el), + ); const current = enabledActions.findIndex((e: Element) => e === evt.target); const nextIndex = getNextElementIndex(evt, current, enabledActions.length); @@ -198,14 +191,13 @@ export class SbbMenu implements ComponentInterface { } if (event.key === 'Escape') { - await this.close(); + this.close(); return; } } // Removes trigger click listener on trigger change. - @Watch('trigger') - public removeTriggerClickListener( + private _removeTriggerClickListener( newValue: string | HTMLElement, oldValue: string | HTMLElement, ): void { @@ -216,17 +208,22 @@ export class SbbMenu implements ComponentInterface { } } - public connectedCallback(): void { + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener('click', (e) => this._onClick(e), { signal }); + this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); // Validate trigger element and attach event listeners this._configure(this.trigger); this._readActions(); if (this._state === 'opened') { - applyInertMechanism(this._element); + applyInertMechanism(this); } } - public disconnectedCallback(): void { + public override disconnectedCallback(): void { + super.disconnectedCallback(); this._menuController?.abort(); this._windowEventsController?.abort(); this._focusTrap.disconnect(); @@ -250,7 +247,7 @@ export class SbbMenu implements ComponentInterface { setAriaOverlayTriggerAttributes( this._triggerElement, 'menu', - this._element.id || this._menuId, + this.id || this._menuId, this._state, ); this._menuController = new AbortController(); @@ -283,10 +280,10 @@ export class SbbMenu implements ComponentInterface { } // Close menu at any click on an interactive element inside the that bubbles to the container. - private async _closeOnInteractiveElementClick(event: Event): Promise { + private _closeOnInteractiveElementClick(event: Event): void { const target = event.target as HTMLElement; if (INTERACTIVE_ELEMENTS.includes(target.nodeName) && !isValidAttribute(target, 'disabled')) { - await this.close(); + this.close(); } } @@ -296,9 +293,9 @@ export class SbbMenu implements ComponentInterface { }; // Close menu on backdrop click. - private _closeOnBackdropClick = async (event: PointerEvent): Promise => { + private _closeOnBackdropClick = (event: PointerEvent): void => { if (!this._isPointerDownEventOnMenu && !isEventOnElement(this._menu, event)) { - await this.close(); + this.close(); } }; @@ -309,10 +306,10 @@ export class SbbMenu implements ComponentInterface { private _onMenuAnimationEnd(event: AnimationEvent): void { if (event.animationName === 'open' && this._state === 'opening') { this._state = 'opened'; - this.didOpen.emit(); - applyInertMechanism(this._element); + this._didOpen.emit(); + applyInertMechanism(this); this._setMenuFocus(); - this._focusTrap.trap(this._element); + this._focusTrap.trap(this); this._attachWindowEvents(); } else if (event.animationName === 'close' && this._state === 'closing') { this._state = 'closed'; @@ -324,7 +321,7 @@ export class SbbMenu implements ComponentInterface { // When inside the sbb-header, we prevent the scroll to avoid the snapping to the top of the page preventScroll: this._triggerElement.tagName === 'SBB-HEADER-ACTION', }); - this.didClose.emit(); + this._didClose.emit(); this._windowEventsController?.abort(); this._focusTrap.disconnect(); @@ -335,7 +332,7 @@ export class SbbMenu implements ComponentInterface { // Set focus on the first focusable element. private _setMenuFocus(): void { - const firstFocusable = this._element.querySelector(IS_FOCUSABLE_QUERY) as HTMLElement; + const firstFocusable = this.querySelector(IS_FOCUSABLE_QUERY) as HTMLElement; setModalityOnNextFocus(firstFocusable); firstFocusable.focus(); } @@ -356,16 +353,16 @@ export class SbbMenu implements ComponentInterface { verticalOffset: MENU_OFFSET, }); - this._element.style.setProperty('--sbb-menu-position-x', `${menuPosition.left}px`); - this._element.style.setProperty('--sbb-menu-position-y', `${menuPosition.top}px`); - this._element.style.setProperty('--sbb-menu-max-height', menuPosition.maxHeight); + this.style.setProperty('--sbb-menu-position-x', `${menuPosition.left}px`); + this.style.setProperty('--sbb-menu-position-y', `${menuPosition.top}px`); + this.style.setProperty('--sbb-menu-max-height', menuPosition.maxHeight); } /** * Create an array with only the sbb-menu-action children */ private _readActions(): void { - const actions = Array.from(this._element.children); + const actions = Array.from(this.children); // If the slotted actions have not changed, we can skip syncing and updating the actions. if ( this._actions && @@ -376,55 +373,59 @@ export class SbbMenu implements ComponentInterface { } if (actions.every((e) => e.tagName === 'SBB-MENU-ACTION')) { - this._actions = actions as HTMLSbbMenuActionElement[]; + this._actions = actions as SbbMenuAction[]; } else { this._actions?.forEach((a) => a.removeAttribute('slot')); this._actions = undefined; } } - public render(): JSX.Element { + protected override render(): TemplateResult { if (this._actions) { this._actions.forEach((action, index) => action.setAttribute('slot', `action-${index}`)); } - return ( - this._menuId)}> -
+ setAttribute(this, 'data-state', this._state); + assignId(() => this._menuId)(this); + + return html` +
+
this._onMenuAnimationEnd(event)} + ${ref((el) => (this._menu = el as HTMLDivElement))} + class="sbb-menu" + >
this._onMenuAnimationEnd(event)} - ref={(el) => (this._menu = el)} - class="sbb-menu" + @click=${(event: Event) => this._closeOnInteractiveElementClick(event)} + ${ref((menuContentRef) => (this._menuContentElement = menuContentRef as HTMLElement))} + class="sbb-menu__content" > - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} -
this._closeOnInteractiveElementClick(event)} - ref={(menuContentRef) => (this._menuContentElement = menuContentRef)} - class="sbb-menu__content" - > - {this._actions ? ( - [ -
    - {this._actions.map((_, index) => ( -
  • - this._readActions()} - /> -
  • - ))} -
, + ${this._actions + ? html`
    + ${this._actions.map( + (_, index) => + html`
  • + this._readActions()} + > +
  • `, + )} +
, - ] - ) : ( - this._readActions()} /> - )} -
+ this._readActions()}> + ` + : html` this._readActions()}>`}
- - ); +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-menu': SbbMenu; } } diff --git a/src/components/sbb-menu/readme.md b/src/components/menu/menu/readme.md similarity index 50% rename from src/components/sbb-menu/readme.md rename to src/components/menu/menu/readme.md index 61d37d0e6e..c2c3babf75 100644 --- a/src/components/sbb-menu/readme.md +++ b/src/components/menu/menu/readme.md @@ -1,13 +1,13 @@ The `sbb-menu` is a component that can be attached to any element to open and display a custom context menu, -which allows to perform actions relevant to the current task or to navigate within or outside the application +which allows to perform actions relevant to the current task or to navigate within or outside the application by using the [sbb-menu-action](/docs/components-sbb-menu-sbb-menu-action--docs) component along with it. ## Interactions -The element that will trigger the menu dialog must be set using the `trigger` property. +The element that will trigger the menu dialog must be set using the `trigger` property. The `sbb-menu` appears on trigger left click, and it is displayed as a sheet with a backdrop on mobile, -while on desktop it will be shown as a floating menu, and it will calculate the optimal position relative to the trigger element +while on desktop it will be shown as a floating menu, and it will calculate the optimal position relative to the trigger element by evaluating the available space with the following priority: start/below, start/above, end/below, end/above. Clicking in the backdrop or pressing the `ESC` key closes the menu. @@ -21,7 +21,7 @@ Clicking in the backdrop or pressing the `ESC` key closes the menu. View Edit Details - + Cancel ``` @@ -37,81 +37,57 @@ You can also provide custom content inside the `sbb-menu`:
Christina Müller
UIS9057 Profile - + View Edit Details - + Cancel ``` -## Style +## Style If only `sbb-menu-action` components are provided, the items are automatically grouped within a list using `
    ` and `
  • ` items, for more complex scenarios the grouping must be done manually. -The default `z-index` of the component is set to `1000`; +The default `z-index` of the component is set to `1000`; to specify a custom stack order, the `z-index` can be changed by defining the CSS variable `--sbb-menu-z-index`. ## Accessibility As the menu opens, the focus will automatically be set to the first focusable item within the component. -When using the `sbb-menu` as a select (e.g. language selection) it's recommended to use the `aria-pressed` attribute +When using the `sbb-menu` as a select (e.g. language selection) it's recommended to use the `aria-pressed` attribute to identify which actions are active and which are not. - ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `disableAnimation` | `disable-animation` | Whether the animation is enabled. | `boolean` | `false` | -| `listAccessibilityLabel` | `list-accessibility-label` | This will be forwarded as aria-label to the inner list. Used only if the menu automatically renders the actions inside as a list. | `string` | `undefined` | -| `trigger` | `trigger` | The element that will trigger the menu overlay. Accepts both a string (id of an element) or an HTML element. | `HTMLElement \| string` | `undefined` | - - -## Events - -| Event | Description | Type | -| ------------ | ------------------------------------------------------ | ------------------- | -| `did-close` | Emits whenever the menu is closed. | `CustomEvent` | -| `did-open` | Emits whenever the menu is opened. | `CustomEvent` | -| `will-close` | Emits whenever the menu begins the closing transition. | `CustomEvent` | -| `will-open` | Emits whenever the menu starts the opening transition. | `CustomEvent` | - +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------------ | -------------------------- | ------- | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `trigger` | `trigger` | public | `string \| HTMLElement` | | The element that will trigger the menu overlay. Accepts both a string (id of an element) or an HTML element. | +| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Whether the animation is enabled. | +| `listAccessibilityLabel` | `list-accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner list. Used only if the menu automatically renders the actions inside as a list. | ## Methods -### `close() => Promise` - -Closes the menu. - -#### Returns - -Type: `Promise` - - - -### `open() => Promise` - -Opens the menu on trigger click. - -#### Returns - -Type: `Promise` - +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------- | ------- | -------------------------------- | ---------- | ------ | -------------- | +| `open` | public | Opens the menu on trigger click. | | `void` | | +| `close` | public | Closes the menu. | | `void` | | +## Events +| Name | Type | Description | Inherited From | +| ------------ | ------------------- | ------------------------------------------------------------ | -------------- | +| `will-open` | `CustomEvent` | Emits whenever the `sbb-menu` starts the opening transition. | | +| `did-open` | `CustomEvent` | Emits whenever the `sbb-menu` is opened. | | +| `will-close` | `CustomEvent` | Emits whenever the `sbb-menu` begins the closing transition. | | +| `did-close` | `CustomEvent` | Emits whenever the `sbb-menu` is closed. | | ## Slots -| Slot | Description | -| ----------- | ----------------------------------------------------- | -| `"unnamed"` | Use this slot to project any content inside the menu. | - - ----------------------------------------------- - - +| Name | Description | +| ---- | ---------------------------------------------------------------------------- | +| | Use the unnamed slot to add `sbb-menu-action` or other elements to the menu. | diff --git a/src/components/message/index.ts b/src/components/message/index.ts new file mode 100644 index 0000000000..f54558745b --- /dev/null +++ b/src/components/message/index.ts @@ -0,0 +1 @@ +export * from './message'; diff --git a/src/components/message/message.e2e.ts b/src/components/message/message.e2e.ts new file mode 100644 index 0000000000..cfce5c7957 --- /dev/null +++ b/src/components/message/message.e2e.ts @@ -0,0 +1,13 @@ +import { assert, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbMessage } from './message'; + +describe('sbb-message', () => { + let element: SbbMessage; + + it('renders', async () => { + element = await fixture(html``); + assert.instanceOf(element, SbbMessage); + }); +}); diff --git a/src/components/sbb-message/sbb-message.scss b/src/components/message/message.scss similarity index 96% rename from src/components/sbb-message/sbb-message.scss rename to src/components/message/message.scss index dc88d15aad..c34e452753 100644 --- a/src/components/sbb-message/sbb-message.scss +++ b/src/components/message/message.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/message/message.spec.ts b/src/components/message/message.spec.ts new file mode 100644 index 0000000000..6d31c867c9 --- /dev/null +++ b/src/components/message/message.spec.ts @@ -0,0 +1,81 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import '.'; + +describe('sbb-message', () => { + it('renders', async () => { + const root = await fixture( + html` + +

    Subtitle.

    +

    Error code: 0001

    + +
    `, + ); + + expect(root).dom.to.be.equal( + ` + + + +

    + Subtitle. +

    +

    + Error code: 0001 +

    + +
    + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    + + + + Title. + + + + + +
    + `, + ); + }); + + it('renders without optional slots', async () => { + const root = await fixture( + html` +

    Subtitle.

    +
    `, + ); + + expect(root).dom.to.be.equal( + ` + + +

    + Subtitle. +

    +
    + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    + + + + Title. + + + + + +
    + `, + ); + }); +}); diff --git a/src/components/sbb-message/sbb-message.stories.tsx b/src/components/message/message.stories.tsx similarity index 93% rename from src/components/sbb-message/sbb-message.stories.tsx rename to src/components/message/message.stories.tsx index e8b01d17f7..ab636bfc5e 100644 --- a/src/components/sbb-message/sbb-message.stories.tsx +++ b/src/components/message/message.stories.tsx @@ -1,10 +1,16 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; -import images from '../../global/images'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import images from '../core/images'; + +import readme from './readme.md?raw'; +import '../image'; +import '../title'; +import '../button'; +import './message'; const DefaultTemplate = (args): JSX.Element => ( @@ -121,7 +127,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), withActions as Decorator, diff --git a/src/components/message/message.ts b/src/components/message/message.ts new file mode 100644 index 0000000000..bc6df332aa --- /dev/null +++ b/src/components/message/message.ts @@ -0,0 +1,48 @@ +import { CSSResult, html, LitElement, TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import type { TitleLevel } from '../title'; +import '../title'; + +import style from './message.scss?lit&inline'; + +/** + * It displays a complex message combining a title, an image, an action and some content. + * + * @slot image - Use this slot to provide a sbb-image component. + * @slot title - Use this slot to provide title text for the component. + * @slot subtitle - Use this slot to provide a subtitle, must be a paragraph. + * @slot legend - Use this slot to provide a legend, must be a paragraph. + * @slot action - Use this slot to provide a sbb-button. + */ +@customElement('sbb-message') +export class SbbMessage extends LitElement { + public static override styles: CSSResult = style; + + /** Content of title. */ + @property({ attribute: 'title-content' }) public titleContent?: string; + + /** Level of title, it will be rendered as heading tag (e.g., h3). Defaults to level 3. */ + @property({ attribute: 'title-level' }) public titleLevel: TitleLevel = '3'; + + protected override render(): TemplateResult { + return html` +
    + + + ${this.titleContent} + + + + +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-message': SbbMessage; + } +} diff --git a/src/components/message/readme.md b/src/components/message/readme.md new file mode 100644 index 0000000000..7044608aa7 --- /dev/null +++ b/src/components/message/readme.md @@ -0,0 +1,45 @@ +The `sbb-message` component can be used to display a complex message. + +## Slots + +It renders by default a [sbb-title](/docs/components-sbb-title--docs), +which can be provided via `titleContent` property or `title` slot. +Optionally, the user can provide other elements such as a subtitle paragraph via the `subtitle` slot, +a [sbb-image](/docs/components-sbb-image--docs) to provide an image via the `image` slot, +a paragraph to provide an error code via the `legend` slot, +and a [sbb-button](/docs/components-sbb-button--docs) to provide a custom action via the `action` slot. + +```html + + +

    Subtitle

    +

    Error code: 0001

    + Action +
    +``` + +## Accessibility + +By default, the `sbb-title` has a visual level of 5 and an actual level of 3. +This can be changed by the user via the `title-level` property. +As all other elements are regularly slotted, their accessibility relies on the standard techniques provided +by the used components (e.g. `alt-text` and `aria-label`). + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------------- | --------------- | ------- | --------------------- | ------- | ----------------------------------------------------------------------------------- | +| `titleContent` | `title-content` | public | `string \| undefined` | | Content of title. | +| `titleLevel` | `title-level` | public | `TitleLevel` | `'3'` | Level of title, it will be rendered as heading tag (e.g., h3). Defaults to level 3. | + +## Slots + +| Name | Description | +| ---------- | --------------------------------------------------------- | +| `image` | Use this slot to provide a sbb-image component. | +| `title` | Use this slot to provide title text for the component. | +| `subtitle` | Use this slot to provide a subtitle, must be a paragraph. | +| `legend` | Use this slot to provide a legend, must be a paragraph. | +| `action` | Use this slot to provide a sbb-button. | diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts new file mode 100644 index 0000000000..ba994440fb --- /dev/null +++ b/src/components/navigation/index.ts @@ -0,0 +1,5 @@ +export * from './navigation'; +export * from './navigation-action'; +export * from './navigation-list'; +export * from './navigation-marker'; +export * from './navigation-section'; diff --git a/src/components/navigation/navigation-action/index.ts b/src/components/navigation/navigation-action/index.ts new file mode 100644 index 0000000000..2b5cf873e0 --- /dev/null +++ b/src/components/navigation/navigation-action/index.ts @@ -0,0 +1 @@ +export * from './navigation-action'; diff --git a/src/components/navigation/navigation-action/navigation-action.e2e.ts b/src/components/navigation/navigation-action/navigation-action.e2e.ts new file mode 100644 index 0000000000..a2df855e65 --- /dev/null +++ b/src/components/navigation/navigation-action/navigation-action.e2e.ts @@ -0,0 +1,77 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing'; + +import { SbbNavigationAction } from './navigation-action'; +import '.'; + +describe('sbb-navigation-action', () => { + let element: SbbNavigationAction; + + beforeEach(async () => { + element = await fixture( + html`Navigation Action`, + ); + }); + + describe('events', () => { + it('dispatches event on click', async () => { + const navigationAction = document.querySelector('sbb-navigation-action'); + const changeSpy = new EventSpy('click'); + navigationAction.click(); + await waitForCondition(() => changeSpy.events.length === 1); + expect(changeSpy.count).to.be.equal(1); + }); + + it('should dispatch click event on pressing Enter', async () => { + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: 'Enter' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Space', async () => { + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: ' ' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should dispatch click event on pressing Enter with href', async () => { + element.setAttribute('href', '#'); + await waitForLitRender(element); + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: 'Enter' }); + expect(changeSpy.count).to.be.greaterThan(0); + }); + + it('should not dispatch click event on pressing Space with href', async () => { + element.setAttribute('href', '#'); + await waitForLitRender(element); + + const changeSpy = new EventSpy('click'); + element.focus(); + await sendKeys({ press: ' ' }); + expect(changeSpy.count).not.to.be.greaterThan(0); + }); + + it('should receive focus', async () => { + element.focus(); + await waitForLitRender(element); + expect(document.activeElement.id).to.be.equal('focus-id'); + }); + }); + + it('renders as a button and triggers click event', async () => { + element = await fixture(html`Label`); + assert.instanceOf(element, SbbNavigationAction); + + const clickedSpy = new EventSpy('click'); + element.click(); + await waitForCondition(() => clickedSpy.events.length === 1); + expect(clickedSpy.count).to.be.equal(1); + }); +}); diff --git a/src/components/sbb-navigation-action/sbb-navigation-action.scss b/src/components/navigation/navigation-action/navigation-action.scss similarity index 95% rename from src/components/sbb-navigation-action/sbb-navigation-action.scss rename to src/components/navigation/navigation-action/navigation-action.scss index 48d1b02b74..7601440f27 100644 --- a/src/components/sbb-navigation-action/sbb-navigation-action.scss +++ b/src/components/navigation/navigation-action/navigation-action.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -12,7 +12,7 @@ --sbb-navigation-action-color: var(--sbb-color-cloud-default); } -:host([active]:not([active='false'])) { +:host([active]) { --sbb-navigation-action-color: var(--sbb-color-storm-default); @include sbb.if-forced-colors { diff --git a/src/components/navigation/navigation-action/navigation-action.spec.ts b/src/components/navigation/navigation-action/navigation-action.spec.ts new file mode 100644 index 0000000000..46ad68fc7c --- /dev/null +++ b/src/components/navigation/navigation-action/navigation-action.spec.ts @@ -0,0 +1,23 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import '.'; + +describe('sbb-navigation-action', () => { + it('renders', async () => { + const root = await fixture(html``); + + expect(root).dom.to.be.equal( + ` + + + `, + ); + expect(root).shadowDom.to.be.equal( + ` + + + + `, + ); + }); +}); diff --git a/src/components/sbb-navigation-action/sbb-navigation-action.stories.tsx b/src/components/navigation/navigation-action/navigation-action.stories.tsx similarity index 92% rename from src/components/sbb-navigation-action/sbb-navigation-action.stories.tsx rename to src/components/navigation/navigation-action/navigation-action.stories.tsx index b8228bfd45..d2f13d37a6 100644 --- a/src/components/sbb-navigation-action/sbb-navigation-action.stories.tsx +++ b/src/components/navigation/navigation-action/navigation-action.stories.tsx @@ -1,8 +1,10 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './navigation-action'; const size: InputType = { control: { @@ -83,7 +85,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), ], diff --git a/src/components/navigation/navigation-action/navigation-action.ts b/src/components/navigation/navigation-action/navigation-action.ts new file mode 100644 index 0000000000..7913a7bbf4 --- /dev/null +++ b/src/components/navigation/navigation-action/navigation-action.ts @@ -0,0 +1,166 @@ +import { spread } from '@open-wc/lit-helpers'; +import { CSSResult, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { html, unsafeStatic } from 'lit/static-html.js'; + +import { hostContext, setAttributes } from '../../core/dom'; +import { + documentLanguage, + HandlerRepository, + actionElementHandlerAspect, + languageChangeHandlerAspect, + ConnectedAbortController, +} from '../../core/eventing'; +import { i18nTargetOpensInNewWindow } from '../../core/i18n'; +import { + ButtonType, + LinkButtonRenderVariables, + LinkTargetType, + resolveRenderVariables, + targetsNewWindow, +} from '../../core/interfaces'; +import type { SbbNavigationMarker } from '../navigation-marker'; + +import style from './navigation-action.scss?lit&inline'; + +/** + * It displays an action element that can be used in the `sbb-navigation` component. + * + * @slot - Use the unnamed slot to add content to the `sbb-navigation-action`. + */ +@customElement('sbb-navigation-action') +export class SbbNavigationAction extends LitElement { + public static override styles: CSSResult = style; + + /** + * Action size variant. + */ + @property({ reflect: true }) public size?: 'l' | 'm' | 's' = 'l'; + + /** + * The href value you want to link to (if it is not present, navigation action becomes a button). + */ + @property() public href: string | undefined; + + /** + * Where to display the linked URL. + */ + @property() public target?: LinkTargetType | string | undefined; + + /** + * The relationship of the linked URL as space-separated link types. + */ + @property() public rel?: string | undefined; + + /** + * Whether the browser will show the download dialog on click. + */ + @property({ type: Boolean }) public download?: boolean; + + /** + * The type attribute to use for the button. + */ + @property() public type: ButtonType | undefined; + + /** + * Whether the action is active. + */ + @property({ reflect: true, type: Boolean }) + public set active(value: boolean) { + const oldValue = this.active; + if (value !== oldValue) { + this._active = value; + this._handleActiveChange(this.active, oldValue); + } + } + public get active(): boolean { + return this._active; + } + private _active = false; + + /** + * The name attribute to use for the button. + */ + @property({ reflect: true }) public name: string | undefined; + + /** + * The value attribute to use for the button. + */ + @property() public value?: string; + + @state() private _currentLanguage = documentLanguage(); + + private _navigationMarker: SbbNavigationMarker; + private _abort = new ConnectedAbortController(this); + + private _handlerRepository = new HandlerRepository( + this, + actionElementHandlerAspect, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + ); + + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener( + 'click', + () => { + if (!this.active && this._navigationMarker) { + this.active = true; + } + }, + { signal }, + ); + this._handlerRepository.connect(); + + // Check if the current element is nested inside a navigation marker. + this._navigationMarker = hostContext('sbb-navigation-marker', this) as SbbNavigationMarker; + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + } + + // Check whether the `active` attribute has been added or removed from the DOM + // and call the `select()` or `reset()` method accordingly. + private _handleActiveChange(newValue: boolean, oldValue: boolean): void { + if (newValue && !oldValue) { + this._navigationMarker?.select(this); + } else if (!newValue && oldValue) { + this._navigationMarker?.reset(); + } + } + + protected override render(): TemplateResult { + const { + tagName: TAG_NAME, + attributes, + hostAttributes, + }: LinkButtonRenderVariables = resolveRenderVariables(this); + + setAttributes(this, hostAttributes); + + /* eslint-disable lit/binding-positions */ + return html` + <${unsafeStatic(TAG_NAME)} class="sbb-navigation-action" ${spread(attributes)}> + + ${ + targetsNewWindow(this) + ? html` + . ${i18nTargetOpensInNewWindow[this._currentLanguage]} + ` + : nothing + } + + `; + /* eslint-disable lit/binding-positions */ + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-navigation-action': SbbNavigationAction; + } +} diff --git a/src/components/navigation/navigation-action/readme.md b/src/components/navigation/navigation-action/readme.md new file mode 100644 index 0000000000..b2aab13175 --- /dev/null +++ b/src/components/navigation/navigation-action/readme.md @@ -0,0 +1,48 @@ +The `sbb-navigation-action` component is an action element contained by +a [sbb-navigation-list](/docs/components-sbb-navigation-sbb-navigation-list--docs) component +or a [sbb-navigation-marker](/docs/components-sbb-navigation-sbb-navigation-marker--docs) component. + +## Link / button properties + +As the [sbb-link](/docs/components-sbb-link--docs) and the [sbb-button](/docs/components-sbb-button--docs), +the component can be internally rendered as a button or as a link, +depending on the value of the `href` property, so the associated properties are available +(`href`, `target`, `rel` and `download` for link; `type`, `name`, `value` and `form` for button). + +```html +Link + +Button +``` + +## Style + +The component has three different sizes, which can be changed using the `size` property (`l`, which is the default, `m` and `s`). + +```html +Link + +Button +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------- | ---------- | ------- | ---------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------- | +| `size` | `size` | public | `'l' \| 'm' \| 's' \| undefined` | `'l'` | Action size variant. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to (if it is not present, navigation action becomes a button). | +| `target` | `target` | public | `LinkTargetType \| string \| undefined \| undefined` | | Where to display the linked URL. | +| `rel` | `rel` | public | `string \| undefined \| 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. | +| `type` | `type` | public | `ButtonType \| undefined` | | The type attribute to use for the button. | +| `active` | `active` | public | `boolean` | | Whether the action is active. | +| `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. | + +## Slots + +| Name | Description | +| ---- | ------------------------------------------------------------------- | +| | Use the unnamed slot to add content to the `sbb-navigation-action`. | diff --git a/src/components/navigation/navigation-list/index.ts b/src/components/navigation/navigation-list/index.ts new file mode 100644 index 0000000000..d2cd952eac --- /dev/null +++ b/src/components/navigation/navigation-list/index.ts @@ -0,0 +1 @@ +export * from './navigation-list'; diff --git a/src/components/navigation/navigation-list/navigation-list.e2e.ts b/src/components/navigation/navigation-list/navigation-list.e2e.ts new file mode 100644 index 0000000000..258464a80b --- /dev/null +++ b/src/components/navigation/navigation-list/navigation-list.e2e.ts @@ -0,0 +1,34 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbNavigationList } from './navigation-list'; +import '../navigation-action'; + +describe('sbb-navigation-list', () => { + let element: SbbNavigationList; + + beforeEach(async () => { + element = await fixture(html` + + Label + + `); + }); + + it('renders', () => { + assert.instanceOf(element, SbbNavigationList); + }); + + it('automatic list generation', () => { + const list = element.shadowRoot.querySelector('ul'); + expect(list.className).to.be.equal('sbb-navigation-list__content'); + + const listItem = list.querySelector('li'); + expect(listItem).to.have.class('sbb-navigation-list__action'); + }); + + it('force size on children elements', () => { + const action = element.querySelector('sbb-navigation-action'); + expect(action).to.have.attribute('size', 'm'); + }); +}); diff --git a/src/components/sbb-navigation-list/sbb-navigation-list.scss b/src/components/navigation/navigation-list/navigation-list.scss similarity index 94% rename from src/components/sbb-navigation-list/sbb-navigation-list.scss rename to src/components/navigation/navigation-list/navigation-list.scss index 00b82829e5..e15b90b330 100644 --- a/src/components/sbb-navigation-list/sbb-navigation-list.scss +++ b/src/components/navigation/navigation-list/navigation-list.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/navigation/navigation-list/navigation-list.spec.ts b/src/components/navigation/navigation-list/navigation-list.spec.ts new file mode 100644 index 0000000000..42d18fd62c --- /dev/null +++ b/src/components/navigation/navigation-list/navigation-list.spec.ts @@ -0,0 +1,56 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './navigation-list'; + +describe('sbb-navigation-list', () => { + it('renders', async () => { + const root = await fixture( + html` + Tickets & Offers + Vacations & Recreation + Travel information + Help & Contact + `, + ); + + expect(root).dom.to.be.equal( + ` + + + Tickets & Offers + + + Vacations & Recreation + + + Travel information + + + Help & Contact + + + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    + + `, + ); + }); +}); diff --git a/src/components/sbb-navigation-list/sbb-navigation-list.stories.tsx b/src/components/navigation/navigation-list/navigation-list.stories.tsx similarity index 90% rename from src/components/sbb-navigation-list/sbb-navigation-list.stories.tsx rename to src/components/navigation/navigation-list/navigation-list.stories.tsx index 7ff71f38b9..f4d36591fc 100644 --- a/src/components/sbb-navigation-list/sbb-navigation-list.stories.tsx +++ b/src/components/navigation/navigation-list/navigation-list.stories.tsx @@ -1,8 +1,11 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './navigation-list'; +import '../navigation-action'; const label: InputType = { control: { @@ -58,7 +61,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), ], diff --git a/src/components/navigation/navigation-list/navigation-list.ts b/src/components/navigation/navigation-list/navigation-list.ts new file mode 100644 index 0000000000..57671b14ea --- /dev/null +++ b/src/components/navigation/navigation-list/navigation-list.ts @@ -0,0 +1,104 @@ +import { spread } from '@open-wc/lit-helpers'; +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { setAttribute } from '../../core/dom'; +import { + createNamedSlotState, + HandlerRepository, + namedSlotChangeHandlerAspect, +} from '../../core/eventing'; +import type { SbbNavigationAction } from '../navigation-action'; + +import style from './navigation-list.scss?lit&inline'; + +/** + * It can be used as a container for one or more `sbb-navigation-action` within a `sbb-navigation-section`. + * + * @slot - Use the unnamed slot to add content to the `sbb-navigation-list`. + * @slot label - Use this to provide a label element. + */ +@customElement('sbb-navigation-list') +export class SbbNavigationList extends LitElement { + public static override styles: CSSResult = style; + + /* + * The label to be shown before the action list. + */ + @property() public label?: string; + + /* + * Navigation action elements. + */ + @state() private _actions: SbbNavigationAction[]; + + /** + * State of listed named slots, by indicating whether any element for a named slot is defined. + */ + @state() private _namedSlots = createNamedSlotState('label'); + + private _handlerRepository = new HandlerRepository( + this, + namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), + ); + + /** + * Create an array with only the sbb-navigation-action children. + */ + private _readActions(): void { + this._actions = Array.from(this.children).filter( + (e): e is SbbNavigationAction => e.tagName === 'SBB-NAVIGATION-ACTION', + ); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._handlerRepository.connect(); + this._readActions(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + } + + protected override render(): TemplateResult { + const hasLabel = !!this.label || this._namedSlots['label']; + this._actions.forEach((action, index) => { + action.setAttribute('slot', `action-${index}`); + action.size = 'm'; + }); + const ariaLabelledByAttribute = hasLabel + ? { 'aria-labelledby': 'sbb-navigation-link-label-id' } + : {}; + + setAttribute(this, 'class', 'sbb-navigation-list'); + + return html` + ${hasLabel + ? html` + ${this.label} + ` + : nothing} +
      + ${this._actions.map( + (_, index) => html` +
    • + this._readActions()}> +
    • + `, + )} +
    + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-navigation-list': SbbNavigationList; + } +} diff --git a/src/components/navigation/navigation-list/readme.md b/src/components/navigation/navigation-list/readme.md new file mode 100644 index 0000000000..03ac1218c2 --- /dev/null +++ b/src/components/navigation/navigation-list/readme.md @@ -0,0 +1,26 @@ +The `sbb-navigation-list` component is a collection of [sbb-navigation-action](/docs/components-sbb-navigation-sbb-navigation-action--docs). +Its intended use is inside a [sbb-navigation-section](/docs/components-sbb-navigation-sbb-navigation-section--docs) component. +Optionally, a label can be provided via slot via the self-named property or the self-named slot. + +```html + + Label 1.1.1 + Label 1.1.2 + Label 1.1.3 + +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------- | --------- | ------- | --------------------- | ------- | ----------- | +| `label` | `label` | public | `string \| undefined` | | | + +## Slots + +| Name | Description | +| ------- | ----------------------------------------------------------------- | +| | Use the unnamed slot to add content to the `sbb-navigation-list`. | +| `label` | Use this to provide a label element. | diff --git a/src/components/navigation/navigation-marker/index.ts b/src/components/navigation/navigation-marker/index.ts new file mode 100644 index 0000000000..4cfd2b5d13 --- /dev/null +++ b/src/components/navigation/navigation-marker/index.ts @@ -0,0 +1 @@ +export * from './navigation-marker'; diff --git a/src/components/navigation/navigation-marker/navigation-marker.e2e.ts b/src/components/navigation/navigation-marker/navigation-marker.e2e.ts new file mode 100644 index 0000000000..2d8ae0d1fe --- /dev/null +++ b/src/components/navigation/navigation-marker/navigation-marker.e2e.ts @@ -0,0 +1,57 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import '../navigation-action'; +import { waitForLitRender } from '../../core/testing'; + +import { SbbNavigationMarker } from './navigation-marker'; +import '.'; + +describe('sbb-navigation-marker', () => { + let element: SbbNavigationMarker; + + beforeEach(async () => { + element = await fixture( + html` + Tickets & Offers + Vacations & Recreation + Travel information + Help & Contact + `, + ); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbNavigationMarker); + }); + + it('selects action on click', async () => { + const firstAction = element.querySelector('sbb-navigation-action#nav-1') as HTMLElement; + const secondAction = element.querySelector('sbb-navigation-action#nav-2') as HTMLElement; + + secondAction.click(); + await waitForLitRender(element); + + expect(secondAction).to.have.attribute('active'); + expect(firstAction).not.to.have.attribute('active'); + + firstAction.click(); + await waitForLitRender(element); + + expect(firstAction).to.have.attribute('active'); + expect(secondAction).not.to.have.attribute('active'); + }); + + it('automatic list generation', () => { + const list = element.shadowRoot.querySelector('ul'); + expect(list.className).to.be.equal('sbb-navigation-marker'); + + const listItem = list.querySelector('li'); + expect(listItem).to.have.class('sbb-navigation-marker__action'); + }); + + it('force size on children elements', () => { + const firstAction = element.querySelector('sbb-navigation-action#nav-1'); + expect(firstAction).to.have.attribute('size', 'l'); + }); +}); diff --git a/src/components/sbb-navigation-marker/sbb-navigation-marker.scss b/src/components/navigation/navigation-marker/navigation-marker.scss similarity index 98% rename from src/components/sbb-navigation-marker/sbb-navigation-marker.scss rename to src/components/navigation/navigation-marker/navigation-marker.scss index 081bbdeab3..aaf22260c0 100644 --- a/src/components/sbb-navigation-marker/sbb-navigation-marker.scss +++ b/src/components/navigation/navigation-marker/navigation-marker.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/navigation/navigation-marker/navigation-marker.spec.ts b/src/components/navigation/navigation-marker/navigation-marker.spec.ts new file mode 100644 index 0000000000..13d31a9a8f --- /dev/null +++ b/src/components/navigation/navigation-marker/navigation-marker.spec.ts @@ -0,0 +1,19 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import '.'; + +describe('sbb-navigation-marker', () => { + it('renders', async () => { + const root = await fixture(html``); + + expect(root).dom.to.be.equal(``); + expect(root).shadowDom.to.be.equal( + ` +
      + + `, + ); + }); +}); diff --git a/src/components/sbb-navigation-marker/sbb-navigation-marker.stories.tsx b/src/components/navigation/navigation-marker/navigation-marker.stories.tsx similarity index 93% rename from src/components/sbb-navigation-marker/sbb-navigation-marker.stories.tsx rename to src/components/navigation/navigation-marker/navigation-marker.stories.tsx index 6ca3b9070e..7cb7f9e418 100644 --- a/src/components/sbb-navigation-marker/sbb-navigation-marker.stories.tsx +++ b/src/components/navigation/navigation-marker/navigation-marker.stories.tsx @@ -1,8 +1,11 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './navigation-marker'; +import '../navigation-action'; const size: InputType = { control: { @@ -87,7 +90,7 @@ const meta: Meta = { decorators: [ (Story) => (
      - +
      ), ], diff --git a/src/components/navigation/navigation-marker/navigation-marker.ts b/src/components/navigation/navigation-marker/navigation-marker.ts new file mode 100644 index 0000000000..5a621c37b2 --- /dev/null +++ b/src/components/navigation/navigation-marker/navigation-marker.ts @@ -0,0 +1,132 @@ +import { CSSResult, html, LitElement, PropertyValues, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { setAttribute } from '../../core/dom'; +import { AgnosticResizeObserver } from '../../core/observers'; +import type { SbbNavigationAction } from '../navigation-action'; + +import style from './navigation-marker.scss?lit&inline'; + +/** + * It can be used as a container for one or more `sbb-navigation-action` within a `sbb-navigation`. + * + * @slot - Use the unnamed slot to add `sbb-navigation-action` elements into the `sbb-navigation-marker`. + */ +@customElement('sbb-navigation-marker') +export class SbbNavigationMarker extends LitElement { + public static override styles: CSSResult = style; + + /** + * Marker size variant. + */ + @property({ reflect: true }) public size?: 'l' | 's' = 'l'; + + /** + * Whether the list has an active action. + */ + @state() private _hasActiveAction = false; + + /** + * Navigation action elements. + */ + @state() private _actions: SbbNavigationAction[]; + + private _currentActiveAction: SbbNavigationAction; + private _navigationMarkerResizeObserver = new AgnosticResizeObserver(() => + this._setMarkerPosition(), + ); + + protected override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('size')) { + this._updateMarkerActions(); + } + } + + private _updateMarkerActions(): void { + for (const action of this._navigationActions) { + action.size = this.size; + } + + this._hasActiveAction = !!this._activeNavigationAction; + this._currentActiveAction = this._activeNavigationAction; + this._setMarkerPosition(); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._navigationMarkerResizeObserver.observe(this); + this._readActions(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._navigationMarkerResizeObserver.disconnect(); + } + + public select(action: SbbNavigationAction): void { + this.reset(); + action.active = true; + this._currentActiveAction = action; + this._hasActiveAction = true; + setTimeout(() => this._setMarkerPosition()); + } + + public reset(): void { + if (!this._hasActiveAction) { + return; + } + this._currentActiveAction.active = false; + this._hasActiveAction = false; + } + + private get _navigationActions(): SbbNavigationAction[] { + return Array.from(this.querySelectorAll('sbb-navigation-action')); + } + + private get _activeNavigationAction(): SbbNavigationAction { + return this._navigationActions.find((action) => action.active); + } + + // Create an array with only the sbb-navigation-action children. + private _readActions(): void { + this._actions = Array.from(this.children).filter( + (e): e is SbbNavigationAction => e.tagName === 'SBB-NAVIGATION-ACTION', + ); + } + + private _setMarkerPosition(): void { + if (this._hasActiveAction) { + this?.style.setProperty( + '--sbb-navigation-marker-position-y', + `${(this.shadowRoot.querySelector('[data-active]') as HTMLElement)?.offsetTop}px`, + ); + } + } + + protected override render(): TemplateResult { + this._actions.forEach((action, index) => action.setAttribute('slot', `action-${index}`)); + setAttribute(this, 'data-has-active-action', this._hasActiveAction); + + return html` +
        + ${this._actions.map( + (action, index) => html` +
      • + this._readActions()}> +
      • + `, + )} +
      + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-navigation-marker': SbbNavigationMarker; + } +} diff --git a/src/components/navigation/navigation-marker/readme.md b/src/components/navigation/navigation-marker/readme.md new file mode 100644 index 0000000000..e0f55c1595 --- /dev/null +++ b/src/components/navigation/navigation-marker/readme.md @@ -0,0 +1,44 @@ +The `sbb-navigation-marker` component is a collection of [sbb-navigation-action](/docs/components-sbb-navigation-sbb-navigation-action--docs). +Its intended use is inside a [sbb-navigation](/docs/components-sbb-navigation-sbb-navigation--docs) component. + +```html + + Label 1 + Label 2 + Label 3 + +``` + +## Style + +The component has a property named `size` which is proxied to all the `sbb-navigation-action` within it. +Possible values are `l` (default) and `s`. + +```html + + ... + +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------ | --------- | ------- | ------------------------- | ------- | -------------------- | +| `size` | `size` | public | `'l' \| 's' \| undefined` | `'l'` | Marker size variant. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| -------- | ------- | ----------- | ----------------------------- | ------ | -------------- | +| `select` | public | | `action: SbbNavigationAction` | `void` | | +| `reset` | public | | | `void` | | + +## Slots + +| Name | Description | +| ---- | ---------------------------------------------------------------------------------------------- | +| | Use the unnamed slot to add `sbb-navigation-action` elements into the `sbb-navigation-marker`. | diff --git a/src/components/navigation/navigation-section/index.ts b/src/components/navigation/navigation-section/index.ts new file mode 100644 index 0000000000..3c507ddeab --- /dev/null +++ b/src/components/navigation/navigation-section/index.ts @@ -0,0 +1 @@ +export * from './navigation-section'; diff --git a/src/components/navigation/navigation-section/navigation-section.e2e.ts b/src/components/navigation/navigation-section/navigation-section.e2e.ts new file mode 100644 index 0000000000..d98263d4aa --- /dev/null +++ b/src/components/navigation/navigation-section/navigation-section.e2e.ts @@ -0,0 +1,55 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForCondition, waitForLitRender } from '../../core/testing'; + +import { SbbNavigationSection } from './navigation-section'; +import '../navigation'; +import '../navigation-list'; +import '../navigation-action'; + +describe('sbb-navigation-section', () => { + let element: SbbNavigationSection; + + beforeEach(async () => { + await fixture(html` + + + + Tickets & Offers + Vacations & Recreation + Travel information + Help & Contact + + + + `); + element = document.querySelector('sbb-navigation-section'); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbNavigationSection); + }); + + it('opens the section', async () => { + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => element.getAttribute('data-state') === 'opened'); + expect(element).to.have.attribute('data-state', 'opened'); + }); + + it('closes the section', async () => { + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => element.getAttribute('data-state') === 'opened'); + expect(element).to.have.attribute('data-state', 'opened'); + + element.close(); + await waitForLitRender(element); + + await waitForCondition(() => element.getAttribute('data-state') === 'closed'); + expect(element).to.have.attribute('data-state', 'closed'); + }); +}); diff --git a/src/components/sbb-navigation-section/sbb-navigation-section.scss b/src/components/navigation/navigation-section/navigation-section.scss similarity index 97% rename from src/components/sbb-navigation-section/sbb-navigation-section.scss rename to src/components/navigation/navigation-section/navigation-section.scss index cb743ea7f5..04efadccbe 100644 --- a/src/components/sbb-navigation-section/sbb-navigation-section.scss +++ b/src/components/navigation/navigation-section/navigation-section.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -84,8 +84,8 @@ --sbb-navigation-section-display: block; } -:host([disable-animation]:not([disable-animation='false'])) { - --sbb-navigation-section-animation-duration: 0s; +:host([disable-animation]) { + --sbb-navigation-section-animation-duration: 0.1ms; } ::slotted(*) { diff --git a/src/components/navigation/navigation-section/navigation-section.spec.ts b/src/components/navigation/navigation-section/navigation-section.spec.ts new file mode 100644 index 0000000000..e610393a27 --- /dev/null +++ b/src/components/navigation/navigation-section/navigation-section.spec.ts @@ -0,0 +1,30 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './navigation-section'; + +describe('sbb-navigation-section', () => { + it('renders', async () => { + const root = await fixture(html``); + + expect(root).dom.to.be.equal( + ` + + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
      + +
      + `, + ); + }); +}); diff --git a/src/components/sbb-navigation-section/sbb-navigation-section.stories.tsx b/src/components/navigation/navigation-section/navigation-section.stories.tsx similarity index 90% rename from src/components/sbb-navigation-section/sbb-navigation-section.stories.tsx rename to src/components/navigation/navigation-section/navigation-section.stories.tsx index 94e0784681..31fe205613 100644 --- a/src/components/sbb-navigation-section/sbb-navigation-section.stories.tsx +++ b/src/components/navigation/navigation-section/navigation-section.stories.tsx @@ -1,12 +1,21 @@ /** @jsx h */ -import { Fragment, h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import isChromatic from 'chromatic'; -import { userEvent, waitFor, within } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; -import { waitForComponentsReady } from '../../global/testing/wait-for-components-ready'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import { waitForComponentsReady } from '../../../storybook/testing/wait-for-components-ready'; +import type { SbbNavigationMarker } from '../navigation-marker'; +import '../navigation-list'; +import '../navigation-action'; +import '../navigation-marker'; +import '../navigation'; +import '../../button'; +import '.'; + +import readme from './readme.md?raw'; // Story interaction executed after the story renders const playStory = async (trigger, canvasElement): Promise => { @@ -87,7 +96,7 @@ const navigationList = (label): JSX.Element[] => [ const onNavigationClose = (dialog): void => { dialog.addEventListener('didClose', () => { - (document.getElementById('nav-marker') as HTMLSbbNavigationMarkerElement).reset(); + (document.getElementById('nav-marker') as SbbNavigationMarker).reset(); }); }; @@ -169,7 +178,7 @@ const meta: Meta = { decorators: [ (Story) => (
      - +
      ), ], diff --git a/src/components/sbb-navigation-section/sbb-navigation-section.tsx b/src/components/navigation/navigation-section/navigation-section.ts similarity index 63% rename from src/components/sbb-navigation-section/sbb-navigation-section.tsx rename to src/components/navigation/navigation-section/navigation-section.ts index 9b7771ff08..131be316aa 100644 --- a/src/components/sbb-navigation-section/sbb-navigation-section.tsx +++ b/src/components/navigation/navigation-section/navigation-section.ts @@ -1,99 +1,114 @@ -import { - Component, - ComponentInterface, - Element, - h, - Host, - JSX, - Method, - Prop, - State, - Watch, -} from '@stencil/core'; +import { spread } from '@open-wc/lit-helpers'; +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref.js'; + import { assignId, getFirstFocusableElement, getFocusableElements, setModalityOnNextFocus, -} from '../../global/a11y'; -import { findReferencedElement, isBreakpoint, isValidAttribute } from '../../global/dom'; +} from '../../core/a11y'; +import { + findReferencedElement, + isBreakpoint, + isValidAttribute, + setAttribute, +} from '../../core/dom'; import { createNamedSlotState, documentLanguage, HandlerRepository, languageChangeHandlerAspect, namedSlotChangeHandlerAspect, -} from '../../global/eventing'; -import { i18nGoBack } from '../../global/i18n'; +} from '../../core/eventing'; +import { i18nGoBack } from '../../core/i18n'; import { removeAriaOverlayTriggerAttributes, SbbOverlayState, setAriaOverlayTriggerAttributes, -} from '../../global/overlay'; +} from '../../core/overlay'; +import type { SbbNavigation } from '../navigation'; +import type { SbbNavigationMarker } from '../navigation-marker'; + +import style from './navigation-section.scss?lit&inline'; +import '../../divider'; +import '../../button'; let nextId = 0; /** - * @slot unnamed - Use this to project any content inside the navigation section. + * It can be used as a container for `sbb-navigation-list` within a `sbb-navigation`. + * + * @slot - Use the unnamed slot to add content into the `sbb-navigation-section`. */ -@Component({ - shadow: true, - styleUrl: 'sbb-navigation-section.scss', - tag: 'sbb-navigation-section', -}) -export class SbbNavigationSection implements ComponentInterface { +@customElement('sbb-navigation-section') +export class SbbNavigationSection extends LitElement { + public static override styles: CSSResult = style; + /* * The label to be shown before the action list. */ - @Prop() public titleContent?: string; + @property({ attribute: 'title-content' }) public titleContent?: string; /** * The element that will trigger the navigation section. * Accepts both a string (id of an element) or an HTML element. */ - @Prop() public trigger: string | HTMLElement; + @property() + public set trigger(value: string | HTMLElement) { + const oldValue = this._trigger; + this._trigger = value; + this._removeTriggerClickListener(this._trigger, oldValue); + } + public get trigger(): string | HTMLElement { + return this._trigger; + } + private _trigger: string | HTMLElement = null; /** * This will be forwarded as aria-label to the nav element and is read as a title of the navigation-section. */ - @Prop() public accessibilityLabel: string | undefined; + @property({ attribute: 'accessibility-label' }) public accessibilityLabel: string | undefined; /** * This will be forwarded as aria-label to the back button element. */ - @Prop() public accessibilityBackLabel: string | undefined; + @property({ attribute: 'accessibility-back-label' }) public accessibilityBackLabel: + | string + | undefined; /** * Whether the animation is enabled. */ - @Prop({ reflect: true }) public disableAnimation = false; + @property({ attribute: 'disable-animation', reflect: true, type: Boolean }) + public disableAnimation = false; /** * The state of the navigation section. */ - @State() private _state: SbbOverlayState = 'closed'; + @state() private _state: SbbOverlayState = 'closed'; /** * State of listed named slots, by indicating whether any element for a named slot is defined. */ - @State() private _namedSlots = createNamedSlotState('title'); + @state() private _namedSlots = createNamedSlotState('title'); - @State() private _currentLanguage = documentLanguage(); + @state() private _currentLanguage = documentLanguage(); - @State() private _renderBackButton = this._isZeroToLargeBreakpoint(); + @state() private _renderBackButton = this._isZeroToLargeBreakpoint(); - private _firstLevelNavigation: HTMLSbbNavigationElement; + private _firstLevelNavigation: SbbNavigation; private _navigationSection: HTMLElement; private _navigationSectionContainerElement: HTMLElement; private _triggerElement: HTMLElement; private _navigationSectionController: AbortController; private _windowEventsController: AbortController; + private _timeoutController: ReturnType; private _navigationSectionId = `sbb-navigation-section-${++nextId}`; - @Element() private _element!: HTMLElement; - private _handlerRepository = new HandlerRepository( - this._element, + this, languageChangeHandlerAspect((l) => (this._currentLanguage = l)), namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), ); @@ -101,14 +116,13 @@ export class SbbNavigationSection implements ComponentInterface { /** * Opens the navigation section on trigger click. */ - @Method() - public async open(): Promise { + public open(): void { if (this._state !== 'closed' || !this._navigationSection) { return; } this._state = 'opening'; - this._element.inert = true; + this.inert = true; this._renderBackButton = this._isZeroToLargeBreakpoint(); this._triggerElement?.setAttribute('aria-expanded', 'true'); } @@ -116,21 +130,19 @@ export class SbbNavigationSection implements ComponentInterface { /** * Closes the navigation section. */ - @Method() - public async close(): Promise { + public close(): void { if (this._state !== 'opened') { return; } - await this._resetMarker(); + this._resetMarker(); this._state = 'closing'; - this._element.inert = true; + this.inert = true; this._triggerElement?.setAttribute('aria-expanded', 'false'); } // Removes trigger click listener on trigger change. - @Watch('trigger') - public removeTriggerClickListener( + private _removeTriggerClickListener( newValue: string | HTMLElement, oldValue: string | HTMLElement, ): void { @@ -158,18 +170,16 @@ export class SbbNavigationSection implements ComponentInterface { setAriaOverlayTriggerAttributes( this._triggerElement, 'menu', - this._element.id || this._navigationSectionId, + this.id || this._navigationSectionId, this._state, ); this._navigationSectionController = new AbortController(); this._triggerElement.addEventListener('click', () => this.open(), { signal: this._navigationSectionController.signal, }); - this._element.addEventListener( - 'keydown', - (event) => this._handleNavigationSectionFocus(event), - { signal: this._navigationSectionController.signal }, - ); + this.addEventListener('keydown', (event) => this._handleNavigationSectionFocus(event), { + signal: this._navigationSectionController.signal, + }); } private _setNavigationInert(): void { @@ -186,7 +196,7 @@ export class SbbNavigationSection implements ComponentInterface { private _onAnimationEnd(event: AnimationEvent): void { if (event.animationName === 'open' && this._state === 'opening') { this._state = 'opened'; - this._element.inert = false; + this.inert = false; this._attachWindowEvents(); this._setNavigationInert(); this._setNavigationSectionFocus(); @@ -223,12 +233,12 @@ export class SbbNavigationSection implements ComponentInterface { } // Check if the click was triggered on an element that should close the section. - private _handleNavigationSectionClose = async (event: Event): Promise => { + private _handleNavigationSectionClose = (event: Event): void => { const composedPathElements = event .composedPath() .filter((el) => el instanceof window.HTMLElement); if (composedPathElements.some((el) => this._isCloseElement(el as HTMLElement))) { - await this.close(); + this.close(); } }; @@ -252,24 +262,24 @@ export class SbbNavigationSection implements ComponentInterface { return isBreakpoint('zero', 'large'); } - private async _resetMarker(): Promise { + private _resetMarker(): void { if (this._isZeroToLargeBreakpoint()) { - await (this._triggerElement?.parentElement as HTMLSbbNavigationMarkerElement)?.reset(); + (this._triggerElement?.parentElement as SbbNavigationMarker)?.reset(); } } // Closes the navigation on "Esc" key pressed. - private async _onKeydownEvent(event: KeyboardEvent): Promise { + private _onKeydownEvent(event: KeyboardEvent): void { if (this._state === 'opened' && event.key === 'Escape') { - await this.close(); + this.close(); } } // Set focus on the first focusable element. private _setNavigationSectionFocus(): void { const firstFocusableElement = getFirstFocusableElement( - [this._element.shadowRoot.querySelector('#sbb-navigation-section-back-button')] - .concat(Array.from(this._element.children)) + [this.shadowRoot.querySelector('#sbb-navigation-section-back-button')] + .concat(Array.from(this.children)) .filter((e): e is HTMLElement => e instanceof window.HTMLElement), ); if (firstFocusableElement) { @@ -285,16 +295,14 @@ export class SbbNavigationSection implements ComponentInterface { // Dynamically get first and last focusable element, as this might have changed since opening overlay const navigationChildren: HTMLElement[] = Array.from( - this._element.closest('sbb-navigation').shadowRoot.children, + this.closest('sbb-navigation').shadowRoot.children, ) as HTMLElement[]; const navigationFocusableElements = getFocusableElements( navigationChildren, (el) => el.nodeName === 'SBB-NAVIGATION-SECTION', ); - const sectionChildren: HTMLElement[] = Array.from( - this._element.shadowRoot.children, - ) as HTMLElement[]; + const sectionChildren: HTMLElement[] = Array.from(this.shadowRoot.children) as HTMLElement[]; const sectionFocusableElements = getFocusableElements(sectionChildren); const firstFocusable = sectionFocusableElements[0] as HTMLElement; @@ -317,42 +325,45 @@ export class SbbNavigationSection implements ComponentInterface { } } - public connectedCallback(): void { + public override connectedCallback(): void { + super.connectedCallback(); this._handlerRepository.connect(); // Validate trigger element and attach event listeners this._configure(this.trigger); this._firstLevelNavigation = this._triggerElement?.closest('sbb-navigation'); } - public disconnectedCallback(): void { + public override disconnectedCallback(): void { + super.disconnectedCallback(); this._handlerRepository.disconnect(); this._navigationSectionController?.abort(); this._windowEventsController?.abort(); + clearTimeout(this._timeoutController); } - public render(): JSX.Element { - const backButton = ( + protected override render(): TemplateResult { + const backButton = html` - ); + `; - const labelElement = ( + const labelElement = html`
      - {this._renderBackButton && backButton} + ${this._renderBackButton ? backButton : nothing} - {this.titleContent} + ${this.titleContent}
      - ); + `; // Accessibility label should win over aria-labelledby let accessibilityAttributes: Record = { 'aria-labelledby': 'title' }; @@ -360,37 +371,45 @@ export class SbbNavigationSection implements ComponentInterface { accessibilityAttributes = { 'aria-label': this.accessibilityLabel }; } - return ( - this._navigationSectionId)} + setAttribute(this, 'slot', 'navigation-section'); + setAttribute(this, 'data-state', this._state); + setAttribute(this, 'aria-hidden', this._state !== 'opened' ? 'true' : null); + assignId(() => this._navigationSectionId)(this); + + return html` +
      (this._navigationSectionContainerElement = el as HTMLElement))} > -
      (this._navigationSectionContainerElement = el)} +
      - - ); +
      + +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-navigation-section': SbbNavigationSection; } } diff --git a/src/components/navigation/navigation-section/readme.md b/src/components/navigation/navigation-section/readme.md new file mode 100644 index 0000000000..0db5c1645b --- /dev/null +++ b/src/components/navigation/navigation-section/readme.md @@ -0,0 +1,48 @@ +The `sbb-navigation-section` is a container for both [sbb-navigation-list](/docs/components-sbb-navigation-sbb-navigation-list--docs) and [sbb-button](/docs/components-sbb-button--docs). +Its intended use is inside a [sbb-navigation](/docs/components-sbb-navigation-sbb-navigation--docs) component, in which it can be seen as a 'second-level' panel. + +## Trigger + +To display the `sbb-navigation-section` component you must provide a trigger element using the `trigger` property, +Optionally a label can be provided via slot or via the `titleContent` property. + +```html + + + Label 1.1.1 + Label 1.1.2 + ... + + Something + +``` + +## Accessibility + +When a navigation action is marked to indicate the user is currently on that page, `aria-current="page"` should be set on that action. +Similarly, if a navigation action is marked to indicate a selected option (e.g., the selected language) `aria-pressed` should be set on that action. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------------ | -------------------------- | ------- | ---------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | +| `titleContent` | `title-content` | public | `string \| undefined` | | | +| `trigger` | `trigger` | public | `string \| HTMLElement` | | The element that will trigger the navigation section. Accepts both a string (id of an element) or an HTML element. | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the nav element and is read as a title of the navigation-section. | +| `accessibilityBackLabel` | `accessibility-back-label` | public | `\| string \| undefined` | | This will be forwarded as aria-label to the back button element. | +| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Whether the animation is enabled. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------- | ------- | ---------------------------------------------- | ---------- | ------ | -------------- | +| `open` | public | Opens the navigation section on trigger click. | | `void` | | +| `close` | public | Closes the navigation section. | | `void` | | + +## Slots + +| Name | Description | +| ---- | ---------------------------------------------------------------------- | +| | Use the unnamed slot to add content into the `sbb-navigation-section`. | diff --git a/src/components/navigation/navigation/index.ts b/src/components/navigation/navigation/index.ts new file mode 100644 index 0000000000..701e50504d --- /dev/null +++ b/src/components/navigation/navigation/index.ts @@ -0,0 +1 @@ +export * from './navigation'; diff --git a/src/components/navigation/navigation/navigation.e2e.ts b/src/components/navigation/navigation/navigation.e2e.ts new file mode 100644 index 0000000000..23bcffdae6 --- /dev/null +++ b/src/components/navigation/navigation/navigation.e2e.ts @@ -0,0 +1,326 @@ +import { assert, expect, fixture, nextFrame } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import '../navigation-marker'; +import { SbbButton } from '../../button'; +import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing'; +import type { SbbNavigationAction } from '../navigation-action'; +import '../navigation-action'; +import type { SbbNavigationSection } from '../navigation-section'; +import '../navigation-section'; + +import { SbbNavigation } from './navigation'; + +describe('sbb-navigation', () => { + let element: SbbNavigation; + + beforeEach(async () => { + element = await fixture(html` + + + Tickets & Offers + Vacations & Recreation + Travel information + Help & Contact + + + + Label + Label + + + Label + Label + + + `); + }); + + it('renders', () => { + assert.instanceOf(element, SbbNavigation); + }); + + it('opens the navigation', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + }); + + it('closes the navigation', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + element.close(); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('closes the navigation on close button click', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + const closeButton: SbbButton = element.shadowRoot.querySelector('.sbb-navigation__close'); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + closeButton.click(); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('closes the navigation on Esc key press', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + + await sendKeys({ down: 'Tab' }); + await waitForLitRender(element); + + await sendKeys({ down: 'Escape' }); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + }); + + it('closes navigation with sbb-navigation-close', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + const section: SbbNavigationSection = element.querySelector('#first-section'); + const action: SbbNavigationAction = element.querySelector( + 'sbb-navigation-marker > sbb-navigation-action#action-1', + ); + const closeEl: SbbNavigationAction = element.querySelector( + 'sbb-navigation-marker > sbb-navigation-action[sbb-navigation-close]', + ); + + element.open(); + await waitForLitRender(element); + + action.click(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'opened'); + + closeEl.click(); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + expect(section).to.have.attribute('data-state', 'closed'); + }); + + it('opens navigation and opens section', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const section: SbbNavigationSection = element.querySelector('#first-section'); + const action: SbbNavigationAction = document.querySelector( + 'sbb-navigation > sbb-navigation-marker > sbb-navigation-action#action-1', + ); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'closed'); + + action.click(); + await waitForLitRender(element); + + await waitForCondition(() => section.getAttribute('data-state') === 'opened'); + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'opened'); + }); + + it('opens navigation and toggles sections', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const firstSection: SbbNavigationSection = document.querySelector('#first-section'); + const secondSection: SbbNavigationSection = document.querySelector('#second-section'); + const firstAction: SbbNavigationAction = document.querySelector( + 'sbb-navigation-marker > sbb-navigation-action#action-1', + ); + const secondAction: SbbNavigationAction = document.querySelector( + 'sbb-navigation-marker > sbb-navigation-action#action-2', + ); + + element.open(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(firstSection).to.have.attribute('data-state', 'closed'); + expect(secondSection).to.have.attribute('data-state', 'closed'); + + firstAction.click(); + + await waitForCondition(() => firstSection.getAttribute('data-state') === 'opened'); + expect(firstSection).to.have.attribute('data-state', 'opened'); + expect(secondSection).to.have.attribute('data-state', 'closed'); + + secondAction.click(); + + await waitForCondition(() => secondSection.getAttribute('data-state') === 'opened'); + expect(firstSection).to.have.attribute('data-state', 'closed'); + expect(secondSection).to.have.attribute('data-state', 'opened'); + }); + + it('closes the navigation and the section on close button click', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + const section: SbbNavigationSection = element.querySelector('#first-section'); + const action: SbbNavigationAction = document.querySelector( + 'sbb-navigation > sbb-navigation-marker > sbb-navigation-action#action-1', + ); + const closeButton: SbbButton = element.shadowRoot.querySelector('.sbb-navigation__close'); + + element.open(); + await waitForLitRender(element); + await nextFrame(); + + action.click(); + await waitForLitRender(element); + await nextFrame(); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + await waitForCondition(() => section.getAttribute('data-state') === 'opened'); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'opened'); + + closeButton.click(); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + await waitForCondition(() => section.getAttribute('data-state') === 'closed'); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + expect(section).to.have.attribute('data-state', 'closed'); + }); + + it('closes the navigation and the section on Esc key press', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const didCloseEventSpy = new EventSpy(SbbNavigation.events.didClose); + const section: SbbNavigationSection = element.querySelector('#first-section'); + const action: SbbNavigationAction = document.querySelector( + 'sbb-navigation > sbb-navigation-marker > sbb-navigation-action#action-1', + ); + + element.open(); + await waitForLitRender(element); + + action.click(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'opened'); + + await sendKeys({ down: 'Tab' }); + await waitForLitRender(element); + + await sendKeys({ down: 'Escape' }); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + expect(section).to.have.attribute('data-state', 'closed'); + }); + + it('closes section with sbb-navigation-section-close', async () => { + const didOpenEventSpy = new EventSpy(SbbNavigation.events.didOpen); + const section: SbbNavigationSection = document.querySelector('#first-section'); + const action: SbbNavigationAction = document.querySelector( + 'sbb-navigation > sbb-navigation-marker > sbb-navigation-action#action-1', + ); + const closeEl: SbbNavigationAction = document.querySelector( + 'sbb-navigation > sbb-navigation-section > sbb-navigation-action[sbb-navigation-section-close]', + ); + + element.open(); + await waitForLitRender(element); + + action.click(); + await waitForLitRender(element); + + await waitForCondition(() => didOpenEventSpy.events.length === 1); + expect(didOpenEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'opened'); + + closeEl.click(); + await waitForLitRender(element); + await waitForCondition(() => section.getAttribute('data-state') === 'closed'); + + expect(element).to.have.attribute('data-state', 'opened'); + expect(section).to.have.attribute('data-state', 'closed'); + }); +}); diff --git a/src/components/sbb-navigation/sbb-navigation.scss b/src/components/navigation/navigation/navigation.scss similarity index 97% rename from src/components/sbb-navigation/sbb-navigation.scss rename to src/components/navigation/navigation/navigation.scss index b2093c8483..80a8df69e2 100644 --- a/src/components/sbb-navigation/sbb-navigation.scss +++ b/src/components/navigation/navigation/navigation.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -94,8 +94,8 @@ } } -:host([disable-animation]:not([disable-animation='false'])) { - --sbb-navigation-animation-duration: 0s; +:host([disable-animation]) { + --sbb-navigation-animation-duration: 0.1ms; } .sbb-navigation__container { diff --git a/src/components/navigation/navigation/navigation.spec.ts b/src/components/navigation/navigation/navigation.spec.ts new file mode 100644 index 0000000000..acf3c226cb --- /dev/null +++ b/src/components/navigation/navigation/navigation.spec.ts @@ -0,0 +1,66 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './navigation'; +import '../../button'; + +describe('sbb-navigation', () => { + it('renders', async () => { + await fixture(html` + Navigation trigger + + + Tickets & Offers + Vacations & Recreation + + + `); + const nav = document.querySelector('sbb-navigation'); + + expect(nav).dom.to.be.equal( + ` + + + + Tickets & Offers + + + Vacations & Recreation + + + + `, + ); + expect(nav).shadowDom.to.be.equal( + ` +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + +
    + `, + ); + }); +}); diff --git a/src/components/sbb-navigation/sbb-navigation.stories.tsx b/src/components/navigation/navigation/navigation.stories.tsx similarity index 91% rename from src/components/sbb-navigation/sbb-navigation.stories.tsx rename to src/components/navigation/navigation/navigation.stories.tsx index 082ba6d7c3..9e877e2ca6 100644 --- a/src/components/sbb-navigation/sbb-navigation.stories.tsx +++ b/src/components/navigation/navigation/navigation.stories.tsx @@ -1,14 +1,23 @@ /** @jsx h */ -import { Fragment, h, JSX } from 'jsx-dom'; -import events from './sbb-navigation.events'; -import readme from './readme.md'; -import isChromatic from 'chromatic'; -import { userEvent, waitFor, within } from '@storybook/testing-library'; -import { expect } from '@storybook/jest'; -import { waitForComponentsReady } from '../../global/testing/wait-for-components-ready'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import { waitForComponentsReady } from '../../../storybook/testing/wait-for-components-ready'; +import type { SbbNavigationMarker } from '../navigation-marker'; + +import { SbbNavigation } from './navigation'; +import readme from './readme.md?raw'; + +import '../navigation-section'; +import '../navigation-marker'; +import '../navigation-list'; +import '../navigation-action'; +import '../../button'; // Story interaction executed after the story renders const playStory = async ({ canvasElement }): Promise => { @@ -133,7 +142,7 @@ const actionLabels = (num): JSX.Element[] => { const onNavigationClose = (dialog): void => { dialog.addEventListener('didClose', () => { - (document.getElementById('nav-marker') as HTMLSbbNavigationMarkerElement).reset(); + (document.getElementById('nav-marker') as SbbNavigationMarker).reset(); }); }; @@ -257,7 +266,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), withActions as Decorator, @@ -265,7 +274,12 @@ const meta: Meta = { parameters: { chromatic: { disableSnapshot: false }, actions: { - handles: [events.willOpen, events.didOpen, events.didClose, events.willClose], + handles: [ + SbbNavigation.events.willOpen, + SbbNavigation.events.didOpen, + SbbNavigation.events.didClose, + SbbNavigation.events.willClose, + ], }, backgrounds: { disable: true, diff --git a/src/components/sbb-navigation/sbb-navigation.tsx b/src/components/navigation/navigation/navigation.ts similarity index 54% rename from src/components/sbb-navigation/sbb-navigation.tsx rename to src/components/navigation/navigation/navigation.ts index 157b6dda3e..cfb1f99aaf 100644 --- a/src/components/sbb-navigation/sbb-navigation.tsx +++ b/src/components/navigation/navigation/navigation.ts @@ -1,27 +1,23 @@ +import { LitElement, CSSResult, TemplateResult, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref.js'; + +import { FocusTrap, assignId, setModalityOnNextFocus } from '../../core/a11y'; import { - Component, - ComponentInterface, - Element, - Event, - EventEmitter, - h, - Host, - JSX, - Listen, - Method, - Prop, - State, - Watch, -} from '@stencil/core'; -import { FocusTrap, assignId, setModalityOnNextFocus } from '../../global/a11y'; -import { ScrollHandler, isValidAttribute, findReferencedElement } from '../../global/dom'; + ScrollHandler, + isValidAttribute, + findReferencedElement, + setAttribute, +} from '../../core/dom'; import { documentLanguage, HandlerRepository, languageChangeHandlerAspect, -} from '../../global/eventing'; -import { i18nCloseNavigation } from '../../global/i18n'; -import { AgnosticMutationObserver } from '../../global/observers'; + EventEmitter, + ConnectedAbortController, +} from '../../core/eventing'; +import { i18nCloseNavigation } from '../../core/i18n'; +import { AgnosticMutationObserver } from '../../core/observers'; import { removeAriaOverlayTriggerAttributes, setAriaOverlayTriggerAttributes, @@ -29,7 +25,10 @@ import { SbbOverlayState, applyInertMechanism, removeInertMechanism, -} from '../../global/overlay'; +} from '../../core/overlay'; +import '../../button'; + +import style from './navigation.scss?lit&inline'; /** Configuration for the attribute to look at if a navigation section is displayed */ const navigationObserverConfig: MutationObserverInit = { @@ -40,84 +39,82 @@ const navigationObserverConfig: MutationObserverInit = { let nextId = 0; /** - * @slot unnamed - Use this to project any content inside the navigation. + * It displays a navigation menu, wrapping one or more `sbb-navigation-*` components. + * + * @slot - Use the unnamed slot to add `sbb-navigation-action` elements into the sbb-navigation menu. + * @event {CustomEvent} will-open - Emits whenever the `sbb-navigation` begins the opening transition. + * @event {CustomEvent} did-open - Emits whenever the `sbb-navigation` is opened. + * @event {CustomEvent} will-close - Emits whenever the `sbb-navigation` begins the closing transition. + * @event {CustomEvent} did-close - Emits whenever the `sbb-navigation` is closed. */ +@customElement('sbb-navigation') +export class SbbNavigation extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + willOpen: 'will-open', + didOpen: 'did-open', + willClose: 'will-close', + didClose: 'did-close', + } as const; -@Component({ - shadow: true, - styleUrl: 'sbb-navigation.scss', - tag: 'sbb-navigation', -}) -export class SbbNavigation implements ComponentInterface { /** * The element that will trigger the navigation. * Accepts both a string (id of an element) or an HTML element. */ - @Prop() public trigger: string | HTMLElement; + @property() + public set trigger(value: string | HTMLElement) { + const oldValue = this._trigger; + this._trigger = value; + this._removeTriggerClickListener(this._trigger, oldValue); + } + public get trigger(): string | HTMLElement { + return this._trigger; + } + private _trigger: string | HTMLElement = null; /** * This will be forwarded as aria-label to the close button element. */ - @Prop() public accessibilityCloseLabel: string | undefined; + @property({ attribute: 'accessibility-close-label' }) public accessibilityCloseLabel: + | string + | undefined; /** * Whether the animation is enabled. */ - @Prop({ reflect: true }) public disableAnimation = false; + @property({ attribute: 'disable-animation', reflect: true, type: Boolean }) + public disableAnimation = false; /** * The state of the navigation. */ - @State() private _state: SbbOverlayState = 'closed'; + @state() private _state: SbbOverlayState = 'closed'; /** * Whether a navigation section is displayed. */ - @State() private _activeNavigationSection: HTMLElement; + @state() private _activeNavigationSection: HTMLElement; - @State() private _currentLanguage = documentLanguage(); + @state() private _currentLanguage = documentLanguage(); - /** - * Emits whenever the navigation begins the opening transition. - */ - @Event({ - bubbles: true, - composed: true, - }) - public willOpen: EventEmitter; + /** Emits whenever the `sbb-navigation` begins the opening transition. */ + private _willOpen: EventEmitter = new EventEmitter(this, SbbNavigation.events.willOpen); - /** - * Emits whenever the navigation is opened. - */ - @Event({ - bubbles: true, - composed: true, - }) - public didOpen: EventEmitter; + /** Emits whenever the `sbb-navigation` is opened. */ + private _didOpen: EventEmitter = new EventEmitter(this, SbbNavigation.events.didOpen); - /** - * Emits whenever the navigation begins the closing transition. - */ - @Event({ - bubbles: true, - composed: true, - }) - public willClose: EventEmitter; + /** Emits whenever the `sbb-navigation` begins the closing transition. */ + private _willClose: EventEmitter = new EventEmitter(this, SbbNavigation.events.willClose); - /** - * Emits whenever the navigation is closed. - */ - @Event({ - bubbles: true, - composed: true, - }) - public didClose: EventEmitter; + /** Emits whenever the `sbb-navigation` is closed. */ + private _didClose: EventEmitter = new EventEmitter(this, SbbNavigation.events.didClose); private _navigation: HTMLDivElement; private _navigationContentElement: HTMLElement; private _triggerElement: HTMLElement; private _navigationController: AbortController; private _windowEventsController: AbortController; + private _abort = new ConnectedAbortController(this); private _focusTrap = new FocusTrap(); private _scrollHandler = new ScrollHandler(); private _isPointerDownEventOnNavigation: boolean; @@ -126,23 +123,20 @@ export class SbbNavigation implements ComponentInterface { ); private _navigationId = `sbb-navigation-${++nextId}`; - @Element() private _element!: HTMLElement; - private _handlerRepository = new HandlerRepository( - this._element, + this, languageChangeHandlerAspect((l) => (this._currentLanguage = l)), ); /** * Opens the navigation. */ - @Method() - public async open(): Promise { + public open(): void { if (this._state !== 'closed' || !this._navigation) { return; } - this.willOpen.emit(); + this._willOpen.emit(); this._state = 'opening'; // Disable scrolling for content below the navigation @@ -153,20 +147,18 @@ export class SbbNavigation implements ComponentInterface { /** * Closes the navigation. */ - @Method() - public async close(): Promise { + public close(): void { if (this._state !== 'opened') { return; } - this.willClose.emit(); + this._willClose.emit(); this._state = 'closing'; this._triggerElement?.setAttribute('aria-expanded', 'false'); } // Removes trigger click listener on trigger change. - @Watch('trigger') - public removeTriggerClickListener( + private _removeTriggerClickListener( newValue: string | HTMLElement, oldValue: string | HTMLElement, ): void { @@ -194,7 +186,7 @@ export class SbbNavigation implements ComponentInterface { setAriaOverlayTriggerAttributes( this._triggerElement, 'menu', - this._element.id || this._navigationId, + this.id || this._navigationId, this._state, ); this._navigationController = new AbortController(); @@ -212,9 +204,9 @@ export class SbbNavigation implements ComponentInterface { private _onAnimationEnd(event: AnimationEvent): void { if (event.animationName === 'open' && this._state === 'opening') { this._state = 'opened'; - this.didOpen.emit(); - applyInertMechanism(this._element); - this._focusTrap.trap(this._element, this._trapFocusFilter); + this._didOpen.emit(); + applyInertMechanism(this); + this._focusTrap.trap(this, this._trapFocusFilter); this._attachWindowEvents(); this._setNavigationFocus(); } else if (event.animationName === 'close' && this._state === 'closing') { @@ -224,7 +216,7 @@ export class SbbNavigation implements ComponentInterface { removeInertMechanism(); // To enable focusing other element than the trigger, we need to call focus() a second time. this._triggerElement?.focus(); - this.didClose.emit(); + this._didClose.emit(); this._windowEventsController?.abort(); this._focusTrap.disconnect(); @@ -240,13 +232,12 @@ export class SbbNavigation implements ComponentInterface { }); } - @Listen('click') - public async handleNavigationClose(event: Event): Promise { + private _handleNavigationClose(event: Event): void { const composedPathElements = event .composedPath() .filter((el) => el instanceof window.HTMLElement); if (composedPathElements.some((el) => this._isCloseElement(el as HTMLElement))) { - await this.close(); + this.close(); } } @@ -258,15 +249,15 @@ export class SbbNavigation implements ComponentInterface { } // Closes the navigation on "Esc" key pressed. - private async _onKeydownEvent(event: KeyboardEvent): Promise { + private _onKeydownEvent(event: KeyboardEvent): void { if (this._state === 'opened' && event.key === 'Escape') { - await this.close(); + this.close(); } } // Set focus on the first focusable element. private _setNavigationFocus(): void { - const closeButton = this._element.shadowRoot.querySelector( + const closeButton = this.shadowRoot.querySelector( '#sbb-navigation-close-button', ) as HTMLElement; setModalityOnNextFocus(closeButton); @@ -278,17 +269,17 @@ export class SbbNavigation implements ComponentInterface { this._isPointerDownEventOnNavigation = isEventOnElement(this._navigation, event) || isEventOnElement( - this._element - .querySelector('sbb-navigation-section[data-state="opened"]') - ?.shadowRoot.querySelector('nav.sbb-navigation-section') as HTMLElement, + this.querySelector('sbb-navigation-section[data-state="opened"]')?.shadowRoot.querySelector( + 'nav.sbb-navigation-section', + ) as HTMLElement, event, ); }; // Close navigation on backdrop click. - private _closeOnBackdropClick = async (event: PointerEvent): Promise => { + private _closeOnBackdropClick = (event: PointerEvent): void => { if (!this._isPointerDownEventOnNavigation && !isEventOnElement(this._navigation, event)) { - await this.close(); + this.close(); } }; @@ -296,25 +287,31 @@ export class SbbNavigation implements ComponentInterface { private _onNavigationSectionChange(mutationsList: MutationRecord[]): void { for (const mutation of mutationsList) { if ((mutation.target as HTMLElement).nodeName === 'SBB-NAVIGATION-SECTION') { - this._activeNavigationSection = this._element.querySelector( + this._activeNavigationSection = this.querySelector( 'sbb-navigation-section[data-state="opening"], sbb-navigation-section[data-state="opened"]', ); } } } - public connectedCallback(): void { + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener('click', (e) => this._handleNavigationClose(e), { signal }); this._handlerRepository.connect(); // Validate trigger element and attach event listeners this._configure(this.trigger); - this._navigationObserver.observe(this._element, navigationObserverConfig); + this._navigationObserver.observe(this, navigationObserverConfig); + this.addEventListener('pointerup', (event) => this._closeOnBackdropClick(event), { signal }); + this.addEventListener('pointerdown', (event) => this._pointerDownListener(event), { signal }); if (this._state === 'opened') { - applyInertMechanism(this._element); + applyInertMechanism(this); } } - public disconnectedCallback(): void { + public override disconnectedCallback(): void { + super.disconnectedCallback(); this._handlerRepository.disconnect(); this._navigationController?.abort(); this._windowEventsController?.abort(); @@ -323,50 +320,54 @@ export class SbbNavigation implements ComponentInterface { removeInertMechanism(); } - public render(): JSX.Element { - const closeButton = ( + protected override render(): TemplateResult { + const closeButton = html` - ); - return ( - this._navigationId)} - onPointerUp={(event) => this._closeOnBackdropClick(event)} - onPointerDown={(event) => this._pointerDownListener(event)} - > -
    -
    (this._navigation = navigationRef)} - id="sbb-navigation-overlay" - onAnimationEnd={(event: AnimationEvent) => this._onAnimationEnd(event)} - class="sbb-navigation" - > -
    {closeButton}
    -
    -
    (this._navigationContentElement = el)} - > - -
    + `; + + setAttribute(this, 'role', 'navigation'); + setAttribute(this, 'data-has-navigation-section', !!this._activeNavigationSection); + setAttribute(this, 'data-state', this._state); + assignId(() => this._navigationId)(this); + + return html` +
    +
    (this._navigation = navigationRef as HTMLDivElement))} + id="sbb-navigation-overlay" + @animationend=${(event: AnimationEvent) => this._onAnimationEnd(event)} + class="sbb-navigation" + > +
    ${closeButton}
    +
    +
    (this._navigationContentElement = el as HTMLElement))} + > +
    -
    - - ); + +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-navigation': SbbNavigation; } } diff --git a/src/components/navigation/navigation/readme.md b/src/components/navigation/navigation/readme.md new file mode 100644 index 0000000000..1d6d02c60b --- /dev/null +++ b/src/components/navigation/navigation/readme.md @@ -0,0 +1,91 @@ +The `sbb-navigation` component provides a way to present a navigation menu. + +Some of its features are: + +- uses a native dialog element; +- creates a backdrop for disabling interaction below the navigation; +- disables scrolling of the page content while open; +- manages focus properly by setting it on the first focusable element; +- can act as a host for components as [sbb-navigation-list](/docs/components-sbb-navigation-sbb-navigation-list--docs), + [sbb-navigation-marker](/docs/components-sbb-navigation-sbb-navigation-marker--docs) + and [sbb-navigation-section](/docs/components-sbb-navigation-sbb-navigation-section--docs); + +## Interactions + +To display the `sbb-navigation` component you can either provide a trigger element using the `trigger` property, +or call the `open()` method on the `sbb-navigation` component. + +```html + +Navigation trigger + + + + + Label 1 + Label 2 + Label 3 + + + + Language 1 + Language 2 + Language 3 + + + + Title 1 + + Label 1.1 + Label 1.1.1 + Label 1.1.2 + Label 1.1.3 + + ... + Something + + ... + +``` + +## Style + +The default `z-index` of the component is set to `1000`; +to specify a custom stack order, the `z-index` can be changed by defining the CSS variable `--sbb-navigation-z-index`. + +## Accessibility + +When a navigation action is marked to indicate the user is currently on that page, `aria-current="page"` should be set on that action. +Similarly, if a navigation action is marked to indicate a selected option (e.g., the selected language) `aria-pressed` should be set on that action. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------------- | --------------------------- | ------- | ---------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | +| `trigger` | `trigger` | public | `string \| HTMLElement` | | The element that will trigger the navigation. Accepts both a string (id of an element) or an HTML element. | +| `accessibilityCloseLabel` | `accessibility-close-label` | public | `\| string \| undefined` | | This will be forwarded as aria-label to the close button element. | +| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Whether the animation is enabled. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------- | ------- | ---------------------- | ---------- | ------ | -------------- | +| `open` | public | Opens the navigation. | | `void` | | +| `close` | public | Closes the navigation. | | `void` | | + +## Events + +| Name | Type | Description | Inherited From | +| ------------ | ------------------- | ------------------------------------------------------------------ | -------------- | +| `will-open` | `CustomEvent` | Emits whenever the `sbb-navigation` begins the opening transition. | | +| `did-open` | `CustomEvent` | Emits whenever the `sbb-navigation` is opened. | | +| `will-close` | `CustomEvent` | Emits whenever the `sbb-navigation` begins the closing transition. | | +| `did-close` | `CustomEvent` | Emits whenever the `sbb-navigation` is closed. | | + +## Slots + +| Name | Description | +| ---- | ------------------------------------------------------------------------------------------ | +| | Use the unnamed slot to add `sbb-navigation-action` elements into the sbb-navigation menu. | diff --git a/src/components/notification/index.ts b/src/components/notification/index.ts new file mode 100644 index 0000000000..d9b217ce3b --- /dev/null +++ b/src/components/notification/index.ts @@ -0,0 +1 @@ +export * from './notification'; diff --git a/src/components/notification/notification.e2e.ts b/src/components/notification/notification.e2e.ts new file mode 100644 index 0000000000..ca2de53ed0 --- /dev/null +++ b/src/components/notification/notification.e2e.ts @@ -0,0 +1,75 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbButton } from '../button'; +import { waitForCondition, EventSpy, waitForLitRender } from '../core/testing'; + +import { SbbNotification } from './notification'; + +import '../link'; + +describe('sbb-notification', () => { + let element: SbbNotification; + + beforeEach(async () => { + element = await fixture(html` + + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + Link one + + `); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbNotification); + }); + + it('closes the notification and removes it from the DOM', async () => { + const willCloseEventSpy = new EventSpy(SbbNotification.events.willClose); + const didCloseEventSpy = new EventSpy(SbbNotification.events.didClose); + + expect(element).not.to.be.null; + expect(element).to.have.attribute('data-state', 'opened'); + + element.close(); + await waitForLitRender(element); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + + element = document.querySelector('sbb-notification'); + expect(element).to.be.null; + }); + + it('closes the notification and removes it from the DOM on close button click', async () => { + const willCloseEventSpy = new EventSpy(SbbNotification.events.willClose); + const didCloseEventSpy = new EventSpy(SbbNotification.events.didClose); + const closeButton = element.shadowRoot.querySelector('.sbb-notification__close') as SbbButton; + + expect(element).not.to.be.null; + expect(element).to.have.attribute('data-state', 'opened'); + + closeButton.click(); + await waitForLitRender(element); + + await waitForCondition(() => willCloseEventSpy.events.length === 1); + expect(willCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + await waitForCondition(() => didCloseEventSpy.events.length === 1); + expect(didCloseEventSpy.count).to.be.equal(1); + await waitForLitRender(element); + + expect(element).to.have.attribute('data-state', 'closed'); + + element = document.querySelector('sbb-notification'); + expect(element).to.be.null; + }); +}); diff --git a/src/components/sbb-notification/sbb-notification.scss b/src/components/notification/notification.scss similarity index 97% rename from src/components/sbb-notification/sbb-notification.scss rename to src/components/notification/notification.scss index 5280c140d9..000f0dec87 100644 --- a/src/components/sbb-notification/sbb-notification.scss +++ b/src/components/notification/notification.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; @use 'sass:color'; @use 'node_modules/@sbb-esta/lyne-design-tokens/dist/scss/sbb-variables.scss' as sbb-tokens; @@ -58,8 +58,8 @@ margin: var(--sbb-notification-margin); } -:host(:is([data-resize-disable-animation], [disable-animation]:not([disable-animation='false']))) { - --sbb-notification-animation-duration: 0s; +:host(:is([data-resize-disable-animation], [disable-animation])) { + --sbb-notification-animation-duration: 0.1ms; } /* Types */ diff --git a/src/components/notification/notification.spec.ts b/src/components/notification/notification.spec.ts new file mode 100644 index 0000000000..6ecc30a906 --- /dev/null +++ b/src/components/notification/notification.spec.ts @@ -0,0 +1,146 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './notification'; +import '../link'; +import '../button'; +import '../icon'; +import '../divider'; + +describe('sbb-notification', () => { + it('renders', async () => { + const root = await fixture( + html`The quick brown fox jumps over the lazy dog.`, + ); + + expect(root).dom.to.be.equal( + ` + + The quick brown fox jumps over the lazy dog. + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    +
    + + + + + + + + +
    +
    + `, + ); + }); + + it('renders with a title', async () => { + const root = await fixture( + html`The quick brown fox jumps over the lazy dog.`, + ); + + expect(root).dom.to.be.equal( + ` + + The quick brown fox jumps over the lazy dog. + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    +
    + + + + + Title + + + + + + + + +
    +
    + `, + ); + }); + + it('renders with a slotted title', async () => { + const root = await fixture( + html`Slotted title + The quick brown fox jumps over the lazy dog. + `, + ); + + expect(root).dom.to.be.equal( + ` + + + Slotted title + + The quick brown fox jumps over the lazy dog. + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    +
    + + + + + + + + + + + +
    +
    + `, + ); + }); + + it('renders without the close button', async () => { + const root = await fixture( + html`The quick brown fox jumps over the lazy dog.`, + ); + + expect(root).dom.to.be.equal( + ` + + The quick brown fox jumps over the lazy dog. + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    +
    + + + + + Title + + + + +
    +
    + `, + ); + }); +}); diff --git a/src/components/sbb-notification/sbb-notification.stories.tsx b/src/components/notification/notification.stories.tsx similarity index 92% rename from src/components/sbb-notification/sbb-notification.stories.tsx rename to src/components/notification/notification.stories.tsx index c07cdb5aff..5399c84654 100644 --- a/src/components/sbb-notification/sbb-notification.stories.tsx +++ b/src/components/notification/notification.stories.tsx @@ -1,11 +1,15 @@ /** @jsx h */ -import events from './sbb-notification.events'; -import { Fragment, h, JSX } from 'jsx-dom'; -import readme from './readme.md'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; import isChromatic from 'chromatic/isChromatic'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import { SbbNotification } from './notification'; +import readme from './readme.md?raw'; + +import '../button'; +import '../link'; const titleContent: InputType = { control: { @@ -47,7 +51,7 @@ const basicArgs: Args = { }; const appendNotification = (args): void => { - const newNotification = document.createElement('SBB-NOTIFICATION') as HTMLSbbNotificationElement; + const newNotification = document.createElement('sbb-notification'); newNotification.style.setProperty( '--sbb-notification-margin', '0 0 var(--sbb-spacing-fixed-4x) 0', @@ -214,14 +218,19 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), withActions as Decorator, ], parameters: { actions: { - handles: [events.didOpen, events.didClose, events.willOpen, events.willClose], + handles: [ + SbbNotification.events.didOpen, + SbbNotification.events.didClose, + SbbNotification.events.willOpen, + SbbNotification.events.willClose, + ], }, backgrounds: { disable: true, diff --git a/src/components/notification/notification.ts b/src/components/notification/notification.ts new file mode 100644 index 0000000000..ecfa133970 --- /dev/null +++ b/src/components/notification/notification.ts @@ -0,0 +1,250 @@ +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref.js'; + +import { setAttribute, toggleDatasetEntry } from '../core/dom'; +import { + createNamedSlotState, + documentLanguage, + HandlerRepository, + languageChangeHandlerAspect, + namedSlotChangeHandlerAspect, + EventEmitter, +} from '../core/eventing'; +import { i18nCloseNotification } from '../core/i18n'; +import { AgnosticResizeObserver } from '../core/observers'; +import type { TitleLevel } from '../title'; + +import style from './notification.scss?lit&inline'; + +import '../button'; +import '../divider'; +import '../icon'; +import '../title'; + +const notificationTypes = new Map([ + ['info', 'circle-information-small'], + ['success', 'circle-tick-small'], + ['warn', 'circle-exclamation-point-small'], + ['error', 'circle-cross-small'], +]); + +/** + * It displays messages which require a user's attention without interrupting its tasks. + * + * @slot - Use the unnamed slot to add content to the notification message. + * @slot title - Use this to provide a notification title (optional). + * @event {CustomEvent} will-open - Emits whenever the `sbb-notification` starts the opening transition. + * @event {CustomEvent} did-open - Emits whenever the `sbb-notification` is opened. + * @event {CustomEvent} will-close - Emits whenever the `sbb-notification` begins the closing transition. + * @event {CustomEvent} did-close - Emits whenever the `sbb-notification` is closed. + */ +@customElement('sbb-notification') +export class SbbNotification extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + willOpen: 'will-open', + didOpen: 'did-open', + willClose: 'will-close', + didClose: 'did-close', + } as const; + + /** + * The type of the notification. + */ + @property({ reflect: true }) public type?: 'info' | 'success' | 'warn' | 'error' = 'info'; + + /** + * Content of title. + */ + @property({ attribute: 'title-content' }) public titleContent?: string; + + /** + * Level of title, it will be rendered as heading tag (e.g. h3). Defaults to level 3. + */ + @property({ attribute: 'title-level' }) public titleLevel: TitleLevel = '3'; + + /** + * Whether the notification is readonly. + * In readonly mode, there is no dismiss button offered to the user. + */ + @property({ reflect: true, type: Boolean }) public readonly = false; + + /** + * Whether the animation is enabled. + */ + @property({ attribute: 'disable-animation', reflect: true, type: Boolean }) + public disableAnimation = false; + + /** + * State of listed named slots, by indicating whether any element for a named slot is defined. + */ + @state() private _namedSlots = createNamedSlotState('title'); + + /** + * The state of the notification. + */ + @state() private _state: 'closed' | 'opening' | 'opened' | 'closing' = 'opened'; + + @state() private _currentLanguage = documentLanguage(); + + private _notificationElement: HTMLElement; + private _resizeObserverTimeout: ReturnType | null = null; + private _notificationResizeObserver = new AgnosticResizeObserver(() => + this._onNotificationResize(), + ); + + /** Emits whenever the `sbb-notification` starts the opening transition. */ + private _willOpen: EventEmitter = new EventEmitter(this, SbbNotification.events.willOpen); + + /** Emits whenever the `sbb-notification` is opened. */ + private _didOpen: EventEmitter = new EventEmitter(this, SbbNotification.events.didOpen); + + /** Emits whenever the `sbb-notification` begins the closing transition. */ + private _willClose: EventEmitter = new EventEmitter(this, SbbNotification.events.willClose); + + /** Emits whenever the `sbb-notification` is closed. */ + private _didClose: EventEmitter = new EventEmitter(this, SbbNotification.events.didClose); + + public close(): void { + if (this._state === 'opened') { + this._state = 'closing'; + this._willClose.emit(); + this.disableAnimation && this._handleClosing(); + } + } + + private _handlerRepository = new HandlerRepository( + this, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), + ); + + public override connectedCallback(): void { + super.connectedCallback(); + this._handlerRepository.connect(); + this._setInlineLinks(); + } + + protected override firstUpdated(): void { + this._willOpen.emit(); + this._setNotificationHeight(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + this._notificationResizeObserver.disconnect(); + } + + private _setInlineLinks(): void { + this.querySelectorAll('sbb-link')?.forEach((link) => (link.variant = 'inline')); + } + + private _setNotificationHeight(): void { + const notificationHeight = + this._notificationElement.scrollHeight && !this.disableAnimation + ? `${this._notificationElement.scrollHeight}px` + : 'auto'; + this.style.setProperty('--sbb-notification-height', notificationHeight); + } + + private _onNotificationResize(): void { + if (this._state !== 'opened') { + return; + } + + clearTimeout(this._resizeObserverTimeout); + + toggleDatasetEntry(this, 'resizeDisableAnimation', true); + this._setNotificationHeight(); + + // Disable the animation when resizing the notification to avoid strange height transition effects. + this._resizeObserverTimeout = setTimeout( + () => toggleDatasetEntry(this, 'resizeDisableAnimation', false), + 150, + ); + } + + private _onNotificationTransitionEnd(event: TransitionEvent): void { + if (this._state === 'closing' && event.propertyName === 'max-height') { + this._handleClosing(); + } + } + + private _onNotificationAnimationEnd(event: AnimationEvent): void { + if (this._state === 'opened' && event.animationName === 'open') { + this._handleOpening(); + } + } + + private _handleOpening(): void { + this._state = 'opened'; + this._didOpen.emit(); + this._notificationResizeObserver.observe(this._notificationElement); + } + + private _handleClosing(): void { + this._state = 'closed'; + this._didClose.emit(); + this._notificationResizeObserver.unobserve(this._notificationElement); + this.remove(); + } + + protected override render(): TemplateResult { + const hasTitle = !!this.titleContent || this._namedSlots['title']; + + setAttribute(this, 'data-state', this._state); + setAttribute(this, 'data-has-title', hasTitle); + + return html` +
    (this._notificationElement = el as HTMLElement))} + @transitionend=${(event: TransitionEvent) => this._onNotificationTransitionEnd(event)} + @animationend=${(event: AnimationEvent) => this._onNotificationAnimationEnd(event)} + > +
    + + + + ${hasTitle + ? html` + ${this.titleContent} + ` + : nothing} + this._setInlineLinks()}> + + + ${!this.readonly + ? html` + + this.close()} + aria-label=${i18nCloseNotification[this._currentLanguage]} + class="sbb-notification__close" + > + ` + : nothing} +
    +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-notification': SbbNotification; + } +} diff --git a/src/components/notification/readme.md b/src/components/notification/readme.md new file mode 100644 index 0000000000..3764719321 --- /dev/null +++ b/src/components/notification/readme.md @@ -0,0 +1,93 @@ +The `sbb-notification` is a component which purpose is to inform users of updates. +A notification is an element that displays a brief, important message +in a way that attracts the user's attention without interrupting the user's task. + +Inline notifications show up in task flows, to notify users of an action status or other information. +They usually appear at the top of the primary content area or close to the item needing attention. + +The `sbb-notification` is structured in the following way: + +- Icon: informs users of the notification type at a glance. +- Title (optional): gives users a quick overview of the notification. +- Close button (optional): closes the notification. +- Message: provides additional detail and/or actionable steps for the user to take. + +```html + + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + Link one + Link two + Link three + +``` + +Note that the notification only supports inline links, therefore any slotted link will be forced to be a `variant="inline"` link. + +## Variants + +The `sbb-notification` supports four types: `info` (default), `success`, `warn` and `error`, based on the type of the information displayed. + +```html +... + +... + +... +``` + +## States + +It is possible to display the component in `readonly` state by using the self-named property. +In this case, the close button will not be shown. + +```html + ... +``` + +## Interactions + +Inline notifications do not dismiss automatically. +They persist on the page until the user dismisses them or takes action that resolves the notification. + +By default, a close button is displayed to dismiss inline notifications. Including the close button is optional +and should not be included if it is critical for a user to read or interact with the notification by setting the `readonly` property to `true`. + +## Style + +If the `sbb-notification` host needs a margin, in order to properly animate it on open/close, +we suggest using the `--sbb-notification-margin` variable to set it. +For example, use `--sbb-notification-margin: 0 0 var(--sbb-spacing-fixed-4x) 0` to apply a bottom margin. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------ | ------------------- | ------- | ------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `type` | `type` | public | `'info' \| 'success' \| 'warn' \| 'error' \| undefined` | `'info'` | The type of the notification. | +| `titleContent` | `title-content` | public | `string \| undefined` | | Content of title. | +| `titleLevel` | `title-level` | public | `TitleLevel` | `'3'` | Level of title, it will be rendered as heading tag (e.g. h3). Defaults to level 3. | +| `readonly` | `readonly` | public | `boolean` | `false` | Whether the notification is readonly. In readonly mode, there is no dismiss button offered to the user. | +| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Whether the animation is enabled. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------- | ------- | ----------- | ---------- | ------ | -------------- | +| `close` | public | | | `void` | | + +## Events + +| Name | Type | Description | Inherited From | +| ------------ | ------------------- | -------------------------------------------------------------------- | -------------- | +| `will-open` | `CustomEvent` | Emits whenever the `sbb-notification` starts the opening transition. | | +| `did-open` | `CustomEvent` | Emits whenever the `sbb-notification` is opened. | | +| `will-close` | `CustomEvent` | Emits whenever the `sbb-notification` begins the closing transition. | | +| `did-close` | `CustomEvent` | Emits whenever the `sbb-notification` is closed. | | + +## Slots + +| Name | Description | +| ------- | ---------------------------------------------------------------- | +| | Use the unnamed slot to add content to the notification message. | +| `title` | Use this to provide a notification title (optional). | diff --git a/src/components/option/index.ts b/src/components/option/index.ts new file mode 100644 index 0000000000..aa9755a578 --- /dev/null +++ b/src/components/option/index.ts @@ -0,0 +1,2 @@ +export * from './optgroup'; +export * from './option'; diff --git a/src/components/option/optgroup/index.ts b/src/components/option/optgroup/index.ts new file mode 100644 index 0000000000..592fec910f --- /dev/null +++ b/src/components/option/optgroup/index.ts @@ -0,0 +1 @@ +export * from './optgroup'; diff --git a/src/components/option/optgroup/optgroup.e2e.ts b/src/components/option/optgroup/optgroup.e2e.ts new file mode 100644 index 0000000000..7ebebf2bdc --- /dev/null +++ b/src/components/option/optgroup/optgroup.e2e.ts @@ -0,0 +1,76 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForLitRender } from '../../core/testing'; +import type { SbbOption } from '../option'; +import '../option'; + +import { SbbOptGroup } from './optgroup'; + +describe('sbb-optgroup', () => { + let element: SbbOptGroup; + + beforeEach(async () => { + element = await fixture(html` + + Label 1 + Label 2 + Label 3 + + `); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbOptGroup); + }); + + it('disabled status is inherited', async () => { + const optionOne = document.querySelector('sbb-optgroup > sbb-option#option-1'); + const optionTwo = document.querySelector('sbb-optgroup > sbb-option#option-2'); + const optionThree = document.querySelector('sbb-optgroup > sbb-option#option-3'); + element.setAttribute('disabled', ''); + await waitForLitRender(element); + + expect(element).to.have.attribute('disabled'); + expect(optionOne).to.have.attribute('data-group-disabled'); + expect(optionTwo).to.have.attribute('data-group-disabled'); + expect(optionTwo).to.have.attribute('disabled'); + expect(optionThree).to.have.attribute('data-group-disabled'); + + element.removeAttribute('disabled'); + await waitForLitRender(element); + expect(optionTwo).not.to.have.attribute('data-group-disabled'); + expect(optionTwo).to.have.attribute('disabled'); + }); + + it('disabled status prevents changes', async () => { + const optionOne: SbbOption = document.querySelector('sbb-optgroup > sbb-option#option-1'); + const optionTwo: SbbOption = document.querySelector('sbb-optgroup > sbb-option#option-2'); + const optionThree: SbbOption = document.querySelector('sbb-optgroup > sbb-option#option-3'); + const options = [optionOne, optionTwo, optionThree]; + + options.forEach((opt) => expect(opt).not.to.have.attribute('selected')); + + element.setAttribute('disabled', ''); + await waitForLitRender(element); + expect(element).to.have.attribute('disabled'); + + // clicks should have no effect since the group is disabled + for (const opt of options) { + opt.click(); + await waitForLitRender(opt); + expect(opt).not.to.have.attribute('selected'); + } + + element.removeAttribute('disabled'); + await waitForLitRender(element); + for (const opt of options) { + opt.click(); + await waitForLitRender(opt); + } + + expect(optionOne).to.have.attribute('selected'); + expect(optionTwo).not.to.have.attribute('selected'); + expect(optionThree).to.have.attribute('selected'); + }); +}); diff --git a/src/components/sbb-optgroup/sbb-optgroup.scss b/src/components/option/optgroup/optgroup.scss similarity index 98% rename from src/components/sbb-optgroup/sbb-optgroup.scss rename to src/components/option/optgroup/optgroup.scss index 49251d65e6..b747083a87 100644 --- a/src/components/sbb-optgroup/sbb-optgroup.scss +++ b/src/components/option/optgroup/optgroup.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/option/optgroup/optgroup.spec.ts b/src/components/option/optgroup/optgroup.spec.ts new file mode 100644 index 0000000000..d067263e5c --- /dev/null +++ b/src/components/option/optgroup/optgroup.spec.ts @@ -0,0 +1,78 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import '../../autocomplete'; +import '../option'; +import './optgroup'; +import { isSafari } from '../../core/dom'; + +describe('sbb-optgroup', () => { + describe('autocomplete', function () { + it('renders', async () => { + const root = ( + await fixture(html` + + + 1 + 2 + + +
    + `) + ).querySelector('sbb-optgroup'); + const groupRoleAttr = 'aria-disabled="false" aria-label="Label" role="group"'; + + expect(root).dom.to.be.equal(` + + 1 + 2 + + `); + expect(root).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders disabled', async () => { + const root = ( + await fixture(html` + + + 1 + 2 + + +
    + `) + ).querySelector('sbb-optgroup'); + const groupRoleAttr = 'aria-disabled="true" aria-label="Label" role="group"'; + + expect(root).dom.to.be.equal(` + + 1 + 2 + + `); + + expect(root).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + }); +}); diff --git a/src/components/sbb-optgroup/sbb-optgroup.stories.tsx b/src/components/option/optgroup/optgroup.stories.tsx similarity index 92% rename from src/components/sbb-optgroup/sbb-optgroup.stories.tsx rename to src/components/option/optgroup/optgroup.stories.tsx index 2904668493..71884b6ed5 100644 --- a/src/components/sbb-optgroup/sbb-optgroup.stories.tsx +++ b/src/components/option/optgroup/optgroup.stories.tsx @@ -1,9 +1,15 @@ /** @jsx h */ -import { Fragment, h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; -import { StoryContext } from '@storybook/html'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import { StoryContext } from '@storybook/web-components'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import '../../form-field'; +import '../../autocomplete'; +import '../../select'; +import '../option'; +import './optgroup'; const wrapperStyle = (context: StoryContext): Record => ({ 'background-color': context.args.negative @@ -100,7 +106,7 @@ const defaultArgs: Args = { const borderDecorator: Decorator = (Story) => (
    - +
    ); @@ -176,7 +182,7 @@ const meta: Meta = { decorators: [ (Story, context) => (
    - +
    ), ], diff --git a/src/components/option/optgroup/optgroup.ts b/src/components/option/optgroup/optgroup.ts new file mode 100644 index 0000000000..98d7fb3ac8 --- /dev/null +++ b/src/components/option/optgroup/optgroup.ts @@ -0,0 +1,127 @@ +import { CSSResult, html, LitElement, TemplateResult, PropertyValues } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { isSafari, isValidAttribute, toggleDatasetEntry, setAttribute } from '../../core/dom'; +import { AgnosticMutationObserver } from '../../core/observers'; +import type { SbbOption, SbbOptionVariant } from '../option'; + +import style from './optgroup.scss?lit&inline'; +import '../../divider'; + +/** + * It can be used as a container for one or more `sbb-option`. + * + * @slot - Use the unnamed slot to add `sbb-option` elements to the `sbb-optgroup`. + */ +@customElement('sbb-optgroup') +export class SbbOptGroup extends LitElement { + public static override styles: CSSResult = style; + + /** Option group label. */ + @property() public label: string; + + /** Whether the group is disabled. */ + @property({ type: Boolean }) public disabled = false; + + @state() private _negative = false; + + private _negativeObserver = new AgnosticMutationObserver(() => this._onNegativeChange()); + + private _variant: SbbOptionVariant; + + /** + * On Safari, the groups labels are not read by VoiceOver. + * To solve the problem, we remove the role="group" and add a hidden span containing the group name + * TODO: We should periodically check if it has been solved and, if so, remove the property. + */ + private _inertAriaGroups = isSafari(); + + private get _isMultiple(): boolean { + return this._variant === 'select' && this.closest('sbb-select')?.hasAttribute('multiple'); + } + + private get _options(): SbbOption[] { + return Array.from(this.querySelectorAll('sbb-option')) as SbbOption[]; + } + + public override connectedCallback(): void { + super.connectedCallback(); + this._negativeObserver?.disconnect(); + this._negative = !!this.closest(`:is(sbb-autocomplete, sbb-select, sbb-form-field)[negative]`); + toggleDatasetEntry(this, 'negative', this._negative); + + this._negativeObserver.observe(this, { + attributes: true, + attributeFilter: ['data-negative'], + }); + + this._setVariantByContext(); + this._proxyGroupLabelToOptions(); + } + + protected override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('disabled')) { + this._proxyDisabledToOptions(); + } + if (changedProperties.has('label')) { + this._proxyGroupLabelToOptions(); + } + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._negativeObserver?.disconnect(); + } + + private _setVariantByContext(): void { + if (this.closest('sbb-autocomplete')) { + this._variant = 'autocomplete'; + } else if (this.closest('sbb-select')) { + this._variant = 'select'; + } + } + + private _proxyGroupLabelToOptions(): void { + if (!this._inertAriaGroups) { + return; + } + + this._options.forEach((opt) => opt.setGroupLabel(this.label)); + } + + private _proxyDisabledToOptions(): void { + for (const option of this._options) { + toggleDatasetEntry(option, 'groupDisabled', this.disabled); + } + } + + private _onNegativeChange(): void { + this._negative = isValidAttribute(this, 'data-negative'); + } + + protected override render(): TemplateResult { + setAttribute(this, 'role', !this._inertAriaGroups ? 'group' : null); + setAttribute(this, 'data-variant', this._variant); + setAttribute(this, 'data-multiple', this._isMultiple); + setAttribute(this, 'aria-label', !this._inertAriaGroups && this.label); + setAttribute(this, 'aria-disabled', !this._inertAriaGroups && this.disabled.toString()); + + return html` +
    + +
    + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-optgroup': SbbOptGroup; + } +} diff --git a/src/components/sbb-optgroup/readme.md b/src/components/option/optgroup/readme.md similarity index 58% rename from src/components/sbb-optgroup/readme.md rename to src/components/option/optgroup/readme.md index 29ac39abf4..98096a88cf 100644 --- a/src/components/sbb-optgroup/readme.md +++ b/src/components/option/optgroup/readme.md @@ -6,7 +6,7 @@ A [sbb-divider](/docs/components-sbb-divider--docs) is displayed at the bottom o ## Slots -It is possible to provide a set of `sbb-option` via an unnamed slot; +It is possible to provide a set of `sbb-option` via an unnamed slot; the component has also a `label` property as name of the group. ```html @@ -31,35 +31,15 @@ The component has a `disabled` property which sets all the `sbb-option` in the g - ## Properties -| Property | Attribute | Description | Type | Default | -| ---------- | ---------- | ------------------------------ | --------- | ----------- | -| `disabled` | `disabled` | Whether the group is disabled. | `boolean` | `false` | -| `label` | `label` | Option group label. | `string` | `undefined` | - +| Name | Attribute | Privacy | Type | Default | Description | +| ---------- | ---------- | ------- | --------- | ------- | ------------------------------ | +| `label` | `label` | public | `string` | | Option group label. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the group is disabled. | ## Slots -| Slot | Description | -| ----------- | ------------------------ | -| `"unnamed"` | Used to display options. | - - -## Dependencies - -### Depends on - -- [sbb-divider](../sbb-divider) - -### Graph -```mermaid -graph TD; - sbb-optgroup --> sbb-divider - style sbb-optgroup fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - +| Name | Description | +| ---- | ------------------------------------------------------------------------ | +| | Use the unnamed slot to add `sbb-option` elements to the `sbb-optgroup`. | diff --git a/src/components/option/option/index.ts b/src/components/option/option/index.ts new file mode 100644 index 0000000000..4216a00732 --- /dev/null +++ b/src/components/option/option/index.ts @@ -0,0 +1 @@ +export * from './option'; diff --git a/src/components/option/option/option.e2e.ts b/src/components/option/option/option.e2e.ts new file mode 100644 index 0000000000..5844bb67cd --- /dev/null +++ b/src/components/option/option/option.e2e.ts @@ -0,0 +1,79 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import '../../autocomplete'; +import { waitForLitRender, EventSpy } from '../../core/testing'; +import type { SbbFormField } from '../../form-field'; +import '../../form-field'; + +import { SbbOption } from './option'; + +describe('sbb-option', () => { + describe('autocomplete', () => { + let element: SbbFormField; + + beforeEach(async () => { + element = await fixture(html` + + + + Option 1 + Option 2 + Option 3 + + + `); + }); + + it('renders', async () => { + const option = element.querySelector('sbb-option'); + assert.instanceOf(option, SbbOption); + }); + + it('set selected and emits on click', async () => { + const selectionChangeSpy = new EventSpy(SbbOption.events.selectionChange); + const optionOne = element.querySelector('sbb-option'); + + optionOne.dispatchEvent(new CustomEvent('click')); + await waitForLitRender(element); + + expect(optionOne.selected).to.be.equal(true); + expect(selectionChangeSpy.count).to.be.equal(1); + }); + + it('highlight on input', async () => { + const input = element.querySelector('input'); + const autocomplete = element.querySelector('sbb-autocomplete'); + const options = element.querySelectorAll('sbb-option'); + const optionOneLabel = options[0].shadowRoot.querySelector('.sbb-option__label'); + const optionTwoLabel = options[1].shadowRoot.querySelector('.sbb-option__label'); + const optionThreeLabel = options[2].shadowRoot.querySelector('.sbb-option__label'); + + input.focus(); + await sendKeys({ press: '1' }); + await waitForLitRender(autocomplete); + + expect(optionOneLabel).dom.to.be.equal(` + + + Option + 1 + + + `); + expect(optionTwoLabel).dom.to.be.equal(` + + + Option 2 + + `); + expect(optionThreeLabel).dom.to.be.equal(` + + + Option 3 + + `); + }); + }); +}); diff --git a/src/components/sbb-option/sbb-option.scss b/src/components/option/option/option.scss similarity index 88% rename from src/components/sbb-option/sbb-option.scss rename to src/components/option/option/option.scss index fe3e6b70a2..e662a33235 100644 --- a/src/components/sbb-option/sbb-option.scss +++ b/src/components/option/option/option.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -31,17 +31,17 @@ --sbb-focus-outline-color: var(--sbb-focus-outline-color-dark); } -:host([active]:not([active='false'])) { +:host([active]) { --sbb-focus-outline-offset: calc(-1 * var(--sbb-spacing-fixed-1x)); } -:host(:hover:not([disabled]:not([disabled='false']), [data-group-disabled])) { +:host(:hover:not([disabled], [data-group-disabled])) { @include sbb.hover-mq($hover: true) { --sbb-option-background-color: var(--sbb-option-background-color-hover); } } -:host(:active:not([disabled]:not([disabled='false']), [data-group-disabled])) { +:host(:active:not([disabled], [data-group-disabled])) { --sbb-option-background-color: var(--sbb-option-background-color-active); } @@ -52,7 +52,7 @@ } } -:host(:is([data-group-disabled], [disabled]:not([disabled='false']))) { +:host(:is([data-group-disabled], [disabled])) { --sbb-option-cursor: default; @include sbb.if-forced-colors { @@ -70,7 +70,7 @@ } .sbb-option__label--highlight { - :host(:not(:is([disabled]:not([disabled='false']), [data-group-disabled]))) & { + :host(:not(:is([disabled], [data-group-disabled]))) & { @include sbb.text--bold; @include sbb.if-forced-colors { color: Highlight; @@ -97,14 +97,14 @@ -webkit-tap-highlight-color: transparent; -webkit-text-fill-color: var(--sbb-option-color); - :host([active]:not([active='false'])) & { + :host([active]) & { @include sbb.focus-outline; border-radius: var(--sbb-option-border-radius); } // Add inner border and background for disabled option when it's not multiple - :host(:is([data-group-disabled], [disabled]:not([disabled='false'])):not([data-multiple])) & { + :host(:is([data-group-disabled], [disabled]):not([data-multiple])) & { position: relative; z-index: 0; diff --git a/src/components/option/option/option.spec.ts b/src/components/option/option/option.spec.ts new file mode 100644 index 0000000000..dcee5aec32 --- /dev/null +++ b/src/components/option/option/option.spec.ts @@ -0,0 +1,68 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import '../../autocomplete'; +import './option'; + +describe('sbb-option', () => { + describe('autocomplete', () => { + it('renders selected and active', async () => { + const root = ( + await fixture(html` + + Option 1 + +
    + `) + ).querySelector('sbb-option'); + + expect(root).dom.to.be.equal(` + + Option 1 + + `); + expect(root).shadowDom.to.be.equal(` +
    +
    + + + + + + Option 1 + +
    +
    + `); + }); + + it('renders disabled', async () => { + const root = ( + await fixture(html` + + Option 1 + +
    + `) + ).querySelector('sbb-option'); + + expect(root).dom.to.be.equal(` + + Option 1 + + `); + expect(root).shadowDom.to.be.equal(` +
    +
    + + + + + + Option 1 + +
    +
    + `); + }); + }); +}); diff --git a/src/components/sbb-option/sbb-option.stories.tsx b/src/components/option/option/option.stories.tsx similarity index 90% rename from src/components/sbb-option/sbb-option.stories.tsx rename to src/components/option/option/option.stories.tsx index 38ea87f5ba..cd274c29b6 100644 --- a/src/components/sbb-option/sbb-option.stories.tsx +++ b/src/components/option/option/option.stories.tsx @@ -1,10 +1,14 @@ /** @jsx h */ -import { Fragment, h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import events from './sbb-option.events'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; -import { StoryContext } from '@storybook/html'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import { StoryContext } from '@storybook/web-components'; +import { Fragment, h, type JSX } from 'jsx-dom'; + +import { SbbOption } from './option'; +import readme from './readme.md?raw'; +import '../../form-field'; +import '../../select'; +import '../../autocomplete'; const wrapperStyle = (context: StoryContext): Record => ({ 'background-color': context.args.negative @@ -121,7 +125,7 @@ const SelectTemplate = (args): JSX.Element => ( const borderDecorator: Decorator = (Story) => (
    - +
    ); @@ -176,13 +180,13 @@ const meta: Meta = { decorators: [ (Story, context) => (
    - +
    ), ], parameters: { actions: { - handles: [events.selectionChange, events.optionSelected], + handles: [SbbOption.events.selectionChange, SbbOption.events.optionSelected], }, backgrounds: { disable: true, diff --git a/src/components/option/option/option.ts b/src/components/option/option/option.ts new file mode 100644 index 0000000000..7fd35602ef --- /dev/null +++ b/src/components/option/option/option.ts @@ -0,0 +1,325 @@ +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { assignId } from '../../core/a11y'; +import { + isSafari, + isValidAttribute, + isAndroid, + toggleDatasetEntry, + setAttribute, +} from '../../core/dom'; +import { + createNamedSlotState, + HandlerRepository, + namedSlotChangeHandlerAspect, + EventEmitter, + ConnectedAbortController, +} from '../../core/eventing'; +import { AgnosticMutationObserver } from '../../core/observers'; + +import style from './option.scss?lit&inline'; +import '../../visual-checkbox'; +import '../../icon'; + +let nextId = 0; + +/** Configuration for the attribute to look at if component is nested in a sbb-checkbox-group */ +const optionObserverConfig: MutationObserverInit = { + attributeFilter: ['data-group-disabled', 'data-negative'], +}; + +export type SbbOptionVariant = 'autocomplete' | 'select'; + +/** + * It displays on option item which can be used in `sbb-select` or `sbb-autocomplete`. + * + * @slot - Use the unnamed slot to add content to the option label. + * @slot icon - Use this slot to provide an icon. If `icon-name` is set, a sbb-icon will be used. + * @event {CustomEvent} option-selection-change - Emits when the option selection status changes. + * @event {CustomEvent} option-selected - Emits when an option was selected by user. + */ +@customElement('sbb-option') +export class SbbOption extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + selectionChange: 'option-selection-change', + optionSelected: 'option-selected', + } as const; + + /** Value of the option. */ + @property() public value?: string; + + /** + * 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. + */ + @property({ attribute: 'icon-name' }) public iconName?: string; + + /** Whether the option is currently active. */ + @property({ reflect: true, type: Boolean }) public active?: boolean; + + /** Whether the option is selected. */ + @property({ reflect: true, type: Boolean }) public selected = false; + + /** Whether the option is disabled. */ + @property({ type: Boolean }) public disabled?: boolean; + + /** Emits when the option selection status changes. */ + private _selectionChange: EventEmitter = new EventEmitter(this, SbbOption.events.selectionChange); + + /** Emits when an option was selected by user. */ + private _optionSelected: EventEmitter = new EventEmitter(this, SbbOption.events.optionSelected); + + /** Wheter to apply the negative styling */ + @state() private _negative = false; + + /** Whether the component must be set disabled due disabled attribute on sbb-checkbox-group. */ + @state() private _disabledFromGroup = false; + + /** State of listed named slots, by indicating whether any element for a named slot is defined. */ + @state() private _namedSlots = createNamedSlotState('icon'); + + @state() private _label: string; + + /** The portion of the highlighted label. */ + @state() private _highlightString: string; + + /** Disable the highlight of the label. */ + @state() private _disableLabelHighlight: boolean; + + @state() private _groupLabel: string; + + private _optionId = `sbb-option-${++nextId}`; + private _variant: SbbOptionVariant; + private _abort = new ConnectedAbortController(this); + + /** + * On Safari, the groups labels are not read by VoiceOver. + * To solve the problem, we remove the role="group" and add an hidden span containing the group name + * TODO: We should periodically check if it has been solved and, if so, remove the property. + */ + private _inertAriaGroups = isSafari(); + + private _handlerRepository = new HandlerRepository( + this, + namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), + ); + + /** MutationObserver on data attributes. */ + private _optionAttributeObserver = new AgnosticMutationObserver((mutationsList) => + this._onOptionAttributesChange(mutationsList), + ); + + private get _isAutocomplete(): boolean { + return this._variant === 'autocomplete'; + } + private get _isSelect(): boolean { + return this._variant === 'select'; + } + private get _isMultiple(): boolean { + return this.closest('sbb-select')?.hasAttribute('multiple'); + } + + /** + * Highlight the label of the option + * @param value the highlighted portion of the label + * @internal + */ + public highlight(value: string): void { + this._highlightString = value; + } + + /** + * Set the option group label (used for a11y) + * @param value the label of the option group + */ + public setGroupLabel(value: string): void { + this._groupLabel = value; + } + + /** + * @internal + */ + public setSelectedViaUserInteraction(selected: boolean): void { + this.selected = selected; + this._selectionChange.emit(); + if (this.selected) { + this._optionSelected.emit(); + } + } + + private _selectByClick(event): void { + if (this.disabled || this._disabledFromGroup) { + event.stopPropagation(); + return; + } + + if (this._isMultiple) { + event.stopPropagation(); + this.setSelectedViaUserInteraction(!this.selected); + } else { + this.setSelectedViaUserInteraction(true); + } + } + + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this._handlerRepository.connect(); + const parentGroup = this.closest('sbb-optgroup'); + if (parentGroup) { + this._disabledFromGroup = isValidAttribute(parentGroup, 'disabled'); + } + this._optionAttributeObserver.observe(this, optionObserverConfig); + + this._negative = !!this.closest( + // :is() selector not possible due to test environment + `sbb-autocomplete[negative],sbb-select[negative],sbb-form-field[negative]`, + ); + toggleDatasetEntry(this, 'negative', this._negative); + + this._setVariantByContext(); + + this.addEventListener('click', (e) => this._selectByClick(e), { signal, passive: true }); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + this._optionAttributeObserver.disconnect(); + } + + private _setVariantByContext(): void { + if (this.closest('sbb-autocomplete')) { + this._variant = 'autocomplete'; + return; + } + + if (this.closest('sbb-select')) { + this._variant = 'select'; + } + } + + /** Observe changes on data attributes and set the appropriate values. */ + private _onOptionAttributesChange(mutationsList: MutationRecord[]): void { + for (const mutation of mutationsList) { + if (mutation.attributeName === 'data-group-disabled') { + this._disabledFromGroup = isValidAttribute(this, 'data-group-disabled'); + } else if (mutation.attributeName === 'data-negative') { + this._negative = isValidAttribute(this, 'data-negative'); + } + } + } + + private _setupHighlightHandler(event): void { + if (!this._isAutocomplete) { + this._disableLabelHighlight = true; + return; + } + + const slotNodes = (event.target as HTMLSlotElement).assignedNodes(); + const labelNode = slotNodes.filter((el) => el.nodeType === Node.TEXT_NODE)[0] as Text; + + // Disable the highlight if the slot does not contain just a text node + if (!labelNode || slotNodes.length !== 1) { + this._disableLabelHighlight = true; + return; + } + this._label = labelNode.wholeText; + } + + private _getHighlightedLabel(): TemplateResult { + if (!this._highlightString || !this._highlightString.trim()) { + return html`${this._label}`; + } + + const matchIndex = this._label.toLowerCase().indexOf(this._highlightString.toLowerCase()); + + if (matchIndex === -1) { + return html`${this._label}`; + } + + const prefix = this._label.substring(0, matchIndex); + const highlighted = this._label.substring( + matchIndex, + matchIndex + this._highlightString.length, + ); + const postfix = this._label.substring(matchIndex + this._highlightString.length); + + return html` + ${prefix}${highlighted}${postfix} + `; + } + + protected override render(): TemplateResult { + const isMultiple = this._isMultiple; + setAttribute(this, 'role', 'option'); + setAttribute(this, 'tabindex', isAndroid() && !this.disabled && 0); + setAttribute(this, 'data-variant', this._variant); + setAttribute(this, 'data-multiple', isMultiple); + setAttribute(this, 'data-disable-highlight', this._disableLabelHighlight); + setAttribute(this, 'aria-selected', `${this.selected}`); + setAttribute(this, 'aria-disabled', `${this.disabled || this._disabledFromGroup}`); + assignId(() => this._optionId)(this); + + return html` +
    +
    + + ${!isMultiple + ? html` + + ${this.iconName ? html`` : nothing} + + ` + : nothing} + + + ${isMultiple + ? html` ` + : nothing} + + + + + + + ${this._isAutocomplete && this._label && !this._disableLabelHighlight + ? this._getHighlightedLabel() + : nothing} + ${this._inertAriaGroups && this._groupLabel + ? html` + (${this._groupLabel})` + : nothing} + + + + ${this._isSelect && !isMultiple && this.selected + ? html`` + : nothing} +
    +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-option': SbbOption; + } +} diff --git a/src/components/option/option/readme.md b/src/components/option/option/readme.md new file mode 100644 index 0000000000..367875193b --- /dev/null +++ b/src/components/option/option/readme.md @@ -0,0 +1,88 @@ +The `sbb-option` is a component which can be used to display items in components like +[sbb-autocomplete](/docs/components-sbb-autocomplete--docs) or a [sbb-select](/docs/components-sbb-select--docs). + +## Slots + +It is possible to provide a label via an unnamed slot; the component can optionally display a `sbb-icon` +at the component start using the `iconName` property or via custom content using the `icon` slot. +Icon space can be reserved even if the `iconName` property is not set by overriding the `--sbb-option-icon-container-display` variable. + +```html +Option label + +Option label +``` + +## States + +Like the native `option`, the component has a `value` property. + +The `selected`, `disabled` and `active` properties are connected to the self-named states. +When disabled, the selection via click is prevented. +If the `sbb-option` is nested in a `sbb-optgroup` component, it inherits from the parent the `disabled` state. + +```html +Option label + +Option label + +Option label +``` + +## Events + +Consumers can listen to the `optionSelected` event on the `sbb-option` component to intercept the selected value; +the event is triggered if the element has been selected by some user interaction. Alternatively, +the `selectionChange` event can be listened to, which is triggered if the element has been both selected or deselected. + +## Style + +If the label slot contains only a **text node**, it is possible to search for text in the `sbb-option` using the +`highlight` method, passing the desired text; if the text is present it will be highlighted in bold. + +```html + + Highlightable caption + + + + Not highlightable caption + + + + + Highlightable caption + +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------- | ----------- | ------- | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `value` | `value` | public | `string \| undefined` | | Value of the option. | +| `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. | +| `active` | `active` | public | `boolean \| undefined` | | Whether the option is currently active. | +| `selected` | `selected` | public | `boolean` | `false` | Whether the option is selected. | +| `disabled` | `disabled` | public | `boolean \| undefined` | | Whether the option is disabled. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| --------------- | ------- | ------------------------------------------ | --------------- | ------ | -------------- | +| `setGroupLabel` | public | Set the option group label (used for a11y) | `value: string` | `void` | | + +## Events + +| Name | Type | Description | Inherited From | +| ------------------------- | ------------------- | ----------------------------------------------- | -------------- | +| `option-selection-change` | `CustomEvent` | Emits when the option selection status changes. | | +| `option-selected` | `CustomEvent` | Emits when an option was selected by user. | | + +## Slots + +| Name | Description | +| ------ | --------------------------------------------------------------------------------- | +| | Use the unnamed slot to add content to the option label. | +| `icon` | Use this slot to provide an icon. If `icon-name` is set, a sbb-icon will be used. | diff --git a/src/components/package.json b/src/components/package.json new file mode 100644 index 0000000000..b341b6159a --- /dev/null +++ b/src/components/package.json @@ -0,0 +1,19 @@ +{ + "name": "@sbb-esta/lyne-components", + "version": "0.0.0-PLACEHOLDER", + "description": "Lyne Design System", + "keywords": [ + "design system", + "web components", + "lit", + "storybook" + ], + "type": "module", + "customElements": "custom-elements.json", + "dependencies": { + "lit": "0.0.0-LIT" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/src/components/pearl-chain-time/index.ts b/src/components/pearl-chain-time/index.ts new file mode 100644 index 0000000000..fbeecd6cbf --- /dev/null +++ b/src/components/pearl-chain-time/index.ts @@ -0,0 +1 @@ +export * from './pearl-chain-time'; diff --git a/src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.scss b/src/components/pearl-chain-time/pearl-chain-time.scss similarity index 94% rename from src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.scss rename to src/components/pearl-chain-time/pearl-chain-time.scss index 9ff6ba96c6..5ca1b60ee6 100644 --- a/src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.scss +++ b/src/components/pearl-chain-time/pearl-chain-time.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -41,6 +41,10 @@ margin-inline-start: calc(var(--sbb-spacing-fixed-2x) - sbb.px-to-rem-build(4)); } +.sbb-pearl-chain__time-walktime-prime-symbol { + float: right; +} + .sbb-pearl-chain__time-transfer { gap: var(--sbb-spacing-fixed-1x); } diff --git a/src/components/pearl-chain-time/pearl-chain-time.spec.ts b/src/components/pearl-chain-time/pearl-chain-time.spec.ts new file mode 100644 index 0000000000..5af7892103 --- /dev/null +++ b/src/components/pearl-chain-time/pearl-chain-time.spec.ts @@ -0,0 +1,237 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForLitRender } from '../core/testing'; +import { PtRideLeg } from '../core/timetable'; + +import type { SbbPearlChainTime } from './pearl-chain-time'; + +import './pearl-chain-time'; + +const now = new Date('2022-08-16T15:00:00Z').valueOf(); + +describe('sbb-pearl-chain-time', () => { + it('should render component with time', async () => { + const element = await fixture(html` + + + `); + element.legs = [ + { + __typename: 'PTRideLeg', + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + + + +
    + `); + }); + + it('should render component with departure walk', async () => { + const element = await fixture(html` + + + `); + element.legs = [ + { + __typename: 'PTRideLeg', + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + + + + + + + +
    + `); + }); + + it('should render component with arrival walk', async () => { + const element = await fixture(html` + + + `); + element.legs = [ + { + __typename: 'PTRideLeg', + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + + + + + + + +
    + `); + }); + + it('should render component with departure and arrival walk', async () => { + const element = await fixture(html` + + + `); + element.legs = [ + { + __typename: 'PTRideLeg', + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + + + + + + + + + + + +
    + `); + }); +}); diff --git a/src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.stories.tsx b/src/components/pearl-chain-time/pearl-chain-time.stories.tsx similarity index 88% rename from src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.stories.tsx rename to src/components/pearl-chain-time/pearl-chain-time.stories.tsx index 42217324c1..eaf9178ac5 100644 --- a/src/components/sbb-pearl-chain-time/sbb-pearl-chain-time.stories.tsx +++ b/src/components/pearl-chain-time/pearl-chain-time.stories.tsx @@ -1,10 +1,14 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import isChromatic from 'chromatic'; -import { extendedLeg, progressLeg } from '../sbb-pearl-chain/sbb-pearl-chain.sample-data'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { h, type JSX } from 'jsx-dom'; + +import { extendedLeg, progressLeg } from '../pearl-chain/pearl-chain.sample-data'; + +import readme from './readme.md?raw'; + +import './pearl-chain-time'; const departureWalk: InputType = { control: { @@ -60,7 +64,7 @@ const defaultArgs: Args = { }; const Template = (args): JSX.Element => { - return ; + return ; }; export const minimal: StoryObj = { @@ -117,7 +121,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), ], diff --git a/src/components/pearl-chain-time/pearl-chain-time.ts b/src/components/pearl-chain-time/pearl-chain-time.ts new file mode 100644 index 0000000000..c2ae8af656 --- /dev/null +++ b/src/components/pearl-chain-time/pearl-chain-time.ts @@ -0,0 +1,127 @@ +import { format } from 'date-fns'; +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { removeTimezoneFromISOTimeString } from '../core/datetime'; +import { documentLanguage, HandlerRepository, languageChangeHandlerAspect } from '../core/eventing'; +import { i18nDeparture, i18nArrival, i18nTransferProcedures } from '../core/i18n'; +import { getDepartureArrivalTimeAttribute, isRideLeg, Leg, PtRideLeg } from '../core/timetable'; + +import style from './pearl-chain-time.scss?lit&inline'; + +import '../pearl-chain'; + +/** + * Combined with `sbb-pearl-chain`, it displays walk time information. + */ +@customElement('sbb-pearl-chain-time') +export class SbbPearlChainTime extends LitElement { + public static override styles: CSSResult = style; + + /** + * define the legs of the pearl-chain. + * Format: + * `{"legs": [{"duration": 25}, ...]}` + * `duration` in minutes. Duration of the leg is relative + * to the total travel time. Example: departure 16:30, change at 16:40, + * arrival at 17:00. So the change should have a duration of 33.33%. + */ + @property({ type: Array }) public legs!: (Leg | PtRideLeg)[]; + + /** Prop to render the departure time - will be formatted as "H:mm" */ + @property({ attribute: 'departure-time' }) public departureTime?: string; + + /** Prop to render the arrival time - will be formatted as "H:mm" */ + @property({ attribute: 'arrival-time' }) public arrivalTime?: string; + + /** Optional prop to render the walk time (in minutes) before departure */ + @property({ attribute: 'departure-walk', type: Number }) public departureWalk?: number; + + /** Optional prop to render the walk time (in minutes) after arrival */ + @property({ attribute: 'arrival-walk', type: Number }) public arrivalWalk?: number; + + /** + * Per default, the current location has a pulsating animation. You can + * disable the animation with this property. + */ + @property({ attribute: 'disable-animation', type: Boolean }) public disableAnimation?: boolean; + + @state() private _currentLanguage = documentLanguage(); + + private _handlerRepository = new HandlerRepository( + this, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + ); + + public override connectedCallback(): void { + super.connectedCallback(); + this._handlerRepository.connect(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + } + + private _now(): number { + const dataNow = +this.dataset?.now; + return isNaN(dataNow) ? Date.now() : dataNow; + } + + protected override render(): TemplateResult { + const departure: Date | undefined = this.departureTime + ? removeTimezoneFromISOTimeString(this.departureTime) + : undefined; + const arrival: Date | undefined = this.arrivalTime + ? removeTimezoneFromISOTimeString(this.arrivalTime) + : undefined; + + const { renderDepartureTimeAttribute, renderArrivalTimeAttribute } = + getDepartureArrivalTimeAttribute( + this.legs, + this.departureWalk || 0, + this.arrivalWalk || 0, + this._currentLanguage, + ); + + const rideLegs = this.legs?.filter((leg) => isRideLeg(leg)); + return html` +
    + ${renderDepartureTimeAttribute()} + ${departure + ? html`` + : nothing} + ${rideLegs?.length > 1 + ? html` + ${rideLegs?.length - 1} ${i18nTransferProcedures[this._currentLanguage]} + ` + : nothing} + + ${arrival + ? html`` + : nothing} + ${renderArrivalTimeAttribute()} +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-pearl-chain-time': SbbPearlChainTime; + } +} diff --git a/src/components/pearl-chain-time/readme.md b/src/components/pearl-chain-time/readme.md new file mode 100644 index 0000000000..ba81f9cb4e --- /dev/null +++ b/src/components/pearl-chain-time/readme.md @@ -0,0 +1,64 @@ +The `sbb-pearl-chain-time` component adds an optional walk icon and a duration in minutes +before and/or after the [sbb-pearl-chain](/docs/timetable-sbb-pearl-chain--docs). + +The walk time indicates that the user has to walk to get to the destination, or to the station to begin the journey. + +The `legs` property is mandatory. + +```json +[ + { + "__typename": "PTRideLeg", + "arrival": { + "time": "2022-12-11T12:13:00+01:00" + }, + "departure": { + "time": "2022-12-07T12:11:00+01:00" + }, + "serviceJourney": { + "serviceAlteration": { + "cancelled": false, + "delayText": "string", + "reachable": true, + "unplannedStopPointsText": "" + }, + "notices": [ + { + "name": "CI", + "text": { + "template": "Extended boarding time (45')" + } + } + ] + } + } +] +``` + +```html + +``` + +## Testing + +To specify a specific date for the current datetime, you can use the `data-now` attribute (timestamp in milliseconds). +This is helpful if you need a specific state of the component. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------ | ------------------- | ------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `legs` | `legs` | public | `(Leg \| PtRideLeg)[]` | | define the legs of the pearl-chain. Format: `{"legs": \[{"duration": 25}, ...]}` `duration` in minutes. Duration of the leg is relative to the total travel time. Example: departure 16:30, change at 16:40, arrival at 17:00. So the change should have a duration of 33.33%. | +| `departureTime` | `departure-time` | public | `string \| undefined` | | Prop to render the departure time - will be formatted as "H:mm" | +| `arrivalTime` | `arrival-time` | public | `string \| undefined` | | Prop to render the arrival time - will be formatted as "H:mm" | +| `departureWalk` | `departure-walk` | public | `number \| undefined` | | Optional prop to render the walk time (in minutes) before departure | +| `arrivalWalk` | `arrival-walk` | public | `number \| undefined` | | Optional prop to render the walk time (in minutes) after arrival | +| `disableAnimation` | `disable-animation` | public | `boolean \| undefined` | | Per default, the current location has a pulsating animation. You can disable the animation with this property. | diff --git a/src/components/pearl-chain-vertical-item/index.ts b/src/components/pearl-chain-vertical-item/index.ts new file mode 100644 index 0000000000..f5231c3ff5 --- /dev/null +++ b/src/components/pearl-chain-vertical-item/index.ts @@ -0,0 +1 @@ +export * from './pearl-chain-vertical-item'; diff --git a/src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.scss b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.scss similarity index 98% rename from src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.scss rename to src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.scss index e16f3b123a..4e7663b254 100644 --- a/src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.scss +++ b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -182,7 +182,7 @@ slot[name='right']::slotted(*) { inset-block-start: var(--sbb-pearl-chain-vertical-item-position); - :host([disable-animation]:not([disable-animation='false'])) & { + :host([disable-animation]) & { animation: unset !important; } } diff --git a/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.spec.ts b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.spec.ts new file mode 100644 index 0000000000..cd21e4f2ba --- /dev/null +++ b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.spec.ts @@ -0,0 +1,243 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForLitRender } from '../core/testing'; + +import type { LineColor, SbbPearlChainVerticalItem } from './pearl-chain-vertical-item'; + +import './pearl-chain-vertical-item'; + +describe('sbb-pearl-chain-vertical-item', () => { + it('renders component with charcoal standard line and bullet', async () => { + const element = await fixture(html` + + + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'default', + bulletType: 'default', + minHeight: 100, + hideLine: false, + bulletSize: 'start-end', + position: 0, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders component with red line and bullet', async () => { + const element = await fixture(html` + + + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'disruption', + bulletType: 'default', + minHeight: 100, + hideLine: false, + bulletSize: 'start-end', + position: 0, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + + + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders component with left slot', async () => { + const element = await fixture(html` + +
    content
    +
    + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'dotted', + lineColor: 'charcoal' as LineColor, + bulletType: 'default', + minHeight: 100, + bulletSize: 'start-end', + hideLine: true, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + +
    content
    +
    + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders component with right slot', async () => { + const element = await fixture(html` + +
    right content
    +
    + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'disruption', + bulletType: 'past', + minHeight: 100, + bulletSize: 'start-end', + hideLine: true, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + +
    right content
    +
    + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders component with both slots', async () => { + const element = await fixture(html` + +
    right content
    +
    left content
    +
    + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'disruption', + bulletType: 'past', + minHeight: 100, + bulletSize: 'start-end', + hideLine: true, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + +
    right content
    +
    left content
    +
    + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders a position', async () => { + const element = await fixture(html` + +
    right content
    +
    left content
    +
    + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'disruption', + bulletType: 'default', + minHeight: 100, + hideLine: true, + bulletSize: 'start-end', + position: 50, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + +
    right content
    +
    left content
    +
    + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); + + it('renders a crossed-bullet', async () => { + const element = await fixture(html` + +
    right content
    +
    left content
    +
    + `); + + element.pearlChainVerticalItemAttributes = { + lineType: 'standard', + lineColor: 'disruption', + bulletType: 'skipped', + minHeight: 100, + hideLine: true, + bulletSize: 'start-end', + position: 0, + }; + await waitForLitRender(element); + expect(element).dom.to.be.equal(` + +
    right content
    +
    left content
    +
    + `); + expect(element).shadowDom.to.be.equal(` +
    + +
    + + + `); + }); +}); diff --git a/src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.stories.tsx b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.stories.tsx similarity index 88% rename from src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.stories.tsx rename to src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.stories.tsx index 8dbfae6afc..aaa52ab909 100644 --- a/src/components/sbb-pearl-chain-vertical-item/sbb-pearl-chain-vertical-item.stories.tsx +++ b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.stories.tsx @@ -1,7 +1,10 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj } from '@storybook/html'; +import type { Meta, StoryObj } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import '../pearl-chain-vertical'; +import './pearl-chain-vertical-item'; // We need to lie to the compiler in order for the CSS variables to work. // Remove once https://github.com/alex-kinokon/jsx-dom/pull/90 is merged and released. @@ -46,7 +49,7 @@ export const pearlChainItem: StoryObj = { }; const meta: Meta = { - decorators: [(Story) => ], + decorators: [(Story) => ], parameters: { docs: { extractComponentDescription: () => readme, diff --git a/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.ts b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.ts new file mode 100644 index 0000000000..05d9f2c325 --- /dev/null +++ b/src/components/pearl-chain-vertical-item/pearl-chain-vertical-item.ts @@ -0,0 +1,87 @@ +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import style from './pearl-chain-vertical-item.scss?lit&inline'; + +export type LineType = 'dotted' | 'standard' | 'thin'; + +export type BulletType = 'default' | 'past' | 'irrelevant' | 'skipped' | 'disruption'; + +export type LineColor = 'default' | 'past' | 'disruption' | 'walk'; + +export type BulletSize = 'start-end' | 'stop'; + +export interface PearlChainVerticalItemAttributes { + lineType: LineType; + lineColor: LineColor; + bulletType?: BulletType; + minHeight: number; + hideLine: boolean; + bulletSize: BulletSize; + position?: number; +} + +/** + * It displays details about connection between stations. + * + * @slot left - Content of the left side of the item + * @slot right - Content of the right side of the item + */ +@customElement('sbb-pearl-chain-vertical-item') +export class SbbPearlChainVerticalItem extends LitElement { + public static override styles: CSSResult = style; + + /** The pearlChainVerticalItemAttributes Prop for styling the bullets and line.*/ + @property({ attribute: 'pearl-chain-vertical-item-attributes', type: Object }) + public pearlChainVerticalItemAttributes!: PearlChainVerticalItemAttributes; + + /** If true, the position won't be animated. */ + @property({ attribute: 'disable-animation', reflect: true, type: Boolean }) + public disableAnimation?: boolean; + + protected override render(): TemplateResult { + const { bulletType, lineType, lineColor, hideLine, minHeight, bulletSize, position } = + this.pearlChainVerticalItemAttributes || {}; + + const bulletTypeClass = + position > 0 && position <= 100 + ? 'sbb-pearl-chain-vertical-item__bullet--past' + : `sbb-pearl-chain-vertical-item__bullet--${bulletType}`; + + return html` +
    + +
    + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-pearl-chain-vertical-item': SbbPearlChainVerticalItem; + } +} diff --git a/src/components/pearl-chain-vertical-item/readme.md b/src/components/pearl-chain-vertical-item/readme.md new file mode 100644 index 0000000000..bb5f9fae49 --- /dev/null +++ b/src/components/pearl-chain-vertical-item/readme.md @@ -0,0 +1,49 @@ +The `sbb-pearl-chain-vertical-item` is intended to be used +with the [sbb-pearl-chain-vertical](/docs/timetable-sbb-pearl-chain-vertical--docs)` component. + +It renders a table-row with three table-cells, and it is used to display the dots and line of the pearl-chain. +There are two slots named `left` and `right` which make it possible to display content on the component sides. + +The `pearlChainVerticalItemAttributes` property is mandatory. + +```json +{ + "lineType": "standard", + "lineColor": "charcoal", + "minHeight": "89", + "hideLine": false, + "bulletType": "thick-bullet", + "bulletSize": "small" +} +``` + +```html + +
    content
    +
    content
    + >
    +``` + +## Style + +The component has many styling options, which can be configured through the 'pearlChainVerticalItemAttributes' property. +The slots themselves are unstyled, so that they can be used in various ways. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ---------------------------------- | -------------------------------------- | ------- | ---------------------------------- | ------- | --------------------------------------------------------------------------- | +| `pearlChainVerticalItemAttributes` | `pearl-chain-vertical-item-attributes` | public | `PearlChainVerticalItemAttributes` | | The pearlChainVerticalItemAttributes Prop for styling the bullets and line. | +| `disableAnimation` | `disable-animation` | public | `boolean \| undefined` | | If true, the position won't be animated. | + +## Slots + +| Name | Description | +| ------- | ------------------------------------- | +| `left` | Content of the left side of the item | +| `right` | Content of the right side of the item | diff --git a/src/components/pearl-chain-vertical/index.ts b/src/components/pearl-chain-vertical/index.ts new file mode 100644 index 0000000000..3bdf8d4052 --- /dev/null +++ b/src/components/pearl-chain-vertical/index.ts @@ -0,0 +1 @@ +export * from './pearl-chain-vertical'; diff --git a/src/components/pearl-chain-vertical/pearl-chain-vertical.e2e.ts b/src/components/pearl-chain-vertical/pearl-chain-vertical.e2e.ts new file mode 100644 index 0000000000..e8e333102d --- /dev/null +++ b/src/components/pearl-chain-vertical/pearl-chain-vertical.e2e.ts @@ -0,0 +1,13 @@ +import { assert, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbPearlChainVertical } from './pearl-chain-vertical'; + +describe('sbb-pearl-chain', () => { + let element: SbbPearlChainVertical; + + it('renders', async () => { + element = await fixture(html``); + assert.instanceOf(element, SbbPearlChainVertical); + }); +}); diff --git a/src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.scss b/src/components/pearl-chain-vertical/pearl-chain-vertical.scss similarity index 90% rename from src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.scss rename to src/components/pearl-chain-vertical/pearl-chain-vertical.scss index 6ce3de4f43..96bcb6f4ad 100644 --- a/src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.scss +++ b/src/components/pearl-chain-vertical/pearl-chain-vertical.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/pearl-chain-vertical/pearl-chain-vertical.spec.ts b/src/components/pearl-chain-vertical/pearl-chain-vertical.spec.ts new file mode 100644 index 0000000000..1a79acc3e2 --- /dev/null +++ b/src/components/pearl-chain-vertical/pearl-chain-vertical.spec.ts @@ -0,0 +1,23 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForLitRender } from '../core/testing'; + +import type { SbbPearlChainVertical } from './pearl-chain-vertical'; + +import './pearl-chain-vertical'; + +describe('sbb-pearl-chain-vertical', () => { + it('renders', async () => { + const element = await fixture( + html``, + ); + await waitForLitRender(element); + expect(element).dom.to.be.equal(``); + expect(element).shadowDom.to.be.equal(` +
    + +
    + `); + }); +}); diff --git a/src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.stories.tsx b/src/components/pearl-chain-vertical/pearl-chain-vertical.stories.tsx similarity index 98% rename from src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.stories.tsx rename to src/components/pearl-chain-vertical/pearl-chain-vertical.stories.tsx index 0c1acf48e6..9192e0c443 100644 --- a/src/components/sbb-pearl-chain-vertical/sbb-pearl-chain-vertical.stories.tsx +++ b/src/components/pearl-chain-vertical/pearl-chain-vertical.stories.tsx @@ -1,9 +1,13 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import isChromatic from 'chromatic'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './pearl-chain-vertical'; +import '../pearl-chain-vertical-item'; +import '../icon'; const lineType: InputType = { options: ['dotted', 'standard', 'thin'], @@ -756,7 +760,7 @@ export const timetableChange: StoryObj = { /** position */ const meta: Meta = { - decorators: [(Story) => ], + decorators: [(Story) => ], parameters: { docs: { extractComponentDescription: () => readme, diff --git a/src/components/pearl-chain-vertical/pearl-chain-vertical.ts b/src/components/pearl-chain-vertical/pearl-chain-vertical.ts new file mode 100644 index 0000000000..ad6f89e35f --- /dev/null +++ b/src/components/pearl-chain-vertical/pearl-chain-vertical.ts @@ -0,0 +1,29 @@ +import { CSSResult, html, LitElement, TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import style from './pearl-chain-vertical.scss?lit&inline'; + +/** + * It can be used as a container for the `sbb-pearl-chain-vertical-item` component. + * + * @slot - The unnamed slot is used for the `sbb-pearl-chain-vertical-item` component. + */ +@customElement('sbb-pearl-chain-vertical') +export class SbbPearlChainVertical extends LitElement { + public static override styles: CSSResult = style; + + protected override render(): TemplateResult { + return html` +
    + +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-pearl-chain-vertical': SbbPearlChainVertical; + } +} diff --git a/src/components/sbb-pearl-chain-vertical/readme.md b/src/components/pearl-chain-vertical/readme.md similarity index 52% rename from src/components/sbb-pearl-chain-vertical/readme.md rename to src/components/pearl-chain-vertical/readme.md index 87d0871d4a..da26f187e1 100644 --- a/src/components/sbb-pearl-chain-vertical/readme.md +++ b/src/components/pearl-chain-vertical/readme.md @@ -1,16 +1,16 @@ The `sbb-pearl-chain-vertical` is a wrapper component for a [sbb-pearl-chain-vertical-item](/docs/timetable-sbb-pearl-chain-vertical-item--docs) component, -which is projected within an unnamed slot. +which is projected within an unnamed slot. Please refer to its documentation for more details. -```json5 +```json { - lineType: 'standard', - lineColor: 'charcoal', - minHeight: '89', - hideLine: false, - bulletType: 'thick-bullet', - bulletSize: 'small', + "lineType": "standard", + "lineColor": "charcoal", + "minHeight": "89", + "hideLine": false, + "bulletType": "thick-bullet", + "bulletSize": "small" } ``` @@ -25,7 +25,8 @@ Please refer to its documentation for more details. +## Slots ----------------------------------------------- - - +| Name | Description | +| ---- | --------------------------------------------------------------------------- | +| | The unnamed slot is used for the `sbb-pearl-chain-vertical-item` component. | diff --git a/src/components/pearl-chain/index.ts b/src/components/pearl-chain/index.ts new file mode 100644 index 0000000000..4c17045264 --- /dev/null +++ b/src/components/pearl-chain/index.ts @@ -0,0 +1 @@ +export * from './pearl-chain'; diff --git a/src/components/pearl-chain/pearl-chain.e2e.ts b/src/components/pearl-chain/pearl-chain.e2e.ts new file mode 100644 index 0000000000..d79f3ea785 --- /dev/null +++ b/src/components/pearl-chain/pearl-chain.e2e.ts @@ -0,0 +1,13 @@ +import { assert, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { SbbPearlChain } from './pearl-chain'; + +describe('sbb-pearl-chain', () => { + let element: SbbPearlChain; + + it('renders', async () => { + element = await fixture(html``); + assert.instanceOf(element, SbbPearlChain); + }); +}); diff --git a/src/components/pearl-chain/pearl-chain.sample-data.ts b/src/components/pearl-chain/pearl-chain.sample-data.ts new file mode 100644 index 0000000000..b85026471d --- /dev/null +++ b/src/components/pearl-chain/pearl-chain.sample-data.ts @@ -0,0 +1,107 @@ +const past2 = '2022-11-30T12:13:00+01:00'; +const past = '2022-12-04T12:13:00+01:00'; +const future = '2022-12-07T12:11:00+01:00'; +const future2 = '2022-12-11T12:13:00+01:00'; + +const defaultService = { + serviceAlteration: { + cancelled: false, + delayText: 'string', + reachable: true, + unplannedStopPointsText: '', + }, +}; +const cancelledService = { serviceAlteration: { cancelled: true } }; +const delayedService = { serviceAlteration: { delay: true } }; +const isNotReachableService = { serviceAlteration: { reachable: false } }; +const unplannedStopService = { serviceAlteration: { unplannedStopPointsText: 'unplannedStop' } }; +const redirectedService = { serviceAlteration: { redirectedText: 'Ausnahmsweise kein Halt' } }; +const departureNotServiced = { + stopPoints: [{ stopStatus: 'NOT_SERVICED' }, { stopStatus: 'PLANNED' }], +}; +const arrivalNotServiced = { + stopPoints: [{ stopStatus: 'PLANNED' }, { stopStatus: 'NOT_SERVICED' }], +}; + +export const futureLeg: any = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: defaultService, +}; + +export const extendedLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: { + ...defaultService, + notices: [{ name: 'CI', text: { template: "Extended boarding time (45')" } }], + }, +}; + +export const longFutureLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: defaultService, +}; + +export const cancelledLeg: any = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: cancelledService, +}; + +export const progressLeg: any = { + __typename: 'PTRideLeg', + arrival: { time: future }, + departure: { time: past }, + serviceJourney: defaultService, +}; + +export const pastLeg: any = { + __typename: 'PTRideLeg', + arrival: { time: past }, + departure: { time: past2 }, + serviceJourney: defaultService, +}; + +export const delayedLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: delayedService, +}; + +export const notReachableLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: isNotReachableService, +}; + +export const unplannedStopLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: unplannedStopService, +}; + +export const redirectedOnDepartureLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: { + ...redirectedService, + ...departureNotServiced, + }, +}; + +export const redirectedOnArrivalLeg = { + __typename: 'PTRideLeg', + arrival: { time: future2 }, + departure: { time: future }, + serviceJourney: { ...redirectedService, ...arrivalNotServiced }, +}; diff --git a/src/components/sbb-pearl-chain/sbb-pearl-chain.scss b/src/components/pearl-chain/pearl-chain.scss similarity index 99% rename from src/components/sbb-pearl-chain/sbb-pearl-chain.scss rename to src/components/pearl-chain/pearl-chain.scss index 7ea9e3c8d6..1b7de557be 100644 --- a/src/components/sbb-pearl-chain/sbb-pearl-chain.scss +++ b/src/components/pearl-chain/pearl-chain.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../core/styles' as sbb; @mixin sbb-pearl-chain-dotted { background-color: unset; diff --git a/src/components/pearl-chain/pearl-chain.spec.ts b/src/components/pearl-chain/pearl-chain.spec.ts new file mode 100644 index 0000000000..01874f28da --- /dev/null +++ b/src/components/pearl-chain/pearl-chain.spec.ts @@ -0,0 +1,167 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForLitRender } from '../core/testing'; +import { PtRideLeg } from '../core/timetable'; + +import type { SbbPearlChain } from './pearl-chain'; + +import './pearl-chain'; + +describe('sbb-pearl-chain', () => { + describe('sbb-pearl-chain with one leg', () => { + it('renders component with config', async () => { + const element = await fixture(html``); + element.legs = [ + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T05:00' }, + departure: { time: '2022-08-18T04:00' }, + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(``); + expect(element).shadowDom.to.be.equal(` +
    + +
    + +
    + `); + }); + }); + + describe('sbb-pearl-chain with two legs', () => { + it('renders component with config', async () => { + const element = await fixture(html``); + element.legs = [ + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T05:00' }, + departure: { time: '2022-08-18T04:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + }, + } as PtRideLeg, + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T16:00' }, + departure: { time: '2022-08-18T05:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + }, + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(``); + expect(element).shadowDom.to.be.equal(` +
    + +
    +
    + +
    + +
    + `); + }); + }); + + describe('sbb-pearl-chain with skipped stops', () => { + it('renders component with departure skipped', async () => { + const element = await fixture(html``); + element.legs = [ + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T05:00' }, + departure: { time: '2022-08-18T04:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + }, + } as PtRideLeg, + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T16:00' }, + departure: { time: '2022-08-18T05:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + stopPoints: [ + { + stopStatus: 'NOT_SERVICED', + }, + { + stopStatus: 'PLANNED', + }, + ], + }, + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(``); + expect(element).shadowDom.to.be.equal(` +
    + +
    +
    + +
    + +
    + `); + }); + + it('renders component with arrival skipped', async () => { + const element = await fixture(html``); + element.legs = [ + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T05:00' }, + departure: { time: '2022-08-18T04:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + }, + } as PtRideLeg, + { + __typename: 'PTRideLeg', + arrival: { time: '2022-08-18T16:00' }, + departure: { time: '2022-08-18T05:00' }, + serviceJourney: { + serviceAlteration: { + cancelled: false, + }, + stopPoints: [ + { + stopStatus: 'PLANNED', + }, + { + stopStatus: 'NOT_SERVICED', + }, + ], + }, + } as PtRideLeg, + ]; + await waitForLitRender(element); + expect(element).dom.to.be.equal(``); + expect(element).shadowDom.to.be.equal(` +
    + +
    +
    + +
    + +
    + `); + }); + }); +}); diff --git a/src/components/sbb-pearl-chain/sbb-pearl-chain.stories.tsx b/src/components/pearl-chain/pearl-chain.stories.tsx similarity index 93% rename from src/components/sbb-pearl-chain/sbb-pearl-chain.stories.tsx rename to src/components/pearl-chain/pearl-chain.stories.tsx index 0a9bec617c..5d817d1d96 100644 --- a/src/components/sbb-pearl-chain/sbb-pearl-chain.stories.tsx +++ b/src/components/pearl-chain/pearl-chain.stories.tsx @@ -1,6 +1,9 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; +import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import isChromatic from 'chromatic'; +import { h, type JSX } from 'jsx-dom'; + import { cancelledLeg, progressLeg, @@ -9,10 +12,9 @@ import { longFutureLeg, redirectedOnDepartureLeg, redirectedOnArrivalLeg, -} from './sbb-pearl-chain.sample-data'; -import isChromatic from 'chromatic'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; -import type { InputType } from '@storybook/types'; +} from './pearl-chain.sample-data'; +import readme from './readme.md?raw'; +import './pearl-chain'; const disableAnimation: InputType = { control: { @@ -37,7 +39,7 @@ const defaultArgs: Args = { }; const Template = (args): JSX.Element => { - return ; + return ; }; export const NoStops: StoryObj = { @@ -150,7 +152,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), ], diff --git a/src/components/sbb-pearl-chain/sbb-pearl-chain.tsx b/src/components/pearl-chain/pearl-chain.ts similarity index 75% rename from src/components/sbb-pearl-chain/sbb-pearl-chain.tsx rename to src/components/pearl-chain/pearl-chain.ts index 7c9ed6a053..e12dcdd1e1 100644 --- a/src/components/sbb-pearl-chain/sbb-pearl-chain.tsx +++ b/src/components/pearl-chain/pearl-chain.ts @@ -1,16 +1,22 @@ -import { Component, Element, h, JSX, Prop } from '@stencil/core'; import { differenceInMinutes, isAfter, isBefore } from 'date-fns'; -import { removeTimezoneFromISOTimeString } from '../../global/datetime'; -import { isRideLeg, Leg, PtRideLeg } from '../../global/timetable'; +import { CSSResult, html, LitElement, nothing, TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import { removeTimezoneFromISOTimeString } from '../core/datetime'; +import { isRideLeg, Leg, PtRideLeg } from '../core/timetable'; + +import style from './pearl-chain.scss?lit&inline'; type Status = 'progress' | 'future' | 'past'; -@Component({ - shadow: true, - styleUrl: 'sbb-pearl-chain.scss', - tag: 'sbb-pearl-chain', -}) -export class SbbPearlChain { +/** + * It visually displays journey information. + */ +@customElement('sbb-pearl-chain') +export class SbbPearlChain extends LitElement { + public static override styles: CSSResult = style; + /** * Define the legs of the pearl-chain. * Format: @@ -19,18 +25,16 @@ export class SbbPearlChain { * to the total travel time. Example: departure 16:30, change at 16:40, * arrival at 17:00. So the change should have a duration of 33.33%. */ - @Prop() public legs: Leg[]; + @property({ type: Array }) public legs: (Leg | PtRideLeg)[]; /** * Per default, the current location has a pulsating animation. You can * disable the animation with this property. */ - @Prop() public disableAnimation?: boolean; - - @Element() private _element: HTMLElement; + @property({ attribute: 'disable-animation', type: Boolean }) public disableAnimation?: boolean; private _now(): number { - const dataNow = +this._element.dataset?.now; + const dataNow = +this.dataset?.now; return isNaN(dataNow) ? Date.now() : dataNow; } @@ -78,7 +82,7 @@ export class SbbPearlChain { return 'future'; } - private _renderPosition(start: Date, end: Date): JSX.Element | undefined { + private _renderPosition(start: Date, end: Date): TemplateResult | undefined { const currentPosition = this._getProgress(start, end); if (currentPosition < 0 && currentPosition > 100) return undefined; @@ -91,10 +95,13 @@ export class SbbPearlChain { const animation = this.disableAnimation ? 'sbb-pearl-chain__position--no-animation' : ''; - return ; + return html``; } - public render(): JSX.Element { + protected override render(): TemplateResult { const rideLegs: PtRideLeg[] = this.legs?.filter((leg) => isRideLeg(leg)) as PtRideLeg[]; const departureTime = @@ -143,21 +150,21 @@ export class SbbPearlChain { : ''; if (this._isAllCancelled(rideLegs)) { - return ( -
    + return html` +
    -
    +
    - ); + `; } - return ( + return html`
    - {rideLegs?.map((leg: PtRideLeg, index: number) => { + ${rideLegs?.map((leg: PtRideLeg, index: number) => { const { stopPoints, serviceAlteration } = leg?.serviceJourney || {}; const duration = this._getRelativeDuration(rideLegs, leg); @@ -200,24 +207,29 @@ export class SbbPearlChain { }; }; - return ( -
    - {index > 0 && index < rideLegs.length && ( - - )} - {this._getStatus(arrival, departure) === 'progress' && - !cancelled && - this._renderPosition(departure, arrival)} -
    - ); + return html`
    + ${index > 0 && index < rideLegs.length + ? html`` + : nothing} + ${this._getStatus(arrival, departure) === 'progress' && !cancelled + ? this._renderPosition(departure, arrival) + : nothing} +
    `; })}
    - ); + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-pearl-chain': SbbPearlChain; } } diff --git a/src/components/sbb-pearl-chain/readme.md b/src/components/pearl-chain/readme.md similarity index 54% rename from src/components/sbb-pearl-chain/readme.md rename to src/components/pearl-chain/readme.md index 6247ae528a..a7aa9a4bcb 100644 --- a/src/components/sbb-pearl-chain/readme.md +++ b/src/components/pearl-chain/readme.md @@ -1,9 +1,9 @@ -The `sbb-pearl-chain` component displays all parts of a journey, including changes of trains or other kinds of transports. +The `sbb-pearl-chain` component displays all parts of a journey, including changes of trains or other kinds of transports. Also, it is possible to render the current position. The `legs` property is mandatory. -```json5 +```json [ { "__typename": "PTRideLeg", @@ -43,10 +43,9 @@ The `legs` property is mandatory. ``` ```html - + ``` - ## Testing To specify a specific date for the current datetime, you can use the `data-now` attribute (timestamp in milliseconds). @@ -54,28 +53,9 @@ This is helpful if you need a specific state of the component. - ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `disableAnimation` | `disable-animation` | Per default, the current location has a pulsating animation. You can disable the animation with this property. | `boolean` | `undefined` | -| `legs` | -- | Define the legs of the pearl-chain. Format: `{"legs": [{"duration": 25}, ...]}` `duration` in minutes. Duration of the leg is relative to the total travel time. Example: departure 16:30, change at 16:40, arrival at 17:00. So the change should have a duration of 33.33%. | `Leg[]` | `undefined` | - - -## Dependencies - -### Used by - - - [sbb-pearl-chain-time](../sbb-pearl-chain-time) - -### Graph -```mermaid -graph TD; - sbb-pearl-chain-time --> sbb-pearl-chain - style sbb-pearl-chain fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------ | ------------------- | ------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `legs` | `legs` | public | `(Leg \| PtRideLeg)[]` | | Define the legs of the pearl-chain. Format: `{"legs": \[{"duration": 25}, ...]}` `duration` in minutes. Duration of the leg is relative to the total travel time. Example: departure 16:30, change at 16:40, arrival at 17:00. So the change should have a duration of 33.33%. | +| `disableAnimation` | `disable-animation` | public | `boolean \| undefined` | | Per default, the current location has a pulsating animation. You can disable the animation with this property. | diff --git a/src/components/radio-button/index.ts b/src/components/radio-button/index.ts new file mode 100644 index 0000000000..a59886720b --- /dev/null +++ b/src/components/radio-button/index.ts @@ -0,0 +1,2 @@ +export * from './radio-button'; +export * from './radio-button-group'; diff --git a/src/components/radio-button/radio-button-group/index.ts b/src/components/radio-button/radio-button-group/index.ts new file mode 100644 index 0000000000..6b0e03fa92 --- /dev/null +++ b/src/components/radio-button/radio-button-group/index.ts @@ -0,0 +1 @@ +export * from './radio-button-group'; diff --git a/src/components/radio-button/radio-button-group/radio-button-group.e2e.ts b/src/components/radio-button/radio-button-group/radio-button-group.e2e.ts new file mode 100644 index 0000000000..2812987c2e --- /dev/null +++ b/src/components/radio-button/radio-button-group/radio-button-group.e2e.ts @@ -0,0 +1,160 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { sendKeys } from '@web/test-runner-commands'; +import { html } from 'lit/static-html.js'; + +import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing'; +import type { SbbRadioButton } from '../radio-button'; +import '../radio-button'; + +import { SbbRadioButtonGroup } from './radio-button-group'; + +describe('sbb-radio-button-group', () => { + let element: SbbRadioButtonGroup; + + beforeEach(async () => { + element = await fixture(html` + + Value one + Value two + Value three + Value four + + `); + }); + + it('renders', () => { + assert.instanceOf(element, SbbRadioButtonGroup); + }); + + describe('events', () => { + it('selects radio on click', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + const radio = element.querySelector('#sbb-radio-2') as SbbRadioButton; + + expect(firstRadio).to.have.attribute('checked'); + + radio.click(); + await waitForLitRender(element); + + expect(radio).to.have.attribute('checked'); + expect(firstRadio).not.to.have.attribute('checked'); + }); + + it('dispatches event on radio change', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + const checkedRadio = element.querySelector('#sbb-radio-2') as SbbRadioButton; + const changeSpy = new EventSpy('change'); + const inputSpy = new EventSpy('input'); + + checkedRadio.click(); + await waitForCondition(() => changeSpy.events.length === 1); + expect(changeSpy.count).to.be.equal(1); + await waitForCondition(() => inputSpy.events.length === 1); + expect(inputSpy.count).to.be.equal(1); + + firstRadio.click(); + await waitForLitRender(element); + expect(firstRadio).to.have.attribute('checked'); + }); + + it('does not select disabled radio on click', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + const disabledRadio = element.querySelector('#sbb-radio-3') as SbbRadioButton; + + disabledRadio.click(); + await waitForLitRender(element); + + expect(disabledRadio).not.to.have.attribute('checked'); + expect(firstRadio).to.have.attribute('checked'); + }); + + it('preserves radio button disabled state after being disabled from group', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + const secondRadio = element.querySelector('#sbb-radio-2') as SbbRadioButton; + const disabledRadio = element.querySelector('#sbb-radio-3') as SbbRadioButton; + + element.disabled = true; + await waitForLitRender(element); + + disabledRadio.click(); + await waitForLitRender(element); + expect(disabledRadio).not.to.have.attribute('checked'); + expect(firstRadio).to.have.attribute('checked'); + + secondRadio.click(); + await waitForLitRender(element); + expect(secondRadio).not.to.have.attribute('checked'); + + element.disabled = false; + await waitForLitRender(element); + + disabledRadio.click(); + await waitForLitRender(element); + expect(disabledRadio).not.to.have.attribute('checked'); + expect(firstRadio).to.have.attribute('checked'); + }); + + it('selects radio on left arrow key pressed', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + + firstRadio.focus(); + await waitForLitRender(element); + + await sendKeys({ down: 'ArrowLeft' }); + await waitForLitRender(element); + + const radio = element.querySelector('#sbb-radio-4'); + expect(radio).to.have.attribute('checked'); + + firstRadio.click(); + await waitForLitRender(element); + + expect(firstRadio).to.have.attribute('checked'); + }); + + it('selects radio on right arrow key pressed', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + + firstRadio.focus(); + await sendKeys({ down: 'ArrowRight' }); + + await waitForLitRender(element); + const radio = element.querySelector('#sbb-radio-2'); + + expect(radio).to.have.attribute('checked'); + + firstRadio.click(); + await waitForLitRender(element); + + expect(firstRadio).to.have.attribute('checked'); + }); + + it('wraps around on arrow key navigation', async () => { + const firstRadio = element.querySelector('#sbb-radio-1') as SbbRadioButton; + const secondRadio = element.querySelector('#sbb-radio-2') as SbbRadioButton; + + secondRadio.click(); + await waitForLitRender(element); + expect(secondRadio).to.have.attribute('checked'); + + secondRadio.focus(); + await waitForLitRender(element); + + await sendKeys({ down: 'ArrowRight' }); + await waitForLitRender(element); + + await sendKeys({ down: 'ArrowRight' }); + await waitForLitRender(element); + + const radio = element.querySelector('#sbb-radio-1'); + expect(radio).to.have.attribute('checked'); + + firstRadio.click(); + await waitForLitRender(element); + + expect(firstRadio).to.have.attribute('checked'); + }); + }); +}); diff --git a/src/components/sbb-radio-button-group/sbb-radio-button-group.scss b/src/components/radio-button/radio-button-group/radio-button-group.scss similarity index 97% rename from src/components/sbb-radio-button-group/sbb-radio-button-group.scss rename to src/components/radio-button/radio-button-group/radio-button-group.scss index 35e191d5fd..ff09a2710f 100644 --- a/src/components/sbb-radio-button-group/sbb-radio-button-group.scss +++ b/src/components/radio-button/radio-button-group/radio-button-group.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin diff --git a/src/components/radio-button/radio-button-group/radio-button-group.spec.ts b/src/components/radio-button/radio-button-group/radio-button-group.spec.ts new file mode 100644 index 0000000000..ea56ba1fd4 --- /dev/null +++ b/src/components/radio-button/radio-button-group/radio-button-group.spec.ts @@ -0,0 +1,23 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './radio-button-group'; + +describe('sbb-radio-button-group', () => { + it('renders', async () => { + const root = await fixture(html``); + + expect(root).dom.to.be.equal( + ` + + + `, + ); + expect(root).shadowDom.to.be.equal( + ` +
    + +
    + `, + ); + }); +}); diff --git a/src/components/sbb-radio-button-group/sbb-radio-button-group.stories.tsx b/src/components/radio-button/radio-button-group/radio-button-group.stories.tsx similarity index 92% rename from src/components/sbb-radio-button-group/sbb-radio-button-group.stories.tsx rename to src/components/radio-button/radio-button-group/radio-button-group.stories.tsx index 4671aa7fbc..882d038133 100644 --- a/src/components/sbb-radio-button-group/sbb-radio-button-group.stories.tsx +++ b/src/components/radio-button/radio-button-group/radio-button-group.stories.tsx @@ -1,9 +1,13 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; import { withActions } from '@storybook/addon-actions/decorator'; -import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './radio-button-group'; +import '../radio-button'; +import '../../form-error'; const value: InputType = { control: { @@ -97,12 +101,11 @@ const ErrorMessageTemplate = (args): JSX.Element => { return ( { + onChange={(event: CustomEvent) => { if (event.detail.value) { sbbFormError.remove(); } else if (args.required) { - document.getElementById('sbb-radio-group').append(sbbFormError); + (event.target as HTMLElement).closest('sbb-radio-button-group').append(sbbFormError); } }} > @@ -185,7 +188,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), withActions as Decorator, diff --git a/src/components/radio-button/radio-button-group/radio-button-group.ts b/src/components/radio-button/radio-button-group/radio-button-group.ts new file mode 100644 index 0000000000..10dc9cb674 --- /dev/null +++ b/src/components/radio-button/radio-button-group/radio-button-group.ts @@ -0,0 +1,326 @@ +import { CSSResult, html, LitElement, nothing, PropertyValues, TemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { isArrowKeyPressed, getNextElementIndex, interactivityChecker } from '../../core/a11y'; +import { toggleDatasetEntry, setAttribute } from '../../core/dom'; +import { + createNamedSlotState, + HandlerRepository, + namedSlotChangeHandlerAspect, + EventEmitter, + ConnectedAbortController, +} from '../../core/eventing'; +import { SbbHorizontalFrom, SbbOrientation } from '../../core/interfaces'; +import type { + SbbRadioButton, + SbbRadioButtonSize, + SbbRadioButtonStateChange, +} from '../radio-button'; + +import style from './radio-button-group.scss?lit&inline'; + +/** + * It can be used as a container for one or more `sbb-radio-button`. + * + * @slot - Use the unnamed slot to add `sbb-radio-button` elements to the `sbb-radio-button-group`. + * @slot error - Use this to provide a `sbb-form-error` to show an error message. + * @event {CustomEvent} did-change - Emits whenever the `sbb-radio-group` value changes. + * @event {CustomEvent} change - Emits whenever the `sbb-radio-group` value changes. + * @event {CustomEvent} input - Emits whenever the `sbb-radio-group` value changes. + */ +@customElement('sbb-radio-button-group') +export class SbbRadioButtonGroup extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + didChange: 'did-change', + change: 'change', + input: 'input', + } as const; + + /** + * Whether the radios can be deselected. + */ + @property({ attribute: 'allow-empty-selection', type: Boolean }) + public set allowEmptySelection(value: boolean) { + this._allowEmptySelection = value; + this._updateAllowEmptySelection(); + } + public get allowEmptySelection(): boolean { + return this._allowEmptySelection; + } + private _allowEmptySelection: boolean = false; + + /** + * Whether the radio group is disabled. + */ + @property({ type: Boolean }) + public set disabled(value: boolean) { + this._disabled = value; + this._updateDisabled(); + } + public get disabled(): boolean { + return this._disabled; + } + private _disabled: boolean = false; + + /** + * Whether the radio group is required. + */ + @property({ type: Boolean }) + public set required(value: boolean) { + this._required = value; + this._updateRequired(); + } + public get required(): boolean { + return this._required; + } + private _required: boolean = false; + + /** + * The value of the radio group. + */ + @property() public value?: any | null; + + /** + * Size variant, either m or s. + */ + @property() + public set size(value: SbbRadioButtonSize) { + this._size = value; + this._updateSize(); + } + public get size(): SbbRadioButtonSize { + return this._size; + } + private _size: SbbRadioButtonSize = 'm'; + + /** + * Overrides the behaviour of `orientation` property. + */ + @property({ attribute: 'horizontal-from', reflect: true }) + public horizontalFrom?: SbbHorizontalFrom; + + /** + * Radio group's orientation, either horizontal or vertical. + */ + @property({ reflect: true }) + public orientation: SbbOrientation = 'horizontal'; + + /** + * State of listed named slots, by indicating whether any element for a named slot is defined. + */ + @state() private _namedSlots = createNamedSlotState('error'); + + private _hasSelectionPanel: boolean; + private _didLoad = false; + private _abort = new ConnectedAbortController(this); + + private _valueChanged(value: any | undefined): void { + for (const radio of this._radioButtons) { + radio.checked = radio.value === value; + radio.tabIndex = this._getRadioTabIndex(radio); + } + this._setFocusableRadio(); + } + + private _updateDisabled(): void { + for (const radio of this._radioButtons) { + toggleDatasetEntry(radio, 'groupDisabled', this.disabled); + radio.tabIndex = this._getRadioTabIndex(radio); + } + this._setFocusableRadio(); + } + + private _updateRequired(): void { + for (const radio of this._radioButtons) { + toggleDatasetEntry(radio, 'groupRequired', this.required); + } + } + + private _updateSize(): void { + for (const radio of this._radioButtons) { + radio.size = this.size; + } + } + + private _updateAllowEmptySelection(): void { + for (const radio of this._radioButtons) { + radio.allowEmptySelection = this.allowEmptySelection; + } + } + + /** + * Emits whenever the `sbb-radio-group` value changes. + * @deprecated only used for React. Will probably be removed once React 19 is available. + */ + private _didChange: EventEmitter = new EventEmitter(this, SbbRadioButtonGroup.events.didChange); + + /** + * Emits whenever the `sbb-radio-group` value changes. + */ + private _change: EventEmitter = new EventEmitter(this, SbbRadioButtonGroup.events.change); + + /** + * Emits whenever the `sbb-radio-group` value changes. + */ + private _input: EventEmitter = new EventEmitter(this, SbbRadioButtonGroup.events.input); + + private _handlerRepository = new HandlerRepository( + this, + namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), + ); + + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener( + 'state-change', + (e: CustomEvent) => this._onRadioButtonSelect(e), + { + signal, + passive: true, + }, + ); + this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); + this._hasSelectionPanel = !!this.querySelector('sbb-selection-panel'); + toggleDatasetEntry(this, 'hasSelectionPanel', this._hasSelectionPanel); + this._handlerRepository.connect(); + this._updateRadios(this.value); + } + + public override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('value')) { + this._valueChanged(this.value); + } + } + + protected override firstUpdated(): void { + this._didLoad = true; + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + } + + private _onRadioButtonSelect(event: CustomEvent): void { + event.stopPropagation(); + if (event.detail.type !== 'checked' || !this._didLoad) { + return; + } + + if (event.detail.checked) { + this.value = (event.target as HTMLInputElement).value; + this._emitChange(this.value); + } else if (this.allowEmptySelection) { + this.value = this._radioButtons.find((radio) => radio.checked)?.value; + if (!this.value) { + this._emitChange(); + } + } + } + + private _emitChange(value?: string): void { + this._change.emit({ value }); + this._input.emit({ value }); + this._didChange.emit({ value }); + } + + private _updateRadios(initValue?: string): void { + this.value = initValue ?? this._radioButtons.find((radio) => radio.checked)?.value; + + for (const radio of this._radioButtons) { + radio.checked = radio.value === this.value; + radio.size = this.size; + radio.allowEmptySelection = this.allowEmptySelection; + + toggleDatasetEntry(radio, 'groupDisabled', this.disabled); + toggleDatasetEntry(radio, 'groupRequired', this.required); + + radio.tabIndex = this._getRadioTabIndex(radio); + } + + this._setFocusableRadio(); + } + + private get _radioButtons(): SbbRadioButton[] { + return (Array.from(this.querySelectorAll('sbb-radio-button')) as SbbRadioButton[]).filter( + (el) => el.closest('sbb-radio-button-group') === this, + ); + } + + private get _enabledRadios(): SbbRadioButton[] | undefined { + if (!this.disabled) { + return this._radioButtons.filter((r) => !r.disabled && interactivityChecker.isVisible(r)); + } + } + + private _setFocusableRadio(): void { + const checked = this._radioButtons.find((radio) => radio.checked && !radio.disabled); + + const enabledRadios = this._enabledRadios; + if (!checked && enabledRadios?.length) { + enabledRadios[0].tabIndex = 0; + } + } + + private _getRadioTabIndex(radio: SbbRadioButton): number { + return (radio.checked || this._hasSelectionPanel) && !radio.disabled && !this.disabled ? 0 : -1; + } + + private _handleKeyDown(evt: KeyboardEvent): void { + const enabledRadios: SbbRadioButton[] = this._enabledRadios; + + if ( + !enabledRadios || + !enabledRadios.length || + // don't trap nested handling + ((evt.target as HTMLElement) !== this && + (evt.target as HTMLElement).parentElement !== this && + (evt.target as HTMLElement).parentElement.nodeName !== 'SBB-SELECTION-PANEL') + ) { + return; + } + + if (!isArrowKeyPressed(evt)) { + return; + } + + let current: number; + let nextIndex: number; + + if (this._hasSelectionPanel) { + current = enabledRadios.findIndex((e: SbbRadioButton) => e === evt.target); + nextIndex = getNextElementIndex(evt, current, enabledRadios.length); + } else { + const checked: number = enabledRadios.findIndex((radio: SbbRadioButton) => radio.checked); + nextIndex = getNextElementIndex(evt, checked, enabledRadios.length); + enabledRadios[nextIndex].select(); + } + + enabledRadios[nextIndex].focus(); + evt.preventDefault(); + } + + protected override render(): TemplateResult { + setAttribute(this, 'role', 'radiogroup'); + + return html` +
    + this._updateRadios()}> +
    + ${this._namedSlots.error + ? html`
    + +
    ` + : nothing} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-radio-button-group': SbbRadioButtonGroup; + } +} diff --git a/src/components/radio-button/radio-button-group/readme.md b/src/components/radio-button/radio-button-group/readme.md new file mode 100644 index 0000000000..64685642b7 --- /dev/null +++ b/src/components/radio-button/radio-button-group/readme.md @@ -0,0 +1,95 @@ +The `sbb-radio-button-group` is a component which can be used as a wrapper for +a collection of [sbb-radio-button](/docs/components-sbb-radio-button-sbb-radio-button--docs)s, +or, alternatively, for a collection of [sbb-selection-panel](/docs/components-sbb-selection-panel--docs)s. + +```html + + + Option one + Option two + Option three + +``` + +Pressing a `sbb-radio-button` checks it and unchecks the previously selected one, if any. +They can also be controlled programmatically by setting the value property of the parent radio group to the value of the radio. + +Please note that within a `sbb-radio-button-group`, only one `sbb-radio-button` can be selected at a time; +if you need to select more than one item, it is recommended to use the `sbb-checkbox-group` component. + +## States + +The radio group can have different states: + +- can be completely disabled by setting the property `disabled`; +- can be required by setting the property `required`. + +```html + + + ... + + + + + ... + +``` + +In order to deselect a `sbb-radio-button` inside the `sbb-radio-button-group`, +you can use the `allowEmptySelection` property, which will be proxied to the inner `sbb-radio-button` +enabling their deselection (by default, a selected `sbb-radio-button` cannot be deselected). + +```html + ... +``` + +## Style + +The `orientation` property is used to set item orientation. Possible values are `horizontal` (default) and `vertical`. +The optional property `horizontalFrom` can be used in combination with `orientation='vertical'` to +indicate the minimum breakpoint from which the orientation changes to `horizontal`. + +```html + + ... + +``` + +## Events + +Consumers can listen to the native `change`/`input` event on the `sbb-radio-button-group` component +to intercept the selection's change; the current value can be read from `event.detail.value`. + +## Accessibility + +In order to ensure readability for screen-readers, please provide an `aria-label` attribute for the `sbb-radio-button-group`. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ----------------------- | ------- | -------------------------------- | -------------- | --------------------------------------------------------- | +| `allowEmptySelection` | `allow-empty-selection` | public | `boolean` | | Whether the radios can be deselected. | +| `disabled` | `disabled` | public | `boolean` | | Whether the radio group is disabled. | +| `required` | `required` | public | `boolean` | | Whether the radio group is required. | +| `value` | `value` | public | `any \| null \| undefined` | | The value of the radio group. | +| `size` | `size` | public | `SbbRadioButtonSize` | | Size variant, either m or s. | +| `horizontalFrom` | `horizontal-from` | public | `SbbHorizontalFrom \| undefined` | | Overrides the behaviour of `orientation` property. | +| `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Radio group's orientation, either horizontal or vertical. | + +## Events + +| Name | Type | Description | Inherited From | +| ------------ | ------------------- | --------------------------------------------------- | -------------- | +| `did-change` | `CustomEvent` | Emits whenever the `sbb-radio-group` value changes. | | +| `change` | `CustomEvent` | Emits whenever the `sbb-radio-group` value changes. | | +| `input` | `CustomEvent` | Emits whenever the `sbb-radio-group` value changes. | | + +## Slots + +| Name | Description | +| ------- | ---------------------------------------------------------------------------------------- | +| | Use the unnamed slot to add `sbb-radio-button` elements to the `sbb-radio-button-group`. | +| `error` | Use this to provide a `sbb-form-error` to show an error message. | diff --git a/src/components/radio-button/radio-button/index.ts b/src/components/radio-button/radio-button/index.ts new file mode 100644 index 0000000000..af6ffac816 --- /dev/null +++ b/src/components/radio-button/radio-button/index.ts @@ -0,0 +1 @@ +export * from './radio-button'; diff --git a/src/components/radio-button/radio-button/radio-button.e2e.ts b/src/components/radio-button/radio-button/radio-button.e2e.ts new file mode 100644 index 0000000000..7d041e359f --- /dev/null +++ b/src/components/radio-button/radio-button/radio-button.e2e.ts @@ -0,0 +1,67 @@ +import { assert, expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing'; + +import { SbbRadioButton } from './radio-button'; + +describe('sbb-radio-button', () => { + let element: SbbRadioButton; + + beforeEach(async () => { + element = await fixture(html`Value label`); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbRadioButton); + }); + + it('should not render accessibility label about containing state', async () => { + element = element.shadowRoot.querySelector('.sbb-radio-button__expanded-label'); + expect(element).not.to.be.ok; + }); + + it('selects radio on click', async () => { + const stateChange = new EventSpy(SbbRadioButton.events.stateChange); + + element.click(); + await waitForLitRender(element); + + expect(element).to.have.attribute('checked'); + await waitForCondition(() => stateChange.events.length === 1); + expect(stateChange.count).to.be.equal(1); + }); + + it('does not deselect radio if already checked', async () => { + const stateChange = new EventSpy(SbbRadioButton.events.stateChange); + + element.click(); + await waitForLitRender(element); + expect(element).to.have.attribute('checked'); + await waitForCondition(() => stateChange.events.length === 1); + expect(stateChange.count).to.be.equal(1); + + element.click(); + await waitForLitRender(element); + expect(element).to.have.attribute('checked'); + await waitForCondition(() => stateChange.events.length === 1); + expect(stateChange.count).to.be.equal(1); + }); + + it('allows empty selection', async () => { + const stateChange = new EventSpy(SbbRadioButton.events.stateChange); + + element.allowEmptySelection = true; + element.click(); + await waitForLitRender(element); + expect(element).to.have.attribute('checked'); + await waitForCondition(() => stateChange.events.length === 1); + expect(stateChange.count).to.be.equal(1); + + element.click(); + await waitForLitRender(element); + expect(element).not.to.have.attribute('checked'); + await waitForCondition(() => stateChange.events.length === 2); + expect(stateChange.count).to.be.equal(2); + }); +}); diff --git a/src/components/sbb-radio-button/sbb-radio-button.scss b/src/components/radio-button/radio-button/radio-button.scss similarity index 97% rename from src/components/sbb-radio-button/sbb-radio-button.scss rename to src/components/radio-button/radio-button/radio-button.scss index 79b0a616dc..0c61e7f52f 100644 --- a/src/components/sbb-radio-button/sbb-radio-button.scss +++ b/src/components/radio-button/radio-button/radio-button.scss @@ -1,4 +1,4 @@ -@use '../../global/styles' as sbb; +@use '../../core/styles' as sbb; // Default component properties, defined for :host. Properties which can not // travel the shadow boundary are defined through this mixin @@ -60,7 +60,7 @@ } } -:host([checked]:not([checked='false'])) { +:host([checked]) { --sbb-radio-button-inner-circle-color: var(--sbb-color-red-default); --sbb-radio-button-background-fake-border-width: calc( (var(--sbb-radio-button-dimension) - var(--sbb-radio-button-inner-circle-dimension)) / 2 @@ -73,7 +73,7 @@ } // Disabled definitions have to be after checked definitions -:host(:is([data-group-disabled], [disabled]):not([disabled='false'])) { +:host(:is([data-group-disabled], [disabled])) { --sbb-radio-button-background-color: var(--sbb-color-milk-default); --sbb-radio-button-subtext-color: var(--sbb-color-smoke-default); --sbb-radio-button-border-style: dashed; diff --git a/src/components/radio-button/radio-button/radio-button.spec.ts b/src/components/radio-button/radio-button/radio-button.spec.ts new file mode 100644 index 0000000000..64fab9827c --- /dev/null +++ b/src/components/radio-button/radio-button/radio-button.spec.ts @@ -0,0 +1,26 @@ +import { expect, fixture } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; +import './radio-button'; + +describe('sbb-radio-button', () => { + it('renders', async () => { + const root = await fixture(html``); + + expect(root).dom.to.be.equal( + ` + + + `, + ); + expect(root).shadowDom.to.be.equal( + ` + + `, + ); + }); +}); diff --git a/src/components/sbb-radio-button/sbb-radio-button.stories.tsx b/src/components/radio-button/radio-button/radio-button.stories.tsx similarity index 94% rename from src/components/sbb-radio-button/sbb-radio-button.stories.tsx rename to src/components/radio-button/radio-button/radio-button.stories.tsx index 2b5643ca63..82845f2425 100644 --- a/src/components/sbb-radio-button/sbb-radio-button.stories.tsx +++ b/src/components/radio-button/radio-button/radio-button.stories.tsx @@ -1,8 +1,10 @@ /** @jsx h */ -import { h, JSX } from 'jsx-dom'; -import readme from './readme.md'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/html'; import type { InputType } from '@storybook/types'; +import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import { h, type JSX } from 'jsx-dom'; + +import readme from './readme.md?raw'; +import './radio-button'; const value: InputType = { control: { @@ -100,7 +102,7 @@ const meta: Meta = { decorators: [ (Story) => (
    - +
    ), ], diff --git a/src/components/radio-button/radio-button/radio-button.ts b/src/components/radio-button/radio-button/radio-button.ts new file mode 100644 index 0000000000..c5034d4e24 --- /dev/null +++ b/src/components/radio-button/radio-button/radio-button.ts @@ -0,0 +1,294 @@ +import { CSSResult, html, LitElement, nothing, TemplateResult, PropertyValues } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; + +import { isValidAttribute, setAttributes } from '../../core/dom'; +import { + createNamedSlotState, + documentLanguage, + HandlerRepository, + languageChangeHandlerAspect, + namedSlotChangeHandlerAspect, + formElementHandlerAspect, + EventEmitter, + ConnectedAbortController, +} from '../../core/eventing'; +import { i18nCollapsed, i18nExpanded } from '../../core/i18n'; +import { + SbbCheckedStateChange, + SbbDisabledStateChange, + SbbStateChange, +} from '../../core/interfaces'; +import { AgnosticMutationObserver } from '../../core/observers'; +import type { SbbRadioButtonGroup } from '../radio-button-group'; + +import style from './radio-button.scss?lit&inline'; + +export type SbbRadioButtonStateChange = Extract< + SbbStateChange, + SbbDisabledStateChange | SbbCheckedStateChange +>; + +export type SbbRadioButtonSize = 's' | 'm'; + +/** Configuration for the attribute to look at if component is nested in a sbb-radio-button-group */ +const radioButtonObserverConfig: MutationObserverInit = { + attributeFilter: ['data-group-required', 'data-group-disabled'], +}; + +/** + * It displays a radio button enhanced with the SBB Design. + * + * @slot - Use the unnamed slot to add content to the radio label. + * @slot subtext - Slot used to render a subtext under the label (only visible within a `sbb-selection-panel`). + * @slot suffix - Slot used to render additional content after the label (only visible within a `sbb-selection-panel`). + */ +@customElement('sbb-radio-button') +export class SbbRadioButton extends LitElement { + public static override styles: CSSResult = style; + public static readonly events = { + stateChange: 'state-change', + radioButtonLoaded: 'radio-button-loaded', + } as const; + + /** + * Whether the radio can be deselected. + */ + @property({ attribute: 'allow-empty-selection', type: Boolean }) public allowEmptySelection = + false; + + /** + * Value of radio button. + */ + @property() public value: string; + + /** + * Whether the radio button is disabled. + */ + @property({ reflect: true, type: Boolean }) public disabled = false; + + /** + * Whether the radio button is required. + */ + @property({ type: Boolean }) public required = false; + + /** + * Whether the radio button is checked. + */ + @property({ reflect: true, type: Boolean }) public checked = false; + + /** + * Label size variant, either m or s. + */ + @property({ reflect: true }) public size: SbbRadioButtonSize = 'm'; + + /** + * Whether the component must be set disabled due disabled attribute on sbb-radio-button-group. + */ + @state() private _disabledFromGroup = false; + + /** + * Whether the component must be set required due required attribute on sbb-radio-button-group. + */ + @state() private _requiredFromGroup = false; + + /** + * State of listed named slots, by indicating whether any element for a named slot is defined. + */ + @state() private _namedSlots = createNamedSlotState('subtext', 'suffix'); + + /** + * Whether the input is the main input of a selection panel. + */ + @state() private _isSelectionPanelInput = false; + + /** + * The label describing whether the selection panel is expanded (for screen readers only). + */ + @state() private _selectionPanelExpandedLabel: string; + + @state() private _currentLanguage = documentLanguage(); + + private _selectionPanelElement: HTMLElement; + private _abort = new ConnectedAbortController(this); + private _radioButtonAttributeObserver = new AgnosticMutationObserver( + this._onRadioButtonAttributesChange.bind(this), + ); + + /** + * @internal + * Internal event that emits whenever the state of the radio option + * in relation to the parent selection panel changes. + */ + private _stateChange: EventEmitter = new EventEmitter( + this, + SbbRadioButton.events.stateChange, + { bubbles: true }, + ); + + /** + * @internal + * Internal event that emits when the radio button is loaded. + */ + private _radioButtonLoaded: EventEmitter = new EventEmitter( + this, + SbbRadioButton.events.radioButtonLoaded, + { bubbles: true }, + ); + + private _handleCheckedChange(currentValue: boolean, previousValue: boolean): void { + if (currentValue !== previousValue) { + this._stateChange.emit({ type: 'checked', checked: currentValue }); + this._isSelectionPanelInput && this._updateExpandedLabel(); + } + } + + private _handleDisabledChange(currentValue: boolean, previousValue: boolean): void { + if (currentValue !== previousValue) { + this._stateChange.emit({ type: 'disabled', disabled: currentValue }); + } + } + + private _handleClick(event: Event): void { + event.preventDefault(); + this.select(); + } + + public select(): void { + if (this.disabled || this._disabledFromGroup) { + return; + } + + if (this.allowEmptySelection) { + this.checked = !this.checked; + } else if (!this.checked) { + this.checked = true; + } + } + + private _handlerRepository = new HandlerRepository( + this, + languageChangeHandlerAspect((l) => (this._currentLanguage = l)), + namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), + formElementHandlerAspect, + ); + + public override connectedCallback(): void { + super.connectedCallback(); + const signal = this._abort.signal; + this.addEventListener('click', (e) => this._handleClick(e), { signal }); + this.addEventListener('keydown', (e) => this._handleKeyDown(e), { signal }); + this._handlerRepository.connect(); + // We can use closest here, as we expect the parent sbb-selection-panel to be in light DOM. + this._selectionPanelElement = this.closest('sbb-selection-panel'); + this._isSelectionPanelInput = + !!this._selectionPanelElement && !this.closest('sbb-selection-panel [slot="content"]'); + this._setupInitialStateAndAttributeObserver(); + this._isSelectionPanelInput && this._radioButtonLoaded.emit(); + } + + protected override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('checked')) { + this._handleCheckedChange(this.checked, changedProperties.get('checked')); + } + if (changedProperties.has('disabled')) { + this._handleDisabledChange(this.disabled, changedProperties.get('disabled')); + } + } + + protected override firstUpdated(): void { + this._isSelectionPanelInput && this._updateExpandedLabel(); + } + + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this._handlerRepository.disconnect(); + this._radioButtonAttributeObserver.disconnect(); + } + + private _handleKeyDown(evt: KeyboardEvent): void { + if (evt.code === 'Space') { + this.select(); + } + } + + // Set up the initial disabled/required values and start observe attributes changes. + private _setupInitialStateAndAttributeObserver(): void { + const parentGroup = this.closest('sbb-radio-button-group') as SbbRadioButtonGroup; + if (parentGroup) { + this._requiredFromGroup = isValidAttribute(parentGroup, 'required'); + this._disabledFromGroup = isValidAttribute(parentGroup, 'disabled'); + this.size = parentGroup.size; + } + this._radioButtonAttributeObserver.observe(this, radioButtonObserverConfig); + } + + /** Observe changes on data attributes and set the appropriate values. */ + private _onRadioButtonAttributesChange(mutationsList: MutationRecord[]): void { + for (const mutation of mutationsList) { + if (mutation.attributeName === 'data-group-disabled') { + this._disabledFromGroup = !!isValidAttribute(this, 'data-group-disabled'); + } + if (mutation.attributeName === 'data-group-required') { + this._requiredFromGroup = !!isValidAttribute(this, 'data-group-required'); + } + } + } + + private _updateExpandedLabel(): void { + if (!this._selectionPanelElement.hasAttribute('data-has-content')) { + this._selectionPanelExpandedLabel = ''; + return; + } + + this._selectionPanelExpandedLabel = this.checked + ? ', ' + i18nExpanded[this._currentLanguage] + : ', ' + i18nCollapsed[this._currentLanguage]; + } + + protected override render(): TemplateResult { + const attributes = { + role: 'radio', + 'aria-checked': this.checked?.toString() ?? 'false', + 'aria-required': (this.required || this._requiredFromGroup).toString(), + 'aria-disabled': (this.disabled || this._disabledFromGroup).toString(), + 'data-is-selection-panel-input': this._isSelectionPanelInput, + }; + setAttributes(this, attributes); + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-radio-button': SbbRadioButton; + } +} diff --git a/src/components/radio-button/radio-button/readme.md b/src/components/radio-button/radio-button/readme.md new file mode 100644 index 0000000000..1dd009ca1a --- /dev/null +++ b/src/components/radio-button/radio-button/readme.md @@ -0,0 +1,66 @@ +The `sbb-radio-button` component provides the same functionality +as a native `` enhanced with the SBB Design: use multiple `sbb-radio-button` components +inside a [sbb-radio-button-group](/docs/components-sbb-radio-button-sbb-radio-button-group--docs) component +in order to display a radio input within a group. + +```html + + Option one + Option two + +``` + +## States + +It is possible to display the component in `disabled` or `checked` state by using the self-named properties. + +The component has a `required` property, which can be useful +for setting a custom [sbb-form-error](/docs/components-sbb-form-field-sbb-form-error--docs) message +within a [sbb-form-field](/docs/components-sbb-form-field-sbb-form-field--docs). + +The `allowEmptySelection` property allows user to deselect the component. + +```html +Option one + +Option two + +Option three + +Option four +``` + +## Style + +The component has two different sizes, which can be changed using the `size` property (`m`, which is the default, and `s`). + +```html +Size +``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ----------------------- | ------- | -------------------- | ------- | ------------------------------------- | +| `allowEmptySelection` | `allow-empty-selection` | public | `boolean` | `false` | Whether the radio can be deselected. | +| `value` | `value` | public | `string` | | Value of radio button. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the radio button is disabled. | +| `required` | `required` | public | `boolean` | `false` | Whether the radio button is required. | +| `checked` | `checked` | public | `boolean` | `false` | Whether the radio button is checked. | +| `size` | `size` | public | `SbbRadioButtonSize` | `'m'` | Label size variant, either m or s. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| -------- | ------- | ----------- | ---------- | ------ | -------------- | +| `select` | public | | | `void` | | + +## Slots + +| Name | Description | +| --------- | ----------------------------------------------------------------------------------------------------- | +| | Use the unnamed slot to add content to the radio label. | +| `subtext` | Slot used to render a subtext under the label (only visible within a `sbb-selection-panel`). | +| `suffix` | Slot used to render additional content after the label (only visible within a `sbb-selection-panel`). | diff --git a/src/components/sbb-accordion/readme.md b/src/components/sbb-accordion/readme.md deleted file mode 100644 index 409fba485c..0000000000 --- a/src/components/sbb-accordion/readme.md +++ /dev/null @@ -1,65 +0,0 @@ -The `sbb-accordion` is a component which acts as a container -for one or more [sbb-expansion-panel](/docs/components-sbb-accordion-sbb-expansion-panel--docs). - -```html - - - Header 1 - Content 1 - - - Header 2 - Content 2 - - -``` - -## Interaction - -The `multi` property, if set, allows having more than one `sbb-expansion-panel` expanded at the same time. - -```html - - ... - -``` - -## Style - -The component has a `titleLevel` property, which is proxied to each inner `sbb-expansion-panel-header`, and can be used -to wrap the header of each `sbb-expansion-panel` in a heading tag; if the property is unset, a `div` is used. - -In the following example, all the `sbb-expansion-panel-header` would be wrapped in a `h3` heading tag. - -```html - - - Header 1 - Content 1 - - ... - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | --------------------------------------------------------------------------- | ---------------------------------------- | ----------- | -| `disableAnimation` | `disable-animation` | Whether the animation should be disabled. | `boolean` | `false` | -| `multi` | `multi` | Whether more than one sbb-expansion-panel can be open at the same time. | `boolean` | `false` | -| `titleLevel` | `title-level` | The heading level for the sbb-expansion-panel-headers within the component. | `"1" \| "2" \| "3" \| "4" \| "5" \| "6"` | `undefined` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------------ | -| `"unnamed"` | Use this to add one or more sbb-expansion-panel. | - - ----------------------------------------------- - - diff --git a/src/components/sbb-accordion/sbb-accordion.e2e.ts b/src/components/sbb-accordion/sbb-accordion.e2e.ts deleted file mode 100644 index 27fb0f6ced..0000000000 --- a/src/components/sbb-accordion/sbb-accordion.e2e.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; -import sbbExpansionPanelEvents from '../sbb-expansion-panel/sbb-expansion-panel.events'; - -describe('sbb-accordion', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - Header 1 - Content 1 - - - Header 2 - Content 2 - - - Header 3 - Content 3 - - - `); - - element = await page.find('sbb-accordion'); - await page.waitForChanges(); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('should set accordion context on expansion panel', async () => { - const panels = await page.findAll('sbb-expansion-panel'); - - expect(panels[0]).toHaveAttribute('data-accordion-first'); - expect(panels[0]).toHaveAttribute('data-accordion'); - expect(panels[1]).toHaveAttribute('data-accordion'); - expect(panels[2]).toHaveAttribute('data-accordion'); - expect(panels[2]).toHaveAttribute('data-accordion-last'); - }); - - it('should set accordion context on expansion panel when removing and adding expansion-panels', async () => { - let panels: E2EElement[]; - await page.waitForChanges(); - - await page.evaluate(() => document.querySelector('sbb-expansion-panel').remove()); - await page.waitForChanges(); - - panels = await page.findAll('sbb-expansion-panel'); - expect(panels[0]).toHaveAttribute('data-accordion-first'); - expect(panels[1]).toHaveAttribute('data-accordion-last'); - - await page.evaluate(() => document.querySelector('sbb-expansion-panel').remove()); - await page.waitForChanges(); - - const lastRemainingPanel = await page.find('sbb-expansion-panel'); - expect(lastRemainingPanel).toHaveAttribute('data-accordion-first'); - expect(lastRemainingPanel).toHaveAttribute('data-accordion-last'); - - await page.evaluate(() => { - const panel = document.createElement('sbb-expansion-panel'); - document.querySelector('sbb-accordion').append(panel); - }); - await page.waitForChanges(); - - panels = await page.findAll('sbb-expansion-panel'); - expect(panels[0]).toHaveAttribute('data-accordion-first'); - expect(panels[0]).not.toHaveAttribute('data-accordion-last'); - expect(panels[1]).toHaveAttribute('data-accordion-last'); - }); - - it('should inherit titleLevel prop by panels', async () => { - const panels = await page.findAll('sbb-expansion-panel'); - expect(panels.length).toEqual(3); - expect(panels[0].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H4', - ); - expect(panels[1].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H4', - ); - expect(panels[2].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H4', - ); - }); - - it('should dynamically update titleLevel prop', async () => { - await element.setProperty('titleLevel', '6'); - await page.waitForChanges(); - const panels = await page.findAll('sbb-expansion-panel'); - expect(panels.length).toEqual(3); - expect(panels[0].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H6', - ); - expect(panels[1].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H6', - ); - expect(panels[2].shadowRoot.querySelector('.sbb-expansion-panel').firstChild.nodeName).toEqual( - 'H6', - ); - }); - - it('should close others when expanding and multi = false', async () => { - const willOpenEventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.willOpen); - const panelOne: E2EElement = await page.find('#panel-1'); - const headerOne: E2EElement = await page.find('#header-1'); - const panelTwo: E2EElement = await page.find('#panel-2'); - const headerTwo: E2EElement = await page.find('#header-2'); - const panelThree: E2EElement = await page.find('#panel-3'); - const headerThree: E2EElement = await page.find('#header-3'); - - for (const panel of [panelOne, panelTwo, panelThree]) { - expect(await panel.getProperty('expanded')).toEqual(false); - } - - await headerTwo.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(false); - expect(await panelTwo.getProperty('expanded')).toEqual(true); - expect(await panelThree.getProperty('expanded')).toEqual(false); - - await headerOne.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); - expect(willOpenEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(true); - expect(await panelTwo.getProperty('expanded')).toEqual(false); - expect(await panelThree.getProperty('expanded')).toEqual(false); - - await headerThree.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); - expect(willOpenEventSpy).toHaveReceivedEventTimes(3); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(false); - expect(await panelTwo.getProperty('expanded')).toEqual(false); - expect(await panelThree.getProperty('expanded')).toEqual(true); - }); - - it('should not change others when expanding and multi = false', async () => { - await element.setProperty('multi', 'true'); - await page.waitForChanges(); - const willOpenEventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.willOpen); - const panelOne: E2EElement = await page.find('#panel-1'); - const panelTwo: E2EElement = await page.find('#panel-2'); - const panelThree: E2EElement = await page.find('#panel-3'); - for (const panel of [panelOne, panelTwo, panelThree]) { - expect(await panel.getProperty('expanded')).toEqual(false); - } - - await panelTwo.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(false); - expect(await panelTwo.getProperty('expanded')).toEqual(true); - expect(await panelThree.getProperty('expanded')).toEqual(false); - - await panelOne.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); - expect(willOpenEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(true); - expect(await panelTwo.getProperty('expanded')).toEqual(true); - expect(await panelThree.getProperty('expanded')).toEqual(false); - - await panelThree.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); - expect(willOpenEventSpy).toHaveReceivedEventTimes(3); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(true); - expect(await panelTwo.getProperty('expanded')).toEqual(true); - expect(await panelThree.getProperty('expanded')).toEqual(true); - }); - - it('should close all panels except the first when multi changes from true to false', async () => { - await element.setProperty('multi', 'true'); - await page.waitForChanges(); - const panelOne: E2EElement = await page.find('#panel-1'); - const panelTwo: E2EElement = await page.find('#panel-2'); - const panelThree: E2EElement = await page.find('#panel-3'); - for (const panel of [panelOne, panelTwo, panelThree]) { - expect(await panel.getProperty('expanded')).toEqual(false); - } - - const willOpenEventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.willOpen); - - await panelTwo.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - expect(await panelTwo.getProperty('expanded')).toEqual(true); - - await panelThree.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); - expect(willOpenEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - expect(await panelThree.getProperty('expanded')).toEqual(true); - - await element.setProperty('multi', 'false'); - await page.waitForChanges(); - expect(await panelOne.getProperty('expanded')).toEqual(true); - expect(await panelTwo.getProperty('expanded')).toEqual(false); - expect(await panelThree.getProperty('expanded')).toEqual(false); - }); -}); diff --git a/src/components/sbb-accordion/sbb-accordion.spec.ts b/src/components/sbb-accordion/sbb-accordion.spec.ts deleted file mode 100644 index c5106f537c..0000000000 --- a/src/components/sbb-accordion/sbb-accordion.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { SbbAccordion } from './sbb-accordion'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-accordion', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbAccordion], - html: ` - - - Header 1 - Content 1 - - - Header 2 - Content 2 - - - `, - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    - - Header 1 - Content 1 - - - Header 2 - Content 2 - -
    - `); - }); -}); diff --git a/src/components/sbb-accordion/sbb-accordion.tsx b/src/components/sbb-accordion/sbb-accordion.tsx deleted file mode 100644 index d8432aa34b..0000000000 --- a/src/components/sbb-accordion/sbb-accordion.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Component, ComponentInterface, Element, h, JSX, Listen, Prop, Watch } from '@stencil/core'; -import { InterfaceTitleAttributes } from '../sbb-title/sbb-title.custom'; -import { toggleDatasetEntry } from '../../global/dom'; - -/** - * @slot unnamed - Use this to add one or more sbb-expansion-panel. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-accordion.scss', - tag: 'sbb-accordion', -}) -export class SbbAccordion implements ComponentInterface { - /** The heading level for the sbb-expansion-panel-headers within the component. */ - @Prop() public titleLevel?: InterfaceTitleAttributes['level']; - - /** Whether the animation should be disabled. */ - @Prop({ reflect: true }) public disableAnimation = false; - - /** Whether more than one sbb-expansion-panel can be open at the same time. */ - @Prop() public multi = false; - - @Listen('will-open') - public closePanels(e): void { - if (e.target?.tagName !== 'SBB-EXPANSION-PANEL' || this.multi) { - return; - } - - this._expansionPanels - .filter((panel) => panel !== e.target) - .forEach((panel) => (panel.expanded = false)); - } - - @Watch('multi') - public resetExpansionPanels(newValue: boolean, oldValue: boolean): void { - // If it's changing from "multi = true" to "multi = false", open the first panel and close all the others. - const expansionPanels = this._expansionPanels; - if (expansionPanels.length > 1 && oldValue && !newValue) { - expansionPanels[0].expanded = true; - expansionPanels - .filter((_, index: number) => index > 0) - .forEach((panel) => (panel.expanded = false)); - } - } - - @Watch('titleLevel') - public setTitleLevelOnChildren(): void { - this._expansionPanels.forEach((panel) => (panel.titleLevel = this.titleLevel)); - } - - @Element() private _element!: HTMLElement; - - private get _expansionPanels(): HTMLSbbExpansionPanelElement[] { - return Array.from(this._element.querySelectorAll('sbb-expansion-panel')); - } - - private _setChildrenParameters(): void { - const expansionPanels = this._expansionPanels; - if (!expansionPanels) { - return; - } - - expansionPanels.forEach((panel: HTMLSbbExpansionPanelElement) => { - panel.titleLevel = this.titleLevel; - - toggleDatasetEntry(panel, 'accordionFirst', false); - toggleDatasetEntry(panel, 'accordionLast', false); - - if (this.disableAnimation) { - panel.setAttribute('disable-animation', 'true'); - } else { - panel.removeAttribute('disable-animation'); - } - }); - toggleDatasetEntry(expansionPanels[0], 'accordionFirst', true); - toggleDatasetEntry(expansionPanels[expansionPanels.length - 1], 'accordionLast', true); - } - - public render(): JSX.Element { - return ( -
    - this._setChildrenParameters()}> -
    - ); - } -} diff --git a/src/components/sbb-action-group/readme.md b/src/components/sbb-action-group/readme.md deleted file mode 100644 index 3f58c2116b..0000000000 --- a/src/components/sbb-action-group/readme.md +++ /dev/null @@ -1,139 +0,0 @@ -The `sbb-action-group` component is a generic content container which can contain up to three action items -([sbb-button](/docs/components-sbb-button--docs) or [sbb-link](/docs/components-sbb-link--docs) or other HTML elements) -in various [allocations](#allocations). - -## Style - -### Orientation - -The `orientation` property is used to set item's orientation. -Possible values are `horizontal` (default) and `vertical`. - -The optional property `horizontalFrom` can be used in combination with `orientation='vertical'` to -indicate the minimum breakpoint from which the orientation changes to `horizontal`. - -```html - - Action 1 - Action 2 - - Action 3 - - -``` - -### Button-size and link-size - -The two props `button-size` and `link-size` can be used to override, respectively, the size of the inner `sbb-button` and `sbb-link`. -Default values are `l` for `sbb-button` and `m` for `sbb-link`. - -```html - - Action 1 - - Action 3 - - -``` - -### Align-group and align-self - -The `align-group` property can be used to set the default alignment of the contained elements; -possible values are `start`, `center`, `stretch` and `end`. - -It is also possible to set the `align-self` attribute on action items in order to move them in the -opposite direction to the group; possible values are `start`, `center` or `end`. - -**NOTE**: The `sbb-action-group` will automatically set variant `block` and will sync the `linkSize` - property with nested `sbb-link` and the `buttonSize` property with the nested `sbb-button` - instances. - -```html - - Action 1 - Action 2 - Action 3 - -``` - -## Allocations - -Items can be displayed inside `sbb-action-group` in different allocations. - -If we define the triad x-y-z as the number of elements aligned at the start, at the center and at the end of the component, -and we consider a template like the following one (possibly removing the link for 2-elements allocations): - -```html - - Button 1 - Button 2 - - Link - - -``` - -The values for `align-group` and `align-self` for the various allocations are as follows: - -### Horizontal - -| orientation='horizontal' | align-group | align-self | -|:------------------------:|:-----------:|:------------------:| -| 3-0-0 | start | / | -| 1-1-1 | start | Button 2: 'center' | -| 2-0-1 | start | Link: 'end' | -| 1-0-2 | end | Button 1: 'start' | -| 2-0-0 | start | / | -| 1-0-1 | start | Button 2: 'end' | - -### Vertical - -| orientation='vertical' | align-group | align-self | -|:----------------------:|:-----------:|:----------:| -| 3-0-0 | start | / | -| 2-0-0 | start | / | -| 0-3-0 | center | / | -| 0-2-0 | center | / | -| 0-0-3 | end | / | -| 0-0-2 | end | / | - -| orientation='vertical' (full width) | align-group | align-self | -|:-----------------------------------:|:-----------:|:--------------:| -| 3-0-0 | stretch | Link: 'start' | -| 2-0-0 | stretch | / | -| 0-3-0 | stretch | Link: 'center' | -| 0-2-0 | stretch | / | -| 0-0-3 | stretch | Link: 'end' | -| 0-0-2 | stretch | / | - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------- | -| `alignGroup` | `align-group` | Set the slotted `` children's alignment. | `"center" \| "end" \| "start" \| "stretch"` | `'start'` | -| `buttonSize` | `button-size` | Size of the nested sbb-button instances. This will overwrite the size attribute of nested sbb-button instances. | `"l" \| "m"` | `'l'` | -| `horizontalFrom` | `horizontal-from` | Overrides the behaviour of `orientation` property. | `"large" \| "medium" \| "micro" \| "small" \| "ultra" \| "wide" \| "zero"` | `'medium'` | -| `linkSize` | `link-size` | Size of the nested sbb-link instances. This will overwrite the size attribute of nested sbb-link instances. | `"m" \| "s" \| "xs"` | `'m'` | -| `orientation` | `orientation` | Indicates the orientation of the components inside the ``. | `"horizontal" \| "vertical"` | `'horizontal'` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------------ | -| `"unnamed"` | Slot to render the content inside the container. | - - ----------------------------------------------- - - diff --git a/src/components/sbb-action-group/sbb-action-group.custom.d.ts b/src/components/sbb-action-group/sbb-action-group.custom.d.ts deleted file mode 100644 index 11040c80b9..0000000000 --- a/src/components/sbb-action-group/sbb-action-group.custom.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface InterfaceSbbActionGroupAttributes { - alignGroup: 'start' | 'center' | 'stretch' | 'end'; - horizontalFrom?: 'zero' | 'micro' | 'small' | 'medium' | 'large' | 'wide' | 'ultra'; - orientation: 'horizontal' | 'vertical'; -} diff --git a/src/components/sbb-action-group/sbb-action-group.e2e.ts b/src/components/sbb-action-group/sbb-action-group.e2e.ts deleted file mode 100644 index 01ebdcb8ec..0000000000 --- a/src/components/sbb-action-group/sbb-action-group.e2e.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-action-group', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - Button - - Link - - - `); - element = await page.find('sbb-action-group'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - describe('property sync', () => { - it('should sync default size with sbb-button', async () => { - await page.waitForChanges(); - const links = await page.findAll('sbb-action-group sbb-button'); - expect(links.every((l) => l.getAttribute('size') === 'l')).toBeTruthy(); - }); - - it('should update attributes with button-size="m"', async () => { - element.setAttribute('button-size', 'm'); - await page.waitForChanges(); - const links = await page.findAll('sbb-action-group sbb-button'); - expect(links.every((l) => l.getAttribute('size') === 'm')).toBeTruthy(); - }); - - it('should update attributes with link-size="s"', async () => { - element.setAttribute('link-size', 's'); - await page.waitForChanges(); - const links = await page.findAll('sbb-action-group sbb-link'); - expect(links.every((l) => l.getAttribute('size') === 's')).toBeTruthy(); - }); - - it('should apply variant block to sbb-link', async () => { - await page.waitForChanges(); - const links = await page.findAll('sbb-action-group sbb-link'); - expect(links.every((l) => l.getAttribute('variant') === 'block')).toBeTruthy(); - }); - }); -}); diff --git a/src/components/sbb-action-group/sbb-action-group.spec.ts b/src/components/sbb-action-group/sbb-action-group.spec.ts deleted file mode 100644 index 50a5f8d721..0000000000 --- a/src/components/sbb-action-group/sbb-action-group.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { SbbActionGroup } from './sbb-action-group'; -import { newSpecPage } from '@stencil/core/testing'; -import { AnyHTMLElement } from '@stencil/core/internal'; -import { patchSlotchangeEvent } from '../../global/testing'; - -describe('sbb-action-group', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbActionGroup], - html: ` - - Button - - Link - - - `, - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    - Button - - Link - -
    - `); - }); - - describe('property sync', () => { - const assertButtons = ( - root: AnyHTMLElement, - assertion: (link: HTMLSbbButtonElement) => boolean, - ): boolean => Array.from(root.querySelectorAll('sbb-button')).every(assertion); - - it('should sync default button-size property with sbb-button', async () => { - const { root } = await newSpecPage({ - components: [SbbActionGroup], - html: ` - - Button - - Link - - - `, - }); - patchSlotchangeEvent(root); - - expect(assertButtons(root, (b) => b.size === 'l')).toBeTruthy(); - }); - - it('should sync button-size property with sbb-button', async () => { - const { root } = await newSpecPage({ - components: [SbbActionGroup], - html: ` - - Button - - Link - - - `, - }); - patchSlotchangeEvent(root); - - expect(assertButtons(root, (b) => b.size === 'm')).toBeTruthy(); - }); - - it('should apply block variant to sbb-link', async () => { - const { root } = await newSpecPage({ - components: [SbbActionGroup], - html: ` - - Button - - Link - - - `, - }); - patchSlotchangeEvent(root); - - expect( - Array.from(root.querySelectorAll('sbb-link')).every((l) => l.variant === 'block'), - ).toBeTruthy(); - }); - - it('should sync link-size property with sbb-link', async () => { - const { root } = await newSpecPage({ - components: [SbbActionGroup], - html: ` - - Button - - Link - - - `, - }); - patchSlotchangeEvent(root); - - expect( - Array.from(root.querySelectorAll('sbb-link')).every((l) => l.size === 's'), - ).toBeTruthy(); - }); - }); -}); diff --git a/src/components/sbb-action-group/sbb-action-group.tsx b/src/components/sbb-action-group/sbb-action-group.tsx deleted file mode 100644 index f8c4129129..0000000000 --- a/src/components/sbb-action-group/sbb-action-group.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Component, Element, h, JSX, Prop, Watch } from '@stencil/core'; -import { InterfaceButtonAttributes } from '../sbb-button/sbb-button.custom'; -import { InterfaceLinkAttributes } from '../sbb-link/sbb-link.custom'; -import { InterfaceSbbActionGroupAttributes } from './sbb-action-group.custom'; - -/** - * @slot unnamed - Slot to render the content inside the container. - */ - -@Component({ - shadow: true, - styleUrl: 'sbb-action-group.scss', - tag: 'sbb-action-group', -}) -export class SbbActionGroup { - /** - * Set the slotted `` children's alignment. - */ - @Prop({ reflect: true }) public alignGroup: InterfaceSbbActionGroupAttributes['alignGroup'] = - 'start'; - - /** - * Overrides the behaviour of `orientation` property. - */ - @Prop({ reflect: true }) - public horizontalFrom?: InterfaceSbbActionGroupAttributes['horizontalFrom'] = 'medium'; - - /** - * Indicates the orientation of the components inside the ``. - */ - @Prop({ reflect: true }) public orientation: InterfaceSbbActionGroupAttributes['orientation'] = - 'horizontal'; - - /** - * Size of the nested sbb-button instances. This will overwrite the size attribute of nested - * sbb-button instances. - */ - @Prop({ reflect: true }) public buttonSize?: InterfaceButtonAttributes['size'] = 'l'; - - /** - * Size of the nested sbb-link instances. This will overwrite the size attribute of nested - * sbb-link instances. - */ - @Prop({ reflect: true }) public linkSize?: InterfaceLinkAttributes['size'] = 'm'; - - @Element() private _element!: HTMLElement; - - @Watch('buttonSize') - public syncButtons(): void { - this._element.querySelectorAll('sbb-button').forEach((b) => (b.size = this.buttonSize)); - } - - @Watch('linkSize') - public syncLinks(): void { - this._element.querySelectorAll('sbb-link').forEach((link) => { - link.variant = 'block'; - link.size = this.linkSize; - }); - } - - public render(): JSX.Element { - return ( -
    - { - this.syncButtons(); - this.syncLinks(); - }} - /> -
    - ); - } -} diff --git a/src/components/sbb-alert-group/readme.md b/src/components/sbb-alert-group/readme.md deleted file mode 100644 index deef26a15e..0000000000 --- a/src/components/sbb-alert-group/readme.md +++ /dev/null @@ -1,72 +0,0 @@ -The `sbb-alert-group` manages the dismissal and accessibility of one or multiple -[sbb-alert](/docs/components-sbb-alert-sbb-alert--docs) and also its visual gap between each other. - -```html - - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - - - Between Berne and Olten from 03.11.2021 to 05.12.2022 each time from 22:30 to 06:00 o'clock - construction work will take place. You have to expect changed travel times and changed - connections. - - -``` - -## Interactions - -If all the `sbb-alert`s are dismissed, it's recommended to completely remove the `sbb-alert-group` from DOM. - -You can catch this moment by listening to `empty` event and react accordingly. - -## Accessibility - -By specifying the `accessibility-title` it's possible to add a hidden title to the `sbb-alert-group`. -The heading level can be set via `accessibility-title-level`. - -By default, the `sbb-alert-group` has the role `status` which means that if a new alert arrives, -it will be read out as soon as the user is idle -(equal to [aria-live="polite"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)). - -You can change the `role` or `aria-live` attributes to fit your needs. -For example, you can set the `role` to `alert` which implicitly sets `aria-live` to `assertive` -and therefore interrupts screen reader flow, to immediately read out the alert content. - -**Note that with role `alert`, in some combinations of screen readers and browsers not every part of the alert is fully read.** - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | -| `accessibilityTitle` | `accessibility-title` | Title for this alert group which is only visible for screen reader users. | `string` | `undefined` | -| `accessibilityTitleLevel` | `accessibility-title-level` | Level of the accessibility title, will be rendered as heading tag (e.g. h2). Defaults to level 2. | `"1" \| "2" \| "3" \| "4" \| "5" \| "6"` | `'2'` | -| `role` | `role` | The role attribute defines how to announce alerts to the user. 'status': sets aria-live to polite and aria-atomic to true. 'alert': sets aria-live to assertive and aria-atomic to true. | `string` | `'status'` | - - -## Events - -| Event | Description | Type | -| ------------------- | ------------------------------------------- | ---------------------------------- | -| `did-dismiss-alert` | Emits when an alert was removed from DOM. | `CustomEvent` | -| `empty` | Emits when `sbb-alert-group` becomes empty. | `CustomEvent` | - - -## Slots - -| Slot | Description | -| ----------------------- | ----------------------------------------------------------------------------- | -| `"accessibility-title"` | title for this sbb-alert-group which is only visible for screen reader users. | -| `"unnamed"` | content slot, should be filled with `sbb-alert` items. | - - ----------------------------------------------- - - diff --git a/src/components/sbb-alert-group/sbb-alert-group.custom.d.ts b/src/components/sbb-alert-group/sbb-alert-group.custom.d.ts deleted file mode 100644 index ef16338ae4..0000000000 --- a/src/components/sbb-alert-group/sbb-alert-group.custom.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterfaceSbbAlertGroupAttributes { - role: 'alert' | 'status' | string; -} diff --git a/src/components/sbb-alert-group/sbb-alert-group.e2e.ts b/src/components/sbb-alert-group/sbb-alert-group.e2e.ts deleted file mode 100644 index 31190897e3..0000000000 --- a/src/components/sbb-alert-group/sbb-alert-group.e2e.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { newE2EPage } from '@stencil/core/testing'; -import events from './sbb-alert-group.events'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-alert-group', () => { - it('should handle events ond states on interacting with alerts', async () => { - const alertGroupId = 'alertgroup'; - const accessibilityTitle = 'Disruptions'; - const accessibilityTitleLevel = '3'; - - // Given sbb-alert-group with two alerts - const page = await newE2EPage(); - await page.setContent(` - - First - Second - -`); - const didDismissAlertSpy = await page.spyOnEvent(events.didDismissAlert); - const emptySpy = await page.spyOnEvent(events.empty); - - // When rendering initially - await page.waitForChanges(); - - // Then two alerts should be rendered and accessibility title should be displayed - expect((await page.findAll('sbb-alert')).length).toBe(2); - const alertGroupTitle = await page.find('sbb-alert-group >>> .sbb-alert-group__title'); - expect(alertGroupTitle.textContent).toBe(accessibilityTitle); - expect(alertGroupTitle.tagName).toBe(`H${accessibilityTitleLevel}`); - - // When clicking on close button of the first alert - await (await page.find('sbb-alert >>> .sbb-alert__close-button-wrapper sbb-button')).click(); - await page.waitForChanges(); - - // Then one alert should be removed from sbb-alert-group, tabindex should be set to 0, - // focus should be on sbb-alert-group and accessibility title should still be rendered. - // Moreover, didDismissAlert event should have been fired. - expect((await page.findAll('sbb-alert')).length).toBe(1); - expect((await page.find('sbb-alert-group')).tabIndex).toBe(0); - expect(await page.evaluate(() => document.activeElement.id)).toBe(alertGroupId); - expect((await page.find('sbb-alert-group >>> .sbb-alert-group__title')).textContent).toBe( - accessibilityTitle, - ); - expect(didDismissAlertSpy).toHaveReceivedEvent(); - expect(emptySpy).not.toHaveReceivedEvent(); - - // When clicking on close button of the second alert - await (await page.find('sbb-alert >>> .sbb-alert__close-button-wrapper sbb-button')).click(); - await page.waitForChanges(); - - // Then the alert should be removed from sbb-alert-group, tabindex should be set to 0, - // focus should be on sbb-alert-group, accessibility title should be removed and empty event should be fired. - expect((await page.findAll('sbb-alert')).length).toBe(0); - expect((await page.find('sbb-alert-group')).tabIndex).toBe(0); - expect(await page.evaluate(() => document.activeElement.id)).toBe(alertGroupId); - expect(await page.find('sbb-alert-group >>> .sbb-alert-group__title')).toBeNull(); - await waitForCondition(() => didDismissAlertSpy.events.length === 2); - expect(didDismissAlertSpy).toHaveReceivedEventTimes(2); - expect(emptySpy).toHaveReceivedEvent(); - - // When clicking away (simulated by blur event) - (await page.find('sbb-alert-group')).triggerEvent('blur'); - await page.waitForChanges(); - - // Then the active element id should be unset and tabindex should be removed - expect(await page.evaluate(() => document.activeElement.id)).toBe(''); - expect((await page.find('sbb-alert-group')).tabIndex).toBe(-1); - }); - - it('should not trigger empty event after initializing with empty sbb-alert-group', async () => { - // Given empty sbb-alert-group - const page = await newE2EPage(); - const emptySpy = await page.spyOnEvent(events.empty); - - await page.setContent(``); - await page.waitForChanges(); - - // Then no title should be rendered and no empty event fired - expect(await page.find('sbb-alert-group >>> .sbb-alert-group__title')).toBeNull(); - expect(emptySpy).not.toHaveReceivedEvent(); - }); -}); diff --git a/src/components/sbb-alert-group/sbb-alert-group.events.ts b/src/components/sbb-alert-group/sbb-alert-group.events.ts deleted file mode 100644 index 3a92f3b07c..0000000000 --- a/src/components/sbb-alert-group/sbb-alert-group.events.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - didDismissAlert: 'did-dismiss-alert', - empty: 'empty', -}; diff --git a/src/components/sbb-alert-group/sbb-alert-group.spec.ts b/src/components/sbb-alert-group/sbb-alert-group.spec.ts deleted file mode 100644 index 6a5509d616..0000000000 --- a/src/components/sbb-alert-group/sbb-alert-group.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { SbbAlertGroup } from './sbb-alert-group'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-alert-group', () => { - it('should render', async () => { - const { root } = await newSpecPage({ - components: [SbbAlertGroup], - html: ` - - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - - -`, - }); - - // TODO: sbb-alert-group-empty class is wrongly placed in test due to missing slotchange support - expect(root).toEqualHtml(` - - -
    - -
    -
    - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - -
    - `); - }); - - it('should render with slots', async () => { - const { root } = await newSpecPage({ - components: [SbbAlertGroup], - html: ` - - Interruptions - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - - -`, - }); - - // TODO: sbb-alert-group-empty class is wrongly placed in test due to missing slotchange support - expect(root).toEqualHtml(` - - -
    - -
    -
    - - Interruptions - - - The rail traffic between Allaman and Morges is interrupted. All trains are cancelled. - -
    - `); - }); -}); diff --git a/src/components/sbb-alert-group/sbb-alert-group.tsx b/src/components/sbb-alert-group/sbb-alert-group.tsx deleted file mode 100644 index cd99098aae..0000000000 --- a/src/components/sbb-alert-group/sbb-alert-group.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { - Component, - Element, - Event, - EventEmitter, - h, - Host, - JSX, - Listen, - Prop, - State, -} from '@stencil/core'; -import { InterfaceSbbAlertGroupAttributes } from './sbb-alert-group.custom'; -import { InterfaceTitleAttributes } from '../sbb-title/sbb-title.custom'; - -/** - * @slot unnamed - content slot, should be filled with `sbb-alert` items. - * @slot accessibility-title - title for this sbb-alert-group which is only visible for screen reader users. - */ - -@Component({ - shadow: true, - styleUrl: 'sbb-alert-group.scss', - tag: 'sbb-alert-group', -}) -export class SbbAlertGroup { - /** - * The role attribute defines how to announce alerts to the user. - * - * 'status': sets aria-live to polite and aria-atomic to true. - * 'alert': sets aria-live to assertive and aria-atomic to true. - */ - @Prop({ reflect: true }) - public role: InterfaceSbbAlertGroupAttributes['role'] = 'status'; - - /** Title for this alert group which is only visible for screen reader users. */ - @Prop() public accessibilityTitle: string; - - /** Level of the accessibility title, will be rendered as heading tag (e.g. h2). Defaults to level 2. */ - @Prop() public accessibilityTitleLevel: InterfaceTitleAttributes['level'] = '2'; - - /** Whether the group currently has any alerts. */ - @State() private _hasAlerts: boolean; - - @Element() private _element: HTMLElement; - - /** Emits when an alert was removed from DOM. */ - @Event({ - eventName: 'did-dismiss-alert', - }) - public didDismissAlert: EventEmitter; - - /** Emits when `sbb-alert-group` becomes empty. */ - @Event({ - eventName: 'empty', - }) - public empty: EventEmitter; - - /** - * @internal - */ - @Listen('dismissal-requested') - public removeAlert(event: Event): void { - const target = event.target as HTMLSbbAlertElement; - const hasFocusInsideAlertGroup = document.activeElement === target; - - target.parentNode.removeChild(target); - this.didDismissAlert.emit(target); - - // Restore focus - if (hasFocusInsideAlertGroup) { - // Set tabindex to 0 the make it focusable and afterwards focus it. - // This is done to not completely lose focus after removal of an alert. - // Once the sbb-alert-group was blurred, make the alert group not focusable again. - this._element.tabIndex = 0; - this._element.focus(); - this._element.addEventListener('blur', () => this._element.removeAttribute('tabindex'), { - once: true, - }); - } - } - - private _slotChanged(event: Event): void { - const hadAlerts = this._hasAlerts; - this._hasAlerts = (event.target as HTMLSlotElement).assignedElements().length > 0; - if (!this._hasAlerts && hadAlerts) { - this.empty.emit(); - } - } - - public render(): JSX.Element { - const TITLE_TAG_NAME = `h${this.accessibilityTitleLevel}`; - - return ( - -
    - {this._hasAlerts && ( - - {this.accessibilityTitle} - - )} - this._slotChanged(event)} /> -
    -
    - ); - } -} diff --git a/src/components/sbb-alert/readme.md b/src/components/sbb-alert/readme.md deleted file mode 100644 index 4a0d81d2a0..0000000000 --- a/src/components/sbb-alert/readme.md +++ /dev/null @@ -1,147 +0,0 @@ -The `sbb-alert` is a component which should be used to display important messages to a client. - -Multiple instances of this component can be used within -the [sbb-alert-group](/docs/components-sbb-alert-sbb-alert-group--docs) component. - -## Slots - -The text content is projected using and unnamed slot, while the title uses the slot named `title` or alternatively the `titleContent` property. -The component can optionally display a `sbb-icon` at the component start using the `iconName` property or via custom content using the `icon` slot. - -```html - - Between Bern and Olten from 03.11.2021 to 05.12.2022 each time from 22:30 to 06:00 o'clock - construction work will take place. - You have to expect changed travel times and changed connections. - - - - Interruption between Berne and Olten - - Between Bern and Olten from 03.11.2021 to 05.12.2022 each time from 22:30 to 06:00 o'clock - construction work will take place. - You have to expect changed travel times and changed connections. - -``` - -## Interactions - -It's possible to place an action, which by clicking navigates somewhere to display more information. -This can be done using the `linkContent` property combined with the `href` one. -The `target` and `rel` property are also configurable via the self-named properties. - -```html - - ... - -``` - -The `sbb-alert` can optionally be hidden by a user, if the `readonly` prop is not set. -Please note that clicking on the close button does not remove it from the DOM, this would be the responsibility -of the library consumer to do it by reacting to the specific event. -See also the [sbb-alert-group](/docs/components-sbb-alert-sbb-alert-group--docs) -which automatically removes an alert after clicking the close button. - -```html - - Between Bern and Olten from 03.11.2021 to 05.12.2022 each time from 22:30 to 06:00 o'clock - construction work will take place. - You have to expect changed travel times and changed connections. - -``` - -## Style - -Users can choose between two `size`, `m` (default) and `l`. - -```html - - ... - -``` - -## Accessibility - -Accessibility is mainly done by wrapping the alerts into the `sbb-alert-group`. - -The description text is wrapped into an `

    ` element to guarantee the semantic meaning. - -Avoid slotting block elements (e.g. `

    `) as this violates semantic rules and can have negative effects on screen readers. - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| -------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | -| `accessibilityLabel` | `accessibility-label` | This will be forwarded as aria-label to the relevant nested element. | `string` | `undefined` | -| `disableAnimation` | `disable-animation` | Whether the fade in animation should be disabled. | `boolean` | `false` | -| `href` | `href` | The href value you want to link to. | `string` | `undefined` | -| `iconName` | `icon-name` | Name of the icon which will be forward to the nested `sbb-icon`. Choose the icons from https://icons.app.sbb.ch. Styling is optimized for icons of type HIM-CUS. | `string` | `undefined` | -| `linkContent` | `link-content` | Content of the link. | `string` | `undefined` | -| `readonly` | `readonly` | Whether the alert is readonly. In readonly mode, there is no dismiss button offered to the user. | `boolean` | `false` | -| `rel` | `rel` | The relationship of the linked URL as space-separated link types. | `string` | `undefined` | -| `size` | `size` | You can choose between `m` or `l` size. | `"l" \| "m"` | `'m'` | -| `target` | `target` | Where to display the linked URL. | `string` | `undefined` | -| `titleContent` | `title-content` | Content of title. | `string` | `undefined` | -| `titleLevel` | `title-level` | Level of title, will be rendered as heading tag (e.g. h3). Defaults to level 3. | `"1" \| "2" \| "3" \| "4" \| "5" \| "6"` | `'3'` | - - -## Events - -| Event | Description | Type | -| --------------------- | ------------------------------------------------------------------ | ------------------- | -| `did-present` | Emits when the fade in animation ends and the button is displayed. | `CustomEvent` | -| `dismissal-requested` | Emits when dismissal of an alert was requested. | `CustomEvent` | -| `will-present` | Emits when the fade in animation starts. | `CustomEvent` | - - -## Methods - -### `requestDismissal() => Promise` - -Requests dismissal of the alert. - -#### Returns - -Type: `Promise` - - - - -## Slots - -| Slot | Description | -| ----------- | ---------------------------------------------------------------------------------------------------------- | -| `"icon"` | Should be a sbb-icon which is displayed next to the title. Styling is optimized for icons of type HIM-CUS. | -| `"title"` | Title content. | -| `"unnamed"` | Content of the alert. | - - -## Dependencies - -### Depends on - -- [sbb-icon](../sbb-icon) -- [sbb-title](../sbb-title) -- [sbb-link](../sbb-link) -- [sbb-divider](../sbb-divider) -- [sbb-button](../sbb-button) - -### Graph -```mermaid -graph TD; - sbb-alert --> sbb-icon - sbb-alert --> sbb-title - sbb-alert --> sbb-link - sbb-alert --> sbb-divider - sbb-alert --> sbb-button - sbb-link --> sbb-icon - sbb-button --> sbb-icon - style sbb-alert fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-alert/sbb-alert.custom.d.ts b/src/components/sbb-alert/sbb-alert.custom.d.ts deleted file mode 100644 index 3afdb3fae2..0000000000 --- a/src/components/sbb-alert/sbb-alert.custom.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterfaceAlertAttributes { - size: 'm' | 'l'; -} diff --git a/src/components/sbb-alert/sbb-alert.e2e.ts b/src/components/sbb-alert/sbb-alert.e2e.ts deleted file mode 100644 index ddb247361e..0000000000 --- a/src/components/sbb-alert/sbb-alert.e2e.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { newE2EPage } from '@stencil/core/testing'; -import events from './sbb-alert.events'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-alert', () => { - let alert, page; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - alert = await page.find('sbb-alert'); - expect(alert).toHaveClass('hydrated'); - }); - - // TODO: maybe fix some day. Test just doesn't work for unknown reason. - // eslint-disable-next-line jest/no-disabled-tests - it.skip('should fire animation events', async () => { - page = await newE2EPage(); - - const willPresentSpy = await page.spyOnEvent(events.willPresent); - const didPresentSpy = await page.spyOnEvent(events.didPresent); - - await page.setContent(`Interruption`); - await page.waitForChanges(); - - await waitForCondition(() => willPresentSpy.events.length === 1); - expect(willPresentSpy).toHaveReceivedEventTimes(1); - await waitForCondition(() => didPresentSpy.events.length === 1); - expect(didPresentSpy).toHaveReceivedEventTimes(1); - }); -}); diff --git a/src/components/sbb-alert/sbb-alert.events.ts b/src/components/sbb-alert/sbb-alert.events.ts deleted file mode 100644 index 22d2a2f035..0000000000 --- a/src/components/sbb-alert/sbb-alert.events.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - didPresent: 'did-present', - dismissalRequested: 'dismissal-requested', - willPresent: 'will-present', -}; diff --git a/src/components/sbb-alert/sbb-alert.spec.ts b/src/components/sbb-alert/sbb-alert.spec.ts deleted file mode 100644 index 00f9aec50d..0000000000 --- a/src/components/sbb-alert/sbb-alert.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { SbbAlert } from './sbb-alert'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-alert', () => { - it('should render default properties', async () => { - const { root } = await newSpecPage({ - components: [SbbAlert], - html: 'Alert content', - }); - - expect(root).toEqualHtml(` - - -
    -
    - - - - - - - - Interruption - -

    - -

    -
    - - - - -
    -
    -
    - Alert content -
    - `); - }); - - it('should render customized properties', async () => { - const { root } = await newSpecPage({ - components: [SbbAlert], - html: 'Alert content', - }); - - expect(root).toEqualHtml(` - - -
    -
    - - - - - - - - Interruption - -

    - -

    - - - Show much more - -
    - - - - -
    -
    -
    - Alert content -
    - `); - }); - - it('should hide close button in readonly mode', async () => { - const { root } = await newSpecPage({ - components: [SbbAlert], - html: 'Alert content', - }); - - expect(root.shadowRoot.querySelector('.sbb-alert__close-button-wrapper')).toBeNull(); - }); -}); diff --git a/src/components/sbb-alert/sbb-alert.tsx b/src/components/sbb-alert/sbb-alert.tsx deleted file mode 100644 index c201a9f5fd..0000000000 --- a/src/components/sbb-alert/sbb-alert.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - Component, - Element, - Event, - EventEmitter, - h, - JSX, - Method, - Prop, - ComponentInterface, - Fragment, - State, -} from '@stencil/core'; -import { InterfaceAlertAttributes } from './sbb-alert.custom'; -import { i18nCloseAlert, i18nFindOutMore } from '../../global/i18n'; -import { LinkProperties, LinkTargetType } from '../../global/interfaces'; -import { InterfaceTitleAttributes } from '../sbb-title/sbb-title.custom'; -import { - documentLanguage, - HandlerRepository, - languageChangeHandlerAspect, -} from '../../global/eventing'; - -/** - * @slot icon - Should be a sbb-icon which is displayed next to the title. Styling is optimized for icons of type HIM-CUS. - * @slot title - Title content. - * @slot unnamed - Content of the alert. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-alert.scss', - tag: 'sbb-alert', -}) -export class SbbAlert implements ComponentInterface, LinkProperties { - /** - * Whether the alert is readonly. - * In readonly mode, there is no dismiss button offered to the user. - */ - @Prop({ reflect: true }) public readonly = false; - - /** You can choose between `m` or `l` size. */ - @Prop({ reflect: true }) public size: InterfaceAlertAttributes['size'] = 'm'; - - /** Whether the fade in animation should be disabled. */ - @Prop() public disableAnimation = false; - - /** - * Name of the icon which will be forward to the nested `sbb-icon`. - * Choose the icons from https://icons.app.sbb.ch. - * Styling is optimized for icons of type HIM-CUS. - */ - @Prop() public iconName?: string; - - /** Content of title. */ - @Prop() public titleContent?: string; - - /** Level of title, will be rendered as heading tag (e.g. h3). Defaults to level 3. */ - @Prop() public titleLevel: InterfaceTitleAttributes['level'] = '3'; - - /** Content of the link. */ - @Prop() public linkContent?: string; - - /** The href value you want to link to. */ - @Prop() public href: string | undefined; - - /** Where to display the linked URL. */ - @Prop() public target: LinkTargetType | string | undefined; - - /** The relationship of the linked URL as space-separated link types. */ - @Prop() public rel: string | undefined; - - /** This will be forwarded as aria-label to the relevant nested element. */ - @Prop() public accessibilityLabel: string | undefined; - - /** Emits when the fade in animation starts. */ - @Event({ - eventName: 'will-present', - }) - public willPresent: EventEmitter; - - /** Emits when the fade in animation ends and the button is displayed. */ - @Event({ - eventName: 'did-present', - }) - public didPresent: EventEmitter; - - /** Emits when dismissal of an alert was requested. */ - @Event({ - eventName: 'dismissal-requested', - }) - public dismissalRequested: EventEmitter; - - @Element() private _element!: HTMLElement; - - @State() private _currentLanguage = documentLanguage(); - private _handlerRepository = new HandlerRepository( - this._element, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - ); - - private _transitionWrapperElement!: HTMLElement; - private _alertElement!: HTMLElement; - - private _firstRenderingDone = false; - - public connectedCallback(): void { - this._handlerRepository.connect(); - // Skip very first render where the animation elements are not yet ready. - // Presentation is postponed to componentDidRender(). - if (this._transitionWrapperElement) { - this._initFadeInTransitionStyles(); - this._present(); - } - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - } - - public componentDidRender(): void { - // During the very first rendering, the animation elements are only present in componentDidRender. - // So we need to fire the fade in animation later than at connectedCallback(). - if (!this._firstRenderingDone) { - this._present(); - } - this._firstRenderingDone = true; - } - - /** Requests dismissal of the alert. */ - @Method() public async requestDismissal(): Promise { - this.dismissalRequested.emit(); - } - - /** Present the alert. */ - private _present(): Promise { - this.willPresent.emit(); - - if (this.disableAnimation) { - this._onHeightTransitionEnd(); - return; - } - - this._transitionWrapperElement.addEventListener( - 'transitionend', - () => this._onHeightTransitionEnd(), - { - once: true, - }, - ); - this._transitionWrapperElement.style.height = `${this._alertElement.offsetHeight}px`; - } - - private _initFadeInTransitionStyles(): void { - if (this.disableAnimation) { - return; - } - this._transitionWrapperElement.style.height = '0'; - this._alertElement.style.opacity = '0'; - } - - private _onHeightTransitionEnd(): void { - this._transitionWrapperElement.style.removeProperty('height'); - this._alertElement.style.removeProperty('opacity'); - - if (this.disableAnimation) { - this._onOpacityTransitionEnd(); - return; - } - - this._alertElement.addEventListener('transitionend', () => this._onOpacityTransitionEnd(), { - once: true, - }); - } - - private _onOpacityTransitionEnd(): void { - this.didPresent.emit(); - } - - private _linkProperties(): Record { - return { - ['aria-label']: this.accessibilityLabel, - href: this.href, - rel: this.rel, - target: this.target, - }; - } - - public render(): JSX.Element { - return ( -
    { - this._transitionWrapperElement = el; - }} - > -
    { - const isFirstInitialization = !this._alertElement; - - this._alertElement = el; - if (isFirstInitialization) { - this._initFadeInTransitionStyles(); - } - }} - > - - {} - - - - {this.titleContent} - -

    - -

    - {this.href && ( - - - - {this.linkContent ? this.linkContent : i18nFindOutMore[this._currentLanguage]} - - - )} -
    - {!this.readonly && ( - - - this.requestDismissal()} - aria-label={i18nCloseAlert[this._currentLanguage]} - class="sbb-alert__close-button" - /> - - )} -
    -
    - ); - } -} diff --git a/src/components/sbb-autocomplete/sbb-autocomplete.e2e.ts b/src/components/sbb-autocomplete/sbb-autocomplete.e2e.ts deleted file mode 100644 index 9ca3ec1eb3..0000000000 --- a/src/components/sbb-autocomplete/sbb-autocomplete.e2e.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import events from './sbb-autocomplete.events'; -import optionEvents from '../sbb-option/sbb-option.events'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-autocomplete', () => { - let element: E2EElement, formField: E2EElement, input: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - - 1 - 2 - 3 - - - - `); - - formField = await page.find('sbb-form-field'); - input = await page.find('input'); - element = await page.find('sbb-autocomplete'); - }); - - it('renders and sets the correct attributes', () => { - expect(formField).toHaveClass('hydrated'); - expect(element).toHaveClass('hydrated'); - - expect(element).not.toHaveAttribute('autocomplete-origin-borderless'); - - expect(input).toEqualAttribute('autocomplete', 'off'); - expect(input).toEqualAttribute('role', 'combobox'); - expect(input).toEqualAttribute('aria-autocomplete', 'list'); - expect(input).toEqualAttribute('aria-haspopup', 'listbox'); - expect(input).toEqualAttribute('aria-controls', 'myAutocomplete'); - expect(input).toEqualAttribute('aria-owns', 'myAutocomplete'); - expect(input).toEqualAttribute('aria-expanded', 'false'); - }); - - it('opens and closes with mouse and keyboard', async () => { - const willOpenEventSpy = await page.spyOnEvent(events.willOpen); - const didOpenEventSpy = await page.spyOnEvent(events.didOpen); - const willCloseEventSpy = await page.spyOnEvent(events.willClose); - const didCloseEventSpy = await page.spyOnEvent(events.didClose); - - await input.focus(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - expect(didOpenEventSpy).toHaveReceivedEventTimes(1); - expect(input.getAttribute('aria-expanded')).toEqual('true'); - - await element.press('Escape'); - await page.waitForChanges(); - await waitForCondition(() => willCloseEventSpy.events.length === 1); - expect(willCloseEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - await waitForCondition(() => didCloseEventSpy.events.length === 1); - expect(didCloseEventSpy).toHaveReceivedEventTimes(1); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await element.press('ArrowDown'); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 2); - expect(willOpenEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 2); - expect(didOpenEventSpy).toHaveReceivedEventTimes(2); - expect(input.getAttribute('aria-expanded')).toEqual('true'); - - await element.press('Tab'); - await page.waitForChanges(); - await waitForCondition(() => willCloseEventSpy.events.length === 2); - expect(willCloseEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - await waitForCondition(() => didCloseEventSpy.events.length === 2); - expect(didCloseEventSpy).toHaveReceivedEventTimes(2); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await input.click(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 3); - expect(willOpenEventSpy).toHaveReceivedEventTimes(3); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 3); - expect(didOpenEventSpy).toHaveReceivedEventTimes(3); - expect(input.getAttribute('aria-expanded')).toEqual('true'); - - const button = await page.find('button'); - await button.click(); - await waitForCondition(() => willCloseEventSpy.events.length === 3); - expect(willCloseEventSpy).toHaveReceivedEventTimes(3); - await page.waitForChanges(); - await waitForCondition(() => didCloseEventSpy.events.length === 3); - expect(didCloseEventSpy).toHaveReceivedEventTimes(3); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - }); - - it('select by mouse', async () => { - const willOpenEventSpy = await page.spyOnEvent(events.willOpen); - const didOpenEventSpy = await page.spyOnEvent(events.didOpen); - const optionSelectedEventSpy = await page.spyOnEvent(optionEvents.optionSelected); - - await input.focus(); - await page.waitForChanges(); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - expect(didOpenEventSpy).toHaveReceivedEventTimes(1); - - await element.press('ArrowDown'); - await element.press('ArrowDown'); - await page.waitForChanges(); - await element.press('Enter'); - await page.waitForChanges(); - - expect(optionSelectedEventSpy).toHaveReceivedEventTimes(1); - expect(optionSelectedEventSpy.firstEvent.target.id).toBe('option-2'); - }); - - it('opens and select with keyboard', async () => { - const didOpenEventSpy = await page.spyOnEvent(events.didOpen); - const didCloseEventSpy = await page.spyOnEvent(events.didClose); - const optionSelectedEventSpy = await page.spyOnEvent(optionEvents.optionSelected); - await input.focus(); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - expect(didOpenEventSpy).toHaveReceivedEventTimes(1); - - await element.press('ArrowDown'); - await page.waitForChanges(); - await element.press('ArrowDown'); - await page.waitForChanges(); - const optOne = await page.find('sbb-autocomplete > sbb-option#option-1'); - expect(await optOne.getProperty('active')).toEqual(false); - expect(await optOne.getProperty('selected')).toEqual(false); - const optTwo = await page.find('sbb-autocomplete > sbb-option#option-2'); - expect(await optTwo.getProperty('active')).toEqual(true); - expect(await optTwo.getProperty('selected')).toEqual(false); - expect(input.getAttribute('aria-activedescendant')).toEqual('option-2'); - - await element.press('Enter'); - await page.waitForChanges(); - await waitForCondition(() => didCloseEventSpy.events.length === 1); - expect(await optTwo.getProperty('active')).toEqual(false); - expect(await optTwo.getProperty('selected')).toEqual(true); - expect(didCloseEventSpy).toHaveReceivedEventTimes(1); - expect(optionSelectedEventSpy).toHaveReceivedEventTimes(1); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - expect(input).not.toHaveAttribute('aria-activedescendant'); - }); - - it('should stay closed when disabled', async () => { - await page.$eval('input', (e) => e.setAttribute('disabled', 'true')); - - await input.focus(); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await input.click(); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await element.press('ArrowDown'); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - }); - - it('should stay closed when readonly', async () => { - await page.$eval('input', (e) => e.setAttribute('readonly', 'true')); - - await input.focus(); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await input.click(); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - - await element.press('ArrowDown'); - await page.waitForChanges(); - expect(input.getAttribute('aria-expanded')).toEqual('false'); - }); -}); diff --git a/src/components/sbb-autocomplete/sbb-autocomplete.events.ts b/src/components/sbb-autocomplete/sbb-autocomplete.events.ts deleted file mode 100644 index cf7d67d8ae..0000000000 --- a/src/components/sbb-autocomplete/sbb-autocomplete.events.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - didClose: 'did-close', - didOpen: 'did-open', - willClose: 'will-close', - willOpen: 'will-open', -}; diff --git a/src/components/sbb-autocomplete/sbb-autocomplete.spec.ts b/src/components/sbb-autocomplete/sbb-autocomplete.spec.ts deleted file mode 100644 index 5aa731ec78..0000000000 --- a/src/components/sbb-autocomplete/sbb-autocomplete.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { SbbAutocomplete } from './sbb-autocomplete'; -import { newSpecPage } from '@stencil/core/testing'; -import { SbbFormField } from '../sbb-form-field/sbb-form-field'; - -describe('sbb-autocomplete', () => { - it('renders standalone', async () => { - const { root } = await newSpecPage({ - components: [SbbAutocomplete], - html: ` -
    - - - 1 - 2 - - `, - }); - - expect(root).toEqualHtml(` - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - 1 - 2 -
    - `); - }); - - it('renders in form field', async () => { - const { root } = await newSpecPage({ - components: [SbbAutocomplete, SbbFormField], - html: ` - - - - 1 - 2 - - - `, - }); - - expect(root).toEqualHtml(` - - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - 1 - 2 -
    -
    - `); - }); -}); diff --git a/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.e2e.ts b/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.e2e.ts deleted file mode 100644 index 9321f1888d..0000000000 --- a/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.e2e.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-breadcrumb-group', () => { - describe('without ellipsis', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - One - Two - - `); - - element = await page.find('sbb-breadcrumb-group'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('keyboard navigation', async () => { - const first = await page.find('sbb-breadcrumb-group > sbb-breadcrumb#breadcrumb-0'); - const second = await page.find('sbb-breadcrumb-group > sbb-breadcrumb#breadcrumb-1'); - const third = await page.find('sbb-breadcrumb-group > sbb-breadcrumb#breadcrumb-2'); - - await first.focus(); - await page.keyboard.down('ArrowRight'); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(second.id); - await page.keyboard.down('ArrowRight'); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(third.id); - }); - }); - - describe('with ellipsis', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setViewport({ width: 320, height: 600 }); - await page.setContent(` - - - First - Second - Third - Fourth - Fifth - Sixth - - `); - - element = await page.find('sbb-breadcrumb-group'); - await page.waitForChanges(); - }); - - it('renders', async () => { - const ellipsisBreadcrumb = ` -
  • - - -
  • `; - - const li = await page.findAll('sbb-breadcrumb-group >>> li'); - const slots = await page.findAll('sbb-breadcrumb-group >>> li > slot'); - expect(li).not.toBeNull(); - expect(li.length).toEqual(3); - expect(li[1]).toEqualHtml(ellipsisBreadcrumb); - expect(slots.length).toEqual(2); - expect(slots[0]).toEqualAttribute('name', 'breadcrumb-0'); - expect(slots[1]).toEqualAttribute('name', 'breadcrumb-6'); - }); - - it('keyboard navigation with ellipsis', async () => { - const ellipsisElement = await page.find( - 'sbb-breadcrumb-group >>> #sbb-breadcrumb-group-ellipsis', - ); - const ellipsisBreadcrumb = await page.find( - 'sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis', - ); - const first = await page.find('sbb-breadcrumb-group > sbb-breadcrumb#breadcrumb-0'); - const last = await page.find('sbb-breadcrumb-group > sbb-breadcrumb#breadcrumb-6'); - - expect(ellipsisElement).not.toBeNull(); - expect(ellipsisBreadcrumb).not.toBeNull(); - - await first.focus(); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(first.id); - - await page.keyboard.down('ArrowRight'); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(element.id); - expect( - await page.evaluate( - () => document.getElementById('sbb-breadcrumb-group').shadowRoot.activeElement.id, - ), - ).toEqual(ellipsisBreadcrumb.id); - - await page.keyboard.down('ArrowRight'); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(last.id); - - await page.keyboard.down('ArrowRight'); - expect(await page.evaluate(() => document.activeElement.id)).toEqual(first.id); - }); - - it('expand breadcrumbs with ellipsis', async () => { - let ellipsisElement = await page.find( - 'sbb-breadcrumb-group >>> #sbb-breadcrumb-group-ellipsis', - ); - let ellipsisBreadcrumb = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis'); - expect(ellipsisElement).not.toBeNull(); - expect(ellipsisBreadcrumb).not.toBeNull(); - - const changeSpy = await ellipsisBreadcrumb.spyOnEvent('click'); - await ellipsisBreadcrumb.click(); - await waitForCondition(() => changeSpy.events.length === 1); - - ellipsisElement = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-group-ellipsis'); - ellipsisBreadcrumb = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis'); - expect(ellipsisElement).toBeNull(); - expect(ellipsisBreadcrumb).toBeNull(); - }); - - it('should expand breadcrumbs and focus correctly by keyboard', async () => { - // When pressing space key on ellipsis - const ellipsisBreadcrumb = await page.find( - 'sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis', - ); - await ellipsisBreadcrumb.press('Space'); - await page.waitForChanges(); - - // Then focus should be on first breadcrumb - expect(await page.evaluate(() => document.activeElement.id)).toEqual('breadcrumb-1'); - - // When blurring the focus - await page.evaluate(() => (document.activeElement as HTMLElement).blur()); - - // Then body should be focused - expect(await page.evaluate(() => document.activeElement.tagName)).toEqual('BODY'); - - // When triggering a slotChange by removing a breadcrumb - await page.evaluate(() => document.getElementById('breadcrumb-6').remove()); - await page.waitForChanges(); - - // Then still the body should be focused - expect(await page.evaluate(() => document.activeElement.tagName)).toEqual('BODY'); - }); - - it('should remove expand button when too less breadcrumbs available', async () => { - let ellipsisElement = await page.find( - 'sbb-breadcrumb-group >>> #sbb-breadcrumb-group-ellipsis', - ); - let ellipsisBreadcrumb = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis'); - expect(ellipsisElement).not.toBeNull(); - expect(ellipsisBreadcrumb).not.toBeNull(); - - // Remove every breadcrumb from DOM except the first two - await page.evaluate(() => - Array.from(document.querySelectorAll('sbb-breadcrumb')) - .slice(2) - .forEach((el) => el.remove()), - ); - - await page.waitForChanges(); - - ellipsisElement = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-group-ellipsis'); - ellipsisBreadcrumb = await page.find('sbb-breadcrumb-group >>> #sbb-breadcrumb-ellipsis'); - expect(ellipsisElement).toBeNull(); - expect(ellipsisBreadcrumb).toBeNull(); - }); - }); -}); diff --git a/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.spec.ts b/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.spec.ts deleted file mode 100644 index 7ff5b60104..0000000000 --- a/src/components/sbb-breadcrumb-group/sbb-breadcrumb-group.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { SbbBreadcrumbGroup } from './sbb-breadcrumb-group'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-breadcrumb-group', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbBreadcrumbGroup], - html: ` - - - One - Two - - `, - }); - - expect(root).toEqualHtml(` - - -
      -
    1. - - -
    2. -
    3. - - -
    4. -
    5. - -
    6. -
    - -
    - - - - One - - - Two - -
    - `); - }); -}); diff --git a/src/components/sbb-breadcrumb/readme.md b/src/components/sbb-breadcrumb/readme.md deleted file mode 100644 index a7d42dfeee..0000000000 --- a/src/components/sbb-breadcrumb/readme.md +++ /dev/null @@ -1,75 +0,0 @@ -The `sbb-breadcrumb` is a component used to display a link to a page. - -When it's used within the [sbb-breadcrumb-group](/docs/components-sbb-breadcrumb-sbb-breadcrumb-group--docs) component, -it can display the list of the links the user visited to arrive at the current page. - -## Slots - -It is possible to provide a text via an unnamed slot; the component can optionally display a `sbb-icon` -at the component start using the `iconName` property or via custom content using the `icon` slot. -Text and icon are not exclusive and can be used together. - -```html -Contact us - - - - - Info - - -``` - -## Link properties - -It's possible to set all the link related properties (`download`, `href`, `rel` and `target`). - -```html -Info -``` - -## Accessibility - -The `aria-current` property should be used to make the breadcrumb read correctly by screen-readers when the component -is used in the `sbb-breadcrumb-group`. - -By default, the `sbb-breadcrumb-group` component sets `aria-current="page"` on the last slotted `sbb-breadcrumb`. - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `download` | `download` | Whether the browser will show the download dialog on click. | `boolean` | `undefined` | -| `href` | `href` | The href value you want to link to. | `string` | `undefined` | -| `iconName` | `icon-name` | 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. | `string` | `undefined` | -| `rel` | `rel` | The relationship of the linked URL as space-separated link types. | `string` | `undefined` | -| `target` | `target` | Where to display the linked URL. | `string` | `undefined` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------ | -| `"icon"` | Use this to display an icon as breadcrumb. | -| `"unnamed"` | Use this to slot the breadcrumb's text. | - - -## Dependencies - -### Depends on - -- [sbb-icon](../sbb-icon) - -### Graph -```mermaid -graph TD; - sbb-breadcrumb --> sbb-icon - style sbb-breadcrumb fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-breadcrumb/sbb-breadcrumb.e2e.ts b/src/components/sbb-breadcrumb/sbb-breadcrumb.e2e.ts deleted file mode 100644 index e65b1e5c88..0000000000 --- a/src/components/sbb-breadcrumb/sbb-breadcrumb.e2e.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-breadcrumb', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent('Test'); - - element = await page.find('sbb-breadcrumb'); - }); - - it('renders', async () => { - await page.waitForChanges(); - expect(element).toHaveClass('hydrated'); - }); - - it('dispatches event on click', async () => { - await page.waitForChanges(); - const changeSpy = await page.spyOnEvent('click'); - - await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('should receive focus', async () => { - await element.focus(); - await page.waitForChanges(); - - expect(await page.evaluate(() => document.activeElement.id)).toBe('focus-id'); - }); -}); diff --git a/src/components/sbb-breadcrumb/sbb-breadcrumb.spec.ts b/src/components/sbb-breadcrumb/sbb-breadcrumb.spec.ts deleted file mode 100644 index 9d34f7ebbd..0000000000 --- a/src/components/sbb-breadcrumb/sbb-breadcrumb.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { SbbBreadcrumb } from './sbb-breadcrumb'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-breadcrumb', () => { - it('renders with text', async () => { - const { root } = await newSpecPage({ - components: [SbbBreadcrumb], - html: 'Breadcrumb', - }); - - expect(root).toEqualHtml(` - - - - - - - . Link target opens in new window. - - - - - Breadcrumb - - `); - }); - - it('renders with icon', async () => { - const { root } = await newSpecPage({ - components: [SbbBreadcrumb], - html: ` - - `, - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - - `); - }); - - it('renders with icon and text', async () => { - const { root } = await newSpecPage({ - components: [SbbBreadcrumb], - html: ` - Home - `, - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - - - Home - - `); - }); - - it('renders as span if no href is provided', async () => { - const { root } = await newSpecPage({ - components: [SbbBreadcrumb], - html: 'Breadcrumb', - }); - - expect(root).toEqualHtml(` - - - - - - - - - Breadcrumb - - `); - }); -}); diff --git a/src/components/sbb-breadcrumb/sbb-breadcrumb.tsx b/src/components/sbb-breadcrumb/sbb-breadcrumb.tsx deleted file mode 100644 index 3667e880d9..0000000000 --- a/src/components/sbb-breadcrumb/sbb-breadcrumb.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Component, ComponentInterface, Element, h, Host, JSX, Prop, State } from '@stencil/core'; -import { - LinkProperties, - LinkTargetType, - resolveLinkOrStaticRenderVariables, - targetsNewWindow, -} from '../../global/interfaces'; -import { i18nTargetOpensInNewWindow } from '../../global/i18n'; -import { - createNamedSlotState, - documentLanguage, - HandlerRepository, - actionElementHandlerAspect, - languageChangeHandlerAspect, - namedSlotChangeHandlerAspect, -} from '../../global/eventing'; - -/** - * @slot unnamed - Use this to slot the breadcrumb's text. - * @slot icon - Use this to display an icon as breadcrumb. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-breadcrumb.scss', - tag: 'sbb-breadcrumb', -}) -export class SbbBreadcrumb implements ComponentInterface, LinkProperties { - /** The href value you want to link to. */ - @Prop() public href: string | undefined; - - /** Where to display the linked URL. */ - @Prop() public target?: LinkTargetType | string | undefined; - - /** The relationship of the linked URL as space-separated link types. */ - @Prop() public rel?: string | undefined; - - /** Whether the browser will show the download dialog on click. */ - @Prop() public download?: boolean; - - /** - * 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. - */ - @Prop() public iconName?: string; - - /** State of listed named slots, by indicating whether any element for a named slot is defined. */ - @State() private _namedSlots = createNamedSlotState('icon'); - - @State() private _currentLanguage = documentLanguage(); - - @State() private _hasText = false; - - @Element() private _element!: HTMLElement; - - private _handlerRepository = new HandlerRepository( - this._element, - actionElementHandlerAspect, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), - ); - - public connectedCallback(): void { - this._hasText = Array.from(this._element.childNodes).some( - (n) => !(n as Element).slot && n.textContent?.trim(), - ); - this._handlerRepository.connect(); - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - } - - private _onLabelSlotChange(event: Event): void { - this._hasText = (event.target as HTMLSlotElement) - .assignedNodes() - .some((n) => !!n.textContent?.trim()); - } - - public render(): JSX.Element { - const { - tagName: TAG_NAME, - attributes, - hostAttributes, - } = resolveLinkOrStaticRenderVariables(this); - - return ( - - - {(this.iconName || this._namedSlots.icon) && ( - - {this.iconName && } - - )} - {this._hasText && ( - - this._onLabelSlotChange(event)} /> - {targetsNewWindow(this) && ( - - . {i18nTargetOpensInNewWindow[this._currentLanguage]} - - )} - - )} - {!this._hasText && ( - - )} - - - ); - } -} diff --git a/src/components/sbb-button/readme.md b/src/components/sbb-button/readme.md deleted file mode 100644 index c9067c85ac..0000000000 --- a/src/components/sbb-button/readme.md +++ /dev/null @@ -1,150 +0,0 @@ -The `sbb-button` component provides the same functionality as a native ` - `); - - const yearCells: E2EElement[] = await page.findAll( - 'sbb-calendar >>> .sbb-calendar__table-year', - ); - expect(yearCells.length).toEqual(24); - expect(yearCells[0]).toEqualHtml(` - - - - `); - - const selectedYear: E2EElement = await page.find({ text: '2023' }); - expect(selectedYear).toHaveClass('sbb-calendar__selected'); - expect(yearCells[yearCells.length - 1].textContent).toEqual('2039'); - await selectedYear.click(); - await page.waitForChanges(); - - const monthSelection: E2EElement = await page.find( - 'sbb-calendar >>> #sbb-calendar__month-selection', - ); - expect(monthSelection).not.toBeNull(); - expect(monthSelection).toEqualHtml(` - - `); - - const monthCells: E2EElement[] = await page.findAll( - 'sbb-calendar >>> .sbb-calendar__table-month', - ); - expect(monthCells.length).toEqual(12); - expect(monthCells[0]).toEqualHtml(` - - - - `); - await monthCells[0].click(); - await page.waitForChanges(); - - const dayCells: E2EElement[] = await page.findAll('sbb-calendar >>> .sbb-calendar__day'); - expect(dayCells.length).toEqual(31); - }); - - describe('navigation', () => { - it('navigates left via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('ArrowLeft'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('14 1 2023'); - }); - - it('navigates right via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('ArrowRight'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('16 1 2023'); - }); - - it('navigates up via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('ArrowUp'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('8 1 2023'); - }); - - it('navigates down via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('ArrowDown'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('22 1 2023'); - }); - - it('navigates to first day via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('Home'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('1 1 2023'); - }); - - it('navigates to last day via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('End'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('31 1 2023'); - }); - - it('navigates to column start via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('PageUp'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('1 1 2023'); - }); - - it('navigates to column end via keyboard', async () => { - await element.focus(); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('15 1 2023'); - - await element.press('PageDown'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.getAttribute('data-day'); - }), - ).toEqual('29 1 2023'); - }); - }); - - describe('navigation for year view', () => { - beforeEach(async () => { - const yearSelectionButton: E2EElement = await page.find( - 'sbb-calendar >>> #sbb-calendar__date-selection', - ); - const table: E2EElement = await page.find('sbb-calendar >>> table'); - const animationSpy = await table.spyOnEvent('animationend'); - - await yearSelectionButton.click(); - await waitForCondition(() => animationSpy.events.length >= 2); - const selectedYear: E2EElement = await page.find({ text: '2023' }); - await selectedYear.focus(); - }); - - it('navigates left via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('ArrowLeft'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2022'); - }); - - it('navigates right via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('ArrowRight'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2024'); - }); - - it('navigates up via keyboard', async () => { - await page.waitForChanges(); - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('ArrowUp'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2019'); - }); - - it('navigates down via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('ArrowDown'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2027'); - }); - - it('navigates to first day via keyboard', async () => { - await page.waitForChanges(); - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('Home'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2016'); - }); - - it('navigates to last day via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('End'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2039'); - }); - - it('navigates to column start via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('PageUp'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2019'); - }); - - it('navigates to column end via keyboard', async () => { - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2023'); - - await element.press('PageDown'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => { - return document.activeElement.shadowRoot.activeElement.textContent; - }), - ).toEqual('2039'); - }); - }); -}); diff --git a/src/components/sbb-calendar/sbb-calendar.events.ts b/src/components/sbb-calendar/sbb-calendar.events.ts deleted file mode 100644 index 78d2fba3d6..0000000000 --- a/src/components/sbb-calendar/sbb-calendar.events.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - dateSelected: 'date-selected', -}; diff --git a/src/components/sbb-card-action/readme.md b/src/components/sbb-card-action/readme.md deleted file mode 100644 index 0134f442fd..0000000000 --- a/src/components/sbb-card-action/readme.md +++ /dev/null @@ -1,64 +0,0 @@ -The `sbb-card-action` is the component used to turn a `sbb-card` into an action. - -```html -Check all the wonderful trips available. -``` - -## Link / button properties - -As the [sbb-link](/docs/components-sbb-link--docs) and the [sbb-button](/docs/components-sbb-button--docs), -the component can be internally rendered as a button or as a link, -depending on the value of the `href` property, so the associated properties are available -(`href`, `target`, `rel` and `download` for link; `type`, `name`, `value` and `form` for button). - -## Accessibility - -It's **important** that a descriptive message is being slotted into the unnamed slot of `sbb-card-action` -as it is used for search engines and screen-reader users. - -```html -Buy a half-fare ticket now -``` - - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------- | ---------- | ------------------------------------------------------------------------------- | --------------------------------- | ----------- | -| `active` | `active` | Whether the card is active. | `boolean` | `false` | -| `download` | `download` | Whether the browser will show the download dialog on click. | `boolean` | `undefined` | -| `form` | `form` | The element to associate the button to it. | `string` | `undefined` | -| `href` | `href` | The href value you want to link to. | `string` | `undefined` | -| `name` | `name` | The name of the button. | `string` | `undefined` | -| `rel` | `rel` | The relationship of the linked URL as space-separated link types. | `string` | `undefined` | -| `target` | `target` | Where to display the linked URL. | `string` | `undefined` | -| `type` | `type` | Default behaviour of the button. | `"button" \| "reset" \| "submit"` | `undefined` | -| `value` | `value` | The value associated with button `name` when it's submitted with the form data. | `string` | `undefined` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------------------------------------------------------------------------------- | -| `"unnamed"` | Slot to render a descriptive label / title of the action (important!). This is relevant for SEO and screen readers. | - - -## Dependencies - -### Used by - - - [sbb-timetable-row](../sbb-timetable-row) - -### Graph -```mermaid -graph TD; - sbb-timetable-row --> sbb-card-action - style sbb-card-action fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-card-action/sbb-card-action.e2e.ts b/src/components/sbb-card-action/sbb-card-action.e2e.ts deleted file mode 100644 index e42cf94ae2..0000000000 --- a/src/components/sbb-card-action/sbb-card-action.e2e.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -// As tests don't work in specs at all (missing :is support in Jest), we moved all tests to e2e. - -describe('sbb-card-action', () => { - let element: E2EElement, page: E2EPage; - - it('should render an sbb-card-action as a link opening in a new window', async () => { - page = await newE2EPage(); - await page.setContent( - ` - - Follow me - Content text - `, - ); - const card = await page.find('sbb-card'); - - await page.waitForChanges(); - - expect(card).toHaveAttribute('data-has-action'); - expect(card).not.toHaveAttribute('data-has-active-action'); - expect(card).toEqualAttribute('data-action-role', 'link'); - expect(await card.find('sbb-card-action')).toEqualHtml(` - - - - - - . Link target opens in new window. - - - - Follow me - - `); - }); - - it('should render an sbb-card-action as a button which is active', async () => { - page = await newE2EPage(); - await page.setContent( - `Click meContent`, - ); - const card = await page.find('sbb-card'); - await page.waitForChanges(); - - expect(card).toHaveAttribute('data-has-action'); - expect(card).toHaveAttribute('data-has-active-action'); - expect(card).toEqualAttribute('data-action-role', 'button'); - expect(await card.find('sbb-card-action')).toEqualHtml(` - - - - - - - - - Click me - - `); - }); - - it('should correctly toggle active state', async () => { - page = await newE2EPage(); - await page.setContent( - `Click meContent`, - ); - const card = await page.find('sbb-card'); - await page.waitForChanges(); - - expect(card).not.toHaveAttribute('data-has-active-action'); - - (await card.find('sbb-card-action')).setAttribute('active', ''); - await page.waitForChanges(); - - expect(card).toHaveAttribute('data-has-active-action'); - }); - - it('should remove data properties from host', async () => { - page = await newE2EPage(); - await page.setContent( - `Click me`, - ); - const card = await page.find('sbb-card'); - - await page.waitForChanges(); - - expect(card).toHaveAttribute('data-has-action'); - expect(card).toHaveAttribute('data-has-active-action'); - expect(card).toEqualAttribute('data-action-role', 'button'); - - // Remove action from DOM - await page.evaluate(() => document.querySelector('sbb-card-action').remove()); - await page.waitForChanges(); - - expect(card).not.toHaveAttribute('data-has-action'); - expect(card).not.toHaveAttribute('data-has-active-action'); - expect(card).not.toEqualAttribute('data-action-role', 'button'); - }); - - it('should detect added button in slotted content to update focusable elements', async () => { - page = await newE2EPage(); - await page.setContent( - `Click me`, - ); - await page.waitForChanges(); - expect(await page.find('button')).toHaveAttribute('data-card-focusable'); - - // Add a second button in content - await page.evaluate(() => - document - .getElementById('content') - .insertBefore(document.createElement('button'), document.querySelector('button')), - ); - - // Both buttons should be marked as focusable - const buttons = await page.findAll('button'); - expect(buttons.length).toBe(2); - expect(buttons.every((el) => el.getAttribute('data-card-focusable') !== null)).toBe(true); - - // Remove all buttons - await page.evaluate(() => document.querySelectorAll('button').forEach((el) => el.remove())); - await page.waitForChanges(); - - // Card should not have marker anymore - expect((await page.findAll('button')).length).toBe(0); - }); - - it('should detect added second element of slot to update focusable elements', async () => { - page = await newE2EPage(); - await page.setContent( - `Click me`, - ); - await page.waitForChanges(); - - // Add a button to slot - await page.evaluate(() => - document - .querySelector('sbb-card') - .insertBefore(document.createElement('button'), document.getElementById('content')), - ); - await page.waitForChanges(); - - // Button should be marked as focusable - expect(await page.find('button')).toHaveAttribute('data-card-focusable'); - }); - - it('should detect focusable elements when action was added at later point', async () => { - page = await newE2EPage(); - await page.setContent(``); - await page.waitForChanges(); - - // Add a sbb-card-action - await page.evaluate(() => - document.querySelector('sbb-card').appendChild(document.createElement('sbb-card-action')), - ); - await page.waitForChanges(); - - // Button should be marked as focusable - expect(await page.find('button')).toHaveAttribute('data-card-focusable'); - }); - - describe('events', () => { - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent( - 'CardContent', - ); - - element = await page.find('sbb-card-action'); - }); - - it('dispatches event on click', async () => { - await page.waitForChanges(); - const changeSpy = await page.spyOnEvent('click'); - - await element.click(); - - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('should dispatch click event on pressing Enter', async () => { - const changeSpy = await page.spyOnEvent('click'); - await element.press('Enter'); - expect(changeSpy).toHaveReceivedEvent(); - }); - - it('should dispatch click event on pressing Space', async () => { - const changeSpy = await page.spyOnEvent('click'); - await element.press(' '); - expect(changeSpy).toHaveReceivedEvent(); - }); - - it('should dispatch click event on pressing Enter with href', async () => { - element.setAttribute('href', 'test'); - await page.waitForChanges(); - - const changeSpy = await page.spyOnEvent('click'); - await element.press('Enter'); - expect(changeSpy).toHaveReceivedEvent(); - }); - - it('should not dispatch click event on pressing Space with href', async () => { - element.setAttribute('href', 'test'); - await page.waitForChanges(); - - const changeSpy = await page.spyOnEvent('click'); - await element.press(' '); - expect(changeSpy).not.toHaveReceivedEvent(); - }); - - it('should receive focus', async () => { - await element.focus(); - await page.waitForChanges(); - - expect(await page.evaluate(() => document.activeElement.id)).toBe('focus-id'); - }); - }); -}); diff --git a/src/components/sbb-card-action/sbb-card-action.tsx b/src/components/sbb-card-action/sbb-card-action.tsx deleted file mode 100644 index 294257564f..0000000000 --- a/src/components/sbb-card-action/sbb-card-action.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - Fragment, - h, - Host, - JSX, - Prop, - State, - Watch, -} from '@stencil/core'; -import { i18nTargetOpensInNewWindow } from '../../global/i18n'; -import { - ButtonType, - LinkButtonProperties, - LinkButtonRenderVariables, - LinkTargetType, - resolveRenderVariables, - targetsNewWindow, -} from '../../global/interfaces'; -import { IS_FOCUSABLE_QUERY } from '../../global/a11y'; -import { toggleDatasetEntry } from '../../global/dom'; -import { - documentLanguage, - HandlerRepository, - actionElementHandlerAspect, - languageChangeHandlerAspect, -} from '../../global/eventing'; -import { AgnosticMutationObserver } from '../../global/observers'; - -/** - * @slot unnamed - Slot to render a descriptive label / title of the action (important!). This is relevant for SEO and screen readers. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-card-action.scss', - tag: 'sbb-card-action', -}) -export class SbbCardAction implements ComponentInterface, LinkButtonProperties { - /** Whether the card is active. */ - @Prop({ reflect: true }) public active = false; - - /** The href value you want to link to. */ - @Prop({ reflect: true }) public href: string | undefined; - - /** Where to display the linked URL. */ - @Prop() public target?: LinkTargetType | string | undefined; - - /** The relationship of the linked URL as space-separated link types. */ - @Prop() public rel?: string | undefined; - - /** Whether the browser will show the download dialog on click. */ - @Prop() public download?: boolean | undefined; - - /** Default behaviour of the button. */ - @Prop() public type: ButtonType | undefined; - - /** The name of the button. */ - @Prop({ reflect: true }) public name: string | undefined; - - /** The element to associate the button to it. */ - @Prop() public form?: string | undefined; - - /** The value associated with button `name` when it's submitted with the form data. */ - @Prop() public value?: string | undefined; - - @State() private _currentLanguage = documentLanguage(); - - @Watch('active') - public onActiveChange(): void { - if (this._card) { - toggleDatasetEntry(this._card, 'hasActiveAction', this.active); - } - } - - @Element() private _element!: HTMLElement; - - private _abortController = new AbortController(); - private _card: HTMLSbbCardElement | null = null; - private _cardMutationObserver = new AgnosticMutationObserver(() => - this._checkForSlottedActions(), - ); - - private _handlerRepository = new HandlerRepository( - this._element, - actionElementHandlerAspect, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - ); - - public constructor() { - // Set slot name as early as possible - this._element.setAttribute('slot', 'action'); - } - - public connectedCallback(): void { - this._abortController = new AbortController(); - - this._card = this._element.closest('sbb-card'); - if (this._card) { - toggleDatasetEntry(this._card, 'hasAction', true); - toggleDatasetEntry(this._card, 'hasActiveAction', this.active); - - this._checkForSlottedActions(); - this._cardMutationObserver.observe(this._card, { - childList: true, - subtree: true, - }); - } - - this._handlerRepository.connect(); - } - - public disconnectedCallback(): void { - 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._handlerRepository.disconnect(); - this._cardMutationObserver.disconnect(); - this._abortController.abort(); - } - - private _checkForSlottedActions(): void { - const cardFocusableAttributeName = 'data-card-focusable'; - - this._card - .querySelectorAll(`[${cardFocusableAttributeName}]:not(${IS_FOCUSABLE_QUERY})`) - .forEach((el) => el.removeAttribute(cardFocusableAttributeName)); - - this._card - .querySelectorAll( - `${IS_FOCUSABLE_QUERY}:not([${cardFocusableAttributeName}], sbb-card-action)`, - ) - .forEach((el) => el.setAttribute(cardFocusableAttributeName, '')); - } - - public render(): JSX.Element { - const { - tagName: TAG_NAME, - attributes, - hostAttributes, - }: LinkButtonRenderVariables = resolveRenderVariables(this); - - if (this._card) { - this._card.dataset.actionRole = hostAttributes.role; - } - - return ( - - - - - {targetsNewWindow(this) && ( - . {i18nTargetOpensInNewWindow[this._currentLanguage]} - )} - - - - ); - } -} diff --git a/src/components/sbb-card-badge/readme.md b/src/components/sbb-card-badge/readme.md deleted file mode 100644 index 6a00f50df6..0000000000 --- a/src/components/sbb-card-badge/readme.md +++ /dev/null @@ -1,56 +0,0 @@ -The `sbb-card-badge` can contain some information like prices or discounts, -and can be used in [sbb-card](/docs/components-sbb-card-sbb-card--docs) or -[sbb-selection-panel](/docs/components-sbb-selection-panel--docs). - -To achieve the correct spacing between elements inside the card badge, we recommend to use `span`-elements. -All content parts are presented with a predefined gap in between. - -```html - - - % - from CHF - 19.99 - - Card content... - -``` - -## Accessibility - -It's recommended to place an `aria-label` on `sbb-card-badge` to describe the displayed information in a full sentence, -as in the example above. - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| -------- | --------- | ------------------------ | ----------------------- | ------------ | -| `color` | `color` | Color of the card badge. | `"charcoal" \| "white"` | `'charcoal'` | - - -## Slots - -| Slot | Description | -| ----------- | --------------------------------------------------------------------------------------------------- | -| `"unnamed"` | Content of the badge. Content parts should be wrapped in `` tags to achieve correct spacings. | - - -## Dependencies - -### Used by - - - [sbb-timetable-row](../sbb-timetable-row) - -### Graph -```mermaid -graph TD; - sbb-timetable-row --> sbb-card-badge - style sbb-card-badge fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-card-badge/sbb-card-badge.custom.d.ts b/src/components/sbb-card-badge/sbb-card-badge.custom.d.ts deleted file mode 100644 index 10ff94f82c..0000000000 --- a/src/components/sbb-card-badge/sbb-card-badge.custom.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterfaceSbbCardBadgeAttributes { - color: 'charcoal' | 'white'; -} diff --git a/src/components/sbb-card-badge/sbb-card-badge.e2e.ts b/src/components/sbb-card-badge/sbb-card-badge.e2e.ts deleted file mode 100644 index cdfa37642f..0000000000 --- a/src/components/sbb-card-badge/sbb-card-badge.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-card-badge', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-card-badge'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-card-badge/sbb-card-badge.spec.ts b/src/components/sbb-card-badge/sbb-card-badge.spec.ts deleted file mode 100644 index baa9da1172..0000000000 --- a/src/components/sbb-card-badge/sbb-card-badge.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SbbCardBadge } from './sbb-card-badge'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-card-badge', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbCardBadge], - html: '', - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - - `); - }); -}); diff --git a/src/components/sbb-card-badge/sbb-card-badge.tsx b/src/components/sbb-card-badge/sbb-card-badge.tsx deleted file mode 100644 index aad05a7d42..0000000000 --- a/src/components/sbb-card-badge/sbb-card-badge.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, ComponentInterface, Element, h, Host, JSX, Prop } from '@stencil/core'; -import { InterfaceSbbCardBadgeAttributes } from './sbb-card-badge.custom'; -import { toggleDatasetEntry, getDocumentWritingMode } from '../../global/dom'; - -/** - * @slot unnamed - Content of the badge. - * Content parts should be wrapped in `` tags to achieve correct spacings. - */ - -@Component({ - shadow: true, - styleUrl: 'sbb-card-badge.scss', - tag: 'sbb-card-badge', -}) -export class SbbCardBadge implements ComponentInterface { - /** Color of the card badge. */ - @Prop({ reflect: true }) public color: InterfaceSbbCardBadgeAttributes['color'] = 'charcoal'; - - @Element() private _element!: HTMLElement; - - public constructor() { - // Set slot name as early as possible - this._element.setAttribute('slot', 'badge'); - } - - private _parentElement?: HTMLElement; - - public connectedCallback(): void { - this._parentElement = this._element.parentElement; - toggleDatasetEntry(this._parentElement, 'hasCardBadge', true); - } - - public disconnectedCallback(): void { - toggleDatasetEntry(this._parentElement, 'hasCardBadge', false); - this._parentElement = undefined; - } - - public render(): JSX.Element { - return ( - - - - - - - - - - - ); - } -} diff --git a/src/components/sbb-card/sbb-card.custom.d.ts b/src/components/sbb-card/sbb-card.custom.d.ts deleted file mode 100644 index b2ae51fe88..0000000000 --- a/src/components/sbb-card/sbb-card.custom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface InterfaceSbbCardAttributes { - size: 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl'; - color: 'white' | 'milk' | 'transparent-bordered' | 'transparent-bordered-dashed'; -} diff --git a/src/components/sbb-card/sbb-card.e2e.ts b/src/components/sbb-card/sbb-card.e2e.ts deleted file mode 100644 index 0941132dab..0000000000 --- a/src/components/sbb-card/sbb-card.e2e.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-card', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-card'); - expect(element).toHaveClass('hydrated'); - }); - - it('should render with sbb-card-badge', async () => { - page = await newE2EPage(); - await page.setContent( - ` - -

    Title

    - Content text - - % - from CHF - 19.99 - -
    -`, - ); - const card = await page.find('sbb-card'); - - expect( - await page.evaluate(() => - getComputedStyle( - document.querySelector('sbb-card').shadowRoot.querySelector('.sbb-card__badge-wrapper'), - ).getPropertyValue('display'), - ), - ).not.toBe('none'); - expect(card).toHaveAttribute('data-has-card-badge'); - expect(card).toEqualHtml(` - - - - - - - - - - - - -

    Title

    - Content text - - % - from CHF - 19.99 - -
    - `); - }); - - it('should render without sbb-card-badge', async () => { - page = await newE2EPage(); - await page.setContent( - ` - -

    Title

    - Content text -
    `, - ); - const card = await page.find('sbb-card'); - - expect( - await page.evaluate(() => - getComputedStyle( - document.querySelector('sbb-card').shadowRoot.querySelector('.sbb-card__badge-wrapper'), - ).getPropertyValue('display'), - ), - ).toBe('none'); - expect(card).not.toHaveAttribute('data-has-card-badge'); - }); -}); diff --git a/src/components/sbb-card/sbb-card.tsx b/src/components/sbb-card/sbb-card.tsx deleted file mode 100644 index 9d0290b427..0000000000 --- a/src/components/sbb-card/sbb-card.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, ComponentInterface, h, JSX, Prop } from '@stencil/core'; -import { InterfaceSbbCardAttributes } from './sbb-card.custom'; - -/** - * @slot unnamed - Slot to render the content. - * @slot badge - Slot to render ``. - * @slot action - Slot to render ``. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-card.scss', - tag: 'sbb-card', -}) -export class SbbCard implements ComponentInterface { - /** Size variant, either xs, s, m, l, xl, xxl or xxxl. */ - @Prop({ reflect: true }) public size?: InterfaceSbbCardAttributes['size'] = 'm'; - - /** Option to set the component's background color. */ - @Prop({ reflect: true }) public color: InterfaceSbbCardAttributes['color'] = 'white'; - - /** - * It is used internally to show the ``. - * - * @returns True whether size is equal to m, l, xl or xxl. - */ - private _isBadgeVisible(): boolean { - return ['m', 'l', 'xl', 'xxl', 'xxxl'].includes(this.size); - } - - public render(): JSX.Element { - return ( - - - - - - {this._isBadgeVisible() && ( - - - - )} - - ); - } -} diff --git a/src/components/sbb-checkbox-group/readme.md b/src/components/sbb-checkbox-group/readme.md deleted file mode 100644 index 5d9a18a083..0000000000 --- a/src/components/sbb-checkbox-group/readme.md +++ /dev/null @@ -1,104 +0,0 @@ -The `sbb-checkbox-group` component is used as a container for one or multiple -[sbb-checkbox](/docs/components-sbb-checkbox-sbb-checkbox--docs) components, -or, alternatively, for a collection of [sbb-selection-panel](/docs/components-sbb-selection-panel--docs). - -```html - - Label 1 - Label 2 - Label 3 - - - - - - Value - - - CHF - 40.00 - - - - -``` - -## Slots - -The content is projected in an unnamed slot. - -The component can display one or more [sbb-form-error](/docs/components-sbb-form-field-sbb-form-error--docs) components -right below the `sbb-checkbox-group` using the `error` slot. - -```html - - Label 1 - Label 2 - Label 3 - You must accept all the terms and conditions. - -``` - -## States - -It is possible to mark the entire group as disabled or required using the properties `disabled` and `required`. - -```html - - - ... - - - - - ... - -``` - -## Style - -The `orientation` property is used to set item orientation. -Possible values are `horizontal` (default) and `vertical`. -The optional property `horizontalFrom` can be used in combination with `orientation='vertical'` to -indicate the minimum breakpoint from which the orientation changes to `horizontal`. - -```html - - ... - -``` - -The component has a `size` property too, which can be used to change the size of all the inner `sbb-checkbox`. -Two values are available, `s` and `m`, which is the default - -```html - - ... - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------------- | ----------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------- | -| `disabled` | `disabled` | Whether the checkbox group is disabled. | `boolean` | `false` | -| `horizontalFrom` | `horizontal-from` | Overrides the behaviour of `orientation` property. | `"large" \| "medium" \| "micro" \| "small" \| "ultra" \| "wide" \| "zero"` | `undefined` | -| `orientation` | `orientation` | Indicates the orientation of the checkboxes inside the ``. | `"horizontal" \| "vertical"` | `'horizontal'` | -| `required` | `required` | Whether the checkbox group is required. | `boolean` | `false` | -| `size` | `size` | Size variant, either m or s. | `"m" \| "s"` | `'m'` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------------------------------------- | -| `"error"` | Slot used to render the inside the . | -| `"unnamed"` | Slot used to render the inside the . | - - ----------------------------------------------- - - diff --git a/src/components/sbb-checkbox-group/sbb-checkbox-group.custom.ts b/src/components/sbb-checkbox-group/sbb-checkbox-group.custom.ts deleted file mode 100644 index 88eec1ae14..0000000000 --- a/src/components/sbb-checkbox-group/sbb-checkbox-group.custom.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface InterfaceSbbCheckboxGroupAttributes { - horizontalFrom?: 'zero' | 'micro' | 'small' | 'medium' | 'large' | 'wide' | 'ultra'; - orientation: 'horizontal' | 'vertical'; - size: 'm' | 's'; -} diff --git a/src/components/sbb-checkbox-group/sbb-checkbox-group.e2e.ts b/src/components/sbb-checkbox-group/sbb-checkbox-group.e2e.ts deleted file mode 100644 index 9df593f011..0000000000 --- a/src/components/sbb-checkbox-group/sbb-checkbox-group.e2e.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-checkbox-group', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - Label 1 - Label 2 - Label 3 - - `); - element = await page.find('sbb-checkbox-group'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('disabled status is inherited', async () => { - element.setAttribute('disabled', 'true'); - await page.waitForChanges(); - expect(element).toEqualAttribute('disabled', 'true'); - const checkboxOne = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-1'); - expect(checkboxOne.getAttribute('data-group-disabled')).not.toBeNull(); - const checkboxTwo = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-2'); - expect(checkboxTwo.getAttribute('data-group-disabled')).not.toBeNull(); - expect(checkboxTwo.getAttribute('disabled')).not.toBeNull(); - const checkboxThree = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-3'); - expect(checkboxThree.getAttribute('data-group-disabled')).not.toBeNull(); - element.removeAttribute('disabled'); - await page.waitForChanges(); - expect(checkboxTwo.getAttribute('data-group-disabled')).toBeNull(); - expect(checkboxTwo.getAttribute('disabled')).not.toBeNull(); - }); - - it('disabled status prevents changes', async () => { - const checkboxOne = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-1'); - const checkboxTwo = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-2'); - const checkboxThree = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-3'); - const checkboxes = [checkboxOne, checkboxTwo, checkboxThree]; - checkboxes.forEach((check: E2EElement) => expect(check).toEqualAttribute('checked', null)); - element.setAttribute('disabled', 'true'); - await page.waitForChanges(); - expect(element).toEqualAttribute('disabled', 'true'); - for (const check of checkboxes) { - await check.click(); - expect(check).toEqualAttribute('checked', null); - } - element.removeAttribute('disabled'); - await page.waitForChanges(); - for (const check of checkboxes) { - await check.click(); - } - expect(checkboxOne).toEqualAttribute('checked', ''); - expect(checkboxTwo).toEqualAttribute('checked', null); - expect(checkboxThree).toEqualAttribute('checked', ''); - }); - - it('required status', async () => { - element.setAttribute('required', 'true'); - await page.waitForChanges(); - expect(element).toEqualAttribute('required', 'true'); - const checkboxOne = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-1'); - expect(checkboxOne.getAttribute('data-group-required')).not.toBeNull(); - const checkboxTwo = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-2'); - expect(checkboxTwo.getAttribute('data-group-required')).not.toBeNull(); - const checkboxThree = await page.find('sbb-checkbox-group > sbb-checkbox#checkbox-3'); - expect(checkboxThree.getAttribute('data-group-required')).not.toBeNull(); - }); -}); diff --git a/src/components/sbb-checkbox-group/sbb-checkbox-group.spec.ts b/src/components/sbb-checkbox-group/sbb-checkbox-group.spec.ts deleted file mode 100644 index 26ec767c56..0000000000 --- a/src/components/sbb-checkbox-group/sbb-checkbox-group.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SbbCheckboxGroup } from './sbb-checkbox-group'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-checkbox-group', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckboxGroup], - html: ` - - Label 1 - Label 2 - Label 3 - - `, - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    - Label 1 - Label 2 - Label 3 -
    - `); - }); -}); diff --git a/src/components/sbb-checkbox-group/sbb-checkbox-group.tsx b/src/components/sbb-checkbox-group/sbb-checkbox-group.tsx deleted file mode 100644 index cdaf6f545c..0000000000 --- a/src/components/sbb-checkbox-group/sbb-checkbox-group.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - h, - Host, - JSX, - Listen, - Prop, - State, - Watch, -} from '@stencil/core'; -import { InterfaceSbbCheckboxGroupAttributes } from './sbb-checkbox-group.custom'; -import { isArrowKeyPressed, getNextElementIndex, interactivityChecker } from '../../global/a11y'; -import { toggleDatasetEntry, isValidAttribute } from '../../global/dom'; -import { - createNamedSlotState, - HandlerRepository, - namedSlotChangeHandlerAspect, -} from '../../global/eventing'; - -/** - * @slot unnamed - Slot used to render the inside the . - * @slot error - Slot used to render the inside the . - */ - -@Component({ - shadow: true, - styleUrl: 'sbb-checkbox-group.scss', - tag: 'sbb-checkbox-group', -}) -export class SbbCheckboxGroup implements ComponentInterface { - /** - * Whether the checkbox group is disabled. - */ - @Prop() public disabled = false; - - /** - * Whether the checkbox group is required. - */ - @Prop() public required = false; - - /** - * Size variant, either m or s. - */ - @Prop() public size: InterfaceSbbCheckboxGroupAttributes['size'] = 'm'; - - /** - * Overrides the behaviour of `orientation` property. - */ - @Prop({ reflect: true }) - public horizontalFrom?: InterfaceSbbCheckboxGroupAttributes['horizontalFrom']; - - /** - * Indicates the orientation of the checkboxes inside the ``. - */ - @Prop({ reflect: true }) public orientation: InterfaceSbbCheckboxGroupAttributes['orientation'] = - 'horizontal'; - - /** - * State of listed named slots, by indicating whether any element for a named slot is defined. - */ - @State() private _namedSlots = createNamedSlotState('error'); - - @Element() private _element!: HTMLElement; - - private _handlerRepository = new HandlerRepository( - this._element, - namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), - ); - - @Watch('disabled') - public updateDisabled(): void { - for (const checkbox of this._checkboxes) { - toggleDatasetEntry(checkbox, 'groupDisabled', this.disabled); - } - } - - @Watch('required') - public updateRequired(): void { - for (const checkbox of this._checkboxes) { - toggleDatasetEntry(checkbox, 'groupRequired', this.required); - } - } - - @Watch('size') - public updateSize(): void { - for (const checkbox of this._checkboxes) { - checkbox.size = this.size; - } - } - - public connectedCallback(): void { - toggleDatasetEntry( - this._element, - 'hasSelectionPanel', - !!this._element.querySelector('sbb-selection-panel'), - ); - this._handlerRepository.connect(); - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - } - - @Listen('keydown') - public handleKeyDown(evt: KeyboardEvent): void { - const enabledCheckboxes: HTMLSbbCheckboxElement[] = this._checkboxes.filter( - (checkbox: HTMLSbbCheckboxElement) => - !isValidAttribute(checkbox, 'disabled') && interactivityChecker.isVisible(checkbox), - ); - - if ( - !enabledCheckboxes || - // don't trap nested handling - ((evt.target as HTMLElement) !== this._element && - (evt.target as HTMLElement).parentElement !== this._element && - (evt.target as HTMLElement).parentElement.nodeName !== 'SBB-SELECTION-PANEL') - ) { - return; - } - - if (isArrowKeyPressed(evt)) { - const current: number = enabledCheckboxes.findIndex( - (e: HTMLSbbCheckboxElement) => e === evt.target, - ); - const nextIndex: number = getNextElementIndex(evt, current, enabledCheckboxes.length); - enabledCheckboxes[nextIndex]?.focus(); - } - } - - private _updateCheckboxes(): void { - const checkboxes = this._checkboxes; - - for (const checkbox of checkboxes) { - checkbox.size = this.size; - toggleDatasetEntry(checkbox, 'groupDisabled', this.disabled); - toggleDatasetEntry(checkbox, 'groupRequired', this.required); - } - } - - private get _checkboxes(): HTMLSbbCheckboxElement[] { - return ( - Array.from(this._element.querySelectorAll('sbb-checkbox')) as HTMLSbbCheckboxElement[] - ).filter((el) => el.closest('sbb-checkbox-group') === this._element); - } - - public render(): JSX.Element { - return ( - -
    - this._updateCheckboxes()} /> -
    - {this._namedSlots.error && ( -
    - -
    - )} -
    - ); - } -} diff --git a/src/components/sbb-checkbox/readme.md b/src/components/sbb-checkbox/readme.md deleted file mode 100644 index 99f48a87a0..0000000000 --- a/src/components/sbb-checkbox/readme.md +++ /dev/null @@ -1,121 +0,0 @@ -The `sbb-checkbox` component provides the same functionality as a native `` enhanced with the SBB Design. - -## Slots - -It is possible to provide a label via an unnamed slot; the component can optionally display a `sbb-icon` using -the `iconName` property or via custom SVG using the `icon` slot. -The icon can be placed before or after the label based on the value of the `iconPlacement` property (default: `end`). - -```html -Example - -Icon - -Icon at start -``` - -## States - -The component could be checked or not depending on the value of the `checked` attribute. - -```html -Checked state -``` - -It has a third state too, which is set if the `indeterminate` property is true. -This is useful when multiple dependent checkboxes are used -(e.g., a parent which is checked only if all the children are checked, otherwise is in indeterminate state). -Clicking on a `sbb-checkbox` in this state sets `checked` to `true` and `indeterminate` to false. - -```html -Indeterminate state -``` - -The component can be displayed in `disabled` or `required` state by using the self-named properties. - -```html -Required - -Disabled -``` - -## Style - -The component has two `size`, named `s` (default) and `m`. - -```html -Size -``` - -## Events - -Consumers can listen to the native `change` event on the `sbb-checkbox` component to intercept the input's change; -the current state can be read from `event.target.checked`, while the value from `event.target.value`. - -## Accessibility - -The component uses an internal `` element to provide an accessible experience. - -This internal checkbox receives focus and is automatically labeled by the text content of the `sbb-checkbox` element. -Avoid adding other interactive controls into the content of `sbb-checkbox`, as this degrades the experience for users of assistive technology. - -Always provide an accessible label via `aria-label` for checkboxes without descriptive text content. -If you don't want the label to appear next to the checkbox, you can use `aria-label` to specify an appropriate label. - -```html - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| --------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------- | -| `checked` | `checked` | Whether the checkbox is checked. | `boolean` | `false` | -| `disabled` | `disabled` | Whether the checkbox is disabled. | `boolean` | `false` | -| `iconName` | `icon-name` | 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). | `string` | `undefined` | -| `iconPlacement` | `icon-placement` | The label position relative to the labelIcon. Defaults to end | `"end" \| "start"` | `'end'` | -| `indeterminate` | `indeterminate` | Whether the checkbox is indeterminate. | `boolean` | `false` | -| `required` | `required` | Whether the checkbox is required. | `boolean` | `false` | -| `size` | `size` | Label size variant, either m or s. | `"m" \| "s"` | `'m'` | -| `value` | `value` | Value of checkbox. | `string` | `undefined` | - - -## Events - -| Event | Description | Type | -| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| `didChange` | **[DEPRECATED]** only used for React. Will probably be removed once React 19 is available.

    | `CustomEvent` | -| `sbb-checkbox-loaded` | Internal event that emits when the input element is loaded. | `CustomEvent` | - - -## Slots - -| Slot | Description | -| ----------- | ----------------------------------------------------------------------------------------------- | -| `"icon"` | Slot used to render the checkbox icon (disabled inside a selection panel). | -| `"subtext"` | Slot used to render a subtext under the label (only visible within a selection panel). | -| `"suffix"` | Slot used to render additional content after the label (only visible within a selection panel). | -| `"unnamed"` | Slot used to render the checkbox label's text. | - - -## Dependencies - -### Depends on - -- [sbb-visual-checkbox](../sbb-visual-checkbox) -- [sbb-icon](../sbb-icon) - -### Graph -```mermaid -graph TD; - sbb-checkbox --> sbb-visual-checkbox - sbb-checkbox --> sbb-icon - style sbb-checkbox fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-checkbox/sbb-checkbox.custom.d.ts b/src/components/sbb-checkbox/sbb-checkbox.custom.d.ts deleted file mode 100644 index 5afbf20b75..0000000000 --- a/src/components/sbb-checkbox/sbb-checkbox.custom.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type CheckboxStateChange = CheckboxStateChangeChecked | CheckboxStateChangeDisabled; - -export interface CheckboxStateChangeDisabled { - type: 'disabled'; - disabled: boolean; -} - -export interface CheckboxStateChangeChecked { - type: 'checked'; - checked: boolean; -} - -export interface InterfaceSbbCheckboxAttributes { - size: 'm' | 's'; - iconPlacement?: 'start' | 'end'; -} diff --git a/src/components/sbb-checkbox/sbb-checkbox.e2e.ts b/src/components/sbb-checkbox/sbb-checkbox.e2e.ts deleted file mode 100644 index a94887306b..0000000000 --- a/src/components/sbb-checkbox/sbb-checkbox.e2e.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-checkbox', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(''); - element = await page.find('sbb-checkbox'); - }); - - it('should render', async () => { - element = await page.find('sbb-checkbox'); - expect(element).toHaveClass('hydrated'); - }); - - it('should not render accessibility label containing expanded state', async () => { - element = await page.find('sbb-checkbox >>> .sbb-checkbox__expanded-label'); - expect(element).toBeFalsy(); - }); - - describe('events', () => { - it('emit event on click', async () => { - await page.waitForChanges(); - const changeSpy = await page.spyOnEvent('change'); - await element.click(); - expect(changeSpy).toHaveReceivedEvent(); - }); - - it('emit event on keypress', async () => { - await page.waitForChanges(); - const changeSpy = await page.spyOnEvent('change'); - await element.press('Tab'); - await element.press('Space'); - await page.waitForChanges(); - expect(changeSpy).toHaveReceivedEvent(); - }); - }); - - describe('indeterminate', () => { - it('should set indeterminate to false after checked', async () => { - page = await newE2EPage(); - await page.setContent('Label'); - element = await page.find('sbb-checkbox'); - await page.waitForChanges(); - - expect(await element.getProperty('checked')).toBe(false); - expect(await element.getProperty('indeterminate')).toBe(true); - - await element.click(); - await page.waitForChanges(); - - expect(await element.getProperty('checked')).toBe(true); - expect(await element.getProperty('indeterminate')).toBeFalsy(); - }); - - it('should update indeterminate state of input', async () => { - await page.waitForChanges(); - - expect(await element.getProperty('indeterminate')).toBeFalsy(); - - element.setProperty('indeterminate', true); - await page.waitForChanges(); - - expect(await element.getProperty('indeterminate')).toBe(true); - }); - }); - - it('should prevent scrolling on space bar press', async () => { - page = await newE2EPage(); - await page.setContent( - `
    -
    - -
    -
    `, - ); - element = await page.find('sbb-checkbox'); - expect(element).not.toHaveAttribute('checked'); - expect(await page.evaluate(() => document.querySelector('#scroll-context').scrollTop)).toBe(0); - - await element.press(' '); - await page.waitForChanges(); - - expect(element).toHaveAttribute('checked'); - expect(await page.evaluate(() => document.querySelector('#scroll-context').scrollTop)).toBe(0); - }); -}); diff --git a/src/components/sbb-checkbox/sbb-checkbox.events.ts b/src/components/sbb-checkbox/sbb-checkbox.events.ts deleted file mode 100644 index 2b5485d0a9..0000000000 --- a/src/components/sbb-checkbox/sbb-checkbox.events.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - didChange: 'didChange', - sbbCheckboxLoaded: 'sbb-checkbox-loaded', - stateChange: 'state-change', -}; diff --git a/src/components/sbb-checkbox/sbb-checkbox.spec.ts b/src/components/sbb-checkbox/sbb-checkbox.spec.ts deleted file mode 100644 index 1d7df46332..0000000000 --- a/src/components/sbb-checkbox/sbb-checkbox.spec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { newSpecPage } from '@stencil/core/testing'; -import { SbbCheckbox } from './sbb-checkbox'; - -describe('sbb-checkbox', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckbox], - html: 'Label', - }); - - expect(root).toEqualHtml(` - - - - - - - Label - - `); - }); - - describe('icon position', () => { - it('start', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckbox], - html: 'Label', - }); - - expect(root).toEqualHtml(` - - - - - - - Label - - `); - }); - }); - - describe('state', () => { - it('checked', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckbox], - html: 'Label', - }); - - expect(root).toEqualHtml(` - - - - - - - Label - - `); - }); - - it('indeterminate', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckbox], - html: 'Label', - }); - - const input = root.shadowRoot.querySelector('input'); - expect(input.indeterminate).toBe(true); - - expect(root).toEqualHtml(` - - - - - - - Label - `); - }); - - it('unchecked disabled', async () => { - const { root } = await newSpecPage({ - components: [SbbCheckbox], - html: 'Label', - }); - expect(root).toEqualHtml(` - - - - - - - Label - - `); - }); - }); -}); diff --git a/src/components/sbb-checkbox/sbb-checkbox.tsx b/src/components/sbb-checkbox/sbb-checkbox.tsx deleted file mode 100644 index c390c69d19..0000000000 --- a/src/components/sbb-checkbox/sbb-checkbox.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { - Component, - Prop, - h, - JSX, - Element, - State, - ComponentInterface, - Listen, - EventEmitter, - Event, - Watch, - Host, -} from '@stencil/core'; -import { CheckboxStateChange, InterfaceSbbCheckboxAttributes } from './sbb-checkbox.custom'; -import { i18nCollapsed, i18nExpanded } from '../../global/i18n'; -import { isValidAttribute } from '../../global/dom'; -import { - createNamedSlotState, - documentLanguage, - HandlerRepository, - languageChangeHandlerAspect, - namedSlotChangeHandlerAspect, - formElementHandlerAspect, - getEventTarget, - forwardEventToHost, -} from '../../global/eventing'; -import { AgnosticMutationObserver } from '../../global/observers'; - -/** Configuration for the attribute to look at if component is nested in a sbb-checkbox-group */ -const checkboxObserverConfig: MutationObserverInit = { - attributeFilter: ['data-group-required', 'data-group-disabled'], -}; - -/** - * @slot unnamed - Slot used to render the checkbox label's text. - * @slot icon - Slot used to render the checkbox icon (disabled inside a selection panel). - * @slot subtext - Slot used to render a subtext under the label (only visible within a selection panel). - * @slot suffix - Slot used to render additional content after the label (only visible within a selection panel). - */ -@Component({ - shadow: true, - styleUrl: 'sbb-checkbox.scss', - tag: 'sbb-checkbox', -}) -export class SbbCheckbox implements ComponentInterface { - /** Value of checkbox. */ - @Prop() public value?: string; - - /** Whether the checkbox is disabled. */ - @Prop({ reflect: true }) public disabled = false; - - /** Whether the checkbox is required. */ - @Prop() public required = false; - - /** Whether the checkbox is indeterminate. */ - @Prop({ reflect: true, mutable: true }) 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). - */ - @Prop() public iconName?: string; - - /** The label position relative to the labelIcon. Defaults to end */ - @Prop({ reflect: true }) public iconPlacement: InterfaceSbbCheckboxAttributes['iconPlacement'] = - 'end'; - - /** Whether the checkbox is checked. */ - @Prop({ mutable: true, reflect: true }) public checked = false; - - /** Label size variant, either m or s. */ - @Prop({ reflect: true, mutable: true }) public size: InterfaceSbbCheckboxAttributes['size'] = 'm'; - - /** Whether the component must be set disabled due disabled attribute on sbb-checkbox-group. */ - @State() private _disabledFromGroup = false; - - /** Whether the component must be set required due required attribute on sbb-checkbox-group. */ - @State() private _requiredFromGroup = false; - - /** State of listed named slots, by indicating whether any element for a named slot is defined. */ - @State() private _namedSlots = createNamedSlotState('icon', 'subtext', 'suffix'); - - @State() private _currentLanguage = documentLanguage(); - - /** Whether the input is the main input of a selection panel. */ - @State() private _isSelectionPanelInput = false; - - /** The label describing whether the selection panel is expanded (for screen readers only). */ - @State() private _selectionPanelExpandedLabel: string; - - private _checkbox: HTMLInputElement; - private _selectionPanelElement: HTMLElement; - - /** MutationObserver on data attributes. */ - private _checkboxAttributeObserver = new AgnosticMutationObserver( - this._onCheckboxAttributesChange.bind(this), - ); - - @Element() private _element!: HTMLElement; - - /** - * @deprecated only used for React. Will probably be removed once React 19 is available. - */ - @Event({ bubbles: true, cancelable: true }) public didChange: EventEmitter; - - /** - * @internal - * Internal event that emits whenever the state of the checkbox - * in relation to the parent selection panel changes. - */ - @Event({ - bubbles: true, - eventName: 'state-change', - }) - public stateChange: EventEmitter; - - /** - * Internal event that emits when the input element is loaded. - */ - @Event({ - bubbles: true, - eventName: 'sbb-checkbox-loaded', - }) - public sbbCheckboxLoaded: EventEmitter; - - @Watch('checked') - public handleCheckedChange(currentValue: boolean, previousValue: boolean): void { - if (this._isSelectionPanelInput && currentValue !== previousValue) { - this.stateChange.emit({ type: 'checked', checked: currentValue }); - this._updateExpandedLabel(); - } - } - - @Watch('disabled') - public handleDisabledChange(currentValue: boolean, previousValue: boolean): void { - if (this._isSelectionPanelInput && currentValue !== previousValue) { - this.stateChange.emit({ type: 'disabled', disabled: currentValue }); - } - } - - private _handlerRepository = new HandlerRepository( - this._element, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), - formElementHandlerAspect, - ); - - // Set up the initial disabled/required values and start observe attributes changes. - private _setupInitialStateAndAttributeObserver(): void { - const parentGroup = this._element.closest('sbb-checkbox-group'); - if (parentGroup) { - this._requiredFromGroup = isValidAttribute(parentGroup, 'required'); - this._disabledFromGroup = isValidAttribute(parentGroup, 'disabled'); - this.size = parentGroup.size; - } - this._checkboxAttributeObserver.observe(this._element, checkboxObserverConfig); - } - - /** Observe changes on data attributes and set the appropriate values. */ - private _onCheckboxAttributesChange(mutationsList: MutationRecord[]): void { - for (const mutation of mutationsList) { - if (mutation.attributeName === 'data-group-disabled') { - this._disabledFromGroup = !!isValidAttribute(this._element, 'data-group-disabled'); - } - if (mutation.attributeName === 'data-group-required') { - this._requiredFromGroup = !!isValidAttribute(this._element, 'data-group-required'); - } - } - } - - public connectedCallback(): void { - // We can use closest here, as we expect the parent sbb-selection-panel to be in light DOM. - this._selectionPanelElement = this._element.closest('sbb-selection-panel'); - this._isSelectionPanelInput = - !!this._selectionPanelElement && - !this._element.closest('sbb-selection-panel [slot="content"]'); - this._handlerRepository.connect(); - this._setupInitialStateAndAttributeObserver(); - this._isSelectionPanelInput && this.sbbCheckboxLoaded.emit(); - } - - public componentDidLoad(): void { - this._isSelectionPanelInput && this._updateExpandedLabel(); - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - this._checkboxAttributeObserver.disconnect(); - } - - @Listen('click') - public handleClick(event: Event): void { - if (!this.disabled && !this._disabledFromGroup && getEventTarget(event) === this._element) { - this._checkbox.click(); - } - } - - @Listen('keyup') - public handleKeyup(event: KeyboardEvent): void { - // The native checkbox input toggles state on keyup with space. - if (!this.disabled && !this._disabledFromGroup && event.key === ' ') { - // The toggle needs to happen after the keyup event finishes, so we schedule - // it to be triggered after the current event loop. - setTimeout(() => this._checkbox.click()); - } - } - - public handleChangeEvent(event: Event): void { - forwardEventToHost(event, this._element); - this.didChange.emit(); - } - - /** - * Method triggered on checkbox input event. - * If not indeterminate, inverts the value; otherwise sets checked to true. - */ - public handleInputEvent(): void { - if (this.indeterminate) { - this.checked = true; - this.indeterminate = false; - } else { - this.checked = this._checkbox?.checked ?? false; - } - } - - private _updateExpandedLabel(): void { - if (!this._selectionPanelElement.hasAttribute('data-has-content')) { - this._selectionPanelExpandedLabel = ''; - return; - } - - this._selectionPanelExpandedLabel = this.checked - ? ', ' + i18nExpanded[this._currentLanguage] - : ', ' + i18nCollapsed[this._currentLanguage]; - } - - public render(): JSX.Element { - const attributes = { - role: 'checkbox', - 'aria-checked': this.indeterminate ? 'mixed' : this.checked?.toString() ?? 'false', - 'aria-required': (this.required || this._requiredFromGroup).toString(), - 'aria-disabled': (this.disabled || this._disabledFromGroup).toString(), - 'data-is-selection-panel-input': this._isSelectionPanelInput, - ...(this.disabled || this._disabledFromGroup ? undefined : { tabIndex: '0' }), - }; - return ( - - - - - - ); - } -} diff --git a/src/components/sbb-chip/readme.md b/src/components/sbb-chip/readme.md deleted file mode 100644 index 8448ccdf3a..0000000000 --- a/src/components/sbb-chip/readme.md +++ /dev/null @@ -1,40 +0,0 @@ -The `sbb-chip` is a visual component used to display compact information, like a filter's name or a tag. - -```html -On sale -``` - -## Style - -It's possible to choose among three different values for the `size` property (`s`, `xs` and `xxs`, which is the default), -and four different values for the `color` property (`charcoal`, `granite`, `white` and `milk`, which is the default). - -```html -Label - -Label - -Label -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| -------- | --------- | ------------------ | ---------------------------------------------- | -------- | -| `color` | `color` | Color of the chip. | `"charcoal" \| "granite" \| "milk" \| "white"` | `'milk'` | -| `size` | `size` | Size of the chip. | `"s" \| "xs" \| "xxs"` | `'xxs'` | - - -## Slots - -| Slot | Description | -| ----------- | --------------------------- | -| `"unnamed"` | Content / Label of the chip | - - ----------------------------------------------- - - diff --git a/src/components/sbb-chip/sbb-chip.custom.d.ts b/src/components/sbb-chip/sbb-chip.custom.d.ts deleted file mode 100644 index 104908568a..0000000000 --- a/src/components/sbb-chip/sbb-chip.custom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface InterfaceSbbChipAttributes { - size: 's' | 'xs' | 'xxs'; - color: 'milk' | 'charcoal' | 'white' | 'granite'; -} diff --git a/src/components/sbb-chip/sbb-chip.e2e.ts b/src/components/sbb-chip/sbb-chip.e2e.ts deleted file mode 100644 index d6f5daccaa..0000000000 --- a/src/components/sbb-chip/sbb-chip.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-chip', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent('Label'); - - element = await page.find('sbb-chip'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-chip/sbb-chip.spec.ts b/src/components/sbb-chip/sbb-chip.spec.ts deleted file mode 100644 index c95c259c54..0000000000 --- a/src/components/sbb-chip/sbb-chip.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SbbChip } from './sbb-chip'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-chip', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbChip], - html: 'Label', - }); - - expect(root).toEqualHtml(` - - - - - - - - - Label - - `); - }); -}); diff --git a/src/components/sbb-chip/sbb-chip.tsx b/src/components/sbb-chip/sbb-chip.tsx deleted file mode 100644 index 1f3d8838f5..0000000000 --- a/src/components/sbb-chip/sbb-chip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Component, ComponentInterface, h, JSX, Prop } from '@stencil/core'; -import { InterfaceSbbChipAttributes } from './sbb-chip.custom'; - -/** - * @slot unnamed - Content / Label of the chip - */ -@Component({ - shadow: true, - styleUrl: 'sbb-chip.scss', - tag: 'sbb-chip', -}) -export class SbbChip implements ComponentInterface { - /** Size of the chip. */ - @Prop({ reflect: true }) - public size: InterfaceSbbChipAttributes['size'] = 'xxs'; - - /** Color of the chip. */ - @Prop({ reflect: true }) - public color: InterfaceSbbChipAttributes['color'] = 'milk'; - - public render(): JSX.Element { - return ( - - - - - - ); - } -} diff --git a/src/components/sbb-clock/sbb-clock.e2e.ts b/src/components/sbb-clock/sbb-clock.e2e.ts deleted file mode 100644 index 3bc65c088d..0000000000 --- a/src/components/sbb-clock/sbb-clock.e2e.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-clock', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-clock'); - - expect(element).toEqualHtml(` - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-clock/sbb-clock.spec.ts b/src/components/sbb-clock/sbb-clock.spec.ts deleted file mode 100644 index e290a90e81..0000000000 --- a/src/components/sbb-clock/sbb-clock.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SbbClock } from './sbb-clock'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-clock', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbClock], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    - - - - -
    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.e2e.ts b/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.e2e.ts deleted file mode 100644 index 2fe6b79da0..0000000000 --- a/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.e2e.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-datepicker-next-day', () => { - describe('standalone', () => { - it('renders', async () => { - const page: E2EPage = await newE2EPage({ - html: '', - }); - const element: E2EElement = await page.find('sbb-datepicker-next-day'); - expect(element).toHaveClass('hydrated'); - }); - }); - - describe('with picker', () => { - it('renders and click', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - - `, - }); - const element: E2EElement = await page.find('sbb-datepicker-next-day'); - const input: E2EElement = await page.find('input'); - await page.waitForChanges(); - expect(element).toHaveClass('hydrated'); - expect(await input.getProperty('value')).toEqual('Sa, 31.12.2022'); - - const changeSpy = await input.spyOnEvent('change'); - const blurSpy = await input.spyOnEvent('blur'); - await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - expect(blurSpy).toHaveReceivedEventTimes(1); - - expect(await input.getProperty('value')).toEqual('Su, 01.01.2023'); - }); - }); - - describe('in form field', () => { - let element: E2EElement, input: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - - - - `); - element = await page.find('sbb-datepicker-next-day'); - input = await page.find('input'); - await page.waitForChanges(); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('click', async () => { - expect(await input.getProperty('value')).toEqual('Sa, 21.01.2023'); - const changeSpy = await input.spyOnEvent('change'); - const blurSpy = await input.spyOnEvent('blur'); - await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - expect(blurSpy).toHaveReceivedEventTimes(1); - expect(await input.getProperty('value')).toEqual('Su, 22.01.2023'); - }); - - it('disabled due max value equals to value', async () => { - page = await newE2EPage(); - await page.setContent(` - - - - - - `); - input = await page.find('input'); - await page.waitForChanges(); - - expect(await input.getProperty('value')).toEqual('Sa, 21.01.2023'); - await page.waitForChanges(); - - expect( - await page.evaluate(() => - document.querySelector('sbb-datepicker-next-day').hasAttribute('data-disabled'), - ), - ).toEqual(true); - - await element.click(); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('Sa, 21.01.2023'); - }); - - it('disabled due disabled picker', async () => { - expect(await input.getProperty('value')).toEqual('Sa, 21.01.2023'); - await page.evaluate(() => document.querySelector('input').setAttribute('disabled', '')); - - await page.waitForChanges(); - - expect(element).toHaveAttribute('data-disabled'); - await element.click(); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('Sa, 21.01.2023'); - }); - }); -}); diff --git a/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.spec.ts b/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.spec.ts deleted file mode 100644 index 3a3a187e14..0000000000 --- a/src/components/sbb-datepicker-next-day/sbb-datepicker-next-day.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { SbbDatepickerNextDay } from './sbb-datepicker-next-day'; -import { newSpecPage, SpecPage } from '@stencil/core/testing'; -import { SbbFormField } from '../sbb-form-field/sbb-form-field'; -import { SbbDatepicker } from '../sbb-datepicker/sbb-datepicker'; - -describe('sbb-datepicker-next-day', () => { - it('renders', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbDatepickerNextDay], - html: '', - }); - - expect(page.root).toEqualHtml(` - - - - - - - - `); - }); - - it('renders with datepicker and input disabled', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerNextDay], - html: ` - - - - - - `, - }); - - const element: HTMLSbbDatepickerNextDayElement = - page.doc.querySelector('sbb-datepicker-next-day'); - expect(element).toHaveAttribute('data-disabled'); - }); - - it('renders with datepicker and input readonly', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerNextDay], - html: ` - - - - - - `, - }); - - const element: HTMLSbbDatepickerNextDayElement = - page.doc.querySelector('sbb-datepicker-next-day'); - expect(element).toHaveAttribute('data-disabled'); - }); -}); diff --git a/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.e2e.ts b/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.e2e.ts deleted file mode 100644 index aaaadfb77e..0000000000 --- a/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.e2e.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-datepicker-previous-day', () => { - describe('standalone', () => { - it('renders', async () => { - const page: E2EPage = await newE2EPage({ - html: '', - }); - const element: E2EElement = await page.find('sbb-datepicker-previous-day'); - expect(element).toHaveClass('hydrated'); - }); - }); - - describe('with picker', () => { - it('renders and click', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - - `, - }); - const element: E2EElement = await page.find('sbb-datepicker-previous-day'); - const input: E2EElement = await page.find('input'); - await page.waitForChanges(); - expect(element).toHaveClass('hydrated'); - expect(await input.getProperty('value')).toEqual('Su, 01.01.2023'); - - const changeSpy = await input.spyOnEvent('change'); - const blurSpy = await input.spyOnEvent('blur'); - await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - expect(blurSpy).toHaveReceivedEventTimes(1); - - expect(await input.getProperty('value')).toEqual('Sa, 31.12.2022'); - }); - }); - - describe('in form field', () => { - let element: E2EElement, input: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - - - - `); - element = await page.find('sbb-datepicker-previous-day'); - input = await page.find('input'); - await page.waitForChanges(); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('click', async () => { - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - const changeSpy = await input.spyOnEvent('change'); - const blurSpy = await input.spyOnEvent('blur'); - await element.click(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(changeSpy).toHaveReceivedEventTimes(1); - expect(blurSpy).toHaveReceivedEventTimes(1); - expect(await input.getProperty('value')).toEqual('Th, 19.01.2023'); - }); - - it('disabled due min equals to value', async () => { - page = await newE2EPage(); - await page.setContent(` - - - - - - `); - input = await page.find('input'); - await page.waitForChanges(); - - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - expect( - await page.evaluate(() => - document.querySelector('sbb-datepicker-previous-day').getAttribute('data-disabled'), - ), - ).toEqual(''); - - await element.click(); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - }); - - it('disabled due disabled picker', async () => { - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - await page.evaluate(() => document.querySelector('input').setAttribute('disabled', '')); - await page.waitForChanges(); - - expect(element).toHaveAttribute('data-disabled'); - await element.click(); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - }); - }); -}); diff --git a/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.spec.ts b/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.spec.ts deleted file mode 100644 index c33734ef58..0000000000 --- a/src/components/sbb-datepicker-previous-day/sbb-datepicker-previous-day.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { SbbDatepickerPreviousDay } from './sbb-datepicker-previous-day'; -import { newSpecPage, SpecPage } from '@stencil/core/testing'; -import { SbbFormField } from '../sbb-form-field/sbb-form-field'; -import { SbbDatepicker } from '../sbb-datepicker/sbb-datepicker'; - -describe('sbb-datepicker-previous-day', () => { - it('renders', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbDatepickerPreviousDay], - html: '', - }); - - expect(page.root).toEqualHtml(` - - - - - - - - `); - }); - - it('renders with datepicker and input disabled', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerPreviousDay], - html: ` - - - - - - `, - }); - - const element: HTMLSbbDatepickerPreviousDayElement = page.doc.querySelector( - 'sbb-datepicker-previous-day', - ); - expect(element).toHaveAttribute('data-disabled'); - }); - - it('renders with datepicker and input readonly', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerPreviousDay], - html: ` - - - - - - `, - }); - - const element: HTMLSbbDatepickerPreviousDayElement = page.doc.querySelector( - 'sbb-datepicker-previous-day', - ); - expect(element).toHaveAttribute('data-disabled'); - }); -}); diff --git a/src/components/sbb-datepicker-toggle/readme.md b/src/components/sbb-datepicker-toggle/readme.md deleted file mode 100644 index 9f10463e69..0000000000 --- a/src/components/sbb-datepicker-toggle/readme.md +++ /dev/null @@ -1,81 +0,0 @@ -The `sbb-datepicker-toggle` is a component -closely connected to the [sbb-datepicker](/docs/components-sbb-datepicker-sbb-datepicker--docs). - -When the two are used together, the `sbb-datepicker-toggle` can be used to link the `sbb-datepicker` -to a [sbb-calendar](/docs/components-sbb-datepicker-sbb-calendar--docs): -a change in the latter, like selecting a date, is propagated to the former; and conversely, changes in the `sbb-datepicker` -properties, or in the date-picker's input attributes, are propagated to the `sbb-calendar` to modify its appearance. - -The components can be connected using the `datePicker` property, which accepts the id of the `sbb-datepicker`, -or directly its reference. - -```html - - - -``` - -## In `sbb-form-field` - -If the two components are used within a [sbb-form-field](/docs/components-sbb-form-field-sbb-form-field--docs), -they are automatically linked and the `sbb-datepicker-toggle` will be projected in the `prefix` slot of the `sbb-form-field`; -otherwise, they can be connected using the `datePicker` property as described above. - -```html - - - - - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | ---------------------------------- | ----------------------- | ----------- | -| `datePicker` | `date-picker` | Datepicker reference. | `HTMLElement \| string` | `undefined` | -| `disableAnimation` | `disable-animation` | Whether the animation is disabled. | `boolean` | `false` | -| `negative` | `negative` | Negative coloring variant flag. | `boolean` | `false` | - - -## Methods - -### `open() => Promise` - -Opens the calendar. - -#### Returns - -Type: `Promise` - - - - -## Dependencies - -### Depends on - -- [sbb-tooltip-trigger](../sbb-tooltip-trigger) -- [sbb-tooltip](../sbb-tooltip) -- [sbb-calendar](../sbb-calendar) - -### Graph -```mermaid -graph TD; - sbb-datepicker-toggle --> sbb-tooltip-trigger - sbb-datepicker-toggle --> sbb-tooltip - sbb-datepicker-toggle --> sbb-calendar - sbb-tooltip-trigger --> sbb-icon - sbb-tooltip --> sbb-button - sbb-button --> sbb-icon - sbb-calendar --> sbb-icon - sbb-calendar --> sbb-button - style sbb-datepicker-toggle fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.e2e.ts b/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.e2e.ts deleted file mode 100644 index 80d28e7468..0000000000 --- a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.e2e.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; - -describe('sbb-datepicker-toggle', () => { - it('renders standalone', async () => { - const page: E2EPage = await newE2EPage({ - html: '', - }); - await page.waitForChanges(); - - const element: E2EElement = await page.find('sbb-datepicker-toggle'); - const tooltipTrigger: E2EElement = await page.find( - 'sbb-datepicker-toggle >>> sbb-tooltip-trigger', - ); - expect(element).toHaveClass('hydrated'); - expect(tooltipTrigger).toHaveAttribute('disabled'); - }); - - it('renders and opens tooltip with picker', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - - `, - }); - const element: E2EElement = await page.find('sbb-datepicker-toggle'); - const didOpenEventSpy = await element.spyOnEvent('did-open'); - const tooltipTrigger: E2EElement = await page.find( - 'sbb-datepicker-toggle >>> sbb-tooltip-trigger', - ); - const tooltip: E2EElement = await page.find('sbb-datepicker-toggle >>> sbb-tooltip'); - await page.waitForChanges(); - expect(element).toHaveClass('hydrated'); - expect(tooltipTrigger).not.toHaveAttribute('disabled'); - expect(tooltip).toEqualAttribute('data-state', 'closed'); - - await tooltipTrigger.click(); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - - expect(tooltip).toEqualAttribute('data-state', 'opened'); - }); - - it('renders and opens tooltip programmatically', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - - `, - }); - const element: E2EElement = await page.find('sbb-datepicker-toggle'); - const didOpenEventSpy = await element.spyOnEvent('did-open'); - const tooltipTrigger: E2EElement = await page.find( - 'sbb-datepicker-toggle >>> sbb-tooltip-trigger', - ); - const tooltip: E2EElement = await page.find('sbb-datepicker-toggle >>> sbb-tooltip'); - await page.waitForChanges(); - expect(element).toHaveClass('hydrated'); - expect(tooltipTrigger).not.toHaveAttribute('disabled'); - expect(tooltip).toEqualAttribute('data-state', 'closed'); - - await page.evaluate(() => - (document.querySelector('sbb-datepicker-toggle') as HTMLSbbDatepickerToggleElement).open(), - ); - - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - - expect(tooltip).toEqualAttribute('data-state', 'opened'); - }); - - it('renders in form field, open calendar and change date', async () => { - const page: E2EPage = await newE2EPage(); - await page.setContent(` - - - - - - `); - await page.waitForChanges(); - const tooltip: E2EElement = await page.find('sbb-datepicker-toggle >>> sbb-tooltip'); - expect(tooltip).toEqualAttribute('data-state', 'closed'); - const element: E2EElement = await page.find('sbb-datepicker-toggle'); - const input: E2EElement = await page.find('input'); - const didOpenEventSpy = await element.spyOnEvent('did-open'); - const changeSpy = await input.spyOnEvent('change'); - const blurSpy = await input.spyOnEvent('blur'); - expect(element).toHaveClass('hydrated'); - - const tooltipTrigger: E2EElement = await page.find( - 'sbb-datepicker-toggle >>> sbb-tooltip-trigger', - ); - await tooltipTrigger.click(); - await page.waitForChanges(); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - expect(tooltip).toEqualAttribute('data-state', 'opened'); - - const calendar: E2EElement = await page.find('sbb-datepicker-toggle >>> sbb-calendar'); - await calendar.triggerEvent('date-selected', { - detail: new Date('2022-01-01'), - }); - await page.waitForChanges(); - - expect(await input.getProperty('value')).toEqual('Sa, 01.01.2022'); - expect(changeSpy).toHaveReceivedEventTimes(1); - expect(blurSpy).toHaveReceivedEventTimes(1); - }); -}); diff --git a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.spec.ts b/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.spec.ts deleted file mode 100644 index 5797af1e87..0000000000 --- a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { SbbDatepickerToggle } from './sbb-datepicker-toggle'; -import { newSpecPage, SpecPage } from '@stencil/core/testing'; -import { SbbFormField } from '../sbb-form-field/sbb-form-field'; -import { SbbDatepicker } from '../sbb-datepicker/sbb-datepicker'; - -describe('sbb-datepicker-toggle', () => { - it('renders', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbDatepickerToggle], - html: '', - }); - - expect(page.root).toEqualHtml(` - - - - - - - - - `); - }); - - describe('renders in form-field', () => { - it('renders in form-field', async () => { - const page: SpecPage = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerToggle], - html: ` - - - - - - `, - }); - const element: HTMLSbbDatepickerToggleElement = - page.doc.querySelector('sbb-datepicker-toggle'); - expect(element).toEqualHtml(` - - - - - - - - - `); - }); - - it('renders in disabled form-field', async () => { - const page = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerToggle], - html: ` - - - - - - `, - }); - const element: HTMLSbbDatepickerToggleElement = - page.doc.querySelector('sbb-datepicker-toggle'); - expect(element).toEqualHtml(` - - - - - - - - - `); - }); - - it('renders in form-field with calendar parameters', async () => { - const page = await newSpecPage({ - components: [SbbFormField, SbbDatepicker, SbbDatepickerToggle], - html: ` - - - - - - `, - }); - const element: HTMLSbbDatepickerToggleElement = - page.doc.querySelector('sbb-datepicker-toggle'); - expect(element).toEqualHtml(` - - - - - - - - - `); - }); - }); -}); diff --git a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.tsx b/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.tsx deleted file mode 100644 index 64a37e6344..0000000000 --- a/src/components/sbb-datepicker-toggle/sbb-datepicker-toggle.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - h, - Host, - JSX, - Method, - Prop, - State, - Watch, -} from '@stencil/core'; -import { SbbCalendarCustomEvent } from '../../components'; -import { i18nShowCalendar } from '../../global/i18n'; -import { - datepickerControlRegisteredEvent, - getDatePicker, - InputUpdateEvent, -} from '../sbb-datepicker/sbb-datepicker.helper'; -import { - documentLanguage, - HandlerRepository, - languageChangeHandlerAspect, -} from '../../global/eventing'; -import { sbbInputModalityDetector } from '../../global/a11y'; -import { isValidAttribute } from '../../global/dom'; - -@Component({ - shadow: true, - styleUrl: 'sbb-datepicker-toggle.scss', - tag: 'sbb-datepicker-toggle', -}) -export class SbbDatepickerToggle implements ComponentInterface { - /** Datepicker reference. */ - @Prop() public datePicker?: string | HTMLElement; - - /** Whether the animation is disabled. */ - @Prop() public disableAnimation = false; - - /** Negative coloring variant flag. */ - @Prop({ reflect: true, mutable: true }) public negative = false; - - @Element() private _element!: HTMLSbbDatepickerToggleElement; - - @State() private _triggerElement: HTMLElement; - - @State() private _disabled = false; - - @State() private _min: string | number; - - @State() private _max: string | number; - - @State() private _currentLanguage = documentLanguage(); - - private _datePickerElement: HTMLSbbDatepickerElement; - - private _calendarElement: HTMLSbbCalendarElement; - - private _datePickerController: AbortController; - - private _handlerRepository = new HandlerRepository( - this._element, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - ); - - @Watch('datePicker') - public async findDatePicker( - newValue: string | HTMLElement, - oldValue: string | HTMLElement, - ): Promise { - if (newValue !== oldValue) { - await this._init(this.datePicker); - } - } - - /** - * Opens the calendar. - */ - @Method() - public async open(): Promise { - if (!this._triggerElement) { - this._triggerElement = this._element.shadowRoot.querySelector('sbb-tooltip-trigger'); - } - this._triggerElement.click(); - } - - public async connectedCallback(): Promise { - this._handlerRepository.connect(); - await this._init(this.datePicker); - - const formField = - this._element.closest('sbb-form-field') ?? this._element.closest('[data-form-field]'); - if (formField) { - this.negative = isValidAttribute(formField, 'negative'); - } - } - - public disconnectedCallback(): void { - this._datePickerController?.abort(); - this._handlerRepository.disconnect(); - } - - private async _init(datePicker?: string | HTMLElement): Promise { - this._datePickerController?.abort(); - this._datePickerController = new AbortController(); - this._datePickerElement = getDatePicker(this._element, datePicker); - if (!this._datePickerElement) { - return; - } - - this._datePickerElement?.addEventListener( - 'inputUpdated', - (event: CustomEvent) => { - this._datePickerElement = event.target as HTMLSbbDatepickerElement; - this._disabled = event.detail.disabled || event.detail.readonly; - this._min = event.detail.min; - this._max = event.detail.max; - }, - { signal: this._datePickerController.signal }, - ); - this._datePickerElement?.addEventListener( - 'change', - (event: Event) => this._datePickerChanged(event), - { - signal: this._datePickerController.signal, - }, - ); - this._datePickerElement?.addEventListener( - 'datePickerUpdated', - (event: Event) => - this._configureCalendar(this._calendarElement, event.target as HTMLSbbDatepickerElement), - { signal: this._datePickerController.signal }, - ); - this._datePickerElement.dispatchEvent(datepickerControlRegisteredEvent); - } - - private _configureCalendar( - calendar: HTMLSbbCalendarElement, - datepicker: HTMLSbbDatepickerElement, - ): void { - calendar.wide = datepicker?.wide; - calendar.dateFilter = datepicker?.dateFilter; - } - - private async _datePickerChanged(event: Event): Promise { - this._datePickerElement = event.target as HTMLSbbDatepickerElement; - this._calendarElement.selectedDate = await this._datePickerElement.getValueAsDate(); - } - - private async _assignCalendar(calendar: HTMLSbbCalendarElement): Promise { - if (this._calendarElement && this._calendarElement === calendar) { - return; - } - this._calendarElement = calendar; - if (!this._datePickerElement || !this._calendarElement.resetPosition) { - return; - } - this._calendarElement.selectedDate = await this._datePickerElement.getValueAsDate(); - this._configureCalendar(this._calendarElement, this._datePickerElement); - await this._calendarElement.resetPosition(); - } - - private _hasDataNow(): boolean { - if (!this._datePickerElement) { - return false; - } - const dataNow = +this._datePickerElement.dataset?.now; - return !isNaN(dataNow); - } - - private _now(): Date { - if (this._hasDataNow()) { - const today = new Date(+this._datePickerElement.dataset?.now); - today.setHours(0, 0, 0, 0); - return today; - } - return undefined; - } - - public render(): JSX.Element { - return ( - - { - this._triggerElement = trigger; - }} - data-icon-small - /> - this._calendarElement.resetPosition()} - onDid-open={() => { - sbbInputModalityDetector.mostRecentModality === 'keyboard' && - this._calendarElement.focus(); - }} - trigger={this._triggerElement} - disableAnimation={this.disableAnimation} - hide-close-button={true} - > - this._assignCalendar(calendar)} - min={this._min} - max={this._max} - wide={this._datePickerElement?.wide} - dateFilter={this._datePickerElement?.dateFilter} - onDate-selected={async (d: SbbCalendarCustomEvent) => { - const newDate = new Date(d.detail); - this._calendarElement.selectedDate = newDate; - await this._datePickerElement.setValueAsDate(newDate); - }} - /> - - - ); - } -} diff --git a/src/components/sbb-datepicker/sbb-datepicker.e2e.ts b/src/components/sbb-datepicker/sbb-datepicker.e2e.ts deleted file mode 100644 index 9c0612abc8..0000000000 --- a/src/components/sbb-datepicker/sbb-datepicker.e2e.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; -import { i18nDateChangedTo } from '../../global/i18n'; - -describe('sbb-datepicker', () => { - it('renders', async () => { - const page: E2EPage = await newE2EPage({ html: '' }); - const element: E2EElement = await page.find('sbb-datepicker'); - expect(element).toHaveClass('hydrated'); - }); - - it('renders and formats date', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - `, - }); - - const input: E2EElement = await page.find('input'); - - expect(await input.getProperty('value')).toEqual('Su, 01.01.2023'); - }); - - it('renders and interprets iso string date', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - `, - }); - - const input: E2EElement = await page.find('input'); - - expect(await input.getProperty('value')).toEqual('Mo, 20.12.2021'); - }); - - it('renders and interprets timestamp', async () => { - const page: E2EPage = await newE2EPage({ - html: ` - - - `, - }); - - const input: E2EElement = await page.find('input'); - - expect(await input.getProperty('value')).toEqual('Su, 12.07.2020'); - }); - - const commonBehaviorTest: (template: string) => void = (template: string) => { - let element: E2EElement, input: E2EElement, button: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(template); - element = await page.find('sbb-datepicker'); - input = await page.find('input'); - button = await page.find('button'); - await page.waitForChanges(); - }); - - it('renders and emit event on value change', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20/01/2023'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2023'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and interpret two digit year correctly in 2000s', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20/01/12'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(await input.getProperty('value')).toEqual('Fr, 20.01.2012'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and interpret two digit year correctly in 1900s', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20/01/99'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(await input.getProperty('value')).toEqual('We, 20.01.1999'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and detects missing month error', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20..2012'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(input).toHaveAttribute('data-sbb-invalid'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and detects missing year error', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20.05.'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(input).toHaveAttribute('data-sbb-invalid'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and detects invalid month error', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('20.00.2012'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(input).toHaveAttribute('data-sbb-invalid'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders and detects invalid day error', async () => { - const changeSpy = await element.spyOnEvent('change'); - await input.type('00.05.2020'); - await button.focus(); - await waitForCondition(() => changeSpy.events.length === 1); - expect(input).toHaveAttribute('data-sbb-invalid'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('renders with errors when typing letters', async () => { - expect(await input.getProperty('value')).toEqual(''); - await input.focus(); - await input.type('invalid'); - await input.press('Enter'); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('invalid'); - expect(input).toHaveAttribute('data-sbb-invalid'); - }); - - it('renders and emits event when input parameter changes', async () => { - const datePickerUpdatedSpy = await page.spyOnEvent('datePickerUpdated'); - const picker = await page.find('sbb-datepicker'); - picker.setProperty('wide', true); - await page.waitForChanges(); - await waitForCondition(() => datePickerUpdatedSpy.events.length === 1); - expect(datePickerUpdatedSpy).toHaveReceivedEventTimes(1); - picker.setProperty('dateFilter', () => null); - await page.waitForChanges(); - await waitForCondition(() => datePickerUpdatedSpy.events.length === 2); - expect(datePickerUpdatedSpy).toHaveReceivedEventTimes(2); - }); - - it('renders and interprets date with custom parse and format functions', async () => { - const changeSpy = await element.spyOnEvent('change'); - - await page.evaluate(() => { - const localDatepicker = document.querySelector('sbb-datepicker'); - localDatepicker.dateParser = (s) => { - s = s.replace(/\D/g, ' ').trim(); - const date = s.split(' '); - return new Date(new Date().getFullYear(), +date[1] - 1, +date[0]); - }; - localDatepicker.format = (d) => { - //Intl.DateTimeFormat API is not available in test environment. - const weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; - const weekday = weekdays[d.getDay()]; - const date = `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart( - 2, - '0', - )}`; - return `${weekday}, ${date}`; - }; - }); - - await page.waitForChanges(); - await input.type('7.8'); - await input.press('Enter'); - await waitForCondition(() => changeSpy.events.length === 1); - await page.waitForChanges(); - expect(await input.getProperty('value')).toEqual('Mo, 07.08'); - expect(changeSpy).toHaveReceivedEventTimes(1); - }); - - it('should emit validation change event', async () => { - let validationChangeSpy = await element.spyOnEvent('validationChange'); - - // When entering 99 - await input.focus(); - await input.type('20'); - await input.press('Tab'); - - // Then validation event should emit with false - expect(validationChangeSpy).toHaveFirstReceivedEventDetail({ valid: false }); - expect(input).toHaveAttribute('data-sbb-invalid'); - - // When adding valid date - await input.focus(); - await input.press('.'); - await input.press('Tab'); - - // Then validation event should not be emitted a second time - expect(validationChangeSpy).toHaveReceivedEventTimes(1); - expect(input).toHaveAttribute('data-sbb-invalid'); - - // Reset event spy - validationChangeSpy = await element.spyOnEvent('validationChange'); - - // When adding missing parts of a valid date - await input.focus(); - await input.type('8.23'); - await input.press('Tab'); - - // Then validation event should be emitted with true - expect(validationChangeSpy).toHaveFirstReceivedEventDetail({ valid: true }); - expect(input).not.toHaveAttribute('data-sbb-invalid'); - }); - - it('should interpret valid values and set accessibility labels', async () => { - const testCases = [ - { - value: '5.5.0', - interpretedAs: 'Fr, 05.05.2000', - accessibilityValue: 'Friday, 05.05.2000', - }, - { - value: '8.2.98', - interpretedAs: 'Su, 08.02.1998', - accessibilityValue: 'Sunday, 08.02.1998', - }, - { - value: '31-12-2020', - interpretedAs: 'Th, 31.12.2020', - accessibilityValue: 'Thursday, 31.12.2020', - }, - { - value: '5 5 21', - interpretedAs: 'We, 05.05.2021', - accessibilityValue: 'Wednesday, 05.05.2021', - }, - { - value: '3/7/26', - interpretedAs: 'Fr, 03.07.2026', - accessibilityValue: 'Friday, 03.07.2026', - }, - { - value: '1.12.2019', - interpretedAs: 'Su, 01.12.2019', - accessibilityValue: 'Sunday, 01.12.2019', - }, - { - value: '6\\1\\2020', - interpretedAs: 'Mo, 06.01.2020', - accessibilityValue: 'Monday, 06.01.2020', - }, - { - value: '5,5,2012', - interpretedAs: 'Sa, 05.05.2012', - accessibilityValue: 'Saturday, 05.05.2012', - }, - ]; - - for (const testCase of testCases) { - // Clear input - await page.evaluate( - () => ((document.getElementById('datepicker-input') as HTMLInputElement).value = ''), - ); - - await input.type(testCase.value); - await input.press('Tab'); - expect(await input.getProperty('value')).toEqual(testCase.interpretedAs); - const paragraphElement = await page.find('sbb-datepicker >>> p'); - expect(paragraphElement.innerText).toBe( - `${i18nDateChangedTo['en']} ${testCase.accessibilityValue}`, - ); - } - }); - - it('should not touch invalid values', async () => { - const testCases = [ - { value: '.12.2020', interpretedAs: '.12.2020' }, - { value: '24..1995', interpretedAs: '24..1995' }, - { value: '24.12.', interpretedAs: '24.12.' }, - { value: '34.06.2020', interpretedAs: '34.06.2020' }, - { value: '24.15.2014', interpretedAs: '24.15.2014' }, - { value: 'invalid', interpretedAs: 'invalid' }, - ]; - - for (const testCase of testCases) { - // Clear input - await page.evaluate( - () => ((document.getElementById('datepicker-input') as HTMLInputElement).value = ''), - ); - - await input.type(testCase.value); - await input.press('Tab'); - expect(await input.getProperty('value')).toEqual(testCase.interpretedAs); - const paragraphElement = await page.find('sbb-datepicker >>> p'); - expect(paragraphElement.innerText).toBe(''); - } - }); - }; - - describe('with input', () => { - const template = ` - - - - `; - - it('renders', async () => { - const page: E2EPage = await newE2EPage(); - await page.setContent(template); - expect(await page.find('sbb-datepicker')).toHaveClass('hydrated'); - expect(await page.find('input')).toEqualHtml( - '', - ); - }); - - commonBehaviorTest(template); - }); - - describe('with form-field', () => { - const template = ` - - - - - - `; - - it('renders', async () => { - const page: E2EPage = await newE2EPage(); - await page.setContent(template); - expect(await page.find('sbb-datepicker')).toHaveClass('hydrated'); - expect(await page.find('input')).toEqualHtml( - '', - ); - }); - - commonBehaviorTest(template); - }); -}); diff --git a/src/components/sbb-datepicker/sbb-datepicker.events.ts b/src/components/sbb-datepicker/sbb-datepicker.events.ts deleted file mode 100644 index c8b90acd32..0000000000 --- a/src/components/sbb-datepicker/sbb-datepicker.events.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - change: 'change', - datePickerUpdated: 'datePickerUpdated', - didChange: 'didChange', - inputUpdated: 'inputUpdated', - validationChange: 'validationChange', -}; diff --git a/src/components/sbb-datepicker/sbb-datepicker.helper.ts b/src/components/sbb-datepicker/sbb-datepicker.helper.ts deleted file mode 100644 index 0c7b13c482..0000000000 --- a/src/components/sbb-datepicker/sbb-datepicker.helper.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { DateAdapter, NativeDateAdapter } from '../../global/datetime'; -import { findReferencedElement } from '../../global/dom'; - -export interface InputUpdateEvent { - disabled: boolean; - readonly: boolean; - min: string | number; - max: string | number; -} - -/** - * Given a SbbDatepickerPreviousDay, a SbbDatepickerNextDay or a SbbDatepickerToggle component, - * it returns the related SbbDatepicker reference, if exists. - * @param element The element potentially connected to the SbbDatepicker. - * @param trigger The id or the reference of the SbbDatePicker. - */ -export function getDatePicker( - element: - | HTMLSbbDatepickerPreviousDayElement - | HTMLSbbDatepickerNextDayElement - | HTMLSbbDatepickerToggleElement, - trigger?: string | HTMLElement, -): HTMLSbbDatepickerElement { - if (!trigger) { - const parent = element.closest('sbb-form-field'); - return parent?.querySelector('sbb-datepicker') as HTMLSbbDatepickerElement; - } - - return findReferencedElement(trigger); -} - -/** - * Returns the first available date before or after a given one, considering the SbbDatepicker `dateFilter` property. - * @param date The starting date for calculations. - * @param delta The number of days to add/subtract from the starting one. - * @param dateFilter The dateFilter function from the SbbDatepicker. - * @param dateAdapter The adapter class. - */ -export function getAvailableDate( - date: Date, - delta: number, - dateFilter: (date: Date) => boolean, - dateAdapter: DateAdapter, -): Date { - let availableDate = dateAdapter.addCalendarDays(date, delta); - - if (dateFilter) { - while (!dateFilter(availableDate)) { - availableDate = dateAdapter.addCalendarDays(availableDate, delta); - } - } - - return availableDate; -} - -/** - * Calculates the first available date before the given one, - * considering the SbbDatepicker `dateFilter` property and `min` parameter (e.g. from the self-named input's attribute). - * @param date The starting date for calculations. - * @param dateFilter The dateFilter function from the SbbDatepicker. - * @param dateAdapter The adapter class. - * @param min The minimum value to consider in calculations. - */ -export function findPreviousAvailableDate( - date: Date, - dateFilter: (date: Date) => boolean, - dateAdapter: DateAdapter, - min: string | number, -): Date { - const previousDate = getAvailableDate(date, -1, dateFilter, dateAdapter); - const dateMin: Date = dateAdapter.deserializeDate(min); - - if ( - !dateMin || - (dateAdapter.isValid(dateMin) && dateAdapter.compareDate(previousDate, dateMin) >= 0) - ) { - return previousDate; - } - return date; -} - -/** - * Calculates the first available date after the given one, - * considering the SbbDatepicker `dateFilter` property and `max` parameter (e.g. from the self-named input's attribute). - * @param date The starting date for calculations. - * @param dateFilter The dateFilter function from the SbbDatepicker. - * @param dateAdapter The adapter class. - * @param max The maximum value to consider in calculations. - */ -export function findNextAvailableDate( - date: Date, - dateFilter: (date: Date) => boolean, - dateAdapter: DateAdapter, - max: string | number, -): Date { - const nextDate = getAvailableDate(date, 1, dateFilter, dateAdapter); - const dateMax: Date = dateAdapter.deserializeDate(max); - - if ( - !dateMax || - (dateAdapter.isValid(dateMax) && dateAdapter.compareDate(nextDate, dateMax) <= 0) - ) { - return nextDate; - } - return date; -} - -/** - * Checks if the provided date is a valid one, considering the SbbDatepicker `dateFilter` property - * and `min` and `max` parameters (e.g. from the self-named input's attributes). - * @param date The starting date for calculations. - * @param dateFilter The dateFilter function from the SbbDatepicker. - * @param min The minimum value to consider in calculations. - * @param max The maximum value to consider in calculations. - */ -export function isDateAvailable( - date: Date, - dateFilter: (date: Date) => boolean, - min: string | number, - max: string | number, -): boolean { - const dateAdapter: DateAdapter = new NativeDateAdapter(); - const dateMin: Date = dateAdapter.deserializeDate(min); - const dateMax: Date = dateAdapter.deserializeDate(max); - - if ( - (dateAdapter.isValid(dateMin) && dateAdapter.compareDate(date, dateMin) < 0) || - (dateAdapter.isValid(dateMax) && dateAdapter.compareDate(date, dateMax) > 0) - ) { - return false; - } - - return dateFilter ? dateFilter(date) : true; -} - -export const datepickerControlRegisteredEvent = new CustomEvent('datepicker-control-registered', { - bubbles: false, - composed: true, -}); diff --git a/src/components/sbb-datepicker/sbb-datepicker.spec.ts b/src/components/sbb-datepicker/sbb-datepicker.spec.ts deleted file mode 100644 index 29410caef1..0000000000 --- a/src/components/sbb-datepicker/sbb-datepicker.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SbbDatepicker } from './sbb-datepicker'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-datepicker', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbDatepicker], - html: '', - }); - - expect(root).toEqualHtml(` - - -

    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-datepicker/sbb-datepicker.tsx b/src/components/sbb-datepicker/sbb-datepicker.tsx deleted file mode 100644 index d974b045a0..0000000000 --- a/src/components/sbb-datepicker/sbb-datepicker.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - Event, - EventEmitter, - h, - JSX, - Listen, - Method, - Prop, - State, - Watch, -} from '@stencil/core'; -import { i18nDatePickerPlaceholder, i18nDateChangedTo } from '../../global/i18n'; -import { InputUpdateEvent, isDateAvailable } from './sbb-datepicker.helper'; -import { DateAdapter } from '../../global/datetime'; -import { findInput, isValidAttribute, toggleDatasetEntry } from '../../global/dom'; -import { - documentLanguage, - HandlerRepository, - languageChangeHandlerAspect, -} from '../../global/eventing'; -import { AgnosticMutationObserver } from '../../global/observers'; -import { readConfig } from '../../global/config'; -import { ValidationChangeEvent } from '../../global/interfaces'; - -const FORMAT_DATE = - /(^0?[1-9]?|[12]?[0-9]?|3?[01]?)[.,\\/\-\s](0?[1-9]?|1?[0-2]?)?[.,\\/\-\s](\d{1,4}$)?/; - -@Component({ - shadow: true, - styleUrl: 'sbb-datepicker.scss', - tag: 'sbb-datepicker', -}) -export class SbbDatepicker implements ComponentInterface { - /** If set to true, two months are displayed */ - @Prop() public wide = false; - - /** A function used to filter out dates. */ - @Prop() public dateFilter: (date: Date | null) => boolean = () => true; - - /** A function used to parse string value into dates. */ - @Prop() public dateParser?: (value: string) => Date | undefined; - - /** A function used to format dates into the preferred string format. */ - @Prop() public format?: (date: Date) => string; - - /** Reference of the native input connected to the datepicker. */ - @Prop() public input?: string | HTMLElement; - - /** Host element */ - @Element() private _element!: HTMLSbbDatepickerElement; - - /** - * @deprecated only used for React. Will probably be removed once React 19 is available. - */ - @Event({ bubbles: true, cancelable: true }) public didChange: EventEmitter; - - @Event({ bubbles: true }) public change: EventEmitter; - - /** Notifies that the attributes of the input connected to the datepicker have changes. */ - @Event({ bubbles: true, cancelable: true }) public inputUpdated: EventEmitter; - - /** Notifies that the attributes of the datepicker have changes. */ - @Event({ bubbles: true, cancelable: true }) public datePickerUpdated: EventEmitter; - - /** Emits whenever the internal validation state changes. */ - @Event() public validationChange: EventEmitter; - - @State() private _inputElement: HTMLInputElement | null; - - @State() private _currentLanguage = documentLanguage(); - - @Watch('input') - public findInput(newValue: string | HTMLElement, oldValue: string | HTMLElement): void { - if (newValue !== oldValue) { - this._inputElement = findInput(this._element, this.input); - } - } - - @Watch('wide') - @Watch('dateFilter') - public datepickerPropChanged(newValue: any, oldValue: any): void { - if (newValue !== oldValue) { - this.datePickerUpdated.emit(); - } - } - - @Watch('_inputElement') - public registerInputElement(newValue: HTMLInputElement, oldValue: HTMLInputElement): void { - if (newValue !== oldValue) { - this._datePickerController?.abort(); - this._datePickerController = new AbortController(); - - if (!this._inputElement) { - return; - } - - this._inputObserver?.disconnect(); - this._inputObserver.observe(this._inputElement, { - attributeFilter: ['disabled', 'readonly', 'min', 'max', 'value'], - }); - - this._inputElement.type = 'text'; - - if (!this._inputElement.placeholder) { - this._inputElement.placeholder = i18nDatePickerPlaceholder[this._currentLanguage]; - } - - this._inputElement.addEventListener( - 'change', - async (event: Event) => { - if (!(event instanceof CustomEvent)) { - await this._valueChanged(event); - } - }, - { - signal: this._datePickerController.signal, - }, - ); - } - } - - /** Gets the input value with the correct date format. */ - @Method() public async getValueAsDate(): Promise { - return this._parse(this._inputElement?.value); - } - - /** Set the input value to the correctly formatted value. */ - @Method() public async setValueAsDate(date: Date | number | string): Promise { - const parsedDate = date instanceof Date ? date : new Date(date); - await this._formatAndUpdateValue(this._inputElement.value, parsedDate); - /* Emit blur event when value is changed programmatically to notify - frameworks that rely on that event to update form status. */ - this._inputElement.dispatchEvent(new FocusEvent('blur', { composed: true })); - } - - @Listen('datepicker-control-registered') - private _onInputPropertiesChange(mutationsList?: MutationRecord[]): void { - this.inputUpdated.emit({ - disabled: this._inputElement?.disabled, - readonly: this._inputElement?.readOnly, - min: this._inputElement?.min, - max: this._inputElement?.max, - }); - - if (mutationsList && Array.from(mutationsList).some((e) => e.attributeName === 'value')) { - this._inputElement.value = this._getValidValue(this._inputElement?.getAttribute('value')); - } - } - - private _datePickerController: AbortController; - - private _inputObserver = new AgnosticMutationObserver(this._onInputPropertiesChange.bind(this)); - - private _dateAdapter: DateAdapter = readConfig().datetime.dateAdapter; - - private _statusContainer: HTMLParagraphElement | null; - - private _handlerRepository = new HandlerRepository( - this._element as HTMLElement, - languageChangeHandlerAspect(async (l) => { - this._currentLanguage = l; - if (this._inputElement) { - this._inputElement.placeholder = i18nDatePickerPlaceholder[this._currentLanguage]; - const valueAsDate = await this.getValueAsDate(); - this._inputElement.value = this._format(valueAsDate); - } - }), - ); - - public connectedCallback(): void { - this._handlerRepository.connect(); - this._inputElement = findInput(this._element, this.input); - if (this._inputElement) { - this._inputElement.value = this._getValidValue(this._inputElement.value); - } - } - - public disconnectedCallback(): void { - this._inputObserver?.disconnect(); - this._datePickerController?.abort(); - this._handlerRepository.disconnect(); - } - - private _parseAndFormatValue(value: string): string { - const d = this._parse(value); - return !this._dateAdapter.isValid(d) ? value : this._format(d); - } - - private _createAndComposeDate(value: string | number | Date): string { - const date = new Date(value); - return this._format(date); - } - - private async _valueChanged(event): Promise { - await this._formatAndUpdateValue(event.target.value, this._parse(event.target.value)); - } - - /** Applies the correct format to values and triggers event dispatch. */ - private _formatAndUpdateValue(value: string, valueAsDate: Date): void { - if (this._inputElement) { - this._inputElement.value = !this._dateAdapter.isValid(valueAsDate) - ? value - : this._format(valueAsDate); - - const isEmptyOrValid = - !value || - (!!valueAsDate && - isDateAvailable( - valueAsDate, - this._element.dateFilter, - this._inputElement?.min, - this._inputElement?.max, - )); - const wasValid = !isValidAttribute(this._inputElement, 'data-sbb-invalid'); - toggleDatasetEntry(this._inputElement, 'sbbInvalid', !isEmptyOrValid); - if (wasValid !== isEmptyOrValid) { - this.validationChange.emit({ valid: isEmptyOrValid }); - } - this._emitChange(valueAsDate); - } - } - - /** Emits the change event. */ - private _emitChange(date: Date): void { - this._setAriaLiveMessage(date); - - this.change.emit(); - this.didChange.emit(); - - if (this._inputElement) { - this._inputElement.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); - this._inputElement.dispatchEvent(new CustomEvent('change', { bubbles: true })); - } - } - - private _getValidValue(value: string): string { - if (!value) { - return ''; - } - - const match: RegExpMatchArray = value.match(FORMAT_DATE); - - if (match?.index === 0) { - return this._parseAndFormatValue(value); - } else if (Number.isInteger(+value)) { - return this._createAndComposeDate(+value); - } else if (this._dateAdapter.isValid(new Date(value))) { - return this._createAndComposeDate(value); - } - - return value; - } - - private _parse(value: string): Date | undefined { - return this.dateParser ? this.dateParser(value) : this._dateAdapter.parseDate(value); - } - - private _format(date: Date): string { - return this.format ? this.format(date) : this._dateAdapter.format(date); - } - - private _setAriaLiveMessage(date: Date): void { - const ariaLiveFormatter = new Intl.DateTimeFormat(`${this._currentLanguage}-CH`, { - weekday: 'long', - year: 'numeric', - month: 'numeric', - day: 'numeric', - }); - - this._statusContainer.innerText = date - ? `${i18nDateChangedTo[this._currentLanguage]} ${ariaLiveFormatter.format(date)}` - : ''; - } - - public render(): JSX.Element { - return

    (this._statusContainer = ref)}>

    ; - } -} diff --git a/src/components/sbb-dialog/readme.md b/src/components/sbb-dialog/readme.md deleted file mode 100644 index 74d2bb2fc8..0000000000 --- a/src/components/sbb-dialog/readme.md +++ /dev/null @@ -1,164 +0,0 @@ -The `sbb-dialog` component provides a way to present content on top of the app's content. -It offers the following features: - -- creates a backdrop for disabling interaction below the modal; -- disables scrolling of the page content while open; -- manages focus properly by setting it on the first focusable element; -- can have a header and a footer, both of which are optional; -- can host a [sbb-action-group](/docs/components-sbb-action-group--docs) component in the footer; -- has a close button, which is always visible; -- can display a back button next to the title; -- adds the appropriate ARIA roles automatically. - -```html - - Dialog content. - -``` - -## Slots - -The content is projected in an unnamed slot, while the dialog's title can be provided via the `titleContent` property or via slot `name="title"`. -It's also possible to display buttons in the component's footer using the `action-group` slot with the `sbb-action-group` component. - -**NOTE**: -- The component will automatically set size `m` on slotted `sbb-action-group`; -- If the title is not present, the footer will not be displayed even if provided; -- If the title is not present, the dialog will be displayed in fullscreen mode with the close button in the content section along with the back button -(if visible, see [next paragraph](#interaction)). - -```html - - Dialog content. - - - - - My dialog title - - Dialog content. - - Abort - Confirm - - -``` - -## Interactions - -In order to show the dialog, you need to call the `open(event?: PointerEvent)` method on the `sbb-dialog` component. -It is necessary to pass the event object to the `open()` method to allow the dialog to detect -whether it has been opened by click or keyboard, so that the focus can be better handled. - -```html - - - Dialog content. -
    ...
    -
    - - -``` - -To dismiss the dialog, you need to get a reference to the `sbb-dialog` element and call -the `close(result?: any, target?: HTMLElement)` method, which will close the dialog element and -emit a close event with an optional result as a payload. - -The component can also be dismissed by clicking on the close button, clicking on the backdrop, pressing the `Esc` key, -or, if an element within the `sbb-dialog` has the `sbb-dialog-close` attribute, by clicking on it. - -You can also set the property `titleBackButton` to display the back button in the title section -(or content section, if title is omitted) which will emit the event `request-back-action` when clicked. - -## Style - -It's possible to display the component in `negative` variant using the self-named property. - -The default `z-index` of the component is set to `1000`; to specify a custom stack order, the -`z-index` can be changed by defining the CSS variable `--sbb-dialog-z-index`. - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------------- | --------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------- | ----------- | -| `accessibilityBackLabel` | `accessibility-back-label` | This will be forwarded as aria-label to the back button element. | `string` | `undefined` | -| `accessibilityCloseLabel` | `accessibility-close-label` | This will be forwarded as aria-label to the close button element. | `string` | `undefined` | -| `accessibilityLabel` | `accessibility-label` | This will be forwarded as aria-label to the relevant nested element. | `string` | `undefined` | -| `backdropAction` | `backdrop-action` | Backdrop click action. | `"close" \| "none"` | `'close'` | -| `disableAnimation` | `disable-animation` | Whether the animation is enabled. | `boolean` | `false` | -| `negative` | `negative` | Negative coloring variant flag. | `boolean` | `false` | -| `titleBackButton` | `title-back-button` | Whether a back button is displayed next to the title. | `boolean` | `false` | -| `titleContent` | `title-content` | Dialog title. | `string` | `undefined` | -| `titleLevel` | `title-level` | Level of title, will be rendered as heading tag (e.g. h1). Defaults to level 1. | `"1" \| "2" \| "3" \| "4" \| "5" \| "6"` | `'1'` | - - -## Events - -| Event | Description | Type | -| --------------------- | -------------------------------------------------------- | ------------------- | -| `did-close` | Emits whenever the dialog is closed. | `CustomEvent` | -| `did-open` | Emits whenever the dialog is opened. | `CustomEvent` | -| `request-back-action` | Emits whenever the back button is clicked. | `CustomEvent` | -| `will-close` | Emits whenever the dialog begins the closing transition. | `CustomEvent` | -| `will-open` | Emits whenever the dialog starts the opening transition. | `CustomEvent` | - - -## Methods - -### `close(result?: any, target?: HTMLElement) => Promise` - -Closes the dialog element. - -#### Returns - -Type: `Promise` - - - -### `open() => Promise` - -Opens the dialog element. - -#### Returns - -Type: `Promise` - - - - -## Slots - -| Slot | Description | -| ---------------- | ------------------------------------------------------- | -| `"action-group"` | Use this slot to display an action group in the footer. | -| `"title"` | Use this slot to provide a title. | -| `"unnamed"` | Use this slot to provide the dialog content. | - - -## Dependencies - -### Depends on - -- [sbb-button](../sbb-button) -- [sbb-title](../sbb-title) - -### Graph -```mermaid -graph TD; - sbb-dialog --> sbb-button - sbb-dialog --> sbb-title - sbb-button --> sbb-icon - style sbb-dialog fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-dialog/sbb-dialog.e2e.ts b/src/components/sbb-dialog/sbb-dialog.e2e.ts deleted file mode 100644 index 8f2d669b09..0000000000 --- a/src/components/sbb-dialog/sbb-dialog.e2e.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; -import events from './sbb-dialog.events'; -import { waitForCondition } from '../../global/testing'; -import { i18nDialog } from '../../global/i18n'; - -describe('sbb-dialog', () => { - let element: E2EElement, ariaLiveRef: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setViewport({ width: 900, height: 600 }); - await page.setContent(` - - Dialog content. -
    Action group
    -
    - `); - element = await page.find('sbb-dialog'); - ariaLiveRef = await page.find('sbb-dialog >>> span.sbb-screen-reader-only'); - await page.waitForChanges(); - }); - - it('renders', () => { - expect(element).toHaveClass('hydrated'); - }); - - it('opens the dialog', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - }); - - it('closes the dialog', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - expect(ariaLiveRef.innerText.trim()).toBe(`${i18nDialog.en}, Title`); - - await element.callMethod('close'); - await page.waitForChanges(); - - await waitForCondition(() => willClose.events.length === 1); - expect(willClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didClose.events.length === 1); - expect(didClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'closed'); - expect(ariaLiveRef.innerText).toBe(''); - }); - - it('closes the dialog on backdrop click', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - // Simulate backdrop click - await page.mouse.click(1, 1); - await page.waitForChanges(); - - await waitForCondition(() => willClose.events.length === 1); - expect(willClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didClose.events.length === 1); - expect(didClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'closed'); - }); - - it('does not close the dialog on backdrop click', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - - await element.setProperty('backdropAction', 'none'); - await page.waitForChanges(); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - // Simulate backdrop click - await page.mouse.click(1, 1); - await page.waitForChanges(); - - expect(willClose).toHaveReceivedEventTimes(0); - await page.waitForChanges(); - - expect(didClose).toHaveReceivedEventTimes(0); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - }); - - it('closes the dialog on close button click', async () => { - const closeButton = await page.find('sbb-dialog >>> [sbb-dialog-close]'); - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - closeButton.triggerEvent('click'); - await page.waitForChanges(); - - await waitForCondition(() => willClose.events.length === 1); - expect(willClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didClose.events.length === 1); - expect(didClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'closed'); - }); - - it('closes the dialog on Esc key press', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - await page.keyboard.down('Tab'); - await page.waitForChanges(); - - await page.keyboard.down('Escape'); - await page.waitForChanges(); - - await waitForCondition(() => willClose.events.length === 1); - expect(willClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didClose.events.length === 1); - expect(didClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'closed'); - }); - - it('does not have the fullscreen attribute', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - await page.waitForChanges(); - expect(element).not.toHaveAttribute('data-fullscreen'); - }); - - it('renders in fullscreen mode if no title is provided', async () => { - page = await newE2EPage(); - await page.setContent(` - - Dialog content. -
    Action group
    -
    - `); - element = await page.find('sbb-dialog'); - ariaLiveRef = await page.find('sbb-dialog >>> span.sbb-screen-reader-only'); - - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - await page.waitForChanges(); - expect(element).toHaveAttribute('data-fullscreen'); - expect(ariaLiveRef.innerText.trim()).toBe(`${i18nDialog.en}`); - }); - - it('closes stacked dialogs one by one on ESC key pressed', async () => { - page = await newE2EPage(); - await page.setContent(` - - Dialog content. -
    Action group
    -
    - - - Stacked dialog. - - `); - element = await page.find('sbb-dialog'); - - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - await page.waitForChanges(); - - const stackedDialog = await page.find('#stacked-dialog'); - - await stackedDialog.callMethod('open'); - await page.waitForChanges(); - - expect(stackedDialog).toEqualAttribute('data-state', 'opened'); - - await page.keyboard.down('Tab'); - await page.waitForChanges(); - - await page.keyboard.down('Escape'); - await page.waitForChanges(); - - expect(stackedDialog).toEqualAttribute('data-state', 'closed'); - expect(element).toEqualAttribute('data-state', 'opened'); - - await page.keyboard.down('Tab'); - await page.waitForChanges(); - - await page.keyboard.down('Escape'); - await page.waitForChanges(); - - expect(stackedDialog).toEqualAttribute('data-state', 'closed'); - expect(element).toEqualAttribute('data-state', 'closed'); - }); - - it('does not close the dialog on other overlay click', async () => { - page = await newE2EPage(); - await page.setViewport({ width: 900, height: 600 }); - await page.setContent(` - - Dialog content. -
    Action group
    - - Dialog content. -
    Action group
    -
    -
    - `); - element = await page.find('sbb-dialog'); - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - const willClose = await page.spyOnEvent(events.willClose); - const didClose = await page.spyOnEvent(events.didClose); - const innerElement = await page.find('sbb-dialog > sbb-dialog'); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - - await innerElement.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 2); - expect(willOpen).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 2); - expect(didOpen).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - - expect(innerElement).toEqualAttribute('data-state', 'opened'); - - // Simulate a click on the inner dialog's backdrop - await page.mouse.click(1, 1); - await page.waitForChanges(); - - await waitForCondition(() => willClose.events.length === 1); - expect(willClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didClose.events.length === 1); - expect(didClose).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(innerElement).toEqualAttribute('data-state', 'closed'); - expect(element).toEqualAttribute('data-state', 'opened'); - }); - - it('should remove ariaLiveRef content on any click interaction', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - expect(ariaLiveRef.innerText.trim()).toBe(`${i18nDialog.en}, Title`); - - await element.press('Tab'); - await page.waitForChanges(); - - expect(ariaLiveRef.innerText).toBe(''); - }); - - it('should remove ariaLiveRef content on any keyboard interaction', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - expect(ariaLiveRef.innerText.trim()).toBe(`${i18nDialog.en}, Title`); - - await element.click(); - await page.waitForChanges(); - - expect(ariaLiveRef.innerText).toBe(''); - }); - - it('should announce accessibility label in ariaLiveRef if explicitly set', async () => { - const willOpen = await page.spyOnEvent(events.willOpen); - const didOpen = await page.spyOnEvent(events.didOpen); - - await element.setProperty('accessibilityLabel', 'Special Dialog'); - await element.callMethod('open'); - await page.waitForChanges(); - - await waitForCondition(() => willOpen.events.length === 1); - expect(willOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - await waitForCondition(() => didOpen.events.length === 1); - expect(didOpen).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - - expect(element).toEqualAttribute('data-state', 'opened'); - expect(ariaLiveRef.innerText.trim()).toBe(`${i18nDialog.en}, Special Dialog`); - }); -}); diff --git a/src/components/sbb-dialog/sbb-dialog.events.ts b/src/components/sbb-dialog/sbb-dialog.events.ts deleted file mode 100644 index c9a2e450f4..0000000000 --- a/src/components/sbb-dialog/sbb-dialog.events.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - backClick: 'request-back-action', - didClose: 'did-close', - didOpen: 'did-open', - willClose: 'will-close', - willOpen: 'will-open', -}; diff --git a/src/components/sbb-dialog/sbb-dialog.spec.ts b/src/components/sbb-dialog/sbb-dialog.spec.ts deleted file mode 100644 index cb90d91c0a..0000000000 --- a/src/components/sbb-dialog/sbb-dialog.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { SbbDialog } from './sbb-dialog'; -import { newSpecPage } from '@stencil/core/testing'; -import { i18nCloseDialog } from '../../global/i18n'; - -describe('sbb-dialog', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbDialog], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - -
    -
    - `); - }); -}); diff --git a/src/components/sbb-divider/readme.md b/src/components/sbb-divider/readme.md deleted file mode 100644 index 84a6ca307e..0000000000 --- a/src/components/sbb-divider/readme.md +++ /dev/null @@ -1,51 +0,0 @@ -The `sbb-divider` is used to visually divide sections. - -## Style - -Based on the `orientation` property, the `sbb-divider` can be displayed vertically or horizontally. - -It's also possible to display the component in `negative` variant using the self-named property. - -```html - - - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------- | ------------- | --------------------------------------------------------------------------------------------- | ---------------------------- | -------------- | -| `negative` | `negative` | Negative coloring variant flag | `boolean` | `false` | -| `orientation` | `orientation` | Orientation property with possible values 'horizontal' \| 'vertical'. Defaults to horizontal. | `"horizontal" \| "vertical"` | `'horizontal'` | - - -## Dependencies - -### Used by - - - [sbb-alert](../sbb-alert) - - [sbb-journey-summary](../sbb-journey-summary) - - [sbb-navigation-section](../sbb-navigation-section) - - [sbb-notification](../sbb-notification) - - [sbb-optgroup](../sbb-optgroup) - - [sbb-selection-panel](../sbb-selection-panel) - -### Graph -```mermaid -graph TD; - sbb-alert --> sbb-divider - sbb-journey-summary --> sbb-divider - sbb-navigation-section --> sbb-divider - sbb-notification --> sbb-divider - sbb-optgroup --> sbb-divider - sbb-selection-panel --> sbb-divider - style sbb-divider fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-divider/sbb-divider.custom.d.ts b/src/components/sbb-divider/sbb-divider.custom.d.ts deleted file mode 100644 index 3e5326cc4e..0000000000 --- a/src/components/sbb-divider/sbb-divider.custom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface InterfaceSbbDividerAttributes { - orientation?: 'horizontal' | 'vertical'; - negative?: boolean; -} diff --git a/src/components/sbb-divider/sbb-divider.e2e.ts b/src/components/sbb-divider/sbb-divider.e2e.ts deleted file mode 100644 index 458c05af0c..0000000000 --- a/src/components/sbb-divider/sbb-divider.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { newE2EPage } from '@stencil/core/testing'; - -describe('sbb-divider', () => { - let element, page; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-divider'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-divider/sbb-divider.spec.ts b/src/components/sbb-divider/sbb-divider.spec.ts deleted file mode 100644 index dcf5404748..0000000000 --- a/src/components/sbb-divider/sbb-divider.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { SbbDivider } from './sbb-divider'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-divider', () => { - it('should render with default values', async () => { - const { root } = await newSpecPage({ - components: [SbbDivider], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    -
    - `); - }); - - it('should render with orientation horizontal', async () => { - const { root } = await newSpecPage({ - components: [SbbDivider], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    -
    - `); - }); - - it('should render with orientation vertical', async () => { - const { root } = await newSpecPage({ - components: [SbbDivider], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-divider/sbb-divider.tsx b/src/components/sbb-divider/sbb-divider.tsx deleted file mode 100644 index 3d4c813f00..0000000000 --- a/src/components/sbb-divider/sbb-divider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, h, Host, JSX, Prop } from '@stencil/core'; -import { InterfaceSbbDividerAttributes } from './sbb-divider.custom'; - -@Component({ - shadow: true, - styleUrl: 'sbb-divider.scss', - tag: 'sbb-divider', -}) -export class SbbDivider { - /** Negative coloring variant flag */ - @Prop({ reflect: true }) public negative?: boolean = false; - - /** Orientation property with possible values 'horizontal' | 'vertical'. Defaults to horizontal. */ - @Prop({ reflect: true }) public orientation?: InterfaceSbbDividerAttributes['orientation'] = - 'horizontal'; - - public render(): JSX.Element { - return ( - -
    -
    - ); - } -} diff --git a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.e2e.ts b/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.e2e.ts deleted file mode 100644 index 00bfab6416..0000000000 --- a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-expansion-panel-content', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent('Content'); - - element = await page.find('sbb-expansion-panel-content'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.spec.ts b/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.spec.ts deleted file mode 100644 index 7208d2e7f1..0000000000 --- a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { SbbExpansionPanelContent } from './sbb-expansion-panel-content'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-expansion-panel-content', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanelContent], - html: 'Content', - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    - Content -
    - `); - }); - - it('renders expanded', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanelContent], - html: 'Content', - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    - Content -
    - `); - }); -}); diff --git a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.tsx b/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.tsx deleted file mode 100644 index 0c6674e5b8..0000000000 --- a/src/components/sbb-expansion-panel-content/sbb-expansion-panel-content.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, ComponentInterface, h, Host, JSX } from '@stencil/core'; - -/** - * @slot unnamed - Slot to render the content in the sbb-expansion-panel. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-expansion-panel-content.scss', - tag: 'sbb-expansion-panel-content', -}) -export class SbbExpansionPanelContent implements ComponentInterface { - public render(): JSX.Element { - return ( - -
    - -
    -
    - ); - } -} diff --git a/src/components/sbb-expansion-panel-header/readme.md b/src/components/sbb-expansion-panel-header/readme.md deleted file mode 100644 index 9c4a060e41..0000000000 --- a/src/components/sbb-expansion-panel-header/readme.md +++ /dev/null @@ -1,77 +0,0 @@ -The `sbb-expansion-panel-header` is a component which is meant to be used as a header -in the [sbb-expansion-panel](/docs/components-sbb-accordion-sbb-expansion-panel--docs), -acting as a control for an expanding / collapsing content, like a native `` tag. - - -```html -Header -``` - -## Slots - -The component is internally rendered as a button, and it is possible to provide text via an unnamed slot. -On the left side, a toggle icon is displayed; it flips based on the host's `aria-expanded` property. - -The component can optionally display a `sbb-icon` at the component start using the `iconName` -property or via custom content using the `icon` slot. -If using the SBB icons, the icon should be a medium size icon. - -```html -Header -``` - -## States - -The component can be displayed in `disabled` state using the self-named property. - -```html -Header -``` - -## Events - -When the element is clicked, the `toggle-expanded` event is emitted. - - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `disabled` | `disabled` | Whether the button is disabled. | `boolean` | `undefined` | -| `iconName` | `icon-name` | 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. | `string` | `undefined` | - - -## Events - -| Event | Description | Type | -| ----------------- | ----------- | ------------------ | -| `toggle-expanded` | | `CustomEvent` | - - -## Slots - -| Slot | Description | -| ----------- | ------------------------------------------ | -| `"icon"` | Slot used to render the panel header icon. | -| `"unnamed"` | Slot used to render the panel header text. | - - -## Dependencies - -### Depends on - -- [sbb-icon](../sbb-icon) - -### Graph -```mermaid -graph TD; - sbb-expansion-panel-header --> sbb-icon - style sbb-expansion-panel-header fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.e2e.ts b/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.e2e.ts deleted file mode 100644 index 872fbb8a41..0000000000 --- a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.e2e.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-expansion-panel-header', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(`Header`); - element = await page.find('sbb-expansion-panel-header'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('should emit event on click', async () => { - const spy = await page.spyOnEvent('toggle-expanded'); - await element.click(); - expect(spy).toHaveReceivedEvent(); - }); - - it('should not emit event on click if disabled', async () => { - page = await newE2EPage(); - await page.setContent( - `Header`, - ); - element = await page.find('sbb-expansion-panel-header'); - const spy = await page.spyOnEvent('toggle-expanded'); - await element.click(); - expect(spy).not.toHaveReceivedEvent(); - }); -}); diff --git a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.events.ts b/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.events.ts deleted file mode 100644 index aeb450e7e3..0000000000 --- a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.events.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - toggleExpanded: 'toggle-expanded', -}; diff --git a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.spec.ts b/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.spec.ts deleted file mode 100644 index 3a5d7e4b25..0000000000 --- a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SbbExpansionPanelHeader } from './sbb-expansion-panel-header'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-expansion-panel-header', () => { - it('renders collapsed', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanelHeader], - html: 'Header', - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - Header - - `); - }); - - it('renders with icon', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanelHeader], - html: 'Header', - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - - - - - - Header - - `); - }); - - it('renders with slotted icon', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanelHeader], - html: ` - - - Header - - `, - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - - - - - - Header - - `); - }); -}); diff --git a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.tsx b/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.tsx deleted file mode 100644 index 9dfe313323..0000000000 --- a/src/components/sbb-expansion-panel-header/sbb-expansion-panel-header.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - Event, - EventEmitter, - h, - Host, - JSX, - Prop, - State, -} from '@stencil/core'; -import { - actionElementHandlerAspect, - createNamedSlotState, - HandlerRepository, - namedSlotChangeHandlerAspect, -} from '../../global/eventing'; -import { ButtonProperties, resolveButtonRenderVariables } from '../../global/interfaces'; -import { toggleDatasetEntry } from '../../global/dom'; - -/** - * @slot icon - Slot used to render the panel header icon. - * @slot unnamed - Slot used to render the panel header text. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-expansion-panel-header.scss', - tag: 'sbb-expansion-panel-header', -}) -export class SbbExpansionPanelHeader implements ButtonProperties, ComponentInterface { - /** - * 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. - */ - @Prop() public iconName?: string; - - /** Whether the button is disabled. */ - @Prop({ reflect: true }) public disabled: boolean; - - @Element() private _element!: HTMLElement; - - /** State of listed named slots, by indicating whether any element for a named slot is defined. */ - @State() private _namedSlots = createNamedSlotState('icon'); - - @Event({ - bubbles: true, - eventName: 'toggle-expanded', - }) - public toggleExpanded: EventEmitter; - - private _handlerRepository = new HandlerRepository( - this._element, - actionElementHandlerAspect, - namedSlotChangeHandlerAspect((m) => (this._namedSlots = m(this._namedSlots))), - ); - - public connectedCallback(): void { - this._handlerRepository.connect(); - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - } - - private _emitExpandedEvent(): void { - if (!this.disabled) { - this.toggleExpanded.emit(); - } - } - - private _onMouseMovement(toggleDataAttribute: boolean): void { - const parent: HTMLSbbExpansionPanelElement = this._element.closest('sbb-expansion-panel'); - // The `sbb.hover-mq` logic has been removed from scss, but it must be replicated to have the correct behavior on mobile. - if (!toggleDataAttribute || (parent && window.matchMedia('(any-hover: hover)').matches)) { - toggleDatasetEntry(parent, 'toggleHover', toggleDataAttribute); - } - } - - public render(): JSX.Element { - const { hostAttributes } = resolveButtonRenderVariables(this); - - return ( - this._emitExpandedEvent()} - onMouseenter={() => this._onMouseMovement(true)} - onMouseleave={() => this._onMouseMovement(false)} - > - - - {(this.iconName || this._namedSlots.icon) && ( - - {this.iconName && } - - )} - - - {!this.disabled && ( - - - - )} - - - ); - } -} diff --git a/src/components/sbb-expansion-panel/readme.md b/src/components/sbb-expansion-panel/readme.md deleted file mode 100644 index 604c840529..0000000000 --- a/src/components/sbb-expansion-panel/readme.md +++ /dev/null @@ -1,111 +0,0 @@ -The `sbb-expansion-panel` is a component which acts as an expandable summary-details widget. - -It can be used standalone or inside a [sbb-accordion](/docs/components-sbb-accordion-sbb-accordion--docs). - -## Slots - -In order to correctly display the component, it must be used together with -a [sbb-expansion-panel-header](/docs/components-sbb-accordion-sbb-expansion-panel-header--docs) -and a [sbb-expansion-panel-content](/docs/components-sbb-accordion-sbb-expansion-panel-content--docs); -the first will work as a state controller, the last will act as the expandable content. - -These two components automatically fill the two available slots, named `header` and `content`. - -```html - - This is the header. - This is the content. - -``` - -## States - -The visibility of the content is controlled by the value of the `expanded` property. - -```html - - ... - -``` - -The `disabled` state can be set using the self-named variable. In this state, the component can not be collapsed or expanded. - -```html - - ... - -``` - -## Style - -The component has two background options (`milk` and `white`, which is the default) that can be set using the `color` variable. - -```html - - ... - -``` - -It's also possible to display the `sbb-expansion-panel` without border by setting the `borderless` variable. - -```html - - ... - -``` - -Using the `titleLevel` variable, it's possible to wrap the `sbb-expansion-panel-header` in a heading tag; -if it's unset, a `
    ` is used as a wrapper. - -```html - - This is the header, and it will be wrapped in a h4 tag. - This is the content. - -``` - -## Accessibility - -When the `sbb-expansion-panel-header` and the `sbb-expansion-panel-content` are slotted into the component, -they both receive an `id`, if not set; then, the content's `id` is set as `aria-controls` attribute of the header, -and the header's `id` is set as `aria-labelledby` attribute on the content. - -The `expanded` attribute is used to correctly set the `aria-expanded` attribute on the header -and the `aria-hidden` attribute on the content. - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ------------------ | ------------------- | ---------------------------------------------------------------------- | ---------------------------------------- | ----------- | -| `borderless` | `borderless` | Whether the panel has no border. | `boolean` | `false` | -| `color` | `color` | The background color of the panel. | `"milk" \| "white"` | `'white'` | -| `disableAnimation` | `disable-animation` | Whether the animations should be disabled. | `boolean` | `false` | -| `disabled` | `disabled` | Whether the panel is disabled, so its expanded state can't be changed. | `boolean` | `false` | -| `expanded` | `expanded` | Whether the panel is expanded. | `boolean` | `false` | -| `titleLevel` | `title-level` | Heading level; if unset, a `div` will be rendered. | `"1" \| "2" \| "3" \| "4" \| "5" \| "6"` | `undefined` | - - -## Events - -| Event | Description | Type | -| ------------ | --------------------------------------------------------------------- | ------------------- | -| `did-close` | Emits whenever the sbb-expansion-panel is closed. | `CustomEvent` | -| `did-open` | Emits whenever the sbb-expansion-panel is opened. | `CustomEvent` | -| `will-close` | Emits whenever the sbb-expansion-panel begins the closing transition. | `CustomEvent` | -| `will-open` | Emits whenever the sbb-expansion-panel starts the opening transition. | `CustomEvent` | - - -## Slots - -| Slot | Description | -| ----------- | --------------------------------------------------- | -| `"content"` | Use this to render the sbb-expansion-panel-content. | -| `"header"` | Use this to render the sbb-expansion-panel-header. | - - ----------------------------------------------- - - diff --git a/src/components/sbb-expansion-panel/sbb-expansion-panel.custom.d.ts b/src/components/sbb-expansion-panel/sbb-expansion-panel.custom.d.ts deleted file mode 100644 index 1bb7111d65..0000000000 --- a/src/components/sbb-expansion-panel/sbb-expansion-panel.custom.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterfaceSbbExpansionPanelAttributes { - color: 'white' | 'milk'; -} diff --git a/src/components/sbb-expansion-panel/sbb-expansion-panel.e2e.ts b/src/components/sbb-expansion-panel/sbb-expansion-panel.e2e.ts deleted file mode 100644 index b09b50d91c..0000000000 --- a/src/components/sbb-expansion-panel/sbb-expansion-panel.e2e.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { E2EElement, E2EPage, EventSpy, newE2EPage } from '@stencil/core/testing'; -import { waitForCondition } from '../../global/testing'; -import sbbExpansionPanelHeaderEvents from '../sbb-expansion-panel-header/sbb-expansion-panel-header.events'; -import sbbExpansionPanelEvents from './sbb-expansion-panel.events'; - -describe('sbb-expansion-panel', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - Header - Content - - `); - - element = await page.find('sbb-expansion-panel'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('has slotted elements with the correct properties', async () => { - const header = await page.find('sbb-expansion-panel-header'); - expect(header).toEqualAttribute('id', 'sbb-expansion-panel-header-1'); - expect(header).toEqualAttribute('aria-controls', 'sbb-expansion-panel-content-1'); - expect(header).toEqualAttribute('data-icon', ''); - const content = await page.find('sbb-expansion-panel-content'); - expect(content).toEqualAttribute('id', 'sbb-expansion-panel-content-1'); - expect(content).toEqualAttribute('aria-labelledby', `sbb-expansion-panel-header-1`); - expect(content).toEqualAttribute('data-icon-space', ''); - }); - - it('has slotted elements with the correct properties when id are set', async () => { - page = await newE2EPage(); - await page.setContent(` - - Header - Content - - `); - - const header = await page.find('sbb-expansion-panel-header'); - expect(header).toEqualAttribute('aria-controls', 'content'); - const content = await page.find('sbb-expansion-panel-content'); - expect(content).toEqualAttribute('aria-labelledby', `header`); - }); - - it('click the header expands the panel, click again collapses it', async () => { - const header: E2EElement = await page.find('sbb-expansion-panel-header'); - const content: E2EElement = await page.find('sbb-expansion-panel-content'); - expect(await element.getProperty('expanded')).toEqual(false); - expect(header.getAttribute('aria-expanded')).toEqual('false'); - expect(content.getAttribute('aria-hidden')).toEqual('true'); - - const toggleExpandedEventSpy: EventSpy = await page.spyOnEvent( - sbbExpansionPanelHeaderEvents.toggleExpanded, - ); - const willOpenEventSpy: EventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.willOpen); - const willCloseEventSpy: EventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.willClose); - const didOpenEventSpy: EventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.didOpen); - const didCloseEventSpy: EventSpy = await page.spyOnEvent(sbbExpansionPanelEvents.didClose); - - await header.click(); - await waitForCondition(() => toggleExpandedEventSpy.events.length === 1); - expect(toggleExpandedEventSpy).toHaveReceivedEventTimes(1); - await page.waitForChanges(); - expect(await element.getProperty('expanded')).toEqual(true); - expect(header.getAttribute('aria-expanded')).toEqual('true'); - expect(content.getAttribute('aria-hidden')).toEqual('false'); - await waitForCondition(() => willOpenEventSpy.events.length === 1); - expect(willOpenEventSpy).toHaveReceivedEventTimes(1); - await waitForCondition(() => didOpenEventSpy.events.length === 1); - expect(didOpenEventSpy).toHaveReceivedEventTimes(1); - - await header.click(); - await waitForCondition(() => toggleExpandedEventSpy.events.length === 2); - expect(toggleExpandedEventSpy).toHaveReceivedEventTimes(2); - await page.waitForChanges(); - expect(await element.getProperty('expanded')).toEqual(false); - expect(header.getAttribute('aria-expanded')).toEqual('false'); - expect(content.getAttribute('aria-hidden')).toEqual('true'); - await waitForCondition(() => willCloseEventSpy.events.length === 1); - expect(willCloseEventSpy).toHaveReceivedEventTimes(1); - await waitForCondition(() => didCloseEventSpy.events.length === 1); - expect(didCloseEventSpy).toHaveReceivedEventTimes(1); - }); - - it('disabled property is proxied to header', async () => { - const header: E2EElement = await page.find('sbb-expansion-panel-header'); - expect(await header.getProperty('disabled')).toBeUndefined(); - expect(header).not.toHaveAttribute('aria-disabled'); - - element.setProperty('disabled', true); - await page.waitForChanges(); - expect(await header.getProperty('disabled')).toEqual(true); - expect(header).toEqualAttribute('aria-disabled', 'true'); - - element.setProperty('disabled', false); - await page.waitForChanges(); - expect(await header.getProperty('disabled')).toEqual(false); - expect(header).toEqualAttribute('aria-disabled', null); - }); -}); diff --git a/src/components/sbb-expansion-panel/sbb-expansion-panel.events.ts b/src/components/sbb-expansion-panel/sbb-expansion-panel.events.ts deleted file mode 100644 index cf7d67d8ae..0000000000 --- a/src/components/sbb-expansion-panel/sbb-expansion-panel.events.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - didClose: 'did-close', - didOpen: 'did-open', - willClose: 'will-close', - willOpen: 'will-open', -}; diff --git a/src/components/sbb-expansion-panel/sbb-expansion-panel.spec.ts b/src/components/sbb-expansion-panel/sbb-expansion-panel.spec.ts deleted file mode 100644 index 2b468f8762..0000000000 --- a/src/components/sbb-expansion-panel/sbb-expansion-panel.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { SbbExpansionPanel } from './sbb-expansion-panel'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-expansion-panel', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanel], - html: ` - - Header - Content - - `, - }); - - expect(root).toEqualHtml(` - - -
    -
    - -
    -
    - - - -
    -
    -
    - Header - Content -
    - `); - }); - - it('renders with level set', async () => { - const { root } = await newSpecPage({ - components: [SbbExpansionPanel], - html: ` - - Header - Content - - `, - }); - - expect(root).toEqualHtml(` - - -
    -

    - -

    -
    - - - -
    -
    -
    - Header - Content -
    - `); - }); -}); diff --git a/src/components/sbb-expansion-panel/sbb-expansion-panel.tsx b/src/components/sbb-expansion-panel/sbb-expansion-panel.tsx deleted file mode 100644 index b66ac849c1..0000000000 --- a/src/components/sbb-expansion-panel/sbb-expansion-panel.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - Event, - EventEmitter, - h, - JSX, - Listen, - Prop, - Watch, -} from '@stencil/core'; -import { InterfaceTitleAttributes } from '../sbb-title/sbb-title.custom'; -import { toggleDatasetEntry } from '../../global/dom'; -import { InterfaceSbbExpansionPanelAttributes } from './sbb-expansion-panel.custom'; - -let nextId = 0; - -/** - * @slot header - Use this to render the sbb-expansion-panel-header. - * @slot content - Use this to render the sbb-expansion-panel-content. - */ -@Component({ - shadow: true, - styleUrl: 'sbb-expansion-panel.scss', - tag: 'sbb-expansion-panel', -}) -export class SbbExpansionPanel implements ComponentInterface { - /** Heading level; if unset, a `div` will be rendered. */ - @Prop() public titleLevel?: InterfaceTitleAttributes['level']; - - /** The background color of the panel. */ - @Prop() public color: InterfaceSbbExpansionPanelAttributes['color'] = 'white'; - - /** Whether the panel is expanded. */ - @Prop({ mutable: true, reflect: true }) public expanded = false; - - /** Whether the panel is disabled, so its expanded state can't be changed. */ - @Prop({ reflect: true }) public disabled = false; - - /** Whether the panel has no border. */ - @Prop({ reflect: true }) public borderless = false; - - /** Whether the animations should be disabled. */ - @Prop({ reflect: true }) public disableAnimation = false; - - /** Emits whenever the sbb-expansion-panel starts the opening transition. */ - @Event({ - bubbles: true, - composed: true, - eventName: 'will-open', - }) - public willOpen: EventEmitter; - - /** Emits whenever the sbb-expansion-panel is opened. */ - @Event({ - bubbles: true, - composed: true, - eventName: 'did-open', - }) - public didOpen: EventEmitter; - - /** Emits whenever the sbb-expansion-panel begins the closing transition. */ - @Event({ - bubbles: true, - composed: true, - eventName: 'will-close', - }) - public willClose: EventEmitter; - - /** Emits whenever the sbb-expansion-panel is closed. */ - @Event({ - bubbles: true, - composed: true, - eventName: 'did-close', - }) - public didClose: EventEmitter; - - @Element() private _element!: HTMLSbbExpansionPanelElement; - - @Listen('toggle-expanded') - public toggleExpanded(): void { - this.expanded = !this.expanded; - } - - @Watch('expanded') - public onExpandedChange(): void { - this._headerRef.setAttribute('aria-expanded', String(this.expanded)); - this._contentRef.setAttribute('aria-hidden', String(!this.expanded)); - - if (this.expanded) { - this.willOpen.emit(); - // As with 0s duration, transitionEnd will not be fired, we need to programmatically trigger didOpen event - if (this.disableAnimation) { - this._onOpened(); - } - } else { - this.willClose.emit(); - // As with 0s duration, transitionEnd will not be fired, we need to programmatically trigger didClose event - if (this.disableAnimation) { - this._onClosed(); - } - } - } - - @Watch('disabled') - public updateDisabledOnHeader(newDisabledValue: boolean): void { - this._headerRef.disabled = newDisabledValue; - } - - private _transitionEventController: AbortController; - private _progressiveId = `-${++nextId}`; - private _headerRef: HTMLSbbExpansionPanelHeaderElement; - private _contentRef: HTMLSbbExpansionPanelContentElement; - - public connectedCallback(): void { - const accordion = this._element.closest('sbb-accordion'); - toggleDatasetEntry(this._element, 'accordion', !!accordion); - } - - public disconnectedCallback(): void { - this._transitionEventController?.abort(); - toggleDatasetEntry(this._element, 'accordion', false); - } - - private _onOpened(): void { - this.didOpen.emit(); - } - - private _onClosed(): void { - this.didClose.emit(); - } - - private _onHeaderSlotChange(event): void { - const elements = (event.target as HTMLSlotElement).assignedElements(); - - // Changing titleLevel sometimes triggers a slot change with no assigned elements. - if (!elements.length) { - return; - } - - this._headerRef = elements.find( - (e): e is HTMLSbbExpansionPanelHeaderElement => e.tagName === 'SBB-EXPANSION-PANEL-HEADER', - ); - - if (!this._headerRef) { - return; - } - - this._headerRef.setAttribute('aria-expanded', String(this.expanded)); - if (this.disabled) { - this._headerRef.setAttribute('disabled', String(this.disabled)); - } - this._linkHeaderAndContent(); - } - - private _onContentSlotChange(event): void { - const elements = (event.target as HTMLSlotElement).assignedElements(); - - if (!elements.length) { - return; - } - - this._transitionEventController?.abort(); - - this._contentRef = (event.target as HTMLSlotElement) - .assignedElements() - .find( - (e): e is HTMLSbbExpansionPanelContentElement => - e.tagName === 'SBB-EXPANSION-PANEL-CONTENT', - ); - - if (!this._contentRef) { - return; - } - - this._transitionEventController = new AbortController(); - this._contentRef.setAttribute('aria-hidden', String(!this.expanded)); - this._contentRef.addEventListener('transitionend', (event) => this._onTransitionEnd(event), { - signal: this._transitionEventController.signal, - }); - this._linkHeaderAndContent(); - } - - private _linkHeaderAndContent(): void { - if (!this._headerRef || !this._contentRef) { - return; - } - - if (!this._headerRef.id) { - this._headerRef.setAttribute('id', `sbb-expansion-panel-header${this._progressiveId}`); - } - this._headerRef.setAttribute( - 'aria-controls', - this._contentRef.id || `sbb-expansion-panel-content${this._progressiveId}`, - ); - - if (!this._contentRef.id) { - this._contentRef.setAttribute('id', `sbb-expansion-panel-content${this._progressiveId}`); - } - this._contentRef.setAttribute( - 'aria-labelledby', - this._headerRef.id || `sbb-expansion-panel-header${this._progressiveId}`, - ); - toggleDatasetEntry(this._contentRef, 'iconSpace', this._headerRef.hasAttribute('data-icon')); - } - - private _onTransitionEnd(event): void { - // All transitions have the same timing and opacity is defined last, be sure that they have all been performed. - if (event.propertyName !== 'opacity') { - return; - } - - if (this.expanded) { - this._onOpened(); - } else { - this._onClosed(); - } - } - - public render(): JSX.Element { - const TAGNAME = this.titleLevel ? `h${this.titleLevel}` : 'div'; - - return ( -
    - - this._onHeaderSlotChange(event)}> - -
    - - this._onContentSlotChange(event)}> - -
    -
    - ); - } -} diff --git a/src/components/sbb-file-selector/readme.md b/src/components/sbb-file-selector/readme.md deleted file mode 100644 index 1b05812bcd..0000000000 --- a/src/components/sbb-file-selector/readme.md +++ /dev/null @@ -1,141 +0,0 @@ -The `sbb-file-selector` is a component which allows user to select one or more files from storage devices. -When files are selected, they appear as a list below the button/dropzone area. -For each file, the name and the size are displayed and an icon allows for deletion. - -### Variants - -It has two different display options based on the value of the `variant` property: -by default, a `sbb-button` is displayed, which mimics the native ``. - -```html - -``` - -Instead, if the `variant` property is set to `dropzone`, the `sbb-button` is shown within a "drag & drop" area. -In this case, it's possible to customize the area's title via the `titleContent` property. - -```html - -``` - -### Multiple and multipleMode - -In both variants, a single file can be selected by default; this can be changed setting the `multiple` property to `true`. - -```html - -``` - -The value of the `multipleMode` property determines whether added files should overwrite existing files (`default`) or be appended to them (`persistent`). - -```html - -``` - -### Accept - -The `accept` property can be used to force the user to select one or more specific file types; -in the next example, only images are allowed. - -```html - -``` - -### Disabled - -User interaction can be disabled using the `disabled` property. - -```html - -``` - -### Error slot - -The `error` named slot can be used to display an error message using the `sbb-form-error` component. - -```html - - An error occurred during file upload. - -``` - -### Events - -Whenever the selection changes, a `file-changed` event is fired, whose `event.detail` property contains the list -of currently selected files. The list can also be retrieved using the `getFiles()` method. - - -## Accessibility - -It's possible to improve the component accessibility using the `accessibilityLabel` property; this will be set -as `aria-label` of the inner native input and read together with the visible button text. -It's suggested to have a different value for each variant, e.g.: - -```html - - -``` - - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| -------------------- | --------------------- | ------------------------------------------------------------------------ | --------------------------- | ----------- | -| `accept` | `accept` | A comma-separated list of allowed unique file type specifiers. | `string` | `undefined` | -| `accessibilityLabel` | `accessibility-label` | This will be forwarded as aria-label to the native input element. | `string` | `undefined` | -| `disabled` | `disabled` | Whether the component is disabled. | `boolean` | `undefined` | -| `multiple` | `multiple` | Whether more than one file can be selected. | `boolean` | `undefined` | -| `multipleMode` | `multiple-mode` | Whether the newly added files should override the previously added ones. | `"default" \| "persistent"` | `undefined` | -| `titleContent` | `title-content` | The title displayed in `dropzone` variant. | `string` | `undefined` | -| `variant` | `variant` | Whether the component has a dropzone area or not. | `"default" \| "dropzone"` | `'default'` | - - -## Events - -| Event | Description | Type | -| -------------- | ---------------------------------------------------------- | --------------------- | -| `file-changed` | An event which is emitted each time the file list changes. | `CustomEvent` | - - -## Methods - -### `getFiles() => Promise` - -Gets the currently selected files. - -#### Returns - -Type: `Promise` - - - - -## Slots - -| Slot | Description | -| --------- | ---------------------------------------------------------------- | -| `"error"` | Use this to provide a `sbb-form-error` to show an error message. | - - -## Dependencies - -### Depends on - -- [sbb-button](../sbb-button) -- [sbb-icon](../sbb-icon) - -### Graph -```mermaid -graph TD; - sbb-file-selector --> sbb-button - sbb-file-selector --> sbb-icon - sbb-button --> sbb-icon - style sbb-file-selector fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-file-selector/sbb-file-selector.custom.d.ts b/src/components/sbb-file-selector/sbb-file-selector.custom.d.ts deleted file mode 100644 index 82761f929b..0000000000 --- a/src/components/sbb-file-selector/sbb-file-selector.custom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface InterfaceSbbFileSelectorAttributes { - variant: 'default' | 'dropzone'; - multipleMode: 'default' | 'persistent'; -} diff --git a/src/components/sbb-file-selector/sbb-file-selector.e2e.ts b/src/components/sbb-file-selector/sbb-file-selector.e2e.ts deleted file mode 100644 index 4322de298b..0000000000 --- a/src/components/sbb-file-selector/sbb-file-selector.e2e.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { E2EElement, E2EPage, EventSpy, newE2EPage } from '@stencil/core/testing'; -import events from './sbb-file-selector.events'; - -async function addFilesToComponentInput(page: E2EPage, numberOfFiles: number): Promise { - await page.evaluate((numberOfFiles: number): void => { - const dataTransfer: DataTransfer = new DataTransfer(); - for (let i: number = 0; i < numberOfFiles; i++) { - dataTransfer.items.add( - new File([`Hello world - ${i}`], `hello${i}.txt`, { - type: 'text/plain', - lastModified: new Date(i).getMilliseconds(), - }), - ); - } - const input: HTMLInputElement = document - .querySelector('sbb-file-selector') - .shadowRoot.querySelector('input'); - input.files = dataTransfer.files; - input.dispatchEvent(new Event('change')); - }, numberOfFiles); -} - -describe('sbb-file-selector', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(''); - element = await page.find('sbb-file-selector'); - }); - - it('renders', () => { - expect(element).toHaveClass('hydrated'); - }); - - it('loads a file, then deletes it', async () => { - const fileChangedSpy: EventSpy = await page.spyOnEvent(events.fileChangedEvent); - await addFilesToComponentInput(page, 1); - await page.waitForChanges(); - - expect(fileChangedSpy).toHaveReceivedEventTimes(1); - expect(await element.getProperty('files')).not.toBeNull(); - - const listItems: E2EElement = await page.find( - 'sbb-file-selector >>> .sbb-file-selector__file-list', - ); - expect(listItems).toEqualHtml(` -
    - - - hello0.txt - 15 B - - - - -
    - `); - - const button: E2EElement = await page.find( - 'sbb-file-selector >>> sbb-button[icon-name="trash-small"]', - ); - expect(button).not.toBeNull(); - await button.click(); - await page.waitForChanges(); - expect(fileChangedSpy).toHaveReceivedEventTimes(2); - expect((await page.findAll('sbb-file-selector >>> .sbb-file-selector__file')).length).toEqual( - 0, - ); - }); - - it('loads more than one file in multiple mode', async () => { - const fileChangedSpy: EventSpy = await page.spyOnEvent(events.fileChangedEvent); - await element.setProperty('multiple', true); - await page.waitForChanges(); - await addFilesToComponentInput(page, 2); - await page.waitForChanges(); - expect(fileChangedSpy).toHaveReceivedEvent(); - - const listItems: E2EElement[] = await page.findAll('sbb-file-selector >>> li'); - expect(listItems.length).toEqual(2); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-details')).length, - ).toEqual(2); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name'))[0], - ).toEqualText('hello0.txt'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name'))[1], - ).toEqualText('hello1.txt'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-size'))[0], - ).toEqualText('15 B'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-size'))[1], - ).toEqualText('15 B'); - }); - - it('loads files in multiple persistent mode', async () => { - const fileChangedSpy: EventSpy = await page.spyOnEvent(events.fileChangedEvent); - await element.setProperty('multiple', true); - await element.setProperty('multipleMode', 'persistent'); - await page.waitForChanges(); - await addFilesToComponentInput(page, 1); - await page.waitForChanges(); - expect(fileChangedSpy).toHaveReceivedEventTimes(1); - - expect(await element.getProperty('files')).not.toBeNull(); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name')).length, - ).toEqual(1); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-details')).length, - ).toEqual(1); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name'))[0], - ).toEqualText('hello0.txt'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-size'))[0], - ).toEqualText('15 B'); - - await page.evaluate((longContent: string) => { - const dataTransfer: DataTransfer = new DataTransfer(); - dataTransfer.items.add( - new File([`Hello world - 0`], `hello0.txt`, { - type: 'text/plain', - lastModified: new Date(0).getMilliseconds(), - }), - ); - dataTransfer.items.add(new File([longContent], 'third.txt', { type: 'text/plain' })); - const input: HTMLInputElement = document - .querySelector('sbb-file-selector') - .shadowRoot.querySelector('input'); - input.files = dataTransfer.files; - input.dispatchEvent(new Event('change')); - }, 'Lorem ipsum dolor sit amet. '.repeat(100)); - await page.waitForChanges(); - expect(fileChangedSpy).toHaveReceivedEventTimes(2); - expect((await page.findAll('sbb-file-selector >>> li')).length).toEqual(2); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name'))[0], - ).toEqualText('third.txt'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-size'))[0], - ).toEqualText('3 kB'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-name'))[1], - ).toEqualText('hello0.txt'); - expect( - (await page.findAll('sbb-file-selector >>> .sbb-file-selector__file-size'))[1], - ).toEqualText('15 B'); - }); -}); diff --git a/src/components/sbb-file-selector/sbb-file-selector.events.ts b/src/components/sbb-file-selector/sbb-file-selector.events.ts deleted file mode 100644 index 800aa618b0..0000000000 --- a/src/components/sbb-file-selector/sbb-file-selector.events.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This file is autogenerated by the event-sync plugin. - * See stencil.config.ts in the root directory. - */ -export default { - fileChangedEvent: 'file-changed', -}; diff --git a/src/components/sbb-file-selector/sbb-file-selector.spec.ts b/src/components/sbb-file-selector/sbb-file-selector.spec.ts deleted file mode 100644 index eb3814ddde..0000000000 --- a/src/components/sbb-file-selector/sbb-file-selector.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { SbbFileSelector } from './sbb-file-selector'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-file-selector', () => { - it('renders default', async () => { - const { root } = await newSpecPage({ - components: [SbbFileSelector], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    - -
    -

    -
    -
    -
    - `); - }); - - it('renders with dropzone area', async () => { - const { root } = await newSpecPage({ - components: [SbbFileSelector], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    -
    - -
    -

    -
    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-footer/sbb-footer.custom.d.ts b/src/components/sbb-footer/sbb-footer.custom.d.ts deleted file mode 100644 index 957d09367d..0000000000 --- a/src/components/sbb-footer/sbb-footer.custom.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface InterfaceFooterAttributes { - variant: 'default' | 'clock-columns'; -} diff --git a/src/components/sbb-footer/sbb-footer.e2e.ts b/src/components/sbb-footer/sbb-footer.e2e.ts deleted file mode 100644 index 6ee43cc33f..0000000000 --- a/src/components/sbb-footer/sbb-footer.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-footer', () => { - let element: E2EElement, page: E2EPage; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-footer'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-footer/sbb-footer.spec.ts b/src/components/sbb-footer/sbb-footer.spec.ts deleted file mode 100644 index 8559353b70..0000000000 --- a/src/components/sbb-footer/sbb-footer.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SbbFooter } from './sbb-footer'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-footer', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbFooter], - html: '', - }); - - expect(root).toEqualHtml(` - - -
    - -
    -
    -
    - `); - }); -}); diff --git a/src/components/sbb-footer/sbb-footer.tsx b/src/components/sbb-footer/sbb-footer.tsx deleted file mode 100644 index 6b5585addc..0000000000 --- a/src/components/sbb-footer/sbb-footer.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Component, h, JSX, Prop } from '@stencil/core'; -import { InterfaceFooterAttributes } from './sbb-footer.custom'; -import { InterfaceTitleAttributes } from '../sbb-title/sbb-title.custom'; - -@Component({ - shadow: true, - styleUrl: 'sbb-footer.scss', - tag: 'sbb-footer', -}) -export class SbbFooter { - /** - * Variants to display the footer. The default, displays the content in regular block element - * approach. The clock-columns, used a css-grid for displaying the content over different - * breakpoints. - */ - @Prop({ reflect: true }) public variant: InterfaceFooterAttributes['variant'] = 'default'; - - /** Negative coloring variant flag. */ - @Prop({ reflect: true }) public negative = false; - - /** - * Whether to allow the footer content to stretch to full width. - * By default, the content has the appropriate page size. - */ - @Prop({ reflect: true }) public expanded = false; - - /** Footer title text, visually hidden, necessary for screen readers. */ - @Prop() public accessibilityTitle?: string; - - /** Level of the accessibility title, will be rendered as heading tag (e.g. h1). Defaults to level 1. */ - @Prop() public accessibilityTitleLevel: InterfaceTitleAttributes['level'] = '1'; - - public render(): JSX.Element { - const TITLE_TAG_NAME = `h${this.accessibilityTitleLevel}`; - - return ( -
    - -
    - ); - } -} diff --git a/src/components/sbb-form-error/readme.md b/src/components/sbb-form-error/readme.md deleted file mode 100644 index ba0d823bcb..0000000000 --- a/src/components/sbb-form-error/readme.md +++ /dev/null @@ -1,34 +0,0 @@ -The `sbb-form-error` component can be used to provide an error message in inputs components like the -[sbb-checkbox-group](/docs/components-sbb-checkbox-sbb-checkbox-group--docs) and -[sbb-radio-button-group](/docs/components-sbb-radio-button-sbb-radio-button-group--docs), -or within the [sbb-form-field](/docs/components-sbb-form-field-sbb-form-field--docs). - -## Slots - -It is possible to provide the error message via an unnamed slot; -the component displays an icon by default, that can be changed using the `icon` slot. - -```html - - This is a required field. - - - - - This is a required field. - -``` - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------- | ---------- | ------------------------------- | --------- | ------- | -| `negative` | `negative` | Negative coloring variant flag. | `boolean` | `false` | - - ----------------------------------------------- - - diff --git a/src/components/sbb-form-error/sbb-form-error.e2e.ts b/src/components/sbb-form-error/sbb-form-error.e2e.ts deleted file mode 100644 index fbb3d2dda6..0000000000 --- a/src/components/sbb-form-error/sbb-form-error.e2e.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { newE2EPage } from '@stencil/core/testing'; - -describe('sbb-form-error', () => { - let element, page; - - it('renders', async () => { - page = await newE2EPage(); - await page.setContent(''); - - element = await page.find('sbb-form-error'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/src/components/sbb-form-error/sbb-form-error.spec.ts b/src/components/sbb-form-error/sbb-form-error.spec.ts deleted file mode 100644 index 138bbf1b7a..0000000000 --- a/src/components/sbb-form-error/sbb-form-error.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SbbFormError } from './sbb-form-error'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-form-error', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbFormError], - html: 'Required', - }); - - expect(root).toEqualHtml(` - - - - - - - - - - - - Required - - `); - }); -}); diff --git a/src/components/sbb-form-error/sbb-form-error.tsx b/src/components/sbb-form-error/sbb-form-error.tsx deleted file mode 100644 index 2227366acd..0000000000 --- a/src/components/sbb-form-error/sbb-form-error.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, h, JSX, Host, Prop, ComponentInterface, Element } from '@stencil/core'; -import { assignId } from '../../global/a11y'; -import { isValidAttribute } from '../../global/dom'; - -let nextId = 0; - -@Component({ - shadow: true, - styleUrl: './sbb-form-error.scss', - tag: 'sbb-form-error', -}) -export class SbbFormError implements ComponentInterface { - /** Negative coloring variant flag. */ - @Prop({ reflect: true, mutable: true }) public negative = false; - - @Element() private _element!: HTMLElement; - - public connectedCallback(): void { - const formField = - this._element.closest('sbb-form-field') ?? this._element.closest('[data-form-field]'); - if (formField) { - this.negative = isValidAttribute(formField, 'negative'); - } - } - - public render(): JSX.Element { - return ( - `sbb-form-error-${++nextId}`)}> - - - - - - - - - - ); - } -} diff --git a/src/components/sbb-form-field-clear/readme.md b/src/components/sbb-form-field-clear/readme.md deleted file mode 100644 index 01bd925665..0000000000 --- a/src/components/sbb-form-field-clear/readme.md +++ /dev/null @@ -1,39 +0,0 @@ -The `sbb-form-field-clear` component can be used with the [sbb-form-field](/docs/components-sbb-form-field-sbb-form-field--docs) component -to provide the possibility to display a clear button which can clear the input value. - -```html - - - - -``` - -**Note:** it currently works with simple inputs and does not support, for example, `select` inputs. - - - - - -## Properties - -| Property | Attribute | Description | Type | Default | -| ---------- | ---------- | ------------------------------- | --------- | ------- | -| `negative` | `negative` | Negative coloring variant flag. | `boolean` | `false` | - - -## Dependencies - -### Depends on - -- [sbb-icon](../sbb-icon) - -### Graph -```mermaid -graph TD; - sbb-form-field-clear --> sbb-icon - style sbb-form-field-clear fill:#f9f,stroke:#333,stroke-width:4px -``` - ----------------------------------------------- - - diff --git a/src/components/sbb-form-field-clear/sbb-form-field-clear.e2e.ts b/src/components/sbb-form-field-clear/sbb-form-field-clear.e2e.ts deleted file mode 100644 index 71936294c5..0000000000 --- a/src/components/sbb-form-field-clear/sbb-form-field-clear.e2e.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { E2EElement, E2EPage, newE2EPage } from '@stencil/core/testing'; - -describe('sbb-form-field-clear', () => { - let element: E2EElement, page: E2EPage; - - beforeEach(async () => { - page = await newE2EPage(); - await page.setContent(` - - - - `); - - element = await page.find('sbb-form-field-clear'); - }); - - it('renders', async () => { - expect(element).toHaveClass('hydrated'); - }); - - it('clears the value and sets the focus on the input', async () => { - await page.waitForChanges(); - expect(await page.evaluate(() => document.querySelector('input').value)).toBe('Input value'); - - await element.click(); - await page.waitForChanges(); - - expect(await page.evaluate(() => document.querySelector('input').value)).toBeFalsy(); - expect(await page.evaluate(() => document.activeElement.id)).toBe('input'); - expect( - await page.evaluate( - () => getComputedStyle(document.querySelector('sbb-form-field-clear')).display, - ), - ).toBe('none'); - }); - - it('is hidden if the form field is disabled', async () => { - page = await newE2EPage(); - await page.setContent(` - - - - `); - - element = await page.find('sbb-form-field-clear'); - await page.waitForChanges(); - - expect( - await page.evaluate( - () => getComputedStyle(document.querySelector('sbb-form-field-clear')).display, - ), - ).toBe('none'); - }); - - it('is hidden if the form field is readonly', async () => { - page = await newE2EPage(); - await page.setContent(` - - - - `); - - element = await page.find('sbb-form-field-clear'); - await page.waitForChanges(); - - expect( - await page.evaluate( - () => getComputedStyle(document.querySelector('sbb-form-field-clear')).display, - ), - ).toBe('none'); - }); -}); diff --git a/src/components/sbb-form-field-clear/sbb-form-field-clear.spec.ts b/src/components/sbb-form-field-clear/sbb-form-field-clear.spec.ts deleted file mode 100644 index 5142610450..0000000000 --- a/src/components/sbb-form-field-clear/sbb-form-field-clear.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SbbFormField } from '../sbb-form-field/sbb-form-field'; -import { SbbFormFieldClear } from './sbb-form-field-clear'; -import { newSpecPage } from '@stencil/core/testing'; - -describe('sbb-form-field-clear', () => { - it('renders', async () => { - const { root } = await newSpecPage({ - components: [SbbFormField, SbbFormFieldClear], - html: ` - - - `, - }); - - expect(root).toEqualHtml(` - - -
    -
    - -
    - - - - - - -
    - -
    -
    - -
    -
    - -
    -
    -
    - - - - - - - - - -
    `); - }); -}); diff --git a/src/components/sbb-form-field-clear/sbb-form-field-clear.tsx b/src/components/sbb-form-field-clear/sbb-form-field-clear.tsx deleted file mode 100644 index 80519bc082..0000000000 --- a/src/components/sbb-form-field-clear/sbb-form-field-clear.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - Component, - ComponentInterface, - Element, - Listen, - h, - Host, - JSX, - State, - Prop, -} from '@stencil/core'; -import { ButtonProperties, resolveButtonRenderVariables } from '../../global/interfaces'; -import { hostContext, isValidAttribute } from '../../global/dom'; -import { - HandlerRepository, - actionElementHandlerAspect, - documentLanguage, - languageChangeHandlerAspect, -} from '../../global/eventing'; -import { i18nClearInput } from '../../global/i18n'; - -@Component({ - shadow: true, - styleUrl: 'sbb-form-field-clear.scss', - tag: 'sbb-form-field-clear', -}) -export class SbbFormFieldClear implements ComponentInterface { - /** Negative coloring variant flag. */ - @Prop({ reflect: true, mutable: true }) public negative = false; - - @Element() private _element!: HTMLElement; - - @State() private _currentLanguage = documentLanguage(); - - private _handlerRepository = new HandlerRepository( - this._element, - actionElementHandlerAspect, - languageChangeHandlerAspect((l) => (this._currentLanguage = l)), - ); - private _formField: HTMLSbbFormFieldElement; - - @Listen('click') - public async handleClick(): Promise { - const input = await this._formField.getInputElement(); - if (!input || input.tagName !== 'INPUT') { - return; - } - await this._formField.clear(); - input.focus(); - input.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); - input.dispatchEvent(new window.Event('change', { bubbles: true })); - } - - public connectedCallback(): void { - this._handlerRepository.connect(); - this._formField = - (hostContext('sbb-form-field', this._element) as HTMLSbbFormFieldElement) ?? - (hostContext('[data-form-field]', this._element) as HTMLSbbFormFieldElement); - - if (this._formField) { - this.negative = isValidAttribute(this._formField, 'negative'); - } - } - - public disconnectedCallback(): void { - this._handlerRepository.disconnect(); - } - - public render(): JSX.Element { - const { hostAttributes } = resolveButtonRenderVariables(this as ButtonProperties); - - return ( - - - - - - ); - } -} diff --git a/src/components/sbb-form-field/readme.md b/src/components/sbb-form-field/readme.md deleted file mode 100644 index d6623bb032..0000000000 --- a/src/components/sbb-form-field/readme.md +++ /dev/null @@ -1,191 +0,0 @@ -The `sbb-form-field` component is intended to be used as a form input wrapper with label and errors. - -```html - - - - - - - - This field is required! - -``` - -In this document, "form field" refers to the wrapper component `sbb-form-field` and -"form field control" refers to the component that the `sbb-form-field` is wrapping -(e.g., the input, select, etc.) - -The following components are designed to work inside a `sbb-form-field`: - -- `` -- ` - This field is required! - -``` - -### Error messages - -Error messages can be shown under the form field by adding `sbb-form-error` elements inside the form field. -The component will automatically assign them to the `slot='error'`. - -```html - - - -``` - -In order to avoid the layout from "jumping" when an error is shown, the option of setting `error-space="reserve"` -on the `sbb-form-field` will reserve space for a single line of an error message. - -### Prefix & Suffix - -It is possible to add content as a prefix or suffix in a `sbb-form-field`. -This can be done via the `prefix` and `suffix` slots. - -Some components, like the [sbb-form-field-clear](/docs/components-sbb-form-field-sbb-form-field-clear--docs) or the -[sbb-slider](/docs/components-sbb-slider--docs), when used within the form field, will automatically occupy -one or both of these slots. -Please refer to their documentation for more details. - -```html - - - - - -``` - -## Style - -By default, the component has a defined width and min-width. However, this behavior can be overridden by setting -the `width` property to `collapse`: in this way the component adapts its width to the inner slotted input component. -This is useful, for example, for the [sbb-time-input](/docs/components-sbb-time-input--docs) component. -However, as the width-styles are exposed to the host, -it's possible to apply any desired width by setting just the `width` and `min-width` CSS properties. - -```html - - - - -``` - -## Accessibility - -By itself, the `sbb-form-field` does not apply any additional accessibility treatment to a form -element. However, several of the form field's optional features interact with the form element -contained within the form field. - -When you provide a label via `