refactor(ui): remove unused components and exports (#26222)

Co-authored-by: Volnei Munhoz <volnei@cal.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Abhay Mishra
2026-01-02 04:11:14 +00:00
committed by GitHub
co-authored by Volnei Munhoz Keith Williams
parent c7a80abcba
commit 18b041514f
17 changed files with 1 additions and 710 deletions
@@ -1,215 +0,0 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { render, fireEvent, screen } from "@testing-library/react";
import * as React from "react";
import type { UseFormReturn } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { expect, vi } from "vitest";
import PhoneInput from "@calcom/features/components/phone-input/PhoneInput";
import { getBookingFieldsWithSystemFields } from "../../../lib/getBookingFields";
import { BookingFields } from "./BookingFields";
// Mock PhoneInput to avoid calling the lazy import
vi.mock("@calcom/features/components/phone-input", () => {
return {
default: PhoneInput,
};
});
vi.mock("@calcom/ui/components/address", async (originalImport) => {
const { AddressInputNonLazy } = (await originalImport()) as Record<string, unknown>;
// Dynamic imports of Components are not supported in Vitest. So, we use the non-lazy version of the components
return {
AddressInput: AddressInputNonLazy,
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FormMethods = UseFormReturn<any>;
// Add tRPC mock before tests
vi.mock("@calcom/trpc/react", () => ({
trpc: {
viewer: {
public: {
countryCode: {
useQuery: () => ({
data: { countryCode: "US" },
isLoading: false,
error: null,
}),
},
},
},
},
}));
const renderComponent = ({
props: props,
formDefaultValues,
}: {
props: Parameters<typeof BookingFields>[0];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formDefaultValues?: any;
}) => {
let formMethods: UseFormReturn | undefined;
const Wrapper = ({ children }: { children: React.ReactNode }) => {
const form = useForm({
defaultValues: formDefaultValues,
});
formMethods = form;
return (
<TooltipProvider>
<FormProvider {...form}>{children}</FormProvider>
</TooltipProvider>
);
};
const result = render(<BookingFields {...props} />, { wrapper: Wrapper });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return { result, formMethods: formMethods! };
};
describe("BookingFields", () => {
it("should correctly render with location fields", () => {
const AttendeePhoneNumberOption = {
label: "attendee_phone_number",
value: "phone",
};
const OrganizerLinkOption = {
label: "https://google.com",
value: "link",
};
const locations = [
{
type: AttendeePhoneNumberOption.value,
},
{
link: "https://google.com",
type: OrganizerLinkOption.value,
displayLocationPublicly: true,
},
];
const { formMethods } = renderComponent({
props: {
fields: getBookingFieldsWithSystemFields({
disableGuests: false,
bookingFields: [],
metadata: null,
workflows: [],
customInputs: [],
}),
locations,
isDynamicGroupBooking: false,
bookingData: null,
},
formDefaultValues: {},
});
component.fillName({ value: "John Doe" });
component.fillEmail({ value: "john.doe@example.com" });
component.fillNotes({ value: "This is a note" });
expectScenarios.expectNameToBe({ value: "John Doe", formMethods });
expectScenarios.expectEmailToBe({ value: "john.doe@example.com", formMethods });
expectScenarios.expectNotesToBe({ value: "This is a note", formMethods });
component.fillRadioInputLocation({ label: AttendeePhoneNumberOption.label, inputValue: "+1234567890" });
expectScenarios.expectLocationToBe({
formMethods,
label: AttendeePhoneNumberOption.label,
toMatch: {
formattedValue: "+1 (234) 567-890",
value: { optionValue: "+1234567890", value: AttendeePhoneNumberOption.value },
},
});
component.fillRadioInputLocation({ label: OrganizerLinkOption.label });
expectScenarios.expectLocationToBe({
formMethods,
label: OrganizerLinkOption.label,
toMatch: {
formattedValue: "+1 (234) 567-890",
value: { optionValue: "", value: OrganizerLinkOption.value },
},
});
});
});
const component = {
getName: ({ label = "your_name" }: { label?: string } = {}) =>
screen.getByRole("textbox", {
name: new RegExp(label),
}) as HTMLInputElement,
getEmail: () => screen.getByRole("textbox", { name: /email/i }) as HTMLInputElement,
getLocationRadioOption: ({ label }: { label: string }) =>
screen.getByRole("radio", { name: new RegExp(label) }) as HTMLInputElement,
getLocationRadioInput: ({ placeholder }: { placeholder: string }) =>
screen.getByPlaceholderText(placeholder) as HTMLInputElement,
getNotes: () => screen.getByRole("textbox", { name: /additional_notes/i }) as HTMLInputElement,
getGuests: () => screen.getByLabelText("guests"),
fillName: ({ value }: { value: string }) => {
fireEvent.change(component.getName(), { target: { value } });
},
fillEmail: ({ value }: { value: string }) => {
fireEvent.change(component.getEmail(), { target: { value } });
},
fillRadioInputLocation: ({ label, inputValue }: { label: string; inputValue?: string }) => {
fireEvent.click(component.getLocationRadioOption({ label }));
if (inputValue) {
let placeholder = label;
if (label === "attendee_phone_number") {
placeholder = "enter_phone_number";
} else {
// radioInput doesn't have a label, so we need to identify by placeholder
throw new Error("Tell me how to identify the placeholder for this location input");
}
fireEvent.change(component.getLocationRadioInput({ placeholder }), {
target: { value: inputValue },
});
}
},
fillNotes: ({ value }: { value: string }) => {
fireEvent.change(component.getNotes(), { target: { value } });
},
};
const expectScenarios = {
expectNameToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => {
expect(component.getName().value).toEqual(value);
expect(formMethods.getValues("responses.name")).toEqual(value);
},
expectEmailToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => {
expect(component.getEmail().value).toEqual(value);
expect(formMethods.getValues("responses.email")).toEqual(value);
},
expectLocationToBe: ({
formMethods,
label,
toMatch: { formattedValue, value },
}: {
label: string;
toMatch: {
formattedValue?: string;
value: {
optionValue: string;
value: string;
};
};
formMethods: FormMethods;
}) => {
expect(component.getLocationRadioOption({ label }).checked).toBe(true);
if (value.optionValue) {
expect(component.getLocationRadioInput({ placeholder: "enter_phone_number" }).value).toEqual(
formattedValue
);
}
expect(formMethods.getValues("responses.location")).toEqual(value);
},
expectNotesToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => {
expect(component.getNotes().value).toEqual(value);
expect(formMethods.getValues("responses.notes")).toEqual(value);
},
};
-1
View File
@@ -1,3 +1,2 @@
export { default as AddressInput } from "./AddressInputLazy";
export { default as MultiEmail } from "./MultiEmailLazy";
export { default as AddressInputNonLazy } from "./AddressInput";
@@ -1,16 +0,0 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
import { Badge } from "./Badge";
export const UpgradeOrgsBadge = function UpgradeOrgsBadge() {
const { t } = useLocale();
return (
<Tooltip content={t("orgs_upgrade_to_enable_feature")}>
<a href="https://cal.com/enterprise" target="_blank">
<Badge variant="gray">{t("upgrade")}</Badge>
</a>
</Tooltip>
);
};
-1
View File
@@ -1,6 +1,5 @@
export { Badge } from "./Badge";
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
export { CreditsBadge } from "./CreditsBadge";
export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge";
export { InfoBadge } from "./InfoBadge";
export type { BadgeProps } from "./Badge";
-4
View File
@@ -4,13 +4,9 @@ export {
DropdownItem,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuTriggerItem,
} from "./Dropdown";
+1 -10
View File
@@ -1,12 +1,8 @@
export { Checkbox, MultiSelectCheckbox, CheckboxField } from "./checkbox";
export type { Option as MultiSelectCheckboxesOptionType } from "./checkbox";
export { HintsOrErrors } from "./inputs/HintOrErrors";
export {
EmailField,
EmailInput,
FieldsetLegend,
InputGroupBox,
InputLeading,
PasswordField,
TextArea,
TextAreaField,
@@ -15,8 +11,6 @@ export {
} from "./inputs/Input";
export { MultiOptionInput } from "./inputs/MultiOptionInput";
export { InputFieldWithSelect } from "./inputs/InputFieldWithSelect";
export type { InputFieldProps, InputProps } from "./inputs/types";
export { InputField, Input, TextField, inputStyles } from "./inputs/TextField";
export { InputError } from "./inputs/InputError";
@@ -25,12 +19,9 @@ export { Label } from "./inputs/Label";
export { Select, SelectField, SelectWithValidation, getReactSelectProps } from "./select";
export { DateRangePickerLazy as DateRangePicker } from "./date-range-picker";
export { Slider } from "./slider";
export { RangeSlider } from "./slider/RangeSlider";
export { RangeSliderPopover } from "./slider/RangeSliderPopover";
export { BooleanToggleGroup, BooleanToggleGroupField, ToggleGroup } from "./toggleGroup";
export { DatePicker } from "./datepicker";
export { FormStep, Steps, Stepper } from "./step";
export { Steps } from "./step";
export { WizardForm } from "./wizard";
export { default as ColorPicker } from "./color-picker/colorpicker";
export { SettingsToggle, Switch } from "./switch";
@@ -14,15 +14,6 @@ import { Input, InputField, inputStyles } from "../inputs/TextField";
import { Label } from "./Label";
import type { InputFieldProps } from "./types";
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
return (
<span className="bg-cal-muted border-default text-subtle inline-flex shrink-0 items-center rounded-l-sm border px-3 ltr:border-r-0 rtl:border-l-0 sm:text-sm sm:leading-4">
{props.children}
</span>
);
}
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
@@ -149,24 +140,6 @@ export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>
);
});
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return (
<legend {...props} className={classNames("text-default text-sm font-medium leading-4", props.className)}>
{props.children}
</legend>
);
}
export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
return (
<div
{...props}
className={classNames("bg-default border-default stack-y-2 rounded-sm border p-2", props.className)}>
{props.children}
</div>
);
}
export const NumberInput = forwardRef<HTMLInputElement, InputFieldProps>(function NumberInput(props, ref) {
return (
<Input
@@ -1,20 +0,0 @@
import React, { forwardRef } from "react";
import { UnstyledSelect } from "../../address/Select";
import { InputField } from "./TextField";
import type { InputFieldProps } from "./types";
export const InputFieldWithSelect = forwardRef<
HTMLInputElement,
InputFieldProps & { selectProps: typeof UnstyledSelect }
>(function EmailField(props, ref) {
return (
<InputField
ref={ref}
{...props}
inputIsFullWidth={false}
addOnClassname="px-0!"
addOnSuffix={<UnstyledSelect {...props.selectProps} />}
/>
);
});
@@ -1,11 +1,8 @@
/* eslint-disable playwright/missing-playwright-await */
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { render, fireEvent } from "@testing-library/react";
import { vi } from "vitest";
import type { UnstyledSelect } from "../../address/Select";
import { EmailField, TextAreaField, PasswordField, NumberInput, FilterSearchField } from "./Input";
import { InputFieldWithSelect } from "./InputFieldWithSelect";
import { InputField } from "./TextField";
const onChangeMock = vi.fn();
@@ -117,31 +114,6 @@ describe("Tests for TextAreaField Component", () => {
});
});
describe("Tests for InputFieldWithSelect Component", () => {
test("Should render correctly with InputField and UnstyledSelect", () => {
const onChangeMock = vi.fn();
const selectProps = {
value: null,
onChange: onChangeMock,
name: "testSelect",
options: [
{ value: "Option 1", label: "Option 1" },
{ value: "Option 2", label: "Option 2" },
{ value: "Option 3", label: "Option 3" },
],
} as unknown as typeof UnstyledSelect;
const { getByText } = render(<InputFieldWithSelect selectProps={selectProps} label="testSelect" />);
const inputElement = getByText("Select...");
fireEvent.mouseDown(inputElement);
const optionElement = getByText("Option 1");
expect(optionElement).toBeInTheDocument();
});
});
describe("Tests for NumberInput Component", () => {
test("Should render correctly with input type number", () => {
const { getByRole } = render(<NumberInput name="numberInput" />);
@@ -1,25 +0,0 @@
"use client";
import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react";
import classNames from "@calcom/ui/classNames";
const RangeSlider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={classNames("relative flex w-full touch-none select-none items-center", className)}
{...props}>
<SliderPrimitive.Track className="bg-subtle border-subtle relative h-2 w-full grow overflow-hidden rounded-full border">
<SliderPrimitive.Range className="bg-brand-default absolute h-full" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="border-subtle bg-brand-default ring-offset-background focus-visible:ring-ring block h-4 w-4 rounded-full border-2 border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="border-subtle bg-brand-default ring-offset-background focus-visible:ring-ring block h-4 w-4 rounded-full border-2 border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
RangeSlider.displayName = SliderPrimitive.Root.displayName;
export { RangeSlider };
@@ -1,129 +0,0 @@
"use client";
import * as Popover from "@radix-ui/react-popover";
import { useState } from "react";
import { Badge } from "../../badge/Badge";
import { Button } from "../../button/Button";
import { inputStyles, TextField } from "../inputs/TextField";
import { RangeSlider } from "./RangeSlider";
interface RangeSliderPopoverProps {
triggerText: string;
resetBtnText?: string;
applyBtnText?: string;
value: number[];
onChange: (value: number[]) => void;
min: number;
max: number;
step?: number;
badgeVariant?: "default" | "success" | "gray" | "warning" | "orange" | "red";
badgeSuffix?: string;
inputSuffix?: string;
inputLeading?: string;
}
export const RangeSliderPopover = ({
resetBtnText = "Reset",
applyBtnText = "Apply",
triggerText,
value,
onChange,
min,
max,
step = 1,
badgeVariant = "default",
badgeSuffix,
inputSuffix,
inputLeading,
}: RangeSliderPopoverProps) => {
const [internalValue, setInternalValue] = useState<number[]>(value);
const [open, setOpen] = useState(false);
const handleReset = () => {
setInternalValue([min, max]);
};
const handleApply = () => {
onChange(internalValue);
setOpen(false);
};
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<button className={inputStyles()}>
<span className="text-subtle text-sm font-medium leading-none">{triggerText}:</span>
{value && (
<Badge variant={badgeVariant} className="ml-2">
{value[0]} - {value[1]} {badgeSuffix}
</Badge>
)}
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
align="start"
className="border-subtle shadow-dropdown bg-default z-50 mt-2 w-72 rounded-lg border px-4 pb-3 pt-4 font-(family-name:--font-sans)">
<div className="mb-4">
<RangeSlider
min={min}
max={max}
step={step}
value={internalValue}
onValueChange={setInternalValue}
/>
</div>
<div className="mb-4 flex items-center justify-between gap-2">
<TextField
labelSrOnly
name="start"
type="number"
min={min}
max={internalValue[1]}
value={internalValue[0]}
onChange={(e) => {
const newValue = parseInt(e.target.value);
if (!isNaN(newValue) && newValue >= min && newValue <= internalValue[1]) {
setInternalValue([newValue, internalValue[1]]);
}
}}
addOnLeading={inputLeading ? inputLeading : undefined}
addOnSuffix={inputSuffix ? inputSuffix : undefined}
containerClassName="w-full"
/>
<TextField
labelSrOnly
name="end"
type="number"
min={internalValue[0]}
max={max}
value={internalValue[1]}
onChange={(e) => {
const newValue = parseInt(e.target.value);
if (!isNaN(newValue) && newValue >= internalValue[0] && newValue <= max) {
setInternalValue([internalValue[0], newValue]);
}
}}
addOnLeading={inputLeading ? inputLeading : undefined}
addOnSuffix={inputSuffix ? inputSuffix : undefined}
containerClassName="w-full"
/>
</div>
<div className="-mx-4">
<hr className="border-subtle" />
<div className="mt-3 flex items-center justify-end gap-2 px-4">
<Button color="minimal" size="sm" onClick={handleReset}>
{resetBtnText}
</Button>
<Button color="secondary" size="sm" onClick={handleApply}>
{applyBtnText}
</Button>
</div>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};
@@ -1,25 +0,0 @@
"use client";
import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react";
import classNames from "@calcom/ui/classNames";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={classNames("relative flex w-full touch-none select-none items-center", className)}
{...props}>
<SliderPrimitive.Track className="bg-subtle border-subtle relative h-2 w-full grow overflow-hidden rounded-full border">
<SliderPrimitive.Range className="bg-brand-default absolute h-full" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="border-subtle bg-brand-default ring-offset-background focus-visible:ring-ring block h-4 w-4 rounded-full border-2 border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="border-subtle bg-brand-default ring-offset-background focus-visible:ring-ring block h-4 w-4 rounded-full border-2 border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
@@ -1,34 +0,0 @@
import React from "react";
import classNames from "@calcom/ui/classNames";
type Props = {
steps: number;
currentStep: number;
};
// It might be worth passing this label string from outside the component so we can translate it?
function FormStep({ currentStep, steps }: Props) {
return (
<div className="w-full">
<p className="text-muted text-xs font-medium">
Step {currentStep} of {steps}
</p>
<div className="flex flex-nowrap space-x-1">
{[...Array(steps)].map((_, j) => {
return (
<div
className={classNames(
"h-1 w-full rounded-sm",
currentStep - 1 >= j ? "bg-black" : "bg-gray-400"
)}
key={j}
/>
);
})}
</div>
</div>
);
}
export default FormStep;
@@ -1,71 +0,0 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
type DefaultStep = {
title: string;
};
function Stepper<T extends DefaultStep>(props: {
href: string;
step: number;
steps: T[];
disableSteps?: boolean;
stepLabel?: (currentStep: number, totalSteps: number) => string;
}) {
const {
href,
steps,
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
} = props;
const [stepperRef] = useAutoAnimate<HTMLOListElement>();
return (
<>
{steps.length > 1 && (
<nav className="flex items-center justify-center" aria-label="Progress">
<p className="text-sm font-medium">{stepLabel(props.step, steps.length)}</p>
<ol role="list" className="ml-8 flex items-center space-x-5" ref={stepperRef}>
{steps.map((mapStep, index) => (
<li key={mapStep.title}>
{index + 1 < props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
<span className="sr-only">{mapStep.title}</span>
</Link>
) : index + 1 === props.step ? (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="relative flex items-center justify-center"
aria-current="step">
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
<span className="bg-emphasis h-full w-full rounded-full" />
</span>
<span
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
aria-hidden="true"
/>
<span className="sr-only">{mapStep.title}</span>
</Link>
) : (
<Link
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
shallow
replace
className="bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400">
<span className="sr-only">{mapStep.title}</span>
</Link>
)}
</li>
))}
</ol>
</nav>
)}
</>
);
}
export default Stepper;
@@ -1,3 +1 @@
export { default as FormStep } from "./FormStep";
export { Steps } from "./Steps";
export { default as Stepper } from "./Stepper";
@@ -1,101 +0,0 @@
import { cva } from "class-variance-authority";
import { Fragment } from "react";
import classNames from "@calcom/ui/classNames";
import { ButtonOrLink } from "../dropdown";
import { Icon } from "../icon";
import type { IconName } from "../icon";
export type NavigationItemType = {
isLastChild?: boolean;
isExpanded?: boolean;
onToggle?: () => void;
name: string;
href?: string;
isLoading?: boolean;
badge?: React.ReactNode;
icon?: IconName;
child?: NavigationItemType[];
onlyMobile?: boolean;
onlyDesktop?: boolean;
moreOnMobile?: boolean;
isCurrent?: boolean;
};
const navigationItemStyles = cva(
"text-default group flex items-center rounded-[10px] p-2 text-sm font-medium transition hover:bg-subtle hover:text-emphasis",
{
variants: {
isChild: {
true: "[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 ml-16 lg:flex lg:ml-10 relative before:absolute before:left-[-24px] before:-top-2 before:h-[calc(100%+0.5rem)] before:w-0.5 before:bg-subtle before:content-[''] first:before:rounded-t-full last:before:rounded-b-full",
false: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm",
},
hasChild: {
true: "aria-[aria-current='page']:bg-transparent! relative after:absolute after:left-[-24px] after:top-6 after:h-[calc(100%-1.5rem)] after:w-0.5 after:bg-subtle after:content-[''] first:after:rounded-t-full last:after:rounded-b-full",
false: "[&[aria-current='page']]:bg-subtle",
},
isFirstChild: {
true: "mt-0",
false: "mt-px",
},
},
defaultVariants: {
isChild: false,
hasChild: false,
isFirstChild: false,
},
}
);
const Label = ({ children }: { children: React.ReactNode }) => {
return <span className="text-default ml-3">{children}</span>;
};
const NavigationItemComponent = ({
item,
isChild,
index,
}: {
item: NavigationItemType;
isChild?: boolean;
index?: number;
}) => {
return (
<Fragment>
<ButtonOrLink
data-test-id={item.name}
href={item.href}
className={navigationItemStyles({
isChild,
hasChild: !!item.child,
isFirstChild: isChild && index === 0,
})}
aria-current={item.isCurrent ? "page" : undefined}
onClick={item.onToggle}>
{item.icon && (
<Icon
name={item.isLoading ? "rotate-cw" : item.icon}
className={classNames(
"text-subtle mr-2 h-4 w-4 shrink-0 rtl:ml-2 md:ltr:mx-auto lg:ltr:mr-2",
item.isLoading && "animate-spin"
)}
aria-hidden="true"
aria-current={item.isCurrent ? "page" : undefined}
/>
)}
<span className="text-emphasis hidden w-full justify-between truncate text-ellipsis lg:flex">
{item.name}
{item.badge && item.badge}
</span>
</ButtonOrLink>
{item.child &&
item.isExpanded &&
item.child.map((childItem, childIndex) => (
<NavigationItem key={childItem.name} item={childItem} isChild index={childIndex} />
))}
</Fragment>
);
};
export const NavigationItem = Object.assign(NavigationItemComponent, { Label });
@@ -5,4 +5,3 @@ export type { NavTabProps } from "./tabs/HorizontalTabs";
export { default as VerticalTabItem } from "./tabs/VerticalTabItem";
export type { VerticalTabItemProps } from "./tabs/VerticalTabItem";
export { default as VerticalTabs } from "./tabs/VerticalTabs";
export { NavigationItem } from "./NavigationItem";