first commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { CheckboxField } from "./Checkbox";
|
||||
|
||||
const basicProps = { label: "Test Label", description: "Test Description" };
|
||||
|
||||
describe("Tests for CheckboxField component", () => {
|
||||
test("Should render the label and the description correctly", () => {
|
||||
const { getByText } = render(<CheckboxField {...basicProps} />);
|
||||
|
||||
const labelElement = getByText("Test Label");
|
||||
expect(labelElement).toBeInTheDocument();
|
||||
|
||||
const descriptionElement = getByText("Test Description");
|
||||
expect(descriptionElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render the description correctly when the prop descriptionAsLabel is true", () => {
|
||||
const { getByText } = render(<CheckboxField {...basicProps} descriptionAsLabel />);
|
||||
|
||||
const descriptionElement = getByText("Test Label");
|
||||
expect(descriptionElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should trigger onChange event correctly", () => {
|
||||
const handleChange = vi.fn();
|
||||
const { getByRole } = render(<CheckboxField {...basicProps} onChange={handleChange} />);
|
||||
|
||||
const checkboxInput = getByRole("checkbox");
|
||||
|
||||
fireEvent.click(checkboxInput);
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
test("Should disable the checkbox when disabled prop is true", () => {
|
||||
const { getByRole } = render(<CheckboxField {...basicProps} disabled />);
|
||||
|
||||
const checkboxInput = getByRole("checkbox");
|
||||
expect(checkboxInput).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Should change the checked state when clicked", () => {
|
||||
const { getByRole } = render(<CheckboxField {...basicProps} disabled />);
|
||||
const checkboxInput = getByRole("checkbox");
|
||||
|
||||
expect(checkboxInput).not.toBeChecked();
|
||||
expect(checkboxInput).toBeTruthy();
|
||||
|
||||
fireEvent.click(checkboxInput);
|
||||
|
||||
expect(checkboxInput).toBeChecked();
|
||||
expect(checkboxInput).toBeTruthy();
|
||||
|
||||
fireEvent.click(checkboxInput);
|
||||
|
||||
expect(checkboxInput).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
115
calcom/packages/ui/components/form/checkbox/Checkbox.tsx
Normal file
115
calcom/packages/ui/components/form/checkbox/Checkbox.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import type { InputHTMLAttributes } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: React.ReactNode;
|
||||
description: string;
|
||||
descriptionAsLabel?: boolean;
|
||||
informationIconText?: string;
|
||||
error?: boolean;
|
||||
className?: string;
|
||||
descriptionClassName?: string;
|
||||
/**
|
||||
* Accepts this special property instead of allowing description itself to be accidentally used in dangerous way.
|
||||
*/
|
||||
descriptionAsSafeHtml?: string;
|
||||
};
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"border-default data-[state=checked]:bg-brand-default data-[state=checked]:text-brand peer h-4 w-4 shrink-0 rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator className={classNames("flex items-center justify-center text-current")}>
|
||||
<Icon name="check" className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||
({ label, description, error, disabled, descriptionAsSafeHtml, ...rest }, ref) => {
|
||||
const descriptionAsLabel = !label || rest.descriptionAsLabel;
|
||||
const id = useId();
|
||||
return (
|
||||
<div className="block items-center sm:flex">
|
||||
{label && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "div" : "label",
|
||||
{
|
||||
className: classNames("flex text-sm font-medium text-emphasis"),
|
||||
...(!descriptionAsLabel
|
||||
? {
|
||||
htmlFor: rest.id ? rest.id : id,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<div className="relative flex items-center">
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "label" : "div",
|
||||
{
|
||||
className: classNames(
|
||||
"relative flex items-start",
|
||||
!error && descriptionAsLabel ? "text-emphasis" : "text-emphasis",
|
||||
error && "text-error"
|
||||
),
|
||||
},
|
||||
<>
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
id={rest.id ? rest.id : id}
|
||||
className={classNames(
|
||||
"text-emphasis focus:ring-emphasis dark:text-muted border-default bg-default focus:bg-default active:bg-default h-4 w-4 rounded checked:hover:bg-gray-600 focus:outline-none focus:ring-0 ltr:mr-2 rtl:ml-2",
|
||||
!error && disabled
|
||||
? "cursor-not-allowed bg-gray-300 checked:bg-gray-300 hover:bg-gray-300 hover:checked:bg-gray-300"
|
||||
: "hover:bg-subtle hover:border-emphasis checked:bg-gray-800",
|
||||
error &&
|
||||
"border-error hover:bg-error hover:border-error checked:bg-darkerror checked:hover:border-error checked:hover:bg-darkerror",
|
||||
rest.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{descriptionAsSafeHtml ? (
|
||||
<span
|
||||
className={classNames("text-sm", rest.descriptionClassName)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: descriptionAsSafeHtml,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className={classNames("text-sm", rest.descriptionClassName)}>{description}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* {informationIconText && <InfoBadge content={informationIconText}></InfoBadge>} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CheckboxField.displayName = "CheckboxField";
|
||||
|
||||
export { Checkbox, CheckboxField };
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import React from "react";
|
||||
import type {
|
||||
GroupBase,
|
||||
OptionProps,
|
||||
MultiValueProps,
|
||||
MultiValue as MultiValueType,
|
||||
SingleValue,
|
||||
} from "react-select";
|
||||
import { components } from "react-select";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Select } from "../select";
|
||||
|
||||
export type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const InputOption: React.FC<OptionProps<Option, boolean, GroupBase<Option>>> = ({
|
||||
isDisabled,
|
||||
isFocused,
|
||||
isSelected,
|
||||
children,
|
||||
innerProps,
|
||||
...rest
|
||||
}) => {
|
||||
const props = {
|
||||
...innerProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<components.Option
|
||||
{...rest}
|
||||
isDisabled={isDisabled}
|
||||
isFocused={isFocused}
|
||||
isSelected={isSelected}
|
||||
innerProps={props}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="text-emphasis focus:ring-emphasis dark:text-muted border-default h-4 w-4 rounded ltr:mr-2 rtl:ml-2"
|
||||
checked={isSelected}
|
||||
readOnly
|
||||
/>
|
||||
{children}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
type MultiSelectionCheckboxesProps = {
|
||||
options: { label: string; value: string }[];
|
||||
setSelected: Dispatch<SetStateAction<Option[]>>;
|
||||
selected: Option[];
|
||||
setValue: (s: Option[]) => unknown;
|
||||
countText?: string;
|
||||
};
|
||||
|
||||
const MultiValue = ({
|
||||
index,
|
||||
getValue,
|
||||
countText,
|
||||
}: {
|
||||
index: number;
|
||||
getValue: () => readonly Option[];
|
||||
countText: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const count = getValue().filter((option) => option.value !== "all").length;
|
||||
return <>{!index && count !== 0 && <div>{t(countText, { count })}</div>}</>;
|
||||
};
|
||||
|
||||
export default function MultiSelectCheckboxes({
|
||||
options,
|
||||
isLoading,
|
||||
selected,
|
||||
setSelected,
|
||||
setValue,
|
||||
className,
|
||||
isDisabled,
|
||||
countText,
|
||||
}: Omit<Props, "options"> & MultiSelectionCheckboxesProps) {
|
||||
const additonalComponents = {
|
||||
MultiValue: (props: MultiValueProps<Option, boolean, GroupBase<Option>>) => (
|
||||
<MultiValue {...props} countText={countText || "selected"} />
|
||||
),
|
||||
};
|
||||
|
||||
const allOptions = [{ label: "Select all", value: "all" }, ...options];
|
||||
|
||||
const allSelected = selected.length === options.length ? allOptions : selected;
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={allSelected}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onChange={(s: MultiValueType<Option> | SingleValue<Option>, event: any) => {
|
||||
const allSelected = [];
|
||||
|
||||
if (s !== null && Array.isArray(s) && s.length > 0) {
|
||||
if (s.find((option) => option.value === "all")) {
|
||||
if (event.action === "select-option") {
|
||||
allSelected.push(...[{ label: "Select all", value: "all" }, ...options]);
|
||||
} else {
|
||||
allSelected.push(...s.filter((option) => option.value !== "all"));
|
||||
}
|
||||
} else {
|
||||
if (s.length === options.length) {
|
||||
if (s.find((option) => option.value === "all")) {
|
||||
allSelected.push(...s.filter((option) => option.value !== "all"));
|
||||
} else {
|
||||
if (event.action === "select-option") {
|
||||
allSelected.push(...[...s, { label: "Select all", value: "all" }]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allSelected.push(...s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(allSelected);
|
||||
setValue(allSelected);
|
||||
}}
|
||||
variant="checkbox"
|
||||
options={allOptions.length > 1 ? allOptions : []}
|
||||
isMulti
|
||||
isDisabled={isDisabled}
|
||||
className={classNames(className ? className : "w-64 text-sm")}
|
||||
isSearchable={true}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
isLoading={isLoading}
|
||||
data-testid="multi-select-check-boxes"
|
||||
components={{
|
||||
...additonalComponents,
|
||||
Option: InputOption,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Note,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { CheckboxField as Checkbox } from "./Checkbox";
|
||||
|
||||
<Meta title="UI/Form/Checkbox" component={Checkbox} />
|
||||
|
||||
<Title title="Checkbox " suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
|
||||
|
||||
## Definition
|
||||
|
||||
Checkboxes are used in forms and databases to indicate an answer to a question, apply a batch of settings or allow the user to make a multi-selection from a list. Alternatively, a single checkbox may be used for making single selections
|
||||
|
||||
## Structure
|
||||
|
||||
<CustomArgsTable of={Checkbox} />
|
||||
|
||||
<Examples title="Checkbox style">
|
||||
<Example title="Default">
|
||||
<Checkbox label="Default" />
|
||||
</Example>
|
||||
<Example title="Error">
|
||||
<Checkbox label="Error" error />
|
||||
</Example>
|
||||
<Example title="Disabled">
|
||||
<Checkbox label="Disabled" disabled />
|
||||
</Example>
|
||||
<Example title="Disabled">
|
||||
<Checkbox label="Disabled Checked" checked disabled />
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
<Examples title="Description As Label">
|
||||
<Example title="Default">
|
||||
<Checkbox descriptionAsLabel description="Default Description" />
|
||||
</Example>
|
||||
<Example title="Error">
|
||||
<Checkbox descriptionAsLabel description="Default Description" error />
|
||||
</Example>
|
||||
<Example title="Disabled">
|
||||
<Checkbox descriptionAsLabel description="Default Description" disabled />
|
||||
</Example>
|
||||
<Example title="Disabled">
|
||||
<Checkbox descriptionAsLabel description="Default Description" disabled checked />
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Checkbox"
|
||||
args={{
|
||||
label: "Default",
|
||||
description: "Default Description",
|
||||
error: false,
|
||||
disabled: false,
|
||||
}}
|
||||
argTypes={{
|
||||
label: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
error: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{({ label, description, error, disabled }) => (
|
||||
<VariantsTable titles={[""]} columnMinWidth={150}>
|
||||
<VariantRow variant="Default">
|
||||
<Checkbox label={label} error={error} disabled={disabled} />
|
||||
</VariantRow>
|
||||
<VariantRow variant="Description As Label">
|
||||
<Checkbox description={description} error={error} disabled={disabled} descriptionAsLabel />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
3
calcom/packages/ui/components/form/checkbox/index.ts
Normal file
3
calcom/packages/ui/components/form/checkbox/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Checkbox, CheckboxField } from "./Checkbox";
|
||||
export { default as MultiSelectCheckbox } from "./MultiSelectCheckboxes";
|
||||
export type { Option } from "./MultiSelectCheckboxes";
|
||||
Reference in New Issue
Block a user