From d2f16047311e2ac489577fff05c9291c15c429ea Mon Sep 17 00:00:00 2001 From: motinados <69296148+motinados@users.noreply.github.com> Date: Fri, 10 Nov 2023 07:42:49 +0900 Subject: [PATCH] fix: static and dynamic children (#792) * Expand children for SearchSelect compatibility * Expand children for Select compatibility * Expand children for MultiSelect compatibility --- .../MultiSelect/MultiSelect.tsx | 18 +++++++++------- .../SearchSelect/SearchSelect.tsx | 16 +++++++++----- .../input-elements/Select/Select.tsx | 10 ++++++--- .../input-elements/MultiSelect.stories.tsx | 11 +++++++++- .../input-elements/SearchSelect.stories.tsx | 11 +++++++++- src/stories/input-elements/Select.stories.tsx | 11 +++++++++- .../helpers/SimpleMultiSelect.tsx | 12 +++++++++++ .../helpers/SimpleSearchSelect.tsx | 12 +++++++++++ .../input-elements/helpers/SimpleSelect.tsx | 12 +++++++++++ src/tests/input-elements/MultiSelect.test.tsx | 21 ++++++++++++++++++- .../input-elements/SearchSelect.test.tsx | 21 ++++++++++++++++++- src/tests/input-elements/Select.test.tsx | 21 ++++++++++++++++++- 12 files changed, 154 insertions(+), 22 deletions(-) diff --git a/src/components/input-elements/MultiSelect/MultiSelect.tsx b/src/components/input-elements/MultiSelect/MultiSelect.tsx index 5c9f63332..231293d03 100644 --- a/src/components/input-elements/MultiSelect/MultiSelect.tsx +++ b/src/components/input-elements/MultiSelect/MultiSelect.tsx @@ -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"; @@ -23,7 +23,7 @@ export interface MultiSelectProps extends React.HTMLAttributes { placeholderSearch?: string; disabled?: boolean; icon?: React.ElementType | React.JSXElementConstructor; - children: React.ReactElement[] | React.ReactElement; + children: React.ReactNode; } const MultiSelect = React.forwardRef((props, ref) => { @@ -43,7 +43,12 @@ const MultiSelect = React.forwardRef((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(""); @@ -53,11 +58,8 @@ const MultiSelect = React.forwardRef((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 = () => { diff --git a/src/components/input-elements/SearchSelect/SearchSelect.tsx b/src/components/input-elements/SearchSelect/SearchSelect.tsx index b5e1cdc8a..ca5303e63 100644 --- a/src/components/input-elements/SearchSelect/SearchSelect.tsx +++ b/src/components/input-elements/SearchSelect/SearchSelect.tsx @@ -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"; @@ -23,7 +23,7 @@ export interface SearchSelectProps extends React.HTMLAttributes disabled?: boolean; icon?: React.ElementType | React.JSXElementConstructor; enableClear?: boolean; - children: React.ReactElement[] | React.ReactElement; + children: React.ReactNode; } const makeSelectClassName = makeClassName("SearchSelect"); @@ -46,10 +46,16 @@ const SearchSelect = React.forwardRef((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 = () => { diff --git a/src/components/input-elements/Select/Select.tsx b/src/components/input-elements/Select/Select.tsx index 58b0a9c47..ed4e70776 100644 --- a/src/components/input-elements/Select/Select.tsx +++ b/src/components/input-elements/Select/Select.tsx @@ -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"; @@ -19,7 +19,7 @@ export interface SelectProps extends React.HTMLAttributes { disabled?: boolean; icon?: React.JSXElementConstructor; enableClear?: boolean; - children: React.ReactElement[] | React.ReactElement; + children: React.ReactNode; } const Select = React.forwardRef((props, ref) => { @@ -38,7 +38,11 @@ const Select = React.forwardRef((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(""); diff --git a/src/stories/input-elements/MultiSelect.stories.tsx b/src/stories/input-elements/MultiSelect.stories.tsx index eee97dd28..0b24d573b 100644 --- a/src/stories/input-elements/MultiSelect.stories.tsx +++ b/src/stories/input-elements/MultiSelect.stories.tsx @@ -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"; @@ -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"] }, diff --git a/src/stories/input-elements/SearchSelect.stories.tsx b/src/stories/input-elements/SearchSelect.stories.tsx index a5971737d..e51f1beea 100644 --- a/src/stories/input-elements/SearchSelect.stories.tsx +++ b/src/stories/input-elements/SearchSelect.stories.tsx @@ -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"; @@ -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) }, diff --git a/src/stories/input-elements/Select.stories.tsx b/src/stories/input-elements/Select.stories.tsx index a4e8159cf..099a98fa3 100644 --- a/src/stories/input-elements/Select.stories.tsx +++ b/src/stories/input-elements/Select.stories.tsx @@ -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"; @@ -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) }, diff --git a/src/stories/input-elements/helpers/SimpleMultiSelect.tsx b/src/stories/input-elements/helpers/SimpleMultiSelect.tsx index 83ad53966..85e295ccb 100644 --- a/src/stories/input-elements/helpers/SimpleMultiSelect.tsx +++ b/src/stories/input-elements/helpers/SimpleMultiSelect.tsx @@ -10,6 +10,18 @@ export const SimpleMultiSelect = (args: any) => ( ); +export const SimpleMultiSelectWithStaticAndDynamicChildren = (args: any) => { + const items = ["item1", "item2"]; + return ( + + item0 + {items.map((item) => { + return ; + })} + + ); +}; + export const SimpleMultiSelectControlled = () => { const [value, setValue] = React.useState([]); diff --git a/src/stories/input-elements/helpers/SimpleSearchSelect.tsx b/src/stories/input-elements/helpers/SimpleSearchSelect.tsx index 01862500b..0355ea623 100644 --- a/src/stories/input-elements/helpers/SimpleSearchSelect.tsx +++ b/src/stories/input-elements/helpers/SimpleSearchSelect.tsx @@ -15,6 +15,18 @@ export const SimpleSearchSelect = (args: any) => ( ); +export const SimpleSearchSelectWithStaticAndDynamicChildren = (args: any) => { + const items = ["item1", "item2"]; + return ( + + item0 + {items.map((item) => { + return ; + })} + + ); +}; + export const SimpleSearchSelectControlled = (args: any) => { const [value, setValue] = React.useState("5"); return ( diff --git a/src/stories/input-elements/helpers/SimpleSelect.tsx b/src/stories/input-elements/helpers/SimpleSelect.tsx index 18bed300a..99029ee45 100644 --- a/src/stories/input-elements/helpers/SimpleSelect.tsx +++ b/src/stories/input-elements/helpers/SimpleSelect.tsx @@ -17,6 +17,18 @@ export const SimpleSelect = (args: any) => ( ); +export const SimpleSelectWithStaticAndDynamicChildren = (args: any) => { + const items = ["item1", "item2"]; + return ( + + ); +}; + export function SimpleSelectControlled() { const [value, setValue] = React.useState("5"); diff --git a/src/tests/input-elements/MultiSelect.test.tsx b/src/tests/input-elements/MultiSelect.test.tsx index 1016cd5a9..9d8d25016 100644 --- a/src/tests/input-elements/MultiSelect.test.tsx +++ b/src/tests/input-elements/MultiSelect.test.tsx @@ -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"; @@ -14,4 +14,23 @@ describe("MultiSelect", () => { , ); }); + + test("renders the MultiSelect component with static and dynamic children", () => { + const placeholder = "Select options..."; + const items = ["item1", "item2"]; + render( + + item0 + {items.map((item) => { + return ; + })} + , + ); + + fireEvent.click(screen.getByText(placeholder)); + + expect(screen.queryByText("item0")).toBeTruthy(); + expect(screen.queryByText("item1")).toBeTruthy(); + expect(screen.queryByText("item2")).toBeTruthy(); + }); }); diff --git a/src/tests/input-elements/SearchSelect.test.tsx b/src/tests/input-elements/SearchSelect.test.tsx index f8ce721b8..13fb22ca2 100644 --- a/src/tests/input-elements/SearchSelect.test.tsx +++ b/src/tests/input-elements/SearchSelect.test.tsx @@ -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"; @@ -14,4 +14,23 @@ describe("SearchSelect", () => { , ); }); + + test("renders the SelectBox component with static and dynamic children", () => { + const placeholder = "Select options..."; + const items = ["item1", "item2"]; + render( + + item0 + {items.map((item) => { + return ; + })} + , + ); + + fireEvent.click(screen.getByPlaceholderText(placeholder)); + + expect(screen.queryByText("item0")).toBeTruthy(); + expect(screen.queryByText("item1")).toBeTruthy(); + expect(screen.queryByText("item2")).toBeTruthy(); + }); }); diff --git a/src/tests/input-elements/Select.test.tsx b/src/tests/input-elements/Select.test.tsx index 92dea20f0..b6922c05a 100644 --- a/src/tests/input-elements/Select.test.tsx +++ b/src/tests/input-elements/Select.test.tsx @@ -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"; @@ -14,4 +14,23 @@ describe("Select", () => { , ); }); + + test("renders the Select component with static and dynamic children", () => { + const placeholder = "Select options..."; + const items = ["item1", "item2"]; + render( + , + ); + + fireEvent.click(screen.getByText(placeholder)); + + expect(screen.queryByText("item0")).toBeTruthy(); + expect(screen.queryByText("item1")).toBeTruthy(); + expect(screen.queryByText("item2")).toBeTruthy(); + }); });