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:
parent
f718bfbb6b
commit
36372468c5
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -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"
|
||||
> {}
|
||||
|
|
@ -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 = {};
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { Checkbox } from "./Checkbox";
|
||||
export type { CheckboxProps } from "./Checkbox";
|
||||
export type { CheckboxProps } from "./types";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./ErrorMessage";
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.errorMessage {
|
||||
margin-block-start: var(--inner-spacing-2);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface ErrorMessageProps {
|
||||
text?: string;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ export interface LabelProps extends HeadlessLabelProps {
|
|||
text?: string;
|
||||
contextualHelp?: string;
|
||||
isRequired?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./src";
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { Radio } from "./Radio";
|
||||
export type { RadioProps } from "./Radio";
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { RadioGroup } from "./RadioGroup";
|
||||
export type { RadioGroupProps } from "./RadioGroup";
|
||||
export type { RadioGroupProps } from "./types";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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} />,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ interface ToggleGroupItemProps {
|
|||
value: string;
|
||||
label?: string;
|
||||
isSelected?: boolean;
|
||||
isDisabled?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user