chore: remove headless radio and use react-aria component instead (#34312)

## Description

- Remove headless radio and use react-aria component instead
- Create ErrorMessage component

Fixes #27677 

## Automation

/ok-to-test tags="@tag.Anvil"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9567388261>
> Commit: 62d1153caa8bf03d827f88593c9dfaf3121091ee
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9567388261&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Anvil`

<!-- end of auto-generated comment: Cypress test results  -->



## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added `ErrorMessage` component for displaying error messages with
specific styling.
- Introduced `isDisabled` prop for `Label` and `ToggleGroup` components.
- Updated `RadioGroup` to accept an items array for easier
configuration.

- **Bug Fixes**
- Improved conditional rendering in `Label` component to prevent issues
when `text` and `contextualHelp` are both falsy.

- **Refactor**
  - Removed `Radio` component export from design system.
- Restructured import statements and prop declarations for `Checkbox`
and `RadioGroup`.

- **Style**
- Updated styles for `RadioGroup` and `ToggleGroup` components for
better state handling (disabled, hovered, selected).

- **Documentation**
- Updated Storybook stories for `RadioGroup` to reflect changes in
component usage.

- **Tests**
  - Adjusted `RadioGroup.test.tsx` to test new items array prop.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Valera Melnikov 2024-06-19 11:08:42 +03:00 committed by GitHub
parent f718bfbb6b
commit 36372468c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 316 additions and 445 deletions

View File

@ -1,70 +0,0 @@
import { useRadio } from "@react-aria/radio";
import { mergeProps } from "@react-aria/utils";
import { useFocusRing } from "@react-aria/focus";
import { useHover } from "@react-aria/interactions";
import { useFocusableRef } from "@react-spectrum/utils";
import type { SpectrumRadioProps } from "@react-types/radio";
import React, { forwardRef, useContext, useRef } from "react";
import { useVisuallyHidden } from "@react-aria/visually-hidden";
import type { FocusableRef, StyleProps } from "@react-types/shared";
import { RadioContext } from "./context";
import type { RadioGroupContext } from "./context";
export interface RadioProps extends Omit<SpectrumRadioProps, keyof StyleProps> {
className?: string;
labelPosition?: "start" | "end";
}
export type RadioRef = FocusableRef<HTMLLabelElement>;
const _Radio = (props: RadioProps, ref: RadioRef) => {
const {
autoFocus,
children,
className,
isDisabled: isDisabledProp = false,
labelPosition = "end",
} = props;
const inputRef = useRef<HTMLInputElement>(null);
const domRef = useFocusableRef(ref, inputRef);
const { visuallyHiddenProps } = useVisuallyHidden();
const radioGroupProps = useContext(RadioContext) as RadioGroupContext;
const { state, validationState } = radioGroupProps;
const isDisabled = isDisabledProp || radioGroupProps.isDisabled;
const { hoverProps, isHovered } = useHover({ isDisabled });
const { focusProps, isFocusVisible } = useFocusRing({ autoFocus });
const { inputProps } = useRadio(
{
...props,
...radioGroupProps,
isDisabled,
},
state,
inputRef,
);
return (
<label
{...hoverProps}
className={className}
data-disabled={Boolean(isDisabled) ? "" : undefined}
data-focused={isFocusVisible ? "" : undefined}
data-hovered={isHovered ? "" : undefined}
data-invalid={validationState === "invalid" ? "" : undefined}
data-label=""
data-label-position={labelPosition}
data-state={state.selectedValue === props.value ? "selected" : undefined}
ref={domRef}
>
<input
{...mergeProps(inputProps, visuallyHiddenProps, focusProps)}
ref={inputRef}
/>
<span aria-hidden="true" data-icon="" role="presentation" />
{children}
</label>
);
};
export const Radio = forwardRef(_Radio);

View File

@ -1,61 +0,0 @@
import React, { forwardRef, useRef } from "react";
import { Field } from "@design-system/headless";
import { useDOMRef } from "@react-spectrum/utils";
import type { DOMRef } from "@react-types/shared";
import { useRadioGroup } from "@react-aria/radio";
import { useRadioGroupState } from "@react-stately/radio";
import { RadioContext } from "./context";
import type { RadioGroupProps } from "./types";
import { useGroupOrientation } from "../../../hooks";
export type RadioGroupRef = DOMRef<HTMLDivElement>;
const _RadioGroup = (props: RadioGroupProps, ref: RadioGroupRef) => {
const {
children,
fieldClassName,
isDisabled = false,
validationState,
} = props;
const domRef = useDOMRef(ref);
const state = useRadioGroupState(props);
const { descriptionProps, errorMessageProps, labelProps, radioGroupProps } =
useRadioGroup(props, state);
const containerRef = useRef<HTMLDivElement>(null);
const { orientation } = useGroupOrientation(
{ orientation: props.orientation },
containerRef,
);
return (
<Field
{...props}
descriptionProps={descriptionProps}
errorMessageProps={errorMessageProps}
fieldType="field-group"
labelProps={labelProps}
ref={domRef}
wrapperClassName={fieldClassName}
>
<div
{...radioGroupProps}
data-field-group=""
data-orientation={orientation}
ref={containerRef}
>
<RadioContext.Provider
value={{
validationState,
state,
isDisabled,
}}
>
{children}
</RadioContext.Provider>
</div>
</Field>
);
};
export const RadioGroup = forwardRef(_RadioGroup);

View File

@ -1,15 +0,0 @@
import React, { useContext } from "react";
import type { RadioGroupState } from "@react-stately/radio";
export interface RadioGroupContext {
name?: string;
validationState?: "valid" | "invalid";
state: RadioGroupState;
isDisabled?: boolean;
}
export const RadioContext = React.createContext<RadioGroupContext | null>(null);
export function useRadioProvider() {
return useContext(RadioContext) as RadioGroupContext;
}

View File

@ -1,5 +0,0 @@
export { Radio } from "./Radio";
export { RadioGroup } from "./RadioGroup";
export type { RadioGroupProps } from "./types";
export type { RadioProps, RadioRef } from "./Radio";
export type { RadioGroupRef } from "./RadioGroup";

View File

@ -1,13 +0,0 @@
import type { StyleProps } from "@react-types/shared";
import type { SpectrumRadioGroupProps } from "@react-types/radio";
import type { TextInputProps } from "../../TextInput";
export interface RadioGroupProps
extends Omit<
SpectrumRadioGroupProps,
keyof StyleProps | "labelPosition" | "labelAlign" | "isEmphasized"
>,
Pick<
TextInputProps,
"fieldClassName" | "labelClassName" | "helpTextClassName" | "className"
> {}

View File

@ -1,28 +0,0 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { RadioGroup, Radio } from "@design-system/headless";
/**
* A Radio group is a group of radio buttons that are related to each other in some way. For example, they may all represent a single question on a survey. The Radio group component is a headless component that provides the logic and accessibility implementation for a group of radio buttons.
*
* Note: The `<input="radio" />` is visually hidden by default. Use the `<span data-icon />` to render custom looking radio.
*/
const meta: Meta<typeof RadioGroup> = {
component: RadioGroup,
title: "Design-system/headless/RadioGroup",
subcomponents: {
//@ts-expect-error: don't need props to pass here
Radio,
},
render: (args) => (
<RadioGroup label="Radio group" {...args}>
<Radio value="option 1">Option 1</Radio>
<Radio value="option 2">Option 2</Radio>
</RadioGroup>
),
};
export default meta;
type Story = StoryObj<typeof RadioGroup>;
export const Main: Story = {};

View File

@ -2,7 +2,6 @@
export * from "./components/Field";
export * from "./components/Icon";
export * from "./components/Tooltip";
export * from "./components/Radio";
export * from "./components/TextInput";
export * from "./components/TextArea";
export * from "./components/Popover";

View File

@ -2,13 +2,8 @@ import React, { forwardRef } from "react";
import { Checkbox as HeadlessCheckbox } from "react-aria-components";
import { Text, Icon } from "@design-system/widgets";
import styles from "./styles.module.css";
import type { POSITION } from "@design-system/widgets";
import type { ForwardedRef } from "react";
import type { CheckboxProps as HeadlessCheckboxProps } from "react-aria-components";
export interface CheckboxProps extends HeadlessCheckboxProps {
labelPosition?: keyof typeof POSITION;
}
import type { CheckboxProps } from "./types";
const _Checkbox = (
props: CheckboxProps,

View File

@ -1,2 +1,2 @@
export { Checkbox } from "./Checkbox";
export type { CheckboxProps } from "./Checkbox";
export type { CheckboxProps } from "./types";

View File

@ -0,0 +1,6 @@
import type { CheckboxProps as HeadlessCheckboxProps } from "react-aria-components";
import type { POSITION } from "../../../shared";
export interface CheckboxProps extends HeadlessCheckboxProps {
labelPosition?: keyof typeof POSITION;
}

View File

@ -0,0 +1,21 @@
import React from "react";
import { Text } from "@design-system/widgets";
import styles from "./styles.module.css";
import type { ErrorMessageProps } from "./types";
export const ErrorMessage = (props: ErrorMessageProps) => {
const { text } = props;
if (!Boolean(text)) return null;
return (
<Text
className={styles.errorMessage}
color="negative"
lineClamp={2}
size="footnote"
>
{text}
</Text>
);
};

View File

@ -0,0 +1 @@
export * from "./ErrorMessage";

View File

@ -0,0 +1,3 @@
.errorMessage {
margin-block-start: var(--inner-spacing-2);
}

View File

@ -0,0 +1,3 @@
export interface ErrorMessageProps {
text?: string;
}

View File

@ -6,11 +6,15 @@ import styles from "./styles.module.css";
import type { LabelProps } from "./types";
export const Label = (props: LabelProps) => {
const { className, contextualHelp, isRequired, text, ...rest } = props;
const { className, contextualHelp, isDisabled, isRequired, text, ...rest } =
props;
if (!Boolean(text) && !Boolean(contextualHelp)) return null;
return (
<HeadlessLabel
className={clsx(className, styles.label)}
data-disabled={isDisabled}
data-field-label-wrapper
elementType="label"
{...rest}

View File

@ -4,6 +4,11 @@
height: var(--sizing-3);
margin-block-end: var(--inner-spacing-3);
gap: var(--inner-spacing-1);
&[data-disabled="true"] {
cursor: not-allowed;
opacity: var(--opacity-disabled);
}
}
.necessityIndicator {

View File

@ -4,4 +4,5 @@ export interface LabelProps extends HeadlessLabelProps {
text?: string;
contextualHelp?: string;
isRequired?: boolean;
isDisabled?: boolean;
}

View File

@ -1,45 +0,0 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { Radio, RadioGroup } from "@design-system/widgets";
import { StoryGrid, DataAttrWrapper } from "@design-system/storybook";
const meta: Meta<typeof Radio> = {
component: Radio,
title: "Design System/Widgets/Radio",
};
export default meta;
type Story = StoryObj<typeof Radio>;
const states = ["", "data-hovered", "data-focused", "data-disabled"];
export const LightMode: Story = {
render: () => (
<StoryGrid>
{states.map((state) => (
<>
<RadioGroup isDisabled={state === "data-disabled"}>
<DataAttrWrapper attr={state} key={state}>
<Radio value={`${state}-unchecked`}>unchecked {state}</Radio>
</DataAttrWrapper>
</RadioGroup>
<RadioGroup
defaultValue={`${state}-checked`}
isDisabled={state === "data-disabled"}
>
<DataAttrWrapper attr={state} key={state}>
<Radio value={`${state}-checked`}>checked {state}</Radio>
</DataAttrWrapper>
</RadioGroup>
</>
))}
</StoryGrid>
),
};
export const DarkMode: Story = Object.assign({}, LightMode);
DarkMode.parameters = {
colorMode: "dark",
};

View File

@ -1 +0,0 @@
export * from "./src";

View File

@ -1,31 +0,0 @@
import clsx from "clsx";
import React, { forwardRef } from "react";
import type {
RadioRef as HeadlessRadioRef,
RadioProps as HeadlessRadioProps,
} from "@design-system/headless";
import { Radio as HeadlessRadio } from "@design-system/headless";
import { Text } from "@design-system/widgets";
import radioStyles from "./styles.module.css";
import { inlineLabelStyles } from "../../../styles";
export type RadioProps = HeadlessRadioProps;
const _Radio = (props: RadioProps, ref: HeadlessRadioRef) => {
const { children, labelPosition = "end", ...rest } = props;
return (
<HeadlessRadio
className={clsx(radioStyles.radio, inlineLabelStyles["inline-label"])}
labelPosition={labelPosition}
ref={ref}
{...rest}
>
{Boolean(children) && <Text>{children}</Text>}
</HeadlessRadio>
);
};
export const Radio = forwardRef(_Radio);

View File

@ -1,2 +0,0 @@
export { Radio } from "./Radio";
export type { RadioProps } from "./Radio";

View File

@ -0,0 +1,41 @@
import React from "react";
import type { Checkbox } from "@design-system/widgets";
import { RadioGroup } from "@design-system/widgets";
import type { Meta, StoryObj } from "@storybook/react";
import { StoryGrid, DataAttrWrapper } from "@design-system/storybook";
const meta: Meta<typeof RadioGroup> = {
component: RadioGroup,
title: "Design System/Widgets/RadioGroup",
};
export default meta;
type Story = StoryObj<typeof Checkbox>;
const states = ["", "data-hovered", "data-focus-visible", "data-disabled"];
const items = [{ label: "Value 1", value: "value-1" }];
export const LightMode: Story = {
render: () => (
<StoryGrid>
{states.map((state) => (
<DataAttrWrapper attr={state} key={state} target="label">
<RadioGroup items={items} />
</DataAttrWrapper>
))}
{states.map((state) => (
<DataAttrWrapper attr={state} key={state} target="label">
<RadioGroup defaultValue="value-1" items={items} />
</DataAttrWrapper>
))}
</StoryGrid>
),
};
export const DarkMode: Story = Object.assign({}, LightMode);
DarkMode.parameters = {
colorMode: "dark",
};

View File

@ -1,35 +1,63 @@
import React, { forwardRef } from "react";
import React, { forwardRef, useRef } from "react";
import { RadioGroup as HeadlessRadioGroup, Radio } from "react-aria-components";
import {
Label,
Flex,
Text,
ErrorMessage,
useGroupOrientation,
} from "@design-system/widgets";
import styles from "./styles.module.css";
import type { ForwardedRef } from "react";
import type { RadioGroupProps } from "./types";
import type {
RadioGroupRef as HeadlessRadioGroupRef,
RadioGroupProps as HeadlessRadioGroupProps,
} from "@design-system/headless";
import { getTypographyClassName } from "@design-system/theming";
import { RadioGroup as HeadlessRadioGroup } from "@design-system/headless";
import { fieldStyles } from "../../../styles";
import { ContextualHelp } from "../../ContextualHelp";
export interface RadioGroupProps extends HeadlessRadioGroupProps {
className?: string;
}
const _RadioGroup = (props: RadioGroupProps, ref: HeadlessRadioGroupRef) => {
const { contextualHelp: contextualHelpProp, ...rest } = props;
const contextualHelp = Boolean(contextualHelpProp) && (
<ContextualHelp contextualHelp={contextualHelpProp} />
const _RadioGroup = (
props: RadioGroupProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
const {
contextualHelp,
errorMessage,
isDisabled,
isRequired,
items,
label,
...rest
} = props;
const containerRef = useRef<HTMLDivElement>(null);
const { orientation } = useGroupOrientation(
{ orientation: props.orientation },
containerRef,
);
return (
<HeadlessRadioGroup
contextualHelp={contextualHelp}
fieldClassName={fieldStyles.field}
helpTextClassName={getTypographyClassName("footnote")}
labelClassName={getTypographyClassName("caption")}
className={styles.radioGroup}
isDisabled={isDisabled}
ref={ref}
{...rest}
/>
>
<Label
contextualHelp={contextualHelp}
isDisabled={isDisabled}
isRequired={isRequired}
text={label}
/>
<Flex
direction={orientation === "vertical" ? "column" : "row"}
gap={orientation === "vertical" ? "spacing-2" : "spacing-4"}
isInner
ref={containerRef}
wrap="wrap"
>
{items.map(({ label, value, ...rest }, index) => (
<Radio className={styles.radio} key={index} value={value} {...rest}>
<Text lineClamp={1}>{label}</Text>
</Radio>
))}
</Flex>
<ErrorMessage text={errorMessage} />
</HeadlessRadioGroup>
);
};

View File

@ -1,2 +1,2 @@
export { RadioGroup } from "./RadioGroup";
export type { RadioGroupProps } from "./RadioGroup";
export type { RadioGroupProps } from "./types";

View File

@ -1,8 +1,29 @@
.radio {
position: relative;
padding-inline-start: calc(var(--sizing-5) + var(--inner-spacing-2));
.radioGroup {
display: flex;
flex-direction: column;
width: 100%;
[data-icon] {
&[data-orientation="vertical"] {
align-items: start;
.radio {
margin-inline-end: auto;
}
}
&[data-disabled] {
cursor: not-allowed;
}
}
.radio {
display: flex;
align-items: center;
gap: var(--inner-spacing-2);
cursor: pointer;
min-width: fit-content;
&:before {
--radio-border-width: var(--border-width-2);
--radio-border-color: var(--color-bd-neutral);
/* Note: we are using box-shadow as the border to avoid the border from
@ -10,12 +31,7 @@
--radio-box-shadow: 0px 0px 0px var(--radio-border-width)
var(--radio-border-color) inset;
/**
Checkbox icon are positioned absolutely because we need to align the elements along the baseline
but icon takes more space than the text content.
*/
position: absolute;
left: 0;
content: "";
width: var(--sizing-5);
height: var(--sizing-5);
box-shadow: var(--radio-box-shadow);
@ -28,7 +44,7 @@
flex-shrink: 0;
}
&[data-hovered]:not([data-disabled]) [data-icon] {
&[data-hovered]:not([data-disabled="true"]):before {
--radio-border-color: var(--color-bd-neutral-hover);
}
@ -37,14 +53,14 @@
* CHECKED AND INDETERMINATE - BUT NOT DISABLED
*-----------------------------------------------------------------------------
*/
&[data-state="selected"] [data-icon] {
&[data-selected="true"]:before {
--radio-border-color: var(--color-bg-accent);
--radio-box-shadow: 0px 0px 0px 4px var(--color-bg-accent) inset;
background: var(--color-fg-on-accent);
}
&[data-hovered][data-state="selected"]:not([data-disabled]) [data-icon] {
&[data-hovered][data-selected="true"]:not([data-disabled="true"]):before {
--radio-border-color: var(--color-bg-accent-hover);
--radio-box-shadow: 0px 0px 0px 4px var(--color-bg-accent-hover) inset;
@ -56,7 +72,7 @@
* FOCUS
*-----------------------------------------------------------------------------
*/
&[data-focused] [data-icon] {
&[data-focus-visible]:before {
box-shadow:
var(--radio-box-shadow),
0 0 0 2px var(--color-bg),
@ -68,11 +84,29 @@
* ERROR ( INVALID )
*-----------------------------------------------------------------------------
*/
&[data-invalid] [data-icon] {
&[data-invalid]:before {
--radio-border-color: var(--color-bd-negative);
}
&[data-hovered][data-invalid] [data-icon] {
&[data-hovered][data-invalid]:before {
--radio-border-color: var(--color-bd-negative-hover);
}
&[data-invalid][data-selected="true"]:before {
--radio-box-shadow: 0px 0px 0px 4px var(--color-bg-negative) inset;
}
&[data-hovered][data-invalid][data-selected="true"]:before {
--radio-box-shadow: 0px 0px 0px 4px var(--color-bg-negative-hover) inset;
}
/**
* ----------------------------------------------------------------------------
* DISABLED
*-----------------------------------------------------------------------------
*/
&[data-disabled] {
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
}

View File

@ -0,0 +1,34 @@
import type { RadioGroupProps as HeadlessRadioGroupProps } from "react-aria-components";
import type { ORIENTATION } from "../../../shared";
interface RadioGroupItemProps {
value: string;
label?: string;
isSelected?: boolean;
isDisabled?: boolean;
index?: number;
}
export interface RadioGroupProps extends HeadlessRadioGroupProps {
/**
* A ContextualHelp element to place next to the label.
*/
contextualHelp?: string;
/**
* The content to display as the label.
*/
label?: string;
/**
* Radio that belong to this group.
*/
items: RadioGroupItemProps[];
/**
* The axis the checkboxes should align with.
* @default 'horizontal'
*/
orientation?: keyof typeof ORIENTATION;
/**
* An error message for the field.
*/
errorMessage?: string;
}

View File

@ -1,6 +1,6 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { RadioGroup, Radio, Flex } from "@design-system/widgets";
import { RadioGroup, Flex } from "@design-system/widgets";
/**
* Radio group is a component that allows users to select one option from a set of options.
@ -13,17 +13,18 @@ const meta: Meta<typeof RadioGroup> = {
export default meta;
type Story = StoryObj<typeof RadioGroup>;
const items = [
{ label: "Value 1", value: "value-1" },
{ label: "Value 2", value: "value-2" },
];
export const Main: Story = {
args: {
label: "Radio Group",
defaultValue: "value-1",
items: items,
},
render: (args) => (
<RadioGroup {...args}>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
),
render: (args) => <RadioGroup {...args} />,
};
/**
@ -32,14 +33,8 @@ export const Main: Story = {
export const Orientation: Story = {
render: () => (
<Flex direction="column" gap="spacing-4">
<RadioGroup>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
<RadioGroup orientation="vertical">
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
<RadioGroup items={items} />
<RadioGroup items={items} orientation="vertical" />
</Flex>
),
};
@ -49,13 +44,9 @@ export const Disabled: Story = {
label: "Radio Group",
defaultValue: "value-1",
isDisabled: true,
items: items,
},
render: (args) => (
<RadioGroup {...args}>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
),
render: (args) => <RadioGroup {...args} />,
};
export const Required: Story = {
@ -63,25 +54,17 @@ export const Required: Story = {
label: "Radio Group",
defaultValue: "value-1",
isRequired: true,
items: items,
},
render: (args) => (
<RadioGroup {...args}>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
),
render: (args) => <RadioGroup {...args} />,
};
export const Invalid: Story = {
args: {
label: "Radio Group",
validationState: "invalid",
isInvalid: true,
errorMessage: "This is a error message",
items: items,
},
render: (args) => (
<RadioGroup {...args}>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>
),
render: (args) => <RadioGroup {...args} />,
};

View File

@ -2,17 +2,17 @@ import React from "react";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";
import { render, screen } from "@testing-library/react";
import { RadioGroup } from "../";
import { Radio } from "../../Radio";
import { RadioGroup } from "@design-system/widgets";
describe("@design-system/widgets/RadioGroup", () => {
const items = [
{ label: "Value 1", value: "value-1" },
{ label: "Value 2", value: "value-2" },
];
it("should render the Radio group", async () => {
const { container } = render(
<RadioGroup label="Radio Group">
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>,
<RadioGroup items={items} label="Radio Group" />,
);
expect(screen.getByText("Value 1")).toBeInTheDocument();
@ -43,11 +43,11 @@ describe("@design-system/widgets/RadioGroup", () => {
it("should support custom props", () => {
render(
<RadioGroup data-testid="t--radio-group" label="Radio Group Label">
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
<Radio value="value-3">Value 3</Radio>
</RadioGroup>,
<RadioGroup
data-testid="t--radio-group"
items={items}
label="Radio Group Label"
/>,
);
const radioGroup = screen.getByTestId("t--radio-group");
@ -56,10 +56,7 @@ describe("@design-system/widgets/RadioGroup", () => {
it("should render checked checkboxes when value is passed", () => {
render(
<RadioGroup label="Radio Group Label" value="value-1">
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>,
<RadioGroup items={items} label="Radio Group Label" value="value-1" />,
);
const options = screen.getAllByRole("radio");
@ -71,10 +68,11 @@ describe("@design-system/widgets/RadioGroup", () => {
const onChangeSpy = jest.fn();
render(
<RadioGroup label="Radio Group Label" onChange={onChangeSpy}>
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>,
<RadioGroup
items={items}
label="Radio Group Label"
onChange={onChangeSpy}
/>,
);
const options = screen.getAllByRole("radio");
@ -83,12 +81,7 @@ describe("@design-system/widgets/RadioGroup", () => {
});
it("should be able to render disabled checkboxes", () => {
render(
<RadioGroup isDisabled label="Radio Group Label">
<Radio value="value-1">Value 1</Radio>
<Radio value="value-2">Value 2</Radio>
</RadioGroup>,
);
render(<RadioGroup isDisabled items={items} label="Radio Group Label" />);
const options = screen.getAllByRole("radio");
expect(options[0]).toBeDisabled();

View File

@ -6,7 +6,6 @@ import {
RadioGroup,
Switch,
Checkbox,
Radio,
} from "@design-system/widgets";
import { StoryGrid } from "@design-system/storybook";
@ -28,11 +27,7 @@ const items = [
export const LightMode: Story = {
render: () => (
<StoryGrid>
<RadioGroup defaultValue="1">
<Radio value="1">Option 1</Radio>
<Radio value="2">Option 2</Radio>
<Radio value="3">Opion 3</Radio>
</RadioGroup>
<RadioGroup defaultValue="1" items={items} />
<ToggleGroup defaultValue={["1"]} items={items}>
{({ label, value }) => (
<Checkbox key={value} value={value}>

View File

@ -1,7 +1,11 @@
import { useGroupOrientation } from "@design-system/headless/src/hooks";
import React, { forwardRef, useRef } from "react";
import { CheckboxGroup as HeadlessToggleGroup } from "react-aria-components";
import { Text, Label, Flex } from "@design-system/widgets";
import {
ErrorMessage,
Label,
Flex,
useGroupOrientation,
} from "@design-system/widgets";
import styles from "./styles.module.css";
import type { ForwardedRef } from "react";
import type { ToggleGroupProps } from "./types";
@ -14,6 +18,7 @@ const _ToggleGroup = (
children,
contextualHelp,
errorMessage,
isDisabled,
isRequired,
items,
label,
@ -29,35 +34,26 @@ const _ToggleGroup = (
<HeadlessToggleGroup
className={styles.toggleGroup}
data-orientation={orientation}
isDisabled={isDisabled}
ref={ref}
{...rest}
>
{(Boolean(label) || Boolean(contextualHelp)) && (
<Label
className={styles.label}
contextualHelp={contextualHelp}
isRequired={isRequired}
text={label}
/>
)}
<Label
contextualHelp={contextualHelp}
isDisabled={isDisabled}
isRequired={isRequired}
text={label}
/>
<Flex
direction={orientation === "vertical" ? "column" : "row"}
gap={orientation === "vertical" ? "spacing-2" : "spacing-4"}
isInner
ref={containerRef}
wrap="wrap"
>
{items.map((item, index) => children({ ...item, index }))}
</Flex>
{Boolean(errorMessage) && (
<Text
className={styles.error}
color="negative"
lineClamp={2}
size="footnote"
>
{errorMessage}
</Text>
)}
<ErrorMessage text={errorMessage} />
</HeadlessToggleGroup>
);
};

View File

@ -7,11 +7,6 @@
cursor: not-allowed;
}
&[data-disabled] .label {
cursor: not-allowed;
opacity: var(--opacity-disabled);
}
&[data-orientation="horizontal"] [data-label-position="end"] {
margin-inline-end: 0;
}
@ -20,7 +15,3 @@
width: fit-content;
}
}
.error {
margin-block-start: var(--inner-spacing-2);
}

View File

@ -5,6 +5,7 @@ interface ToggleGroupItemProps {
value: string;
label?: string;
isSelected?: boolean;
isDisabled?: boolean;
index?: number;
}

View File

@ -7,7 +7,6 @@ export * from "./components/Text";
export * from "./components/ToggleGroup";
export * from "./components/Tooltip";
export * from "./components/Flex";
export * from "./components/Radio";
export * from "./components/RadioGroup";
export * from "./components/Switch";
export * from "./components/TextInput";
@ -23,8 +22,9 @@ export * from "./components/ContextualHelp";
export * from "./components/Link";
export * from "./components/Popover";
export * from "./components/Label";
export * from "./components/ErrorMessage";
export * from "./utils";
export * from "./styles";
export * from "./hooks";
export * from "./shared";

View File

@ -12,7 +12,6 @@ import {
Flex,
Switch,
RadioGroup,
Radio,
IconButton,
TextArea,
Modal,
@ -105,12 +104,27 @@ export const ComplexForm = () => {
)}
</ToggleGroup>
<RadioGroup label="Portion size">
<Radio value="s">S</Radio>
<Radio value="M">M</Radio>
<Radio value="L">L</Radio>
<Radio value="XL">XL</Radio>
</RadioGroup>
<RadioGroup
items={[
{
value: "s",
label: "S",
},
{
value: "m",
label: "M",
},
{
value: "l",
label: "L",
},
{
value: "xl",
label: "XL",
},
]}
label="Portion size"
/>
<Flex direction="column" gap="spacing-3">
<Flex direction="column" gap="spacing-2">

View File

@ -3,10 +3,11 @@ import React, { useEffect, useRef } from "react";
interface DataAttrWrapperProps {
children: React.ReactNode;
attr: string;
target?: string;
}
export const DataAttrWrapper = (props: DataAttrWrapperProps) => {
const { attr, children } = props;
const { attr, children, target } = props;
// Adding any type here because WDS components has different types for ref
// some are HTMLElement and some are objects only ( For e.g - CheckboxRef )
@ -19,7 +20,11 @@ export const DataAttrWrapper = (props: DataAttrWrapperProps) => {
Boolean(ref.current.setAttribute) &&
typeof ref.current.setAttribute === "function"
) {
ref.current.setAttribute(attr, "");
if (Boolean(target)) {
ref.current.querySelector(target).setAttribute(attr, "");
} else {
ref.current.setAttribute(attr, "");
}
return;
}

View File

@ -6,7 +6,7 @@ import type {
import isNumber from "lodash/isNumber";
import BaseWidget from "widgets/BaseWidget";
import type { WidgetState } from "widgets/BaseWidget";
import { Radio, RadioGroup } from "@design-system/widgets";
import { RadioGroup } from "@design-system/widgets";
import type { SetterConfig, Stylesheet } from "entities/AppTheming";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
@ -122,8 +122,7 @@ class WDSRadioGroupWidget extends BaseWidget<
};
getWidgetView() {
const { labelTooltip, options, selectedOptionValue, widgetId, ...rest } =
this.props;
const { labelTooltip, options, selectedOptionValue, ...rest } = this.props;
const validation = validateInput(this.props);
@ -132,16 +131,11 @@ class WDSRadioGroupWidget extends BaseWidget<
{...rest}
contextualHelp={labelTooltip}
errorMessage={validation.errorMessage}
isInvalid={validation.validationStatus === "invalid"}
items={options}
onChange={this.onRadioSelectionChange}
validationState={validation.validationStatus}
value={selectedOptionValue}
>
{options.map((option, index) => (
<Radio key={`${widgetId}-option-${index}`} value={option.value}>
{option.label}
</Radio>
))}
</RadioGroup>
/>
);
}
}

View File

@ -1,13 +1,8 @@
import type { RadioGroupProps } from "@design-system/widgets";
import type { WidgetProps } from "widgets/BaseWidget";
export interface RadioOption {
label: string;
value: string;
}
export interface RadioGroupWidgetProps extends WidgetProps {
options: RadioOption[];
options: RadioGroupProps["items"];
selectedOptionValue: string;
onSelectionChange: string;
defaultOptionValue: string;