OTTODesign System

Code

Dropdown

Storybook group: Components · Sidebar path: Components/Dropdown · Extracted 28.09.2026

Version Tag Status API
v1 <oc-dropdown-v1> Stable, allowed for generation DropdownV1

Overview (v1)

Source: ./src/components/dropdown/v1/Overview.mdx

The dropdown component is used to provide a list of selectable options to the user. It can be configured with a label, a hint, an error state, to be required and to be disabled.

Default variation

Story Default:

<oc-dropdown-v1 label="Bitte auswählen" name="my-dropdown" empty-option options='[{"label": "Option 1", "value": "value1"}, {"label": "Option 2", "value": "value2"}, {"label": "Option 3", "value": "value3"}]'></oc-dropdown-v1>

Configuration

The dropdown component offers a variety of styling variants such as default and error. 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 dropdown 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 Dropdown 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.

Set options

There are two ways of setting options of the dropdown component:

  • Via the options attribute
  • Via the default slot
Set options via the options attribute

The following code shows the recommended way of setting options via the options attribute:

<oc-dropdown-v1
  name="my-dropdown"
  label="Label"
  options='[{ "label":"label1","value":"value1"}, { "label":"label2","value":"value2"}, { "label":"label3","value":"value3"}]'
></oc-dropdown-v1>
Setting options via the default slot

In case you are working with a template engine, the recommended way of setting options is via slots. Note that the options are not reactive when set via the default slot. This means that it is impossible to add, remove, or change the options once the dropdown is rendered. The following code shows the recommended way of setting options via slots:

<oc-dropdown-v1 name="my-dropdown" label="Label">
  <option value="value1">Option 1</option>
  <option value="value2">Option 2</option>
  <option value="value3">Option 3</option>
</oc-dropdown-v1>

Accessibility

The dropdown component comes with a set of built-in accessibility features to ensure a seamless experience for all users.

Use oc-aria-label

To make the dropdown recognizable for screen readers, use the label attribute to provide a clear and descriptive label. Use the aria-label attribute on the label to provide additional context if the visible label is not sufficient. See the general accessibility documentation for guidance on using oc-aria-label, including how it works with link and masked link behavior.

Keyboard navigation

The dropdown component supports keyboard navigation for accessibility purposes. The following table lists the keyboard shortcuts available for this component:

Shortcut Description
Tab Focuses the dropdown element
Space Open the dropdown if it is closed
Key Up / Down Navigate through the available options
Space / Enter Select option
Esc Close the dropdown if it is open

Configuration (v1)

Source: ./src/components/dropdown/v1/Configuration.mdx

Configure the dropdown 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-dropdown-v1 label="Bitte auswählen" name="my-dropdown" empty-option options='[{"label": "Option 1", "value": "value1"}, {"label": "Option 2", "value": "value2"}, {"label": "Option 3", "value": "value3"}]'></oc-dropdown-v1>

Interactive configurator (Storybook controls); every option is listed in the API section of this file.

API v1

Source: ./src/components/dropdown/v1/DropdownV1.API.g.mdx

API: <oc-dropdown-v1> (DropdownV1)

The Dropdown component is used to provide a list of selectable options to the user. It can be configured with a label, a hint, an error state, to be required and to be disabled.

Attributes / properties
Attribute Type Default Required Description
variant "default" | "error" "default" no Sets the main styling and behavior of the dropdown.
label string undefined no Sets the text content for the dropdown label to provide a brief description of the input field.
value string undefined no Sets the initial value of the dropdown to the provided value for form processing.
options { label: string; value: string; disabled?: boolean | undefined; }[] [] no Sets the selectable options by providing an array or JSON string. This is an alternative to the default slot. Read more in the section setting options.
hint string undefined no Provides additional information related to the dropdown below the input element.
validation-message string undefined no Provides a validation message related to the dropdown below the input element.

Implicitly sets the dropdown to the error state.
disabled boolean false no Disables the dropdown, 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.
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 dropdown. Set to one of the enum values to enable specific autocomplete.
empty-option boolean false no Provides an empty option at the top of the dropdown.
name string undefined no Sets the name tag to identify the dropdown when submitting a form.
oc-aria-label string undefined no Sets the ARIA label of the dropdown.
without-optional-label boolean false no Excludes the automatic "(optional)" label suffix even if the feature toggle is active.
Slots
Slot Required Description
default no Sets the selectable options by providing option elements. This is an alternative to the options attribute. Read more in the section setting options.
Events
Event Detail type Description
oc-property-change OcDropdownV1Events["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.

Variations (v1)

Source: ./src/components/dropdown/v1/Variations.mdx

Variations

Listed below are the most common variations of the dropdown 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-dropdown-variations--default · tags: components

The default configuration uses the default variant and an options attribute to pass the options.

Args: name=my-dropdown, label=Bitte auswählen, options=(see snippet)

<oc-dropdown-v1 label="Bitte auswählen" name="my-dropdown" empty-option options='[{"label": "Option 1", "value": "value1"}, {"label": "Option 2", "value": "value2"}, {"label": "Option 3", "value": "value3"}]'></oc-dropdown-v1>

With hint

Story: components-dropdown-variations--with-hint · tags: components

The defaultvariant using the hint attribute to provide additional information.

Args: name=my-dropdown, label=Label, hint=Hint, options=(see snippet)

<oc-dropdown-v1 label="Label" name="my-dropdown" empty-option hint="Hint" options='[{"label": "label1", "value": "value1"}, {"label": "label2", "value": "value2"}, {"label": "label3", "value": "value3"}]'></oc-dropdown-v1>

Error

Story: components-dropdown-variations--error · tags: components

The error variant using a red color to indicate an error state and the hint attribute to provide additional information.

Args: name=my-dropdown, label=Label, validation-message=Invalid input, hint=Hint, options=(see snippet)

<oc-dropdown-v1 label="Label" name="my-dropdown" empty-option validation-message="Invalid input" hint="Hint" options='[{"label": "label1", "value": "value1"}, {"label": "label2", "value": "value2"}, {"label": "label3", "value": "value3"}]'></oc-dropdown-v1>

Disabled

Story: components-dropdown-variations--disabled · tags: components

The default variant using the disabled attribute to disable the dropdown, preventing user interaction.

Args: name=my-dropdown, label=Label, disabled=true, hint=Hint, options=(see snippet)

<oc-dropdown-v1 label="Label" name="my-dropdown" empty-option disabled hint="Hint" options='[{"label": "label1", "value": "value1"}, {"label": "label2", "value": "value2"}, {"label": "label3", "value": "value3"}]'></oc-dropdown-v1>

Demo alternative way to pass options

Story: components-dropdown-variations--demo-alternative-way-to-pass-options · tags: components

A demonstration of how to pass options using the default slot instead of the options attribute.

Args: name=my-dropdown, label=Label, disabled=false, hint=Hint, defaultSlot=(see snippet)

<oc-dropdown-v1 label="Label" name="my-dropdown" empty-option hint="Hint">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
  <option value="option3">Option 3</option>
</oc-dropdown-v1>

Demo form

Story: components-dropdown-variations--demo-form · tags: components

A demonstration of how to use the dropdown in a form.

<h1>Demo Form</h1>
<form>
  <oc-dropdown-v1 name="food" hint="What do you like to eat?" label="Something to eat">
    <option value="food1">Pizza</option>
    <option value="food2">Sushi</option>
    <option value="food3">Steak</option>
  </oc-dropdown-v1>
  <br />
  <oc-dropdown-v1
    name="beverages"
    hint="What do you like to drink?"
    label="Something to drink (required)"
    required
  >
    <option value="drink1">Coffee</option>
    <option value="drink2">Tea</option>
    <option value="drink3">Beer</option>
  </oc-dropdown-v1>
  <br />
  <div style="display: flex; gap: 8px;">
    <oc-button-v1 type="reset" variant="secondary" id="clear" fit-content>Reset</oc-button-v1>
    <oc-button-v1 type="submit" id="submit" fit-content>Submit</oc-button-v1>
  </div>
</form>
<script>
  (() => {
    const form = document.querySelector("form");
    let formData = {};
    form.addEventListener("submit", (e) => {
      e.preventDefault();
      formData = Object.fromEntries(new FormData(form).entries());
      alert("Submitted form data: " + JSON.stringify(formData));
    });
  })();
</script>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo form",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html` <h1>Demo Form</h1>
      <form>
        <oc-dropdown-v1 name="food" hint="What do you like to eat?" label="Something to eat">
          <option value="food1">Pizza</option>
          <option value="food2">Sushi</option>
          <option value="food3">Steak</option>
        </oc-dropdown-v1>
        <br />
        <oc-dropdown-v1
          name="beverages"
          hint="What do you like to drink?"
          label="Something to drink (required)"
          required
        >
          <option value="drink1">Coffee</option>
          <option value="drink2">Tea</option>
          <option value="drink3">Beer</option>
        </oc-dropdown-v1>
        <br />
        <div style="display: flex; gap: 8px;">
          <oc-button-v1 type="reset" variant="secondary" id="clear" fit-content>Reset</oc-button-v1>
          <oc-button-v1 type="submit" id="submit" fit-content>Submit</oc-button-v1>
        </div>
      </form>
      <script>
        (() => {
          const form = document.querySelector("form");
          let formData = {};
          form.addEventListener("submit", (e) => {
            e.preventDefault();
            formData = Object.fromEntries(new FormData(form).entries());
            alert("Submitted form data: " + JSON.stringify(formData));
          });
        })();
      </script>`;
  }
}

Complex Demo Form

Story: components-dropdown-variations--complex-demo-form · tags: components

A demonstration of how to use the dropdown in a form.

<h1>Complex Demo Form</h1>
<p>
  This form contains two dropdowns. The first dropdown has options and optgroups defined via
  attributes and the slot, while the second dropdown uses only options defined via the slot.
</p>
<form>
  <oc-dropdown-v1
    name="food"
    hint="What do you like to eat?"
    label="Something to eat"
    options='[{"label":"Cake","value":"food4"},{"label":"Ice Cream","value":"food5"}]'
  >
    <option value="food1">Bread</option>
    <option value="food2">Fries</option>
    <option value="food3">Salad</option>
    <optgroup label="Italian">
      <option value="it-food4">Pasta</option>
      <option value="it-food5">Pizza</option>
    </optgroup>
    <optgroup label="Japanese">
      <option value="jp-food6">Sushi</option>
      <option value="jp-food7">Ramen</option>
    </optgroup>
    <optgroup label="American">
      <option value="am-food8">Steak</option>
      <option value="am-food9">Burger</option>
    </optgroup>
  </oc-dropdown-v1>
  <br />
  <oc-dropdown-v1
    name="beverages"
    hint="What do you like to drink?"
    label="Something to drink (required)"
    required
  >
    <option value="drink1">Coffee</option>
    <option value="drink2">Tea</option>
    <option value="drink3">Beer</option>
  </oc-dropdown-v1>
  <br />
  <div style="display: flex; gap: 8px;">
    <oc-button-v1 type="reset" variant="secondary" id="clear" fit-content>Reset</oc-button-v1>
    <oc-button-v1 type="submit" id="submit" fit-content>Submit</oc-button-v1>
  </div>
</form>
<script>
  (() => {
    const form = document.querySelector("form");
    let formData = {};
    form.addEventListener("submit", (e) => {
      e.preventDefault();
      formData = Object.fromEntries(new FormData(form).entries());
      alert("Submitted form data: " + JSON.stringify(formData));
    });
  })();
</script>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Complex Demo Form",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html` <h1>Complex Demo Form</h1>
      <p>
        This form contains two dropdowns. The first dropdown has options and optgroups defined via
        attributes and the slot, while the second dropdown uses only options defined via the slot.
      </p>
      <form>
        <oc-dropdown-v1
          name="food"
          hint="What do you like to eat?"
          label="Something to eat"
          options='[{"label":"Cake","value":"food4"},{"label":"Ice Cream","value":"food5"}]'
        >
          <option value="food1">Bread</option>
          <option value="food2">Fries</option>
          <option value="food3">Salad</option>
          <optgroup label="Italian">
            <option value="it-food4">Pasta</option>
            <option value="it-food5">Pizza</option>
          </optgroup>
          <optgroup label="Japanese">
            <option value="jp-food6">Sushi</option>
            <option value="jp-food7">Ramen</option>
          </optgroup>
          <optgroup label="American">
            <option value="am-food8">Steak</option>
            <option value="am-food9">Burger</option>
          </optgroup>
        </oc-dropdown-v1>
        <br />
        <oc-dropdown-v1
          name="beverages"
          hint="What do you like to drink?"
          label="Something to drink (required)"
          required
        >
          <option value="drink1">Coffee</option>
          <option value="drink2">Tea</option>
          <option value="drink3">Beer</option>
        </oc-dropdown-v1>
        <br />
        <div style="display: flex; gap: 8px;">
          <oc-button-v1 type="reset" variant="secondary" id="clear" fit-content>Reset</oc-button-v1>
          <oc-button-v1 type="submit" id="submit" fit-content>Submit</oc-button-v1>
        </div>
      </form>
      <script>
        (() => {
          const form = document.querySelector("form");
          let formData = {};
          form.addEventListener("submit", (e) => {
            e.preventDefault();
            formData = Object.fromEntries(new FormData(form).entries());
            alert("Submitted form data: " + JSON.stringify(formData));
          });
        })();
      </script>`;
  }
}

Interaction tests (DropdownV1.form.interactions.stories.ts)

Interaction test stories (automated tests, not usage patterns).

Should Fire Keydown Event

Story: components-dropdown-interaction-tests--should-fire-keydown-event · tags: play-fn

<form>
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
  <button type="submit"></button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form>
        <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
        <button type="submit"></button>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    canvasElement.getElementsByTagName("form").item(0)!.addEventListener("submit", ev => ev.preventDefault());
    const dropdown = canvasElement.getElementsByTagName("oc-dropdown-v1").item(0)!;
    let fired = 0;
    dropdown.addEventListener("keydown", () => fired += 1);
    await userEvent.type(dropdown.shadowRoot!.querySelector("select")!, "{Enter}");
    await expect(fired, "keydown should be fired one time").toBe(1);
  }
}

Should Submit Like Native Inputs

Story: components-dropdown-interaction-tests--should-submit-like-native-inputs · tags: play-fn

<form id="form1">
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
  <button type="submit" data-should-submit></button>
  <button type="submit" data-should-not-submit></button>
</form>

<form id="form2">
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
  <input type="submit" data-should-submit />
  <button type="submit" data-should-not-submit></button>
</form>

<form id="form3">
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
  Should submit the form
</form>

<form id="form4">Should submit the form</form>
<oc-dropdown-v1 name="name" form="form4">{Enter}</oc-dropdown-v1>

<form id="form5">
  <button type="submit" data-should-submit></button>
  <button type="submit" data-should-not-submit></button>
</form>
<oc-dropdown-v1 name="name" form="form5">{Enter}</oc-dropdown-v1>

<form id="form6">
  <input type="submit" data-should-submit />
  <button type="submit" data-should-not-submit></button>
</form>
<oc-dropdown-v1 name="name" form="form6">{Enter}</oc-dropdown-v1>

<form id="form7">
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
  <button type="submit" disabled data-should-not-submit></button>
  <button type="submit" data-should-not-submit></button>
</form>

<form id="form8">
  <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-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-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
        <button type="submit" data-should-submit></button>
        <button type="submit" data-should-not-submit></button>
      </form>

      <form id="form2">
        <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
        <input type="submit" data-should-submit />
        <button type="submit" data-should-not-submit></button>
      </form>

      <form id="form3">
        <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
        Should submit the form
      </form>

      <form id="form4">Should submit the form</form>
      <oc-dropdown-v1 name="name" form="form4">{Enter}</oc-dropdown-v1>

      <form id="form5">
        <button type="submit" data-should-submit></button>
        <button type="submit" data-should-not-submit></button>
      </form>
      <oc-dropdown-v1 name="name" form="form5">{Enter}</oc-dropdown-v1>

      <form id="form6">
        <input type="submit" data-should-submit />
        <button type="submit" data-should-not-submit></button>
      </form>
      <oc-dropdown-v1 name="name" form="form6">{Enter}</oc-dropdown-v1>

      <form id="form7">
        <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-v1>
        <button type="submit" disabled data-should-not-submit></button>
        <button type="submit" data-should-not-submit></button>
      </form>

      <form id="form8">
        <oc-dropdown-v1 name="name">{Enter}</oc-dropdown-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 dropdowns = Array.from(canvasElement.querySelectorAll(`oc-dropdown-v1[name="name"]`)) as (HTMLOcTextFieldV1Element & {
      internals: ElementInternals;
    })[];
    const shadowInputs = dropdowns.map(oc => oc.shadowRoot!.querySelector("select")!);
    for (let i = 0; i < dropdowns.length; i += 1) {
      const dropdown = dropdowns[i];
      // eslint-disable-next-line no-await-in-loop
      await expect(dropdown.internals.form?.id, "dropdown 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 (DropdownV1.interactions.stories.ts)

Interaction test stories (automated tests, not usage patterns).

Should Submit Selected Option

Story: components-dropdown-interaction-tests--should-submit-selected-option · tags: play-fn

<form>
  <oc-dropdown-v1 name="music" label="Favorite music">
    <option value="1">Rock</option>
    <option value="2">Electro</option>
    <option value="3">Hip-Hop</option>
  </oc-dropdown-v1>
  <button type="submit" style="margin-top: 20px;">submit</button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html` <form>
      <oc-dropdown-v1 name="music" label="Favorite music">
        <option value="1">Rock</option>
        <option value="2">Electro</option>
        <option value="3">Hip-Hop</option>
      </oc-dropdown-v1>
      <button type="submit" style="margin-top: 20px;">submit</button>
    </form>`;
  },
  async play({
    canvasElement
  }) {
    const selectElement = deepQuerySelector(canvasElement, "select")!;
    await userEvent.selectOptions(selectElement, "Electro");
    const form = canvasElement.getElementsByTagName("form").item(0)!;
    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;
    // https://github.com/testing-library/user-event/issues/1075
    await fireEvent.click(button);
    await expect(formData).toStrictEqual({
      music: "2"
    });
  }
}

Should Reset Selection

Story: components-dropdown-interaction-tests--should-reset-selection · tags: play-fn

<form>
  <oc-dropdown-v1 name="music" label="please select ...">
    <option value="1">Rock</option>
    <option value="2">Electro</option>
    <option value="3">Hip-Hop</option>
  </oc-dropdown-v1>
  <button type="reset" style="margin-top: 20px;">reset</button>
</form>
<script>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form>
        <oc-dropdown-v1 name="music" label="please select ...">
          <option value="1">Rock</option>
          <option value="2">Electro</option>
          <option value="3">Hip-Hop</option>
        </oc-dropdown-v1>
        <button type="reset" style="margin-top: 20px;">reset</button>
      </form>
      <script>`;
  },
  async play({
    canvasElement
  }) {
    const selectElement = deepQuerySelector(canvasElement, "select") as HTMLSelectElement;
    await expect(selectElement?.value).toBe("");
    await userEvent.selectOptions(selectElement, "Electro");
    await expect(selectElement?.value).toBe("2");
    const button = canvasElement.querySelector("button") as HTMLButtonElement;
    // Test in Firefox needs extra help here
    // https://github.com/testing-library/user-event/issues/1075
    await fireEvent.click(button);
    await userEvent.click(button);
    await expect(selectElement?.value).toBe("");
  }
}

Should Send Property Change Event

Story: components-dropdown-interaction-tests--should-send-property-change-event · tags: play-fn

<form>
  <oc-dropdown-v1 name="music" label="please select ...">
    <option value="1">Rock</option>
    <option value="2">Electro</option>
    <option value="3">Hip-Hop</option>
  </oc-dropdown-v1>
  <button type="reset" style="margin-top: 20px;">reset</button>
</form>
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <form>
        <oc-dropdown-v1 name="music" label="please select ...">
          <option value="1">Rock</option>
          <option value="2">Electro</option>
          <option value="3">Hip-Hop</option>
        </oc-dropdown-v1>
        <button type="reset" style="margin-top: 20px;">reset</button>
      </form>
    `;
  },
  async play({
    canvasElement
  }) {
    const selectElement = deepQuerySelector(canvasElement, "select") as HTMLSelectElement;
    const dropdownElement = document.querySelector("oc-dropdown-v1");
    let lastChangeEvent: PropertyChangeEvent | undefined;
    dropdownElement?.addEventListener("oc-property-change", e => {
      lastChangeEvent = e;
    });
    await expect(selectElement?.value).toBe("");
    await userEvent.selectOptions(selectElement, "Electro");
    await expect(selectElement?.value).toBe("2");
    await expect(lastChangeEvent?.detail.value).toEqual({
      previous: "",
      current: "2"
    });
  }
}

Should Handle Value And Option Modifications

Story: components-dropdown-interaction-tests--should-handle-value-and-option-modifications · tags: play-fn

<oc-dropdown-v1 label="please select ..." required> </oc-dropdown-v1>
<oc-dropdown-v1 label="please select ..." required> </oc-dropdown-v1>
Story source (TypeScript, verbatim from Storybook)
{
  parameters: {
    docs: {
      disable: true
    }
  },
  render() {
    return html`
      <oc-dropdown-v1 label="please select ..." required> </oc-dropdown-v1>
      <oc-dropdown-v1 label="please select ..." required> </oc-dropdown-v1>
    `;
  },
  async play({
    canvasElement
  }) {
    const [dropdown1, dropdown2] = Array.from(canvasElement.getElementsByTagName("oc-dropdown-v1"));

    // Initialize dropdowns
    dropdown1.setAttribute("options", JSON.stringify([{
      label: "Rock",
      value: "1"
    }, {
      label: "Electro",
      value: "2"
    }, {
      label: "Hip-Hop",
      value: "3"
    }]));
    await tick();
    dropdown1.setAttribute("value", "2");
    dropdown2.innerHTML = `
      <option value="1">Rock</option>
      <option value="2">Electro</option>
      <option value="3">Hip-Hop</option>
    `;
    await tick();
    dropdown2.value = "2";

    // register event listener
    let eventCounter = 0;
    const listener = (): void => {
      eventCounter += 1;
    };
    dropdown1.addEventListener("oc-property-change", listener);
    dropdown2.addEventListener("oc-property-change", listener);

    // initial checks
    await expect(dropdown1.value).toBe("2");
    await expect(dropdown2.value).toBe("2");
    await expect(eventCounter, "2 events have been fired till now").toBe(2);
    eventCounter = 0;

    // add option to dropdown1
    dropdown1.setAttribute("options", JSON.stringify([{
      label: "Rock",
      value: "1"
    }, {
      label: "Electro",
      value: "2"
    }, {
      label: "Hip-Hop",
      value: "3"
    }, {
      label: "Pop",
      value: "4"
    }]));

    // add option to dropdown2
    dropdown2.innerHTML = `
      <option value="1">Rock</option>
      <option value="2">Electro</option>
      <option value="3">Hip-Hop</option>
      <option value="4">Pop</option>
    `;
    await tick();
    await expect(dropdown1.value).toBe("2");
    await expect(dropdown2.value).toBe("2");
    await expect(eventCounter, "1 event for option change has been fired").toBe(1);
    eventCounter = 0;

    // remove selected option from dropdown1
    dropdown1.setAttribute("options", JSON.stringify([{
      label: "Rock",
      value: "1"
    }, {
      label: "Hip-Hop",
      value: "3"
    }, {
      label: "Pop",
      value: "4"
    }]));

    // remove selected option from dropdown2
    dropdown2.innerHTML = `
      <option value="1">Rock</option>
      <option value="3">Hip-Hop</option>
      <option value="4">Pop</option>
    `;
    await tick();
    await expect(dropdown1.shadowRoot!.querySelector("select")!.value).toBe("");
    await expect(dropdown2.shadowRoot!.querySelector("select")!.value).toBe("");

    // restore initial state
    dropdown1.setAttribute("options", JSON.stringify([{
      label: "Rock",
      value: "1"
    }, {
      label: "Electro",
      value: "2"
    }, {
      label: "Hip-Hop",
      value: "3"
    }]));
    dropdown1.setAttribute("value", "2");
    dropdown2.innerHTML = `
      <option value="1">Rock</option>
      <option value="2">Electro</option>
      <option value="3">Hip-Hop</option>
    `;
    dropdown2.value = "2";
    await expect(dropdown1.value).toBe("2");
    await expect(dropdown2.value).toBe("2");
    await expect(eventCounter, "2 events has been fired").toBe(2);
  }
}