Files
Dhairyashil ShindeandGitHub 60a4d3f2b4 feat(companion): Add DropdownMenu and Alert Dialog for Android (#26385)
* feat(companion): add DropdownMenu for Android event types list

Replace Alert.alert with react-native-reusables DropdownMenu component
for Android event type list items, providing a native-feeling menu
experience that matches iOS functionality.

Changes:
- Install react-native-reusables dropdown-menu component and dependencies
  (lucide-react-native, tailwindcss-animate, class-variance-authority,
  clsx, tailwind-merge)
- Create EventTypeListItem.android.tsx with DropdownMenu implementation
  - Single ellipsis button triggers dropdown menu
  - Menu includes: Preview, Copy link, Edit, Duplicate, Delete
  - Delete action marked as destructive variant
  - Proper safe area insets handling
- Add PortalHost to app/_layout.tsx for portal rendering support
- Create lib/utils.ts with cn() helper for className merging
- Update global.css with theme CSS variables (popover, border, accent,
  destructive, etc.) for dropdown menu styling
- Update tailwind.config.js with theme colors and tailwindcss-animate plugin
- Update metro.config.js with inlineRem: 16 for proper rem unit handling
- Remove Android Alert.alert fallback from event types index.tsx
- Fix lint issues in generated dropdown-menu.tsx (remove unnecessary fragments)

The Android experience now matches iOS with a single menu button that
opens a dropdown containing all event type actions, replacing the
previous Alert.alert dialog and separate action buttons.

Refs: https://reactnativereusables.com/docs/components/dropdown-menu

* adjust with of menu

* feat(companion): add DropdownMenu for Android booking and availability list items

- Add BookingListItem.android.tsx with DropdownMenu for booking actions
- Add BookingDetailScreen.android.tsx with DropdownMenu in header
- Add AvailabilityListItem.android.tsx with DropdownMenu for schedule actions

Actions include: Reschedule, Edit Location, Add Guests, View Recordings,
Meeting Session Details, Mark as No-Show, Report Booking, Cancel Event
for bookings; Set as Default, Duplicate, Delete for availability schedules.

* fix lint issues

* feat(android): replace native alerts with AlertDialog and Toast in event types

- Install AlertDialog and Alert components from react-native-reusables
- Create Android-specific event types screen (index.android.tsx)
- Replace native Alert.alert() with AlertDialog for delete confirmation
- Add inline validation errors (red border + error text) in create modal
- Implement Toast snackbar for success/error notifications (no layout shift)
- Auto-dismiss toast after 2.5 seconds
- Fix React Compiler compatibility by removing animated refs

This provides a more consistent and polished UI experience on Android,
matching the design system used in other parts of the app.

* feat(android): add AlertDialog for availability delete and booking cancel

- Add index.android.tsx for availability with AlertDialog for delete confirmation
- Add index.android.tsx for bookings with AlertDialog for cancel (with reason input)
- Both screens include Toast snackbar for success/error notifications
- Follows the same pattern as event types Android implementation

* feat(companion): add DropdownMenu and AlertDialog for Android

- Replace native Alert.alert context menus with DropdownMenu component
  for event types, bookings, and availability lists
- Replace FullScreenModal with AlertDialog for create/delete/cancel flows
- Add toast notifications for success/error feedback on Android
- Set consistent 380px max-width for all AlertDialogs
- Fix layout headers showing "index" text on event-types and availability
- Create Android-specific More screen with AlertDialog logout confirmation

Uses react-native-reusables components for polished Android UI

* better code

* fix cubics comments

* refactor(android): revert delete confirmations to native Alert.alert()

- Logout: reverted to native Alert.alert() (simple yes/no)
- Event Types Delete: reverted to native Alert.alert() (simple yes/no)
- Availability Delete: reverted to native Alert.alert() (simple yes/no)

Keep AlertDialog only where user input is needed:
- Event Types Create (has TextInput for title)
- Availability Create (has TextInput for name)
- Bookings Cancel (has TextInput for cancellation reason)

* refactor(android): remove redundant index.android.tsx files

The main index.tsx files already handle Android via Platform.OS checks.
Android-specific behavior for actions is in the list item components:
- EventTypeListItem.android.tsx
- BookingListItem.android.tsx
- AvailabilityListItem.android.tsx

This consolidates the code and reduces duplication.

* corrected the implementation both native and alert dialog

* address cubics comments

* fix lint issue and address cubic comments
2026-01-02 19:05:26 -03:00

312 lines
9.9 KiB
TypeScript

import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
import { Check, ChevronDown, ChevronRight, ChevronUp } from "lucide-react-native";
import * as React from "react";
import {
Platform,
type StyleProp,
StyleSheet,
Text,
type TextProps,
View,
type ViewStyle,
} from "react-native";
import { FadeIn } from "react-native-reanimated";
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
import { Icon } from "@/components/ui/icon";
import { NativeOnlyAnimatedView } from "@/components/ui/native-only-animated-view";
import { TextClassContext } from "@/components/ui/text";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
function DropdownMenuSubTrigger({
className,
inset,
children,
iconClassName,
...props
}: DropdownMenuPrimitive.SubTriggerProps &
React.RefAttributes<DropdownMenuPrimitive.SubTriggerRef> & {
children?: React.ReactNode;
iconClassName?: string;
inset?: boolean;
}) {
const { open } = DropdownMenuPrimitive.useSubContext();
const icon = Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown;
return (
<TextClassContext.Provider
value={cn(
"text-sm select-none group-active:text-accent-foreground",
open && "text-accent-foreground"
)}
>
<DropdownMenuPrimitive.SubTrigger
className={cn(
"active:bg-accent group flex flex-row items-center rounded-sm px-2 py-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none [&_svg]:pointer-events-none",
}),
open && "bg-accent",
inset && "pl-8"
)}
{...props}
>
{children}
<Icon as={icon} className={cn("text-foreground ml-auto size-4 shrink-0", iconClassName)} />
</DropdownMenuPrimitive.SubTrigger>
</TextClassContext.Provider>
);
}
function DropdownMenuSubContent({
className,
...props
}: DropdownMenuPrimitive.SubContentProps &
React.RefAttributes<DropdownMenuPrimitive.SubContentRef>) {
return (
<NativeOnlyAnimatedView entering={FadeIn}>
<DropdownMenuPrimitive.SubContent
className={cn(
"bg-popover border-border overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
Platform.select({
web: "animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 fade-in-0 data-[state=closed]:zoom-out-95 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin) z-50 min-w-[8rem]",
}),
className
)}
{...props}
/>
</NativeOnlyAnimatedView>
);
}
const FullWindowOverlay = Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
function DropdownMenuContent({
className,
overlayClassName,
overlayStyle,
portalHost,
...props
}: DropdownMenuPrimitive.ContentProps &
React.RefAttributes<DropdownMenuPrimitive.ContentRef> & {
overlayStyle?: StyleProp<ViewStyle>;
overlayClassName?: string;
portalHost?: string;
}) {
return (
<DropdownMenuPrimitive.Portal hostName={portalHost}>
<FullWindowOverlay>
<DropdownMenuPrimitive.Overlay
style={Platform.select({
web: overlayStyle ?? undefined,
native: overlayStyle
? StyleSheet.flatten([
StyleSheet.absoluteFill,
overlayStyle as typeof StyleSheet.absoluteFill,
])
: StyleSheet.absoluteFill,
})}
className={overlayClassName}
>
<NativeOnlyAnimatedView entering={FadeIn}>
<TextClassContext.Provider value="text-popover-foreground">
<DropdownMenuPrimitive.Content
className={cn(
"bg-popover border-border min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
Platform.select({
web: cn(
"animate-in fade-in-0 zoom-in-95 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) z-50 cursor-default",
props.side === "bottom" && "slide-in-from-top-2",
props.side === "top" && "slide-in-from-bottom-2"
),
}),
className
)}
{...props}
/>
</TextClassContext.Provider>
</NativeOnlyAnimatedView>
</DropdownMenuPrimitive.Overlay>
</FullWindowOverlay>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuItem({
className,
inset,
variant,
...props
}: DropdownMenuPrimitive.ItemProps &
React.RefAttributes<DropdownMenuPrimitive.ItemRef> & {
className?: string;
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<TextClassContext.Provider
value={cn(
"select-none text-sm text-popover-foreground group-active:text-popover-foreground",
variant === "destructive" && "text-destructive group-active:text-destructive"
)}
>
<DropdownMenuPrimitive.Item
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm px-2 py-2 sm:py-1.5",
Platform.select({
web: cn(
"focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
variant === "destructive" && "focus:bg-destructive/10 dark:focus:bg-destructive/20"
),
}),
variant === "destructive" && "active:bg-destructive/10 dark:active:bg-destructive/20",
props.disabled && "opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
</TextClassContext.Provider>
);
}
function DropdownMenuCheckboxItem({
className,
children,
...props
}: DropdownMenuPrimitive.CheckboxItemProps &
React.RefAttributes<DropdownMenuPrimitive.CheckboxItemRef> & {
children?: React.ReactNode;
}) {
return (
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
<DropdownMenuPrimitive.CheckboxItem
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
}),
props.disabled && "opacity-50",
className
)}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Icon
as={Check}
className={cn(
"text-foreground size-4",
Platform.select({ web: "pointer-events-none" })
)}
/>
</DropdownMenuPrimitive.ItemIndicator>
</View>
{children}
</DropdownMenuPrimitive.CheckboxItem>
</TextClassContext.Provider>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: DropdownMenuPrimitive.RadioItemProps &
React.RefAttributes<DropdownMenuPrimitive.RadioItemRef> & {
children?: React.ReactNode;
}) {
return (
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
<DropdownMenuPrimitive.RadioItem
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
}),
props.disabled && "opacity-50",
className
)}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<View className="bg-foreground h-2 w-2 rounded-full" />
</DropdownMenuPrimitive.ItemIndicator>
</View>
{children}
</DropdownMenuPrimitive.RadioItem>
</TextClassContext.Provider>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: DropdownMenuPrimitive.LabelProps &
React.RefAttributes<DropdownMenuPrimitive.LabelRef> & {
className?: string;
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
className={cn(
"text-foreground px-2 py-2 text-sm font-medium sm:py-1.5",
inset && "pl-8",
className
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: DropdownMenuPrimitive.SeparatorProps & React.RefAttributes<DropdownMenuPrimitive.SeparatorRef>) {
return (
<DropdownMenuPrimitive.Separator
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: TextProps & React.RefAttributes<Text>) {
return (
<Text
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};