From 57fbe1f248914cdbc21b1f90854d79c5caf386a4 Mon Sep 17 00:00:00 2001 From: Daniel Sil Date: Fri, 18 Oct 2024 15:03:54 +0200 Subject: [PATCH] feat(Radio): add defaultSelected prop and uncontrolled behavior --- packages/orbit-components/src/Radio/README.md | 37 ++++++++++--------- .../src/Radio/Radio.stories.tsx | 2 +- .../src/Radio/__tests__/index.test.tsx | 31 ++++++++++++++++ packages/orbit-components/src/Radio/index.tsx | 34 +++++++++++------ .../orbit-components/src/Radio/types.d.ts | 1 + 5 files changed, 74 insertions(+), 31 deletions(-) diff --git a/packages/orbit-components/src/Radio/README.md b/packages/orbit-components/src/Radio/README.md index da5c16aff9..827f89fe55 100644 --- a/packages/orbit-components/src/Radio/README.md +++ b/packages/orbit-components/src/Radio/README.md @@ -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` | | 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` | | 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 or 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 { diff --git a/packages/orbit-components/src/Radio/Radio.stories.tsx b/packages/orbit-components/src/Radio/Radio.stories.tsx index ff5e9ee5d9..06caaf71eb 100644 --- a/packages/orbit-components/src/Radio/Radio.stories.tsx +++ b/packages/orbit-components/src/Radio/Radio.stories.tsx @@ -16,7 +16,7 @@ const meta: Meta = { parameters: { info: "Radio component. Check Orbit.Kiwi for more detailed guidelines.", controls: { - exclude: ["onChange"], + exclude: ["onChange", "defaultChecked"], }, }, diff --git a/packages/orbit-components/src/Radio/__tests__/index.test.tsx b/packages/orbit-components/src/Radio/__tests__/index.test.tsx index b579520983..5fba288aa8 100644 --- a/packages/orbit-components/src/Radio/__tests__/index.test.tsx +++ b/packages/orbit-components/src/Radio/__tests__/index.test.tsx @@ -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(); @@ -49,4 +50,34 @@ describe(`Radio`, () => { render( {}} />); expect(screen.getByRole("radio")).toHaveAttribute("data-state", "error"); }); + + it("can be uncontrolled", async () => { + const onChange = jest.fn(); + render(); + + 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( + , + ); + + const radio = screen.getByRole("radio") as HTMLInputElement; + expect(radio.checked).toBeTruthy(); + await user.click(radio); + expect(onChange).not.toHaveBeenCalled(); + }); }); diff --git a/packages/orbit-components/src/Radio/index.tsx b/packages/orbit-components/src/Radio/index.tsx index 4851151266..abe39ff1da 100644 --- a/packages/orbit-components/src/Radio/index.tsx +++ b/packages/orbit-components/src/Radio/index.tsx @@ -13,7 +13,8 @@ const Radio = React.forwardRef((props, ref) => { value, hasError = false, disabled = false, - checked = false, + checked, + defaultChecked, onChange, name, info, @@ -29,15 +30,23 @@ const Radio = React.forwardRef((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", - ], + 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", + ], )} > ((props, ref) => { value={value} type="radio" disabled={disabled} - checked={checked} + checked={checked || undefined} + defaultChecked={defaultChecked} id={id} onChange={onChange} name={name} @@ -68,7 +78,7 @@ const Radio = React.forwardRef((props, ref) => { 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", + "peer-checked:border-2 peer-checked:[&_span]:visible", )} > ; readonly readOnly?: boolean;