expo-starter / expo-local-first-template

πŸ“± A template for your local-first Expo project: Bun, Expo 51, TypeScript, TailwindCSS, DrizzleORM, Sqlite, EAS, GitHub Actions, Env Vars, expo-router, react-hook-form.
https://expostarter.com
Apache License 2.0
174 stars 10 forks source link

[suggestion] πŸ’‘ List Item component #4

Closed fluid-design-io closed 4 months ago

fluid-design-io commented 4 months ago

First of all, thanks for the great starter template :)

I was thinking about creating a list component which is widely used in mobile applications, and I also loved using ionic's IonItem component back in the old days. So I've created a simple List and ListItem component combined with cva() to create different variants.

In addition, users can also pass href directly to convert it into an Expo Link Component, here's the example screenshot:

CleanShot 2024-05-18 at 23 43 56@2x

And here's the code:

import List, { ListHeader } from "@/components/ui/list";
import ListItem from "@/components/ui/list-item";
import {
    ChevronLeft,
    CircleHelp,
    ExternalLink,
    Settings,
} from "@/components/icons";

function Example() {
    return (
                <List className="px-6">
                    <ListHeader>Header</ListHeader>
                    <ListItem
                        iconLeft={(props) => <Settings {...props} />}
                        label="Settings"
                        href="/settings" // Chevron Right appears automatically for defined href or onPress props
                    />
                    <ListItem
                        label="Help"
                        description="Get help with the app"
                        iconLeft={(props) => <CircleHelp {...props} />}
                        iconRight={(props) => <ExternalLink {...props} />} // Overwrite Icon
                        onPress={() => console.log("Clicked!")} 
                        variant="link" // using cva to change text and icon colors
                    />
                </List>
                <List className="px-6">
                    <ListItem
                        label="Delete Account..."
                        onPress={() => console.log("Clicked Mega Man X")}
                        detail={false} // No Chevron even though onPress is defined
                        variant="destructive"
                    />
                </List>
        )
}

BTW: I'm not an expert in creating re-usable components, please feel free to modify them to make it more robust!! ✨

Here's the `list.tsx` and `list-item.tsx`: ```tsx //list.tsx import React from "react"; import { View, ViewProps } from "react-native"; import ListItem from "./list-item"; import { H4 } from "./typography"; import { cn } from "@/lib/utils"; interface ListHeaderProps extends ViewProps { children: React.ReactNode; } export const ListHeader: React.FC = ({ children }) => { return (

{children}

); }; interface ListProps extends ViewProps {} const List: React.FC = ({ children, className, ...props }) => { const childrenArray = React.Children.toArray(children); const modifiedChildren = childrenArray.map((child, index) => { // return if is not valid if (!React.isValidElement(child)) { return child; } let injectClassName = ""; // if first child and type is listItem, or if the previous child is a listHeader if ( (index === 0 && child.type === ListItem) || //@ts-ignore childrenArray[index - 1]?.type === ListHeader ) { injectClassName += "rounded-t-md "; } if (index === childrenArray.length - 1) { injectClassName += "rounded-b-md border-b-0"; } return React.cloneElement(child, { // @ts-expect-error className: cn(child.props.className, injectClassName), }); }); return ( {modifiedChildren} ); }; export default List; ``` ```tsx // list-item.tsx import { cva, type VariantProps } from "class-variance-authority"; import { Link } from "expo-router"; import { LinkProps } from "expo-router/build/link/Link"; import { ExpoRouter } from "expo-router/types/expo-router"; import React, { ElementType } from "react"; import { Text, TouchableOpacity, TouchableOpacityProps, View, ViewProps, } from "react-native"; import { Muted } from "./typography"; import { ChevronRight } from "@/components/icons"; import { cn } from "@/lib/utils"; const listItemTextVariants = cva( "text-base font-normal", // base styles { variants: { variant: { default: "text-foreground", primary: "text-primary", link: "text-blue-500", destructive: "text-destructive", }, }, defaultVariants: { variant: "default", }, }, ); interface IconProps { className: string; } // Define props for ListItem with TypeScript interface type ListItemProps = VariantProps & { label: string; description?: string; iconLeft?: (iconProps: IconProps) => JSX.Element; iconRight?: (iconProps: IconProps) => JSX.Element; onPress?: () => void; /** * If true, a detail arrow will appear on the item. */ detail?: boolean; /** * Convert the default TouchableOpacity with a Link component. */ href?: ExpoRouter.Href; className?: string; } & (ViewProps | TouchableOpacityProps | LinkProps); // ListItem component const ListItem: React.FC = ({ label, description, iconLeft, iconRight, detail = true, variant, className, href, ...props }) => { // Automatically add ChevronRight if onPress is defined and detail is true const RightIcon = () => { if (iconRight) { return iconRight({ className: cn( "size-5 text-foreground", listItemTextVariants({ variant }), ), }); } else if ((props?.onPress && detail) || (href && detail)) { return ( ); } return null; }; const Component = ( href || props?.onPress ? TouchableOpacity : View ) as ElementType; const body = ( {iconLeft && ( {iconLeft({ className: cn( "size-5 text-foreground", listItemTextVariants({ variant }), ), })} )} {label} {description && {description}} ); if (href) { return ( {body} ); } else { return body; } }; export default ListItem; ```

Thanks!

younes200 commented 4 months ago

Looks great, you can create PR to add list.tsxandlist-item.tsx` πŸ˜‰