OTTODesign System

Code

Text Field

Storybook group: Components · Sidebar path: Components/Text Field · Extracted 28.09.2026

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 Enter while focused on the text field triggers the submit action immediately (implicit submit) if an oc-button component or a normal HTML button with the type submit is 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: number

    Gets 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: number

    Gets 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");
  }
}