| Version | Tag | Status | API |
|---|---|---|---|
| v1 | <oc-dropdown-v1> |
Stable, allowed for generation | DropdownV1 |
Overview (v1)
Source: ./src/components/dropdown/v1/Overview.mdx
Dropdown
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
optionsattribute - Via the
defaultslot
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
Dropdown configuration
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
Dropdown v1 API
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);
}
}