refactor: radio area group (#9113)

* refactor: radio area group

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: remove select

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: e2e test

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Udit Takkar 2023-05-25 23:54:42 +05:30 committed by GitHub
parent e36252f677
commit 30469f486f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 117 deletions

View File

@ -39,10 +39,10 @@ test.describe("Managed Event Types tests", () => {
await page.click("[data-testid=new-event-type-dropdown]");
await page.click("[data-testid=option-team-1]");
// Expecting we can add a managed event type as team owner
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
// Actually creating a managed event type to test things further
await page.click('input[value="MANAGED"]');
await page.click('button[value="MANAGED"]');
await page.fill("[name=title]", "managed");
await page.click("[type=submit]");
});

View File

@ -254,6 +254,9 @@ export default function CreateEventTypeDialog({
/>
)}
<RadioArea.Group
onValueChange={(val: SchedulingType) => {
form.setValue("schedulingType", val);
}}
className={classNames(
"mt-1 flex gap-4",
isAdmin && flags["managed-event-types"] && "flex-col"

View File

@ -1,59 +1,57 @@
import React from "react";
import { useId } from "@radix-ui/react-id";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import type { ReactNode } from "react";
import classNames from "@calcom/lib/classNames";
type RadioAreaProps = React.InputHTMLAttributes<HTMLInputElement> & { classNames?: { container?: string } };
type RadioAreaProps = RadioGroupPrimitive.RadioGroupItemProps & {
children: ReactNode;
classNames?: { container?: string };
};
const RadioArea = React.forwardRef<HTMLInputElement, RadioAreaProps>(
({ children, className, classNames: innerClassNames, ...props }, ref) => {
return (
<label className={classNames("relative flex", className)}>
<input
ref={ref}
className="text-emphasis bg-subtle border-emphasis focus:ring-none peer absolute top-[0.9rem] left-3 align-baseline"
type="radio"
{...props}
/>
<div
className={classNames(
"text-default peer-checked:border-emphasis border-subtle rounded-md border p-4 pt-3 pl-10",
innerClassNames?.container
)}>
{children}
</div>
</label>
);
}
);
type MaybeArray<T> = T[] | T;
type ChildrenOfType<T extends React.ElementType> = MaybeArray<
React.ReactElement<React.ComponentPropsWithoutRef<T>>
>;
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
onChange?: (value: string) => void;
children: ChildrenOfType<typeof RadioArea>;
}
const RadioArea = ({ children, className, classNames: innerClassNames, ...props }: RadioAreaProps) => {
const radioAreaId = useId();
const id = props.id ?? radioAreaId;
const RadioAreaGroup = ({ children, className, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
const childrenWithProps = React.Children.map(children, (child) => {
if (onChange && React.isValidElement(child)) {
return React.cloneElement(child, {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
},
});
}
return child;
});
return (
<div className={className} {...passThroughProps}>
{childrenWithProps}
<div
className={classNames(
"border-subtle [&:has(input:checked)]:border-emphasis relative flex items-start rounded-md border",
className
)}>
<RadioGroupPrimitive.Item
id={id}
{...props}
className={classNames(
"hover:bg-subtle border-default focus:ring-emphasis absolute top-[0.9rem] left-3 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2",
props.disabled && "opacity-60"
)}>
<RadioGroupPrimitive.Indicator
className={classNames(
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
props.disabled ? "after:bg-muted" : "bg-black"
)}
/>
</RadioGroupPrimitive.Item>
<label htmlFor={id} className={classNames("text-default p-4 pt-3 pl-10", innerClassNames?.container)}>
{children}
</label>
</div>
);
};
RadioAreaGroup.displayName = "RadioAreaGroup";
RadioArea.displayName = "RadioArea";
const RadioAreaGroup = ({
children,
className,
onValueChange,
...passThroughProps
}: RadioGroupPrimitive.RadioGroupProps) => {
return (
<RadioGroupPrimitive.Root className={className} onValueChange={onValueChange} {...passThroughProps}>
{children}
</RadioGroupPrimitive.Root>
);
};
const Item = RadioArea;
const Group = RadioAreaGroup;

View File

@ -1,68 +0,0 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import React from "react";
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ChevronDown } from "@calcom/ui/components/icon";
import { RadioArea, RadioAreaGroup } from "./RadioAreaGroup";
interface OptionProps
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
description?: string;
}
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
interface RadioAreaSelectProps<TFieldValues extends FieldValues>
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "form"> {
options: OptionProps[]; // allow options to be passed programmatically, like options={}
onChange?: (value: string) => void;
form: UseFormReturn<TFieldValues>;
name: FieldPath<TFieldValues>;
}
export const Select = function RadioAreaSelect<TFieldValues extends FieldValues>(
props: RadioAreaSelectProps<TFieldValues>
) {
const { t } = useLocale();
const {
options,
form,
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
placeholder = t("select"),
} = props;
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
options.find((option: OptionProps) => option.value === value)?.label;
return (
<Collapsible className={classNames("w-full", props.className)}>
<CollapsibleTrigger
type="button"
disabled={disabled}
className={classNames(
"focus:ring-primary-500 border-default bg-default mb-1 block w-full cursor-pointer rounded-sm border border p-2 text-left shadow-sm sm:text-sm",
disabled && "bg-emphasis cursor-default focus:ring-0 "
)}>
{getLabel(props.value) ?? placeholder}
<ChevronDown className="text-subtle float-right h-5 w-5" />
</CollapsibleTrigger>
<CollapsibleContent>
<RadioAreaGroup className="space-y-2 text-sm" onChange={props.onChange}>
{options.map((option) => (
<RadioArea
{...form.register(props.name)}
{...option}
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}>
<strong className="mb-1 block">{option.label}</strong>
<p>{option.description}</p>
</RadioArea>
))}
</RadioAreaGroup>
</CollapsibleContent>
</Collapsible>
);
};
export default Select;

View File

@ -1,3 +1,2 @@
export * as RadioGroup from "./RadioAreaGroup";
export { default as Select } from "./Select";
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";