Skip to content

Commit

Permalink
fix: static and dynamic children (#792)
Browse files Browse the repository at this point in the history
* Expand children for SearchSelect  compatibility

* Expand children for Select compatibility

* Expand children for MultiSelect compatibility
  • Loading branch information
motinados authored Nov 9, 2023
1 parent bfe5d43 commit d2f1604
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 22 deletions.
18 changes: 10 additions & 8 deletions src/components/input-elements/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { tremorTwMerge } from "lib";
import React, { useMemo, useState } from "react";
import React, { isValidElement, useMemo, useState } from "react";

import { SelectedValueContext } from "contexts";

Expand All @@ -23,7 +23,7 @@ export interface MultiSelectProps extends React.HTMLAttributes<HTMLDivElement> {
placeholderSearch?: string;
disabled?: boolean;
icon?: React.ElementType | React.JSXElementConstructor<any>;
children: React.ReactElement[] | React.ReactElement;
children: React.ReactNode;
}

const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>((props, ref) => {
Expand All @@ -43,7 +43,12 @@ const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>((props, r
const Icon = icon;

const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value);
const optionsAvailable = getFilteredOptions("", children as React.ReactElement[]);

const { reactElementChildren, optionsAvailable } = useMemo(() => {
const reactElementChildren = React.Children.toArray(children).filter(isValidElement);
const optionsAvailable = getFilteredOptions("", reactElementChildren);
return { reactElementChildren, optionsAvailable };
}, [children]);

const [searchQuery, setSearchQuery] = useState("");

Expand All @@ -53,11 +58,8 @@ const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>((props, r
const hasSelection = selectedItems.length > 0;

const filteredOptions = useMemo(
() =>
searchQuery
? getFilteredOptions(searchQuery, children as React.ReactElement[])
: optionsAvailable,
[searchQuery, children, optionsAvailable],
() => (searchQuery ? getFilteredOptions(searchQuery, reactElementChildren) : optionsAvailable),
[searchQuery, reactElementChildren, optionsAvailable],
);

const handleReset = () => {
Expand Down
16 changes: 11 additions & 5 deletions src/components/input-elements/SearchSelect/SearchSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { useInternalState } from "hooks";
import { tremorTwMerge } from "lib";
import React, { useMemo, useState } from "react";
import React, { isValidElement, useMemo, useState } from "react";

import { Combobox, Transition } from "@headlessui/react";
import { ArrowDownHeadIcon, XCircleIcon } from "assets";
Expand All @@ -23,7 +23,7 @@ export interface SearchSelectProps extends React.HTMLAttributes<HTMLDivElement>
disabled?: boolean;
icon?: React.ElementType | React.JSXElementConstructor<any>;
enableClear?: boolean;
children: React.ReactElement[] | React.ReactElement;
children: React.ReactNode;
}

const makeSelectClassName = makeClassName("SearchSelect");
Expand All @@ -46,10 +46,16 @@ const SearchSelect = React.forwardRef<HTMLDivElement, SearchSelectProps>((props,
const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value);

const Icon = icon;
const valueToNameMapping = useMemo(() => constructValueToNameMapping(children), [children]);

const { reactElementChildren, valueToNameMapping } = useMemo(() => {
const reactElementChildren = React.Children.toArray(children).filter(isValidElement);
const valueToNameMapping = constructValueToNameMapping(reactElementChildren);
return { reactElementChildren, valueToNameMapping };
}, [children]);

const filteredOptions = useMemo(
() => getFilteredOptions(searchQuery, children as React.ReactElement[]),
[searchQuery, children],
() => getFilteredOptions(searchQuery, reactElementChildren),
[searchQuery, reactElementChildren],
);

const handleReset = () => {
Expand Down
10 changes: 7 additions & 3 deletions src/components/input-elements/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { ArrowDownHeadIcon, XCircleIcon } from "assets";
import { border, makeClassName, sizing, spacing } from "lib";
import React, { useMemo } from "react";
import React, { isValidElement, useMemo } from "react";
import { constructValueToNameMapping, getSelectButtonColors, hasValue } from "../selectUtils";

import { Listbox, Transition } from "@headlessui/react";
Expand All @@ -19,7 +19,7 @@ export interface SelectProps extends React.HTMLAttributes<HTMLDivElement> {
disabled?: boolean;
icon?: React.JSXElementConstructor<any>;
enableClear?: boolean;
children: React.ReactElement[] | React.ReactElement;
children: React.ReactNode;
}

const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
Expand All @@ -38,7 +38,11 @@ const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) => {

const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value);
const Icon = icon;
const valueToNameMapping = useMemo(() => constructValueToNameMapping(children), [children]);
const valueToNameMapping = useMemo(() => {
const reactElementChildren = React.Children.toArray(children).filter(isValidElement);
const valueToNameMapping = constructValueToNameMapping(reactElementChildren);
return valueToNameMapping;
}, [children]);

const handleReset = () => {
setSelectedValue("");
Expand Down
11 changes: 10 additions & 1 deletion src/stories/input-elements/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";

import { MultiSelect } from "components";
import { SimpleMultiSelect, SimpleMultiSelectControlled } from "./helpers/SimpleMultiSelect";
import {
SimpleMultiSelect,
SimpleMultiSelectControlled,
SimpleMultiSelectWithStaticAndDynamicChildren,
} from "./helpers/SimpleMultiSelect";

import { CalendarIcon } from "assets";

Expand All @@ -18,6 +22,11 @@ export const UncontrolledDefault: Story = {
args: {},
};

export const UncontrolledDefaultWithStaticAndDynamicChildren: Story = {
render: SimpleMultiSelectWithStaticAndDynamicChildren,
args: {},
};

export const UncontrolledDefaultValues: Story = {
render: SimpleMultiSelect,
args: { defaultValue: ["5", "1"] },
Expand Down
11 changes: 10 additions & 1 deletion src/stories/input-elements/SearchSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";

import { SearchSelect } from "components";
import { SimpleSearchSelect, SimpleSearchSelectControlled } from "./helpers/SimpleSearchSelect";
import {
SimpleSearchSelect,
SimpleSearchSelectControlled,
SimpleSearchSelectWithStaticAndDynamicChildren,
} from "./helpers/SimpleSearchSelect";

import { CalendarIcon } from "assets";

Expand All @@ -18,6 +22,11 @@ export const UncontrolledDefault: Story = {
args: {},
};

export const UncontrolledDefaultWithStaticAndDynamicChildren: Story = {
render: SimpleSearchSelectWithStaticAndDynamicChildren,
args: {},
};

export const UncontrolledOnValueChange: Story = {
render: SimpleSearchSelect,
args: { onValueChange: (v: string) => alert(v) },
Expand Down
11 changes: 10 additions & 1 deletion src/stories/input-elements/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Select } from "components";
import { SimpleSelect, SimpleSelectControlled } from "./helpers/SimpleSelect";
import {
SimpleSelect,
SimpleSelectControlled,
SimpleSelectWithStaticAndDynamicChildren,
} from "./helpers/SimpleSelect";

import { CalendarIcon } from "assets";

Expand All @@ -18,6 +22,11 @@ export const UncontrolledDefault: Story = {
args: {},
};

export const UncontrolledDefaultWithStaticAndDynamicChildren: Story = {
render: SimpleSelectWithStaticAndDynamicChildren,
args: {},
};

export const UncontrolledOnValueChange: Story = {
render: SimpleSelect,
args: { onValueChange: (v: string) => alert(v) },
Expand Down
12 changes: 12 additions & 0 deletions src/stories/input-elements/helpers/SimpleMultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ export const SimpleMultiSelect = (args: any) => (
</MultiSelect>
);

export const SimpleMultiSelectWithStaticAndDynamicChildren = (args: any) => {
const items = ["item1", "item2"];
return (
<MultiSelect {...args}>
<MultiSelectItem value="item0">item0</MultiSelectItem>
{items.map((item) => {
return <MultiSelectItem value={item} key={item} />;
})}
</MultiSelect>
);
};

export const SimpleMultiSelectControlled = () => {
const [value, setValue] = React.useState<string[]>([]);

Expand Down
12 changes: 12 additions & 0 deletions src/stories/input-elements/helpers/SimpleSearchSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export const SimpleSearchSelect = (args: any) => (
</SearchSelect>
);

export const SimpleSearchSelectWithStaticAndDynamicChildren = (args: any) => {
const items = ["item1", "item2"];
return (
<SearchSelect {...args}>
<SearchSelectItem value="item0">item0</SearchSelectItem>
{items.map((item) => {
return <SearchSelectItem value={item} key={item} />;
})}
</SearchSelect>
);
};

export const SimpleSearchSelectControlled = (args: any) => {
const [value, setValue] = React.useState<string>("5");
return (
Expand Down
12 changes: 12 additions & 0 deletions src/stories/input-elements/helpers/SimpleSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export const SimpleSelect = (args: any) => (
</Select>
);

export const SimpleSelectWithStaticAndDynamicChildren = (args: any) => {
const items = ["item1", "item2"];
return (
<Select {...args}>
<SelectItem value="item0">item0</SelectItem>
{items.map((item) => {
return <SelectItem value={item} key={item} />;
})}
</Select>
);
};

export function SimpleSelectControlled() {
const [value, setValue] = React.useState("5");

Expand Down
21 changes: 20 additions & 1 deletion src/tests/input-elements/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";

import MultiSelect from "components/input-elements/MultiSelect/MultiSelect";
Expand All @@ -14,4 +14,23 @@ describe("MultiSelect", () => {
</MultiSelect>,
);
});

test("renders the MultiSelect component with static and dynamic children", () => {
const placeholder = "Select options...";
const items = ["item1", "item2"];
render(
<MultiSelect placeholder={placeholder}>
<MultiSelectItem value="item0">item0</MultiSelectItem>
{items.map((item) => {
return <MultiSelectItem value={item} key={item} />;
})}
</MultiSelect>,
);

fireEvent.click(screen.getByText(placeholder));

expect(screen.queryByText("item0")).toBeTruthy();
expect(screen.queryByText("item1")).toBeTruthy();
expect(screen.queryByText("item2")).toBeTruthy();
});
});
21 changes: 20 additions & 1 deletion src/tests/input-elements/SearchSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";

import SearchSelect from "components/input-elements/SearchSelect/SearchSelect";
Expand All @@ -14,4 +14,23 @@ describe("SearchSelect", () => {
</SearchSelect>,
);
});

test("renders the SelectBox component with static and dynamic children", () => {
const placeholder = "Select options...";
const items = ["item1", "item2"];
render(
<SearchSelect placeholder={placeholder}>
<SearchSelectItem value="item0">item0</SearchSelectItem>
{items.map((item) => {
return <SearchSelectItem value={item} key={item} />;
})}
</SearchSelect>,
);

fireEvent.click(screen.getByPlaceholderText(placeholder));

expect(screen.queryByText("item0")).toBeTruthy();
expect(screen.queryByText("item1")).toBeTruthy();
expect(screen.queryByText("item2")).toBeTruthy();
});
});
21 changes: 20 additions & 1 deletion src/tests/input-elements/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";

import Select from "components/input-elements/Select/Select";
Expand All @@ -14,4 +14,23 @@ describe("Select", () => {
</Select>,
);
});

test("renders the Select component with static and dynamic children", () => {
const placeholder = "Select options...";
const items = ["item1", "item2"];
render(
<Select placeholder={placeholder}>
<SelectItem value="item0">item0</SelectItem>
{items.map((item) => {
return <SelectItem value={item} key={item} />;
})}
</Select>,
);

fireEvent.click(screen.getByText(placeholder));

expect(screen.queryByText("item0")).toBeTruthy();
expect(screen.queryByText("item1")).toBeTruthy();
expect(screen.queryByText("item2")).toBeTruthy();
});
});

0 comments on commit d2f1604

Please sign in to comment.