diff --git a/packages/core/src/components/button/ButtonGroup.tsx b/packages/core/src/components/button/ButtonGroup.tsx index f778a00..7bc578f 100644 --- a/packages/core/src/components/button/ButtonGroup.tsx +++ b/packages/core/src/components/button/ButtonGroup.tsx @@ -21,8 +21,9 @@ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { cloneChildren } from "@react-fabric/utilities"; import classNames from "classnames"; -import { Children, cloneElement } from "react"; +import { cloneElement } from "react"; import { type AriaProps, type ChildrenProp, @@ -96,7 +97,7 @@ export const ButtonGroup = ({ )} {...aria} > - {Children.map(children, (child: AnyObject) => + {cloneChildren(children, (child: AnyObject) => cloneElement(child, { color, size, diff --git a/packages/core/src/components/button/ToggleButtonGroup.tsx b/packages/core/src/components/button/ToggleButtonGroup.tsx index 5e9944c..c96f0f6 100644 --- a/packages/core/src/components/button/ToggleButtonGroup.tsx +++ b/packages/core/src/components/button/ToggleButtonGroup.tsx @@ -21,9 +21,9 @@ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { EMPTY_ARRAY, isArray } from "@react-fabric/utilities"; +import { cloneChildren, EMPTY_ARRAY, isArray } from "@react-fabric/utilities"; import classNames from "classnames"; -import { Children, cloneElement, useCallback, useMemo } from "react"; +import { cloneElement, useCallback, useMemo } from "react"; import { useControlledValue } from "../../hooks/useControlledValue"; import { type AriaProps, @@ -140,7 +140,7 @@ export const ToggleButtonGroup = ({ )} {...aria} > - {Children.map(children, (child: AnyObject) => + {cloneChildren(children, (child: AnyObject) => cloneElement(child, { color, size, diff --git a/packages/core/src/components/menu/Menu.tsx b/packages/core/src/components/menu/Menu.tsx index 4c05a56..64d4fd3 100644 --- a/packages/core/src/components/menu/Menu.tsx +++ b/packages/core/src/components/menu/Menu.tsx @@ -45,10 +45,9 @@ import { useRole, useTypeahead, } from "@floating-ui/react"; -import { mergeRefs } from "@react-fabric/utilities"; +import { cloneChildren, mergeRefs } from "@react-fabric/utilities"; import classNames from "classnames"; import { - Children, cloneElement, Fragment, useCallback, @@ -231,25 +230,23 @@ const MenuComponent = ({ // @ts-expect-error ignore {...{ style: !isNested ? rest.style : floatingStyles }} > - {Children.map(children, (child: AnyObject, index) => { - if (child) { - labelsRef.current[index] = child.props.label; - return cloneElement( - child, - nodeCheck(child, MenuItem, Menu, MenuComponent) - ? { - minimal: !isNested && minimal, - "data-focus": activeIndex === index, - ref: (el: HTMLElement) => - (elementsRef.current[index] = el), - ...getItemProps({ - onClick: child.props.onClick, - tabIndex: activeIndex === index ? 0 : -1, - }), - } - : {}, - ); - } + {cloneChildren(children, (child: AnyObject, index) => { + labelsRef.current[index] = child.props.label; + return cloneElement( + child, + nodeCheck(child, MenuItem, Menu, MenuComponent) + ? { + minimal: !isNested && minimal, + "data-focus": activeIndex === index, + ref: (el: HTMLElement) => + (elementsRef.current[index] = el), + ...getItemProps({ + onClick: child.props.onClick, + tabIndex: activeIndex === index ? 0 : -1, + }), + } + : {}, + ); })} diff --git a/packages/core/stories/components/button/ButtonGroup.stories.tsx b/packages/core/stories/components/button/ButtonGroup.stories.tsx index a8a0672..013b488 100644 --- a/packages/core/stories/components/button/ButtonGroup.stories.tsx +++ b/packages/core/stories/components/button/ButtonGroup.stories.tsx @@ -52,9 +52,12 @@ export const _ButtonGroup: ButtonGroupStory = { return ( - + + + + {args.className === "red" && } ); diff --git a/packages/core/stories/components/menu/Menu.stories.tsx b/packages/core/stories/components/menu/Menu.stories.tsx index a5017e0..6212790 100644 --- a/packages/core/stories/components/menu/Menu.stories.tsx +++ b/packages/core/stories/components/menu/Menu.stories.tsx @@ -132,8 +132,10 @@ export const Minimal: MenuStory = { appendLabel="NEW" /> - - + + + + diff --git a/packages/form/src/input/ArrayInput.tsx b/packages/form/src/input/ArrayInput.tsx index 98edf07..a1f0ac2 100644 --- a/packages/form/src/input/ArrayInput.tsx +++ b/packages/form/src/input/ArrayInput.tsx @@ -24,10 +24,9 @@ import { DndContext, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, useSortable } from "@dnd-kit/sortable"; import { Button, CoreIcons, Icon, Tooltip } from "@react-fabric/core"; -import { isString } from "@react-fabric/utilities"; +import { cloneChildren, isString } from "@react-fabric/utilities"; import classNames from "classnames"; import { - Children, cloneElement, useCallback, useImperativeHandle, @@ -255,7 +254,7 @@ export const ArrayInput = ({ index, name: `${name}.${index}`, }) - : Children.map(children, (child: AnyObject) => + : cloneChildren(children, (child: AnyObject) => cloneElement( child, Object.assign( diff --git a/packages/form/src/input/Field.tsx b/packages/form/src/input/Field.tsx index 80cf6e8..72254f7 100644 --- a/packages/form/src/input/Field.tsx +++ b/packages/form/src/input/Field.tsx @@ -25,8 +25,9 @@ import { type ChildrenProp, type CssProp, } from "@react-fabric/core/dist/types/types"; +import { cloneChildren } from "@react-fabric/utilities"; import classNames from "classnames"; -import { Children, cloneElement } from "react"; +import { cloneElement } from "react"; import classes from "../internal/Field.module.css"; import { FieldWrapper } from "../internal/FieldWrapper"; @@ -90,13 +91,10 @@ export const Field = ({ !vertical && "flex flex-nowrap", )} > - {Children.map( - children, - (child) => - child && - cloneElement(child as AnyObject, { - "data-inner": true, - }), + {cloneChildren(children, (child: AnyObject) => + cloneElement(child, { + "data-inner": true, + }), )} diff --git a/packages/form/src/input/RadioGroup.tsx b/packages/form/src/input/RadioGroup.tsx index 601a901..617ce4e 100644 --- a/packages/form/src/input/RadioGroup.tsx +++ b/packages/form/src/input/RadioGroup.tsx @@ -25,7 +25,8 @@ import { type ChildrenProp, type CssProp, } from "@react-fabric/core/dist/types/types"; -import { Children, cloneElement } from "react"; +import { cloneChildren } from "@react-fabric/utilities"; +import { cloneElement } from "react"; import { FieldWrapper } from "../internal/FieldWrapper"; import { type Radio } from "./Radio"; @@ -84,8 +85,8 @@ export const RadioGroup = ({ return (
- {Children.map(children, (child) => - cloneElement(child as AnyObject, { + {cloneChildren(children, (child: AnyObject) => + cloneElement(child, { name, onChange, width: optionWidth, diff --git a/packages/utilities/src/_cloneElement.ts b/packages/utilities/src/_cloneElement.ts new file mode 100644 index 0000000..0351156 --- /dev/null +++ b/packages/utilities/src/_cloneElement.ts @@ -0,0 +1,42 @@ +/* + * React Fabric + * @version: 1.0.0 + * + * + * The MIT License (MIT) + * Copyright (c) 2024 Adarsh Pastakia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { type ReactNode, Children, isValidElement } from "react"; +import { Fragment } from "react/jsx-runtime"; + +export const cloneChildren = ( + children: ReactNode, + callback: (child: ReactNode, index: number) => ReactNode, + index = 0, +): ReactNode => { + return Children.map(children, (c: ReactNode) => { + if (isValidElement(c)) { + if (c.type === Fragment) { + // just compare to `Fragment` + return cloneChildren(c.props?.children, callback); + } + return callback(c, index++); + } + return c; + }); +}; diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index c6aa568..4398c83 100755 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -26,6 +26,7 @@ export * from "./_ascii"; export * from "./_boundingBox"; export { getBoundingBox, getBox } from "./_boundingBox"; +export * from "./_cloneElement"; export { default as Countries } from "./_countries"; export type { Country } from "./_countries"; export * from "./_debounce";