diff --git a/plasmicpkgs/react-aria/src/ListBoxItemIdManager.ts b/plasmicpkgs/react-aria/src/OptionsItemIdManager.ts similarity index 97% rename from plasmicpkgs/react-aria/src/ListBoxItemIdManager.ts rename to plasmicpkgs/react-aria/src/OptionsItemIdManager.ts index 7d6a27f292e..16bc6b43ff9 100644 --- a/plasmicpkgs/react-aria/src/ListBoxItemIdManager.ts +++ b/plasmicpkgs/react-aria/src/OptionsItemIdManager.ts @@ -1,6 +1,6 @@ type Observer = (ids: string[]) => void; -export class ListBoxItemIdManager { +export class OptionsItemIdManager { private readonly _ids: Set = new Set(); private readonly _observers: Set = new Set(); diff --git a/plasmicpkgs/react-aria/src/contexts.tsx b/plasmicpkgs/react-aria/src/contexts.tsx index 20f1fdbbed0..1539d126d9a 100644 --- a/plasmicpkgs/react-aria/src/contexts.tsx +++ b/plasmicpkgs/react-aria/src/contexts.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ListBoxItemIdManager } from "./ListBoxItemIdManager"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { BaseCheckboxGroup } from "./registerCheckboxGroup"; import { BaseDialogTrigger } from "./registerDialogTrigger"; import type { BaseInput } from "./registerInput"; @@ -22,11 +22,17 @@ export const PlasmicTextFieldContext = React.createContext< >(undefined); export const PlasmicCheckboxGroupContext = React.createContext< - React.ComponentProps | undefined + | (React.ComponentProps & { + idManager: OptionsItemIdManager; + }) + | undefined >(undefined); export const PlasmicRadioGroupContext = React.createContext< - React.ComponentProps | undefined + | (React.ComponentProps & { + idManager: OptionsItemIdManager; + }) + | undefined >(undefined); export const PlasmicDialogTriggerContext = React.createContext< @@ -50,7 +56,7 @@ export const PlasmicPopoverTriggerContext = React.createContext< export const PlasmicListBoxContext = React.createContext< | { - idManager: ListBoxItemIdManager; + idManager: OptionsItemIdManager; } | undefined >(undefined); diff --git a/plasmicpkgs/react-aria/src/registerCheckbox.tsx b/plasmicpkgs/react-aria/src/registerCheckbox.tsx index a687ec63bf9..eb821c56224 100644 --- a/plasmicpkgs/react-aria/src/registerCheckbox.tsx +++ b/plasmicpkgs/react-aria/src/registerCheckbox.tsx @@ -1,10 +1,11 @@ import { PlasmicElement } from "@plasmicapp/host"; -import React from "react"; +import React, { useEffect, useState } from "react"; import type { CheckboxProps } from "react-aria-components"; import { Checkbox } from "react-aria-components"; import { getCommonProps, hasParent } from "./common"; import { PlasmicCheckboxGroupContext } from "./contexts"; import { + BaseControlContextData, CodeComponentMetaOverrides, HasControlContextData, Registerable, @@ -25,9 +26,13 @@ const CHECKBOX_VARIANTS = [ "selected" as const, ]; +export interface BaseCheckboxControlContextData extends BaseControlContextData { + idError?: string; +} + interface BaseCheckboxProps extends CheckboxProps, - HasControlContextData, + HasControlContextData, WithVariants { children: React.ReactNode; } @@ -36,17 +41,54 @@ const { variants, withObservedValues } = pickAriaComponentVariants(CHECKBOX_VARIANTS); export function BaseCheckbox(props: BaseCheckboxProps) { - const { children, plasmicUpdateVariant, setControlContextData, ...rest } = - props; + const { + children, + plasmicUpdateVariant, + setControlContextData, + value, + ...rest + } = props; const contextProps = React.useContext(PlasmicCheckboxGroupContext); + const isStandalone = !contextProps; + + const [registeredId, setRegisteredId] = useState(); + + useEffect(() => { + if (!contextProps?.idManager) { + return; + } + + const localId = contextProps.idManager.register(value); + setRegisteredId(localId); + + return () => { + contextProps.idManager.unregister(localId); + setRegisteredId(undefined); + }; + }, [value, contextProps?.idManager]); setControlContextData?.({ parent: contextProps, + idError: (() => { + if (value === undefined) { + return "Value must be defined"; + } + if (typeof value !== "string") { + return "Value must be a string"; + } + if (!value.trim()) { + return "Value must be defined"; + } + if (!isStandalone && value != registeredId) { + return "Value must be unique"; + } + return undefined; + })(), }); return ( <> - + {({ isHovered, isPressed, @@ -166,6 +208,12 @@ export function registerCheckbox( description: 'The value of the checkbox in "selected" state, used when submitting an HTML form.', defaultValueHint: "on", + validator: (_value, _props, ctx) => { + if (ctx?.idError) { + return ctx.idError; + } + return true; + }, }, isSelected: { type: "boolean", diff --git a/plasmicpkgs/react-aria/src/registerCheckboxGroup.tsx b/plasmicpkgs/react-aria/src/registerCheckboxGroup.tsx index 394edf5d593..88ec03b3e75 100644 --- a/plasmicpkgs/react-aria/src/registerCheckboxGroup.tsx +++ b/plasmicpkgs/react-aria/src/registerCheckboxGroup.tsx @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useEffect, useMemo, useState } from "react"; import type { CheckboxGroupProps } from "react-aria-components"; import { CheckboxGroup } from "react-aria-components"; import { getCommonProps } from "./common"; import { PlasmicCheckboxGroupContext } from "./contexts"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { CHECKBOX_COMPONENT_NAME, makeDefaultCheckboxChildren, @@ -11,17 +12,22 @@ import { DESCRIPTION_COMPONENT_NAME } from "./registerDescription"; import { LABEL_COMPONENT_NAME } from "./registerLabel"; import { CodeComponentMetaOverrides, - makeChildComponentName, + HasControlContextData, makeComponentName, Registerable, registerComponentHelper, } from "./utils"; import { pickAriaComponentVariants, WithVariants } from "./variant-utils"; +export interface BaseCheckboxControlContextData { + values: string[]; +} + const CHECKBOX_GROUP_VARIANTS = ["disabled" as const, "readonly" as const]; export interface BaseCheckboxGroupProps extends CheckboxGroupProps, + HasControlContextData, WithVariants { children?: React.ReactNode; } @@ -31,10 +37,25 @@ const { variants, withObservedValues } = pickAriaComponentVariants( ); export function BaseCheckboxGroup(props: BaseCheckboxGroupProps) { - const { children, plasmicUpdateVariant, ...rest } = props; + const { children, plasmicUpdateVariant, setControlContextData, ...rest } = + props; + const [ids, setIds] = useState([]); + const idManager = useMemo(() => new OptionsItemIdManager(), []); + + useEffect(() => { + setControlContextData?.({ + values: ids, + }); + }, [ids, setControlContextData]); + + useEffect(() => { + idManager.subscribe((_ids: string[]) => { + setIds(_ids); + }); + }, [idManager]); return ( - + {({ isDisabled, isReadOnly }) => withObservedValues( @@ -57,11 +78,6 @@ export function registerCheckboxGroup( loader?: Registerable, overrides?: CodeComponentMetaOverrides ) { - const thisName = makeChildComponentName( - overrides?.parentComponentName, - componentName - ); - registerComponentHelper( loader, BaseCheckboxGroup, @@ -150,10 +166,12 @@ export function registerCheckboxGroup( ], }, value: { - type: "array", + type: "choice", editOnly: true, uncontrolledProp: "defaultValue", description: "The current value", + options: (_props, ctx) => (ctx?.values ? Array.from(ctx.values) : []), + multiSelect: true, }, isInvalid: { displayName: "Invalid", diff --git a/plasmicpkgs/react-aria/src/registerComboBox.tsx b/plasmicpkgs/react-aria/src/registerComboBox.tsx index 783379d6c97..1aafcb1d541 100644 --- a/plasmicpkgs/react-aria/src/registerComboBox.tsx +++ b/plasmicpkgs/react-aria/src/registerComboBox.tsx @@ -11,7 +11,7 @@ import { PlasmicListBoxContext, PlasmicPopoverTriggerContext, } from "./contexts"; -import { ListBoxItemIdManager } from "./ListBoxItemIdManager"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { BUTTON_COMPONENT_NAME } from "./registerButton"; import { INPUT_COMPONENT_NAME } from "./registerInput"; import { LABEL_COMPONENT_NAME } from "./registerLabel"; @@ -85,7 +85,7 @@ export function BaseComboBox(props: BaseComboboxProps) { [className, plasmicUpdateVariant] ); - const idManager = useMemo(() => new ListBoxItemIdManager(), []); + const idManager = useMemo(() => new OptionsItemIdManager(), []); useEffect(() => { idManager.subscribe((ids: string[]) => { diff --git a/plasmicpkgs/react-aria/src/registerListBox.tsx b/plasmicpkgs/react-aria/src/registerListBox.tsx index 30760b017cd..cfde1e92466 100644 --- a/plasmicpkgs/react-aria/src/registerListBox.tsx +++ b/plasmicpkgs/react-aria/src/registerListBox.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Key, ListBox, ListBoxRenderProps } from "react-aria-components"; import { PlasmicListBoxContext } from "./contexts"; -import { ListBoxItemIdManager } from "./ListBoxItemIdManager"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { makeDefaultListBoxItemChildren, registerListBoxItem, @@ -70,7 +70,7 @@ export function BaseListBox(props: BaseListBoxProps) { const isStandalone = !context; const [ids, setIds] = useState([]); const idManager = useMemo( - () => context?.idManager ?? new ListBoxItemIdManager(), + () => context?.idManager ?? new OptionsItemIdManager(), [] ); diff --git a/plasmicpkgs/react-aria/src/registerRadio.tsx b/plasmicpkgs/react-aria/src/registerRadio.tsx index 4c1ca386bc0..772acce7525 100644 --- a/plasmicpkgs/react-aria/src/registerRadio.tsx +++ b/plasmicpkgs/react-aria/src/registerRadio.tsx @@ -1,11 +1,12 @@ import { PlasmicElement } from "@plasmicapp/host"; -import React from "react"; +import React, { useEffect, useState } from "react"; import type { RadioProps } from "react-aria-components"; import { Radio, RadioGroup } from "react-aria-components"; import { getCommonProps } from "./common"; import { PlasmicRadioGroupContext } from "./contexts"; import { LABEL_COMPONENT_NAME } from "./registerLabel"; import { + BaseControlContextData, CodeComponentMetaOverrides, HasControlContextData, Registerable, @@ -25,9 +26,13 @@ const RADIO_VARIANTS = [ "selected" as const, ]; +export interface BaseRadioControlContextData extends BaseControlContextData { + idError?: string; +} + export interface BaseRadioProps extends RadioProps, - HasControlContextData, + HasControlContextData, WithVariants { children: React.ReactNode; } @@ -36,17 +41,52 @@ const { variants, withObservedValues } = pickAriaComponentVariants(RADIO_VARIANTS); export function BaseRadio(props: BaseRadioProps) { - const { children, setControlContextData, plasmicUpdateVariant, ...rest } = - props; + const { + children, + setControlContextData, + plasmicUpdateVariant, + value, + ...rest + } = props; const contextProps = React.useContext(PlasmicRadioGroupContext); const isStandalone = !contextProps; + const [registeredId, setRegisteredId] = useState(""); + + useEffect(() => { + if (!contextProps?.idManager) { + return; + } + + const localId = contextProps.idManager.register(value); + setRegisteredId(localId); + + return () => { + contextProps.idManager.unregister(localId); + setRegisteredId(""); + }; + }, [value, contextProps?.idManager]); setControlContextData?.({ parent: contextProps, + idError: (() => { + if (value === undefined) { + return "Value must be defined"; + } + if (typeof value !== "string") { + return "Value must be a string"; + } + if (!value.trim()) { + return "Value must be defined"; + } + if (!isStandalone && value != registeredId) { + return "Value must be unique"; + } + return undefined; + })(), }); const radio = ( - + {({ isHovered, isPressed, @@ -141,6 +181,12 @@ export function registerRadio( type: "string", description: "The value of the input element, used when submitting an HTML form.", + validator: (_value, _props, ctx) => { + if (ctx?.idError) { + return ctx.idError; + } + return true; + }, }, }, trapsFocus: true, diff --git a/plasmicpkgs/react-aria/src/registerRadioGroup.tsx b/plasmicpkgs/react-aria/src/registerRadioGroup.tsx index d2ccff3717f..385aa5c7242 100644 --- a/plasmicpkgs/react-aria/src/registerRadioGroup.tsx +++ b/plasmicpkgs/react-aria/src/registerRadioGroup.tsx @@ -1,13 +1,15 @@ -import React from "react"; +import React, { useEffect, useMemo, useState } from "react"; import type { RadioGroupProps } from "react-aria-components"; import { RadioGroup } from "react-aria-components"; import { getCommonProps } from "./common"; import { PlasmicRadioGroupContext } from "./contexts"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { DESCRIPTION_COMPONENT_NAME } from "./registerDescription"; import { LABEL_COMPONENT_NAME } from "./registerLabel"; import { makeDefaultRadioChildren, registerRadio } from "./registerRadio"; import { CodeComponentMetaOverrides, + HasControlContextData, Registerable, makeChildComponentName, makeComponentName, @@ -15,10 +17,15 @@ import { } from "./utils"; import { WithVariants, pickAriaComponentVariants } from "./variant-utils"; +export interface BaseRadioGroupControlContextData { + values: string[]; +} + const RADIO_GROUP_VARIANTS = ["disabled" as const, "readonly" as const]; export interface BaseRadioGroupProps extends RadioGroupProps, + HasControlContextData, WithVariants { children: React.ReactNode; } @@ -27,10 +34,26 @@ const { variants, withObservedValues } = pickAriaComponentVariants(RADIO_GROUP_VARIANTS); export function BaseRadioGroup(props: BaseRadioGroupProps) { - const { children, plasmicUpdateVariant, ...rest } = props; + const { children, plasmicUpdateVariant, setControlContextData, ...rest } = + props; + + const [ids, setIds] = useState([]); + const idManager = useMemo(() => new OptionsItemIdManager(), []); + + useEffect(() => { + setControlContextData?.({ + values: ids, + }); + }, [ids, setControlContextData]); + + useEffect(() => { + idManager.subscribe((_ids: string[]) => { + setIds(_ids); + }); + }, [idManager]); return ( - + {({ isDisabled, isReadOnly }) => withObservedValues( @@ -139,11 +162,13 @@ export function registerRadioGroup( ], }, value: { - type: "string", + type: "choice", editOnly: true, displayName: "Initial value", uncontrolledProp: "defaultValue", description: "The current value", + options: (_props, ctx) => (ctx?.values ? Array.from(ctx.values) : []), + multiSelect: false, }, isInvalid: { displayName: "Invalid", diff --git a/plasmicpkgs/react-aria/src/registerSelect.tsx b/plasmicpkgs/react-aria/src/registerSelect.tsx index a72c2fe0d56..06e3360434f 100644 --- a/plasmicpkgs/react-aria/src/registerSelect.tsx +++ b/plasmicpkgs/react-aria/src/registerSelect.tsx @@ -11,7 +11,7 @@ import { PlasmicListBoxContext, PlasmicPopoverTriggerContext, } from "./contexts"; -import { ListBoxItemIdManager } from "./ListBoxItemIdManager"; +import { OptionsItemIdManager } from "./OptionsItemIdManager"; import { BUTTON_COMPONENT_NAME } from "./registerButton"; import { LABEL_COMPONENT_NAME } from "./registerLabel"; import { LIST_BOX_COMPONENT_NAME } from "./registerListBox"; @@ -115,7 +115,7 @@ export function BaseSelect(props: BaseSelectProps) { "aria-label": ariaLabel, } = props; - const idManager = useMemo(() => new ListBoxItemIdManager(), []); + const idManager = useMemo(() => new OptionsItemIdManager(), []); useEffect(() => { idManager.subscribe((ids: string[]) => {