Skip to content

Commit

Permalink
feat(Radio): add defaultSelected prop and uncontrolled behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
DSil committed Oct 21, 2024
1 parent a18ebbf commit 9f11db7
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 33 deletions.
37 changes: 19 additions & 18 deletions packages/orbit-components/src/Radio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,29 @@ After adding import into your project you can use it simply like:

Table below contains all types of the props available in Radio component.

| Name | Type | Default | Description |
| :------- | :------------------------- | :------ | :-------------------------------------------------------------------------------------------------------- |
| checked | `boolean` | `false` | If `true`, the Radio will be checked. |
| dataTest | `string` | | Optional prop for testing purposes. |
| id | `string` | | Set `id` for `Radio` input |
| disabled | `boolean` | `false` | If `true`, the Radio will be set up as disabled. |
| hasError | `boolean` | `false` | If `true`, the border of the Radio will turn red. [See Functional specs](#functional-specs) |
| info | `React.Node` | | The additional info about the Radio. |
| label | `string` | | The label of the Radio. |
| name | `string` | | The name for the Radio. |
| onChange | `event => void \| Promise` | | Function for handling onChange event. |
| ref | `func` | | Prop for forwarded ref of the Radio. [See Functional specs](#functional-specs) |
| tabIndex | `string \| number` | | Specifies the tab order of an element |
| tooltip | `Element<Tooltip>` | | Optional property when you need to attach Tooltip to the Radio. [See Functional specs](#functional-specs) |
| value | `string` | | The value of the Radio. |
| readOnly | `boolean` | | If `true`, the Radio will be set up as readOnly. |
| Name | Type | Default | Description |
| :------------- | :------------------------- | :------ | :-------------------------------------------------------------------------------------------------------- |
| checked | `boolean` | `false` | If `true`, the Radio will be checked. |
| defaultChecked | `boolean` | | If `true`, the Radio will be checked by default. Only to be used in uncontrolled. |
| dataTest | `string` | | Optional prop for testing purposes. |
| id | `string` | | Set `id` for `Radio` input |
| disabled | `boolean` | `false` | If `true`, the Radio will be set up as disabled. |
| hasError | `boolean` | `false` | If `true`, the border of the Radio will turn red. [See Functional specs](#functional-specs) |
| info | `React.Node` | | The additional info about the Radio. |
| label | `string` | | The label of the Radio. |
| name | `string` | | The name for the Radio. |
| onChange | `event => void \| Promise` | | Function for handling onChange event. |
| ref | `func` | | Prop for forwarded ref of the Radio. [See Functional specs](#functional-specs) |
| tabIndex | `string \| number` | | Specifies the tab order of an element |
| tooltip | `Element<Tooltip>` | | Optional property when you need to attach Tooltip to the Radio. [See Functional specs](#functional-specs) |
| value | `string` | | The value of the Radio. |
| readOnly | `boolean` | | If `true`, the Radio will be set up as readOnly. |

## Functional specs

- The`hasError` prop will be visible only when the Radio has `checked` or `disabled` prop set on false.
- The`hasError` prop will be visible only when the Radio is not checked nor disabled.

- `ref` can be used for example auto-focus the elements immediately after render.
- `ref` can be used, for example, to control focus or to get the status (checked) of the element.

```jsx
class Component extends React.PureComponent<Props> {
Expand Down
18 changes: 16 additions & 2 deletions packages/orbit-components/src/Radio/Radio.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const meta: Meta<typeof Radio> = {
parameters: {
info: "Radio component. Check Orbit.Kiwi for more detailed guidelines.",
controls: {
exclude: ["onChange"],
exclude: ["onChange", "defaultChecked", "readOnly", "value", "name"],
},
},

Expand All @@ -42,7 +42,7 @@ export const Default: Story = {
parameters: {
info: "Default settings of component. Check Orbit.Kiwi for more detailed guidelines.",
controls: {
exclude: ["info", "hasError", "disabled", "readOnly", "onChange", "tabIndex"],
exclude: ["info", "hasError", "disabled", "onChange", "tabIndex", "value", "name"],
},
},

Expand All @@ -51,6 +51,20 @@ export const Default: Story = {
},
};

export const Uncontrolled: Story = {
parameters: {
info: `Uncontrolled Radio. It doesn't require "checked" prop. Can be used with "defaultChecked" prop.`,
controls: {
exclude: ["checked", "info", "hasError", "disabled", "onChange", "tabIndex", "value", "name"],
},
},

args: {
checked: undefined,
info: undefined,
},
};

export const WithError: Story = {
args: {
hasError: true,
Expand Down
31 changes: 31 additions & 0 deletions packages/orbit-components/src/Radio/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe(`Radio`, () => {
expect(radio).toHaveAttribute("name", name);
expect(radio).toHaveAttribute("readonly");
expect(radio).toHaveAttribute("data-state", "ok");
expect(radio).not.toHaveAttribute("checked");
expect(screen.getByDisplayValue(value)).toBeInTheDocument();
await user.click(radio);
expect(onChange).toHaveBeenCalled();
Expand All @@ -49,4 +50,34 @@ describe(`Radio`, () => {
render(<Radio hasError onChange={() => {}} />);
expect(screen.getByRole("radio")).toHaveAttribute("data-state", "error");
});

it("can be uncontrolled", async () => {
const onChange = jest.fn();
render(<Radio label="Radio" onChange={onChange} value="option" dataTest="test" name="name" />);

const radio = screen.getByRole("radio") as HTMLInputElement;
expect(radio.checked).toBeFalsy();
await user.click(radio);
expect(onChange).toHaveBeenCalled();
expect(radio.checked).toBeTruthy();
});

it("can be uncontrolled and checked by default", async () => {
const onChange = jest.fn();
render(
<Radio
label="Radio"
onChange={onChange}
value="option"
dataTest="test"
name="name"
defaultChecked
/>,
);

const radio = screen.getByRole("radio") as HTMLInputElement;
expect(radio.checked).toBeTruthy();
await user.click(radio);
expect(onChange).not.toHaveBeenCalled();
});
});
36 changes: 23 additions & 13 deletions packages/orbit-components/src/Radio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const Radio = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
value,
hasError = false,
disabled = false,
checked = false,
checked,
defaultChecked,
onChange,
name,
info,
Expand All @@ -29,25 +30,36 @@ const Radio = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
htmlFor={id}
className={cx(
"font-base text-form-element-label-foreground relative flex w-full [align-items:self-start]",
disabled ? "cursor-not-allowed" : "cursor-pointer",
!disabled && [
!checked &&
(hasError
? "[&>.orbit-radio-icon-container]:border-form-element-error [&>.orbit-radio-icon-container]:hover:border-form-element-error-hover [&>.orbit-radio-icon-container]:active:border-form-element-error"
: "[&>.orbit-radio-icon-container]:border-form-element [&>.orbit-radio-icon-container]:hover:border-form-element-hover [&>.orbit-radio-icon-container]:active:border-form-element-active"),
checked &&
"[&>.orbit-radio-icon-container]:border-form-element-focus active:border-form-element-focus [&>.orbit-radio-icon-container]:bg-white-normal",
],
"[&_.orbit-radio-icon-container]:has-[:checked]:border-2 [&_.orbit-radio-icon-container_span]:has-[:checked]:visible",
"[&_.orbit-radio-icon-container]:has-[:focus]:outline-blue-normal [&_.orbit-radio-icon-container]:has-[:focus]:outline [&_.orbit-radio-icon-container]:has-[:focus]:outline-2",
disabled
? [
"cursor-not-allowed",
"[&_.orbit-radio-icon-container]:bg-cloud-light [&_.orbit-radio-icon-container]:border-cloud-dark",
]
: [
"cursor-pointer",
"[&_.orbit-radio-icon-container]:has-[:checked]:border-form-element-focus [&_.orbit-radio-icon-container]:has-[:checked]:active:border-form-element-focus [&_.orbit-radio-icon-container]:has-[:checked]:bg-white-normal [&_.orbit-radio-icon-container]:bg-form-element-background",
!checked &&
hasError &&
"[&_.orbit-radio-icon-container]:border-form-element-error [&_.orbit-radio-icon-container]:hover:border-form-element-error-hover [&_.orbit-radio-icon-container]:active:border-form-element-error",
!hasError &&
"[&_.orbit-radio-icon-container]:border-form-element [&_.orbit-radio-icon-container]:hover:border-form-element-hover [&_.orbit-radio-icon-container]:active:border-form-element-active",
checked &&
!hasError &&
"[&_.orbit-radio-icon-container]:border-form-element-focus active:border-form-element-focus [&_.orbit-radio-icon-container]:bg-white-normal",
],
)}
>
<input
data-test={dataTest}
data-state={getFieldDataState(hasError)}
className="peer absolute opacity-0"
className="absolute opacity-0"
value={value}
type="radio"
disabled={disabled}
checked={checked}
defaultChecked={defaultChecked}
id={id}
onChange={onChange}
name={name}
Expand All @@ -67,8 +79,6 @@ const Radio = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
"border-solid",
checked ? "border-2" : "border",
"active:scale-95",
"peer-focus:outline-blue-normal peer-focus:outline peer-focus:outline-2",
disabled ? "bg-cloud-light border-cloud-dark" : "bg-form-element-background",
)}
>
<span
Expand Down
1 change: 1 addition & 0 deletions packages/orbit-components/src/Radio/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface Props extends Common.Globals {
readonly disabled?: boolean;
readonly name?: string;
readonly checked?: boolean;
readonly defaultChecked?: boolean;
readonly info?: React.ReactNode;
readonly tooltip?: React.ReactElement<typeof Tooltip>;
readonly readOnly?: boolean;
Expand Down

0 comments on commit 9f11db7

Please sign in to comment.