| Version | Tag | Status | API |
|---|---|---|---|
| v1 | <oc-button-v1> |
Stable, allowed for generation | ButtonV1 |
Overview (v1)
Source: ./src/components/button/v1/Overview.mdx
Button
The button component is used to trigger specific actions in your application. It supports various styles, sizes, and can incorporate icons. It can be configured to fit content, be disabled, and set with an ARIA label for accessibility.
Default variation
Story Default:
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>
Configuration
The button component is available in the main styling variants primary, secondary, and tertiary.
This component is configurable, allowing you to tailor its features and appearance to your specific needs.
To explore all the available options and adjust the component, use the component configurator and see the changes affect the component in real-time.
Usage guidelines
Before integrating the button component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations. Here, you can discover both common and specific variations that address different use cases.
Info
See the Button UX documentation for detailed user experience guidelines.
Accessibility
The button component comes with a set of built-in accessibility features to ensure a seamless experience for all users.
Keyboard navigation
| Shortcut | Description |
|---|---|
| Space / Enter | Activates the button |
Important
If the button component or a normal HTML button with the type
submitis used in a form, pressingEnteron any of the following form elements triggers the submit action immediately (implicit submit): oc-checkbox, oc-radio-button, oc-switch-v2, oc-selection-tile, and oc-text-field.
Use oc-aria-label
To make the button recognizable for screen readers, use the oc-aria-label attribute to provide clear and descriptive context information.
See the general accessibility documentation for guidance on using oc-aria-label, including how it works with link and masked link behavior.
Multiple buttons
When multiple buttons with the same visible label are present, ensure each button is uniquely labeled so users understand their purpose.
See an example here.
Buttons with icons
The icon inside the button component has the state aria-hidden="true"
, which hides the icon from assistive technologies and prevents it from being read out loud by screen readers.
Further reading
Configuration (v1)
Source: ./src/components/button/v1/Configuration.mdx
Button configuration
Configure the button component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current component configuration.
Story Default:
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>
Interactive configurator (Storybook controls); every option is listed in the API section of this file.
API v1
Source: ./src/components/button/v1/ButtonV1.API.g.mdx
Button v1 API
API: <oc-button-v1> (ButtonV1)
The Button component is used to trigger specific actions in your application. It supports various styles, sizes, and can incorporate icons. It can be configured to fit content, be disabled, and set with an ARIA label for accessibility.
Attributes / properties
| Attribute | Type | Default | Required | Description |
|---|---|---|---|---|
variant |
"primary" | "secondary" | "secondary-on-color" | "secondary-over-color" | "tertiary" | "custom-color-strong" | "custom-color-soft" |
"primary" |
no | Sets the main styling variant of the button. Note: "secondary-on-color" is deprecated, use "secondary-over-color" instead. |
type |
"submit" | "reset" | "button" |
undefined |
no | Sets the type of the button. Can be one of: "button", "reset", "submit". |
size |
"50" | "100" |
"100" |
no | Sets the size of the button. |
icon-type-left |
icon name (428 values; see Icon list in `storybook/components/icon/README.md`) |
undefined |
no | Sets the icon for the left side of the button. |
icon-type-right |
icon name (428 values; see Icon list in `storybook/components/icon/README.md`) |
undefined |
no | Sets the icon for the right side of the button. |
disabled |
boolean |
false |
no | Indicates whether the button is disabled. |
loading |
"" | "keep-native-events" | "true" |
false |
no | Indicates whether the button is in a loading state. When enabled, displays a loading animation. For buttons of type "submit", the loading state also stops the propagation of click events after the first click to prevent multiple form submissions. Set to "keep-native-events" to enable the loading state but keep native events for this case.Note: The loading animation overwrites the icon on the right side set via icon-type-right. |
success |
boolean |
false |
no | Indicates whether the button is in a success state. This is a visual state that indicates a successful action. It does not change the functionality of the button. |
fit-content |
boolean |
false |
no | Indicates whether the button width should fit its content. This attribute only applies to size="100". |
fill-parent |
boolean |
false |
no | Indicates whether the button width should fill to its parent. This attribute only applies to size="50". |
form |
string |
undefined |
no | Sets the form id the button belongs to. Only available as an attribute. |
formaction |
string |
undefined |
no | Sets the action for form submission. Only available as an attribute. Only for type="submit". |
formmethod |
string |
undefined |
no | Sets the method for form submission. Only available as an attribute. Only for type="submit". |
href |
string |
undefined |
no | Deprecated: Use an a tag in the default slot instead.Visit SEO optimization techniques for more information. |
base64-href |
string |
undefined |
no | Sets the base64 encoded link target of the button. Use this attribute to prevent search engines from indexing the link target. |
target |
"_blank" | "_self" | "_parent" | "_top" |
undefined |
no | Sets the target attribute of the button. Note: Only applies when href or base64Href is set. |
rel |
string |
undefined |
no | Sets the rel attribute of the button. Note: Only applies when href or base64Href is set. |
oc-aria-label |
string |
undefined |
no | Sets the ARIA label of the button. |
Slots
| Slot | Required | Description |
|---|---|---|
default |
yes | Sets the text content of the button. |
Events
| Event | Detail type | Description |
|---|---|---|
click |
PointerEvent |
Clicking the button or pressing Enter or Space while the button is focused triggers the click event. |
oc-property-change |
OcButtonV1Events["oc-property-change"] |
Whenever a property value changes, this event triggers. Use this event to track all property changes within the component. Refer to the Events documentation for more information. |
oc-mount |
{ component: string; } |
Fired when the component is mounted to the DOM. The event is fired when the onMount hook of the component is called by the runtime. |
oc-unmount |
{ component: string; } |
Fired when the component is unmounted from the DOM. The event is fired when the function returned by the onMount hook of the component is called by the runtime. |
CSS custom properties
| Custom property | Default | Description |
|---|---|---|
--background-color |
undefined |
Sets a custom background color through a CSS variable. Note: The preferred way of using colors is via design tokens instead of hex values. Note: This CSS variable only applies when using variant="custom-color-strong" or variant="custom-color-soft". |
--text-color |
undefined |
Sets a custom text color through a CSS variable. Note: The preferred way of using colors is via design tokens instead of hex values. Note: This CSS variable only applies when using variant="custom-color-strong" or variant="custom-color-soft". |
Variations (v1)
Source: ./src/components/button/v1/Variations.mdx
Variations
Listed below are the most common variations of the button component as well as specific component variations for different use cases.
You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.
Default
Story: components-button-variations--default · tags: components, button, v1
The default configuration uses size=100 and the primary variant.
<oc-button-v1 variant="primary" size="100">Label</oc-button-v1>
Primary 100 with icons left and right
Story: components-button-variations--button-100-primary-with-icons-left-right · tags: components, button, v1
The primary variant, with size=100, and icons on both the left and right sides.
Args: icon-type-left=arrow-left, icon-type-right=arrow-right
<oc-button-v1 variant="primary" size="100" icon-type-left="arrow-left" icon-type-right="arrow-right">
Label
</oc-button-v1>
Primary 100 with fit-content and icon left
Story: components-button-variations--button-100-primary-with-icon-left-fit-content · tags: components, button, v1
The primary variant, with size=100 and fit-content=true, and an icon on the left side.
Args: icon-type-left=arrow-left, fit-content=true
<oc-button-v1 variant="primary" size="100" icon-type-left="arrow-left" fit-content>Label</oc-button-v1>
Primary 100 disabled with icon right
Story: components-button-variations--button-100-primary-disabled-with-icon-right · tags: components, button, v1
The disabled primary variant, with size=100, and an icon on the right side.
Args: icon-type-right=arrow-right, disabled=true
<oc-button-v1 variant="primary" size="100" icon-type-right="arrow-right" disabled>Label</oc-button-v1>
Primary 100 with loading state
Story: components-button-variations--button-100-with-loading-state · tags: components, button, v1
The primary variant, with size=100, and the setting for showing a loading spinner.
Make sure to place the button as well as the placeholder / actual content in a common
parent container. This way - together witha couple of aria roles -, a screen reader is able to
identify the container as a status / live region that is being updated.
Args: size=100, variant=primary, defaultSlot=Label, loading=true
<div role="status" aria-live="assertive" aria-atomic="true">
<!-- Content goes here -->
<oc-button-v1 variant="primary" size="100" loading>Label</oc-button-v1>
</div>
Story source (TypeScript, verbatim from Storybook)
{
name: "Primary 100 with loading state",
parameters: {
// Disables Chromatic's snapshotting, as the spinner is animated und thus leads to diffs
chromatic: {
disableSnapshot: true,
hideInChromatic: true
}
},
args: {
size: "100",
variant: "primary",
defaultSlot: "Label",
loading: true
},
render(args) {
const {
defaultSlot,
...props
} = args;
return html` <div role="status" aria-live="assertive" aria-atomic="true">
<!-- Content goes here -->
<oc-button-v1 ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
</div>`;
}
}
Primary 50 with icons left and right
Story: components-button-variations--button-50-primary-with-icons-left-right · tags: components, button, v1
The primary variant, with size=50, and icons on both the left and right sides.
Args: size=50, icon-type-left=arrow-left, icon-type-right=arrow-right
<oc-button-v1 variant="primary" size="50" icon-type-left="arrow-left" icon-type-right="arrow-right">
Label
</oc-button-v1>
Primary 50 disabled with icon right
Story: components-button-variations--button-50-primary-disabled-with-icon-right · tags: components, button, v1
The disabled primary variant, with size=50, and an icon on the right side.
Args: size=50, icon-type-right=arrow-right, disabled=true
<oc-button-v1 variant="primary" size="50" icon-type-right="arrow-right" disabled>Label</oc-button-v1>
Secondary 100
Story: components-button-variations--button-100-secondary · tags: components, button, v1
The secondary variant, with size=100.
Args: variant=secondary
<oc-button-v1 variant="secondary" size="100">Label</oc-button-v1>
Secondary 50
Story: components-button-variations--button-50-secondary · tags: components, button, v1
The secondary variant, with size=50.
Args: size=50, variant=secondary
<oc-button-v1 variant="secondary" size="50">Label</oc-button-v1>
Secondary-Over-Color 100
Story: components-button-variations--button-100-secondary-on-color · tags: components, button, v1
The secondary-over-color variant, with size=100.
Args: variant=secondary-over-color
<oc-button-v1 variant="secondary-over-color" size="100">Label</oc-button-v1>
Tertiary 100 with icon left
Story: components-button-variations--button-100-tertiary-with-icon-left · tags: components, button, v1
The tertiary variant, with size=100, and an icon on the left side.
Args: variant=tertiary, icon-type-left=arrow-left
<oc-button-v1 variant="tertiary" size="100" icon-type-left="arrow-left">Label</oc-button-v1>
Tertiary 50 with icon left
Story: components-button-variations--button-50-tertiary-with-icon-left · tags: components, button, v1
The tertiary variant, with size=50, and an icon on the left side.
Args: size=50, variant=tertiary, icon-type-left=arrow-left
<oc-button-v1 variant="tertiary" size="50" icon-type-left="arrow-left">Label</oc-button-v1>
With link behavior
Story: components-button-variations--with-link-behavior · tags: components, button, v1
The button component as a link. Search engines will detect the href.
Args: defaultSlot=<!--The <a> tag should be placed inside the light DOM to improve SEO.--><a href='#'>Label</a>
<oc-button-v1 variant="primary" size="100">
<!--The <a> tag should be placed inside the light DOM to improve SEO.--><a href='#'>Label</a>
</oc-button-v1>
With masked link behavior
Story: components-button-variations--with-masked-link-behavior · tags: components, button, v1
The button component as a masked link. Search engines will not detect the href.
Args: base64-href=Iw==
<oc-button-v1 variant="primary" size="100" base64-href="Iw==">Label</oc-button-v1>
With link switch behavior
Story: components-button-variations--with-link-switch-behavior · tags: components, button, v1
The button component as a link with switch behavior. Search engines will detect the href, while users receive the Base64-encoded version.
Args: base64-href=Iz92YXJpYW50PWZvbw==, defaultSlot=<!--Search engines will detect the href, while users receive the Base64-encoded version.--><a href='#'>Label</a>
<oc-button-v1 variant="primary" size="100" base64-href="Iz92YXJpYW50PWZvbw==">
<!--Search engines will detect the href, while users receive the Base64-encoded version.--><a href='#'>Label</a>
</oc-button-v1>
Demo: accessible multiple buttons
Story: components-button-variations--demo-accessible-multiple-buttons · tags: components, button, v1
Demonstration of how to make multiple buttons with the same label accessible.
<ul>
<li class="oc-p-50">
Produkt 1
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 1 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
<li class="oc-p-50">
Produkt 2
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
<li class="oc-p-50">
Produkt 3
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
</ul>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo: accessible multiple buttons",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<ul>
<li class="oc-p-50">
Produkt 1
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 1 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
<li class="oc-p-50">
Produkt 2
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
<li class="oc-p-50">
Produkt 3
<oc-button-v1
size="50"
variant="primary"
oc-aria-label="Produkt 2 zum Warenkorb hinzufügen"
>Zum Warenkorb hinzufügen
</oc-button-v1>
</li>
</ul>
`;
}
}
Demo: secondary button to success
Story: components-button-variations--demo-secondary-button-to-success · tags: components, button, v1
Demonstration of how to change a secondary button to a success button.
<oc-button-v1 id="follow" size="100" variant="secondary" fit-content oc-aria-label="Folgen">
Folgen
</oc-button-v1>
<script>
(() => {
const followButton = document.getElementById("follow");
followButton.addEventListener("click", followAction);
function followAction() {
followButton.success = true;
followButton.iconTypeLeft = "check";
followButton.innerHTML = "Gefolgt";
followButton.ocAriaLabel = "Gefolgt";
followButton.removeEventListener("click", followAction);
followButton.addEventListener("click", unfollowAction);
}
function unfollowAction() {
followButton.success = false;
followButton.iconTypeLeft = undefined;
followButton.innerHTML = "Folgen";
followButton.ocAriaLabel = "Folgen";
followButton.removeEventListener("click", unfollowAction);
followButton.addEventListener("click", followAction);
}
})();
</script>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo: secondary button to success",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<oc-button-v1 id="follow" size="100" variant="secondary" fit-content oc-aria-label="Folgen">
Folgen
</oc-button-v1>
<script>
(() => {
const followButton = document.getElementById("follow");
followButton.addEventListener("click", followAction);
function followAction() {
followButton.success = true;
followButton.iconTypeLeft = "check";
followButton.innerHTML = "Gefolgt";
followButton.ocAriaLabel = "Gefolgt";
followButton.removeEventListener("click", followAction);
followButton.addEventListener("click", unfollowAction);
}
function unfollowAction() {
followButton.success = false;
followButton.iconTypeLeft = undefined;
followButton.innerHTML = "Folgen";
followButton.ocAriaLabel = "Folgen";
followButton.removeEventListener("click", unfollowAction);
followButton.addEventListener("click", followAction);
}
})();
</script>
`;
}
}
Demo: using native button element
Story: components-button-variations--demo-using-native-button-element · tags: components, button, v1
Args: defaultSlot=<button>Label</button>
<oc-button-v1 variant="primary" size="100"><button>Label</button></oc-button-v1>
Custom color strong
Story: components-button-variations--custom-color-strong · tags: components, button, v1
Demonstration of how to use the custom-color-strong variant with custom background and text colors.
Args: variant=custom-color-strong, icon-type-left=smiley-positive, icon-type-right=arrow-right, --background-color=var(--oc-semantic-color-background-strong-pink), --text-color=var(--oc-semantic-color-background-soft-pink)
<oc-button-v1 class="${className}" variant="custom-color-strong" size="100" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)">Label</oc-button-v1>
${css}
Story source (TypeScript, verbatim from Storybook)
{
name: "Custom color strong",
args: {
variant: "custom-color-strong",
"icon-type-left": "smiley-positive",
"icon-type-right": "arrow-right",
"--background-color": "var(--oc-semantic-color-background-strong-pink)",
"--text-color": "var(--oc-semantic-color-background-soft-pink)"
},
render(args) {
const {
defaultSlot,
...props
} = args;
const {
css,
className
} = cssVariablesExample(props);
return html`
<oc-button-v1 class="${className}" ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
${css}
`;
}
}
Custom color soft
Story: components-button-variations--custom-color-soft · tags: components, button, v1
Demonstration of how to use the custom-color-soft variant with custom background and text colors.
Args: variant=custom-color-soft, icon-type-left=smiley-positive, icon-type-right=arrow-right, --background-color=var(--oc-semantic-color-background-soft-pink), --text-color=var(--oc-semantic-color-background-strong-pink)
<oc-button-v1 class="${className}" variant="custom-color-soft" size="100" icon-type-left="smiley-positive" icon-type-right="arrow-right" style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)">Label</oc-button-v1>
${css}
Story source (TypeScript, verbatim from Storybook)
{
name: "Custom color soft",
args: {
variant: "custom-color-soft",
"icon-type-left": "smiley-positive",
"icon-type-right": "arrow-right",
"--background-color": "var(--oc-semantic-color-background-soft-pink)",
"--text-color": "var(--oc-semantic-color-background-strong-pink)"
},
render(args) {
const {
defaultSlot,
...props
} = args;
const {
css,
className
} = cssVariablesExample(props);
return html`
<oc-button-v1 class="${className}" ${spread(props)}>${unsafeHTML(defaultSlot)}</oc-button-v1>
${css}
`;
}
}
Demo: custom colors
Story: components-button-variations--demo-custom-colors · tags: components, button, v1
Comprehensive demo showcasing multiple buttons with custom background and text colors using design tokens.
This demo demonstrates the flexibility of the custom-color-strong and custom-color-soft variants.
<style>
.demo-custom-colors {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 2rem;
}
.demo-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.demo-item {
width: 100%;
max-width: 328px;
}
</style>
<div class="demo-custom-colors">
<div class="demo-row">
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-strong-mint);"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: #522EB7"
>
Label
</oc-button-v1>
</div>
</div>
<div class="demo-row" style="margin-top: 1.5rem">
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-soft-mint)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: #E5D5FE; --text-color: #522EB7"
>
Label
</oc-button-v1>
</div>
</div>
</div>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo: custom colors",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<style>
.demo-custom-colors {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 2rem;
}
.demo-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.demo-item {
width: 100%;
max-width: 328px;
}
</style>
<div class="demo-custom-colors">
<div class="demo-row">
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-strong-mint);"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-strong-pink); --text-color: var(--oc-semantic-color-background-soft-pink)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-strong"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: #522EB7"
>
Label
</oc-button-v1>
</div>
</div>
<div class="demo-row" style="margin-top: 1.5rem">
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-soft-mint)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: var(--oc-semantic-color-background-soft-pink); --text-color: var(--oc-semantic-color-background-strong-pink)"
>
Label
</oc-button-v1>
</div>
<div class="demo-item">
<oc-button-v1
variant="custom-color-soft"
icon-type-left="smiley-positive"
icon-type-right="arrow-right"
style="--background-color: #E5D5FE; --text-color: #522EB7"
>
Label
</oc-button-v1>
</div>
</div>
</div>
`;
}
}
Interaction tests (ButtonV1.form.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Form Association
Story: components-button-interaction-tests--form-association · tags: play-fn
<div id="form-association">
<oc-button-v1 type="submit">no form</oc-button-v1>
<oc-button-v1 type="submit" form="form-association-form" formaction="/bar"
>referenced form
</oc-button-v1>
<form id="form-association-form" method="get" action="/foo">
<oc-button-v1 type="submit" formmethod="post">within form</oc-button-v1>
</form>
</div>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html` <div id="form-association">
<oc-button-v1 type="submit">no form</oc-button-v1>
<oc-button-v1 type="submit" form="form-association-form" formaction="/bar"
>referenced form
</oc-button-v1>
<form id="form-association-form" method="get" action="/foo">
<oc-button-v1 type="submit" formmethod="post">within form</oc-button-v1>
</form>
</div>`;
},
async play({
canvasElement
}) {
const form = canvasElement.getElementsByTagName("form").item(0)!;
const [button1, button2, button3] = Array.from(canvasElement.getElementsByTagName("oc-button-v1"));
// @ts-expect-error -- internally defined property
await expect(button1.internals.form).toBe(null);
// @ts-expect-error -- internally defined property
await expect(button2.internals.form).toBe(form);
// @ts-expect-error -- internally defined property
await expect(button3.internals.form).toBe(form);
}
}
Should Not Submit When Disabled
Story: components-button-interaction-tests--should-not-submit-when-disabled · tags: play-fn
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit" disabled>never submit me</oc-button-v1>
<oc-button-v1 type="submit" disabled formaction="/trigger-me"
>never submit me
</oc-button-v1>
<button type="submit"></button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit" disabled>never submit me</oc-button-v1>
<oc-button-v1 type="submit" disabled formaction="/trigger-me"
>never submit me
</oc-button-v1>
<button type="submit"></button>
</form>
`;
},
async play({
canvasElement
}) {
const [button1] = Array.from(canvasElement.getElementsByTagName("oc-button-v1"));
const input = canvasElement.getElementsByTagName("input").item(0)!;
const form = canvasElement.querySelector("form") as HTMLFormElement;
let numberOfSubmitEvents = 0;
form.addEventListener("submit", ev => {
ev.preventDefault();
numberOfSubmitEvents += 1;
});
await userEvent.type(input, "{Enter}");
await userEvent.type(button1, "{Enter}");
await expect(numberOfSubmitEvents, "never submit disabled button").toBe(0);
}
}
Should Not Submit From Textarea
Story: components-button-interaction-tests--should-not-submit-from-textarea · tags: play-fn
<form id="form1">
<label>
<textarea name="user"> </textarea>
</label>
<oc-button-v1 type="submit">never submit me</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1">
<label>
<textarea name="user"> </textarea>
</label>
<oc-button-v1 type="submit">never submit me</oc-button-v1>
</form>
`;
},
async play({
canvasElement
}) {
const input = canvasElement.getElementsByTagName("textarea").item(0)!;
const form = canvasElement.querySelector("form") as HTMLFormElement;
let numberOfSubmitEvents = 0;
form.addEventListener("submit", ev => {
ev.preventDefault();
numberOfSubmitEvents += 1;
});
await userEvent.type(input, "{Enter}");
await expect(numberOfSubmitEvents, "never submit disabled button").toBe(0);
}
}
Should Work With Two Buttons
Story: components-button-interaction-tests--should-work-with-two-buttons · tags: play-fn
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit">do not submit me</oc-button-v1>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit">do not submit me</oc-button-v1>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
`;
},
async play({
canvasElement
}) {
const button2 = canvasElement.getElementsByTagName("oc-button-v1").item(1)!;
const form = canvasElement.querySelector("form") as HTMLFormElement;
let submitter: HTMLElement | null = null;
let numberOfSubmitEvents = 0;
form.addEventListener("submit", ev => {
ev.preventDefault();
submitter = ev.submitter;
numberOfSubmitEvents += 1;
});
await userEvent.type(button2, "{Enter}");
await expect(numberOfSubmitEvents, "submit once").toBe(1);
await expect(
// @ts-expect-error -- internally defined property
submitter?.formAction.endsWith("/trigger-me"), "should triggered second button only").toBeTruthy();
await userEvent.click(button2);
await expect(numberOfSubmitEvents, "second submit").toBe(2);
await expect(
// @ts-expect-error -- internally defined property
submitter?.formAction.endsWith("/trigger-me"), "should triggered second button only").toBeTruthy();
}
}
Should Trigger Form Action
Story: components-button-interaction-tests--should-trigger-form-action · tags: play-fn
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
<input type="submit" formaction="/do-not-trigger-me" />
<button type="submit" formaction="/do-not-trigger-me"></button>
<oc-button-v1 type="submit" formaction="/do-not-trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
<input type="submit" formaction="/do-not-trigger-me" />
<button type="submit" formaction="/do-not-trigger-me"></button>
<oc-button-v1 type="submit" formaction="/do-not-trigger-me">submit</oc-button-v1>
</form>
`;
},
async play({
canvasElement
}) {
const input = canvasElement.getElementsByTagName("input").item(0)!;
const form = canvasElement.querySelector("form") as HTMLFormElement;
let submitter: HTMLElement | null = null;
let numberOfSubmitEvents = 0;
form.addEventListener("submit", ev => {
ev.preventDefault();
submitter = ev.submitter;
numberOfSubmitEvents += 1;
});
await userEvent.type(input, "{Enter}");
await expect(numberOfSubmitEvents, "submit once").toBe(1);
await expect(
// @ts-expect-error -- internally defined property
submitter?.formAction.endsWith("/trigger-me"), "should have form action '/trigger-me'").toBeTruthy();
}
}
Should Not Be Triggered By Non Form Elements
Story: components-button-interaction-tests--should-not-be-triggered-by-non-form-elements · tags: play-fn
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<a href="#">Foo</a>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1" method="get" action="/foo">
<label>
<input type="text" name="user" />
</label>
<a href="#">Foo</a>
<oc-button-v1 type="submit" formaction="/trigger-me">submit</oc-button-v1>
</form>
`;
},
async play({
canvasElement
}) {
const link = canvasElement.getElementsByTagName("a").item(0)!;
const form = canvasElement.querySelector("form") as HTMLFormElement;
link.addEventListener("click", ev => ev.preventDefault());
let numberOfSubmitEvents = 0;
form.addEventListener("submit", ev => {
ev.preventDefault();
numberOfSubmitEvents += 1;
});
await userEvent.type(link, "{Enter}");
await expect(numberOfSubmitEvents, "should not trigger submit").toBe(0);
}
}
Should Submit Like Native Buttons
Story: components-button-interaction-tests--should-submit-like-native-buttons · tags: play-fn
<form id="form1">
<label>
{Enter}
<input type="text" name="name" />
</label>
<oc-button-v1 type="submit" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form2">
<label>
{Enter}
<input type="text" name="name" />
</label>
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form3">
<label>
{Enter}
<input type="text" name="name" />
</label>
Should submit the form but not by oc-button-v1
</form>
<form id="form4">
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
<label>
{Enter}
<input type="text" name="name" form="form4" />
</label>
</form>
<form id="form5">
<label>
{Enter}
<input type="text" name="name" />
</label>
<oc-button-v1 type="submit" disabled data-should-not-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form6">
<label>
{Enter}
<input type="text" name="name" />
</label>
<input type="submit" disabled data-should-not-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form7">
<label>
{Enter}
<input type="text" name="name" />
</label>
</form>
<oc-button-v1 type="submit" form="form7" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" form="form7" data-should-not-submit></oc-button-v1>
<button type="submit" form="form7" data-should-not-submit></button>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1">
<label>
{Enter}
<input type="text" name="name" />
</label>
<oc-button-v1 type="submit" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form2">
<label>
{Enter}
<input type="text" name="name" />
</label>
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form3">
<label>
{Enter}
<input type="text" name="name" />
</label>
Should submit the form but not by oc-button-v1
</form>
<form id="form4">
<input type="submit" data-should-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
<label>
{Enter}
<input type="text" name="name" form="form4" />
</label>
</form>
<form id="form5">
<label>
{Enter}
<input type="text" name="name" />
</label>
<oc-button-v1 type="submit" disabled data-should-not-submit></oc-button-v1>
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form6">
<label>
{Enter}
<input type="text" name="name" />
</label>
<input type="submit" disabled data-should-not-submit />
<oc-button-v1 type="submit" data-should-not-submit></oc-button-v1>
</form>
<form id="form7">
<label>
{Enter}
<input type="text" name="name" />
</label>
</form>
<oc-button-v1 type="submit" form="form7" data-should-submit></oc-button-v1>
<oc-button-v1 type="submit" form="form7" data-should-not-submit></oc-button-v1>
<button type="submit" form="form7" data-should-not-submit></button>
`;
},
async play({
canvasElement
}) {
const inputs = Array.from(canvasElement.querySelectorAll(`input[name="name"]`)) as HTMLInputElement[];
const submits: {
form: string;
submitter: HTMLElement | null;
}[] = [];
canvasElement.querySelectorAll("form").forEach(form => {
form.addEventListener("submit", ev => {
ev.preventDefault();
submits.push({
form: form.id,
submitter: ev.submitter
});
});
});
// eslint-disable-next-line no-restricted-syntax
for (const input of inputs) {
// eslint-disable-next-line no-await-in-loop
await userEvent.type(input, "{Enter}");
}
await expect(submits.length).toBe(5);
await expect(submits[0].form).toBe("form1");
await expect(submits[0].submitter?.dataset.shouldNotSubmit).toBeUndefined();
await expect(submits[1].form).toBe("form2");
await expect(submits[1].submitter?.dataset.shouldNotSubmit).toBeUndefined();
await expect(submits[2].form).toBe("form3");
await expect(submits[2].submitter).toBe(undefined);
await expect(submits[3].form).toBe("form4");
await expect(submits[3].submitter?.dataset.shouldSubmit).toBeDefined();
await expect(submits[4].form).toBe("form7");
await expect(submits[4].submitter?.dataset.shouldNotSubmit).toBeUndefined();
}
}
Interaction tests (ButtonV1.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Trigger Form Submit When Button Type Is Set
Story: components-button-interaction-tests--trigger-form-submit-when-button-type-is-set · tags: play-fn
Args: variant=primary, size=100, type=submit
<form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100" type="submit">Submit Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
args: {
variant: "primary",
size: "100",
type: "submit"
},
render(args) {
return html` <form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 ${spread(args)}>Submit Form</oc-button-v1>
</form>`;
},
play: async ({
canvasElement
}) => {
const innerButton = getByShadowRole(canvasElement, "button");
const form = canvasElement.querySelector("form");
let formSubmitted = false;
if (form) {
form.addEventListener("submit", event => {
formSubmitted = true;
event.preventDefault();
});
}
await expect(formSubmitted).toBeFalsy();
await click(innerButton);
await expect(formSubmitted).toBeTruthy();
}
}
Do Not Trigger Form Submit When Button Type Is Not Set
Story: components-button-interaction-tests--do-not-trigger-form-submit-when-button-type-is-not-set · tags: play-fn
Args: variant=primary, size=100
<form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100">Submit Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
args: {
variant: "primary",
size: "100"
},
render(args) {
return html` <form onsubmit="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 ${spread(args)}>Submit Form</oc-button-v1>
</form>`;
},
play: async ({
canvasElement
}) => {
const innerButton = getByShadowRole(canvasElement, "button");
const form = canvasElement.querySelector("form");
let formSubmitted = false;
if (form) {
form.addEventListener("submit", event => {
formSubmitted = true;
event.preventDefault();
});
}
await expect(formSubmitted).toBeFalsy();
await click(innerButton);
await expect(formSubmitted).toBeFalsy();
}
}
Trigger Form Reset When Button Type Is Set
Story: components-button-interaction-tests--trigger-form-reset-when-button-type-is-set · tags: play-fn
Args: variant=primary, size=100, type=reset
<form onsubmit="return false;" onreset="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 variant="primary" size="100" type="reset">Reset Form</oc-button-v1>
</form>
Story source (TypeScript, verbatim from Storybook)
{
args: {
variant: "primary",
size: "100",
type: "reset"
},
render(args) {
return html` <form onsubmit="return false;" onreset="return false;">
<label for="username">Username</label>
<input id="username" />
<oc-button-v1 ${spread(args)}>Reset Form</oc-button-v1>
</form>`;
},
play: async ({
canvasElement
}) => {
const innerButton = getByShadowRole(canvasElement, "button");
const form = canvasElement.querySelector("form");
let formResetted = false;
if (form) {
form.addEventListener("reset", event => {
formResetted = true;
event.preventDefault();
});
}
await expect(formResetted).toBeFalsy();
await click(innerButton);
await expect(formResetted).toBeTruthy();
}
}
Should Show Loading On Click
Story: components-button-interaction-tests--should-show-loading-on-click · tags: play-fn
Args: variant=primary, size=100, type=button
<oc-button-v1 variant="primary" size="100" type="button">Button</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
args: {
variant: "primary",
size: "100",
type: "button"
},
parameters: {
// Disables Chromatic's snapshotting, as the spinner is animated und thus leads to diffs
chromatic: {
disableSnapshot: true
}
},
render(args) {
return html` <oc-button-v1 ${spread(args)}>Button</oc-button-v1>`;
},
play: async ({
canvasElement
}) => {
// given
const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
const button = getByShadowRole(canvasElement, "button");
button.addEventListener("click", () => {
ocButton.setAttribute("loading", "");
});
// there should be no spinner before clicking the button
const noSpinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
await expect(noSpinner, "no spinner should be rendered before clicking the button").toBeFalsy();
// when
await click(button);
// then
// wait for some time because of the default debounce (?)
await waitFor("spinner to arrive", async () => {
const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
await expect(spinner, "spinner should be rendered after clicking the button").toBeTruthy();
});
}
}
Should Not Trigger Click When Loading Is True
Story: components-button-interaction-tests--should-not-trigger-click-when-loading-is-true · tags: play-fn
<oc-button-v1 type="submit">Button</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html` <oc-button-v1 type="submit">Button</oc-button-v1>`;
},
play: async ({
canvasElement
}) => {
const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
const button = getByShadowRole(canvasElement, "button");
let clickCount = 0;
let documentClickCount = 0;
document.addEventListener("click", () => {
documentClickCount++;
});
ocButton.addEventListener("click", () => {
clickCount++;
});
// when
await click(button);
// then
await expect(clickCount, "direct click count should be 1 after first click").toBe(1);
await expect(documentClickCount, "document click count should be 1 after first click").toBe(1);
// when loading is set to true
ocButton.setAttribute("loading", "");
// give it time to settle
await waitFor("spinner to arrive", async () => {
const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
await expect(spinner, "spinner should be rendered after clicking the button").toBeTruthy();
});
await click(button);
// then the click event should not be triggered again
await expect(clickCount, "direct click count should increase when loading is true").toBe(2);
// TODO slop: the document click count should not increase because the click event should not propagate, but currently it does for the first click after setting loading to true. This is because the bouncer logic is only applied on subsequent clicks, but not on the first click after setting loading to true. This is a known issue and will be fixed in the next iteration.
await expect(documentClickCount, "document click count should not increase when loading is true, but does for the first click after setting loading to true").toBe(2);
await click(button);
// then the click event should not be triggered again
await expect(clickCount, "direct click count should increase when loading is true").toBe(3);
await expect(documentClickCount, "document click count should not increase when loading is true, and now works for the second click after setting loading to true").toBe(2);
// and the spinner should be rendered inside the button
const spinner = Array.from(button.childNodes).find(node => node.nodeName === "OC-SPINNER-V1");
await expect(spinner, "spinner should be rendered when loading is true").toBeTruthy();
}
}
Should Handle Base 64 Href
Story: components-button-interaction-tests--should-handle-base-64-href · tags: play-fn
<oc-button-v1 base64-href="L2Zvbw==">Link</oc-button-v1>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html` <oc-button-v1 base64-href="L2Zvbw==">Link</oc-button-v1>`;
},
async play({
canvasElement
}) {
const ocButton = canvasElement.getElementsByTagName("oc-button-v1").item(0)!;
ocButton.focus();
let linkElement: HTMLAnchorElement;
await waitFor("render link element inside shadow root", async () => {
// instead of the button element there should now be an anchor element
const buttonElement = ocButton.shadowRoot!.querySelector("button");
linkElement = ocButton.shadowRoot!.querySelector("a")!;
await expect(buttonElement, "no button should be rendered").toBeNull();
await expect(linkElement.tagName, "anchor should be rendered").toBe("A");
});
// anchor element inside the shadow root should have the correct href attribute
await expect(linkElement!.getAttribute("href")).toBe(`/foo`);
// anchor element inside the shadow root should have the same fully href as the host
await expect(ocButton.href).toBe(`${window.location.origin}/foo`);
await expect(linkElement!.href).toBe(`${window.location.origin}/foo`);
}
}