Skip to content

Commit

Permalink
[PLA-11369]: Aria Checkbox Group default values could use a better pr…
Browse files Browse the repository at this point in the history
…op type (#229)

* feat(plasmicpkgs/react-aria): changed CheckboxGroup default values prop type to choice

* feat(plasmicpkgs/react-aria): added validator for Value prop in CheckboxGroup

* chore(plasmicpkgs/react-aria): fixed typescript errors for BaseCheckboxControlContextData interface

* chore(plasmicpkgs/react-aria): added contextProps?.idManager to useEffect dependency in react-aria Checkbox

* feat(plasmicpkgs/react-aria): changed RadioGroup default values prop type to choice

---------

Co-authored-by: Asim <[email protected]>
GitOrigin-RevId: a4d173ee1e7f6fc3334f4ea7d72c29d405089f1c
  • Loading branch information
2 people authored and actions-user committed Dec 13, 2024
1 parent ba3b869 commit a3994c9
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type Observer = (ids: string[]) => void;

export class ListBoxItemIdManager {
export class OptionsItemIdManager {
private readonly _ids: Set<string> = new Set();
private readonly _observers: Set<Observer> = new Set();

Expand Down
14 changes: 10 additions & 4 deletions plasmicpkgs/react-aria/src/contexts.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,11 +22,17 @@ export const PlasmicTextFieldContext = React.createContext<
>(undefined);

export const PlasmicCheckboxGroupContext = React.createContext<
React.ComponentProps<typeof BaseCheckboxGroup> | undefined
| (React.ComponentProps<typeof BaseCheckboxGroup> & {
idManager: OptionsItemIdManager;
})
| undefined
>(undefined);

export const PlasmicRadioGroupContext = React.createContext<
React.ComponentProps<typeof BaseRadioGroup> | undefined
| (React.ComponentProps<typeof BaseRadioGroup> & {
idManager: OptionsItemIdManager;
})
| undefined
>(undefined);

export const PlasmicDialogTriggerContext = React.createContext<
Expand All @@ -50,7 +56,7 @@ export const PlasmicPopoverTriggerContext = React.createContext<

export const PlasmicListBoxContext = React.createContext<
| {
idManager: ListBoxItemIdManager;
idManager: OptionsItemIdManager;
}
| undefined
>(undefined);
Expand Down
58 changes: 53 additions & 5 deletions plasmicpkgs/react-aria/src/registerCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,9 +26,13 @@ const CHECKBOX_VARIANTS = [
"selected" as const,
];

export interface BaseCheckboxControlContextData extends BaseControlContextData {
idError?: string;
}

interface BaseCheckboxProps
extends CheckboxProps,
HasControlContextData,
HasControlContextData<BaseCheckboxControlContextData>,
WithVariants<typeof CHECKBOX_VARIANTS> {
children: React.ReactNode;
}
Expand All @@ -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<string | undefined>();

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 (
<>
<Checkbox {...rest}>
<Checkbox {...rest} value={registeredId} key={registeredId}>
{({
isHovered,
isPressed,
Expand Down Expand Up @@ -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",
Expand Down
38 changes: 28 additions & 10 deletions plasmicpkgs/react-aria/src/registerCheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<BaseCheckboxControlContextData>,
WithVariants<typeof CHECKBOX_GROUP_VARIANTS> {
children?: React.ReactNode;
}
Expand All @@ -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<string[]>([]);
const idManager = useMemo(() => new OptionsItemIdManager(), []);

useEffect(() => {
setControlContextData?.({
values: ids,
});
}, [ids, setControlContextData]);

useEffect(() => {
idManager.subscribe((_ids: string[]) => {
setIds(_ids);
});
}, [idManager]);

return (
<PlasmicCheckboxGroupContext.Provider value={rest}>
<PlasmicCheckboxGroupContext.Provider value={{ ...rest, idManager }}>
<CheckboxGroup {...rest}>
{({ isDisabled, isReadOnly }) =>
withObservedValues(
Expand All @@ -57,11 +78,6 @@ export function registerCheckboxGroup(
loader?: Registerable,
overrides?: CodeComponentMetaOverrides<typeof BaseCheckboxGroup>
) {
const thisName = makeChildComponentName(
overrides?.parentComponentName,
componentName
);

registerComponentHelper(
loader,
BaseCheckboxGroup,
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions plasmicpkgs/react-aria/src/registerComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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[]) => {
Expand Down
4 changes: 2 additions & 2 deletions plasmicpkgs/react-aria/src/registerListBox.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -70,7 +70,7 @@ export function BaseListBox(props: BaseListBoxProps) {
const isStandalone = !context;
const [ids, setIds] = useState<string[]>([]);
const idManager = useMemo(
() => context?.idManager ?? new ListBoxItemIdManager(),
() => context?.idManager ?? new OptionsItemIdManager(),
[]
);

Expand Down
56 changes: 51 additions & 5 deletions plasmicpkgs/react-aria/src/registerRadio.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,9 +26,13 @@ const RADIO_VARIANTS = [
"selected" as const,
];

export interface BaseRadioControlContextData extends BaseControlContextData {
idError?: string;
}

export interface BaseRadioProps
extends RadioProps,
HasControlContextData,
HasControlContextData<BaseRadioControlContextData>,
WithVariants<typeof RADIO_VARIANTS> {
children: React.ReactNode;
}
Expand All @@ -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<string>("");

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 = (
<Radio {...rest}>
<Radio {...rest} value={registeredId} key={registeredId}>
{({
isHovered,
isPressed,
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit a3994c9

Please sign in to comment.