radix-ui / primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos.
https://radix-ui.com/primitives
MIT License
15.71k stars 812 forks source link

Add data-motion to Tabs component #2290

Open FilipePfluck opened 1 year ago

FilipePfluck commented 1 year ago

Add data-motion to Tabs component

Overview

I created a tab component with animations when switching tabs. The tab enters and exits from the right or from the left, based on the direction the new tab is. I use framer motion to do this. I see that in the navigation-menu there is something similar using just css. It uses data-motion to determine which animation should be used. I wonder if something similar could be implemented in the tabs component?

Examples in other libraries

https://www.radix-ui.com/docs/primitives/components/navigation-menu

Who does this impact? Who is this for?

For anyone wanting to make more complex animations without necessarily using other tools for this.

Additional context

If anyone is curious to see how I implemented my tabs component, it is here: https://github.com/FilipePfluck/my-components/tree/main/src/components/Tabs

QuentinFrc commented 1 year ago

Hi, I also provide my implementation which keep in mind reusibility and flexibility using custom attribute from framer-motion and expose a method resolveDirection on top of my Tabs component. Built with ts, tailwind and framer-motion on top of shadcn/ui tabs component.

type TabsContext = {
        value: string;
        setValue: (value: string) => void;
        getDirection: (value: string) => -1 | 0 | 1;
};

const TabsContext = createContext<TabsContext>({
        value: '',
        setValue: () => {},
        getDirection: () => 0,
});

const useTabsContext = () => {
        const context = useContext(TabsContext);
        if (!context) {
                throw new Error(
                        'Tabs compound components cannot be rendered outside the Tabs component',
                );
        }
        return context;
};

type TabsProps = ComponentPropsWithoutRef<typeof TabsPrimitive.Root> & {
        resolveDirection: (selected: string, current: string) => -1 | 0 | 1;
};

const Tabs = forwardRef<ElementRef<typeof TabsPrimitive.Root>, TabsProps>(
        ({ resolveDirection, ...props }, ref) => {
                const [selectedItem = '', setSelectedItem] = useControllableState<string>({
                        prop: props.value,
                        defaultProp: props.defaultValue,
                        onChange: props.onValueChange,
                });

                const getDirection = useCallback(
                        (current: string) => resolveDirection(selectedItem, current),
                        [selectedItem, resolveDirection],
                );

                return (
                        <TabsContext.Provider
                                value={useMemo(
                                        () => ({
                                                value: selectedItem,
                                                setValue: setSelectedItem,
                                                getDirection,
                                        }),
                                        [selectedItem, setSelectedItem, getDirection],
                                )}>
                                <TabsPrimitive.Root
                                        ref={ref}
                                        {...props}
                                        value={selectedItem}
                                        onValueChange={setSelectedItem}
                                        className={'overflow-hidden'}
                                />
                        </TabsContext.Provider>
                );
        },
);

Tabs.displayName = TabsPrimitive.Root.displayName;
const TabsContentVariants = {
        hidden: (direction: -1 | 0 | 1) => ({
                x: direction * 382,
                opacity: 0,
        }),
        visible: () => ({
                opacity: 1,
                x: 0,
        }),
};

const TabsContent = forwardRef<
        ElementRef<typeof TabsPrimitive.Content>,
        ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, children, ...props }, ref) => {
        const { value, getDirection } = useTabsContext();
        const active = value === props.value;

        const direction = useMemo(() => getDirection(props.value), [getDirection, props.value]);

        return (
                <AnimatePresence initial={false}>
                        {active ? (
                        <TabsPrimitive.Content
                                ref={ref}
                                className={cn(
                                        'mt-2 ring-offset-background',
                                        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
                                        className,
                                )}
                                forceMount
                                {...props}
                                asChild>
                                <motion.div
                                        custom={direction}
                                        variants={TabsContentVariants}
                                        animate={active ? 'visible' : 'hidden'}
                                        initial={active ? 'visible' : 'hidden'}
                                        exit={'hidden'}>
                                        {children}
                                </motion.div>
                        </TabsPrimitive.Content>
                        ) : null}
                </AnimatePresence>
        );
});