From b840686d296c0ff5fa7378b713519c03c42a3f4c Mon Sep 17 00:00:00 2001 From: Daniel Sil Date: Fri, 18 Oct 2024 15:04:15 +0200 Subject: [PATCH] feat(Checkbox): add defaultSelected prop and uncontrolled behavior --- .../src/Checkbox/Checkbox.stories.tsx | 18 ++++++++-- .../orbit-components/src/Checkbox/README.md | 35 ++++++++++--------- .../src/Checkbox/__tests__/index.test.tsx | 23 +++++++++++- .../orbit-components/src/Checkbox/index.tsx | 34 ++++++++++-------- .../orbit-components/src/Checkbox/types.d.ts | 1 + 5 files changed, 76 insertions(+), 35 deletions(-) diff --git a/packages/orbit-components/src/Checkbox/Checkbox.stories.tsx b/packages/orbit-components/src/Checkbox/Checkbox.stories.tsx index ec51344ab7..319f59ceb2 100644 --- a/packages/orbit-components/src/Checkbox/Checkbox.stories.tsx +++ b/packages/orbit-components/src/Checkbox/Checkbox.stories.tsx @@ -10,13 +10,13 @@ import Tooltip from "../Tooltip"; import Checkbox from "."; const meta: Meta = { - title: "CheckBox", + title: "Checkbox", component: Checkbox, parameters: { info: "Additionally you can add tooltip to this component.", controls: { - exclude: ["onChange"], + exclude: ["onChange", "defaultChecked", "readOnly", "value", "name"], }, }, @@ -50,6 +50,20 @@ export const Default: Story = { }, }; +export const Uncontrolled: Story = { + parameters: { + info: `Uncontrolled Checkbox. It doesn't require "checked" prop. Can be used with "defaultChecked" prop.`, + controls: { + exclude: ["checked", "info", "hasError", "disabled", "name", "value", "onChange"], + }, + }, + + args: { + checked: undefined, + info: undefined, + }, +}; + export const WithHelp: Story = { parameters: { info: "Additional information about this choice", diff --git a/packages/orbit-components/src/Checkbox/README.md b/packages/orbit-components/src/Checkbox/README.md index 0d67b5d13b..a5b33d9928 100644 --- a/packages/orbit-components/src/Checkbox/README.md +++ b/packages/orbit-components/src/Checkbox/README.md @@ -16,27 +16,28 @@ After adding import into your project you can use it simply like: Table below contains all types of the props available in Checkbox component. -| Name | Type | Default | Description | -| :------- | :------------------------- | :------ | :----------------------------------------------------------------------------------------------------------- | -| checked | `boolean` | `false` | If `true`, the Checkbox will be checked. | -| disabled | `boolean` | `false` | If `true`, the Checkbox will be set up as disabled. | -| dataTest | `string` | | Optional prop for testing purposes. | -| id | `string` | | Set `id` for `Checkbox` | -| hasError | `boolean` | `false` | If `true`, the border of the Checkbox will turn red. [See Functional specs](#functional-specs) | -| info | `React.Node` | | The additional info about the Checkbox. | -| label | `string` | | The label of the Checkbox. | -| name | `string` | | The name for the Checkbox. | -| onChange | `event => void \| Promise` | | Function for handling onChange event. | -| ref | `func` | | Prop for forwarded ref of the Checkbox. [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 Checkbox. [See Functional specs](#functional-specs) | -| value | `string` | | The value of the Checkbox. | +| Name | Type | Default | Description | +| :------------- | :------------------------- | :------ | :----------------------------------------------------------------------------------------------------------- | +| checked | `boolean` | | If `true`, the Checkbox will be checked. | +| defaultChecked | `boolean` | | If `true`, the Checkbox will be checked by default. Only to be used in uncontrolled. | +| disabled | `boolean` | `false` | If `true`, the Checkbox will be set up as disabled. | +| dataTest | `string` | | Optional prop for testing purposes. | +| id | `string` | | Set `id` for `Checkbox` | +| hasError | `boolean` | `false` | If `true`, the border of the Checkbox will turn red. [See Functional specs](#functional-specs) | +| info | `React.Node` | | The additional info about the Checkbox. | +| label | `string` | | The label of the Checkbox. | +| name | `string` | | The name for the Checkbox. | +| onChange | `event => void \| Promise` | | Function for handling onChange event. | +| ref | `func` | | Prop for forwarded ref of the Checkbox. [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 Checkbox. [See Functional specs](#functional-specs) | +| value | `string` | | The value of the Checkbox. | ## Functional specs -- The `hasError` prop will be visible only when the Checkbox has `checked` or `disabled` prop set on **false**. +- The `hasError` prop will be visible only when the Checkbox 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 import * as React from "react"; diff --git a/packages/orbit-components/src/Checkbox/__tests__/index.test.tsx b/packages/orbit-components/src/Checkbox/__tests__/index.test.tsx index 3c88835878..855d08f393 100644 --- a/packages/orbit-components/src/Checkbox/__tests__/index.test.tsx +++ b/packages/orbit-components/src/Checkbox/__tests__/index.test.tsx @@ -7,7 +7,7 @@ import CheckBox from ".."; describe("CheckBox", () => { const user = userEvent.setup(); - it("default", async () => { + it("can be controlled", async () => { const onChange = jest.fn(); render( { expect(checkbox).toHaveAttribute("value", "option"); expect(checkbox).toHaveAttribute("name", "name"); expect(checkbox).toHaveAttribute("tabIndex", "-1"); + expect(checkbox).not.toHaveAttribute("checked"); await user.click(checkbox); expect(onChange).toHaveBeenCalled(); }); + + it("can be uncontrolled", async () => { + const onChange = jest.fn(); + render( + , + ); + expect(screen.getByTestId("test")).toBeInTheDocument(); + const checkbox = screen.getByRole("checkbox", { name: "Checkbox" }) as HTMLInputElement; + expect(checkbox.checked).toBeTruthy(); + await user.click(checkbox); + expect(onChange).toHaveBeenCalled(); + expect(checkbox.checked).toBeFalsy(); + }); }); diff --git a/packages/orbit-components/src/Checkbox/index.tsx b/packages/orbit-components/src/Checkbox/index.tsx index c7827eca22..f22fe4910b 100644 --- a/packages/orbit-components/src/Checkbox/index.tsx +++ b/packages/orbit-components/src/Checkbox/index.tsx @@ -14,7 +14,8 @@ const Checkbox = React.forwardRef((props, ref) => { value, hasError = false, disabled = false, - checked = false, + checked, + defaultChecked, name, onChange, dataTest, @@ -33,24 +34,31 @@ const Checkbox = React.forwardRef((props, ref) => { "flex flex-row", "relative w-full", "[&_.orbit-checkbox-icon-container]:hover:shadow-none", + "[&_.orbit-checkbox-icon-container]:has-[:checked]:bg-blue-normal [&_.orbit-checkbox-icon-container]:has-[:checked]:hover:bg-blue-dark", + "[&_.orbit-checkbox-icon-container]:has-[:focus]:outline-blue-normal [&_.orbit-checkbox-icon-container]:has-[:focus]:outline [&_.orbit-checkbox-icon-container]:has-[:focus]:outline-2", + "[&_.orbit-checkbox-icon-container>svg]:has-[:checked]:visible", disabled - ? "cursor-not-allowed" + ? [ + "cursor-not-allowed", + "[&_.orbit-checkbox-icon-container]:bg-form-element-disabled-background", + "[&_.orbit-checkbox-icon-container]:has-[:checked]:bg-cloud-dark", + checked && "[&_.orbit-checkbox-icon-container]:bg-cloud-dark", + ] : [ "cursor-pointer", + "[&_.orbit-checkbox-icon-container]:has-[:checked]:border-blue-normal [&_.orbit-checkbox-icon-container]:has-[:checked]:hover:border-blue-dark", checked && + !hasError && "[&_.orbit-checkbox-icon-container]:bg-blue-normal [&_.orbit-checkbox-icon-container]:border-blue-normal [&_.orbit-checkbox-icon-container]:hover:bg-blue-dark [&_.orbit-checkbox-icon-container]:hover:border-blue-dark", !checked && "[&_.orbit-checkbox-icon-container]:bg-form-element-background", - !checked && - hasError && - "[&_.orbit-checkbox-icon-container]:border-form-element-error", - !checked && - !hasError && - "[&_.orbit-checkbox-icon-container]:border-form-element-border-color [&_.orbit-checkbox-icon-container]:hover:border-blue-light-active [&_.orbit-checkbox-icon-container]:active:border-form-element-focus", + !checked && hasError + ? "[&_.orbit-checkbox-icon-container]:border-form-element-error" + : "[&_.orbit-checkbox-icon-container]:border-form-element-border-color [&_.orbit-checkbox-icon-container]:hover:border-blue-light-active [&_.orbit-checkbox-icon-container]:active:border-form-element-focus", ], )} > ((props, ref) => { name={name} tabIndex={tabIndex ? Number(tabIndex) : undefined} checked={checked} + defaultChecked={defaultChecked} onChange={!readOnly ? onChange : undefined} onClick={e => readOnly && e.stopPropagation()} ref={ref} @@ -76,16 +85,11 @@ const Checkbox = React.forwardRef((props, ref) => { "size-icon-medium", "rounded-150 de:rounded-100", "duration-fast transition-all ease-in-out", - "peer-focus:outline-blue-normal peer-focus:outline peer-focus:outline-2", "[&>svg]:size-icon-small", "[&>svg]:flex [&>svg]:items-center [&>svg]:justify-center", "active:scale-95", checked ? "[&>svg]:visible" : "[&>svg]:invisible", - disabled && [ - "border-cloud-dark", - checked && "bg-cloud-dark", - !checked && "bg-form-element-disabled-background", - ], + disabled && ["border-cloud-dark"], )} > diff --git a/packages/orbit-components/src/Checkbox/types.d.ts b/packages/orbit-components/src/Checkbox/types.d.ts index 93d06d51fe..963a0cf822 100644 --- a/packages/orbit-components/src/Checkbox/types.d.ts +++ b/packages/orbit-components/src/Checkbox/types.d.ts @@ -11,6 +11,7 @@ export interface Props extends Common.Globals { readonly hasError?: boolean; readonly disabled?: boolean; readonly checked?: boolean; + readonly defaultChecked?: boolean; readonly name?: string; readonly info?: React.ReactNode; readonly tabIndex?: string | number;