fix: Update forms UI (#8093)
* Routing Forms: Update forms UI * fix failing test * fix: Routing Forms forms UI Update * fix: Routing Forms forms UI Update * Apply suggestions from code review --------- Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
gitstart-calcom
Peer Richelsen
Omar López
Hariom Balhara
parent
9d91d72a72
commit
175f2482e1
@@ -1713,6 +1713,7 @@
|
||||
"spot_popular_event_types": "Spot popular event types",
|
||||
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
|
||||
"no_responses_yet": "No responses yet",
|
||||
"no_routes_defined": "No routes defined",
|
||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||
"error_booking_event": "An error occured when booking the event, please refresh the page and try again",
|
||||
"timeslot_missing_title": "No timeslot selected",
|
||||
|
||||
@@ -34,10 +34,18 @@ import {
|
||||
Tooltip,
|
||||
VerticalDivider,
|
||||
} from "@calcom/ui";
|
||||
import { ExternalLink, Link as LinkIcon, Download, Code, Trash } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
ExternalLink,
|
||||
Link as LinkIcon,
|
||||
Download,
|
||||
Code,
|
||||
Trash,
|
||||
MessageCircle,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { RoutingPages } from "../lib/RoutingPages";
|
||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||
import { isFallbackRoute } from "../lib/isFallbackRoute";
|
||||
import { processRoute } from "../lib/processRoute";
|
||||
import type { Response, Route, SerializableForm } from "../types/types";
|
||||
import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions";
|
||||
@@ -371,18 +379,26 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
|
||||
{t("test_preview")}
|
||||
</Button>
|
||||
</div>
|
||||
{form.routes?.every(isFallbackRoute) && (
|
||||
<Alert
|
||||
className="mt-6 !bg-orange-100 font-semibold text-orange-900"
|
||||
iconClassName="!text-orange-900"
|
||||
severity="neutral"
|
||||
title={t("no_routes_defined")}
|
||||
/>
|
||||
)}
|
||||
{!form._count?.responses && (
|
||||
<>
|
||||
<Alert
|
||||
className="mt-6"
|
||||
className="mt-2 px-4 py-3"
|
||||
severity="neutral"
|
||||
title={t("no_responses_yet")}
|
||||
message={t("responses_collection_waiting_description")}
|
||||
CustomIcon={MessageCircle}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-subtle w-full rounded-md border p-8">
|
||||
<div className="border-subtle bg-muted w-full rounded-md border p-8">
|
||||
<RoutingNavBar appUrl={appUrl} form={form} />
|
||||
<Page hookForm={hookForm} form={form} appUrl={appUrl} />
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
Button,
|
||||
EmptyScreen,
|
||||
FormCard,
|
||||
Label,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
Skeleton,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { Plus, FileText } from "@calcom/ui/components/icon";
|
||||
import { Plus, FileText, X } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -27,6 +28,7 @@ import SingleForm, {
|
||||
|
||||
export { getServerSideProps };
|
||||
type HookForm = UseFormReturn<RoutingFormWithResponseCount>;
|
||||
type SelectOption = { placeholder: string; value: string };
|
||||
|
||||
export const FieldTypes = [
|
||||
{
|
||||
@@ -42,11 +44,11 @@ export const FieldTypes = [
|
||||
value: "textarea",
|
||||
},
|
||||
{
|
||||
label: "Select",
|
||||
label: "Single Selection",
|
||||
value: "select",
|
||||
},
|
||||
{
|
||||
label: "MultiSelect",
|
||||
label: "Multiple Selection",
|
||||
value: "multiselect",
|
||||
},
|
||||
{
|
||||
@@ -85,30 +87,70 @@ function Field({
|
||||
};
|
||||
appUrl: string;
|
||||
}) {
|
||||
const [identifier, _setIdentifier] = useState(hookForm.getValues(`${hookFieldNamespace}.identifier`));
|
||||
const { t } = useLocale();
|
||||
|
||||
const setUserChangedIdentifier = (val: string) => {
|
||||
_setIdentifier(val);
|
||||
// Also, update the form identifier so tha it can be persisted
|
||||
hookForm.setValue(`${hookFieldNamespace}.identifier`, val);
|
||||
const [options, setOptions] = useState<SelectOption[]>([
|
||||
{ placeholder: "< 10", value: "" },
|
||||
{ placeholder: "10-100", value: "" },
|
||||
{ placeholder: "100-500", value: "" },
|
||||
{ placeholder: "> 500", value: "" },
|
||||
]);
|
||||
|
||||
const handleRemoveOptions = (index: number) => {
|
||||
const updatedOptions = options.filter((_, i) => i !== index);
|
||||
setOptions(updatedOptions);
|
||||
updateSelectText(updatedOptions);
|
||||
};
|
||||
|
||||
const label = hookForm.watch(`${hookFieldNamespace}.label`);
|
||||
const handleAddOptions = () => {
|
||||
setOptions((prevState) => [
|
||||
...prevState,
|
||||
{
|
||||
placeholder: "New Option",
|
||||
value: "",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hookForm.getValues(`${hookFieldNamespace}.identifier`)) {
|
||||
_setIdentifier(label);
|
||||
const originalValues = hookForm.getValues(`${hookFieldNamespace}.selectText`);
|
||||
if (originalValues) {
|
||||
const values: SelectOption[] = originalValues.split("\n").map((fieldValue) => ({
|
||||
value: fieldValue,
|
||||
placeholder: "",
|
||||
}));
|
||||
setOptions(values);
|
||||
}
|
||||
}, [label, hookFieldNamespace, hookForm]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const router = hookForm.getValues(`${hookFieldNamespace}.router`);
|
||||
const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, optionIndex: number) => {
|
||||
const updatedOptions = options.map((opt, index) => ({
|
||||
...opt,
|
||||
...(index === optionIndex ? { value: e.target.value } : {}),
|
||||
}));
|
||||
setOptions(updatedOptions);
|
||||
updateSelectText(updatedOptions);
|
||||
};
|
||||
|
||||
const updateSelectText = (updatedOptions: SelectOption[]) => {
|
||||
hookForm.setValue(
|
||||
`${hookFieldNamespace}.selectText`,
|
||||
updatedOptions
|
||||
.filter((opt) => opt.value)
|
||||
.map((opt) => opt.value)
|
||||
.join("\n")
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
data-testid="field"
|
||||
className="group mb-4 flex w-full items-center justify-between ltr:mr-2 rtl:ml-2">
|
||||
className="bg-default group mb-4 flex w-full items-center justify-between ltr:mr-2 rtl:ml-2">
|
||||
<FormCard
|
||||
label={label || `Field ${fieldIndex + 1}`}
|
||||
label="Field"
|
||||
moveUp={moveUp}
|
||||
moveDown={moveDown}
|
||||
badge={
|
||||
@@ -120,6 +162,7 @@ function Field({
|
||||
<TextField
|
||||
disabled={!!router}
|
||||
label="Label"
|
||||
className="flex-grow"
|
||||
placeholder={t("this_is_what_your_users_would_see")}
|
||||
/**
|
||||
* This is a bit of a hack to make sure that for routerField, label is shown from there.
|
||||
@@ -137,18 +180,16 @@ function Field({
|
||||
name={`${hookFieldNamespace}.identifier`}
|
||||
required
|
||||
placeholder={t("identifies_name_field")}
|
||||
value={identifier}
|
||||
defaultValue={routerField?.identifier || routerField?.label}
|
||||
onChange={(e) => setUserChangedIdentifier(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 w-full">
|
||||
<TextField
|
||||
disabled={!!router}
|
||||
label={t("placeholder")}
|
||||
placeholder={t("this_will_be_the_placeholder")}
|
||||
defaultValue={routerField?.placeholder}
|
||||
{...hookForm.register(`${hookFieldNamespace}.placeholder`)}
|
||||
//This change has the same effects that already existed in relation to this field,
|
||||
// but written in a different way.
|
||||
// The identifier field will have the same value as the label field until it is changed
|
||||
defaultValue={
|
||||
hookForm.watch(`${hookFieldNamespace}.identifier`) ||
|
||||
hookForm.watch(`${hookFieldNamespace}.label`)
|
||||
}
|
||||
onChange={(e) => {
|
||||
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 w-full ">
|
||||
@@ -160,6 +201,17 @@ function Field({
|
||||
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
|
||||
return (
|
||||
<SelectField
|
||||
maxMenuHeight={200}
|
||||
styles={{
|
||||
singleValue: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
option: (baseStyles) => ({
|
||||
...baseStyles,
|
||||
fontSize: "14px",
|
||||
}),
|
||||
}}
|
||||
label="Type"
|
||||
isDisabled={!!router}
|
||||
containerClassName="data-testid-field-type"
|
||||
@@ -177,21 +229,48 @@ function Field({
|
||||
/>
|
||||
</div>
|
||||
{["select", "multiselect"].includes(hookForm.watch(`${hookFieldNamespace}.type`)) ? (
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<div className="w-full">
|
||||
<TextAreaField
|
||||
disabled={!!router}
|
||||
rows={3}
|
||||
label="Options"
|
||||
defaultValue={routerField?.selectText}
|
||||
placeholder={t("add_1_option_per_line")}
|
||||
{...hookForm.register(`${hookFieldNamespace}.selectText`)}
|
||||
/>
|
||||
<div className="mt-2 w-full">
|
||||
<Skeleton as={Label} loadingClassName="w-16" title={t("Options")}>
|
||||
{t("options")}
|
||||
</Skeleton>
|
||||
{options.map((field, index) => (
|
||||
<div key={`select-option-${index}`}>
|
||||
<TextField
|
||||
disabled={!!router}
|
||||
containerClassName="[&>*:first-child]:border [&>*:first-child]:border-default hover:[&>*:first-child]:border-gray-400"
|
||||
className="border-0 focus:ring-0 focus:ring-offset-0"
|
||||
labelSrOnly
|
||||
placeholder={field.placeholder.toString()}
|
||||
value={field.value}
|
||||
type="text"
|
||||
addOnClassname="bg-transparent border-0"
|
||||
onChange={(e) => handleChange(e, index)}
|
||||
addOnSuffix={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveOptions(index)}
|
||||
aria-label={t("remove")}>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={classNames("flex")}>
|
||||
<Button
|
||||
data-testid="add-attribute"
|
||||
className="border-none"
|
||||
type="button"
|
||||
StartIcon={Plus}
|
||||
color="secondary"
|
||||
onClick={handleAddOptions}>
|
||||
Add an option
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="w-full">
|
||||
<div className="w-[106px]">
|
||||
<Controller
|
||||
name={`${hookFieldNamespace}.required`}
|
||||
control={hookForm.control}
|
||||
@@ -199,6 +278,7 @@ function Field({
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<BooleanToggleGroupField
|
||||
variant="small"
|
||||
disabled={!!router}
|
||||
label={t("required")}
|
||||
value={value}
|
||||
@@ -256,7 +336,7 @@ const FormEdit = ({
|
||||
return hookFormFields.length ? (
|
||||
<div className="flex flex-col-reverse lg:flex-row">
|
||||
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||
<div ref={animationRef} className="flex w-full flex-col">
|
||||
<div ref={animationRef} className="flex w-full flex-col rounded-md">
|
||||
{hookFormFields.map((field, key) => {
|
||||
return (
|
||||
<Field
|
||||
@@ -298,14 +378,14 @@ const FormEdit = ({
|
||||
StartIcon={Plus}
|
||||
color="secondary"
|
||||
onClick={addField}>
|
||||
Add Field
|
||||
Add field
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<div className="bg-default w-full">
|
||||
<EmptyScreen
|
||||
Icon={FileText}
|
||||
headline="Create your first field"
|
||||
|
||||
@@ -165,7 +165,7 @@ const Reporter = ({ form }: { form: inferSSRProps<typeof getServerSideProps>["fo
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col-reverse md:flex-row">
|
||||
<div className="cal-query-builder w-full ltr:mr-2 rtl:ml-2">
|
||||
<div className="cal-query-builder bg-default w-full ltr:mr-2 rtl:ml-2">
|
||||
<Query
|
||||
{...config}
|
||||
value={query.state.tree}
|
||||
|
||||
@@ -420,7 +420,7 @@ const Routes = ({
|
||||
hookForm.setValue("routes", routesToSave);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse md:flex-row">
|
||||
<div className="bg-default border-subtle flex flex-col-reverse rounded-md border p-8 md:flex-row">
|
||||
<div ref={animationRef} className="w-full ltr:mr-2 rtl:ml-2">
|
||||
{mainRoutes.map((route, key) => {
|
||||
return (
|
||||
|
||||
@@ -136,7 +136,7 @@ test.describe("Routing Forms", () => {
|
||||
label: "Test Field",
|
||||
});
|
||||
const queryString =
|
||||
"firstField=456&Test Field Number=456&Test Field Select=456&Test Field MultiSelect=456&Test Field MultiSelect=789&Test Field Phone=456&Test Field Email=456@example.com";
|
||||
"firstField=456&Test Field Number=456&Test Field Single Selection=456&Test Field Multiple Selection=456&Test Field Multiple Selection=789&Test Field Phone=456&Test Field Email=456@example.com";
|
||||
|
||||
await gotoRoutingLink({ page, queryString });
|
||||
|
||||
@@ -167,8 +167,8 @@ test.describe("Routing Forms", () => {
|
||||
// All other params come from prefill URL
|
||||
expect(url.searchParams.get("Test Field Number")).toBe("456");
|
||||
expect(url.searchParams.get("Test Field Long Text")).toBe("manual-fill");
|
||||
expect(url.searchParams.get("Test Field Select")).toBe("456");
|
||||
expect(url.searchParams.getAll("Test Field MultiSelect")).toMatchObject(["456", "789"]);
|
||||
expect(url.searchParams.get("Test Field Multiple Selection")).toBe("456");
|
||||
expect(url.searchParams.getAll("Test Field Multiple Selection")).toMatchObject(["456", "789"]);
|
||||
expect(url.searchParams.get("Test Field Phone")).toBe("456");
|
||||
expect(url.searchParams.get("Test Field Email")).toBe("456@example.com");
|
||||
});
|
||||
@@ -447,7 +447,7 @@ async function addAllTypesOfFieldsAndSaveForm(
|
||||
|
||||
const { optionsInUi: fieldTypesList } = await verifySelectOptions(
|
||||
{ selector: ".data-testid-field-type", nth: 0 },
|
||||
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
|
||||
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
|
||||
page
|
||||
);
|
||||
|
||||
@@ -507,7 +507,7 @@ export async function addOneFieldAndDescriptionAndSaveForm(
|
||||
// Verify all Options of SelectBox
|
||||
const { optionsInUi: types } = await verifySelectOptions(
|
||||
{ selector: ".data-testid-field-type", nth: 0 },
|
||||
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
|
||||
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
|
||||
page
|
||||
);
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ const ProfileView = () => {
|
||||
<>
|
||||
<Label className="text-emphasis mt-5">{t("about")}</Label>
|
||||
<div
|
||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600 break-words"
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import type { IconType } from "react-icons";
|
||||
|
||||
import { CheckCircle2, Info, XCircle, AlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -14,9 +15,10 @@ export interface AlertProps {
|
||||
iconClassName?: string;
|
||||
// @TODO: Success and info shouldn't exist as per design?
|
||||
severity: "success" | "warning" | "error" | "info" | "neutral";
|
||||
CustomIcon?: IconType;
|
||||
}
|
||||
export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
|
||||
const { severity, iconClassName } = props;
|
||||
const { severity, iconClassName, CustomIcon } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -31,38 +33,45 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
|
||||
severity === "neutral" && "bg-subtle text-default border-none"
|
||||
)}>
|
||||
<div className="relative flex flex-col md:flex-row">
|
||||
<div className="flex-shrink-0">
|
||||
{severity === "error" && (
|
||||
<XCircle
|
||||
className={classNames("h-5 w-5 fill-red-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "warning" && (
|
||||
<AlertTriangle
|
||||
className={classNames("h-5 w-5 fill-yellow-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "info" && (
|
||||
<Info
|
||||
className={classNames("h-5 w-5 fill-sky-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "neutral" && (
|
||||
<Info
|
||||
className={classNames("text-default h-5 w-5 fill-transparent", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "success" && (
|
||||
<CheckCircle2
|
||||
className={classNames("fill-muted h-5 w-5 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{CustomIcon ? (
|
||||
<div className="flex-shrink-0">
|
||||
<CustomIcon aria-hidden="true" className={classNames("text-default h-5 w-5", iconClassName)} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0">
|
||||
{severity === "error" && (
|
||||
<XCircle
|
||||
className={classNames("h-5 w-5 fill-red-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "warning" && (
|
||||
<AlertTriangle
|
||||
className={classNames("h-5 w-5 fill-yellow-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "info" && (
|
||||
<Info
|
||||
className={classNames("h-5 w-5 fill-sky-400 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "neutral" && (
|
||||
<Info
|
||||
className={classNames("text-default h-5 w-5 fill-transparent", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{severity === "success" && (
|
||||
<CheckCircle2
|
||||
className={classNames("fill-muted h-5 w-5 text-white", iconClassName)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-3 flex-grow">
|
||||
<h3 className="text-sm font-medium">{props.title}</h3>
|
||||
<div className="text-sm">{props.message}</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { classNames } from "@calcom/lib";
|
||||
import type { BadgeProps } from "../..";
|
||||
import { Badge } from "../..";
|
||||
import { Divider } from "../divider";
|
||||
import { ArrowDown, ArrowUp, Trash } from "../icon";
|
||||
import { ArrowDown, ArrowUp, Trash2 } from "../icon";
|
||||
|
||||
type Action = { check: () => boolean; fn: () => void };
|
||||
export default function FormCard({
|
||||
@@ -68,7 +68,7 @@ export default function FormCard({
|
||||
deleteField?.fn();
|
||||
}}
|
||||
color="secondary">
|
||||
<Trash className="text-muted h-4 w-4" />
|
||||
<Trash2 className="text-default h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -10,18 +10,38 @@ import { Label } from "../../../components/form/inputs/Label";
|
||||
const boolean = (yesNo: "yes" | "no") => (yesNo === "yes" ? true : yesNo === "no" ? false : undefined);
|
||||
const yesNo = (boolean?: boolean) => (boolean === true ? "yes" : boolean === false ? "no" : undefined);
|
||||
|
||||
type VariantStyles = {
|
||||
commonClass?: string;
|
||||
toggleGroupPrimitiveClass?: string;
|
||||
};
|
||||
|
||||
const getVariantStyles = (variant: string) => {
|
||||
const variants: Record<string, VariantStyles> = {
|
||||
default: {
|
||||
commonClass: "px-4 w-full py-[10px]",
|
||||
},
|
||||
small: {
|
||||
commonClass: "w-[49px] px-3 py-1.5",
|
||||
toggleGroupPrimitiveClass: "space-x-1",
|
||||
},
|
||||
};
|
||||
return variants[variant];
|
||||
};
|
||||
|
||||
export const BooleanToggleGroup = function BooleanToggleGroup({
|
||||
defaultValue = true,
|
||||
value,
|
||||
disabled = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onValueChange = () => {},
|
||||
variant = "default",
|
||||
...passThrough
|
||||
}: {
|
||||
defaultValue?: boolean;
|
||||
value?: boolean;
|
||||
onValueChange?: (value?: boolean) => void;
|
||||
disabled?: boolean;
|
||||
variant?: "default" | "small";
|
||||
}) {
|
||||
// Maintain a state because it is not necessary that onValueChange the parent component would re-render. Think react-hook-form
|
||||
// Also maintain a string as boolean isn't accepted as ToggleGroupPrimitive value
|
||||
@@ -33,9 +53,11 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
|
||||
return null;
|
||||
}
|
||||
const commonClass = classNames(
|
||||
"w-full inline-flex items-center justify-center rounded py-[10px] px-4 text-sm font-medium leading-4",
|
||||
getVariantStyles(variant).commonClass,
|
||||
"inline-flex items-center justify-center rounded text-sm font-medium leading-4",
|
||||
disabled && "cursor-not-allowed"
|
||||
);
|
||||
|
||||
const selectedClass = classNames(commonClass, "bg-emphasis text-emphasis");
|
||||
const unselectedClass = classNames(commonClass, "text-default hover:bg-subtle hover:text-emphasis");
|
||||
return (
|
||||
@@ -43,7 +65,10 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
|
||||
value={yesNoValue}
|
||||
type="single"
|
||||
disabled={disabled}
|
||||
className="border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse"
|
||||
className={classNames(
|
||||
"border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse",
|
||||
getVariantStyles(variant).toggleGroupPrimitiveClass
|
||||
)}
|
||||
onValueChange={(yesNoValue: "yes" | "no") => {
|
||||
setYesNoValue(yesNoValue);
|
||||
onValueChange(boolean(yesNoValue));
|
||||
|
||||
@@ -31,7 +31,7 @@ const HorizontalTabItem = function ({ name, href, linkProps, avatar, ...props }:
|
||||
href={href}
|
||||
{...linkProps}
|
||||
className={classNames(
|
||||
isCurrent ? "bg-subtle text-emphasis" : " hover:bg-subtle hover:text-emphasis text-default ",
|
||||
isCurrent ? "bg-emphasis text-emphasis" : "hover:bg-subtle hover:text-emphasis text-default",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-[6px] p-2 text-sm font-medium leading-4 md:mb-0",
|
||||
props.disabled && "pointer-events-none !opacity-30",
|
||||
props.className
|
||||
|
||||
@@ -15,7 +15,7 @@ const HorizontalTabs = function ({ tabs, linkProps, actions, ...props }: NavTabP
|
||||
aria-label="Tabs"
|
||||
{...props}>
|
||||
{tabs.map((tab, idx) => (
|
||||
<HorizontalTabItem {...tab} key={idx} {...linkProps} />
|
||||
<HorizontalTabItem className="py-2.5 px-4" {...tab} key={idx} {...linkProps} />
|
||||
))}
|
||||
</nav>
|
||||
{actions && actions}
|
||||
|
||||
Reference in New Issue
Block a user