diff --git a/apps/builder/app/shared/content-model.ts b/apps/builder/app/shared/content-model.ts index ae544dfeb705..f75834a7b25a 100644 --- a/apps/builder/app/shared/content-model.ts +++ b/apps/builder/app/shared/content-model.ts @@ -424,7 +424,7 @@ export const richTextPlaceholders: Map = new Map([ ["blockquote", "Blockquote"], ["li", "List item"], ["a", "Link"], - ["span", "Span"], + ["span", ""], ]); const findContentTags = ({ diff --git a/packages/sdk-components-react-radix/package.json b/packages/sdk-components-react-radix/package.json index e4c3045de5cf..e639748ba4b5 100644 --- a/packages/sdk-components-react-radix/package.json +++ b/packages/sdk-components-react-radix/package.json @@ -58,7 +58,6 @@ "@radix-ui/react-switch": "^1.2.2", "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-tooltip": "^1.2.4", - "@radix-ui/react-use-controllable-state": "^1.2.2", "@webstudio-is/css-engine": "workspace:*", "@webstudio-is/icons": "workspace:*", "@webstudio-is/react-sdk": "workspace:*", diff --git a/packages/sdk-components-react-radix/src/__generated__/tabs.props.ts b/packages/sdk-components-react-radix/src/__generated__/tabs.props.ts index e59078285f7f..2d7f8543fdd4 100644 --- a/packages/sdk-components-react-radix/src/__generated__/tabs.props.ts +++ b/packages/sdk-components-react-radix/src/__generated__/tabs.props.ts @@ -31,10 +31,10 @@ export const propsTabs: Record = { options: ["horizontal", "vertical"], }, value: { + description: "The value for the selected tab, if controlled", required: false, control: "text", type: "string", - description: "Current value of the element", }, }; export const propsTabsList: Record = { diff --git a/packages/sdk-components-react-radix/src/accordion.template.tsx b/packages/sdk-components-react-radix/src/accordion.template.tsx index 8bb7c8240323..f0a98a795994 100644 --- a/packages/sdk-components-react-radix/src/accordion.template.tsx +++ b/packages/sdk-components-react-radix/src/accordion.template.tsx @@ -98,7 +98,7 @@ export const meta: TemplateMeta = { "A vertically stacked set of interactive headings that each reveal an associated section of content. Clicking on the heading will open the item and close other items.", order: 3, template: ( - + {createAccordionItem( "Is it accessible?", "Yes. It adheres to the WAI-ARIA design pattern." diff --git a/packages/sdk-components-react-radix/src/accordion.tsx b/packages/sdk-components-react-radix/src/accordion.tsx index 5854fb772c1c..15e4c821dbe8 100644 --- a/packages/sdk-components-react-radix/src/accordion.tsx +++ b/packages/sdk-components-react-radix/src/accordion.tsx @@ -4,6 +4,8 @@ import { forwardRef, type ComponentProps, type RefAttributes, + useState, + useEffect, } from "react"; import { Root, @@ -21,8 +23,20 @@ export const Accordion = forwardRef< Extract, { type: "single" }>, "type" | "asChild" > ->((props, ref) => { - return ; +>(({ defaultValue, ...props }, ref) => { + const currentValue = props.value ?? defaultValue ?? ""; + const [value, setValue] = useState(currentValue); + // synchronize external value with local one when changed + useEffect(() => setValue(currentValue), [currentValue]); + return ( + + ); }); export const AccordionItem = forwardRef< diff --git a/packages/sdk-components-react-radix/src/checkbox.tsx b/packages/sdk-components-react-radix/src/checkbox.tsx index dcf99a39ec11..a40401e9bffb 100644 --- a/packages/sdk-components-react-radix/src/checkbox.tsx +++ b/packages/sdk-components-react-radix/src/checkbox.tsx @@ -3,9 +3,10 @@ import { type ComponentPropsWithRef, forwardRef, type ComponentProps, + useState, + useEffect, } from "react"; import { Root, Indicator } from "@radix-ui/react-checkbox"; -import { useControllableState } from "@radix-ui/react-use-controllable-state"; export const Checkbox = forwardRef< HTMLButtonElement, @@ -16,16 +17,16 @@ export const Checkbox = forwardRef< defaultChecked?: boolean; } >(({ defaultChecked, ...props }, ref) => { - const [checked, onCheckedChange] = useControllableState({ - prop: props.checked ?? defaultChecked ?? false, - defaultProp: false, - }); + const currentChecked = props.checked ?? defaultChecked ?? false; + const [checked, setChecked] = useState(currentChecked); + // synchronize external value with local one when changed + useEffect(() => setChecked(currentChecked), [currentChecked]); return ( onCheckedChange(open === true)} + onCheckedChange={(open) => setChecked(open === true)} /> ); }); diff --git a/packages/sdk-components-react-radix/src/collapsible.tsx b/packages/sdk-components-react-radix/src/collapsible.tsx index 90c269935b38..7053df9cbdb6 100644 --- a/packages/sdk-components-react-radix/src/collapsible.tsx +++ b/packages/sdk-components-react-radix/src/collapsible.tsx @@ -5,14 +5,22 @@ import { Children, type ComponentProps, type RefAttributes, + useState, + useEffect, } from "react"; import { Root, Trigger, Content } from "@radix-ui/react-collapsible"; import { type Hook, getClosestInstance } from "@webstudio-is/react-sdk/runtime"; -export const Collapsible: ForwardRefExoticComponent< - Omit, "defaultOpen" | "asChild"> & - RefAttributes -> = Root; +export const Collapsible = forwardRef< + HTMLDivElement, + Omit, "defaultOpen" | "asChild"> +>((props, ref) => { + const currentOpen = props.open ?? false; + const [open, setOpen] = useState(currentOpen); + // synchronize external value with local one when changed + useEffect(() => setOpen(currentOpen), [currentOpen]); + return ; +}); /** * We're not exposing the 'asChild' property for the Trigger. diff --git a/packages/sdk-components-react-radix/src/dialog.tsx b/packages/sdk-components-react-radix/src/dialog.tsx index af249abd2b5b..2efe26b5fceb 100644 --- a/packages/sdk-components-react-radix/src/dialog.tsx +++ b/packages/sdk-components-react-radix/src/dialog.tsx @@ -1,13 +1,14 @@ +import interactionResponse from "await-interaction-response"; import { - type ComponentPropsWithoutRef, type ReactNode, + type ComponentProps, forwardRef, Children, - type ComponentProps, useEffect, useRef, useContext, useCallback, + useState, } from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { @@ -15,8 +16,6 @@ import { getClosestInstance, type Hook, } from "@webstudio-is/react-sdk/runtime"; -import { useControllableState } from "@radix-ui/react-use-controllable-state"; -import interactionResponse from "await-interaction-response"; /** * Naive heuristic to determine if a click event will cause navigate @@ -50,23 +49,19 @@ const willNavigate = (event: MouseEvent) => { // wrap in forwardRef because Root is functional component without ref export const Dialog = forwardRef< HTMLDivElement, - Omit, "defaultOpen"> + Omit, "defaultOpen"> >((props, _ref) => { const { renderer } = useContext(ReactSdkContext); - const [open, onOpenChange] = useControllableState({ - prop: props.open, - defaultProp: false, - onChange: props.onOpenChange, - }); - - const onOpenChangeHandler = useCallback( - async (open: boolean) => { - await interactionResponse(); - onOpenChange(open); - }, - [onOpenChange] - ); + const currentOpen = props.open ?? false; + const [open, setOpen] = useState(currentOpen); + // synchronize external value with local one when changed + useEffect(() => setOpen(currentOpen), [currentOpen]); + + const onOpenChangeHandler = useCallback(async (open: boolean) => { + await interactionResponse(); + setOpen(open); + }, []); /** * Close the dialog when a navigable link within it is clicked. @@ -130,7 +125,7 @@ export const DialogTrigger = forwardRef< export const DialogOverlay = forwardRef< HTMLDivElement, - ComponentPropsWithoutRef + ComponentProps >((props, ref) => { return ( @@ -141,7 +136,7 @@ export const DialogOverlay = forwardRef< export const DialogContent = forwardRef< HTMLDivElement, - ComponentPropsWithoutRef + ComponentProps >((props, ref) => { const preventAutoFocusOnClose = useRef(false); const { renderer } = useContext(ReactSdkContext); diff --git a/packages/sdk-components-react-radix/src/dialog.ws.ts b/packages/sdk-components-react-radix/src/dialog.ws.ts index e5d2ca917be2..45579ea22d3d 100644 --- a/packages/sdk-components-react-radix/src/dialog.ws.ts +++ b/packages/sdk-components-react-radix/src/dialog.ws.ts @@ -98,5 +98,6 @@ export const metaDialog: WsComponentMeta = { children: ["instance"], descendants: [radix.DialogTrigger, radix.DialogOverlay], }, + initialProps: ["open"], props: propsDialog, }; diff --git a/packages/sdk-components-react-radix/src/popover.tsx b/packages/sdk-components-react-radix/src/popover.tsx index 249c727b9e3d..2df6fc085513 100644 --- a/packages/sdk-components-react-radix/src/popover.tsx +++ b/packages/sdk-components-react-radix/src/popover.tsx @@ -3,6 +3,8 @@ import { type ReactNode, forwardRef, Children, + useState, + useEffect, } from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import { getClosestInstance, type Hook } from "@webstudio-is/react-sdk/runtime"; @@ -12,7 +14,13 @@ export const Popover = forwardRef< HTMLDivElement, Omit, "defaultOpen"> >((props, _ref) => { - return ; + const currentOpen = props.open ?? false; + const [open, setOpen] = useState(currentOpen); + // synchronize external value with local one when changed + useEffect(() => setOpen(currentOpen), [currentOpen]); + return ( + + ); }); /** diff --git a/packages/sdk-components-react-radix/src/radio-group.tsx b/packages/sdk-components-react-radix/src/radio-group.tsx index b706e34d8f21..0b977bdc558d 100644 --- a/packages/sdk-components-react-radix/src/radio-group.tsx +++ b/packages/sdk-components-react-radix/src/radio-group.tsx @@ -4,24 +4,20 @@ import { type RefAttributes, type ElementRef, forwardRef, + useState, + useEffect, } from "react"; import { Root, Item, Indicator } from "@radix-ui/react-radio-group"; -import { useControllableState } from "@radix-ui/react-use-controllable-state"; - -const defaultTag = "div"; export const RadioGroup = forwardRef< - ElementRef, - ComponentProps & RefAttributes - // Make sure children are not passed down to an input, because this will result in error. + ElementRef<"div">, + ComponentProps >(({ defaultValue, ...props }, ref) => { - const [value, onValueChange] = useControllableState({ - prop: props.value ?? defaultValue ?? "", - defaultProp: "", - }); - return ( - - ); + const currentValue = props.value ?? defaultValue ?? ""; + const [value, setValue] = useState(currentValue); + // synchronize external value with local one when changed + useEffect(() => setValue(currentValue), [currentValue]); + return ; }); export const RadioGroupItem: ForwardRefExoticComponent< diff --git a/packages/sdk-components-react-radix/src/select.tsx b/packages/sdk-components-react-radix/src/select.tsx index 0ccf9c3d6147..00939e089664 100644 --- a/packages/sdk-components-react-radix/src/select.tsx +++ b/packages/sdk-components-react-radix/src/select.tsx @@ -6,6 +6,8 @@ import { type RefAttributes, useContext, type ComponentPropsWithRef, + useState, + useEffect, } from "react"; import { Root, @@ -26,8 +28,26 @@ import { export const Select: ForwardRefExoticComponent< ComponentPropsWithRef -> = forwardRef(({ value, defaultValue, ...props }, _ref) => { - return ; +> = forwardRef(({ defaultOpen, defaultValue, ...props }, _ref) => { + // open state + const currentOpen = props.open ?? defaultOpen ?? false; + const [open, setOpen] = useState(currentOpen); + // synchronize external value with local one when changed + useEffect(() => setOpen(currentOpen), [currentOpen]); + // value state + const currentValue = props.value ?? defaultValue ?? ""; + const [value, setValue] = useState(currentValue); + // synchronize external value with local one when changed + useEffect(() => setValue(currentValue), [currentValue]); + return ( + + ); }); export const SelectTrigger = forwardRef< diff --git a/packages/sdk-components-react-radix/src/switch.tsx b/packages/sdk-components-react-radix/src/switch.tsx index 55d94d6087c0..e5b4e6ec3973 100644 --- a/packages/sdk-components-react-radix/src/switch.tsx +++ b/packages/sdk-components-react-radix/src/switch.tsx @@ -3,25 +3,21 @@ import { type ComponentProps, type RefAttributes, forwardRef, + useEffect, + useState, } from "react"; import { Root, Thumb } from "@radix-ui/react-switch"; -import { useControllableState } from "@radix-ui/react-use-controllable-state"; export const Switch = forwardRef< HTMLButtonElement, ComponentProps >(({ defaultChecked, ...props }, ref) => { - const [checked, onCheckedChange] = useControllableState({ - prop: props.checked ?? defaultChecked ?? false, - defaultProp: false, - }); + const currentChecked = props.checked ?? defaultChecked ?? false; + const [checked, setChecked] = useState(currentChecked); + // synchronize external value with local one when changed + useEffect(() => setChecked(currentChecked), [currentChecked]); return ( - + ); }); diff --git a/packages/sdk-components-react-radix/src/tabs.template.tsx b/packages/sdk-components-react-radix/src/tabs.template.tsx index e54b44c75eff..81ca57ecf52c 100644 --- a/packages/sdk-components-react-radix/src/tabs.template.tsx +++ b/packages/sdk-components-react-radix/src/tabs.template.tsx @@ -73,7 +73,7 @@ export const meta: TemplateMeta = { "A set of panels with content that are displayed one at a time. Duplicate both a tab trigger and tab content to add more tabs. Triggers and content are connected according to their order in the Navigator.", order: 2, template: ( - + , "value" | "onValueChange"> & { - value?: string; - onValueChange?: (value: string) => void; - } ->(({ defaultValue, ...props }, ref) => { - const [value, onValueChange] = useControllableState({ - prop: props.value, - defaultProp: defaultValue ?? "", - onChange: props.onValueChange, - }); +export const Tabs = forwardRef>( + ({ defaultValue, ...props }, ref) => { + const currentValue = props.value ?? defaultValue ?? ""; + const [value, setValue] = useState(currentValue); + // synchronize external value with local one when changed + useEffect(() => setValue(currentValue), [currentValue]); - const handleValueChange = useCallback( - async (value: string) => { + const handleValueChange = useCallback(async (value: string) => { await interactionResponse(); - onValueChange(value); - }, - [onValueChange] - ); + setValue(value); + }, []); - return ( - - ); -}); + return ( + + ); + } +); export const TabsList = List; diff --git a/packages/sdk-components-react-radix/src/tooltip.tsx b/packages/sdk-components-react-radix/src/tooltip.tsx index cddc87639c72..0983deed7ffa 100644 --- a/packages/sdk-components-react-radix/src/tooltip.tsx +++ b/packages/sdk-components-react-radix/src/tooltip.tsx @@ -6,15 +6,21 @@ import { type ComponentPropsWithoutRef, type ReactNode, Children, + useState, + useEffect, } from "react"; export const Tooltip = forwardRef< HTMLDivElement, Omit, "defaultOpen"> >((props, _ref) => { + const currentOpen = props.open ?? false; + const [open, setOpen] = useState(currentOpen); + // synchronize external value with local one when changed + useEffect(() => setOpen(currentOpen), [currentOpen]); return ( - + ); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a2bcb128044..fe9a0dfacc67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2132,9 +2132,6 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.4 version: 1.2.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.3.0-canary-14898b6a9-20240318(react@18.3.0-canary-14898b6a9-20240318))(react@18.3.0-canary-14898b6a9-20240318) - '@radix-ui/react-use-controllable-state': - specifier: ^1.2.2 - version: 1.2.2(@types/react@18.2.79)(react@18.3.0-canary-14898b6a9-20240318) '@webstudio-is/css-engine': specifier: workspace:* version: link:../css-engine