diff --git a/playwright/components/Dropdown/Dropdown.test.ts b/playwright/components/Dropdown/Dropdown.test.ts new file mode 100644 index 00000000..071bc8ad --- /dev/null +++ b/playwright/components/Dropdown/Dropdown.test.ts @@ -0,0 +1,56 @@ +/* + * + * Copyright 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ +import { expect, test } from "@playwright/test"; + +test.describe("Dropdown", () => { + test("should be able to select a value", async ({ page }) => { + await page.goto(`iframe.html?viewMode=story&id=dropdown--default`, { + waitUntil: "networkidle", + }); + + await page.getByRole("combobox").click(); + await expect(page).toHaveScreenshot({ fullPage: true }); + + await page.getByRole("option", { name: "Option 2" }).click(); + await expect(page.getByRole("combobox")).toHaveText("Option 2"); + + await page.getByRole("combobox").click(); + await expect(page).toHaveScreenshot({ fullPage: true }); + }); + + test("should to use keyboard shortcut", async ({ page }) => { + await page.goto(`iframe.html?viewMode=story&id=dropdown--default`, { + waitUntil: "networkidle", + }); + + await page.getByRole("combobox").focus(); + await page.keyboard.press("ArrowDown"); + + await expect(page).toHaveScreenshot({ fullPage: true }); + + await page.keyboard.press("End"); + await expect(page.getByRole("option", { name: "Option 3" })).toBeFocused(); + + await page.keyboard.press("Home"); + await expect(page.getByRole("option", { name: "Option 1" })).toBeFocused(); + + await page.keyboard.press("Enter"); + await expect(page.getByRole("combobox")).toHaveText("Option 1"); + await expect(page).toHaveScreenshot({ fullPage: true }); + }); +}); diff --git a/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-1-chromium-linux.png b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-1-chromium-linux.png new file mode 100644 index 00000000..5bcb9b8f Binary files /dev/null and b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-1-chromium-linux.png differ diff --git a/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-2-chromium-linux.png b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-2-chromium-linux.png new file mode 100644 index 00000000..708e3177 Binary files /dev/null and b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-be-able-to-select-a-value-2-chromium-linux.png differ diff --git a/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-1-chromium-linux.png b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-1-chromium-linux.png new file mode 100644 index 00000000..77aeee55 Binary files /dev/null and b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-1-chromium-linux.png differ diff --git a/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-2-chromium-linux.png b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-2-chromium-linux.png new file mode 100644 index 00000000..1212904b Binary files /dev/null and b/playwright/components/Dropdown/Dropdown.test.ts-snapshots/Dropdown-should-to-use-keyboard-shortcut-2-chromium-linux.png differ diff --git a/playwright/visual.test.ts-snapshots/Dropdown-Default-1-chromium-linux.png b/playwright/visual.test.ts-snapshots/Dropdown-Default-1-chromium-linux.png new file mode 100644 index 00000000..b36c524f Binary files /dev/null and b/playwright/visual.test.ts-snapshots/Dropdown-Default-1-chromium-linux.png differ diff --git a/playwright/visual.test.ts-snapshots/Dropdown-With-Default-Value-1-chromium-linux.png b/playwright/visual.test.ts-snapshots/Dropdown-With-Default-Value-1-chromium-linux.png new file mode 100644 index 00000000..4b041914 Binary files /dev/null and b/playwright/visual.test.ts-snapshots/Dropdown-With-Default-Value-1-chromium-linux.png differ diff --git a/playwright/visual.test.ts-snapshots/Dropdown-With-Error-1-chromium-linux.png b/playwright/visual.test.ts-snapshots/Dropdown-With-Error-1-chromium-linux.png new file mode 100644 index 00000000..fb6b7bd9 Binary files /dev/null and b/playwright/visual.test.ts-snapshots/Dropdown-With-Error-1-chromium-linux.png differ diff --git a/playwright/visual.test.ts-snapshots/Dropdown-With-Help-Label-1-chromium-linux.png b/playwright/visual.test.ts-snapshots/Dropdown-With-Help-Label-1-chromium-linux.png new file mode 100644 index 00000000..208a4707 Binary files /dev/null and b/playwright/visual.test.ts-snapshots/Dropdown-With-Help-Label-1-chromium-linux.png differ diff --git a/src/components/Dropdown/Dropdown.module.css b/src/components/Dropdown/Dropdown.module.css new file mode 100644 index 00000000..8d249e1b --- /dev/null +++ b/src/components/Dropdown/Dropdown.module.css @@ -0,0 +1,147 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.container { + display: flex; + flex-direction: column; + + label { + font: var(--cpd-font-body-md-medium); + margin-block-end: var(--cpd-space-1x); + } + + button { + inline-size: 100%; + border: 1px solid var(--cpd-color-border-interactive-primary); + background: var(--cpd-color-bg-canvas-default); + border-radius: 0.5rem; + padding: var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) + var(--cpd-space-4x); + box-sizing: border-box; + color: var(--cpd-color-text-primary); + font: var(--cpd-font-body-md-regular); + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--cpd-space-4x); + + svg { + transition: transform 0.1s linear; + } + } + + /** + * When the dropdown is open, rotate the arrow icon + */ + button[aria-expanded="true"] { + svg { + transform: rotate(180deg); + } + } + + button.placeholder { + color: var(--cpd-color-text-placeholder); + } + + .border { + display: none; + border-inline-start: 1px solid var(--cpd-color-border-interactive-secondary); + border-inline-end: 1px solid var(--cpd-color-border-interactive-secondary); + block-size: var(--cpd-space-1x); + margin-block-start: calc(var(--cpd-space-1x) * -1); + box-sizing: border-box; + } + + .content { + display: none; + position: relative; + + ul { + /** + * To make the component going over the other elements + */ + position: absolute; + display: block; + inline-size: 100%; + background: var(--cpd-color-bg-canvas-default); + border: 1px solid var(--cpd-color-border-interactive-secondary); + border-block-start: 0; + border-end-start-radius: var(--cpd-space-4x); + border-end-end-radius: var(--cpd-space-4x); + box-sizing: border-box; + box-shadow: 0 4px 24px 0 rgb(27 29 34 / 10%); + margin: 0; + padding: 0; + padding-block-end: var(--cpd-space-4x); + cursor: pointer; + + li { + list-style: none; + font: var(--cpd-font-body-md-medium); + padding: var(--cpd-space-3x) var(--cpd-space-4x); + border-block-end: 1px solid var(--cpd-color-gray-300); + color: var(--cpd-color-text-secondary); + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--cpd-space-4x); + + @media (hover) { + &:hover { + background: var(--cpd-color-gray-200); + } + } + + &[aria-selected="true"] { + color: var(--cpd-color-text-primary); + background: var(--cpd-color-gray-300); + } + } + } + } + + .open { + display: block; + } + + .help { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + } + + .error { + font: var(--cpd-font-body-sm-medium); + color: var(--cpd-color-text-critical-primary); + display: flex; + gap: var(--cpd-space-2x); + } + + .error, + .help { + margin-block-start: var(--cpd-space-2x); + } + + &[aria-invalid="true"] { + label { + color: var(--cpd-color-text-critical-primary); + } + + button { + border-color: var(--cpd-color-text-critical-primary); + } + } +} diff --git a/src/components/Dropdown/Dropdown.stories.tsx b/src/components/Dropdown/Dropdown.stories.tsx new file mode 100644 index 00000000..8b0ca71e --- /dev/null +++ b/src/components/Dropdown/Dropdown.stories.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { Dropdown } from "./Dropdown"; +import { fn } from "@storybook/test"; +import { Meta } from "@storybook/react"; +import { ComponentProps } from "react"; + +export default { + title: "Dropdown", + component: Dropdown, + tags: ["autodocs"], + parameters: { + controls: { + include: ["defaultValue", "placeholder", "error"], + }, + }, + argTypes: { + label: { + type: "string", + }, + error: { + type: "string", + }, + placeholder: { + type: "string", + }, + values: { + type: "string", + }, + }, + args: { + label: "Label", + placeholder: "Select an option", + onValueChange: fn(), + values: [ + ["Option1", "Option 1"], + ["Option2", "Option 2"], + ["Option3", "Option 3"], + ], + }, +} satisfies Meta>; + +export const Default = { + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=799-5732&t=g2Ex9sbzgku1nTIN-4", + }, + }, +}; +export const WithHelpLabel = { + args: { + helpLabel: "Optional help text.", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=799-345&t=g2Ex9sbzgku1nTIN-4", + }, + }, +}; +export const WithError = { + args: { + error: "Select an option", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=799-370&t=g2Ex9sbzgku1nTIN-4", + }, + }, +}; +export const WithDefaultValue = { + args: { + defaultValue: "Option2", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=799-381&t=g2Ex9sbzgku1nTIN-4", + }, + }, +}; diff --git a/src/components/Dropdown/Dropdown.test.tsx b/src/components/Dropdown/Dropdown.test.tsx new file mode 100644 index 00000000..6005a6a0 --- /dev/null +++ b/src/components/Dropdown/Dropdown.test.tsx @@ -0,0 +1,102 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { describe, expect, it } from "vitest"; +import { composeStories } from "@storybook/react"; +import * as stories from "./Dropdown.stories"; +import { act, render, waitFor } from "@testing-library/react"; +import React from "react"; +import { userEvent } from "@storybook/test"; + +const { Default, WithHelpLabel, WithError, WithDefaultValue } = + composeStories(stories); + +describe("Dropdown", () => { + it("renders a Default dropdown", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders a dropdown with a help label", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders a dropdown with an error ", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders a dropdown with a default value ", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("can be opened", async () => { + const { getByRole, container } = render(); + await act(async () => { + await userEvent.click(getByRole("combobox")); + }); + expect(container).toMatchSnapshot(); + }); + it("can select a value", async () => { + const { getByRole, container } = render(); + await act(async () => { + await userEvent.click(getByRole("combobox")); + }); + + await waitFor(() => + expect(getByRole("option", { name: "Option 2" })).toBeVisible(), + ); + + await act(async () => { + await userEvent.click(getByRole("option", { name: "Option 2" })); + }); + + expect(getByRole("combobox")).toHaveTextContent("Option 2"); + + await act(async () => { + await userEvent.click(getByRole("combobox")); + }); + + await waitFor(() => + expect(getByRole("option", { name: "Option 2" })).toHaveAttribute( + "aria-selected", + "true", + ), + ); + + // Option 2 should be selected + expect(container).toMatchSnapshot(); + }); + it("can use keyboard shortcuts", async () => { + const { getByRole } = render(); + + await act(async () => userEvent.type(getByRole("combobox"), "{arrowdown}")); + await waitFor(() => + expect(getByRole("combobox")).toHaveAttribute("aria-expanded", "true"), + ); + + await act(async () => userEvent.keyboard("{arrowdown}")); + expect(getByRole("option", { name: "Option 1" })).toHaveFocus(); + + await act(async () => userEvent.keyboard("{End}")); + expect(getByRole("option", { name: "Option 3" })).toHaveFocus(); + + await act(async () => userEvent.keyboard("{Enter}")); + + await waitFor(() => { + expect(getByRole("combobox")).toHaveTextContent("Option 3"); + expect(getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); + }); + }); +}); diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx new file mode 100644 index 00000000..73679f57 --- /dev/null +++ b/src/components/Dropdown/Dropdown.tsx @@ -0,0 +1,400 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import ChevronDown from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down"; +import Check from "@vector-im/compound-design-tokens/assets/web/icons/check"; +import Error from "@vector-im/compound-design-tokens/assets/web/icons/error"; + +import React, { + Dispatch, + forwardRef, + HTMLProps, + memo, + RefObject, + SetStateAction, + useCallback, + useEffect, + useRef, + useState, + KeyboardEvent, +} from "react"; + +import classNames from "classnames"; + +import styles from "./Dropdown.module.css"; +import { useId } from "@floating-ui/react"; + +type DropdownProps = { + /** + * The CSS class name. + */ + className?: string; + /** + * The default value of the dropdown. + */ + defaultValue?: string; + /** + * The values of the dropdown. + * [value, text] + */ + values: [string, string][]; + /** + * The placeholder text. + */ + placeholder: string; + /** + * The label to display at the top of the dropdown + */ + label: string; + /** + * The help label to display at the bottom of the dropdown + */ + helpLabel?: string; + /** + * Callback for when the value changes. + * @param value + */ + onValueChange?: (value: string) => void; + /** + * The error message to display. + */ + error?: string; +}; + +/** + * The dropdown content. + */ +export const Dropdown = forwardRef( + function Dropdown( + { + className, + label, + placeholder, + helpLabel, + onValueChange, + error, + defaultValue, + values, + ...props + }, + ref, + ) { + const [state, setState] = useInitialState( + values, + placeholder, + defaultValue, + ); + const [open, setOpen, dropdownRef] = useOpen(); + const { listRef, onComboboxKeyDown, onOptionKeyDown } = useKeyboardShortcut( + open, + setOpen, + setState, + ); + + const buttonRef = useRef(null); + useEffect(() => { + // Focus the button when the value is set + // Test if the value is undefined to avoid focusing on the first render + if (state.value !== undefined) { + buttonRef.current?.focus(); + } + }, [state]); + + const hasPlaceholder = state.text === placeholder; + const buttonClasses = classNames({ + [styles.placeholder]: hasPlaceholder, + }); + const borderClasses = classNames(styles.border, { + [styles.open]: open, + }); + const contentClasses = classNames(styles.content, { + [styles.open]: open, + }); + + /** + * Ids for accessibility. + */ + const labelId = useId(); + const contentId = useId(); + + return ( +
+ + +
+
+
    + {values.map(([value, text]) => ( + { + setOpen(false); + setState({ value, text }); + onValueChange?.(value); + }} + onKeyDown={(e) => onOptionKeyDown(e, value, text)} + > + {text} + + ))} +
+
+ {!error && helpLabel && ( + {helpLabel} + )} + {error && ( + + + {error} + + )} +
+ ); + }, +); + +type DropdownItemProps = HTMLProps & { + /** + * Whether the dropdown item is selected. + */ + isSelected: boolean; + /** + * Whether the dropdown item is displayed. + */ + isDisplayed: boolean; + /** + * The text to display in the dropdown item. + */ + children: string; +}; + +/** + * A dropdown item component. + */ +const DropdownItem = memo(function DropdownItem({ + children, + isSelected, + isDisplayed, + ...props +}: DropdownItemProps) { + const ref = useRef(null); + + // Focus the item if the dropdown is open and the item is already selected + useEffect(() => { + if (isSelected && isDisplayed) { + ref.current?.focus(); + } + }, [isSelected, isDisplayed]); + + return ( +
  • + {children} {isSelected && } +
  • + ); +}); + +/** + * A hook to manage the open state of the dropdown. + */ +function useOpen(): [ + boolean, + Dispatch>, + RefObject, +] { + const [open, setOpen] = useState(false); + const ref = useRef(null); + + // If the user clicks outside the dropdown, close it + useEffect(() => { + const closeIfOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + + document.addEventListener("click", closeIfOutside); + return () => document.removeEventListener("click", closeIfOutside); + }, [setOpen]); + + return [open, setOpen, ref]; +} + +/** + * A hook to manage the initial state of the dropdown. + * @param values - The values of the dropdown. + * @param placeholder - The placeholder text. + * @param defaultValue - The default value of the dropdown. + */ +function useInitialState( + values: [string, string][], + placeholder: string, + defaultValue?: string, +) { + return useState(() => { + const defaultTuple = { + value: undefined, + text: placeholder, + }; + if (!defaultValue) return defaultTuple; + + const foundTuple = values.find(([value]) => value === defaultValue); + return foundTuple + ? { value: foundTuple[0], text: foundTuple[1] } + : defaultTuple; + }); +} + +/** + * A hook to manage the keyboard shortcuts of the dropdown. + * @param open - the dropdown open state. + * @param setOpen - the dropdown open state setter. + * @param setValue - set the selected value and text + */ +function useKeyboardShortcut( + open: boolean, + setOpen: Dispatch>, + setValue: ({ text, value }: { text: string; value: string }) => void, +) { + const listRef = useRef(null); + const onComboboxKeyDown = useCallback( + ({ key }: KeyboardEvent) => { + switch (key) { + // Enter and Space already managed because it's a button + case "Escape": + setOpen(false); + break; + case "ArrowDown": + setOpen(true); + // If open, focus the first element + if (open) { + (listRef.current?.firstElementChild as HTMLElement)?.focus(); + } + break; + case "ArrowUp": + setOpen(true); + break; + case "Home": { + setOpen(true); + // Wait for the dropdown to be opened + Promise.resolve().then(() => { + (listRef.current?.firstElementChild as HTMLElement)?.focus(); + }); + break; + } + case "End": { + setOpen(true); + // Wait for the dropdown to be opened + Promise.resolve().then(() => { + (listRef.current?.lastElementChild as HTMLElement)?.focus(); + }); + break; + } + } + }, + [listRef, open, setOpen], + ); + + const onOptionKeyDown = useCallback( + (evt: KeyboardEvent, value: string, text: string) => { + const { key, altKey } = evt; + evt.stopPropagation(); + evt.preventDefault(); + + switch (key) { + case "Enter": + case " ": { + setValue({ text, value }); + setOpen(false); + break; + } + case "Tab": + case "Escape": + setOpen(false); + break; + case "ArrowDown": { + const currentFocus = document.activeElement; + if (listRef.current?.contains(currentFocus) && currentFocus) { + (currentFocus.nextElementSibling as HTMLElement)?.focus(); + } + break; + } + case "ArrowUp": { + if (altKey) { + setValue({ text, value }); + setOpen(false); + } else { + const currentFocus = document.activeElement; + if (listRef.current?.contains(currentFocus) && currentFocus) { + (currentFocus.previousElementSibling as HTMLElement)?.focus(); + } + } + break; + } + case "Home": { + (listRef.current?.firstElementChild as HTMLElement)?.focus(); + break; + } + case "End": { + (listRef.current?.lastElementChild as HTMLElement)?.focus(); + break; + } + } + }, + [listRef, setValue, setOpen], + ); + + return { listRef, onComboboxKeyDown, onOptionKeyDown }; +} diff --git a/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap b/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap new file mode 100644 index 00000000..95dbd697 --- /dev/null +++ b/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap @@ -0,0 +1,482 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Dropdown > can be opened 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + +
    • +
    • + Option 3 + +
    • +
    +
    +
    +
    +`; + +exports[`Dropdown > can select a value 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + + + + +
    • +
    • + Option 3 + +
    • +
    +
    +
    +
    +`; + +exports[`Dropdown > renders a Default dropdown 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + +
    • +
    • + Option 3 + +
    • +
    +
    +
    +
    +`; + +exports[`Dropdown > renders a dropdown with a default value 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + + + + +
    • +
    • + Option 3 + +
    • +
    +
    +
    +
    +`; + +exports[`Dropdown > renders a dropdown with a help label 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + +
    • +
    • + Option 3 + +
    • +
    +
    + + Optional help text. + +
    +
    +`; + +exports[`Dropdown > renders a dropdown with an error 1`] = ` +
    +
    + + +
    +
    +
      +
    • + Option 1 + +
    • +
    • + Option 2 + +
    • +
    • + Option 3 + +
    • +
    +
    + + + + + Select an option + +
    +
    +`; diff --git a/src/components/Dropdown/index.ts b/src/components/Dropdown/index.ts new file mode 100644 index 00000000..af480fe8 --- /dev/null +++ b/src/components/Dropdown/index.ts @@ -0,0 +1,17 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export { Dropdown } from "./Dropdown"; diff --git a/src/index.ts b/src/index.ts index f83ac988..427a085e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ export { Tooltip } from "./components/Tooltip/Tooltip"; export { TooltipProvider } from "./components/Tooltip/TooltipProvider"; export { ReleaseAnnouncement } from "./components/ReleaseAnnouncement"; export { Toast } from "./components/Toast/Toast"; +export { Dropdown } from "./components//Dropdown"; export { TextControl,