diff --git a/HACKING.md b/HACKING.md index 18efde421..f82f3579c 100644 --- a/HACKING.md +++ b/HACKING.md @@ -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 diff --git a/src/components/Field/Field.tsx b/src/components/Field/Field.tsx index 21a9d7bb3..6dc09eb17 100644 --- a/src/components/Field/Field.tsx +++ b/src/components/Field/Field.tsx @@ -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. */ diff --git a/src/components/MultiSelect/MultiSelect.test.tsx b/src/components/MultiSelect/MultiSelect.test.tsx index 59f0b7efe..47090abd8 100644 --- a/src/components/MultiSelect/MultiSelect.test.tsx +++ b/src/components/MultiSelect/MultiSelect.test.tsx @@ -51,19 +51,29 @@ it("can have some options preselected", async () => { it("can select options from the dropdown", async () => { const onItemsUpdate = jest.fn(); - render(); + const onSelectItem = jest.fn(); + render( + + ); 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( ); await userEvent.click(screen.getByRole("combobox")); @@ -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 () => { @@ -99,6 +110,20 @@ it("can display a custom dropdown header and footer", async () => { ).toBeInTheDocument(); }); +it("can not display the footer", async () => { + render( + custom footer 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( @@ -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( + + ); + 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" }, diff --git a/src/components/MultiSelect/MultiSelect.tsx b/src/components/MultiSelect/MultiSelect.tsx index 753e4f322..fcced7ad0 100644 --- a/src/components/MultiSelect/MultiSelect.tsx +++ b/src/components/MultiSelect/MultiSelect.tsx @@ -24,7 +24,10 @@ 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[]; @@ -32,6 +35,7 @@ export type MultiSelectProps = { renderItem?: (item: MultiSelectItem) => ReactNode; dropdownHeader?: ReactNode; dropdownFooter?: ReactNode; + showDropdownFooter?: boolean; variant?: "condensed" | "search"; }; @@ -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; @@ -88,6 +94,8 @@ export const MultiSelectDropdown: React.FC = ({ disabledItems, header, updateItems, + onSelectItem, + onDeselectItem, isOpen, footer, sortFn = sortAlphabetically, @@ -126,6 +134,11 @@ export const MultiSelectDropdown: React.FC = ({ ? [...selectedItems, foundItem] : selectedItems.filter((item) => `${item.value}` !== value) ?? []; updateItems(newSelectedItems); + if (checked) { + onSelectItem?.(foundItem); + } else { + onDeselectItem?.(foundItem); + } } }; @@ -173,13 +186,17 @@ export const MultiSelect: React.FC = ({ disabled, selectedItems: externalSelectedItems = [], label, + listSelected = true, onItemsUpdate, + onSelectItem, + onDeselectItem, placeholder, required = false, items = [], disabledItems = [], dropdownHeader, dropdownFooter, + showDropdownFooter = true, variant = "search", }: MultiSelectProps) => { const wrapperRef = useClickOutside(() => { @@ -218,6 +235,44 @@ export const MultiSelect: React.FC = ({ ) .map((el) => el.label) .join(", "); + let footer: ReactNode = null; + if (showDropdownFooter) { + footer = dropdownFooter ? ( + dropdownFooter + ) : ( + <> + + + + ); + } return (
@@ -256,7 +311,7 @@ export const MultiSelect: React.FC = ({ }} > - {selectedItems.length > 0 + {listSelected && selectedItems.length > 0 ? selectedItemsLabel : placeholder ?? "Select items"} @@ -276,43 +331,9 @@ export const MultiSelect: React.FC = ({ disabledItems={disabledItems} header={dropdownHeader} updateItems={updateItems} - footer={ - dropdownFooter ? ( - dropdownFooter - ) : ( - <> - - - - ) - } + onSelectItem={onSelectItem} + onDeselectItem={onDeselectItem} + footer={footer} />