first commit
This commit is contained in:
209
calcom/packages/ui/components/form/select/Select.tsx
Normal file
209
calcom/packages/ui/components/form/select/Select.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import * as React from "react";
|
||||
import type { GroupBase, Props, SingleValue, MultiValue } from "react-select";
|
||||
import ReactSelect from "react-select";
|
||||
|
||||
import cx from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Label } from "../inputs/Label";
|
||||
import { getReactSelectProps } from "./selectTheme";
|
||||
|
||||
export type SelectProps<
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
> = Props<Option, IsMulti, Group> & { variant?: "default" | "checkbox"; "data-testid"?: string };
|
||||
|
||||
export const Select = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
components,
|
||||
variant = "default",
|
||||
...props
|
||||
}: SelectProps<Option, IsMulti, Group> & {
|
||||
innerClassNames?: {
|
||||
input?: string;
|
||||
option?: string;
|
||||
control?: string;
|
||||
singleValue?: string;
|
||||
valueContainer?: string;
|
||||
multiValue?: string;
|
||||
menu?: string;
|
||||
menuList?: string;
|
||||
};
|
||||
}) => {
|
||||
const { classNames, innerClassNames, menuPlacement = "auto", ...restProps } = props;
|
||||
const reactSelectProps = React.useMemo(() => {
|
||||
return getReactSelectProps<Option, IsMulti, Group>({
|
||||
components: components || {},
|
||||
menuPlacement,
|
||||
});
|
||||
}, [components, menuPlacement]);
|
||||
|
||||
// Annoyingly if we update styles here we have to update timezone select too
|
||||
// We cant create a generate function for this as we can't force state changes - onSelect styles dont change for example
|
||||
return (
|
||||
<ReactSelect
|
||||
{...reactSelectProps}
|
||||
menuPlacement={menuPlacement}
|
||||
classNames={{
|
||||
input: () => cx("text-emphasis", innerClassNames?.input),
|
||||
option: (state) =>
|
||||
cx(
|
||||
"bg-default flex cursor-pointer justify-between py-2.5 px-3 rounded-none text-default ",
|
||||
state.isFocused && "bg-subtle",
|
||||
state.isDisabled && "bg-muted",
|
||||
state.isSelected && "bg-emphasis text-default",
|
||||
innerClassNames?.option
|
||||
),
|
||||
placeholder: (state) => cx("text-muted", state.isFocused && variant !== "checkbox" && "hidden"),
|
||||
dropdownIndicator: () => "text-default",
|
||||
control: (state) =>
|
||||
cx(
|
||||
"bg-default border-default !min-h-9 h-9 text-sm leading-4 placeholder:text-sm placeholder:font-normal dark:focus:border-emphasis focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-default hover:border-emphasis rounded-md border",
|
||||
state.isMulti
|
||||
? variant === "checkbox"
|
||||
? "px-3 py-2 h-fit"
|
||||
: state.hasValue
|
||||
? "p-1 h-fit"
|
||||
: "px-3 py-2 h-fit"
|
||||
: "py-2 px-3",
|
||||
props.isDisabled && "bg-subtle",
|
||||
innerClassNames?.control
|
||||
),
|
||||
singleValue: () => cx("text-emphasis placeholder:text-muted", innerClassNames?.singleValue),
|
||||
valueContainer: () =>
|
||||
cx("text-emphasis placeholder:text-muted flex gap-1", innerClassNames?.valueContainer),
|
||||
multiValue: () =>
|
||||
cx(
|
||||
"bg-subtle text-default rounded-md py-1.5 px-2 flex items-center text-sm leading-tight",
|
||||
innerClassNames?.multiValue
|
||||
),
|
||||
menu: () =>
|
||||
cx(
|
||||
" rounded-md bg-default text-sm leading-4 text-default mt-1 border border-subtle",
|
||||
innerClassNames?.menu
|
||||
),
|
||||
groupHeading: () => "leading-none text-xs uppercase text-default pl-2.5 pt-4 pb-2",
|
||||
menuList: () => cx("scroll-bar scrollbar-track-w-20 rounded-md", innerClassNames?.menuList),
|
||||
indicatorsContainer: (state) =>
|
||||
cx(
|
||||
state.selectProps.menuIsOpen
|
||||
? state.isMulti
|
||||
? "[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform"
|
||||
: "rotate-180 transition-transform"
|
||||
: "text-default" // Woo it adds another SVG here on multi for some reason
|
||||
),
|
||||
multiValueRemove: () => "text-default py-auto ml-2",
|
||||
...classNames,
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectField = function SelectField<
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: {
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
containerClassName?: string;
|
||||
label?: string;
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
className?: string;
|
||||
error?: string;
|
||||
} & SelectProps<Option, IsMulti, Group>
|
||||
) {
|
||||
const { t } = useLocale();
|
||||
const { label = t(props.name || ""), containerClassName, labelProps, className, ...passThrough } = props;
|
||||
const id = useId();
|
||||
return (
|
||||
<div className={cx(containerClassName)}>
|
||||
<div className={cx(className)}>
|
||||
{!!label && (
|
||||
<Label htmlFor={id} {...labelProps} className={cx(props.error && "text-error")}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
<Select {...passThrough} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: It should replace Select after through testing
|
||||
*/
|
||||
export function SelectWithValidation<
|
||||
Option extends { label: string; value: string },
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
required = false,
|
||||
onChange,
|
||||
value,
|
||||
...remainingProps
|
||||
}: SelectProps<Option, IsMulti, Group> & { required?: boolean }) {
|
||||
const [hiddenInputValue, _setHiddenInputValue] = React.useState(() => {
|
||||
if (value instanceof Array || !value) {
|
||||
return "";
|
||||
}
|
||||
return value.value || "";
|
||||
});
|
||||
|
||||
const setHiddenInputValue = React.useCallback((value: MultiValue<Option> | SingleValue<Option>) => {
|
||||
let hiddenInputValue = "";
|
||||
if (value instanceof Array) {
|
||||
hiddenInputValue = value.map((val) => val.value).join(",");
|
||||
} else {
|
||||
hiddenInputValue = value?.value || "";
|
||||
}
|
||||
_setHiddenInputValue(hiddenInputValue);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setHiddenInputValue(value);
|
||||
}, [value, setHiddenInputValue]);
|
||||
|
||||
return (
|
||||
<div className={cx("relative", remainingProps.className)}>
|
||||
<Select
|
||||
value={value}
|
||||
{...remainingProps}
|
||||
onChange={(value, ...remainingArgs) => {
|
||||
setHiddenInputValue(value);
|
||||
if (onChange) {
|
||||
onChange(value, ...remainingArgs);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{required && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
style={{
|
||||
opacity: 0,
|
||||
width: "100%",
|
||||
height: 1,
|
||||
position: "absolute",
|
||||
}}
|
||||
value={hiddenInputValue}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onChange={() => {}}
|
||||
// TODO:Not able to get focus to work
|
||||
// onFocus={() => selectRef.current?.focus()}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
calcom/packages/ui/components/form/select/components.tsx
Normal file
87
calcom/packages/ui/components/form/select/components.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { GroupBase, InputProps, OptionProps, ControlProps } from "react-select";
|
||||
import { components as reactSelectComponents } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { Icon } from "../../..";
|
||||
import { UpgradeTeamsBadge } from "../../badge";
|
||||
import type { SelectProps } from "./Select";
|
||||
|
||||
export const InputComponent = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
inputClassName,
|
||||
...props
|
||||
}: InputProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<reactSelectComponents.Input
|
||||
// disables our default form focus hightlight on the react-select input element
|
||||
inputClassName={classNames(
|
||||
"focus:ring-0 focus:ring-offset-0 dark:!text-darkgray-900 !text-emphasis",
|
||||
inputClassName
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ExtendedOption = {
|
||||
value: string | number;
|
||||
label: string;
|
||||
needsTeamsUpgrade?: boolean;
|
||||
};
|
||||
|
||||
export const OptionComponent = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
...props
|
||||
}: OptionProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
// This gets styled in the select classNames prop now - handles overrides with styles vs className here doesnt
|
||||
<reactSelectComponents.Option {...props}>
|
||||
<div className="flex">
|
||||
<span className="mr-auto" data-testid={`select-option-${(props as unknown as ExtendedOption).value}`}>
|
||||
{props.label || <> </>}
|
||||
</span>
|
||||
{(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? <UpgradeTeamsBadge /> : <></>}
|
||||
{props.isSelected && <Icon name="check" className="ml-2 h-4 w-4" />}
|
||||
</div>
|
||||
</reactSelectComponents.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export const ControlComponent = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
controlProps: ControlProps<Option, IsMulti, Group> & {
|
||||
selectProps: SelectProps<Option, IsMulti, Group>;
|
||||
}
|
||||
) => {
|
||||
const dataTestId = controlProps.selectProps["data-testid"] ?? "select-control";
|
||||
return (
|
||||
<span data-testid={dataTestId}>
|
||||
<reactSelectComponents.Control {...controlProps} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// We need to override this component if we need a icon - we can't simpily override styles
|
||||
type IconLeadingProps = {
|
||||
icon: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
} & React.ComponentProps<typeof reactSelectComponents.Control>;
|
||||
|
||||
export const IconLeading = ({ icon, children, ...props }: IconLeadingProps) => {
|
||||
return (
|
||||
<reactSelectComponents.Control {...props}>
|
||||
{icon}
|
||||
{children}
|
||||
</reactSelectComponents.Control>
|
||||
);
|
||||
};
|
||||
3
calcom/packages/ui/components/form/select/index.ts
Normal file
3
calcom/packages/ui/components/form/select/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SelectWithValidation, SelectField, Select } from "./Select";
|
||||
|
||||
export { getReactSelectProps } from "./selectTheme";
|
||||
129
calcom/packages/ui/components/form/select/select.stories.mdx
Normal file
129
calcom/packages/ui/components/form/select/select.stories.mdx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantRow,
|
||||
VariantsTable,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { SelectField } from "./Select";
|
||||
|
||||
<Meta title="UI/Form/Select Field" component={SelectField} />
|
||||
|
||||
<Title title="Select" suffix="Brief" subtitle="Version 2.0 — Last Update: 29 Aug 2022" />
|
||||
|
||||
## Definition
|
||||
|
||||
Dropdown fields allow users to input existing options that is preset by the deisgner/ developer. It can be just one choice per field, or they might be multiple choices depends on the circumstances.
|
||||
|
||||
## Structure
|
||||
|
||||
<CustomArgsTable of={SelectField} />
|
||||
|
||||
export const options = [
|
||||
{ value: 0, label: "Option One" },
|
||||
{ value: 1, label: "Option Two" },
|
||||
{ value: 3, label: "Option Three" },
|
||||
{ value: 4, label: "Option Four" },
|
||||
];
|
||||
|
||||
## Examples
|
||||
|
||||
<Examples
|
||||
title=" Single Selected / Unselected"
|
||||
footnote={
|
||||
<ul>
|
||||
<li>The difference between the types are when they are filled. </li>
|
||||
</ul>
|
||||
}>
|
||||
<Example title="Single Select [Unselected]" isFullWidth>
|
||||
<SelectField label={"Single Select"} options={options} />
|
||||
</Example>
|
||||
<Example title="Single Select [Selected]" isFullWidth>
|
||||
<SelectField label={"Single Select"} options={options} defaultValue={options[0]} />
|
||||
</Example>
|
||||
<Example title="Multi Select [Unselected]" isFullWidth>
|
||||
<SelectField label={"Multi Select"} options={options} isMulti={true} />
|
||||
</Example>
|
||||
<Example title="Multi Select [Selected]" isFullWidth>
|
||||
<SelectField label={"Multi Select"} options={options} isMulti={true} defaultValue={options[0]} />
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
<Examples title="Variants">
|
||||
<Example title="Default">
|
||||
<SelectField label={"Default Select"} options={options} />
|
||||
</Example>
|
||||
<Example title="Icon Left">
|
||||
WIP
|
||||
{/* <SelectField options={options} components={{ Control }}/> */}
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
## Variant Caviats (WIP) - To be updated
|
||||
|
||||
Using Icons is a bit of a strange one cause you can't simpily pass in an icon as a prop. You have to pass in a component. To the select field.
|
||||
|
||||
```js
|
||||
// Bad: Inline declaration will cause remounting issues
|
||||
const BadSelect = (props) => (
|
||||
<Select
|
||||
{...props}
|
||||
components={{
|
||||
Control: ({ children, ...rest }) => <components.Control {...rest}>👎 {children}</components.Control>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Good: Custom component declared outside of the Select scope
|
||||
const Control = <IconLeading icon="plus" />;
|
||||
|
||||
const GoodSelect = (props) => <Select {...props} components={{ Control }} />;
|
||||
```
|
||||
|
||||
<Examples title="States ">
|
||||
<Example title="Default">
|
||||
<SelectField options={options} label={"Default Select"} />
|
||||
</Example>
|
||||
{/* <Example title="Hover">
|
||||
<SelectField options={options} className="sb-pseudo--hover"/>
|
||||
</Example>
|
||||
<Example title="Focus">
|
||||
<SelectField options={options} className="sb-pseudo--focus"/>
|
||||
</Example> */}
|
||||
</Examples>
|
||||
|
||||
## Select Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
required: false,
|
||||
name: "select-field",
|
||||
error: "Some error",
|
||||
variant: "default",
|
||||
label: "Select an item",
|
||||
isMulti: false,
|
||||
options,
|
||||
}}
|
||||
argTypes={{
|
||||
variant: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["default", "checkbox"],
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{(args) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={300}>
|
||||
<VariantRow>
|
||||
<SelectField {...args} />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
308
calcom/packages/ui/components/form/select/select.test.tsx
Normal file
308
calcom/packages/ui/components/form/select/select.test.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { Select, SelectField, SelectWithValidation } from "./Select";
|
||||
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
|
||||
const onChangeMock = vi.fn();
|
||||
|
||||
const props: any = {
|
||||
name: "test",
|
||||
options: options,
|
||||
defaultValue: { label: "Option 3", value: "option3" },
|
||||
onChange: onChangeMock,
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
singleValue: () => "w-1",
|
||||
valueContainer: () => "w-2",
|
||||
control: () => "w-3",
|
||||
input: () => "w-4",
|
||||
option: () => "w-5",
|
||||
menuList: () => "w-6",
|
||||
menu: () => "w-7",
|
||||
multiValue: () => "w-8",
|
||||
};
|
||||
|
||||
const renderSelectWithForm = (newProps?: any) => {
|
||||
render(
|
||||
<form aria-label="test-form">
|
||||
<label htmlFor="test">Test</label>
|
||||
<Select {...props} {...newProps} inputId="test" />
|
||||
<p>Click Outside</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const selectOption = async (optionText: string) => {
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
screen.getByText(optionText);
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
};
|
||||
|
||||
describe("Tests for Select File", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Test Select Component", () => {
|
||||
describe("Tests the default Behavior of Select component", () => {
|
||||
beforeEach(() => {
|
||||
renderSelectWithForm();
|
||||
});
|
||||
|
||||
test("Should render with the correct default value", async () => {
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should select a correct option", async () => {
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
});
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "select-option",
|
||||
name: "test",
|
||||
option: undefined,
|
||||
})
|
||||
);
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option2" });
|
||||
});
|
||||
|
||||
test("Should keep the default value after selections are shown and the user clicks outside the selection element", async () => {
|
||||
await waitFor(async () => {
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
|
||||
screen.getByText("Option 2");
|
||||
const outsideButton = screen.getByText("Click Outside");
|
||||
fireEvent.click(outsideButton);
|
||||
});
|
||||
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should keep the selected value after the user has selected an option and clicked out of the selection element", async () => {
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
const outsideButton = screen.getByText("Click Outside");
|
||||
fireEvent.click(outsideButton);
|
||||
});
|
||||
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option2" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the Select Component with isMulti", () => {
|
||||
test("Should have the right behavior when it has the prop isMulti", async () => {
|
||||
renderSelectWithForm({ isMulti: true });
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
});
|
||||
|
||||
const option2Selected = screen.getByLabelText("Remove Option 2");
|
||||
const option3Selected = screen.getAllByLabelText("Remove Option 3");
|
||||
|
||||
expect(option2Selected).toBeInTheDocument();
|
||||
expect(option3Selected.length).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.click(option2Selected);
|
||||
|
||||
expect(option2Selected).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the classes and CSS of the Select component", () => {
|
||||
test("Should render classes correctly when isDisabled is true", async () => {
|
||||
renderSelectWithForm({ isDisabled: true });
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const valueContainerEl = singleValueEl.parentElement;
|
||||
const cotrolEl = valueContainerEl?.parentElement;
|
||||
|
||||
expect(cotrolEl).toHaveClass("bg-subtle");
|
||||
});
|
||||
|
||||
test("Should render classes correctly when classNames props is passed", async () => {
|
||||
renderSelectWithForm({ classNames });
|
||||
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const valueContainerEl = singleValueEl.parentElement;
|
||||
const cotrolEl = valueContainerEl?.parentElement;
|
||||
const inputEl = screen.getByRole("combobox", { hidden: true }).parentElement;
|
||||
|
||||
expect(singleValueEl).toHaveClass("w-1");
|
||||
expect(valueContainerEl).toHaveClass("w-2");
|
||||
expect(cotrolEl).toHaveClass("w-3");
|
||||
expect(inputEl).toHaveClass("w-4");
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
screen.getByText(optionText);
|
||||
});
|
||||
const optionEl = screen.getByText("Option 2").parentElement?.parentElement;
|
||||
const menuListEl = optionEl?.parentElement;
|
||||
const menuEl = menuListEl?.parentElement;
|
||||
|
||||
expect(optionEl).toHaveClass("w-5");
|
||||
expect(menuListEl).toHaveClass("w-6");
|
||||
expect(menuEl).toHaveClass("w-7");
|
||||
});
|
||||
|
||||
test("Should render classes correctly for multiValue when classNames and isMulti props are passed and menu is open", async () => {
|
||||
renderSelectWithForm({ classNames, isMulti: true });
|
||||
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const multiValueEl = singleValueEl.parentElement;
|
||||
|
||||
expect(singleValueEl).not.toHaveClass("w-1");
|
||||
expect(multiValueEl).toHaveClass("w-8");
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
});
|
||||
|
||||
const option2 = screen.getByText(options[1].label);
|
||||
const menuIsOpenEl = option2.parentElement?.parentElement?.nextSibling;
|
||||
expect(menuIsOpenEl).toHaveClass(
|
||||
"[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform "
|
||||
);
|
||||
});
|
||||
|
||||
test("Should render classes correctly when focused, selected and menu is open", async () => {
|
||||
renderSelectWithForm();
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
option.focus();
|
||||
});
|
||||
const option1 = screen.getByText("Option 1");
|
||||
const option1Parent = option1.parentElement?.parentElement;
|
||||
const option3 = screen.getAllByText("Option 3");
|
||||
const option3Parent = option3[1].parentElement?.parentElement;
|
||||
const menuIsOpenEl = option3[0].parentElement?.nextSibling;
|
||||
|
||||
expect(option1).toBeInTheDocument();
|
||||
expect(option1Parent).toHaveClass("bg-subtle");
|
||||
expect(option3[1]).toBeInTheDocument();
|
||||
expect(option3Parent).toHaveClass("bg-emphasis text-default");
|
||||
expect(menuIsOpenEl).toHaveClass("rotate-180 transition-transform");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for SelectField component", () => {
|
||||
const renderSelectField = (newProps?: any) => {
|
||||
render(
|
||||
<form aria-label="test-form">
|
||||
<SelectField {...{ ...props, ...newProps }} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
test("Should render name as fallback label", () => {
|
||||
renderSelectField();
|
||||
expect(screen.getByText(props.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should not render the label element when label not passed and name is undefined", () => {
|
||||
renderSelectField({ name: undefined });
|
||||
expect(screen.queryByRole("label")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render with the default value and label", () => {
|
||||
renderSelectField({ label: "Test SelectField", name: "test" });
|
||||
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
|
||||
const labelElement = screen.getByText("Test SelectField");
|
||||
expect(labelElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for SelectWithValidation component", () => {
|
||||
const handleSubmit = vi.fn((event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
const renderSelectWithValidation = (required: boolean) => {
|
||||
render(
|
||||
<form onSubmit={handleSubmit} aria-label="test-form">
|
||||
<label htmlFor="test">Test</label>
|
||||
<SelectWithValidation {...props} required={required} inputId="test" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
test("Should render with the default value", () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should render an input element with the value passed as prop", () => {
|
||||
renderSelectWithValidation(false);
|
||||
expect(screen.getAllByDisplayValue("option3")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("Should submit the form with the selected value after validation", async () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("Should fail to submit if nothing is selected", async () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
26
calcom/packages/ui/components/form/select/selectTheme.ts
Normal file
26
calcom/packages/ui/components/form/select/selectTheme.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { GroupBase, SelectComponentsConfig, MenuPlacement } from "react-select";
|
||||
|
||||
import { InputComponent, OptionComponent, ControlComponent } from "./components";
|
||||
|
||||
export const getReactSelectProps = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
components,
|
||||
menuPlacement = "auto",
|
||||
}: {
|
||||
components: SelectComponentsConfig<Option, IsMulti, Group>;
|
||||
menuPlacement?: MenuPlacement;
|
||||
}) => {
|
||||
return {
|
||||
menuPlacement,
|
||||
components: {
|
||||
Input: InputComponent,
|
||||
Option: OptionComponent,
|
||||
Control: ControlComponent,
|
||||
...components,
|
||||
},
|
||||
unstyled: true,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user