first commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import { Root as ToggleGroupPrimitive, Item as ToggleGroupItemPrimitive } from "@radix-ui/react-toggle-group";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
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
|
||||
const [yesNoValue, setYesNoValue] = useState<"yes" | "no" | undefined>(yesNo(value));
|
||||
|
||||
if (!yesNoValue) {
|
||||
setYesNoValue(yesNo(defaultValue));
|
||||
onValueChange(defaultValue);
|
||||
return null;
|
||||
}
|
||||
const commonClass = classNames(
|
||||
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 (
|
||||
<ToggleGroupPrimitive
|
||||
value={yesNoValue}
|
||||
type="single"
|
||||
disabled={disabled}
|
||||
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));
|
||||
}}
|
||||
{...passThrough}>
|
||||
<ToggleGroupItemPrimitive
|
||||
className={classNames(boolean(yesNoValue) ? selectedClass : unselectedClass)}
|
||||
disabled={disabled}
|
||||
value="yes">
|
||||
Yes
|
||||
</ToggleGroupItemPrimitive>
|
||||
|
||||
<ToggleGroupItemPrimitive
|
||||
disabled={disabled}
|
||||
className={classNames(!boolean(yesNoValue) ? selectedClass : unselectedClass)}
|
||||
value="no">
|
||||
No
|
||||
</ToggleGroupItemPrimitive>
|
||||
</ToggleGroupPrimitive>
|
||||
);
|
||||
};
|
||||
|
||||
export const BooleanToggleGroupField = function BooleanToggleGroupField(
|
||||
props: Parameters<typeof BooleanToggleGroup>[0] & {
|
||||
label?: string;
|
||||
containerClassName?: string;
|
||||
name?: string;
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
className?: string;
|
||||
error?: string;
|
||||
}
|
||||
) {
|
||||
const { t } = useLocale();
|
||||
const { label = t(props.name || ""), containerClassName, labelProps, className, ...passThrough } = props;
|
||||
const id = useId();
|
||||
return (
|
||||
<div className={classNames(containerClassName)}>
|
||||
<div className={className}>
|
||||
{!!label && (
|
||||
<Label htmlFor={id} {...labelProps} className={classNames(props.error && "text-error", "mt-4")}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
<BooleanToggleGroup {...passThrough} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
|
||||
interface ToggleGroupProps extends Omit<RadixToggleGroup.ToggleGroupSingleProps, "type"> {
|
||||
options: {
|
||||
value: string;
|
||||
label: string | ReactNode;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
iconLeft?: ReactNode;
|
||||
}[];
|
||||
isFullWidth?: boolean;
|
||||
}
|
||||
|
||||
const OptionalTooltipWrapper = ({
|
||||
children,
|
||||
tooltipText,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tooltipText?: ReactNode;
|
||||
}) => {
|
||||
if (tooltipText) {
|
||||
return (
|
||||
<Tooltip delayDuration={150} sideOffset={12} side="bottom" content={tooltipText}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const ToggleGroup = ({
|
||||
options,
|
||||
onValueChange,
|
||||
isFullWidth,
|
||||
customClassNames,
|
||||
...props
|
||||
}: ToggleGroupProps & { customClassNames?: string }) => {
|
||||
return (
|
||||
<>
|
||||
<RadixToggleGroup.Root
|
||||
type="single"
|
||||
{...props}
|
||||
onValueChange={onValueChange}
|
||||
className={classNames(
|
||||
`min-h-9 border-default bg-default relative inline-flex gap-0.5 rounded-md border p-1 rtl:flex-row-reverse`,
|
||||
props.className,
|
||||
isFullWidth && "w-full",
|
||||
customClassNames
|
||||
)}>
|
||||
{options.map((option) => (
|
||||
<OptionalTooltipWrapper key={option.value} tooltipText={option.tooltip}>
|
||||
<RadixToggleGroup.Item
|
||||
disabled={option.disabled}
|
||||
value={option.value}
|
||||
data-testid={`toggle-group-item-${option.value}`}
|
||||
className={classNames(
|
||||
"aria-checked:bg-emphasis relative rounded-[4px] px-3 py-1 text-sm leading-tight transition",
|
||||
option.disabled
|
||||
? "text-gray-400 hover:cursor-not-allowed"
|
||||
: "text-default [&[aria-checked='false']]:hover:text-emphasis",
|
||||
isFullWidth && "w-full"
|
||||
)}>
|
||||
<div className="item-center flex justify-center ">
|
||||
{option.iconLeft && <span className="mr-2 flex h-4 w-4 items-center">{option.iconLeft}</span>}
|
||||
{option.label}
|
||||
</div>
|
||||
</RadixToggleGroup.Item>
|
||||
</OptionalTooltipWrapper>
|
||||
))}
|
||||
</RadixToggleGroup.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
2
calcom/packages/ui/components/form/toggleGroup/index.ts
Normal file
2
calcom/packages/ui/components/form/toggleGroup/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ToggleGroup } from "./ToggleGroup";
|
||||
export { BooleanToggleGroup, BooleanToggleGroupField } from "./BooleanToggleGroup";
|
||||
@@ -0,0 +1,108 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
CustomArgsTable,
|
||||
Examples,
|
||||
Example,
|
||||
Title,
|
||||
VariantsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import { ToggleGroup } from "./ToggleGroup";
|
||||
|
||||
<Meta title="UI/Form/ToggleGroup" component={ToggleGroup} />
|
||||
|
||||
<Title title="ToggleGroup" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `ToggleGroup` component is used to create a group of toggle items with optional tooltips.
|
||||
|
||||
## Structure
|
||||
|
||||
<CustomArgsTable of={ToggleGroup} />
|
||||
|
||||
## Examples
|
||||
|
||||
<Examples title="Toggle Group With Icon Left">
|
||||
<Example>
|
||||
<TooltipProvider>
|
||||
<ToggleGroup
|
||||
options={[
|
||||
{ value: "option1", label: "Option 1", tooltip: "Tooltip for Option 1", iconLeft: <Icon name="arrow-right" /> },
|
||||
{ value: "option2", label: "Option 2", iconLeft: <Icon name="arrow-right" /> },
|
||||
{ value: "option3", label: "Option 3", iconLeft: <Icon name="arrow-right" /> },
|
||||
{
|
||||
value: "option4",
|
||||
label: "Option 4",
|
||||
tooltip: "Tooltip for Option 4",
|
||||
iconLeft: <Icon name="arrow-right" />,
|
||||
},
|
||||
{ value: "option5", label: "Option 5", iconLeft: <Icon name="arrow-right" />, disabled: true },
|
||||
]}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</Example>
|
||||
</Examples>
|
||||
|
||||
## ToggleGroup Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
options: [
|
||||
{ value: "option1", label: "Option 1", tooltip: "Tooltip for Option 1" },
|
||||
{ value: "option2", label: "Option 2" },
|
||||
{ value: "option3", label: "Option 3" },
|
||||
{
|
||||
value: "option4",
|
||||
label: "Option 4",
|
||||
tooltip: "Tooltip for Option 4",
|
||||
},
|
||||
{ value: "option5", label: "Option 5", disabled: true },
|
||||
],
|
||||
}}
|
||||
argTypes={{
|
||||
options: {
|
||||
value: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
lable: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
isFullWidth: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{({ options, isFullWidth }) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<TooltipProvider>
|
||||
<ToggleGroup options={options} isFullWidth={isFullWidth} />
|
||||
</TooltipProvider>
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
Reference in New Issue
Block a user