Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,37 @@ describe("DropdownNew", () => {
expect(getByTestId("dropdown-chip-opt3")).toBeInTheDocument();
});

it("should label searchable input and describe it with selected item labels", () => {
const { getByRole, getByTestId } = renderDropdown({
multi: true,
label: "Selected options",
defaultValue: [
{ label: "Option 1", value: "opt1", index: 0 },
{ label: "Option 3", value: "opt3", index: 2 }
]
});

const input = getByRole("combobox", { name: "Selected options" });
const selectedItemsDescription = getByTestId("dropdown-selected-items-description");

expect(selectedItemsDescription).toHaveTextContent("Selected items Option 1, Option 3");
expect(input.getAttribute("aria-labelledby")).not.toContain(selectedItemsDescription.id);
expect(input).toHaveAttribute("aria-describedby", selectedItemsDescription.id);
expect(input).not.toHaveAttribute("aria-label");
});

it("should not use generated downshift ids as fallback aria labels", () => {
const { container } = renderDropdown({
multi: true,
defaultValue: [
{ label: "Option 1", value: "opt1", index: 0 },
{ label: "Option 3", value: "opt3", index: 2 }
]
});

expect(container.querySelector('[aria-label^="downshift-"]')).not.toBeInTheDocument();
});

it("should remove an item when its chip is deleted", () => {
const onChange = vi.fn();
const { getByPlaceholderText, getByText, getAllByRole } = renderDropdown({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type BaseItemData } from "../../../BaseItem";
import { Chips } from "../../../Chips";
import { Flex } from "@vibe/layout";
import { DialogContentContainer, Dialog } from "@vibe/dialog";
import HiddenText from "../../../HiddenText/HiddenText";
import useItemsOverflow from "../../../../hooks/useItemsOverflow/useItemsOverflow";
import styles from "./MultiSelectedValues.module.scss";
import cx from "classnames";
Expand All @@ -17,6 +18,7 @@ type MultiSelectedValuesProps<Item> = {
disabled?: boolean;
readOnly?: boolean;
minVisibleCount?: number;
selectedItemsDescriptionId?: string;
};

function MultiSelectedValues<Item extends BaseItemData<Record<string, unknown>>>({
Expand All @@ -25,7 +27,8 @@ function MultiSelectedValues<Item extends BaseItemData<Record<string, unknown>>>
renderInput,
disabled,
readOnly,
minVisibleCount = 0
minVisibleCount = 0,
selectedItemsDescriptionId
}: MultiSelectedValuesProps<Item>) {
const containerRef = useRef<HTMLDivElement>(null);
const deductedSpaceRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -100,6 +103,7 @@ function MultiSelectedValues<Item extends BaseItemData<Record<string, unknown>>>
if (!selectedItems?.length) return null;

const isSingleChip = selectedItems.length === 1;
const selectedItemsDescription = `Selected items ${selectedItems.map(item => item.label).join(", ")}`;

return (
<Flex
Expand All @@ -112,6 +116,13 @@ function MultiSelectedValues<Item extends BaseItemData<Record<string, unknown>>>
[styles.measuring]: !hasMeasured
})}
>
{selectedItemsDescriptionId && (
<HiddenText
id={selectedItemsDescriptionId}
text={selectedItemsDescription}
data-testid="dropdown-selected-items-description"
/>
)}
{chipElements}

<Flex gap="xs" className={styles.inputAndCounterWrapper}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import React, { useRef } from "react";
import cx from "classnames";
import { BaseInput } from "@vibe/base";
import styles from "./Trigger.module.scss";
import { Text } from "@vibe/typography";
import { useDropdownContext } from "../../context/DropdownContext";
import { type BaseItemData } from "../../../BaseItem";
import { Text } from "@vibe/typography";
import styles from "./Trigger.module.scss";

const DropdownInput = ({ inputSize, fullWidth }: { inputSize?: "small" | "medium" | "large"; fullWidth?: boolean }) => {
const DropdownInput = ({
inputSize,
fullWidth,
selectedItemsDescriptionId
}: {
inputSize?: "small" | "medium" | "large";
fullWidth?: boolean;
selectedItemsDescriptionId?: string;
}) => {
const {
inputValue,
autoFocus,
Expand All @@ -17,6 +25,7 @@ const DropdownInput = ({ inputSize, fullWidth }: { inputSize?: "small" | "medium
selectedItem,
selectedItems = [],
inputAriaLabel,
"aria-label": ariaLabel,
searchable,
size,
label,
Expand All @@ -29,14 +38,16 @@ const DropdownInput = ({ inputSize, fullWidth }: { inputSize?: "small" | "medium
const inputRef = useRef<HTMLInputElement>(null);
const hasSelection = multi ? selectedItems.length > 0 : !!selectedItem;
const multipleSelectionDropdownProps = getDropdownProps ? getDropdownProps({ preventKeyAction: isOpen }) : {};
const inputLabel = inputAriaLabel || (label ? undefined : ariaLabel);

return (
<>
{searchable ? (
<BaseInput
{...getInputProps({
"aria-labelledby": label ? getLabelProps().id : undefined,
"aria-label": inputAriaLabel || (label ? undefined : getLabelProps()?.id),
"aria-label": label ? undefined : inputLabel,
"aria-describedby": selectedItemsDescriptionId,
placeholder: hasSelection ? "" : placeholder,
ref: inputRef,
...multipleSelectionDropdownProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const MultiSelectTrigger = () => {
"aria-label": ariaLabel,
minVisibleCount
} = useDropdownContext<BaseItemData>();
const selectedItemsDescriptionId =
searchable && selectedItems.length > 0 ? `${getLabelProps().id}-selected-items` : undefined;

return (
<Flex justify="space-between" align="center">
Expand All @@ -35,7 +37,7 @@ const MultiSelectTrigger = () => {
? getToggleButtonProps({
"aria-haspopup": "dialog",
"aria-labelledby": label ? getLabelProps().id : undefined,
"aria-label": ariaLabel || (label ? undefined : getLabelProps()?.id),
"aria-label": label ? undefined : ariaLabel,
"aria-disabled": disabled ? "true" : undefined,
"aria-invalid": error ? "true" : undefined,
"aria-readonly": readOnly ? "true" : undefined
Expand All @@ -52,8 +54,19 @@ const MultiSelectTrigger = () => {
onRemove={item => {
contextOnOptionRemove?.(item);
}}
renderInput={searchable ? () => <DropdownInput inputSize="small" fullWidth /> : undefined}
renderInput={
searchable
? () => (
<DropdownInput
inputSize="small"
fullWidth
selectedItemsDescriptionId={selectedItemsDescriptionId}
/>
)
: undefined
}
minVisibleCount={minVisibleCount}
selectedItemsDescriptionId={selectedItemsDescriptionId}
/>
) : (
<Flex gap="xs" wrap>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const SingleSelectTrigger = () => {
? getToggleButtonProps({
"aria-haspopup": "dialog",
"aria-labelledby": label ? getLabelProps().id : undefined,
"aria-label": ariaLabel || (label ? undefined : getLabelProps()?.id),
"aria-label": label ? undefined : ariaLabel,
"aria-disabled": disabled ? "true" : undefined,
"aria-invalid": error ? "true" : undefined,
"aria-readonly": readOnly ? "true" : undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,19 @@ export const MultiSelect: Story = {
() => [
{
value: "1",
label: "Chip one"
label: "Bulbasaur"
},
{
value: "2",
label: "Chip two"
label: "Charmander"
},
{
value: "3",
label: "Chip three"
label: "Squirtle"
},
{
value: "4",
label: "Chip four"
label: "Pikachu"
}
],
[]
Expand All @@ -200,10 +200,12 @@ export const MultiSelect: Story = {
<Text>Single line with hidden options</Text>
<div style={{ width: "350px", marginBottom: "50px" }}>
<Dropdown
label="Pokémon"
placeholder="Single line multi state"
defaultValue={[options[0], options[1], options[2]]}
options={options}
multi
searchable
clearAriaLabel="Clear"
/>
</div>
Expand Down
Loading