diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 88155ca4e6..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -dist/ -tools/generate-component/boilerplate/ -**/__snapshots__/ - -# not ignored folders/files -!.github/ diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 72a37f9ac2..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "es2020": true, - "node": true - }, - "extends": ["eslint:recommended"], - "ignorePatterns": ["**/*.chromatic.stories.*"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2020 - }, - "overrides": [ - { - "files": ["*.js"], - "extends": ["prettier"], - "rules": { - "no-unused-vars": [ - "error", - { - "vars": "all", - "varsIgnorePattern": "h" - } - ] - } - }, - { - "files": ["*.ts"], - "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/consistent-type-imports": "error", - "@typescript-eslint/explicit-function-return-type": ["warn", { "allowExpressions": true }], - "@typescript-eslint/explicit-member-accessibility": "error", - "@typescript-eslint/naming-convention": [ - "error", - { - "format": ["PascalCase"], - "selector": "interface" - }, - { - "format": ["camelCase"], - "selector": "default" - }, - { - "format": ["camelCase", "UPPER_CASE"], - "selector": "variable" - }, - { - "format": ["camelCase"], - "leadingUnderscore": "allow", - "selector": "parameter" - }, - { - "format": ["camelCase"], - "leadingUnderscore": "require", - "modifiers": ["private"], - "selector": "memberLike" - }, - { - "format": ["PascalCase"], - "selector": "typeLike" - }, - { - "format": null, - "selector": "objectLiteralProperty" - } - ], - // TODO: Remove this after fixing issues - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "args": "all", - "argsIgnorePattern": "^_" - } - ], - "@typescript-eslint/no-use-before-define": "error", - // TODO: Remove this after fixing issues - "@typescript-eslint/no-var-requires": "off", - // TODO: Evaluate this rule - "@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" - } - }, - { - "files": ["*.yaml", "*.yml"], - "plugins": ["yaml"] - }, - { - "files": ["*.stories.ts"], - "rules": { - "@typescript-eslint/naming-convention": "off" - } - } - ] -} diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 1f7b203056..a9bcad9d01 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -15,9 +15,9 @@ body: attributes: label: Preflight Checklist options: - - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/CONTRIBUTING.md) for this project. + - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CONTRIBUTING.md) for this project. required: true - - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CODE_OF_CONDUCT.md) that this project adheres to. required: true - label: I have searched the [issue tracker](https://github.com/orgs/lyne-design-system/projects/4) for an issue that matches the one I want to file, without success. required: true @@ -30,11 +30,11 @@ body: attributes: label: Bug type options: - - 'Functionality' - - 'Accessibility' - - 'Performance' - - 'Security vulnerability' - - 'Other' + - Functionality + - Accessibility + - Performance + - Security vulnerability + - Other validations: required: true - type: dropdown @@ -42,10 +42,10 @@ body: attributes: label: It affects the following packages options: - - 'Design Tokens' - - 'Components' - - 'Both' - - 'Other' + - Design Tokens + - Components + - Both + - Other validations: required: true - type: input @@ -78,9 +78,9 @@ body: attributes: label: Input mode options: - - 'Mouse/Keyboard' - - 'Touch' - - 'Other' + - Mouse/Keyboard + - Touch + - Other validations: required: false - type: input diff --git a/.github/ISSUE_TEMPLATE/contributing-request.yml b/.github/ISSUE_TEMPLATE/contributing-request.yml index 656e4f12fd..8a2c2b2129 100644 --- a/.github/ISSUE_TEMPLATE/contributing-request.yml +++ b/.github/ISSUE_TEMPLATE/contributing-request.yml @@ -10,9 +10,9 @@ body: attributes: label: Preflight Checklist options: - - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/CONTRIBUTING.md) for this project. + - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CONTRIBUTING.md) for this project. required: true - - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CODE_OF_CONDUCT.md) that this project adheres to. required: true - label: I have searched the [issue tracker](https://github.com/orgs/lyne-design-system/projects/4) for an issue that matches the one I want to file, without success. required: true @@ -25,8 +25,8 @@ body: attributes: label: Contribution type options: - - 'Contribute a new component' - - 'Contribute an enhancement of a component' + - Contribute a new component + - Contribute an enhancement of a component validations: required: true - type: dropdown @@ -34,10 +34,10 @@ body: attributes: label: It affects the following packages options: - - 'Design Tokens' - - 'Components' + - Design Tokens + - Components - 'Design Tokens & Components' - - 'Other' + - Other validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature-request-or-enhancement.yml b/.github/ISSUE_TEMPLATE/feature-request-or-enhancement.yml index 6e96309805..4a8e3ee19f 100644 --- a/.github/ISSUE_TEMPLATE/feature-request-or-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/feature-request-or-enhancement.yml @@ -10,9 +10,9 @@ body: attributes: label: Preflight Checklist options: - - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/CONTRIBUTING.md) for this project. + - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CONTRIBUTING.md) for this project. required: true - - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CODE_OF_CONDUCT.md) that this project adheres to. required: true - label: I have searched the [issue tracker](https://github.com/orgs/lyne-design-system/projects/4) for an issue that matches the one I want to file, without success. required: true @@ -25,8 +25,8 @@ body: attributes: label: Request type options: - - 'Request for a new component' - - 'Request for enhancement of a component' + - Request for a new component + - Request for enhancement of a component validations: required: true - type: dropdown @@ -34,10 +34,10 @@ body: attributes: label: It affects the following packages options: - - 'Design Tokens' - - 'Components' + - Design Tokens + - Components - 'Design Tokens & Components' - - 'Other' + - Other validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 4e94fd1545..6d6443291f 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -10,9 +10,9 @@ body: attributes: label: Preflight Checklist options: - - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/CONTRIBUTING.md) for this project. + - label: I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CONTRIBUTING.md) for this project. required: true - - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. + - label: I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CODE_OF_CONDUCT.md) that this project adheres to. required: true - label: I have searched the [issue tracker](https://github.com/orgs/lyne-design-system/projects/4) for an issue that matches the one I want to file, without success. required: true @@ -25,10 +25,10 @@ body: attributes: label: It affects the following packages options: - - 'Design Tokens' - - 'Components' + - Design Tokens + - Components - 'Design Tokens & Components' - - 'Other' + - Other validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/story.yml b/.github/ISSUE_TEMPLATE/story.yml index 546f5d2d44..3a29b83af5 100644 --- a/.github/ISSUE_TEMPLATE/story.yml +++ b/.github/ISSUE_TEMPLATE/story.yml @@ -1,7 +1,7 @@ name: '[Internal] Story' description: An internal story for lyne-components title: 'story(COMPONENT): TITLE' -labels: ['story', 'to-refine'] +labels: [story, to-refine] body: - type: textarea id: description diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1c376c50bf..af6a71f28a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,8 +2,8 @@ -- [ ] I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/CONTRIBUTING.md) for this project. -- [ ] I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/CODE_OF_CONDUCT.md) that this project adheres to. +- [ ] I have read the [Contributing Guidelines](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CONTRIBUTING.md) for this project. +- [ ] I agree to follow the [Code of Conduct](https://github.com/lyne-design-system/lyne-components/blob/main/docs/CODE_OF_CONDUCT.md) that this project adheres to. - [ ] I have searched the [pull request tracker](https://github.com/lyne-design-system/lyne-components/pulls) for a Pull Request (PR) that matches the one I want to submit, without success. ## Issue @@ -17,7 +17,7 @@ Please check if your PR fulfills the following requirements: - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) -See [Review Guidelines](../REVIEW.md) for more information what is checked during review process. +See [Review Guidelines](../docs/REVIEW.md) for more information what is checked during review process. ## Changes diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1e3d5bd69a..5ba4456c20 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,7 +9,7 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: 'CodeQL' +name: CodeQL on: push: @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - language: ['javascript'] + language: [javascript] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed @@ -63,7 +63,7 @@ jobs: # and modify them (or add more) to build your code if your project # uses a compiled language - #- run: | + # - run: | # make bootstrap # make release diff --git a/.github/workflows/continuous-integration-secure.yml b/.github/workflows/continuous-integration-secure.yml index 59f75244fe..13262d420b 100644 --- a/.github/workflows/continuous-integration-secure.yml +++ b/.github/workflows/continuous-integration-secure.yml @@ -5,7 +5,7 @@ name: Continuous Integration Secure # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ on: workflow_run: - workflows: ['Continuous Integration'] + workflows: [Continuous Integration] types: [completed] permissions: @@ -73,7 +73,7 @@ jobs: # This label is used for filtering deployments in ArgoCD uses: actions-ecosystem/action-add-labels@v1 with: - labels: 'preview-available' + labels: preview-available number: ${{ env.PR_NUMBER }} codecov: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4ce6272a57..2beaf55455 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -14,8 +14,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: Run eslint @@ -27,8 +27,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: 'Integrity: Verify workspace integrity' @@ -42,8 +42,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: Install browser dependencies @@ -52,7 +52,7 @@ jobs: run: yarn test env: NODE_ENV: production - - name: 'Assert no new snapshots (run `yarn test --ci` if this fails)' + - name: Assert no new snapshots (run `yarn test --ci` if this fails) run: git diff --exit-code - name: Store coverage if: github.event_name == 'pull_request' @@ -67,8 +67,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: Run build @@ -124,8 +124,8 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: Generate chromatic stories run: yarn generate:chromatic-stories diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index 899464c057..0df1b2d696 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -12,8 +12,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: 'Lint: Pull request title' diff --git a/.github/workflows/preview-image-cleanup.yml b/.github/workflows/preview-image-cleanup.yml index 2fc3e88cf7..3a9a8df931 100644 --- a/.github/workflows/preview-image-cleanup.yml +++ b/.github/workflows/preview-image-cleanup.yml @@ -56,6 +56,6 @@ jobs: } - uses: actions/delete-package-versions@v4 with: - package-name: 'lyne-components/storybook-preview' - package-type: 'container' + package-name: lyne-components/storybook-preview + package-type: container delete-only-untagged-versions: 'true' diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index edc545ff6b..21720da636 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -43,8 +43,8 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn registry-url: 'https://registry.npmjs.org' scope: sbb-esta - run: yarn install --frozen-lockfile --non-interactive @@ -106,8 +106,8 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: .nvmrc + cache: yarn - run: yarn install --frozen-lockfile --non-interactive - name: Run build diff --git a/.storybook/main.ts b/.storybook/main.ts index 58377756ca..3def8b8f00 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -18,7 +18,7 @@ const config: StorybookConfig = { sourcemap: false, rollupOptions: { output: { - manualChunks(id) { + manualChunks(_id) { return 'main'; }, }, diff --git a/.storybook/manager.js b/.storybook/manager.js index 4f0c473d27..2367745518 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -1,5 +1,5 @@ import { addons } from '@storybook/manager-api'; -import theme from './theme'; +import theme from './theme.js'; addons.setConfig({ enableShortcuts: false, diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 23e3aa0b37..19cce4138c 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -46,6 +46,10 @@ .sbdocs-content { max-width: var(--sbb-breakpoint-medium-max) !important; } + + .sb-show-main.sb-main-padded { + padding: 2rem; + } +``` + +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 `backButton` on the `sbb-dialog-title` component to display the back button in the title section which will emit the event `requestBackAction` when clicked. + +## Style + +It's possible to display the component in `negative` variant using the self-named property. + +```html + + Title + Dialog content. + +``` + +## Accessibility + +When using a button to trigger the dialog, ensure to manage the appropriate ARIA attributes on the button element itself. This includes: `aria-haspopup="dialog"` that signals to assistive technologies that the button controls a dialog element, +`aria-controls="dialog-id"` that connects the button to the dialog by referencing the dialog's ID. Consider using `aria-expanded` to indicate the dialog's current state (open or closed). + +The `sbb-dialog` component may visually hide the title thanks to the `hideOnScroll` property of the [sbb-dialog-title](/docs/components-sbb-dialog-sbb-dialog-title--docs) to create more space for content, this is useful especially on smaller screens. Screen readers and other assistive technologies will still have access to the title information for context. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------------------- | --------------------- | ------- | --------------------- | --------- | -------------------------------------------------------------------- | +| `backdropAction` | `backdrop-action` | public | `'close' \| 'none'` | `'close'` | Backdrop click action. | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the relevant nested element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | + +## Methods + +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------- | ------- | -------------------------- | ---------------------------------- | ------ | -------------- | +| `open` | public | Opens the dialog element. | | `void` | | +| `close` | public | Closes the dialog element. | `result: any, target: HTMLElement` | `any` | | + +## Events + +| Name | Type | Description | Inherited From | +| ----------- | ----------------------------------------- | ------------------------------------------------------------------------------- | -------------- | +| `willOpen` | `CustomEvent` | Emits whenever the `sbb-dialog` starts the opening transition. Can be canceled. | | +| `didOpen` | `CustomEvent` | Emits whenever the `sbb-dialog` is opened. | | +| `willClose` | `CustomEvent` | Emits whenever the `sbb-dialog` begins the closing transition. Can be canceled. | | +| `didClose` | `CustomEvent` | Emits whenever the `sbb-dialog` is closed. | | + +## CSS Properties + +| Name | Default | Description | +| ---------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--sbb-dialog-z-index` | `var(--sbb-overlay-default-z-index)` | To specify a custom stack order, the `z-index` can be overridden by defining this CSS variable. The default `z-index` of the component is set to `var(--sbb-overlay-default-z-index)` with a value of `1000`. | + +## Slots + +| Name | Description | +| ---- | ---------------------------------------------------------------------------------------------------------------- | +| | Use the unnamed slot to provide a `sbb-dialog-title`, `sbb-dialog-content` and an optional `sbb-dialog-actions`. | diff --git a/src/components/dialog/index.ts b/src/components/dialog/index.ts deleted file mode 100644 index 20da8e550a..0000000000 --- a/src/components/dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dialog'; diff --git a/src/components/dialog/readme.md b/src/components/dialog/readme.md deleted file mode 100644 index 80c5831236..0000000000 --- a/src/components/dialog/readme.md +++ /dev/null @@ -1,125 +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](#interactions)). - -```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 `requestBackAction` when clicked. - -## Style - -It's possible to display the component in `negative` variant using the self-named property. - -## Animation - -Add the `sbb-disable-animation` class to disable animation and transition effects for the element and all its children. - - - -## Properties - -| Name | Attribute | Privacy | Type | Default | Description | -| ------------------------- | --------------------------- | ------- | ---------------------------- | --------- | ------------------------------------------------------------------------------- | -| `titleContent` | `title-content` | public | `string \| undefined` | | Dialog title. | -| `titleLevel` | `title-level` | public | `SbbTitleLevel` | `'1'` | Level of title, will be rendered as heading tag (e.g. h1). Defaults to level 1. | -| `titleBackButton` | `title-back-button` | public | `boolean` | `false` | Whether a back button is displayed next to the title. | -| `backdropAction` | `backdrop-action` | public | `'close' \| 'none'` | `'close'` | Backdrop click action. | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the relevant nested element. | -| `accessibilityCloseLabel` | `accessibility-close-label` | public | `\| string \| undefined` | | This will be forwarded as aria-label to the close button element. | -| `accessibilityBackLabel` | `accessibility-back-label` | public | `\| string \| undefined` | | This will be forwarded as aria-label to the back button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | - -## Methods - -| Name | Privacy | Description | Parameters | Return | Inherited From | -| ------- | ------- | -------------------------- | ---------------------------------- | ------ | -------------- | -| `open` | public | Opens the dialog element. | | `void` | | -| `close` | public | Closes the dialog element. | `result: any, target: HTMLElement` | `any` | | - -## Events - -| Name | Type | Description | Inherited From | -| ------------------- | ------------------- | ------------------------------------------------------------------------------- | -------------- | -| `willOpen` | `CustomEvent` | Emits whenever the `sbb-dialog` starts the opening transition. Can be canceled. | | -| `didOpen` | `CustomEvent` | Emits whenever the `sbb-dialog` is opened. | | -| `willClose` | `CustomEvent` | Emits whenever the `sbb-dialog` begins the closing transition. Can be canceled. | | -| `didClose` | `CustomEvent` | Emits whenever the `sbb-dialog` is closed. | | -| `requestBackAction` | `CustomEvent` | Emits whenever the back button is clicked. | | - -## CSS Properties - -| Name | Default | Description | -| ---------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--sbb-dialog-z-index` | `var(--sbb-overlay-z-index)` | To specify a custom stack order, the `z-index` can be overridden by defining this CSS variable. The default `z-index` of the component is set to `var(--sbb-overlay-z-index)` with a value of `1000`. | - -## Slots - -| Name | Description | -| -------------- | ------------------------------------------------------------ | -| | Use the unnamed slot to add content to the `sbb-dialog`. | -| `title` | Use this slot to provide a title. | -| `action-group` | Use this slot to display a `sbb-action-group` in the footer. | diff --git a/src/components/divider.ts b/src/components/divider.ts new file mode 100644 index 0000000000..06ac499007 --- /dev/null +++ b/src/components/divider.ts @@ -0,0 +1 @@ +export * from './divider/divider.js'; diff --git a/src/components/divider/divider.e2e.ts b/src/components/divider/divider.e2e.ts index be46dc95ba..bdddc72069 100644 --- a/src/components/divider/divider.e2e.ts +++ b/src/components/divider/divider.e2e.ts @@ -1,9 +1,9 @@ import { assert } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture } from '../core/testing/private'; +import { fixture } from '../core/testing/private.js'; -import { SbbDividerElement } from './divider'; +import { SbbDividerElement } from './divider.js'; describe(`sbb-divider with ${fixture.name}`, () => { it('renders', async () => { diff --git a/src/components/divider/divider.scss b/src/components/divider/divider.scss index b3c904b171..cdece11213 100644 --- a/src/components/divider/divider.scss +++ b/src/components/divider/divider.scss @@ -1,12 +1,13 @@ @use '../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-divider-color: var(--sbb-color-cloud); --sbb-divider-border-width: var(--sbb-border-width-1x); + + display: block; } :host([orientation='vertical']) { diff --git a/src/components/divider/divider.spec.ts b/src/components/divider/divider.spec.ts index 849b39ef45..14e6cc5bf7 100644 --- a/src/components/divider/divider.spec.ts +++ b/src/components/divider/divider.spec.ts @@ -1,13 +1,16 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; +import { waitForLitRender } from '../core/testing.js'; -import './divider'; +import type { SbbDividerElement } from './divider.js'; + +import './divider.js'; describe(`sbb-divider`, () => { it('should render with default values', async () => { - const element: Element = await fixture(html``); + const element: SbbDividerElement = await fixture(html``); expect(element).dom.to.be.equal( ``, ); @@ -15,7 +18,7 @@ describe(`sbb-divider`, () => { }); it('should render with orientation horizontal', async () => { - const element: Element = await fixture( + const element: SbbDividerElement = await fixture( html``, ); expect(element).dom.to.be.equal( @@ -25,7 +28,7 @@ describe(`sbb-divider`, () => { }); it('should render with orientation vertical', async () => { - const element: Element = await fixture( + const element: SbbDividerElement = await fixture( html``, ); expect(element).dom.to.be.equal( @@ -34,5 +37,13 @@ describe(`sbb-divider`, () => { expect(element).shadowDom.to.be.equal(`
`); }); + it('should react to change of orientation', async () => { + const element: SbbDividerElement = await fixture(html``); + + element.orientation = 'vertical'; + await waitForLitRender(element); + expect(element).to.have.attribute('aria-orientation', 'vertical'); + }); + testA11yTreeSnapshot(html``); }); diff --git a/src/components/divider/divider.stories.ts b/src/components/divider/divider.stories.ts index 801c25ed87..3e6d09d98d 100644 --- a/src/components/divider/divider.stories.ts +++ b/src/components/divider/divider.stories.ts @@ -4,16 +4,10 @@ import type { TemplateResult } from 'lit'; import { html } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../core/dom'; +import { sbbSpread } from '../../storybook/helpers/spread.js'; import readme from './readme.md?raw'; -import './divider'; - -const wrapperStyle = (context: StoryContext): Record => ({ - 'background-color': context.args.negative - ? 'var(--sbb-color-charcoal)' - : 'var(--sbb-color-white)', -}); +import './divider.js'; const Template = (args: Args): TemplateResult => html`
@@ -43,7 +37,7 @@ const defaultArgTypes: ArgTypes = { }; const defaultArgs: Args = { - orientation: orientation.options[0], + orientation: orientation.options![0], negative: false, }; @@ -72,13 +66,9 @@ export const dividerNegative: StoryObj = { }; const meta: Meta = { - decorators: [ - (story, context) => html`
${story()}
`, - ], parameters: { - backgrounds: { - disable: true, - }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-charcoal)' : 'var(--sbb-color-white)', docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/divider/divider.ts b/src/components/divider/divider.ts index d3228fbf76..19eea19ee6 100644 --- a/src/components/divider/divider.ts +++ b/src/components/divider/divider.ts @@ -1,10 +1,10 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { hostAttributes, SbbNegativeMixin } from '../core/common-behaviors'; -import { setAttribute } from '../core/dom'; -import type { SbbOrientation } from '../core/interfaces'; +import { hostAttributes } from '../core/decorators.js'; +import type { SbbOrientation } from '../core/interfaces.js'; +import { SbbNegativeMixin } from '../core/mixins.js'; import style from './divider.scss?lit&inline'; @@ -19,11 +19,17 @@ export class SbbDividerElement extends SbbNegativeMixin(LitElement) { public static override styles: CSSResultGroup = style; /** Orientation property with possible values 'horizontal' | 'vertical'. Defaults to horizontal. */ - @property({ reflect: true }) public orientation?: SbbOrientation = 'horizontal'; + @property({ reflect: true }) public orientation: SbbOrientation = 'horizontal'; - protected override render(): TemplateResult { - setAttribute(this, 'aria-orientation', this.orientation); + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has('orientation')) { + this.setAttribute('aria-orientation', this.orientation); + } + } + protected override render(): TemplateResult { return html`
`; } } diff --git a/src/components/divider/index.ts b/src/components/divider/index.ts deleted file mode 100644 index bf4ed01967..0000000000 --- a/src/components/divider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './divider'; diff --git a/src/components/divider/readme.md b/src/components/divider/readme.md index b65dd68718..c1954fd998 100644 --- a/src/components/divider/readme.md +++ b/src/components/divider/readme.md @@ -16,7 +16,7 @@ It's also possible to display the component in `negative` variant using the self ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ------------- | ------------- | ------- | ----------------------------- | -------------- | --------------------------------------------------------------------------------------------- | -| `orientation` | `orientation` | public | `SbbOrientation \| undefined` | `'horizontal'` | Orientation property with possible values 'horizontal' \| 'vertical'. Defaults to horizontal. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| Name | Attribute | Privacy | Type | Default | Description | +| ------------- | ------------- | ------- | ---------------- | -------------- | --------------------------------------------------------------------------------------------- | +| `orientation` | `orientation` | public | `SbbOrientation` | `'horizontal'` | Orientation property with possible values 'horizontal' \| 'vertical'. Defaults to horizontal. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | diff --git a/src/components/expansion-panel.ts b/src/components/expansion-panel.ts new file mode 100644 index 0000000000..5e5664417a --- /dev/null +++ b/src/components/expansion-panel.ts @@ -0,0 +1,3 @@ +export * from './expansion-panel/expansion-panel.js'; +export * from './expansion-panel/expansion-panel-content.js'; +export * from './expansion-panel/expansion-panel-header.js'; diff --git a/src/components/expansion-panel/expansion-panel-content.ts b/src/components/expansion-panel/expansion-panel-content.ts new file mode 100644 index 0000000000..69a30a3390 --- /dev/null +++ b/src/components/expansion-panel/expansion-panel-content.ts @@ -0,0 +1 @@ +export * from './expansion-panel-content/expansion-panel-content.js'; diff --git a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.e2e.ts b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.e2e.ts index 9aa817d32f..75e902f270 100644 --- a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.e2e.ts +++ b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.e2e.ts @@ -1,9 +1,9 @@ import { assert } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture } from '../../core/testing/private'; +import { fixture } from '../../core/testing/private.js'; -import { SbbExpansionPanelContentElement } from './expansion-panel-content'; +import { SbbExpansionPanelContentElement } from './expansion-panel-content.js'; describe(`sbb-expansion-panel-content with ${fixture.name}`, () => { let element: SbbExpansionPanelContentElement; diff --git a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.scss b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.scss index 58cf5ef9cb..0aa89537f2 100644 --- a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.scss +++ b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.scss @@ -1,14 +1,15 @@ @use '../../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-expansion-panel-content-padding-inline: var(--sbb-spacing-fixed-6x); --sbb-expansion-panel-content-padding-inline-start: var( --sbb-expansion-panel-content-padding-inline ); + + display: block; } :host([data-icon-space]) { diff --git a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.spec.ts b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.spec.ts index 435620d9db..bd319398fa 100644 --- a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.spec.ts +++ b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.spec.ts @@ -1,9 +1,9 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; -import './expansion-panel-content'; +import './expansion-panel-content.js'; describe(`sbb-expansion-panel-content`, () => { it('renders', async () => { diff --git a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.stories.ts b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.stories.ts index 052d456f25..5f4b2e71b9 100644 --- a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.stories.ts +++ b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.stories.ts @@ -4,7 +4,7 @@ import { html } from 'lit'; import readme from './readme.md?raw'; -import '../../card'; +import '../../card.js'; const Template = (): TemplateResult => html` diff --git a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.ts b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.ts index abcd11ddf6..f75db2825d 100644 --- a/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.ts +++ b/src/components/expansion-panel/expansion-panel-content/expansion-panel-content.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { hostAttributes } from '../../core/common-behaviors'; +import { hostAttributes } from '../../core/decorators.js'; import style from './expansion-panel-content.scss?lit&inline'; diff --git a/src/components/expansion-panel/expansion-panel-content/index.ts b/src/components/expansion-panel/expansion-panel-content/index.ts deleted file mode 100644 index bd7ab61267..0000000000 --- a/src/components/expansion-panel/expansion-panel-content/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './expansion-panel-content'; diff --git a/src/components/expansion-panel/expansion-panel-header.ts b/src/components/expansion-panel/expansion-panel-header.ts new file mode 100644 index 0000000000..be28523d25 --- /dev/null +++ b/src/components/expansion-panel/expansion-panel-header.ts @@ -0,0 +1 @@ +export * from './expansion-panel-header/expansion-panel-header.js'; diff --git a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.e2e.ts b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.e2e.ts index 2d5d311ea1..3124258eb5 100644 --- a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.e2e.ts +++ b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.e2e.ts @@ -1,10 +1,10 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { EventSpy } from '../../core/testing'; -import { fixture } from '../../core/testing/private'; +import { fixture } from '../../core/testing/private.js'; +import { EventSpy } from '../../core/testing.js'; -import { SbbExpansionPanelHeaderElement } from './expansion-panel-header'; +import { SbbExpansionPanelHeaderElement } from './expansion-panel-header.js'; describe(`sbb-expansion-panel-header with ${fixture.name}`, () => { let element: SbbExpansionPanelHeaderElement; diff --git a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.scss b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.scss index c7aec04b13..eca77f8c70 100644 --- a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.scss +++ b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.scss @@ -1,8 +1,7 @@ @use '../../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-expansion-panel-header-cursor: pointer; @@ -14,6 +13,8 @@ @include sbb.if-forced-colors { --sbb-expansion-panel-header-text-color: ButtonText; } + + display: block; } :host([disabled]) { diff --git a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.spec.ts b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.spec.ts index 132dbdae2d..db72720674 100644 --- a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.spec.ts +++ b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.spec.ts @@ -1,10 +1,10 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; -import './expansion-panel-header'; -import '../../icon'; +import './expansion-panel-header.js'; +import '../../icon.js'; describe(`sbb-expansion-panel-header`, () => { it('renders collapsed', async () => { diff --git a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.stories.ts b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.stories.ts index 8dda9bb9d3..cb86cf80c6 100644 --- a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.stories.ts +++ b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.stories.ts @@ -4,7 +4,7 @@ import { html } from 'lit'; import readme from './readme.md?raw'; -import '../../card'; +import '../../card.js'; const Template = (): TemplateResult => html` diff --git a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.ts b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.ts index 04bcfbe588..59ddc3bddb 100644 --- a/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.ts +++ b/src/components/expansion-panel/expansion-panel-header/expansion-panel-header.ts @@ -1,18 +1,14 @@ -import { type CSSResultGroup, nothing, type TemplateResult } from 'lit'; -import { html } from 'lit'; +import { type CSSResultGroup, html, nothing, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { - NamedSlotStateController, - SbbButtonBaseElement, - SbbDisabledTabIndexActionMixin, - SbbIconNameMixin, - hostAttributes, -} from '../../core/common-behaviors'; -import { EventEmitter, ConnectedAbortController } from '../../core/eventing'; -import type { SbbExpansionPanelElement } from '../expansion-panel'; +import { SbbButtonBaseElement } from '../../core/base-elements.js'; +import { SbbConnectedAbortController, SbbSlotStateController } from '../../core/controllers.js'; +import { hostAttributes } from '../../core/decorators.js'; +import { EventEmitter } from '../../core/eventing.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; +import { SbbIconNameMixin } from '../../icon.js'; +import type { SbbExpansionPanelElement } from '../expansion-panel.js'; -import '../../icon'; import style from './expansion-panel-header.scss?lit&inline'; /** @@ -42,8 +38,8 @@ export class SbbExpansionPanelHeaderElement extends SbbDisabledTabIndexActionMix bubbles: true, }, ); - private _abort = new ConnectedAbortController(this); - private _namedSlots = new NamedSlotStateController(this, () => this._setDataIconAttribute()); + private _abort = new SbbConnectedAbortController(this); + private _namedSlots = new SbbSlotStateController(this, () => this._setDataIconAttribute()); public override connectedCallback(): void { super.connectedCallback(); diff --git a/src/components/expansion-panel/expansion-panel-header/index.ts b/src/components/expansion-panel/expansion-panel-header/index.ts deleted file mode 100644 index 5323dabf32..0000000000 --- a/src/components/expansion-panel/expansion-panel-header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './expansion-panel-header'; diff --git a/src/components/expansion-panel/expansion-panel.ts b/src/components/expansion-panel/expansion-panel.ts new file mode 100644 index 0000000000..29326b1a46 --- /dev/null +++ b/src/components/expansion-panel/expansion-panel.ts @@ -0,0 +1 @@ +export * from './expansion-panel/expansion-panel.js'; diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.e2e.ts b/src/components/expansion-panel/expansion-panel/expansion-panel.e2e.ts index 7816bf8d6f..545cc222d0 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.e2e.ts +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.e2e.ts @@ -1,13 +1,13 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing'; -import { fixture } from '../../core/testing/private'; -import type { SbbExpansionPanelContentElement } from '../expansion-panel-content'; -import '../expansion-panel-content'; -import { SbbExpansionPanelHeaderElement } from '../expansion-panel-header'; +import { fixture } from '../../core/testing/private.js'; +import { waitForCondition, waitForLitRender, EventSpy } from '../../core/testing.js'; +import type { SbbExpansionPanelContentElement } from '../expansion-panel-content.js'; +import '../expansion-panel-content.js'; +import { SbbExpansionPanelHeaderElement } from '../expansion-panel-header.js'; -import { SbbExpansionPanelElement } from './expansion-panel'; +import { SbbExpansionPanelElement } from './expansion-panel.js'; describe(`sbb-expansion-panel with ${fixture.name}`, () => { let element: SbbExpansionPanelElement; @@ -23,8 +23,8 @@ describe(`sbb-expansion-panel with ${fixture.name}`, () => { { modules: [ './expansion-panel.ts', - '../expansion-panel-header/index.ts', - '../expansion-panel-content/index.ts', + '../expansion-panel-header.ts', + '../expansion-panel-content.ts', ], }, ); @@ -57,8 +57,8 @@ describe(`sbb-expansion-panel with ${fixture.name}`, () => { { modules: [ './expansion-panel.ts', - '../expansion-panel-header/index.ts', - '../expansion-panel-content/index.ts', + '../expansion-panel-header.ts', + '../expansion-panel-content.ts', ], }, ); diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.scss b/src/components/expansion-panel/expansion-panel/expansion-panel.scss index 841fca812e..432eb89cdb 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.scss +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.scss @@ -1,8 +1,7 @@ @use '../../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; // Open/Close animation vars $open-anim-rows-from: 0fr; @@ -30,6 +29,8 @@ $open-anim-opacity-to: 1; // Vars which will be used by child components --sbb-expansion-panel-title-gap: var(--sbb-spacing-fixed-4x); --sbb-expansion-panel-icon-size: var(--sbb-size-icon-ui-medium); + + display: block; } :host([disabled]) { diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.spec.ts b/src/components/expansion-panel/expansion-panel/expansion-panel.spec.ts index 146c99ade4..a84abd6b8c 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.spec.ts +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.spec.ts @@ -1,13 +1,13 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; -import type { SbbExpansionPanelElement } from './expansion-panel'; +import type { SbbExpansionPanelElement } from './expansion-panel.js'; -import './expansion-panel'; -import '../expansion-panel-header'; -import '../expansion-panel-content'; +import './expansion-panel.js'; +import '../expansion-panel-header.js'; +import '../expansion-panel-content.js'; describe(`sbb-expansion-panel`, () => { describe('renders', () => { diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.stories.ts b/src/components/expansion-panel/expansion-panel/expansion-panel.stories.ts index ed8cc67e75..9dfe4e7e71 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.stories.ts +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.stories.ts @@ -10,16 +10,15 @@ import type { } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html, nothing } from 'lit'; -import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../../core/dom'; -import { SbbExpansionPanelHeaderElement } from '../expansion-panel-header'; +import { sbbSpread } from '../../../storybook/helpers/spread.js'; +import { SbbExpansionPanelHeaderElement } from '../expansion-panel-header.js'; -import { SbbExpansionPanelElement } from './expansion-panel'; +import { SbbExpansionPanelElement } from './expansion-panel.js'; import readme from './readme.md?raw'; -import '../expansion-panel-content'; -import '../../icon'; +import '../expansion-panel-content.js'; +import '../../icon.js'; const longText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer enim elit, ultricies in tincidunt quis, mattis eu quam. Nulla sit amet lorem fermentum, molestie nunc ut, hendrerit risus. Vestibulum rutrum elit et @@ -102,8 +101,8 @@ const defaultArgs: Args = { iconName: undefined, contentText: 'Content', expanded: false, - 'title-level': titleLevel.options[2], - color: color.options[0], + 'title-level': titleLevel.options![2], + color: color.options![0], borderless: false, disabled: false, }; @@ -141,7 +140,7 @@ export const Default: StoryObj = { export const Milk: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options[1] }, + args: { ...defaultArgs, color: color.options![1] }, }; export const Borderless: StoryObj = { @@ -171,7 +170,7 @@ export const WithSlottedIcon: StoryObj = { export const NoHeadingTag: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...defaultArgs, 'title-level': titleLevel.options[6] }, + args: { ...defaultArgs, 'title-level': titleLevel.options![6] }, }; export const Expanded: StoryObj = { @@ -192,21 +191,13 @@ export const LongText: StoryObj = { args: { ...defaultArgs, headerText: longText, contentText: longText }, }; -const wrapperStyle = (context: StoryContext): Record => ({ - 'background-color': - context.args.color === 'white' && context.args.borderless - ? '#bdbdbd' - : 'var(--sbb-color-white)', -}); - const meta: Meta = { - decorators: [ - (story, context) => html` -
${story()}
- `, - withActions as Decorator, - ], + decorators: [withActions as Decorator], parameters: { + backgroundColor: (context: StoryContext) => + context.args.color === 'white' && context.args.borderless + ? 'var(--sbb-color-cement)' + : 'var(--sbb-color-white)', actions: { handles: [ SbbExpansionPanelElement.events.willOpen, @@ -216,9 +207,6 @@ const meta: Meta = { SbbExpansionPanelHeaderElement.events.toggleExpanded, ], }, - backgrounds: { - disable: true, - }, docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/expansion-panel/expansion-panel/expansion-panel.ts b/src/components/expansion-panel/expansion-panel/expansion-panel.ts index 52ca80ff33..fbcbda8237 100644 --- a/src/components/expansion-panel/expansion-panel/expansion-panel.ts +++ b/src/components/expansion-panel/expansion-panel/expansion-panel.ts @@ -1,15 +1,15 @@ import type { CSSResultGroup, TemplateResult } from 'lit'; import { LitElement } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { html, unsafeStatic } from 'lit/static-html.js'; -import { SbbHydrationMixin } from '../../core/common-behaviors'; -import { setAttribute } from '../../core/dom'; -import { EventEmitter, ConnectedAbortController } from '../../core/eventing'; -import type { SbbOverlayState } from '../../core/overlay'; -import type { SbbTitleLevel } from '../../title'; -import type { SbbExpansionPanelContentElement } from '../expansion-panel-content'; -import type { SbbExpansionPanelHeaderElement } from '../expansion-panel-header'; +import { SbbConnectedAbortController } from '../../core/controllers.js'; +import { EventEmitter } from '../../core/eventing.js'; +import type { SbbOpenedClosedState } from '../../core/interfaces.js'; +import { SbbHydrationMixin } from '../../core/mixins.js'; +import type { SbbTitleLevel } from '../../title.js'; +import type { SbbExpansionPanelContentElement } from '../expansion-panel-content.js'; +import type { SbbExpansionPanelHeaderElement } from '../expansion-panel-header.js'; import style from './expansion-panel.scss?lit&inline'; @@ -65,7 +65,15 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) { /** Whether the panel has no border. */ @property({ reflect: true, type: Boolean }) public borderless = false; - @state() private _state: SbbOverlayState = 'closed'; + /** + * The state of the notification. + */ + private set _state(state: SbbOpenedClosedState) { + this.setAttribute('data-state', state); + } + private get _state(): SbbOpenedClosedState { + return this.getAttribute('data-state') as SbbOpenedClosedState; + } /** Emits whenever the `sbb-expansion-panel` starts the opening transition. */ private _willOpen: EventEmitter = new EventEmitter( @@ -95,7 +103,7 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) { private _progressiveId = `-${++nextId}`; private _headerRef?: SbbExpansionPanelHeaderElement; private _contentRef?: SbbExpansionPanelContentElement; - private _abort = new ConnectedAbortController(this); + private _abort = new SbbConnectedAbortController(this); private _initialized: boolean = false; public override connectedCallback(): void { @@ -109,7 +117,7 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) { public override disconnectedCallback(): void { super.disconnectedCallback(); this._transitionEventController?.abort(); - this.toggleAttribute('data-accordion', false); + this.removeAttribute('data-accordion'); } protected override firstUpdated(): void { @@ -196,7 +204,6 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) { } protected override render(): TemplateResult { - setAttribute(this, 'data-state', this._state); const TAGNAME = this.titleLevel ? `h${this.titleLevel}` : 'div'; /* eslint-disable lit/binding-positions */ @@ -212,7 +219,6 @@ export class SbbExpansionPanelElement extends SbbHydrationMixin(LitElement) {
`; - /* eslint-disable lit/binding-positions */ } } diff --git a/src/components/expansion-panel/expansion-panel/index.ts b/src/components/expansion-panel/expansion-panel/index.ts deleted file mode 100644 index b498fbb4d6..0000000000 --- a/src/components/expansion-panel/expansion-panel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './expansion-panel'; diff --git a/src/components/expansion-panel/index.ts b/src/components/expansion-panel/index.ts deleted file mode 100644 index 336b111207..0000000000 --- a/src/components/expansion-panel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './expansion-panel'; -export * from './expansion-panel-content'; -export * from './expansion-panel-header'; diff --git a/src/components/file-selector.ts b/src/components/file-selector.ts new file mode 100644 index 0000000000..e7ed86ce1f --- /dev/null +++ b/src/components/file-selector.ts @@ -0,0 +1 @@ +export * from './file-selector/file-selector.js'; diff --git a/src/components/file-selector/file-selector.e2e.ts b/src/components/file-selector/file-selector.e2e.ts index 9352c0af4f..4c0887fcd1 100644 --- a/src/components/file-selector/file-selector.e2e.ts +++ b/src/components/file-selector/file-selector.e2e.ts @@ -1,12 +1,12 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import type { SbbSecondaryButtonElement } from '../button'; -import { EventSpy, waitForLitRender } from '../core/testing'; -import { fixture } from '../core/testing/private'; +import type { SbbSecondaryButtonElement } from '../button.js'; +import { fixture } from '../core/testing/private.js'; +import { EventSpy, waitForLitRender } from '../core/testing.js'; -import { SbbFileSelectorElement } from './file-selector'; -import '../button/secondary-button'; +import { SbbFileSelectorElement } from './file-selector.js'; +import '../button/secondary-button.js'; function addFilesToComponentInput( elem: SbbFileSelectorElement, diff --git a/src/components/file-selector/file-selector.scss b/src/components/file-selector/file-selector.scss index 4b8d1dfc77..0eec4f9b3a 100644 --- a/src/components/file-selector/file-selector.scss +++ b/src/components/file-selector/file-selector.scss @@ -1,8 +1,7 @@ @use '../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-file-selector-color: var(--sbb-color-black); @@ -15,6 +14,7 @@ ); --sbb-file-selector-transition-easing-function: var(--sbb-animation-easing); + display: block; width: #{sbb.px-to-rem-build(320)}; } diff --git a/src/components/file-selector/file-selector.spec.ts b/src/components/file-selector/file-selector.spec.ts index cb1657c245..4cfbe386c1 100644 --- a/src/components/file-selector/file-selector.spec.ts +++ b/src/components/file-selector/file-selector.spec.ts @@ -1,10 +1,10 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { waitForLitRender } from '../core/testing'; -import { fixture, testA11yTreeSnapshot } from '../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; +import { waitForLitRender } from '../core/testing.js'; -import './file-selector'; +import './file-selector.js'; describe(`sbb-file-selector`, () => { it('renders default', async () => { diff --git a/src/components/file-selector/file-selector.stories.ts b/src/components/file-selector/file-selector.stories.ts index 68a368dc6f..f88ff6c782 100644 --- a/src/components/file-selector/file-selector.stories.ts +++ b/src/components/file-selector/file-selector.stories.ts @@ -4,13 +4,13 @@ import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/web-c import type { TemplateResult } from 'lit'; import { html } from 'lit'; -import { sbbSpread } from '../core/dom'; -import type { SbbFormErrorElement } from '../form-error'; +import { sbbSpread } from '../../storybook/helpers/spread.js'; +import type { SbbFormErrorElement } from '../form-error.js'; -import { SbbFileSelectorElement } from './file-selector'; +import { SbbFileSelectorElement } from './file-selector.js'; import readme from './readme.md?raw'; -import '../form-error'; +import '../form-error.js'; const variant: InputType = { control: { @@ -67,11 +67,11 @@ const defaultArgTypes: ArgTypes = { }; const defaultArgs: Args = { - variant: variant.options[0], + variant: variant.options![0], disabled: false, 'title-content': 'Title', multiple: false, - 'multiple-mode': multipleMode.options[0], + 'multiple-mode': multipleMode.options![0], accept: undefined, 'accessibility-label': 'Select from hard disk', }; @@ -126,25 +126,25 @@ export const DefaultMulti: StoryObj = { export const DefaultMultiPersistent: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...multipleDefaultArgs, 'multiple-mode': multipleMode.options[1] }, + args: { ...multipleDefaultArgs, 'multiple-mode': multipleMode.options![1] }, }; export const Dropzone: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...defaultArgs, variant: variant.options[1] }, + args: { ...defaultArgs, variant: variant.options![1] }, }; export const DropzoneDisabled: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...defaultArgs, variant: variant.options[1], disabled: true }, + args: { ...defaultArgs, variant: variant.options![1], disabled: true }, }; export const DropzoneMulti: StoryObj = { render: Template, argTypes: defaultArgTypes, - args: { ...multipleDefaultArgs, variant: variant.options[1] }, + args: { ...multipleDefaultArgs, variant: variant.options![1] }, }; export const DropzoneMultiPersistent: StoryObj = { @@ -152,8 +152,8 @@ export const DropzoneMultiPersistent: StoryObj = { argTypes: defaultArgTypes, args: { ...multipleDefaultArgs, - variant: variant.options[1], - 'multiple-mode': multipleMode.options[1], + variant: variant.options![1], + 'multiple-mode': multipleMode.options![1], }, }; @@ -166,7 +166,7 @@ export const DefaultWithError: StoryObj = { export const DropzoneWithError: StoryObj = { render: TemplateWithError, argTypes: defaultArgTypes, - args: { ...defaultArgs, variant: variant.options[1] }, + args: { ...defaultArgs, variant: variant.options![1] }, }; export const DefaultOnlyPDF: StoryObj = { @@ -176,14 +176,11 @@ export const DefaultOnlyPDF: StoryObj = { }; const meta: Meta = { - decorators: [(story) => html`
${story()}
`, withActions as Decorator], + decorators: [withActions as Decorator], parameters: { actions: { handles: [SbbFileSelectorElement.events.fileChangedEvent], }, - backgrounds: { - disable: true, - }, docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/file-selector/file-selector.ts b/src/components/file-selector/file-selector.ts index f2da1e737f..49f5bbb696 100644 --- a/src/components/file-selector/file-selector.ts +++ b/src/components/file-selector/file-selector.ts @@ -4,26 +4,24 @@ import { customElement, property, state } from 'lit/decorators.js'; import { ref } from 'lit/directives/ref.js'; import { html, unsafeStatic } from 'lit/static-html.js'; -import type { SbbSecondaryButtonStaticElement } from '../button'; -import { sbbInputModalityDetector } from '../core/a11y'; -import { - LanguageController, - NamedSlotStateController, - SbbDisabledMixin, -} from '../core/common-behaviors'; -import { EventEmitter } from '../core/eventing'; +import type { SbbSecondaryButtonStaticElement } from '../button.js'; +import { sbbInputModalityDetector } from '../core/a11y.js'; +import { SbbLanguageController, SbbSlotStateController } from '../core/controllers.js'; +import { EventEmitter } from '../core/eventing.js'; import { i18nFileSelectorButtonLabel, i18nFileSelectorCurrentlySelected, i18nFileSelectorDeleteFile, i18nFileSelectorSubtitleLabel, -} from '../core/i18n'; -import '../button/secondary-button'; -import '../button/secondary-button-static'; -import '../icon'; +} from '../core/i18n.js'; +import { SbbDisabledMixin } from '../core/mixins.js'; import style from './file-selector.scss?lit&inline'; +import '../button/secondary-button.js'; +import '../button/secondary-button-static.js'; +import '../icon.js'; + export type DOMEvent = globalThis.Event; /** @@ -91,11 +89,11 @@ export class SbbFileSelectorElement extends SbbDisabledMixin(LitElement) { private _suffixes: string[] = ['B', 'kB', 'MB', 'GB', 'TB']; private _liveRegion!: HTMLParagraphElement; - private _language = new LanguageController(this); + private _language = new SbbLanguageController(this); public constructor() { super(); - new NamedSlotStateController(this); + new SbbSlotStateController(this); } private _blockEvent(event: DragEvent): void { @@ -144,7 +142,7 @@ export class SbbFileSelectorElement extends SbbDisabledMixin(LitElement) { private _onBlur(): void { if (sbbInputModalityDetector.mostRecentModality === 'keyboard') { - this._loadButton.toggleAttribute('data-focus-visible', false); + this._loadButton.removeAttribute('data-focus-visible'); } } @@ -271,7 +269,6 @@ export class SbbFileSelectorElement extends SbbDisabledMixin(LitElement) { )} `; - /* eslint-disable lit/binding-positions */ } protected override render(): TemplateResult { diff --git a/src/components/file-selector/index.ts b/src/components/file-selector/index.ts deleted file mode 100644 index 8fcce72ed3..0000000000 --- a/src/components/file-selector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './file-selector'; diff --git a/src/components/footer.ts b/src/components/footer.ts new file mode 100644 index 0000000000..9c739378a5 --- /dev/null +++ b/src/components/footer.ts @@ -0,0 +1 @@ +export * from './footer/footer.js'; diff --git a/src/components/footer/footer.e2e.ts b/src/components/footer/footer.e2e.ts index ebffd6b2de..415b34294d 100644 --- a/src/components/footer/footer.e2e.ts +++ b/src/components/footer/footer.e2e.ts @@ -1,9 +1,9 @@ import { assert } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture } from '../core/testing/private'; +import { fixture } from '../core/testing/private.js'; -import { SbbFooterElement } from './footer'; +import { SbbFooterElement } from './footer.js'; describe(`sbb-footer with ${fixture.name}`, () => { let element: SbbFooterElement; diff --git a/src/components/footer/footer.scss b/src/components/footer/footer.scss index b30921764c..2edfc54751 100644 --- a/src/components/footer/footer.scss +++ b/src/components/footer/footer.scss @@ -1,8 +1,7 @@ @use '../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-footer-gap-horizontal: var(--sbb-grid-base-gutter-responsive); @@ -14,6 +13,8 @@ @include sbb.mq($from: small) { --sbb-footer-clock-width: #{sbb.px-to-rem-build(112)}; } + + display: block; } :host([negative]) { diff --git a/src/components/footer/footer.spec.ts b/src/components/footer/footer.spec.ts index 4e9faeaa20..48e1b6d8e8 100644 --- a/src/components/footer/footer.spec.ts +++ b/src/components/footer/footer.spec.ts @@ -1,11 +1,11 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; -import type { SbbFooterElement } from './footer'; +import type { SbbFooterElement } from './footer.js'; -import './footer'; +import './footer.js'; describe(`sbb-footer`, () => { it('renders', async () => { diff --git a/src/components/footer/footer.stories.ts b/src/components/footer/footer.stories.ts index ee9f403964..3c3d391a30 100644 --- a/src/components/footer/footer.stories.ts +++ b/src/components/footer/footer.stories.ts @@ -1,21 +1,21 @@ import type { InputType } from '@storybook/types'; import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; -import isChromatic from 'chromatic'; +import isChromatic from 'chromatic/isChromatic'; import type { TemplateResult } from 'lit'; import { html } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../core/dom'; +import { sbbSpread } from '../../storybook/helpers/spread.js'; import readme from './readme.md?raw'; -import '../clock'; -import '../button'; -import '../divider'; -import '../link'; -import '../link-list'; -import '../title'; -import './footer'; +import '../clock.js'; +import '../button.js'; +import '../divider.js'; +import '../link.js'; +import '../link-list.js'; +import '../title.js'; +import './footer.js'; const variant: InputType = { control: { @@ -50,7 +50,7 @@ const defaultArgTypes: ArgTypes = { }; const defaultArgs: Args = { - variant: variant.options[1], + variant: variant.options![1], negative: false, expanded: false, 'accessibility-title': 'Footer', @@ -259,7 +259,7 @@ export const FooterDefault: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, - variant: variant.options[0], + variant: variant.options![0], }, }; @@ -268,7 +268,7 @@ export const FooterDefaultNegative: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, - variant: variant.options[0], + variant: variant.options![0], negative: true, }, }; @@ -278,7 +278,7 @@ export const FooterDefaultExpanded: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, - variant: variant.options[0], + variant: variant.options![0], expanded: true, }, }; diff --git a/src/components/footer/footer.ts b/src/components/footer/footer.ts index 5770afe5bb..8cfc9d690b 100644 --- a/src/components/footer/footer.ts +++ b/src/components/footer/footer.ts @@ -3,8 +3,8 @@ import { LitElement, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { html, unsafeStatic } from 'lit/static-html.js'; -import { SbbNegativeMixin } from '../core/common-behaviors'; -import type { SbbTitleLevel } from '../title'; +import { SbbNegativeMixin } from '../core/mixins.js'; +import type { SbbTitleLevel } from '../title.js'; import style from './footer.scss?lit&inline'; diff --git a/src/components/footer/index.ts b/src/components/footer/index.ts deleted file mode 100644 index a058eae01c..0000000000 --- a/src/components/footer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './footer'; diff --git a/src/components/form-error.ts b/src/components/form-error.ts new file mode 100644 index 0000000000..ed829dac44 --- /dev/null +++ b/src/components/form-error.ts @@ -0,0 +1 @@ +export * from './form-error/form-error.js'; diff --git a/src/components/form-error/__snapshots__/form-error.spec.snap.js b/src/components/form-error/__snapshots__/form-error.spec.snap.js index 0fea97c31a..8c65c24270 100644 --- a/src/components/form-error/__snapshots__/form-error.spec.snap.js +++ b/src/components/form-error/__snapshots__/form-error.spec.snap.js @@ -1,7 +1,14 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["sbb-form-error renders"] = +snapshots["sbb-form-error renders DOM"] = +` + Required + +`; +/* end snapshot sbb-form-error renders DOM */ + +snapshots["sbb-form-error renders Shadow DOM"] = ` @@ -11,9 +18,9 @@ snapshots["sbb-form-error renders"] = `; -/* end snapshot sbb-form-error renders */ +/* end snapshot sbb-form-error renders Shadow DOM */ -snapshots["sbb-form-error A11y tree Chrome"] = +snapshots["sbb-form-error renders A11y tree Chrome"] = `

{ "role": "WebArea", @@ -27,9 +34,9 @@ snapshots["sbb-form-error A11y tree Chrome"] = }

`; -/* end snapshot sbb-form-error A11y tree Chrome */ +/* end snapshot sbb-form-error renders A11y tree Chrome */ -snapshots["sbb-form-error A11y tree Firefox"] = +snapshots["sbb-form-error renders A11y tree Firefox"] = `

{ "role": "document", @@ -43,5 +50,5 @@ snapshots["sbb-form-error A11y tree Firefox"] = }

`; -/* end snapshot sbb-form-error A11y tree Firefox */ +/* end snapshot sbb-form-error renders A11y tree Firefox */ diff --git a/src/components/form-error/form-error.e2e.ts b/src/components/form-error/form-error.e2e.ts index 2e8009994f..6d3b148b1a 100644 --- a/src/components/form-error/form-error.e2e.ts +++ b/src/components/form-error/form-error.e2e.ts @@ -1,9 +1,9 @@ import { assert } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture } from '../core/testing/private'; +import { fixture } from '../core/testing/private.js'; -import { SbbFormErrorElement } from './form-error'; +import { SbbFormErrorElement } from './form-error.js'; describe(`sbb-form-error with ${fixture.name}`, () => { let element: SbbFormErrorElement; diff --git a/src/components/form-error/form-error.scss b/src/components/form-error/form-error.scss index 58d1adb0d1..7f8d699f8f 100644 --- a/src/components/form-error/form-error.scss +++ b/src/components/form-error/form-error.scss @@ -1,16 +1,16 @@ @use '../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { - --sbb-form-error-height: calc(var(--sbb-typo-line-height-body-text) * 1em); --sbb-form-error-color: var(--sbb-color-red125); + --sbb-form-error-height: calc(var(--sbb-typo-line-height-body-text) * 1em); + --sbb-form-error-icon-size: #{sbb.px-to-rem-build(17)}; // Overrides sbb icon - --sbb-icon-svg-width: var(--sbb-size-icon-form-error); - --sbb-icon-svg-height: var(--sbb-size-icon-form-error); + --sbb-icon-svg-width: var(--sbb-form-error-icon-size); + --sbb-icon-svg-height: var(--sbb-form-error-icon-size); @include sbb.if-forced-colors { --sbb-form-error-color: LinkText !important; diff --git a/src/components/form-error/form-error.spec.ts b/src/components/form-error/form-error.spec.ts index 395f53024d..f28e9e2ba4 100644 --- a/src/components/form-error/form-error.spec.ts +++ b/src/components/form-error/form-error.spec.ts @@ -1,21 +1,28 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; -import './form-error'; +import type { SbbFormErrorElement } from './form-error.js'; + +import './form-error.js'; describe(`sbb-form-error`, () => { - it('renders', async () => { - const root = await fixture(html`Required`); - - expect(root).dom.to.be.equal(` - - Required - - `); - await expect(root).shadowDom.to.be.equalSnapshot(); - }); + let element: SbbFormErrorElement; + + describe('renders', async () => { + beforeEach(async () => { + element = await fixture(html`Required`); + }); - testA11yTreeSnapshot(html`Required`); + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); + }); }); diff --git a/src/components/form-error/form-error.stories.ts b/src/components/form-error/form-error.stories.ts index 90ea0522e8..d82ff3209c 100644 --- a/src/components/form-error/form-error.stories.ts +++ b/src/components/form-error/form-error.stories.ts @@ -2,17 +2,12 @@ import type { InputType } from '@storybook/types'; import type { Meta, StoryContext, StoryObj, Args, ArgTypes } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html } from 'lit'; -import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../core/dom'; +import { sbbSpread } from '../../storybook/helpers/spread.js'; import readme from './readme.md?raw'; -import '../icon'; -import './form-error'; - -const wrapperStyle = (context: StoryContext): Record => ({ - 'background-color': context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', -}); +import '../icon.js'; +import './form-error.js'; const longText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer enim elit, ultricies in tincidunt quis, mattis eu quam. Nulla sit amet lorem fermentum, molestie nunc ut, hendrerit risus. Vestibulum rutrum elit et @@ -84,15 +79,9 @@ export const ErrorWithCustomIconAndLongMessage: StoryObj = { }; const meta: Meta = { - decorators: [ - (story, context) => html` -
${story()}
- `, - ], parameters: { - backgrounds: { - disable: true, - }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/form-error/form-error.ts b/src/components/form-error/form-error.ts index ae0406cfd6..48a06ddeac 100644 --- a/src/components/form-error/form-error.ts +++ b/src/components/form-error/form-error.ts @@ -2,9 +2,7 @@ import type { CSSResultGroup, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { assignId } from '../core/a11y'; -import { SbbNegativeMixin } from '../core/common-behaviors'; -import { isValidAttribute } from '../core/dom'; +import { SbbNegativeMixin } from '../core/mixins.js'; import style from './form-error.scss?lit&inline'; @@ -22,15 +20,14 @@ export class SbbFormErrorElement extends SbbNegativeMixin(LitElement) { public override connectedCallback(): void { super.connectedCallback(); + this.id ||= `sbb-form-error-${nextId++}`; const formField = this.closest?.('sbb-form-field') ?? this.closest?.('[data-form-field]'); if (formField) { - this.negative = isValidAttribute(formField, 'negative'); + this.negative = formField.hasAttribute('negative'); } } protected override render(): TemplateResult { - assignId(() => `sbb-form-error-${++nextId}`)(this); - return html` diff --git a/src/components/form-error/index.ts b/src/components/form-error/index.ts deleted file mode 100644 index fcaf6f8b8b..0000000000 --- a/src/components/form-error/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './form-error'; diff --git a/src/components/form-field.ts b/src/components/form-field.ts new file mode 100644 index 0000000000..348ff6a6c4 --- /dev/null +++ b/src/components/form-field.ts @@ -0,0 +1,2 @@ +export * from './form-field/form-field.js'; +export * from './form-field/form-field-clear.js'; diff --git a/src/components/form-field/form-field-clear.ts b/src/components/form-field/form-field-clear.ts new file mode 100644 index 0000000000..94c8c1b59d --- /dev/null +++ b/src/components/form-field/form-field-clear.ts @@ -0,0 +1 @@ +export * from './form-field-clear/form-field-clear.js'; diff --git a/src/components/form-field/form-field-clear/form-field-clear.e2e.ts b/src/components/form-field/form-field-clear/form-field-clear.e2e.ts index b38771bdfb..fd11ae7371 100644 --- a/src/components/form-field/form-field-clear/form-field-clear.e2e.ts +++ b/src/components/form-field/form-field-clear/form-field-clear.e2e.ts @@ -1,11 +1,11 @@ import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { waitForLitRender } from '../../core/testing'; -import { fixture } from '../../core/testing/private'; -import { SbbFormFieldElement } from '../form-field'; +import { fixture } from '../../core/testing/private.js'; +import { waitForLitRender } from '../../core/testing.js'; +import { SbbFormFieldElement } from '../form-field.js'; -import { SbbFormFieldClearElement } from './form-field-clear'; +import { SbbFormFieldClearElement } from './form-field-clear.js'; describe(`sbb-form-field-clear with ${fixture.name}`, () => { let element: SbbFormFieldClearElement; @@ -19,7 +19,7 @@ describe(`sbb-form-field-clear with ${fixture.name}`, () => { `, - { modules: ['../form-field/index.ts', './form-field-clear.ts'] }, + { modules: ['../form-field.ts', './form-field-clear.ts'] }, ); element = formField.querySelector('sbb-form-field-clear')!; input = formField.querySelector('input')!; @@ -42,7 +42,7 @@ describe(`sbb-form-field-clear with ${fixture.name}`, () => { }); it('is hidden if the form field is disabled', async () => { - input.setAttribute('disabled', ''); + input.toggleAttribute('disabled', true); await waitForLitRender(element); @@ -50,7 +50,7 @@ describe(`sbb-form-field-clear with ${fixture.name}`, () => { }); it('is hidden if the form field is readonly', async () => { - input.setAttribute('readonly', ''); + input.toggleAttribute('readonly', true); await waitForLitRender(element); diff --git a/src/components/form-field/form-field-clear/form-field-clear.scss b/src/components/form-field/form-field-clear/form-field-clear.scss index bf7dad4273..a9a859d44b 100644 --- a/src/components/form-field/form-field-clear/form-field-clear.scss +++ b/src/components/form-field/form-field-clear/form-field-clear.scss @@ -1,10 +1,11 @@ @use '../../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { + display: block; + // Use !important here to not interfere with Firefox focus ring definition // which appears in normalize css of several frameworks. outline: none !important; diff --git a/src/components/form-field/form-field-clear/form-field-clear.spec.ts b/src/components/form-field/form-field-clear/form-field-clear.spec.ts index 9b280cfe0f..927a086120 100644 --- a/src/components/form-field/form-field-clear/form-field-clear.spec.ts +++ b/src/components/form-field/form-field-clear/form-field-clear.spec.ts @@ -1,12 +1,12 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../../core/testing/private'; -import type { SbbFormFieldElement } from '../form-field'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; +import type { SbbFormFieldElement } from '../form-field.js'; -import type { SbbFormFieldClearElement } from './form-field-clear'; -import './form-field-clear'; -import '../form-field'; +import type { SbbFormFieldClearElement } from './form-field-clear.js'; +import './form-field-clear.js'; +import '../form-field.js'; describe(`sbb-form-field-clear`, () => { describe('renders', () => { diff --git a/src/components/form-field/form-field-clear/form-field-clear.stories.ts b/src/components/form-field/form-field-clear/form-field-clear.stories.ts index c5866ea520..55cd2804fb 100644 --- a/src/components/form-field/form-field-clear/form-field-clear.stories.ts +++ b/src/components/form-field/form-field-clear/form-field-clear.stories.ts @@ -10,17 +10,12 @@ import type { } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html } from 'lit'; -import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../../core/dom'; +import { sbbSpread } from '../../../storybook/helpers/spread.js'; import readme from './readme.md?raw'; -import './form-field-clear'; -import '../form-field'; - -const wrapperStyle = (context: StoryContext): Record => ({ - 'background-color': context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', -}); +import './form-field-clear.js'; +import '../form-field.js'; const negativeArg: InputType = { control: { @@ -104,16 +99,10 @@ export const ReadonlyNegative: StoryObj = { }; const meta: Meta = { - decorators: [ - (story, context) => html` -
${story()}
- `, - withActions as Decorator, - ], + decorators: [withActions as Decorator], parameters: { - backgrounds: { - disable: true, - }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/form-field/form-field-clear/form-field-clear.ts b/src/components/form-field/form-field-clear/form-field-clear.ts index 8021b21dae..4802b75522 100644 --- a/src/components/form-field/form-field-clear/form-field-clear.ts +++ b/src/components/form-field/form-field-clear/form-field-clear.ts @@ -2,20 +2,18 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { - hostAttributes, - LanguageController, - SbbButtonBaseElement, - SbbNegativeMixin, -} from '../../core/common-behaviors'; -import { hostContext, isValidAttribute } from '../../core/dom'; -import { ConnectedAbortController } from '../../core/eventing'; -import { i18nClearInput } from '../../core/i18n'; -import type { SbbFormFieldElement } from '../form-field'; -import '../../icon'; +import { SbbButtonBaseElement } from '../../core/base-elements.js'; +import { SbbConnectedAbortController, SbbLanguageController } from '../../core/controllers.js'; +import { hostAttributes } from '../../core/decorators.js'; +import { hostContext } from '../../core/dom.js'; +import { i18nClearInput } from '../../core/i18n.js'; +import { SbbNegativeMixin } from '../../core/mixins.js'; +import type { SbbFormFieldElement } from '../form-field.js'; import style from './form-field-clear.scss?lit&inline'; +import '../../icon.js'; + /** * Combined with `sbb-form-field`, it displays a button which clears the input value. */ @@ -27,8 +25,8 @@ export class SbbFormFieldClearElement extends SbbNegativeMixin(SbbButtonBaseElem public static override styles: CSSResultGroup = style; private _formField?: SbbFormFieldElement; - private _abort = new ConnectedAbortController(this); - private _language = new LanguageController(this); + private _abort = new SbbConnectedAbortController(this); + private _language = new SbbLanguageController(this); public override connectedCallback(): void { super.connectedCallback(); @@ -39,7 +37,7 @@ export class SbbFormFieldClearElement extends SbbNegativeMixin(SbbButtonBaseElem (hostContext('[data-form-field]', this) as SbbFormFieldElement); if (this._formField) { - this.negative = isValidAttribute(this._formField, 'negative'); + this.negative = this._formField.hasAttribute('negative'); } } diff --git a/src/components/form-field/form-field-clear/index.ts b/src/components/form-field/form-field-clear/index.ts deleted file mode 100644 index 54a7606bcd..0000000000 --- a/src/components/form-field/form-field-clear/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './form-field-clear'; diff --git a/src/components/form-field/form-field.ts b/src/components/form-field/form-field.ts new file mode 100644 index 0000000000..8bbb38c246 --- /dev/null +++ b/src/components/form-field/form-field.ts @@ -0,0 +1 @@ +export * from './form-field/form-field.js'; diff --git a/src/components/form-field/form-field/form-field.e2e.ts b/src/components/form-field/form-field/form-field.e2e.ts index 018c6ee65a..24649e64ee 100644 --- a/src/components/form-field/form-field/form-field.e2e.ts +++ b/src/components/form-field/form-field/form-field.e2e.ts @@ -2,12 +2,12 @@ import { assert, expect, nextFrame } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html } from 'lit/static-html.js'; -import { waitForCondition, waitForLitRender } from '../../core/testing'; -import { fixture } from '../../core/testing/private'; -import { SbbOptionElement } from '../../option'; -import { SbbSelectElement } from '../../select'; +import { fixture } from '../../core/testing/private.js'; +import { waitForCondition, waitForLitRender } from '../../core/testing.js'; +import { SbbOptionElement } from '../../option.js'; +import { SbbSelectElement } from '../../select.js'; -import { SbbFormFieldElement } from './form-field'; +import { SbbFormFieldElement } from './form-field.js'; describe(`sbb-form-field with ${fixture.name}`, () => { describe('with input', () => { @@ -129,6 +129,147 @@ describe(`sbb-form-field with ${fixture.name}`, () => { }); }); + describe('with textarea', () => { + let element: SbbFormFieldElement; + let textarea: HTMLTextAreaElement; + + beforeEach(async () => { + element = await fixture(html``, { + modules: ['./form-field.ts'], + }); + textarea = element.querySelector('textarea')!; + }); + + it('renders', async () => { + assert.instanceOf(element, SbbFormFieldElement); + }); + + it('should set default rows', () => { + expect(textarea.rows).to.be.equal(3); + }); + + it('should respect user defined rows attribute', async () => { + element = await fixture( + html``, + { + modules: ['./form-field.ts'], + }, + ); + expect(element.querySelector('textarea')!.rows).to.be.equal(2); + }); + + it('should respect changing rows at a later time', async () => { + textarea.rows = 1; + expect(textarea.rows).to.be.equal(1); + + textarea.setAttribute('rows', '4'); + expect(textarea.rows).to.be.equal(4); + }); + + it('should update empty input state', async () => { + expect(element).to.have.attribute('data-input-empty'); + + textarea.focus(); + await sendKeys({ type: 'v' }); + await waitForLitRender(element); + expect(element).not.to.have.attribute('data-input-empty'); + + await sendKeys({ press: 'Backspace' }); + await waitForLitRender(element); + expect(element).to.have.attribute('data-input-empty'); + + await sendKeys({ type: 'v' }); + await waitForLitRender(element); + expect(element).not.to.have.attribute('data-input-empty'); + + // Clearing value programmatically which does not trigger input event but can be caught by blur event. + textarea.value = ''; + textarea.blur(); + await waitForLitRender(element); + expect(element).to.have.attribute('data-input-empty'); + }); + + it('should react to focus state', async () => { + element = await fixture( + html` + + + `, + { modules: ['./form-field.ts'] }, + ); + textarea = element.querySelector('textarea')!; + + expect(element).not.to.have.attribute('data-input-focused'); + + textarea.focus(); + await sendKeys({ type: 'v' }); + await waitForLitRender(element); + expect(element).to.have.attribute('data-input-focused'); + + textarea.focus(); + await sendKeys({ press: 'Tab' }); + await waitForLitRender(element); + + expect(element).not.to.have.attribute('data-input-focused'); + }); + + it('should assign id to input and reference it in the label', async () => { + const newLabel = document.createElement('label'); + newLabel.textContent = 'Example'; + element.prepend(newLabel); + + await waitForLitRender(element); + const label = element.querySelector('label'); + + expect(textarea.id).to.match(/^sbb-form-field-input-/); + expect(label).to.have.attribute('for', textarea.id); + }); + + it('should reference sbb-form-error', async () => { + // When adding a sbb-form-error + const formError = document.createElement('sbb-form-error'); + element.append(formError); + await waitForLitRender(element); + await nextFrame(); + + // Then input should be linked and sbb-form-error configured + expect(textarea) + .to.have.attribute('aria-describedby') + .match(/^sbb-form-field-error-/); + expect(formError.id).to.be.equal(textarea.getAttribute('aria-describedby')); + expect(formError).to.have.attribute('role', 'status'); + + // When removing sbb-form-error + formError.remove(); + await waitForLitRender(element); + + // Then aria-describedby should be removed + expect(textarea).not.to.have.attribute('aria-describedby'); + }); + + it('should reference sbb-form-error with original aria-describedby', async () => { + textarea.setAttribute('aria-describedby', 'foo'); + // When adding a sbb-form-error + const formError = document.createElement('sbb-form-error'); + element.append(formError); + await waitForLitRender(element); + await nextFrame(); + + // Then input should be linked and original aria-describedby preserved + expect(textarea) + .to.have.attribute('aria-describedby') + .match(/^foo sbb-form-field-error-/); + + // When removing sbb-form-error + + formError.remove(); + await waitForLitRender(element); + + // Then aria-describedby should be set to foo + expect(textarea).to.have.attribute('aria-describedby'); + }); + }); + describe('with sbb-select', () => { let element: SbbFormFieldElement; let select: SbbSelectElement; @@ -141,7 +282,7 @@ describe(`sbb-form-field with ${fixture.name}`, () => { Test `, - { modules: ['./form-field.ts', '../../select/index.ts', '../../option/index.ts'] }, + { modules: ['./form-field.ts', '../../select.ts', '../../option.ts'] }, ); select = element.querySelector('sbb-select')!; }); @@ -175,7 +316,7 @@ describe(`sbb-form-field with ${fixture.name}`, () => { }); it('should focus select on form field click readonly', async () => { - select.setAttribute('readonly', ''); + select.toggleAttribute('readonly', true); await waitForLitRender(element); expect(element).not.to.have.attribute('data-input-focused'); @@ -246,7 +387,7 @@ describe(`sbb-form-field with ${fixture.name}`, () => { `, - { modules: ['./form-field.ts', '../../select/index.ts', '../../option/index.ts'] }, + { modules: ['./form-field.ts', '../../select.ts', '../../option.ts'] }, ); expect(element).to.have.attribute('data-input-empty'); @@ -262,7 +403,7 @@ describe(`sbb-form-field with ${fixture.name}`, () => { `, - { modules: ['./form-field.ts', '../../select/index.ts', '../../option/index.ts'] }, + { modules: ['./form-field.ts', '../../select.ts', '../../option.ts'] }, ); expect(element).not.to.have.attribute('data-input-empty'); @@ -275,7 +416,7 @@ describe(`sbb-form-field with ${fixture.name}`, () => { Displayed Value `, - { modules: ['./form-field.ts', '../../select/index.ts', '../../option/index.ts'] }, + { modules: ['./form-field.ts', '../../select.ts', '../../option.ts'] }, ); element.querySelector('sbb-select')!.value = ''; diff --git a/src/components/form-field/form-field/form-field.scss b/src/components/form-field/form-field/form-field.scss index a307d02272..72ba435f5e 100644 --- a/src/components/form-field/form-field/form-field.scss +++ b/src/components/form-field/form-field/form-field.scss @@ -1,8 +1,7 @@ @use '../../core/styles' as sbb; -// Default component properties, defined for :host. Properties which can not -// travel the shadow boundary are defined through this mixin -@include sbb.host-component-properties; +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; :host { --sbb-form-field-background-color: var(--sbb-color-white); @@ -10,7 +9,7 @@ --sbb-form-field-border-style: solid; --sbb-form-field-border-radius: var(--sbb-border-radius-4x); --sbb-form-field-border-width: var(--sbb-border-width-1x); - --sbb-form-field-height: var(--sbb-size-form-element-m-min-height); + --sbb-form-field-min-height: var(--sbb-size-element-m); --sbb-form-field-label-color: var(--sbb-color-metal); --sbb-form-field-prefix-color: var(--sbb-color-metal); --sbb-form-field-padding-inline: var(--sbb-spacing-fixed-3x); @@ -19,11 +18,24 @@ --sbb-form-field-error-divider-width: var(--sbb-spacing-fixed-1x); --sbb-form-field-error-padding-block-start: var(--sbb-form-field-error-divider-width); --sbb-form-field-gap: var(--sbb-spacing-fixed-2x); - --sbb-form-field-label-to-input-gap: calc(-1 * var(--sbb-spacing-fixed-1x)); + --sbb-form-field-label-to-input-overlapping: var(--sbb-spacing-fixed-1x); --sbb-form-field-select-inline-padding-end: calc( var(--sbb-icon-svg-width) + var(--sbb-form-field-gap) ); --sbb-form-field-overflow: hidden; + --sbb-form-field-label-size: calc( + var(--sbb-font-size-text-xs) * var(--sbb-typo-line-height-body-text) + ); + --sbb-form-field-input-size: calc( + var(--sbb-font-size-text-m) * var(--sbb-typo-line-height-body-text) + ); + --sbb-form-field-margin-block-start: calc( + ( + var(--sbb-form-field-min-height) - var(--sbb-form-field-label-size) - var( + --sbb-form-field-input-size + ) + var(--sbb-form-field-label-to-input-overlapping) + ) / 2 + ); // Lock sbb-icon size --sbb-icon-svg-width: var(--sbb-size-icon-ui-small); @@ -60,12 +72,12 @@ } :host([size='l']) { - --sbb-form-field-height: var(--sbb-size-form-element-l-min-height); + --sbb-form-field-min-height: var(--sbb-size-element-l); --sbb-form-field-padding-inline: var(--sbb-spacing-responsive-xxxs); } :host([error-space='reserve']) { - --form-field-error-min-height: calc( + --sbb-form-field-error-min-height: calc( var(--sbb-typo-line-height-body-text) * var(--sbb-font-size-text-xs) ); --sbb-form-field-error-padding-block-start-override: var(--sbb-form-field-error-divider-width); @@ -154,10 +166,18 @@ } } -// Should be after other definitions to override overflow and background +:host(:is(:not([data-slot-names~='label'], [label]), [hidden-label])) { + --sbb-form-field-label-size: 0rem; + --sbb-form-field-label-to-input-overlapping: 0rem; +} + +// Should be after other definitions to override overflow :host([data-input-type='sbb-slider']) { --sbb-form-field-overflow: visible; - --sbb-form-field-background-color: var(--sbb-color-white); +} + +:host([data-input-type='textarea']) { + --sbb-form-field-max-height: fit-content; } .sbb-form-field__space-wrapper { @@ -169,14 +189,24 @@ .sbb-form-field__wrapper { display: flex; - align-items: center; gap: var(--sbb-form-field-gap); - padding-inline: calc(var(--sbb-form-field-padding-inline) - var(--sbb-form-field-border-width)); - border: var(--sbb-form-field-border-width) var(--sbb-form-field-border-style) - var(--sbb-form-field-border-color); + padding-inline: var(--sbb-form-field-padding-inline); border-radius: var(--sbb-form-field-border-radius); - height: var(--sbb-form-field-height); + min-height: var(--sbb-form-field-min-height); + max-height: var(--sbb-form-field-max-height); background-color: var(--sbb-form-field-background-color); + position: relative; + + // Use ::before to avoid content shifting when border width changes. + &::before { + content: ''; + display: block; + position: absolute; + inset: 0; + border: var(--sbb-form-field-border-width) var(--sbb-form-field-border-style) + var(--sbb-form-field-border-color); + border-radius: var(--sbb-form-field-border-radius); + } :host([data-input-focused][data-focus-origin='keyboard']) & { @include sbb.focus-outline; @@ -185,33 +215,34 @@ // In high contrast, there is no borderless variant @media (forced-colors: none) { :host(:is([borderless], [data-input-type='sbb-slider'])) & { - border-color: transparent; + &::before { + border-color: transparent; + } } :host(:is([data-input-focused], [data-has-popup-open])[borderless]) & { - position: relative; - &::after { content: ''; position: absolute; border-block-end: var(--sbb-border-width-1x) var(--sbb-form-field-border-style) var(--sbb-form-field-border-color); - inset-inline: calc(var(--sbb-form-field-padding-inline) - 2 * var(--sbb-border-width-1x)); - inset-block-end: calc(-2 * var(--sbb-border-width-1x)); + inset-inline: var(--sbb-form-field-padding-inline); + inset-block-end: 0; } } } } +::slotted(*[slot='prefix']) { + color: var(--sbb-form-field-prefix-color); +} + // Ensure slotted inline elements are vertically centered ::slotted(*[slot='prefix']), ::slotted(*[slot='suffix']) { - min-width: var(--sbb-icon-svg-width); display: flex; -} - -::slotted(*[slot='prefix']) { - color: var(--sbb-form-field-prefix-color); + min-width: var(--sbb-icon-svg-width); + margin-block-start: calc((var(--sbb-form-field-min-height) - var(--sbb-size-icon-ui-small)) / 2); } ::slotted(sbb-form-field-clear) { @@ -220,6 +251,11 @@ } } +// As the calendar should be shown below the form field border, we have to stretch the toggle's height. +::slotted(sbb-datepicker-toggle) { + height: calc((var(--sbb-form-field-min-height) + var(--sbb-size-icon-ui-small)) / 2); +} + @include sbb.if-forced-colors { // Align with prefix color ::slotted(*[slot='suffix']) { @@ -230,8 +266,9 @@ .sbb-form-field__select-input-icon { @include sbb.absolute-center-y; - inset-inline-end: 0; position: absolute; + inset-inline-end: 0; + margin-block-start: calc(-1 * var(--sbb-form-field-margin-block-start) / 2); pointer-events: none; color: var(--sbb-form-field-arrow-color); } @@ -239,6 +276,7 @@ .sbb-form-field__input-container { flex: 1 1 auto; position: relative; + margin-block-start: var(--sbb-form-field-margin-block-start); // Prevents overflowing parent min-width: 0; @@ -249,7 +287,7 @@ height: calc(var(--sbb-font-size-text-xs) * var(--sbb-typo-line-height-body-text)); // Moves label down and input up to meet positioning requirements - margin-block-end: var(--sbb-form-field-label-to-input-gap); + margin-block-end: calc(-1 * var(--sbb-form-field-label-to-input-overlapping)); } // To avoid doubled payload, we group the rules. @@ -317,7 +355,7 @@ // Input -.sbb-form-field__input ::slotted(:where(input, select, sbb-select)) { +.sbb-form-field__input ::slotted(:where(input, select, textarea, sbb-select)) { @include sbb.text-m--regular; @include sbb.ellipsis; @include sbb.input-reset; @@ -370,9 +408,25 @@ padding-inline-end: var(--sbb-form-field-select-inline-padding-end); } +.sbb-form-field__input ::slotted(textarea) { + @include sbb.scrollbar; + + position: relative; + resize: none; + + // White-space break needed for Firefox + white-space: break-spaces; + overflow-y: auto; + min-height: calc((var(--sbb-typo-line-height-body-text) * 1em)); + + :host([negative]) & { + @include sbb.scrollbar($negative: true); + } +} + .sbb-form-field__error { display: flex; - min-height: var(--form-field-error-min-height); + min-height: var(--sbb-form-field-error-min-height); margin-block-start: var( --sbb-form-field-error-padding-block-start-override, var(--sbb-form-field-error-padding-block-start) diff --git a/src/components/form-field/form-field/form-field.spec.ts b/src/components/form-field/form-field/form-field.spec.ts index e4ad14bb25..9adc4ee142 100644 --- a/src/components/form-field/form-field/form-field.spec.ts +++ b/src/components/form-field/form-field/form-field.spec.ts @@ -1,11 +1,11 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture, testA11yTreeSnapshot } from '../../core/testing/private'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; -import './form-field'; -import '../../form-error'; -import type { SbbFormFieldElement } from './form-field'; +import './form-field.js'; +import '../../form-error.js'; +import type { SbbFormFieldElement } from './form-field.js'; describe(`sbb-form-field`, () => { describe('renders input', () => { diff --git a/src/components/form-field/form-field/form-field.stories.ts b/src/components/form-field/form-field/form-field.stories.ts index 466eb84ac5..f554458b04 100644 --- a/src/components/form-field/form-field/form-field.stories.ts +++ b/src/components/form-field/form-field/form-field.stories.ts @@ -4,21 +4,49 @@ import type { TemplateResult } from 'lit'; import { html, nothing } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; -import { sbbSpread } from '../../core/dom'; -import type { SbbFormErrorElement } from '../../form-error'; +import type { SbbFormErrorElement } from '../../form-error.js'; import readme from './readme.md?raw'; -import './form-field'; -import '../form-field-clear'; -import '../../button/mini-button'; -import '../../form-error'; -import '../../link'; -import '../../popover'; -import '../../title'; - -const wrapperStyle = (context: StoryContext): Record => ({ - 'background-color': context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', -}); +import './form-field.js'; +import '../form-field-clear.js'; +import '../../button/mini-button.js'; +import '../../form-error.js'; +import '../../link.js'; +import '../../popover.js'; +import '../../title.js'; + +const formField = ( + { + 'error-space': errorSpace, + label, + optional, + size, + borderless, + width, + negative, + 'hidden-label': hiddenLabel, + 'floating-label': floatingLabel, + slottedLabel, + }: Args, + template: TemplateResult, +): TemplateResult => + html` + ${label && !slottedLabel + ? html`` + : label && slottedLabel + ? html`${label}` + : nothing} + ${template} + `; const PopoverTrigger = (): TemplateResult => html` ht `; -const TemplateInput = ({ - 'error-space': errorSpace, - label, - optional, - size, - borderless, - width, - negative, - 'hidden-label': hiddenLabel, - 'floating-label': floatingLabel, - ...args -}: Args): TemplateResult => html` - + html` `; -const TemplateInputWithSlottedSpanLabel = ({ - 'error-space': errorSpace, - label, - optional, - size, - borderless, - width, - negative, - 'hidden-label': hiddenLabel, - 'floating-label': floatingLabel, - ...args -}: Args): TemplateResult => html` - - ${label} - ${TemplateBasicInput(args)} - -`; +const TemplateInput = (args: Args): TemplateResult => formField(args, TemplateBasicInput(args)); + +const TemplateInputWithSlottedSpanLabel = (args: Args): TemplateResult => + formField({ ...args, slottedLabel: true }, TemplateBasicInput(args)); const TemplateInputWithErrorSpace = (args: Args): TemplateResult => { const sbbFormError: SbbFormErrorElement = document.createElement('sbb-form-error'); @@ -124,35 +120,26 @@ const TemplateInputWithErrorSpace = (args: Args): TemplateResult => { return html`
- - ${args.label ? html`` : nothing} - { - const input = event.currentTarget as HTMLInputElement; - if (input.value !== '') { - sbbFormError.remove(); - input.classList.remove(args.cssClass); - } else { - input.closest('sbb-form-field')!.append(sbbFormError); - input.classList.add(args.cssClass); - } - }} - class=${args.cssClass} - placeholder=${args.placeholder} - ?disabled=${args.disabled} - ?readonly=${args.readonly} - /> - ${sbbFormError} - + ${formField( + args, + html` { + const input = event.currentTarget as HTMLInputElement; + if (input.value !== '') { + sbbFormError.remove(); + input.classList.remove(args.cssClass); + } else { + input.closest('sbb-form-field')!.append(sbbFormError); + input.classList.add(args.cssClass); + } + }} + class=${args.cssClass} + placeholder=${args.placeholder} + ?disabled=${args.disabled} + ?readonly=${args.readonly} + /> + ${sbbFormError}`, + )}
Some text, right below the form-field, inside a div. @@ -161,64 +148,96 @@ const TemplateInputWithErrorSpace = (args: Args): TemplateResult => { `; }; -const TemplateInputWithIcons = ({ label, ...args }: Args): TemplateResult => html` - - ${label ? html`` : nothing} - - ${TemplateBasicInput(args)} ${PopoverTrigger()} - -`; +const TemplateInputWithIcons = (args: Args): TemplateResult => + formField( + args, + html` ${TemplateBasicInput(args)} + ${PopoverTrigger()}`, + ); const TemplateInputWithMiniButton = ({ - label, disabled, readonly, active, ...args -}: Args): TemplateResult => html` - - ${label ? html`` : nothing} - ${TemplateBasicInput({ disabled, readonly, ...args })} - - -`; +}: Args): TemplateResult => + formField( + args, + html`${TemplateBasicInput({ disabled, readonly, ...args })} + `, + ); + +const TemplateInputWithClearButton = ({ active, ...args }: Args): TemplateResult => + formField( + args, + html`${TemplateBasicInput(args)} + `, + ); + +const TemplateSelect = (args: Args): TemplateResult => formField(args, TemplateBasicSelect(args)); -const TemplateInputWithClearButton = ({ - label, - disabled, - readonly, - active, - ...args -}: Args): TemplateResult => html` - - ${label ? html`` : nothing} - ${TemplateBasicInput({ disabled, readonly, ...args })} - - -`; +const TemplateSelectWithErrorSpace = (args: Args): TemplateResult => { + const sbbFormError: SbbFormErrorElement = document.createElement('sbb-form-error'); + sbbFormError.setAttribute('slot', 'error'); + sbbFormError.textContent = args.errorText; -const TemplateSelect = (args: Args): TemplateResult => html` - - ${args.label ? html`` : nothing} ${TemplateBasicSelect(args)} - -`; + return html` + +
+ ${formField( + args, + html` + ${sbbFormError}`, + )} +
+
+
+ Some text, right below the form-field, inside a div. +
+
+ + `; +}; -const TemplateSelectWithErrorSpace = (args: Args): TemplateResult => { +const TemplateSelectWithIcons = (args: Args): TemplateResult => + formField( + args, + html` + + + + ${TemplateBasicSelect(args)} ${PopoverTrigger()} + `, + ); + +const TemplateTextarea = (args: Args): TemplateResult => + formField(args, TemplateBasicTextarea(args)); + +const TemplateTextareaWithErrorSpace = (args: Args): TemplateResult => { const sbbFormError: SbbFormErrorElement = document.createElement('sbb-form-error'); sbbFormError.setAttribute('slot', 'error'); sbbFormError.textContent = args.errorText; @@ -226,38 +245,28 @@ const TemplateSelectWithErrorSpace = (args: Args): TemplateResult => { return html`
- - ${args.label ? html`` : nothing} - - ${sbbFormError} - + ${formField( + args, + html` + ${sbbFormError}`, + )}
@@ -268,16 +277,14 @@ const TemplateSelectWithErrorSpace = (args: Args): TemplateResult => { `; }; -const TemplateSelectWithIcons = ({ label, ...args }: Args): TemplateResult => html` - - ${label ? html`` : nothing} - - - - ${TemplateBasicSelect(args)} - ${PopoverTrigger()} - -`; +const TemplateTextareaWithIcon = (args: Args): TemplateResult => + formField( + args, + html` + + + ${TemplateBasicTextarea(args)}`, + ); const placeholder: InputType = { control: { @@ -452,7 +459,7 @@ const basicArgs: Args = { 'floating-label': false, optional: false, borderless: false, - size: size.options[0], + size: size.options![0], negative: false, cssClass: '', placeholder: 'Input placeholder', @@ -460,7 +467,7 @@ const basicArgs: Args = { disabled: false, readonly: false, errorText: 'This is a required field.', - width: width.options[0], + width: width.options![0], active: false, }; @@ -633,10 +640,73 @@ export const SelectOptionalAndIcons: StoryObj = { args: { ...basicArgs, optional: true }, }; +export const Textarea: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs }, +}; + +export const TextareaWithoutBorder: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, borderless: true }, +}; + +export const TextareaDisabled: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, disabled: true }, +}; + +export const TextareaReadonly: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, readonly: true }, +}; + +export const TextareaErrorSpace: StoryObj = { + render: TemplateTextareaWithErrorSpace, + argTypes: basicArgTypes, + args: { ...basicArgs, 'error-space': 'reserve', cssClass: 'sbb-invalid' }, +}; + +export const TextareaOptionalAndIcon: StoryObj = { + render: TemplateTextareaWithIcon, + argTypes: basicArgTypes, + args: { ...basicArgs, optional: true }, +}; + +export const TextareaFloatingLabel: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, 'floating-label': true, value: undefined }, +}; + +export const TextareaFloatingLongLabel: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { + ...basicArgs, + 'floating-label': true, + value: undefined, + label: 'This is a very long label which receives ellipsis', + }, +}; + +export const TextareaFloatingWithIcon: StoryObj = { + render: TemplateTextareaWithIcon, + argTypes: basicArgTypes, + args: { + ...basicArgs, + 'floating-label': true, + value: undefined, + }, +}; + export const InputCollapsedWidth: StoryObj = { render: TemplateInput, argTypes: basicArgTypes, - args: { ...basicArgs, width: width.options[1] }, + args: { ...basicArgs, width: width.options![1] }, }; export const InputWithIconsDisabled: StoryObj = { @@ -802,7 +872,7 @@ export const SelectOptionalAndIconsNegative: StoryObj = { export const InputCollapsedWidthNegative: StoryObj = { render: TemplateInput, argTypes: basicArgTypes, - args: { ...basicArgs, width: width.options[1], negative: true }, + args: { ...basicArgs, width: width.options![1], negative: true }, }; export const InputWithIconsDisabledNegative: StoryObj = { @@ -811,17 +881,76 @@ export const InputWithIconsDisabledNegative: StoryObj = { args: { ...basicArgs, disabled: true, negative: true }, }; +export const TextareaNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, negative: true }, +}; + +export const TextareaWithoutBorderNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, borderless: true, negative: true }, +}; + +export const TextareaDisabledNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, disabled: true, negative: true }, +}; + +export const TextareaReadonlyNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, readonly: true, negative: true }, +}; + +export const TextareaErrorSpaceNegative: StoryObj = { + render: TemplateTextareaWithErrorSpace, + argTypes: basicArgTypes, + args: { ...basicArgs, 'error-space': 'reserve', cssClass: 'sbb-invalid', negative: true }, +}; + +export const TextareaOptionalAndIconNegative: StoryObj = { + render: TemplateTextareaWithIcon, + argTypes: basicArgTypes, + args: { ...basicArgs, optional: true, negative: true }, +}; + +export const TextareaFloatingLabelNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { ...basicArgs, 'floating-label': true, value: undefined, negative: true }, +}; + +export const TextareaFloatingLongLabelNegative: StoryObj = { + render: TemplateTextarea, + argTypes: basicArgTypes, + args: { + ...basicArgs, + 'floating-label': true, + value: undefined, + label: 'This is a very long label which receives ellipsis', + negative: true, + }, +}; + +export const TextareaFloatingWithIconNegative: StoryObj = { + render: TemplateTextareaWithIcon, + argTypes: basicArgTypes, + args: { + ...basicArgs, + 'floating-label': true, + value: undefined, + negative: true, + }, +}; + const meta: Meta = { excludeStories: /.*(Active|ActiveNegative)$/, - decorators: [ - (story, context) => html` -
${story()}
- `, - ], parameters: { - backgrounds: { - disable: true, - }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', docs: { extractComponentDescription: () => readme, }, diff --git a/src/components/form-field/form-field/form-field.ts b/src/components/form-field/form-field/form-field.ts index 3a0456a781..521b1576c1 100644 --- a/src/components/form-field/form-field/form-field.ts +++ b/src/components/form-field/form-field/form-field.ts @@ -1,20 +1,20 @@ -import type { CSSResultGroup, TemplateResult, PropertyValues } from 'lit'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import type { SbbInputModality } from '../../core/a11y'; -import { sbbInputModalityDetector } from '../../core/a11y'; +import type { SbbInputModality } from '../../core/a11y.js'; +import { sbbInputModalityDetector } from '../../core/a11y.js'; import { - LanguageController, - NamedSlotStateController, - SbbNegativeMixin, -} from '../../core/common-behaviors'; -import { isFirefox, isValidAttribute } from '../../core/dom'; -import { ConnectedAbortController } from '../../core/eventing'; -import { i18nOptional } from '../../core/i18n'; -import { AgnosticMutationObserver } from '../../core/observers'; -import type { SbbSelectElement } from '../../select'; -import '../../icon'; + SbbConnectedAbortController, + SbbLanguageController, + SbbSlotStateController, +} from '../../core/controllers.js'; +import { isFirefox, setOrRemoveAttribute } from '../../core/dom.js'; +import { i18nOptional } from '../../core/i18n.js'; +import { SbbNegativeMixin } from '../../core/mixins.js'; +import { AgnosticMutationObserver } from '../../core/observers.js'; +import type { SbbSelectElement } from '../../select.js'; +import '../../icon.js'; import style from './form-field.scss?lit&inline'; @@ -36,7 +36,7 @@ const supportedPopupTagNames = ['sbb-autocomplete', 'sbb-select']; export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { public static override styles: CSSResultGroup = style; - private readonly _supportedNativeInputElements = ['input', 'select']; + private readonly _supportedNativeInputElements = ['input', 'select', 'textarea']; // List of supported element selectors in unnamed slot private readonly _supportedInputElements = [ ...this._supportedNativeInputElements, @@ -46,7 +46,12 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { // List of elements that should not focus input on click private readonly _excludedFocusElements = ['button', 'sbb-popover']; - private readonly _floatingLabelSupportedInputElements = ['input', 'select', 'sbb-select']; + private readonly _floatingLabelSupportedInputElements = [ + 'input', + 'select', + 'sbb-select', + 'textarea', + ]; private readonly _floatingLabelSupportedInputTypes = [ 'email', @@ -104,8 +109,8 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { return this._input; } - private _abort = new ConnectedAbortController(this); - private _language = new LanguageController(this); + private _abort = new SbbConnectedAbortController(this); + private _language = new SbbLanguageController(this); /** * Listens to the changes on `readonly` and `disabled` attributes of ``. @@ -122,7 +127,7 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { public constructor() { super(); - new NamedSlotStateController(this); + new SbbSlotStateController(this); } public override connectedCallback(): void { @@ -154,7 +159,7 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { private _onPopupClose({ target }: CustomEvent): void { if (supportedPopupTagNames.includes((target as HTMLElement).localName)) { - this.toggleAttribute('data-has-popup-open', false); + this.removeAttribute('data-has-popup-open'); } } @@ -183,7 +188,7 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { private _onSlotLabelChange(): void { const labels = Array.from(this.querySelectorAll('label')); - if (labels.length > 1) { + if (import.meta.env.DEV && labels.length > 1) { console.warn( `Detected more than one label in sbb-form-field#${this.id}. Only one label is supported.`, ); @@ -210,12 +215,16 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { this._readInputState(); this._registerInputListener(); + if (this._input.tagName === 'TEXTAREA') { + this._input.setAttribute('rows', this._input.getAttribute('rows') || '3'); + } + this._formFieldAttributeObserver.disconnect(); this._formFieldAttributeObserver.observe(this._input, { attributes: true, attributeFilter: ['readonly', 'disabled', 'class', 'data-sbb-invalid'], }); - this.dataset.inputType = this._input.localName; + this.setAttribute('data-input-type', this._input.localName); this._syncLabelInputReferences(); } @@ -282,8 +291,10 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { 'focusin', () => { this.toggleAttribute('data-input-focused', true); - (this.dataset.focusOrigin as SbbInputModality) = - sbbInputModalityDetector.mostRecentModality; + this.setAttribute( + 'data-focus-origin', + (sbbInputModalityDetector.mostRecentModality as SbbInputModality) ?? '', + ); }, { signal: this._inputAbortController.signal, @@ -292,10 +303,8 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { inputFocusElement.addEventListener( 'focusout', - () => { - delete this.dataset.focusOrigin; - this.toggleAttribute('data-input-focused', false); - }, + () => + ['data-focus-origin', 'data-input-focused'].forEach((name) => this.removeAttribute(name)), { signal: this._inputAbortController.signal, }, @@ -348,8 +357,8 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { if (!this._input) { return; } - this.toggleAttribute('data-readonly', isValidAttribute(this._input, 'readonly')); - this.toggleAttribute('data-disabled', isValidAttribute(this._input, 'disabled')); + this.toggleAttribute('data-readonly', this._input.hasAttribute('readonly')); + this.toggleAttribute('data-disabled', this._input.hasAttribute('disabled')); this.toggleAttribute( 'data-invalid', this._input.hasAttribute('data-sbb-invalid') || @@ -396,10 +405,8 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { } const ariaDescribedby = ids.join(' '); - if (ariaDescribedby) { - this._input?.setAttribute('aria-describedby', ariaDescribedby); - } else { - this._input?.removeAttribute('aria-describedby'); + if (this._input) { + setOrRemoveAttribute(this._input, 'aria-describedby', ariaDescribedby); } } @@ -428,9 +435,7 @@ export class SbbFormFieldElement extends SbbNegativeMixin(LitElement) { private _syncNegative(): void { this.querySelectorAll?.( 'sbb-form-error,sbb-mini-button,sbb-popover-trigger,sbb-form-field-clear,sbb-datepicker-next-day,sbb-datepicker-previous-day,sbb-datepicker-toggle,sbb-select,sbb-autocomplete', - ).forEach((element) => - this.negative ? element.setAttribute('negative', '') : element.removeAttribute('negative'), - ); + ).forEach((element) => element.toggleAttribute('negative', this.negative)); } protected override render(): TemplateResult { diff --git a/src/components/form-field/form-field/index.ts b/src/components/form-field/form-field/index.ts deleted file mode 100644 index 6d5f766fc8..0000000000 --- a/src/components/form-field/form-field/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './form-field'; diff --git a/src/components/form-field/form-field/readme.md b/src/components/form-field/form-field/readme.md index 96e63c4353..282d184c54 100644 --- a/src/components/form-field/form-field/readme.md +++ b/src/components/form-field/form-field/readme.md @@ -21,6 +21,7 @@ The following components are designed to work inside a `sbb-form-field`: - `` - `