| Version | Tag | Status | API |
|---|---|---|---|
| v1 | <oc-text-field-v1> |
Stable, allowed for generation | TextFieldV1 |
Overview (v1)
Source: ./src/components/text-field/v1/Overview.mdx
Text field
The text field component provides a configurable single-line input field for user text input. It has attributes for form identification, initial value setting and additional information display. It supports various styling variants, as well as different input types, such as text, email, and more.
Default variation
Story Default:
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text">Label</oc-text-field-v1>
Configuration
The text field component offers a variety of styling variants such as default, error, success, and warning.
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 text field 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 Text field UX documentation for detailed user experience guidelines.
Layout considerations
This component has visual overflow.
It extends beyond its bounding box and is clipped by parent containers with overflow: hidden.
Ensure the parent container has sufficient padding to accommodate the component's full visual area.
Accessibility
The text field component comes with a set of built-in accessibility features to ensure a seamless experience for all users.
Important
If the text field component is used in a form, pressing
Enterwhile focused on the text field triggers the submit action immediately (implicit submit) if anoc-buttoncomponent or a normal HTML button with the typesubmitis also present in the form.
Use oc-aria-label
To make the text field recognizable for screen readers, use the oc-aria-label attribute to provide a clear and descriptive label when the component does not have content in the default slot.
See the general accessibility documentation for guidance on using oc-aria-label, including how it works with link and masked link behavior.
Further reading
Configuration (v1)
Source: ./src/components/text-field/v1/Configuration.mdx
Text field configuration
Configure the text field 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-text-field-v1 name="myName" placeholder="this is a placeholder" type="text">Label</oc-text-field-v1>
Interactive configurator (Storybook controls); every option is listed in the API section of this file.
API v1
Source: ./src/components/text-field/v1/TextFieldV1.API.g.mdx
Text Field v1 API
API: <oc-text-field-v1> (TextFieldV1)
The Text Field component provides a single-line input field for user text input. It has attributes for form identification, initial value setting and additional information display. It supports various styling variants, as well as different input types, such as text, email, and more.
Attributes / properties
| Attribute | Type | Default | Required | Description |
|---|---|---|---|---|
variant |
"default" | "error" | "success" | "warning" |
"default" |
no | Sets the main styling and behavior of the text field. |
type |
"number" | "text" | "email" | "tel" | "url" | "integer" | "decimal" | "file" | "date" |
"text" |
no | Defines the type of the input value. The type integer is a stricter variant of type number and only accepts integer numbers.The type decimal is a stricter variant of type number and only allows decimal numbers. |
value |
string |
"" |
no | Sets the initial and reset value of the text field to the provided value for form processing. |
placeholder |
string |
undefined |
no | Provides additional information related to the desired input value of the text field. Displayed as a faint text in the text field when it's empty. Note: This does not work when the type is set to date. |
hint |
string |
undefined |
no | Provides additional information related to the text field below the input element. |
validation-message |
string |
undefined |
no | Provides a validation message related to the text field below the input element. Implicitly sets the text field to the error state. |
prefix-icon |
icon name (427 values; see Icon list in `storybook/components/icon/README.md`) |
undefined |
no | Sets an icon that shows on the left side of the text field and before the prefix-text. |
prefix-text |
string |
undefined |
no | Sets a text or symbol that is displayed before the input text to provide the user some context, e.g. a unit. |
suffix-icon |
icon name (427 values; see Icon list in `storybook/components/icon/README.md`) |
undefined |
no | Sets an icon that shows on the right side of the text field and after the suffix-text. |
suffix-text |
string |
undefined |
no | Sets a text or symbol that is displayed on the right side of the text field to provide the user some context or information. |
text-transform |
uppercase |
undefined |
no | Defines the text transformation of the input value. - "uppercase" transforms the input value to uppercase letters. |
disabled |
boolean |
false |
no | Disables the text field, preventing user interaction and input. |
required |
boolean |
false |
no | Marks the input as required for screen readers. Doesn't prevent form submission on its own. |
hide-counter |
boolean |
false |
no | Hides the counter. Maximum number of characters allowed in the text field is still enforced. |
name |
string |
undefined |
no | Sets the name tag to identify the text field when submitting a form. |
autocomplete |
"" | "off" | "on" | "email" | "tel" | "tel-area-code" | "tel-country-code" | "tel-extension" | "tel-local" | "tel-local-prefix" | "tel-local-suffix" | "tel-national" | "additional-name" | "address-level1" | "address-level2" | "address-level3" | "address-level4" | "address-line1" | "address-line2" | "address-line3" | "bday-day" | "bday-month" | "bday-year" | "cc-csc" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-family-name" | "cc-given-name" | "cc-name" | "cc-number" | "cc-type" | "country" | "country-name" | "current-password" | "family-name" | "given-name" | "honorific-prefix" | "honorific-suffix" | "name" | "new-password" | "one-time-code" | "organization" | "postal-code" | "street-address" | "transaction-amount" | "transaction-currency" | "username" |
"off" |
no | Specifies if browsers are permitted to provide assistance in filling out the field value. Set to one of the enum values to enable specific autocomplete. |
inputmode |
"text" | "search" | "none" | "email" | "tel" | "url" | "decimal" | "numeric" |
undefined |
no | Specifies the input mode for the text field, which hints at the type of data that might be entered by the user. This can influence the virtual keyboard layout on mobile devices. |
min |
number |
undefined |
no | Specifies the minimum value allowed in the text field. Note: This only applies when the type is set to number or integer |
max |
number |
undefined |
no | Specifies the maximum value allowed in the text field. Note: This only applies when the type is set to number or integer |
minlength |
number |
undefined |
no | Specifies the minimum number of characters required in the text field. |
maxlength |
number |
undefined |
no | Specifies the maximum number of characters allowed in the text field. |
pattern |
string |
undefined |
no | Sets a regular expression that defines a required pattern for the input value. The input is considered valid if it matches the pattern. |
oc-aria-label |
string |
undefined |
no | Sets the ARIA label of the text field. |
without-optional-label |
boolean |
false |
no | Excludes the automatic "(optional)" label suffix even if the feature toggle is active. |
Slots
| Slot | Required | Description |
|---|---|---|
default |
yes | Sets the label of the text field to provide a brief description of the input field. |
Events
| Event | Detail type | Description |
|---|---|---|
oc-property-change |
OcTextFieldV1Events["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. |
Methods
selectionStart:numberGets or sets the start position of the current text selection in the text field. This is a property with getter/setter access, not a callable method.
selectionEnd:numberGets or sets the end position of the current text selection in the text field. This is a property with getter/setter access, not a callable method.
Variations (v1)
Source: ./src/components/text-field/v1/Variations.mdx
Variations
Listed below are the most common variations of the text field 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-text-field-variations--default · tags: components
The default configuration uses the default variant and the placeholder attribute to show a placeholder text in the Text Field when clicked.
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text">Label</oc-text-field-v1>
Error
Story: components-text-field-variations--error · tags: components
The error variant uses a red color to indicate an error state.
Args: validation-message=Invalid input
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" validation-message="Invalid input">
Label
</oc-text-field-v1>
Success
Story: components-text-field-variations--success · tags: components
The success variant uses a green color to indicate a success state.
Args: variant=success
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" variant="success">
Label
</oc-text-field-v1>
Warning
Story: components-text-field-variations--warning · tags: components
The warning variant uses a yellow color to indicate a warning state.
Args: variant=warning
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" variant="warning">
Label
</oc-text-field-v1>
Disabled
Story: components-text-field-variations--disabled · tags: components
Variation with the attribute disabled set to true disables the Text Field, preventing users from interacting with it.
Args: disabled=true
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" disabled>Label</oc-text-field-v1>
With hint
Story: components-text-field-variations--with-hint · tags: components
The hint attribute displays a hint below the Text Field.
Args: hint=Hint
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" hint="Hint">Label</oc-text-field-v1>
With input mode
Story: components-text-field-variations--with-input-mode · tags: components
The inputmode attribute controls the type of virtual keyboard displayed on mobile devices.
Args: inputmode=tel
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" inputmode="tel">
Label
</oc-text-field-v1>
With maxlength and counter and hint
Story: components-text-field-variations--with-maxlength-and-counter-and-hint · tags: components
Variation using the maxlength attribute limits the number of characters that can be entered in the Text Field and displays a counter below the Text Field to show the remaining characters.
The hint attribute displays a hint below the Text Field.
Args: maxlength=50, hint=Hint
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" maxlength="50" hint="Hint">
Label
</oc-text-field-v1>
With maxlength without counter
Story: components-text-field-variations--with-maxlength-without-counter · tags: components
Variation using the maxlength attribute limits the number of characters that can be entered in the Text Field and displays a counter below the Text Field to show the remaining characters.
The hide-counter attribute hides the counter of the Text Field. Maximum number of characters allowed in the text field is still enforced.
Args: maxlength=50, hide-counter=true
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" hide-counter maxlength="50">
Label
</oc-text-field-v1>
With prefix text
Story: components-text-field-variations--with-prefix-text · tags: components
Variation with the attribute prefix-text that adds a text label before the input field.
Args: prefix-text=€
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" prefix-text="€">
Label
</oc-text-field-v1>
With suffix text
Story: components-text-field-variations--with-suffix-text · tags: components
Variation with the attribute suffix-text that adds a text label after the input field.
Args: suffix-text=cm
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" suffix-text="cm">
Label
</oc-text-field-v1>
With prefix icon
Story: components-text-field-variations--with-prefix-icon · tags: components
Variation with the attribute prefix-icon that adds an icon before the input field.
Args: prefix-icon=euro
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" prefix-icon="euro">
Label
</oc-text-field-v1>
With suffix icon
Story: components-text-field-variations--with-suffix-icon · tags: components
Variation with the attribute suffix-icon that adds an icon after the input field.
Args: suffix-icon=euro
<oc-text-field-v1 name="myName" placeholder="this is a placeholder" type="text" suffix-icon="euro">
Label
</oc-text-field-v1>
Demo form
Story: components-text-field-variations--demo-form · tags: components
The demo form showcases various configurations of the Text Area component using the attributes minlength, maxlength, type, placeholder, suffix-icon, suffix-text, and hint.
<h1 style="margin-bottom: 32px">Demo form</h1>
<form id="form1">
<oc-text-field-v1
name="first-name"
placeholder="Max"
minlength="3"
value=""
hint="First part of your name"
>First Name</oc-text-field-v1
>
<oc-text-field-v1
name="last-name"
placeholder="Mustermann"
maxlength="50"
hint="Last part of your name"
>Last Name</oc-text-field-v1
>
<oc-text-field-v1 name="email" type="email" placeholder="foo@bar.de" required
>E-Mail</oc-text-field-v1
>
<oc-text-field-v1 name="size" type="integer" suffix-text="cm">Size</oc-text-field-v1>
<oc-text-field-v1 name="money" type="decimal" suffix-icon="euro">Money</oc-text-field-v1>
<div class="buttons">
<oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
<oc-button-v1 type="submit">Submit</oc-button-v1>
</div>
</form>
<script>
(() => {
const [form] = document.getElementsByTagName("form");
form.addEventListener("submit", (ev) => {
let formData = new FormData(form);
let data = "";
formData.forEach((value, key) => (data += key + "=" + value + "\\n"));
console.log(data);
alert("sending:\\n" + data);
ev.preventDefault();
});
})();
</script>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo form",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<h1 style="margin-bottom: 32px">Demo form</h1>
<form id="form1">
<oc-text-field-v1
name="first-name"
placeholder="Max"
minlength="3"
value=""
hint="First part of your name"
>First Name</oc-text-field-v1
>
<oc-text-field-v1
name="last-name"
placeholder="Mustermann"
maxlength="50"
hint="Last part of your name"
>Last Name</oc-text-field-v1
>
<oc-text-field-v1 name="email" type="email" placeholder="foo@bar.de" required
>E-Mail</oc-text-field-v1
>
<oc-text-field-v1 name="size" type="integer" suffix-text="cm">Size</oc-text-field-v1>
<oc-text-field-v1 name="money" type="decimal" suffix-icon="euro">Money</oc-text-field-v1>
<div class="buttons">
<oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
<oc-button-v1 type="submit">Submit</oc-button-v1>
</div>
</form>
<script>
(() => {
const [form] = document.getElementsByTagName("form");
form.addEventListener("submit", (ev) => {
let formData = new FormData(form);
let data = "";
formData.forEach((value, key) => (data += key + "=" + value + "\\n"));
console.log(data);
alert("sending:\\n" + data);
ev.preventDefault();
});
})();
</script>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
`;
}
}
Demo autocomplete
Story: components-text-field-variations--autocomplete · tags: components
A Autocomplete demo form showcases various configurations of the Text Area component using the autocomplete attribute to permit browsers to provide assistance in filling out the field value.
<h1 style="margin-bottom: 32px">Autocomplete</h1>
<form>
<oc-text-field-v1 name="name" placeholder="Max Mustermann" autocomplete="name"
>Full Name
</oc-text-field-v1>
<oc-text-field-v1 name="last-name" placeholder="Mustermann" autocomplete="family-name"
>Last Name
</oc-text-field-v1>
<oc-text-field-v1 name="first-name" placeholder="Max" autocomplete="given-name"
>First Name
</oc-text-field-v1>
<oc-text-field-v1 name="email" type="email" autocomplete="email">E-Mail</oc-text-field-v1>
<oc-text-field-v1 name="tel" type="tel" autocomplete="tel">Phone number</oc-text-field-v1>
<oc-text-field-v1 name="country" autocomplete="country">Country</oc-text-field-v1>
<oc-text-field-v1 name="postal-code" autocomplete="postal-code"
>postal-code
</oc-text-field-v1>
<oc-text-field-v1 name="street-address" autocomplete="street-address"
>street-address
</oc-text-field-v1>
<oc-text-field-v1 name="address-line1" autocomplete="address-line1"
>address-line1
</oc-text-field-v1>
<div class="buttons">
<oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
</div>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo autocomplete",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<h1 style="margin-bottom: 32px">Autocomplete</h1>
<form>
<oc-text-field-v1 name="name" placeholder="Max Mustermann" autocomplete="name"
>Full Name
</oc-text-field-v1>
<oc-text-field-v1 name="last-name" placeholder="Mustermann" autocomplete="family-name"
>Last Name
</oc-text-field-v1>
<oc-text-field-v1 name="first-name" placeholder="Max" autocomplete="given-name"
>First Name
</oc-text-field-v1>
<oc-text-field-v1 name="email" type="email" autocomplete="email">E-Mail</oc-text-field-v1>
<oc-text-field-v1 name="tel" type="tel" autocomplete="tel">Phone number</oc-text-field-v1>
<oc-text-field-v1 name="country" autocomplete="country">Country</oc-text-field-v1>
<oc-text-field-v1 name="postal-code" autocomplete="postal-code"
>postal-code
</oc-text-field-v1>
<oc-text-field-v1 name="street-address" autocomplete="street-address"
>street-address
</oc-text-field-v1>
<oc-text-field-v1 name="address-line1" autocomplete="address-line1"
>address-line1
</oc-text-field-v1>
<div class="buttons">
<oc-button-v1 type="reset" variant="secondary">Reset</oc-button-v1>
</div>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
`;
}
}
Demo birthday
Story: components-text-field-variations--demo-birthday · tags: components
A demo showcasing a birthday input using three separate Text Fields for day, month, and year with appropriate attributes for each field. Utilizes the min and max attributes to restrict input values.
<h1 style="margin-bottom: 32px">Birthday</h1>
<p>
This story demonstrates a birthday input using three separate Text Fields for day, month,
and year with appropriate attributes for each field.
</p>
<p>
Note that this is just a visual demo. In a real-world application, additional logic would be
needed to validate the combined date input.
</p>
<p>
Values are restricted using the <code>min</code> and <code>max</code> attributes to ensure
valid date components.
</p>
<div style="margin-bottom: 16px">
<ul class="oc-list--unordered">
<li><strong>Day:</strong> <code>min="1", max="31", maxlength="2"</code></li>
<li><strong>Month:</strong> <code>min="1", max="12", maxlength="2"</code></li>
<li><strong>Year:</strong> <code>min="1900", max="2025", maxlength="4"</code></li>
</ul>
</div>
<div style="display: flex; gap: 16px">
<oc-text-field-v1
name="day"
placeholder="DD"
type="integer"
min="1"
max="31"
maxlength="2"
style="flex-shrink: 0; width: 60px"
hide-counter
>Day
</oc-text-field-v1>
<oc-text-field-v1
name="month"
placeholder="MM"
type="integer"
min="1"
max="12"
maxlength="2"
style="flex-shrink: 0; width: 80px"
hide-counter
>Month
</oc-text-field-v1>
<oc-text-field-v1
name="year"
placeholder="YYYY"
type="integer"
min="1900"
max="2025"
maxlength="4"
style="flex-shrink: 0; width: 80px"
hide-counter
>Year
</oc-text-field-v1>
</div>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo birthday",
parameters: {
controls: {
disabled: true
},
chromatic: {
disableSnapshot: true,
hideInChromatic: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<h1 style="margin-bottom: 32px">Birthday</h1>
<p>
This story demonstrates a birthday input using three separate Text Fields for day, month,
and year with appropriate attributes for each field.
</p>
<p>
Note that this is just a visual demo. In a real-world application, additional logic would be
needed to validate the combined date input.
</p>
<p>
Values are restricted using the <code>min</code> and <code>max</code> attributes to ensure
valid date components.
</p>
<div style="margin-bottom: 16px">
<ul class="oc-list--unordered">
<li><strong>Day:</strong> <code>min="1", max="31", maxlength="2"</code></li>
<li><strong>Month:</strong> <code>min="1", max="12", maxlength="2"</code></li>
<li><strong>Year:</strong> <code>min="1900", max="2025", maxlength="4"</code></li>
</ul>
</div>
<div style="display: flex; gap: 16px">
<oc-text-field-v1
name="day"
placeholder="DD"
type="integer"
min="1"
max="31"
maxlength="2"
style="flex-shrink: 0; width: 60px"
hide-counter
>Day
</oc-text-field-v1>
<oc-text-field-v1
name="month"
placeholder="MM"
type="integer"
min="1"
max="12"
maxlength="2"
style="flex-shrink: 0; width: 80px"
hide-counter
>Month
</oc-text-field-v1>
<oc-text-field-v1
name="year"
placeholder="YYYY"
type="integer"
min="1900"
max="2025"
maxlength="4"
style="flex-shrink: 0; width: 80px"
hide-counter
>Year
</oc-text-field-v1>
</div>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
`;
}
}
Demo inputmode
Story: components-text-field-variations--demo-input-mode · tags: components
A demo showcasing all possible inputmodes.
<h1 style="margin-bottom: 32px">Inputmode</h1>
<form>
<oc-text-field-v1 placeholder="Text" inputmode="text">Text </oc-text-field-v1>
<oc-text-field-v1 placeholder="Numeric" inputmode="numeric">Numeric </oc-text-field-v1>
<oc-text-field-v1 placeholder="Decimal" inputmode="decimal">Decimal </oc-text-field-v1>
<oc-text-field-v1 type="email" inputmode="email">E-Mail</oc-text-field-v1>
<oc-text-field-v1 type="tel" inputmode="tel">Phone number</oc-text-field-v1>
<oc-text-field-v1 type="url" inputmode="url">URL</oc-text-field-v1>
<oc-text-field-v1 type="search" inputmode="search">Search </oc-text-field-v1>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
Story source (TypeScript, verbatim from Storybook)
{
name: "Demo inputmode",
parameters: {
controls: {
disabled: true
}
},
argTypes: hideControlsBadge(Metadata),
render() {
return html`
<h1 style="margin-bottom: 32px">Inputmode</h1>
<form>
<oc-text-field-v1 placeholder="Text" inputmode="text">Text </oc-text-field-v1>
<oc-text-field-v1 placeholder="Numeric" inputmode="numeric">Numeric </oc-text-field-v1>
<oc-text-field-v1 placeholder="Decimal" inputmode="decimal">Decimal </oc-text-field-v1>
<oc-text-field-v1 type="email" inputmode="email">E-Mail</oc-text-field-v1>
<oc-text-field-v1 type="tel" inputmode="tel">Phone number</oc-text-field-v1>
<oc-text-field-v1 type="url" inputmode="url">URL</oc-text-field-v1>
<oc-text-field-v1 type="search" inputmode="search">Search </oc-text-field-v1>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 24px;
}
.buttons {
display: flex;
gap: 16px;
}
</style>
`;
}
}
Interaction tests (TextFieldV1.form.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Should Fire Keydown Event
Story: components-text-field-interaction-tests--should-fire-keydown-event · tags: play-fn
<form>
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit"></button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form>
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit"></button>
</form>
`;
},
async play({
canvasElement
}) {
canvasElement.getElementsByTagName("form").item(0)!.addEventListener("submit", ev => ev.preventDefault());
const textField = canvasElement.getElementsByTagName("oc-text-field-v1").item(0)!;
let fired = 0;
textField.addEventListener("keydown", () => fired += 1);
await userEvent.type(textField.shadowRoot!.querySelector("input")!, "{Enter}");
await expect(fired, "keydown should be fired one time").toBe(1);
}
}
Should Submit Like Native Inputs
Story: components-text-field-interaction-tests--should-submit-like-native-inputs · tags: play-fn
<form id="form1">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit" data-should-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<form id="form2">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<input type="submit" data-should-submit />
<button type="submit" data-should-not-submit></button>
</form>
<form id="form3">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
Should submit the form
</form>
<form id="form4">Should submit the form</form>
<oc-text-field-v1 name="name" form="form4">{Enter}</oc-text-field-v1>
<form id="form5">
<button type="submit" data-should-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<oc-text-field-v1 name="name" form="form5">{Enter}</oc-text-field-v1>
<form id="form6">
<input type="submit" data-should-submit />
<button type="submit" data-should-not-submit></button>
</form>
<oc-text-field-v1 name="name" form="form6">{Enter}</oc-text-field-v1>
<form id="form7">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit" disabled data-should-not-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<form id="form8">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<input type="submit" disabled data-should-not-submit />
<input type="submit" data-should-not-submit />
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html`
<form id="form1">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit" data-should-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<form id="form2">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<input type="submit" data-should-submit />
<button type="submit" data-should-not-submit></button>
</form>
<form id="form3">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
Should submit the form
</form>
<form id="form4">Should submit the form</form>
<oc-text-field-v1 name="name" form="form4">{Enter}</oc-text-field-v1>
<form id="form5">
<button type="submit" data-should-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<oc-text-field-v1 name="name" form="form5">{Enter}</oc-text-field-v1>
<form id="form6">
<input type="submit" data-should-submit />
<button type="submit" data-should-not-submit></button>
</form>
<oc-text-field-v1 name="name" form="form6">{Enter}</oc-text-field-v1>
<form id="form7">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<button type="submit" disabled data-should-not-submit></button>
<button type="submit" data-should-not-submit></button>
</form>
<form id="form8">
<oc-text-field-v1 name="name">{Enter}</oc-text-field-v1>
<input type="submit" disabled data-should-not-submit />
<input type="submit" data-should-not-submit />
</form>
`;
},
async play({
canvasElement
}) {
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
});
}));
const textFields = Array.from(canvasElement.querySelectorAll(`oc-text-field-v1[name="name"]`)) as (HTMLOcTextFieldV1Element & {
internals: ElementInternals;
})[];
const shadowInputs = textFields.map(oc => oc.shadowRoot!.querySelector("input")!);
for (let i = 0; i < textFields.length; i += 1) {
const textField = textFields[i];
// eslint-disable-next-line no-await-in-loop
await expect(textField.internals.form?.id, "text field should have correct associated form").toBe(`form${i + 1}`);
}
// eslint-disable-next-line no-restricted-syntax
for (const input of shadowInputs) {
// eslint-disable-next-line no-await-in-loop
await userEvent.type(input, "{Enter}");
}
await expect(submits.length, "number of submits").toBe(6);
await expect(submits[0].form).toBe("form1");
await expect(submits[0].submitter?.dataset.shouldSubmit).toBeDefined();
await expect(submits[1].form).toBe("form2");
await expect(submits[1].submitter?.dataset.shouldSubmit).toBeDefined();
await expect(submits[2].form).toBe("form3");
await expect(submits[2].submitter).toBe(null);
await expect(submits[3].form).toBe("form4");
await expect(submits[3].submitter).toBe(null);
await expect(submits[4].form).toBe("form5");
await expect(submits[4].submitter?.dataset.shouldSubmit).toBeDefined();
await expect(submits[5].form).toBe("form6");
await expect(submits[5].submitter?.dataset.shouldSubmit).toBeDefined();
}
}
Interaction tests (TextFieldV1.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Should Reset Values With Button
Story: components-text-field-interaction-tests--should-reset-values-with-button · tags: play-fn
<form>
<oc-text-field-v1 name="key1" value="value 1">label 1</oc-text-field-v1>
<oc-text-field-v1 name="key2" value="value 2">label 2</oc-text-field-v1>
<oc-text-field-v1 name="key3">label 3</oc-text-field-v1>
<button type="reset">Reset</button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html` <form>
<oc-text-field-v1 name="key1" value="value 1">label 1</oc-text-field-v1>
<oc-text-field-v1 name="key2" value="value 2">label 2</oc-text-field-v1>
<oc-text-field-v1 name="key3">label 3</oc-text-field-v1>
<button type="reset">Reset</button>
</form>`;
},
async play({
canvasElement
}) {
const [textField1, textField2, textField3] = Array.from(canvasElement.querySelectorAll("oc-text-field-v1"));
const [input1, input2, input3] = deepQuerySelectorAll(canvasElement, ".text-field__input");
await userEvent.type(input1, " foo");
await userEvent.type(input2, " bar");
await userEvent.type(input3, "foo bar");
const button = canvasElement.querySelector("button") as HTMLButtonElement;
await expect(textField1.value).toBe("value 1 foo");
await expect(textField2.value).toBe("value 2 bar");
await expect(textField3.value).toBe("foo bar");
await fireEvent.click(button);
await expect(textField1.value).toBe("value 1");
await expect(textField2.value).toBe("value 2");
await expect(textField3.value).toBe("");
}
}
Should Have Correct Form Data
Story: components-text-field-interaction-tests--should-have-correct-form-data · tags: play-fn
<form>
<oc-text-field-v1 name="key1">label 1</oc-text-field-v1>
<oc-text-field-v1 name="key2">label 2</oc-text-field-v1>
<oc-text-field-v1 name="key3">label 3</oc-text-field-v1>
<oc-text-field-v1 name="key4" value="I am disabled" disabled>label 4</oc-text-field-v1>
<button type="submit">submit</button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
render() {
return html` <form>
<oc-text-field-v1 name="key1">label 1</oc-text-field-v1>
<oc-text-field-v1 name="key2">label 2</oc-text-field-v1>
<oc-text-field-v1 name="key3">label 3</oc-text-field-v1>
<oc-text-field-v1 name="key4" value="I am disabled" disabled>label 4</oc-text-field-v1>
<button type="submit">submit</button>
</form>`;
},
async play({
canvasElement
}) {
const [input1, input2, input3] = deepQuerySelectorAll(canvasElement, ".text-field__input");
await userEvent.type(input1, "foo");
await userEvent.type(input2, "bar");
await userEvent.type(input3, "foo bar");
const form = canvasElement.querySelector("form") as HTMLFormElement;
let formData = {};
form.addEventListener("submit", ev => {
ev.preventDefault();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
formData = Object.fromEntries(new FormData(form).entries());
});
const button = canvasElement.querySelector("button") as HTMLButtonElement;
await fireEvent.click(button);
await expect(formData).toStrictEqual({
key1: "foo",
key2: "bar",
key3: "foo bar"
});
}
}
Handle Default Value
Story: components-text-field-interaction-tests--handle-default-value · tags: play-fn
<oc-text-field-v1>no defaults set</oc-text-field-v1>
<oc-text-field-v1 value="test">Test Label</oc-text-field-v1>
Story source (TypeScript, verbatim from Storybook)
{
parameters: {
chromatic: {
disableSnapshot: true
}
},
render() {
return html`
<oc-text-field-v1>no defaults set</oc-text-field-v1>
<oc-text-field-v1 value="test">Test Label</oc-text-field-v1>
`;
},
async play({
canvasElement
}) {
const [sutWithoutAttributes, sutWithAttributes] = Array.from(canvasElement.getElementsByTagName("oc-text-field-v1")) as (HTMLOcTextFieldV1Element & {
defaultChecked: boolean;
defaultValue: string;
})[];
// TEST sutWithoutAttributes
await expect(sutWithoutAttributes.value).toBe("");
await expect(sutWithoutAttributes.defaultValue).toBe("");
await expect(sutWithoutAttributes.hasAttribute("value")).toBeFalsy();
sutWithoutAttributes.value = "new on";
await expect(sutWithoutAttributes.value).toBe("new on");
await expect(sutWithoutAttributes.defaultValue).toBe("");
await expect(sutWithoutAttributes.hasAttribute("value")).toBeFalsy();
// TEST sutWithAttributes
await expect(sutWithAttributes.value).toBe("test");
await expect(sutWithAttributes.defaultValue).toBe("test");
await expect(sutWithAttributes.getAttribute("value")).toBe("test");
sutWithAttributes.value = "new test";
await expect(sutWithAttributes.value).toBe("new test");
await expect(sutWithAttributes.defaultValue).toBe("test");
await expect(sutWithAttributes.getAttribute("value")).toBe("test");
// TEST programmatic created
const sutCreated = document.createElement("oc-text-field-v1") as HTMLOcTextFieldV1Element & {
defaultValue: string;
};
sutCreated.innerText = "created";
sutCreated.value = "foo";
canvasElement.append(sutCreated);
await expect(sutCreated.value).toBe("foo");
await expect(sutCreated.defaultValue).toBe("");
await expect(sutCreated.hasAttribute("value")).toBeFalsy();
sutCreated.defaultValue = "foo value";
await expect(sutCreated.value).toBe("foo value");
await expect(sutCreated.defaultValue).toBe("foo value");
await expect(sutCreated.getAttribute("value")).toBe("foo value");
}
}
Interaction tests/Birthday field (TextFieldV1.birthdayfield.interactions.stories.ts)
Interaction test stories (automated tests, not usage patterns).
Birthday: valid inputs
Story: components-text-field-interaction-tests-birthday-field--birthday-valid-inputs · tags: play-fn
Story source (TypeScript, verbatim from Storybook)
{
name: "Birthday: valid inputs",
parameters: {
controls: {
disabled: true
}
},
render: renderBirthday,
async play({
canvasElement
}) {
const [day, month, year] = deepQuerySelectorAll(canvasElement, ".text-field__input") as HTMLInputElement[];
const microtask = () => new Promise(r => setTimeout(r, 0));
const clearAndType = async (el: HTMLInputElement, text: string) => {
el.focus();
await userEvent.keyboard("{Control>}a{/Control}{Backspace}");
await microtask();
await userEvent.type(el, text);
};
await clearAndType(day, "01");
await microtask();
await userEvent.tab();
await clearAndType(month, "01");
await microtask();
await userEvent.tab();
await clearAndType(year, "2000");
await microtask();
await expect(day.value).toBe("01");
await expect(month.value).toBe("01");
await expect(year.value).toBe("2000");
await clearAndType(day, "31");
await microtask();
await userEvent.tab();
await clearAndType(month, "12");
await microtask();
await userEvent.tab();
await clearAndType(year, "1999");
await microtask();
await expect(day.value).toBe("31");
await expect(month.value).toBe("12");
await expect(year.value).toBe("1999");
}
}
Birthday: non-digits
Story: components-text-field-interaction-tests-birthday-field--birthday-non-digits · tags: play-fn
Story source (TypeScript, verbatim from Storybook)
{
name: "Birthday: non-digits",
parameters: {
controls: {
disabled: true
}
},
render: renderBirthday,
async play({
canvasElement
}) {
const [day, month, year] = deepQuerySelectorAll(canvasElement, ".text-field__input") as HTMLInputElement[];
const microtask = () => new Promise(r => setTimeout(r, 0));
const clearAndType = async (el: HTMLInputElement, text: string) => {
el.focus();
await userEvent.keyboard("{Control>}a{/Control}{Backspace}");
await microtask();
await userEvent.type(el, text);
};
await clearAndType(day, "a");
await microtask();
await expect(day.value).toBe("");
await clearAndType(month, "1b");
await microtask();
await expect(month.value).toBe("1");
await clearAndType(year, "19c9");
await microtask();
await expect(year.value).toBe("199");
}
}