diff --git a/src/elements/button/button-link/__snapshots__/button-link.snapshot.spec.snap.js b/src/elements/button/button-link/__snapshots__/button-link.snapshot.spec.snap.js index a6285e81c8..86c17e6626 100644 --- a/src/elements/button/button-link/__snapshots__/button-link.snapshot.spec.snap.js +++ b/src/elements/button/button-link/__snapshots__/button-link.snapshot.spec.snap.js @@ -67,6 +67,7 @@ snapshots["sbb-button-link renders a disabled sbb-button-link with slotted icon aria-disabled="true" class="sbb-action-base sbb-button-link" href="https://www.sbb.ch" + tabindex="-1" > diff --git a/src/elements/button/button-link/button-link.ts b/src/elements/button/button-link/button-link.ts index 37a1b5d487..714f34ee34 100644 --- a/src/elements/button/button-link/button-link.ts +++ b/src/elements/button/button-link/button-link.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; -import { SbbDisabledMixin } from '../../core/mixins.js'; +import { SbbDisabledInteractiveMixin, SbbDisabledMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonPrimaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonPrimaryStyle, SbbButtonCommonElementMixin } fr */ @customElement('sbb-button-link') export class SbbButtonLinkElement extends SbbButtonCommonElementMixin( - SbbDisabledMixin(SbbLinkBaseElement), + SbbDisabledInteractiveMixin(SbbDisabledMixin(SbbLinkBaseElement)), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonPrimaryStyle]; } diff --git a/src/elements/button/button-link/readme.md b/src/elements/button/button-link/readme.md index fa8d4295d5..ab5728e234 100644 --- a/src/elements/button/button-link/readme.md +++ b/src/elements/button/button-link/readme.md @@ -72,17 +72,18 @@ Use the accessibility properties in case of an icon-only button to describe the ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | --------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | -| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | ## Slots diff --git a/src/elements/button/button/__snapshots__/button.snapshot.spec.snap.js b/src/elements/button/button/__snapshots__/button.snapshot.spec.snap.js index 619bc0bef5..7a6679a059 100644 --- a/src/elements/button/button/__snapshots__/button.snapshot.spec.snap.js +++ b/src/elements/button/button/__snapshots__/button.snapshot.spec.snap.js @@ -14,7 +14,6 @@ snapshots["sbb-button renders a sbb-button without icon DOM"] = negative="" role="button" size="m" - tabindex="0" type="button" value="value" > diff --git a/src/elements/button/button/button.ts b/src/elements/button/button/button.ts index f28717e1a1..f956f86828 100644 --- a/src/elements/button/button/button.ts +++ b/src/elements/button/button/button.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonPrimaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonPrimaryStyle, SbbButtonCommonElementMixin } fr */ @customElement('sbb-button') export class SbbButtonElement extends SbbButtonCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonPrimaryStyle]; } diff --git a/src/elements/button/button/readme.md b/src/elements/button/button/readme.md index ee6fbec01e..f1894686c5 100644 --- a/src/elements/button/button/readme.md +++ b/src/elements/button/button/readme.md @@ -62,28 +62,34 @@ sbb-button { Use the accessibility properties in case of an icon-only button to describe the purpose of the `sbb-button` for screen-reader users. -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The
element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/button/common/button-common-stories.ts b/src/elements/button/common/button-common-stories.ts index 8c4bb9cb75..960b9530f5 100644 --- a/src/elements/button/common/button-common-stories.ts +++ b/src/elements/button/common/button-common-stories.ts @@ -39,6 +39,15 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Button', + }, +}; + const name: InputType = { control: { type: 'text', @@ -76,6 +85,7 @@ export const buttonDefaultArgTypes: ArgTypes = { ...commonDefaultArgTypes, type, disabled, + 'disabled-interactive': disabledInteractive, name, value, form, @@ -86,6 +96,7 @@ export const buttonDefaultArgs: Args = { ...commonDefaultArgs, type: type.options![0], disabled: false, + 'disabled-interactive': false, name: 'Button Name', value: undefined, form: undefined, diff --git a/src/elements/button/common/button-link-common-stories.ts b/src/elements/button/common/button-link-common-stories.ts index 118f4fca27..3415d4184b 100644 --- a/src/elements/button/common/button-link-common-stories.ts +++ b/src/elements/button/common/button-link-common-stories.ts @@ -55,6 +55,15 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Button', + }, +}; + const accessibilityLabel: InputType = { control: { type: 'text', @@ -68,6 +77,7 @@ export const buttonLinkDefaultArgTypes: ArgTypes = { rel, download, disabled, + disabledInteractive: disabledInteractive, 'accessibility-label': accessibilityLabel, }; @@ -78,5 +88,6 @@ export const buttonLinkDefaultArgs: Args = { rel: 'noopener', download: false, disabled: false, + 'disabled-interactive': false, 'accessibility-label': undefined, }; diff --git a/src/elements/button/mini-button/mini-button.ts b/src/elements/button/mini-button/mini-button.ts index fa364199b9..a7fd7bb56f 100644 --- a/src/elements/button/mini-button/mini-button.ts +++ b/src/elements/button/mini-button/mini-button.ts @@ -1,7 +1,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbMiniButtonBaseElement } from './mini-button-base-element.js'; import style from './mini-button.scss?lit&inline'; @@ -13,9 +13,7 @@ import style from './mini-button.scss?lit&inline'; * @slot icon - Slot used to display the icon, if one is set */ @customElement('sbb-mini-button') -export class SbbMiniButtonElement extends SbbFocusableDisabledActionMixin( - SbbMiniButtonBaseElement, -) { +export class SbbMiniButtonElement extends SbbDisabledTabIndexActionMixin(SbbMiniButtonBaseElement) { public static override styles: CSSResultGroup = style; } diff --git a/src/elements/button/mini-button/readme.md b/src/elements/button/mini-button/readme.md index d16bcd8b34..8029cbd66d 100644 --- a/src/elements/button/mini-button/readme.md +++ b/src/elements/button/mini-button/readme.md @@ -70,27 +70,33 @@ sbb-mini-button { Use the accessibility properties to describe the purpose of the `sbb-mini-button` for screen-reader users. -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/button/secondary-button-link/__snapshots__/secondary-button-link.snapshot.spec.snap.js b/src/elements/button/secondary-button-link/__snapshots__/secondary-button-link.snapshot.spec.snap.js index 091064df0d..13337d055e 100644 --- a/src/elements/button/secondary-button-link/__snapshots__/secondary-button-link.snapshot.spec.snap.js +++ b/src/elements/button/secondary-button-link/__snapshots__/secondary-button-link.snapshot.spec.snap.js @@ -67,6 +67,7 @@ snapshots["sbb-secondary-button-link renders a disabled sbb-secondary-button-lin aria-disabled="true" class="sbb-action-base sbb-secondary-button-link" href="https://www.sbb.ch" + tabindex="-1" > diff --git a/src/elements/button/secondary-button-link/readme.md b/src/elements/button/secondary-button-link/readme.md index 8eb552d762..3fa66b7b58 100644 --- a/src/elements/button/secondary-button-link/readme.md +++ b/src/elements/button/secondary-button-link/readme.md @@ -77,17 +77,18 @@ Use the accessibility properties in case of an icon-only button to describe the ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | --------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | -| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | ## Slots diff --git a/src/elements/button/secondary-button-link/secondary-button-link.ts b/src/elements/button/secondary-button-link/secondary-button-link.ts index a6db60cfbc..b3893acfad 100644 --- a/src/elements/button/secondary-button-link/secondary-button-link.ts +++ b/src/elements/button/secondary-button-link/secondary-button-link.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; -import { SbbDisabledMixin } from '../../core/mixins.js'; +import { SbbDisabledInteractiveMixin, SbbDisabledMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonSecondaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonSecondaryStyle, SbbButtonCommonElementMixin } */ @customElement('sbb-secondary-button-link') export class SbbSecondaryButtonLinkElement extends SbbButtonCommonElementMixin( - SbbDisabledMixin(SbbLinkBaseElement), + SbbDisabledInteractiveMixin(SbbDisabledMixin(SbbLinkBaseElement)), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonSecondaryStyle]; } diff --git a/src/elements/button/secondary-button/__snapshots__/secondary-button.snapshot.spec.snap.js b/src/elements/button/secondary-button/__snapshots__/secondary-button.snapshot.spec.snap.js index 856d839f46..fc5ba895fb 100644 --- a/src/elements/button/secondary-button/__snapshots__/secondary-button.snapshot.spec.snap.js +++ b/src/elements/button/secondary-button/__snapshots__/secondary-button.snapshot.spec.snap.js @@ -14,7 +14,6 @@ snapshots["sbb-secondary-button renders a sbb-secondary-button without icon DOM" negative="" role="button" size="m" - tabindex="0" type="button" value="value" > diff --git a/src/elements/button/secondary-button/readme.md b/src/elements/button/secondary-button/readme.md index 0b46eff48f..1797f3daa0 100644 --- a/src/elements/button/secondary-button/readme.md +++ b/src/elements/button/secondary-button/readme.md @@ -67,28 +67,34 @@ sbb-secondary-button { Use the accessibility properties in case of an icon-only button to describe the purpose of the `sbb-secondary-button` for screen-reader users. -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/button/secondary-button/secondary-button.ts b/src/elements/button/secondary-button/secondary-button.ts index f365c64c94..415f2d3cdb 100644 --- a/src/elements/button/secondary-button/secondary-button.ts +++ b/src/elements/button/secondary-button/secondary-button.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonSecondaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonSecondaryStyle, SbbButtonCommonElementMixin } */ @customElement('sbb-secondary-button') export class SbbSecondaryButtonElement extends SbbButtonCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonSecondaryStyle]; } diff --git a/src/elements/button/tertiary-button-link/__snapshots__/tertiary-button-link.snapshot.spec.snap.js b/src/elements/button/tertiary-button-link/__snapshots__/tertiary-button-link.snapshot.spec.snap.js index 86827b2880..2781b1fd48 100644 --- a/src/elements/button/tertiary-button-link/__snapshots__/tertiary-button-link.snapshot.spec.snap.js +++ b/src/elements/button/tertiary-button-link/__snapshots__/tertiary-button-link.snapshot.spec.snap.js @@ -67,6 +67,7 @@ snapshots["sbb-tertiary-button-link renders a disabled sbb-tertiary-button-link aria-disabled="true" class="sbb-action-base sbb-tertiary-button-link" href="https://www.sbb.ch" + tabindex="-1" > diff --git a/src/elements/button/tertiary-button-link/readme.md b/src/elements/button/tertiary-button-link/readme.md index 3b1be844b5..f0305783e1 100644 --- a/src/elements/button/tertiary-button-link/readme.md +++ b/src/elements/button/tertiary-button-link/readme.md @@ -77,17 +77,18 @@ Use the accessibility properties in case of an icon-only button to describe the ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | --------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | -| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | ## Slots diff --git a/src/elements/button/tertiary-button-link/tertiary-button-link.ts b/src/elements/button/tertiary-button-link/tertiary-button-link.ts index 1cfc6d810a..72cfba79d2 100644 --- a/src/elements/button/tertiary-button-link/tertiary-button-link.ts +++ b/src/elements/button/tertiary-button-link/tertiary-button-link.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; -import { SbbDisabledMixin } from '../../core/mixins.js'; +import { SbbDisabledInteractiveMixin, SbbDisabledMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonTertiaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonTertiaryStyle, SbbButtonCommonElementMixin } f */ @customElement('sbb-tertiary-button-link') export class SbbTertiaryButtonLinkElement extends SbbButtonCommonElementMixin( - SbbDisabledMixin(SbbLinkBaseElement), + SbbDisabledInteractiveMixin(SbbDisabledMixin(SbbLinkBaseElement)), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonTertiaryStyle]; } diff --git a/src/elements/button/tertiary-button/__snapshots__/tertiary-button.snapshot.spec.snap.js b/src/elements/button/tertiary-button/__snapshots__/tertiary-button.snapshot.spec.snap.js index 26a66884fd..bc471dc362 100644 --- a/src/elements/button/tertiary-button/__snapshots__/tertiary-button.snapshot.spec.snap.js +++ b/src/elements/button/tertiary-button/__snapshots__/tertiary-button.snapshot.spec.snap.js @@ -14,7 +14,6 @@ snapshots["sbb-tertiary-button renders a sbb-tertiary-button without icon DOM"] negative="" role="button" size="m" - tabindex="0" type="button" value="value" > diff --git a/src/elements/button/tertiary-button/readme.md b/src/elements/button/tertiary-button/readme.md index 72b5d60af0..6c5150dbd8 100644 --- a/src/elements/button/tertiary-button/readme.md +++ b/src/elements/button/tertiary-button/readme.md @@ -67,28 +67,34 @@ sbb-tertiary-button { Use the accessibility properties in case of an icon-only button to describe the purpose of the `sbb-tertiary-button` for screen-reader users. -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/button/tertiary-button/tertiary-button.ts b/src/elements/button/tertiary-button/tertiary-button.ts index 928dffa9b2..02c656130b 100644 --- a/src/elements/button/tertiary-button/tertiary-button.ts +++ b/src/elements/button/tertiary-button/tertiary-button.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonTertiaryStyle, SbbButtonCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { buttonCommonStyle, buttonTertiaryStyle, SbbButtonCommonElementMixin } f */ @customElement('sbb-tertiary-button') export class SbbTertiaryButtonElement extends SbbButtonCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonTertiaryStyle]; } diff --git a/src/elements/button/transparent-button-link/__snapshots__/transparent-button-link.snapshot.spec.snap.js b/src/elements/button/transparent-button-link/__snapshots__/transparent-button-link.snapshot.spec.snap.js index 97f5f29535..f2d1b6c999 100644 --- a/src/elements/button/transparent-button-link/__snapshots__/transparent-button-link.snapshot.spec.snap.js +++ b/src/elements/button/transparent-button-link/__snapshots__/transparent-button-link.snapshot.spec.snap.js @@ -67,6 +67,7 @@ snapshots["sbb-transparent-button-link renders a disabled sbb-transparent-button aria-disabled="true" class="sbb-action-base sbb-transparent-button-link" href="https://www.sbb.ch" + tabindex="-1" > diff --git a/src/elements/button/transparent-button-link/readme.md b/src/elements/button/transparent-button-link/readme.md index b7688f24ec..98c627da08 100644 --- a/src/elements/button/transparent-button-link/readme.md +++ b/src/elements/button/transparent-button-link/readme.md @@ -77,17 +77,18 @@ Use the accessibility properties in case of an icon-only button to describe the ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | --------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | -| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | ## Slots diff --git a/src/elements/button/transparent-button-link/transparent-button-link.ts b/src/elements/button/transparent-button-link/transparent-button-link.ts index ff9322b9a6..7618cf8deb 100644 --- a/src/elements/button/transparent-button-link/transparent-button-link.ts +++ b/src/elements/button/transparent-button-link/transparent-button-link.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; -import { SbbDisabledMixin } from '../../core/mixins.js'; +import { SbbDisabledInteractiveMixin, SbbDisabledMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonTransparentStyle, @@ -17,7 +17,7 @@ import { */ @customElement('sbb-transparent-button-link') export class SbbTransparentButtonLinkElement extends SbbButtonCommonElementMixin( - SbbDisabledMixin(SbbLinkBaseElement), + SbbDisabledInteractiveMixin(SbbDisabledMixin(SbbLinkBaseElement)), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonTransparentStyle]; } diff --git a/src/elements/button/transparent-button/__snapshots__/transparent-button.snapshot.spec.snap.js b/src/elements/button/transparent-button/__snapshots__/transparent-button.snapshot.spec.snap.js index 8a58debf08..64d3e150ad 100644 --- a/src/elements/button/transparent-button/__snapshots__/transparent-button.snapshot.spec.snap.js +++ b/src/elements/button/transparent-button/__snapshots__/transparent-button.snapshot.spec.snap.js @@ -14,7 +14,6 @@ snapshots["sbb-transparent-button renders a sbb-transparent-button without icon negative="" role="button" size="m" - tabindex="0" type="button" value="value" > diff --git a/src/elements/button/transparent-button/readme.md b/src/elements/button/transparent-button/readme.md index 87c84f231f..a373b33ef9 100644 --- a/src/elements/button/transparent-button/readme.md +++ b/src/elements/button/transparent-button/readme.md @@ -67,28 +67,34 @@ sbb-transparent-button { Use the accessibility properties in case of an icon-only button to describe the purpose of the `sbb-transparent-button` for screen-reader users. -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `size` | `size` | public | `SbbButtonSize \| undefined` | `'l'` | Size variant, either l or m. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/button/transparent-button/transparent-button.ts b/src/elements/button/transparent-button/transparent-button.ts index 616f453bbf..b8fbb74862 100644 --- a/src/elements/button/transparent-button/transparent-button.ts +++ b/src/elements/button/transparent-button/transparent-button.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { buttonCommonStyle, buttonTransparentStyle, @@ -17,7 +17,7 @@ import { */ @customElement('sbb-transparent-button') export class SbbTransparentButtonElement extends SbbButtonCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = [buttonCommonStyle, buttonTransparentStyle]; } diff --git a/src/elements/core/base-elements/action-base-element.ts b/src/elements/core/base-elements/action-base-element.ts index 2f0266071a..ab9af4ce1e 100644 --- a/src/elements/core/base-elements/action-base-element.ts +++ b/src/elements/core/base-elements/action-base-element.ts @@ -11,6 +11,7 @@ import { getLocalName } from '../dom.js'; type MaybeDisabled = { disabled?: boolean; formDisabled?: boolean; + disabledInteractive?: boolean; }; @hostAttributes({ @@ -22,6 +23,10 @@ export abstract class SbbActionBaseElement extends LitElement { return maybeDisabled.disabled || maybeDisabled.formDisabled; } + protected get maybeDisabledInteractive(): boolean | undefined { + return (this as MaybeDisabled).disabledInteractive; + } + public override connectedCallback(): void { super.connectedCallback(); @@ -41,7 +46,7 @@ export abstract class SbbActionBaseElement extends LitElement { this.addEventListener( 'click', (event) => { - if (this.maybeDisabled) { + if (this.maybeDisabled && !this.maybeDisabledInteractive) { event.preventDefault(); event.stopImmediatePropagation(); } diff --git a/src/elements/core/base-elements/button-base-element.spec.ts b/src/elements/core/base-elements/button-base-element.spec.ts index 77e52a1459..67006811f1 100644 --- a/src/elements/core/base-elements/button-base-element.spec.ts +++ b/src/elements/core/base-elements/button-base-element.spec.ts @@ -1,6 +1,7 @@ import { assert, expect } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; import { fixture } from '../testing/private.js'; import { EventSpy, waitForLitRender } from '../testing.js'; @@ -8,7 +9,8 @@ import { EventSpy, waitForLitRender } from '../testing.js'; import { SbbButtonBaseElement } from './button-base-element.js'; class GenericButton extends SbbButtonBaseElement { - public disabled = false; + @property() public disabled = false; + @property() public disabledInteractive = false; protected override renderTemplate(): TemplateResult { return html`Button`; @@ -53,6 +55,19 @@ describe(`SbbButtonBaseElement`, () => { expect(clickSpy.count).not.to.be.greaterThan(0); }); + it('dispatch click if disabled and disabledInteractive', async () => { + const clickSpy = new EventSpy('click'); + + element.disabled = true; + element.disabledInteractive = true; + await waitForLitRender(element); + + element.click(); + await waitForLitRender(element); + + expect(clickSpy.count).to.be.equal(1); + }); + it('dispatch click if type button', async () => { element.type = 'button'; await waitForLitRender(element); diff --git a/src/elements/core/base-elements/link-base-element.spec.ts b/src/elements/core/base-elements/link-base-element.spec.ts index 19c3871354..385a992176 100644 --- a/src/elements/core/base-elements/link-base-element.spec.ts +++ b/src/elements/core/base-elements/link-base-element.spec.ts @@ -1,6 +1,7 @@ import { assert, expect } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; import { fixture } from '../testing/private.js'; import { EventSpy, waitForLitRender } from '../testing.js'; @@ -8,7 +9,8 @@ import { EventSpy, waitForLitRender } from '../testing.js'; import { SbbLinkBaseElement } from './link-base-element.js'; class GenericLink extends SbbLinkBaseElement { - public disabled = false; + @property() public disabled = false; + @property() public disabledInteractive = false; protected override renderTemplate(): TemplateResult { return html`Link`; @@ -50,6 +52,31 @@ describe(`SbbLinkBaseElement`, () => { expect(clickSpy.count).not.to.be.greaterThan(0); }); + it('dispatch click if disabled and disabledInteractive', async () => { + const clickSpy = new EventSpy('click'); + const a = element.shadowRoot!.querySelector('a'); + + expect(a).not.to.have.attribute('tabindex'); + expect(a).not.to.have.attribute('aria-disabled'); + + element.disabled = true; + await waitForLitRender(element); + + expect(a).to.have.attribute('tabindex', '-1'); + expect(a).to.have.attribute('aria-disabled', 'true'); + + element.disabledInteractive = true; + await waitForLitRender(element); + + expect(a).not.to.have.attribute('tabindex'); + expect(a).to.have.attribute('aria-disabled', 'true'); + + element.click(); + await waitForLitRender(element); + + expect(clickSpy.count).to.be.equal(1); + }); + it('dispatch event', async () => { const clickSpy = new EventSpy('click'); element.click(); diff --git a/src/elements/core/base-elements/link-base-element.ts b/src/elements/core/base-elements/link-base-element.ts index 28d76c12cd..f1e2019152 100644 --- a/src/elements/core/base-elements/link-base-element.ts +++ b/src/elements/core/base-elements/link-base-element.ts @@ -71,6 +71,7 @@ export abstract class SbbLinkBaseElement extends SbbActionBaseElement { target=${this.target || nothing} rel=${this._evaluateRelAttribute()} aria-label=${this.accessibilityLabel || nothing} + tabindex=${this.maybeDisabled && !this.maybeDisabledInteractive ? '-1' : nothing} aria-disabled=${this.maybeDisabled ? 'true' : nothing} > ${this.renderTemplate()} diff --git a/src/elements/core/mixins/disabled-mixin.spec.ts b/src/elements/core/mixins/disabled-mixin.spec.ts new file mode 100644 index 0000000000..56caae5c2c --- /dev/null +++ b/src/elements/core/mixins/disabled-mixin.spec.ts @@ -0,0 +1,126 @@ +import { expect } from '@open-wc/testing'; +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../testing/private.js'; +import { waitForLitRender } from '../testing.js'; + +import { SbbDisabledMixin, SbbDisabledTabIndexActionMixin } from './disabled-mixin.js'; + +/** Dummy docs */ +@customElement('sbb-disabled-test') +class SbbDisabledTestElement extends SbbDisabledTabIndexActionMixin(SbbDisabledMixin(LitElement)) {} + +describe(`sbb-button`, () => { + let element: SbbDisabledTestElement; + + function assertDisabled(element: SbbDisabledTestElement): void { + expect(element.tabIndex).to.be.equal(-1); + expect(element).not.to.have.attribute('tabindex'); + expect(element).to.have.attribute('aria-disabled', 'true'); + } + + function assertEnabled(element: SbbDisabledTestElement): void { + expect(element.tabIndex).to.be.equal(0); + expect(element).to.have.attribute('tabindex', '0'); + expect(element).not.to.have.attribute('aria-disabled'); + } + + function assertDisabledInteractive(element: SbbDisabledTestElement): void { + expect(element.tabIndex).to.be.equal(0); + expect(element).to.have.attribute('tabindex', '0'); + expect(element).to.have.attribute('aria-disabled', 'true'); + } + + beforeEach(async () => { + element = await fixture(html`I am a test button`); + }); + + it('should set attributes when enabled', async () => { + assertEnabled(element); + }); + + it('should set attributes when disabled initially', async () => { + element = await fixture( + html`I am a test button`, + ); + + assertDisabled(element); + }); + + it('should update attributes on attribute change', async () => { + element.setAttribute('disabled', 'true'); + await waitForLitRender(element); + + assertDisabled(element); + + element.removeAttribute('disabled'); + await waitForLitRender(element); + + assertEnabled(element); + }); + + it('should update attributes on property change', async () => { + element.disabled = true; + await waitForLitRender(element); + + assertDisabled(element); + + element.disabled = false; + await waitForLitRender(element); + + assertEnabled(element); + }); + + it('should ignore disabledInteractive when enabled', async () => { + element.disabledInteractive = true; + + assertEnabled(element); + }); + + describe('disabled interactive', () => { + it('should set attributes when disabled initially', async () => { + element = await fixture( + html` + I am a test button + `, + ); + + assertDisabledInteractive(element); + }); + + it('should update attributes on attribute change', async () => { + element.setAttribute('disabled', 'true'); + element.setAttribute('disabled-interactive', 'true'); + await waitForLitRender(element); + + assertDisabledInteractive(element); + + element.removeAttribute('disabled-interactive'); + await waitForLitRender(element); + + assertDisabled(element); + }); + + it('should update attributes on property change', async () => { + element.disabled = true; + element.disabledInteractive = true; + await waitForLitRender(element); + + assertDisabledInteractive(element); + + element.disabledInteractive = false; + await waitForLitRender(element); + + assertDisabled(element); + }); + }); +}); + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-disabled-test': SbbDisabledTestElement; + } +} diff --git a/src/elements/core/mixins/disabled-mixin.ts b/src/elements/core/mixins/disabled-mixin.ts index 2187a0e364..bd4cf3da30 100644 --- a/src/elements/core/mixins/disabled-mixin.ts +++ b/src/elements/core/mixins/disabled-mixin.ts @@ -9,6 +9,10 @@ export declare class SbbDisabledMixinType { protected isDisabledExternally(): boolean; } +export declare class SbbDisabledInteractiveMixinType { + public disabledInteractive: boolean; +} + /** * Enhance your component with a disabled property. */ @@ -42,40 +46,65 @@ export const SbbDisabledMixin = >( }; /** - * @deprecated Will be removed with next major version + * Enhance your component with a disabled interactive property. */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbDisabledInteractiveMixin = < + T extends AbstractConstructor, +>( + superClass: T, +): AbstractConstructor & T => { + abstract class SbbDisabledInteractiveElement + extends superClass + implements Partial + { + /** Whether disabled buttons should be interactive. */ + @property({ attribute: 'disabled-interactive', type: Boolean }) public disabledInteractive = + false; + } + + return SbbDisabledInteractiveElement as unknown as AbstractConstructor & + T; +}; + // eslint-disable-next-line @typescript-eslint/naming-convention export const SbbDisabledTabIndexActionMixin = >( superClass: T, -): AbstractConstructor & T => { +): AbstractConstructor & T => { abstract class SbbDisabledTabIndexAction - extends SbbDisabledMixin(superClass) - implements SbbDisabledMixinType + extends SbbDisabledInteractiveMixin(SbbDisabledMixin(superClass)) + implements SbbDisabledMixinType, SbbDisabledInteractiveMixinType { protected override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (!changedProperties.has('disabled')) { + if (!changedProperties.has('disabled') && !changedProperties.has('disabledInteractive')) { return; } - // FIXME if tabindex is not needed in combination with aria-disabled, - // use the SbbDisabledMixin and implement a different willUpdate method. + if (!this.disabled || this.disabledInteractive) { + this.setAttribute('tabindex', '0'); + } else { + this.removeAttribute('tabindex'); + } + if (this.disabled) { this.setAttribute('aria-disabled', 'true'); - this.removeAttribute('tabindex'); } else { this.removeAttribute('aria-disabled'); - this.setAttribute('tabindex', '0'); } } } - return SbbDisabledTabIndexAction as AbstractConstructor & T; + return SbbDisabledTabIndexAction as AbstractConstructor< + SbbDisabledMixinType & SbbDisabledInteractiveMixinType + > & + T; }; /** - * Extends `SbbDisabledMixin` with the `aria-disabled` attribute. - * For a11y purposes, keeps the element focusable even when disabled. + * Extends `SbbDisabledMixin` with the `aria-disabled` attribute. + * For a11y purposes, keeps the element focusable even when disabled. + * @deprecated Will be removed with next major version */ // eslint-disable-next-line @typescript-eslint/naming-convention export const SbbFocusableDisabledActionMixin = >( diff --git a/src/elements/datepicker/datepicker-toggle/__snapshots__/datepicker-toggle.snapshot.spec.snap.js b/src/elements/datepicker/datepicker-toggle/__snapshots__/datepicker-toggle.snapshot.spec.snap.js index fc7a0cdaf3..23fcbc9e04 100644 --- a/src/elements/datepicker/datepicker-toggle/__snapshots__/datepicker-toggle.snapshot.spec.snap.js +++ b/src/elements/datepicker/datepicker-toggle/__snapshots__/datepicker-toggle.snapshot.spec.snap.js @@ -20,7 +20,6 @@ snapshots["sbb-datepicker-toggle renders Shadow DOM"] = disabled="" icon-name="calendar-small" role="button" - tabindex="-1" > { "role": "WebArea", diff --git a/src/elements/datepicker/datepicker-toggle/datepicker-toggle.ts b/src/elements/datepicker/datepicker-toggle/datepicker-toggle.ts index 1ccf60cd56..084c1ccec7 100644 --- a/src/elements/datepicker/datepicker-toggle/datepicker-toggle.ts +++ b/src/elements/datepicker/datepicker-toggle/datepicker-toggle.ts @@ -106,14 +106,6 @@ export class SbbDatepickerToggleElement extends SbbNegativeMixin( this._datePickerController?.abort(); } - public override firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this._triggerElement.updateComplete.then(() => { - // We have to rewrite the tabindex of the mini-button as the popover trigger itself sets the tabindex to always 0. - this._triggerElement.tabIndex = this._isDisabled() ? -1 : 0; - }); - } - private _init(datePicker?: string | SbbDatepickerElement): void { this._datePickerController?.abort(); this._datePickerController = new AbortController(); @@ -200,18 +192,13 @@ export class SbbDatepickerToggleElement extends SbbNegativeMixin( return this._datePickerElement?.hasCustomNow() ? this._datePickerElement.now : undefined; } - private _isDisabled(): boolean { - return !isServer && (!this._datePickerElement || this._disabled); - } - protected override render(): TemplateResult { return html` (this._triggerElement = el as SbbMiniButtonElement))} > diff --git a/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts b/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts index 1687c3e3ff..59ddc3bddb 100644 --- a/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts +++ b/src/elements/expansion-panel/expansion-panel-header/expansion-panel-header.ts @@ -5,7 +5,7 @@ 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 { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbIconNameMixin } from '../../icon.js'; import type { SbbExpansionPanelElement } from '../expansion-panel.js'; @@ -22,7 +22,7 @@ import style from './expansion-panel-header.scss?lit&inline'; @hostAttributes({ slot: 'header', }) -export class SbbExpansionPanelHeaderElement extends SbbFocusableDisabledActionMixin( +export class SbbExpansionPanelHeaderElement extends SbbDisabledTabIndexActionMixin( SbbIconNameMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = style; diff --git a/src/elements/expansion-panel/expansion-panel-header/readme.md b/src/elements/expansion-panel/expansion-panel-header/readme.md index 1bffc019eb..1535bc06b1 100644 --- a/src/elements/expansion-panel/expansion-panel-header/readme.md +++ b/src/elements/expansion-panel/expansion-panel-header/readme.md @@ -35,14 +35,15 @@ When the element is clicked, the `toggleExpanded` event is emitted. ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Events diff --git a/src/elements/expansion-panel/expansion-panel/expansion-panel.stories.ts b/src/elements/expansion-panel/expansion-panel/expansion-panel.stories.ts index 0b974660d2..7433ffcff9 100644 --- a/src/elements/expansion-panel/expansion-panel/expansion-panel.stories.ts +++ b/src/elements/expansion-panel/expansion-panel/expansion-panel.stories.ts @@ -85,6 +85,12 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, +}; + const size: InputType = { control: { type: 'inline-radio', @@ -101,6 +107,7 @@ const defaultArgTypes: ArgTypes = { color, borderless, disabled, + 'disabled-interactive': disabledInteractive, size, }; @@ -113,14 +120,24 @@ const defaultArgs: Args = { color: color.options![0], borderless: false, disabled: false, + 'disabled-interactive': false, size: size.options![0], }; -const Template = ({ headerText, iconName, contentText, ...args }: Args): TemplateResult => html` +const Template = ({ + headerText, + iconName, + contentText, + 'disabled-interactive': disabledInteractive, + ...args +}: Args): TemplateResult => html` - ${headerText} + ${headerText} + ${contentText} `; @@ -129,10 +146,11 @@ const TemplateSlottedIcon = ({ headerText, iconName, contentText, + 'disabled-interactive': disabledInteractive, ...args }: Args): TemplateResult => html` - + ${headerText} diff --git a/src/elements/link/block-link-button/block-link-button.ts b/src/elements/link/block-link-button/block-link-button.ts index 65763c2332..114492c085 100644 --- a/src/elements/link/block-link-button/block-link-button.ts +++ b/src/elements/link/block-link-button/block-link-button.ts @@ -1,7 +1,7 @@ import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbBlockLinkCommonElementMixin } from '../common.js'; /** @@ -12,7 +12,7 @@ import { SbbBlockLinkCommonElementMixin } from '../common.js'; */ @customElement('sbb-block-link-button') export class SbbBlockLinkButtonElement extends SbbBlockLinkCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) {} declare global { diff --git a/src/elements/link/block-link-button/readme.md b/src/elements/link/block-link-button/readme.md index eccaba3a86..be9702a6a2 100644 --- a/src/elements/link/block-link-button/readme.md +++ b/src/elements/link/block-link-button/readme.md @@ -44,28 +44,34 @@ The component has three sizes (`xs`, `s`, which is the default, and `m`). ## Accessibility -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to receive focus and dispatch events. +The button will have `aria-disabled="true"` for assistive technology. +It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| --------------- | ---------------- | ------- | ------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `iconPlacement` | `icon-placement` | public | `SbbIconPlacement \| undefined` | `'start'` | Moves the icon to the end of the component if set to true. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size, the link should get in the non-button variation. With inline variant, the text size adapts to where it is used. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | ------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `iconPlacement` | `icon-placement` | public | `SbbIconPlacement \| undefined` | `'start'` | Moves the icon to the end of the component if set to true. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size, the link should get in the non-button variation. With inline variant, the text size adapts to where it is used. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/link/common/link-common-stories.ts b/src/elements/link/common/link-common-stories.ts index da8916555b..3b446f472b 100644 --- a/src/elements/link/common/link-common-stories.ts +++ b/src/elements/link/common/link-common-stories.ts @@ -63,6 +63,15 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Button', + }, +}; + const tag: InputType = { control: { type: 'text', @@ -182,6 +191,7 @@ export const linkDefaultArgTypes: ArgTypes = { rel, download, disabled, + 'disabled-interactive': disabledInteractive, 'accessibility-label': accessibilityLabel, }; @@ -192,6 +202,7 @@ export const linkDefaultArgs: Args = { rel: undefined, download: false, disabled: false, + 'disabled-interactive': false, 'accessibility-label': undefined, }; @@ -244,6 +255,7 @@ export const linkButtonDefaultArgTypes: ArgTypes = { ...linkCommonDefaultArgTypes, type, disabled, + 'disabled-interactive': disabledInteractive, name, value, form, @@ -254,6 +266,7 @@ export const linkButtonDefaultArgs: Args = { ...linkCommonDefaultArgs, type: type.options![0], disabled: false, + 'disabled-interactive': false, name: 'Button name', value: undefined, form: undefined, diff --git a/src/elements/link/link-button/link-button.ts b/src/elements/link/link-button/link-button.ts index 7a58555138..a04569a680 100644 --- a/src/elements/link/link-button/link-button.ts +++ b/src/elements/link/link-button/link-button.ts @@ -1,7 +1,7 @@ import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbInlineLinkCommonElementMixin } from '../common.js'; /** @@ -11,7 +11,7 @@ import { SbbInlineLinkCommonElementMixin } from '../common.js'; */ @customElement('sbb-link-button') export class SbbLinkButtonElement extends SbbInlineLinkCommonElementMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) {} declare global { diff --git a/src/elements/link/link-button/readme.md b/src/elements/link/link-button/readme.md index 88247ebdc4..6e92e47d58 100644 --- a/src/elements/link/link-button/readme.md +++ b/src/elements/link/link-button/readme.md @@ -30,27 +30,33 @@ accepting its associated properties (`type`, `name`, `value` and `form`). ## Accessibility -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to receive focus and dispatch events. +The button will have `aria-disabled="true"` for assistive technology. +It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ---------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size, the link should get in the non-button variation. With inline variant, the text size adapts to where it is used. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `size` | `size` | public | `SbbLinkSize` | `'s'` | Text size, the link should get in the non-button variation. With inline variant, the text size adapts to where it is used. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/menu/common/menu-action-common.ts b/src/elements/menu/common/menu-action-common.ts index fa13f7cb4b..9a143fb0e6 100644 --- a/src/elements/menu/common/menu-action-common.ts +++ b/src/elements/menu/common/menu-action-common.ts @@ -13,10 +13,10 @@ import { SbbIconNameMixin, type SbbIconNameMixinType } from '../../icon.js'; import style from './menu-action.scss?lit&inline'; export declare class SbbMenuActionCommonElementMixinType - implements Partial, Partial + extends SbbDisabledMixinType + implements Partial { public amount?: string; - public disabled: boolean; public iconName?: string; } diff --git a/src/elements/menu/menu-button/menu-button.stories.ts b/src/elements/menu/menu-button/menu-button.stories.ts index 1b73a8b33c..01a498aa77 100644 --- a/src/elements/menu/menu-button/menu-button.stories.ts +++ b/src/elements/menu/menu-button/menu-button.stories.ts @@ -68,6 +68,15 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Button', + }, +}; + const name: InputType = { control: { type: 'text', @@ -107,6 +116,7 @@ const defaultArgTypes: ArgTypes = { 'icon-name': iconName, type, disabled, + 'disabled-interactive': disabledInteractive, name, value, form, @@ -118,6 +128,7 @@ const defaultArgs: Args = { amount: '99', 'icon-name': 'tick-small', disabled: false, + 'disabled-interactive': false, type: type.options![0], name: 'detail', value: 'Value', diff --git a/src/elements/menu/menu-button/menu-button.ts b/src/elements/menu/menu-button/menu-button.ts index 2477187f6f..1602155c61 100644 --- a/src/elements/menu/menu-button/menu-button.ts +++ b/src/elements/menu/menu-button/menu-button.ts @@ -1,7 +1,7 @@ import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbMenuActionCommonElementMixin } from '../common.js'; /** @@ -13,7 +13,7 @@ import { SbbMenuActionCommonElementMixin } from '../common.js'; * to modify horizontal padding. */ @customElement('sbb-menu-button') -export class SbbMenuButtonElement extends SbbFocusableDisabledActionMixin( +export class SbbMenuButtonElement extends SbbDisabledTabIndexActionMixin( SbbMenuActionCommonElementMixin(SbbButtonBaseElement), ) {} diff --git a/src/elements/menu/menu-button/readme.md b/src/elements/menu/menu-button/readme.md index abcaa7be7e..a8aab39f4f 100644 --- a/src/elements/menu/menu-button/readme.md +++ b/src/elements/menu/menu-button/readme.md @@ -28,27 +28,33 @@ accepting its associated properties (`type`, `name`, `value` and `form`). ## Accessibility -### Disabled buttons +### Interactive disabled buttons -Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive -technology and do not provide the reason for which they are disabled. -To partially address the problem, disabled elements are kept focusable (other interactions are still prevented). -However, it is still the consumers responsibility to provide a reason for the element being disabled. +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `amount` | `amount` | public | `string \| undefined` | | Value shown as badge at component end. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `amount` | `amount` | public | `string \| undefined` | | Value shown as badge at component end. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## CSS Properties diff --git a/src/elements/menu/menu-link/menu-link.stories.ts b/src/elements/menu/menu-link/menu-link.stories.ts index 925732d30b..50b13c5c02 100644 --- a/src/elements/menu/menu-link/menu-link.stories.ts +++ b/src/elements/menu/menu-link/menu-link.stories.ts @@ -102,6 +102,15 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Button', + }, +}; + const accessibilityLabel: InputType = { control: { type: 'text', @@ -117,6 +126,7 @@ const defaultArgTypes: ArgTypes = { rel, download, disabled, + 'disabled-interactive': disabledInteractive, 'accessibility-label': accessibilityLabel, }; @@ -129,6 +139,7 @@ const defaultArgs: Args = { rel: undefined, download: false, disabled: false, + 'disabled-interactive': false, 'accessibility-label': '', }; diff --git a/src/elements/menu/menu-link/menu-link.ts b/src/elements/menu/menu-link/menu-link.ts index 1d4b4726d1..e71d53fd67 100644 --- a/src/elements/menu/menu-link/menu-link.ts +++ b/src/elements/menu/menu-link/menu-link.ts @@ -1,6 +1,7 @@ import { customElement } from 'lit/decorators.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; +import { SbbDisabledInteractiveMixin } from '../../core/mixins.js'; import { SbbMenuActionCommonElementMixin } from '../common.js'; /** @@ -12,7 +13,9 @@ import { SbbMenuActionCommonElementMixin } from '../common.js'; * to modify horizontal padding. */ @customElement('sbb-menu-link') -export class SbbMenuLinkElement extends SbbMenuActionCommonElementMixin(SbbLinkBaseElement) {} +export class SbbMenuLinkElement extends SbbDisabledInteractiveMixin( + SbbMenuActionCommonElementMixin(SbbLinkBaseElement), +) {} declare global { interface HTMLElementTagNameMap { diff --git a/src/elements/menu/menu-link/readme.md b/src/elements/menu/menu-link/readme.md index 1d368339ee..cb4937bd77 100644 --- a/src/elements/menu/menu-link/readme.md +++ b/src/elements/menu/menu-link/readme.md @@ -30,16 +30,17 @@ accepting its associated properties (`href`, `target`, `rel` and `download`). ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| -------------------- | --------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | -| `amount` | `amount` | public | `string \| undefined` | | Value shown as badge at component end. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | -| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | -| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `accessibilityLabel` | `accessibility-label` | public | `string \| undefined` | | This will be forwarded as aria-label to the inner anchor element. | +| `amount` | `amount` | public | `string \| undefined` | | Value shown as badge at component end. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `download` | `download` | public | `boolean \| undefined` | | Whether the browser will show the download dialog on click. | +| `href` | `href` | public | `string \| undefined` | | The href value you want to link to. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `rel` | `rel` | public | `string \| undefined` | | The relationship of the linked URL as space-separated link types. | +| `target` | `target` | public | `LinkTargetType \| string \| undefined` | | Where to display the linked URL. | ## CSS Properties diff --git a/src/elements/menu/menu/__snapshots__/menu.snapshot.spec.snap.js b/src/elements/menu/menu/__snapshots__/menu.snapshot.spec.snap.js index 7efeb908e0..12b168a8a0 100644 --- a/src/elements/menu/menu/__snapshots__/menu.snapshot.spec.snap.js +++ b/src/elements/menu/menu/__snapshots__/menu.snapshot.spec.snap.js @@ -35,7 +35,6 @@ snapshots["sbb-menu renders DOM"] = disabled="" icon-name="pen-small" role="button" - tabindex="0" > Edit @@ -105,7 +104,6 @@ snapshots["sbb-menu renders with list DOM"] = icon-name="pen-small" role="button" slot="li-1" - tabindex="0" > Edit diff --git a/src/elements/menu/menu/menu.ts b/src/elements/menu/menu/menu.ts index 0621896677..5422d5ddc0 100644 --- a/src/elements/menu/menu/menu.ts +++ b/src/elements/menu/menu/menu.ts @@ -148,7 +148,9 @@ export class SbbMenuElement extends SbbNamedSlotListMixin< this.querySelectorAll( 'sbb-menu-button, sbb-menu-link', ), - ).filter((el: SbbMenuButtonElement | SbbMenuLinkElement) => interactivityChecker.isVisible(el)); + ).filter( + (el) => (!el.disabled || el.disabledInteractive) && interactivityChecker.isVisible(el), + ); const current = enabledActions.findIndex((e: Element) => e === evt.target); const nextIndex = getNextElementIndex(evt, current, enabledActions.length); diff --git a/src/elements/popover/popover-trigger/popover-trigger.spec.ts b/src/elements/popover/popover-trigger/popover-trigger.spec.ts index a0ea3b50c0..e4e4422386 100644 --- a/src/elements/popover/popover-trigger/popover-trigger.spec.ts +++ b/src/elements/popover/popover-trigger/popover-trigger.spec.ts @@ -86,15 +86,18 @@ describe(`sbb-popover-trigger`, () => { expect(popover).to.have.attribute('data-state', 'opened'); }); - it("doesn't open popover when disabled", async () => { + it("doesn't focus popover-trigger on keyboard event when disabled", async () => { + const changeSpy = new EventSpy('focus', element); + element.disabled = true; popover.hoverTrigger = true; await waitForLitRender(element); - element.click(); + element.focus(); await waitForLitRender(element); + expect(changeSpy.count).not.to.be.greaterThan(0); expect(popover).to.have.attribute('data-state', 'closed'); }); }); diff --git a/src/elements/popover/popover-trigger/popover-trigger.stories.ts b/src/elements/popover/popover-trigger/popover-trigger.stories.ts index 471ac5732e..01a5362464 100644 --- a/src/elements/popover/popover-trigger/popover-trigger.stories.ts +++ b/src/elements/popover/popover-trigger/popover-trigger.stories.ts @@ -40,11 +40,18 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, +}; + const defaultArgTypes: ArgTypes = { negative, 'aria-label': ariaLabel, 'icon-name': iconName, disabled, + 'disabled-interactive': disabledInteractive, }; const defaultArgs: Args = { @@ -52,6 +59,7 @@ const defaultArgs: Args = { 'aria-label': 'Click to open the popover', 'icon-name': 'circle-information-small', disabled: false, + 'disabled-interactive': false, }; const popover = (): TemplateResult => html` diff --git a/src/elements/popover/popover-trigger/popover-trigger.ts b/src/elements/popover/popover-trigger/popover-trigger.ts index f0cde4e3b9..58acc5b8b8 100644 --- a/src/elements/popover/popover-trigger/popover-trigger.ts +++ b/src/elements/popover/popover-trigger/popover-trigger.ts @@ -3,7 +3,7 @@ import { customElement } from 'lit/decorators.js'; import { SbbButtonBaseElement } from '../../core/base-elements.js'; import { hostContext } from '../../core/dom.js'; -import { SbbFocusableDisabledActionMixin, SbbNegativeMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin, SbbNegativeMixin } from '../../core/mixins.js'; import { SbbIconNameMixin } from '../../icon.js'; import style from './popover-trigger.scss?lit&inline'; @@ -14,7 +14,7 @@ import style from './popover-trigger.scss?lit&inline'; * @slot - Use the unnamed slot to add content to the `sbb-popover-trigger`. */ @customElement('sbb-popover-trigger') -export class SbbPopoverTriggerElement extends SbbFocusableDisabledActionMixin( +export class SbbPopoverTriggerElement extends SbbDisabledTabIndexActionMixin( SbbNegativeMixin(SbbIconNameMixin(SbbButtonBaseElement)), ) { public static override styles: CSSResultGroup = style; diff --git a/src/elements/popover/popover-trigger/readme.md b/src/elements/popover/popover-trigger/readme.md index 2efdc2de94..cb8a6a3a58 100644 --- a/src/elements/popover/popover-trigger/readme.md +++ b/src/elements/popover/popover-trigger/readme.md @@ -67,19 +67,33 @@ associate the popover trigger with the popover via `aria-describedby` and an `id ``` +### Interactive disabled buttons + +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. +This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. + +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `negative` | `negative` | public | `boolean` | `false` | Negative coloring variant flag. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Slots diff --git a/src/elements/slider/slider.spec.ts b/src/elements/slider/slider.spec.ts index 59742eb4a8..f69dc923bb 100644 --- a/src/elements/slider/slider.spec.ts +++ b/src/elements/slider/slider.spec.ts @@ -82,4 +82,21 @@ describe(`sbb-slider`, () => { await keyboardPressTimes(element, 'ArrowDown'); expect(changeEvent.count).not.to.be.greaterThan(0); }); + + it('should not be focused when disabled', async () => { + expect(element.tabIndex).to.be.equal(0); + expect(element.getAttribute('aria-disabled')).to.be.null; + + element.toggleAttribute('disabled', true); + await waitForLitRender(element); + + expect(element.tabIndex).to.be.equal(-1); + expect(element.getAttribute('aria-disabled')).to.be.equal('true'); + + element.toggleAttribute('disabled', false); + await waitForLitRender(element); + + expect(element.tabIndex).to.be.equal(0); + expect(element.getAttribute('aria-disabled')).to.be.null; + }); }); diff --git a/src/elements/slider/slider.ts b/src/elements/slider/slider.ts index 4d4af5531c..fa460bea14 100644 --- a/src/elements/slider/slider.ts +++ b/src/elements/slider/slider.ts @@ -8,7 +8,7 @@ import { SbbConnectedAbortController } from '../core/controllers.js'; import { hostAttributes } from '../core/decorators.js'; import { setOrRemoveAttribute } from '../core/dom.js'; import { EventEmitter, forwardEventToHost } from '../core/eventing.js'; -import { SbbFocusableDisabledActionMixin } from '../core/mixins.js'; +import { SbbDisabledMixin } from '../core/mixins.js'; import style from './slider.scss?lit&inline'; @@ -25,7 +25,7 @@ import '../icon.js'; @hostAttributes({ role: 'slider', }) -export class SbbSliderElement extends SbbFocusableDisabledActionMixin(LitElement) { +export class SbbSliderElement extends SbbDisabledMixin(LitElement) { public static override styles: CSSResultGroup = style; public static readonly events = { didChange: 'didChange', @@ -90,14 +90,6 @@ export class SbbSliderElement extends SbbFocusableDisabledActionMixin(LitElement protected override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (changedProperties.has('disabled')) { - if (this.disabled) { - this.removeAttribute('tabindex'); - } else { - this.setAttribute('tabindex', '0'); - } - } - if (changedProperties.has('value')) { this._handleChange(Number(this.value)); } else if (changedProperties.has('valueAsNumber')) { @@ -113,6 +105,16 @@ export class SbbSliderElement extends SbbFocusableDisabledActionMixin(LitElement if (changedProperties.has('readonly')) { setOrRemoveAttribute(this, 'aria-readonly', this.readonly ? 'true' : null); } + + if (changedProperties.has('disabled')) { + if (this.disabled) { + this.setAttribute('aria-disabled', 'true'); + this.removeAttribute('tabindex'); + } else { + this.removeAttribute('aria-disabled'); + this.setAttribute('tabindex', '0'); + } + } } private _syncValues(newValue: string | number): void { diff --git a/src/elements/tag/tag/__snapshots__/tag.snapshot.spec.snap.js b/src/elements/tag/tag/__snapshots__/tag.snapshot.spec.snap.js index 4b3e4efe67..a4b0bc025e 100644 --- a/src/elements/tag/tag/__snapshots__/tag.snapshot.spec.snap.js +++ b/src/elements/tag/tag/__snapshots__/tag.snapshot.spec.snap.js @@ -83,7 +83,6 @@ snapshots["sbb-tag renders disabled with icon and amount DOM"] = icon-name="circle-information-small" role="button" size="m" - tabindex="0" value="information" > Info diff --git a/src/elements/tag/tag/readme.md b/src/elements/tag/tag/readme.md index 8b0db8242b..6894adddee 100644 --- a/src/elements/tag/tag/readme.md +++ b/src/elements/tag/tag/readme.md @@ -53,6 +53,19 @@ It's recommended to check the parent's `sbb-tag-group` for the value. The component imitates an `button` element to provide an accessible experience. The state is reflected via `aria-pressed` attribute. +### Interactive disabled buttons + +Native disabled elements cannot receive focus and do not dispatch any events. This can +be problematic in some cases because it can prevent the app from telling the user why the button is +disabled. Consumers can use the `disabledInteractive` property to style the button as disabled but allow for +it to receive focus and dispatch events. The button will have `aria-disabled="true"` for assistive +technology. It is the consumers responsibility to provide a reason for the element being disabled. +This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-describedby` attribute. + +**Note:** Using the `disabledInteractive` property can result in buttons that previously prevented +actions to no longer do so, for example a submit button in a form. When using this input, you should +guard against such cases in your component. + ### Disabled elements Generally speaking, `disabled` elements are considered a bad pattern for accessibility. They are invisible to assistive @@ -65,17 +78,18 @@ This can be achieved by adding an `aria-label`, `aria-labelledby` or `aria-descr ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ---------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `amount` | `amount` | public | `string \| undefined` | | Amount displayed inside the tag. | -| `checked` | `checked` | public | `boolean` | `false` | Whether the tag is checked. | -| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | -| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | -| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | -| `name` | `name` | public | `string` | | The name of the button element. | -| `size` | `size` | public | `SbbTagSize` | `'m'` | Tag size. | -| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | -| `value` | `value` | public | `string` | | The value of the button element. | +| Name | Attribute | Privacy | Type | Default | Description | +| --------------------- | ---------------------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `amount` | `amount` | public | `string \| undefined` | | Amount displayed inside the tag. | +| `checked` | `checked` | public | `boolean` | `false` | Whether the tag is checked. | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `disabledInteractive` | `disabled-interactive` | public | `boolean` | `false` | Whether disabled buttons should be interactive. | +| `form` | `form` | public | `string \| undefined` | | The element to associate the button with. | +| `iconName` | `icon-name` | public | `string \| undefined` | | The icon name we want to use, choose from the small icon variants from the ui-icons category from here https://icons.app.sbb.ch. | +| `name` | `name` | public | `string` | | The name of the button element. | +| `size` | `size` | public | `SbbTagSize` | `'m'` | Tag size. | +| `type` | `type` | public | `SbbButtonType` | `'button'` | The type attribute to use for the button. | +| `value` | `value` | public | `string` | | The value of the button element. | ## Events diff --git a/src/elements/tag/tag/tag.stories.ts b/src/elements/tag/tag/tag.stories.ts index 7cbae8a6c8..405cba5344 100644 --- a/src/elements/tag/tag/tag.stories.ts +++ b/src/elements/tag/tag/tag.stories.ts @@ -21,6 +21,12 @@ const disabled: InputType = { }, }; +const disabledInteractive: InputType = { + control: { + type: 'boolean', + }, +}; + const label: InputType = { control: { type: 'text', @@ -61,6 +67,7 @@ const size: InputType = { const defaultArgTypes: ArgTypes = { checked, disabled, + 'disabled-interactive': disabledInteractive, label, value, 'icon-name': icon, @@ -72,6 +79,7 @@ const defaultArgTypes: ArgTypes = { const defaultArgs: Args = { checked: false, disabled: false, + 'disabled-interactive': false, label: 'Label', value: 'Value', 'icon-name': undefined, diff --git a/src/elements/tag/tag/tag.ts b/src/elements/tag/tag/tag.ts index 4bbf3ac227..999058bdd0 100644 --- a/src/elements/tag/tag/tag.ts +++ b/src/elements/tag/tag/tag.ts @@ -6,7 +6,7 @@ import { SbbButtonBaseElement } from '../../core/base-elements.js'; import { SbbConnectedAbortController } from '../../core/controllers.js'; import { slotState } from '../../core/decorators.js'; import { EventEmitter } from '../../core/eventing.js'; -import { SbbFocusableDisabledActionMixin } from '../../core/mixins.js'; +import { SbbDisabledTabIndexActionMixin } from '../../core/mixins.js'; import { SbbIconNameMixin } from '../../icon.js'; import type { SbbTagGroupElement } from '../tag-group.js'; @@ -27,7 +27,7 @@ export type SbbTagSize = 's' | 'm'; @customElement('sbb-tag') @slotState() export class SbbTagElement extends SbbIconNameMixin( - SbbFocusableDisabledActionMixin(SbbButtonBaseElement), + SbbDisabledTabIndexActionMixin(SbbButtonBaseElement), ) { public static override styles: CSSResultGroup = style; public static readonly events = {