Skip to content

Commit

Permalink
feat: add MultiSelect variations
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Aug 19, 2024
1 parent e22f884 commit b716ae7
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 40 deletions.
12 changes: 12 additions & 0 deletions HACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ Then in `react-components` run:
dotrun unlink-package
```

## Linking from Vite

When using yarn link from a project using Vite you may need to temporarily set the following
option in your vite.config.ts:

```
resolve: {
dedupe: ["react", "react-dom", "formik"],
preserveSymlinks: true,
},
```

If you do not wish do use dotrun then replace dotrun in the command above. Note that you must use dotrun or yarn on one project you must use the same command on the other project so that they both link the same node modules.

## Developing integration tests with cypress
Expand Down
2 changes: 1 addition & 1 deletion src/components/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export type Props = {
/**
* Optional class(es) to pass to the label component.
*/
labelClassName?: string;
labelClassName?: string | null;
/**
* Whether the label should show before the input.
*/
Expand Down
41 changes: 40 additions & 1 deletion src/components/MultiSelect/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,29 @@ it("can have some options preselected", async () => {

it("can select options from the dropdown", async () => {
const onItemsUpdate = jest.fn();
render(<MultiSelect items={items} onItemsUpdate={onItemsUpdate} />);
const onSelectItem = jest.fn();
render(
<MultiSelect
items={items}
onItemsUpdate={onItemsUpdate}
onSelectItem={onSelectItem}
/>
);
await userEvent.click(screen.getByRole("combobox"));
await userEvent.click(screen.getByLabelText(items[0].label));
await waitFor(() => expect(onItemsUpdate).toHaveBeenCalledWith([items[0]]));
await waitFor(() => expect(onSelectItem).toHaveBeenCalledWith(items[0]));
});

it("can remove options that have been selected", async () => {
const onItemsUpdate = jest.fn();
const onDeselectItem = jest.fn();
render(
<MultiSelect
items={items}
selectedItems={items}
onItemsUpdate={onItemsUpdate}
onDeselectItem={onDeselectItem}
/>
);
await userEvent.click(screen.getByRole("combobox"));
Expand All @@ -72,6 +82,7 @@ it("can remove options that have been selected", async () => {
within(screen.getByRole("listbox")).getByLabelText(items[0].label)
);
expect(onItemsUpdate).toHaveBeenCalledWith(items.slice(1));
expect(onDeselectItem).toHaveBeenCalledWith(items[0]);
});

it("can filter option list", async () => {
Expand Down Expand Up @@ -99,6 +110,20 @@ it("can display a custom dropdown header and footer", async () => {
).toBeInTheDocument();
});

it("can not display the footer", async () => {
render(
<MultiSelect
dropdownFooter={<button>custom footer button</button>}
items={items}
showDropdownFooter={false}
/>
);
await userEvent.click(screen.getByRole("combobox"));
expect(
screen.queryByRole("button", { name: "custom footer button" })
).not.toBeInTheDocument();
});

it("selects all items and clears selection when respective buttons are clicked", async () => {
const onItemsUpdate = jest.fn();
render(
Expand Down Expand Up @@ -127,6 +152,20 @@ it("updates text in the input field if something is selected", async () => {
expect(screen.getByRole("combobox")).toHaveTextContent(items[0].label);
});

it("can display the placeholder when items are selected", async () => {
const placeholder = "Select a few items";
render(
<MultiSelect
items={items}
selectedItems={[items[0]]}
variant="condensed"
listSelected={false}
placeholder={placeholder}
/>
);
expect(screen.getByRole("combobox")).toHaveTextContent(placeholder);
});

it("can have one or more sections with titles", async () => {
const itemsWithGroup = [
{ label: "item one", value: 1, group: "Group 1" },
Expand Down
97 changes: 59 additions & 38 deletions src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export type MultiSelectProps = {
selectedItems?: MultiSelectItem[];
help?: string;
label?: string | null;
listSelected?: boolean;
onDeselectItem?: (item: MultiSelectItem) => void;
onItemsUpdate?: (items: MultiSelectItem[]) => void;
onSelectItem?: (item: MultiSelectItem) => void;
placeholder?: string;
required?: boolean;
items: MultiSelectItem[];
disabledItems?: MultiSelectItem[];
renderItem?: (item: MultiSelectItem) => ReactNode;
dropdownHeader?: ReactNode;
dropdownFooter?: ReactNode;
showDropdownFooter?: boolean;
variant?: "condensed" | "search";
};

Expand All @@ -47,6 +51,8 @@ type MultiSelectDropdownProps = {
disabledItems: MultiSelectItem[];
header?: ReactNode;
updateItems: (newItems: MultiSelectItem[]) => void;
onDeselectItem?: (item: MultiSelectItem) => void;
onSelectItem?: (item: MultiSelectItem) => void;
footer?: ReactNode;
groupFn?: GroupFn;
sortFn?: SortFn;
Expand Down Expand Up @@ -88,6 +94,8 @@ export const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({
disabledItems,
header,
updateItems,
onSelectItem,
onDeselectItem,
isOpen,
footer,
sortFn = sortAlphabetically,
Expand Down Expand Up @@ -126,6 +134,11 @@ export const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({
? [...selectedItems, foundItem]
: selectedItems.filter((item) => `${item.value}` !== value) ?? [];
updateItems(newSelectedItems);
if (checked) {
onSelectItem?.(foundItem);
} else {
onDeselectItem?.(foundItem);
}
}
};

Expand Down Expand Up @@ -173,13 +186,17 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
disabled,
selectedItems: externalSelectedItems = [],
label,
listSelected = true,
onItemsUpdate,
onSelectItem,
onDeselectItem,
placeholder,
required = false,
items = [],
disabledItems = [],
dropdownHeader,
dropdownFooter,
showDropdownFooter = true,
variant = "search",
}: MultiSelectProps) => {
const wrapperRef = useClickOutside<HTMLDivElement>(() => {
Expand Down Expand Up @@ -218,6 +235,44 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
)
.map((el) => el.label)
.join(", ");
let footer: ReactNode = null;
if (showDropdownFooter) {
footer = dropdownFooter ? (
dropdownFooter
) : (
<>
<Button
appearance="link"
onClick={() => {
const enabledItems = items.filter(
(item) =>
!disabledItems.some(
(disabledItem) => disabledItem.value === item.value
)
);
updateItems([...selectedItems, ...enabledItems]);
}}
type="button"
>
Select all
</Button>
<Button
appearance="link"
onClick={() => {
const disabledSelectedItems = selectedItems.filter((item) =>
disabledItems.some(
(disabledItem) => disabledItem.value === item.value
)
);
updateItems(disabledSelectedItems);
}}
type="button"
>
Clear
</Button>
</>
);
}
return (
<div ref={wrapperRef}>
<div className="multi-select">
Expand Down Expand Up @@ -256,7 +311,7 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
}}
>
<span className="multi-select__condensed-text">
{selectedItems.length > 0
{listSelected && selectedItems.length > 0
? selectedItemsLabel
: placeholder ?? "Select items"}
</span>
Expand All @@ -276,43 +331,9 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
disabledItems={disabledItems}
header={dropdownHeader}
updateItems={updateItems}
footer={
dropdownFooter ? (
dropdownFooter
) : (
<>
<Button
appearance="link"
onClick={() => {
const enabledItems = items.filter(
(item) =>
!disabledItems.some(
(disabledItem) => disabledItem.value === item.value
)
);
updateItems([...selectedItems, ...enabledItems]);
}}
type="button"
>
Select all
</Button>
<Button
appearance="link"
onClick={() => {
const disabledSelectedItems = selectedItems.filter((item) =>
disabledItems.some(
(disabledItem) => disabledItem.value === item.value
)
);
updateItems(disabledSelectedItems);
}}
type="button"
>
Clear
</Button>
</>
)
}
onSelectItem={onSelectItem}
onDeselectItem={onDeselectItem}
footer={footer}
/>
</div>
</div>
Expand Down

0 comments on commit b716ae7

Please sign in to comment.