Bläddra i källkod

Feature: Menu system structure completed.

lfabl 1 månad sedan
förälder
incheckning
3a3263cfd1

+ 55 - 2
example/src/pages/home/index.tsx

@@ -56,6 +56,45 @@ const Home = () => {
                 subTitle: "Core Tech",
                 title: "NİBGAT®"
             },
+            renderFooter: () => {
+                return <View
+                    style={{
+                        padding: spaces.spacingMd,
+                        flexDirection: "row",
+                        alignItems: "center"
+                    }}
+                >
+                    <View
+                        style={{
+                            marginRight: spaces.spacingSm,
+                            justifyContent: "center",
+                            flexDirection: "row",
+                            alignItems: "center"
+                        }}
+                    >
+                        <Text
+                            variant="labelLargeSize"
+                        >
+                            Palet:
+                        </Text>
+                        <PaletteSwitcher/>
+                    </View>
+                    <View
+                        style={{
+                            justifyContent: "center",
+                            flexDirection: "row",
+                            alignItems: "center"
+                        }}
+                    >
+                        <Text
+                            variant="labelLargeSize"
+                        >
+                            Tema:
+                        </Text>
+                        <ThemeSwitcher/>
+                    </View>
+                </View>;
+            },
             isAutoClosed: true,
             buttons: [
                 {
@@ -74,6 +113,22 @@ const Home = () => {
                         }
                     ]
                 },
+                {
+                    redirectMain: "Text",
+                    isCollabsable: true,
+                    title: "Deneme",
+                    icon: ({
+                        color
+                    }) => <HomeIcon
+                        color={colors.content.icon[color]}
+                    />,
+                    subButtons: [
+                        {
+                            title: "Noliii",
+                            redirectMain: "Home"
+                        }
+                    ]
+                },
                 {
                     title: "Text",
                     redirectMain: "TextPage"
@@ -110,8 +165,6 @@ const Home = () => {
                 });
             }}
         />
-        <PaletteSwitcher/>
-        <ThemeSwitcher/>
         <View
             style={{
                 justifyContent: "center",

+ 207 - 0
src/components/embeddedMenu/components/menuButton/index.tsx

@@ -0,0 +1,207 @@
+import {
+    useLayoutEffect,
+    useEffect,
+    useState,
+    type FC,
+    useRef
+} from "react";
+import {
+    Platform,
+    Animated,
+    Easing,
+    View
+} from "react-native";
+import type {
+    MenuButton as MenuButtonType
+} from "./type";
+import type IMenuButton from "./type";
+import {
+    NCoreUIKitTheme
+} from "../../../../core/hooks";
+import {
+    ChevronRightIcon,
+    ChevronDownIcon,
+    ChevronLeftIcon
+} from "lucide-react-native";
+import RowCard from "../../../rowCard";
+
+const isPageActive = ((buttonItem: MenuButtonType, navigation: NCoreUIKit.Navigation) => {
+    const state = navigation.getState();
+    const currentRoute = state?.routes?.[state.index];
+
+    if(!currentRoute) return false;
+
+    if(currentRoute.state) {
+        const nestedRoute = currentRoute.state.routes?.[currentRoute.state.index];
+        return nestedRoute?.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+    } else if(currentRoute.name) {
+        return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+    }
+
+    return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+});
+
+const MenuButton: FC<IMenuButton> = ({
+    updateMenuButtonCollabs,
+    closeAction,
+    buttonIndex,
+    buttonItem,
+    navigation,
+    depthMap,
+    id
+}) => {
+    const {
+        colors
+    } = NCoreUIKitTheme.useContext();
+
+    const [
+        measure,
+        setMeasure
+    ] = useState<null | number>(null);
+
+    const [
+        ,
+        setNavStateTracker
+    ] = useState(Date.now());
+
+    const animatedCollabs = useRef(new Animated.Value(0)).current;
+
+    useEffect(() => {
+        if (!navigation || typeof navigation.addListener !== "function") return;
+
+        const unsubscribeState = navigation.addListener("state", () => {
+            setNavStateTracker(Date.now());
+        });
+
+        const unsubscribeFocus = navigation.addListener("focus", () => {
+            setNavStateTracker(Date.now());
+        });
+
+        return () => {
+            if (unsubscribeState) unsubscribeState();
+            if (unsubscribeFocus) unsubscribeFocus();
+        };
+    }, [navigation]);
+
+    if(buttonItem.isCollabsable) {
+        useLayoutEffect(() => {
+            if(measure === null) return;
+
+            if(buttonItem.isCollabs) {
+                Animated.timing(animatedCollabs, {
+                    useNativeDriver: Platform.OS !== "web",
+                    toValue: Number(measure),
+                    easing: Easing.linear,
+                    duration: 150
+                }).start();
+            } else {
+                Animated.timing(animatedCollabs, {
+                    useNativeDriver: Platform.OS !== "web",
+                    easing: Easing.linear,
+                    duration: 150,
+                    toValue: 0
+                }).start();
+            }
+        }, [
+            buttonItem.isCollabs,
+            measure
+        ]);
+    }
+
+    const isPActive = isPageActive(buttonItem, navigation);
+
+    return <View
+        key={`menu-button-${id}-${buttonIndex}`}
+    >
+        <RowCard
+            {...buttonItem.props}
+            rightIcon={
+                buttonItem.isCollabsable || buttonItem.isShowRedirectIcon ?
+                    () => {
+                        if(buttonItem.isCollabsable) {
+                            if(buttonItem.isCollabs) {
+                                return <ChevronDownIcon
+                                    color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                    size={18}
+                                />;
+                            } else {
+                                return <ChevronLeftIcon
+                                    color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                    size={18}
+                                />;
+                            }
+                        } else {
+                            return <ChevronRightIcon
+                                color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                size={18}
+                            />;
+                        }
+                    }
+                    :
+                    undefined
+            }
+            backgroundColor={isPActive ? "primary" : undefined}
+            titleColor={isPActive ? "onPrimary" : undefined}
+            iconColor={isPActive ? "onPrimary" : undefined}
+            title={buttonItem.title}
+            icon={buttonItem.icon}
+            onPress={() => {
+                if(buttonItem.isCollabsable) {
+                    updateMenuButtonCollabs({
+                        status: !buttonItem.isCollabs,
+                        buttonItem,
+                        depthMap
+                    });
+                } else {
+                    if(buttonItem.redirectMain || buttonItem.redirectSub) {
+                        if(buttonItem.redirectMain && buttonItem.redirectSub) {
+                            navigation.navigate(buttonItem.redirectMain, {
+                                screen: buttonItem.redirectSub
+                            });
+                        } else if(buttonItem.redirectMain) {
+                            navigation.navigate(buttonItem.redirectMain);
+                        }
+                    }
+
+                    if(buttonItem.props?.onPress) buttonItem.props.onPress();
+                }
+
+                if(closeAction && !buttonItem.isCollabsable) closeAction();
+            }}
+        />
+        {
+            buttonItem.subButtons && buttonItem.subButtons.length ?
+                <Animated.View
+                    onLayout={buttonItem.isCollabsable ? (event) => {
+                        const height = event.nativeEvent.layout.height;
+
+                        if(height !== 0 && measure === null) setMeasure(height);
+                    } : undefined}
+                    style={[
+                        {
+                            height: measure === null ? "auto" : animatedCollabs,
+                            overflow: "hidden"
+                        }
+                    ]}
+                >
+                    {buttonItem.subButtons.map((subButtonItem, subButtonIndex) => {
+                        return <MenuButton
+                            updateMenuButtonCollabs={updateMenuButtonCollabs}
+                            buttonIndex={subButtonIndex}
+                            buttonItem={subButtonItem}
+                            closeAction={closeAction}
+                            navigation={navigation}
+                            depthMap={[
+                                ...depthMap,
+                                subButtonIndex
+                            ]}
+                            id={id}
+                        />;
+                    })}
+                </Animated.View>
+                :
+                null
+        }
+    </View>;
+};
+export default MenuButton;

+ 33 - 0
src/components/embeddedMenu/components/menuButton/type.ts

@@ -0,0 +1,33 @@
+import type {
+    NCoreUIKitIcon
+} from "../../../../types";
+import type IRowCardProps from "../../../rowCard/type";
+
+export type MenuButton = {
+    isShowRedirectIcon?: NCoreUIKitIcon;
+    subButtons?: Array<MenuButton>;
+    isCollabsable?: boolean;
+    redirectMain?: string;
+    props?: IRowCardProps;
+    icon?: NCoreUIKitIcon;
+    redirectSub?: string;
+    isCollabs?: boolean;
+    title: string;
+};
+
+interface IMenuButton {
+    updateMenuButtonCollabs: (props: {
+        depthMap: Array<number>;
+        buttonItem: MenuButton;
+        status: boolean;
+    }) => void;
+    navigation: NCoreUIKit.Navigation;
+    closeAction?: () => void;
+    depthMap: Array<number>;
+    buttonItem: MenuButton;
+    buttonIndex: number;
+    id: string;
+};
+export type {
+    IMenuButton as default
+};

+ 390 - 0
src/components/embeddedMenu/index.tsx

@@ -0,0 +1,390 @@
+import {
+    useImperativeHandle,
+    useLayoutEffect,
+    forwardRef,
+    useEffect,
+    useState,
+    Fragment,
+    useRef
+} from "react";
+import {
+    Platform,
+    Animated,
+    Easing,
+    View
+} from "react-native";
+import {
+    type MenuButton as MenuButtonType
+} from "./components/menuButton/type";
+import {
+    type IMenuRef
+} from "./type";
+import type IMenuProps from "./type";
+import stylesheet, {
+    useStyles
+} from "./stylesheet";
+import MenuButton from "./components/menuButton";
+import {
+    NCoreUIKitMenu,
+    NCoreUIKitTheme
+} from "../../core/hooks";
+import type {
+    RefForwardingComponent
+} from "../../types";
+import {
+    SafeAreaView
+} from "react-native-safe-area-context";
+import {
+    Portal
+} from "../../helpers/portalize";
+import Modal from "../modal";
+import Seperator from "../seperator";
+import SiteLogo from "../siteLogo";
+
+const Menu: RefForwardingComponent<IMenuRef, IMenuProps> = ({
+    isWorkWithSafeAreaView = true,
+    isWorkWithAnimation = true,
+    renderHeader: RenderHeader,
+    renderFooter: RenderFooter,
+    portalName = "menu-system",
+    isWorkWithPortal = true,
+    isWorkWithModal = true,
+    close: closeAction,
+    siteLogoProps,
+    customTheme,
+    navigation,
+    modalProps,
+    isCollabs,
+    onClosed,
+    onOpened,
+    buttons,
+    onClose,
+    onOpen,
+    style,
+    id,
+    ...props
+}, ref) => {
+    const {
+        colors,
+        spaces
+    } = NCoreUIKitTheme.useContext(customTheme);
+
+    const {
+        headerContainer: headerContainerDynamicStyle,
+        seperator: seperatorDynamicStyle,
+        container: containerDynamicStyle
+    } = useStyles({
+        colors,
+        spaces
+    });
+
+    const [
+        isOpacityVisible,
+        setIsOpacityVisible
+    ] = useState(false);
+
+    const [
+        measures,
+        setMeasures
+    ] = useState<null | number>(null);
+
+    const [
+        isActive,
+        setIsActive
+    ] = useState(false);
+
+    const animatedX = useRef(new Animated.Value(0)).current;
+
+    useImperativeHandle(
+        ref,
+        () => ({
+            close,
+            open
+        }),
+        []
+    );
+
+    useEffect(() => {
+        if(measures !== null) {
+            animatedX.setValue(measures * -1);
+
+            if(!isOpacityVisible) setIsOpacityVisible(true);
+
+            if(isActive) setIsActive(false);
+        }
+    }, [measures]);
+
+    useEffect(() => {
+        if(isCollabs !== undefined) setIsActive(isCollabs);
+    }, [isCollabs]);
+
+    useLayoutEffect(() => {
+        if(isActive && isOpacityVisible) {
+            if(isWorkWithAnimation) {
+                openAnimation();
+            } else {
+                if(onOpen) onOpen({
+                    id
+                });
+
+                animatedX.setValue(0);
+
+                if(onOpened) onOpened({
+                    id
+                });
+            }
+        } else {
+            if(!isWorkWithModal) {
+                if(isWorkWithAnimation) {
+                    closeAnimation();
+                } else {
+                    if(onClose) onClose({
+                        id
+                    });
+
+                    animatedX.setValue(measures! * -1);
+
+                    if(onClosed) onClosed({
+                        id
+                    });
+                }
+            }
+        }
+    }, [
+        isOpacityVisible,
+        isActive
+    ]);
+
+    const open = () => {
+        setIsActive(true);
+    };
+
+    const close = () => {
+        if(isWorkWithModal) {
+            if(isWorkWithAnimation) {
+                closeAnimation();
+            } else {
+                animatedX.setValue(measures! * -1);
+
+                if(onClosed) onClosed({
+                    id
+                });
+            }
+        } else {
+            setIsActive(false);
+        }
+    };
+
+    const openAnimation = () => {
+        if(onOpen) onOpen({
+            id
+        });
+
+        Animated.timing(animatedX, {
+            useNativeDriver: Platform.OS !== "web",
+            easing: Easing.linear,
+            duration: 300,
+            toValue: 0
+        }).start(({
+            finished
+        }) => {
+            if(finished) {
+                if(onOpened) onOpened({
+                    id
+                });
+            }
+        });
+    };
+
+    const closeAnimation = () => {
+        if(onClose) onClose({
+            id
+        });
+
+        Animated.timing(animatedX, {
+            useNativeDriver: Platform.OS !== "web",
+            toValue: measures! * -1,
+            easing: Easing.linear,
+            duration: 300
+        }).start(({
+            finished
+        }) => {
+            if(finished) {
+                if(onClosed) onClosed({
+                    id
+                });
+            }
+        });
+    };
+
+    const updateMenuButtonCollabs = ({
+        depthMap,
+        status
+    }: {
+        depthMap: Array<number>;
+        status: boolean;
+    }) => {
+        const state = NCoreUIKitMenu.get({
+            id
+        });
+
+        if(!state) {
+            return;
+        }
+
+        const recursivelyCatch = (item: Array<MenuButtonType>, index = 0): Array<MenuButtonType> => {
+            const tIndex = depthMap[index];
+
+            return item.map((c_item, c_index) => {
+                if(c_index !== tIndex) {
+                    return c_item;
+                }
+
+                if(index === depthMap.length - 1) {
+                    return {
+                        ...c_item,
+                        isCollabs: status
+                    };
+                }
+
+                return {
+                    ...c_item,
+                    subButtons: recursivelyCatch(c_item.subButtons as Array<MenuButtonType>, index + 1)
+                };
+            });
+        };
+
+        const newButtons = recursivelyCatch(state.buttons);
+
+        state.buttons = newButtons;
+
+        NCoreUIKitMenu.update({
+            menuData: state,
+            id: id
+        });
+    };
+
+    const renderHeader = () => {
+        if(RenderHeader) {
+            return <RenderHeader/>;
+        }
+
+        return <View
+            style={[
+                stylesheet.headerContainer,
+                headerContainerDynamicStyle
+            ]}
+        >
+            <SiteLogo
+                {...siteLogoProps}
+            />
+            <Seperator
+                style={[
+                    stylesheet.seperator,
+                    seperatorDynamicStyle
+                ]}
+            />
+        </View>;
+    };
+
+    const renderButtons = () => {
+        if(!buttons || !buttons.length) {
+            return null;
+        }
+
+        return buttons.map((buttonItem, buttonIndex) => {
+            return <MenuButton
+                updateMenuButtonCollabs={updateMenuButtonCollabs}
+                closeAction={closeAction}
+                buttonIndex={buttonIndex}
+                depthMap={[buttonIndex]}
+                buttonItem={buttonItem}
+                navigation={navigation}
+                id={id}
+            />;
+        });
+    };
+
+    const renderFooter = () => {
+        if(!RenderFooter) {
+            return null;
+        }
+
+        return <RenderFooter/>;
+    };
+
+    const renderSafeAreaContext = () => {
+        if(!isWorkWithSafeAreaView) {
+            return <Fragment>
+                {renderHeader()}
+            </Fragment>;
+        }
+
+        return <SafeAreaView
+            style={[
+                stylesheet.safeAreaView
+            ]}
+        >
+            {renderHeader()}
+            <View
+                style={[
+                    stylesheet.content
+                ]}
+            >
+                {renderButtons()}
+            </View>
+            {renderFooter()}
+        </SafeAreaView>;
+    };
+
+    const renderContent = () => {
+        return <Animated.View
+            {...props}
+            style={[
+                style,
+                stylesheet.container,
+                containerDynamicStyle,
+                {
+                    opacity: isOpacityVisible ? 1 : 0,
+                    transform: [{
+                        translateX: animatedX
+                    }]
+                }
+            ]}
+            onLayout={(event) => {
+                const width = event.nativeEvent.layout.width;
+
+                setMeasures(width);
+            }}
+        >
+            {renderSafeAreaContext()}
+        </Animated.View>;
+    };
+
+    if(isWorkWithModal && measures !== null) {
+        return <Modal
+            {...modalProps}
+            isWorkWithPortal={isWorkWithPortal}
+            isOverlayVisible={isOpacityVisible}
+            onOverlayPress={() => {
+                close();
+            }}
+            portalName={portalName}
+            isActive={isActive}
+            id="menu-modal"
+        >
+            {renderContent()}
+        </Modal>;
+    }
+
+    if(isWorkWithPortal && measures !== null) {
+        return <Portal
+            name={portalName}
+        >
+            {renderContent()}
+        </Portal>;
+    }
+
+    return renderContent();
+};
+export default forwardRef(Menu);

+ 72 - 0
src/components/embeddedMenu/stylesheet.ts

@@ -0,0 +1,72 @@
+import {
+    type ViewStyle,
+    StyleSheet,
+    Platform
+} from "react-native";
+import type {
+    MenuDynamicStyle
+} from "./type";
+import type {
+    Mutable
+} from "../../types";
+
+const stylesheet = StyleSheet.create({
+    container: {
+        flexDirection: "column",
+        ...Platform.select({
+            web: {
+                minWidth: 280,
+                maxWidth: 340,
+                width: "auto"
+            },
+            default: {
+                width: "82%"
+            }
+        }),
+        position: "absolute",
+        display: "flex",
+        height: "100%",
+        zIndex: 99997,
+        bottom: 0,
+        left: 0,
+        top: 0
+    },
+    content: {
+        justifyContent: "flex-start",
+        flexDirection: "column",
+        alignItems: "stretch",
+        flex: 1
+    },
+    safeAreaView: {
+        flex: 1
+    },
+    headerContainer: {
+        justifyContent: "center",
+        alignItems: "flex-start",
+        width: "100%"
+    },
+    seperator: {
+    }
+});
+
+export const useStyles = ({
+    colors,
+    spaces
+}: MenuDynamicStyle) => {
+    const styles = {
+        container: {
+            backgroundColor: colors.content.container.default
+        } as Mutable<ViewStyle>,
+        headerContainer: {
+            paddingHorizontal: spaces.spacingMd,
+            paddingTop: spaces.spacingMd
+        } as Mutable<ViewStyle>,
+        seperator: {
+            marginVertical: spaces.spacingMd
+        } as Mutable<ViewStyle>
+    };
+
+    return styles;
+};
+
+export default stylesheet;

+ 88 - 0
src/components/embeddedMenu/type.ts

@@ -0,0 +1,88 @@
+import type {
+    ComponentType,
+    ReactNode
+} from "react";
+import type {
+    ImageProps,
+    StyleProp,
+    ViewStyle
+} from "react-native";
+import type {
+    NCoreUIKitIcon
+} from "../../types";
+import type ISiteLogoProps from "../siteLogo/type";
+import type IRowCardProps from "../rowCard/type";
+import type IModalProps from "../modal/type";
+
+export type IMenuRef = {
+    close: () => void;
+    open: () => void;
+};
+
+export type MenuDynamicStyle = {
+    spaces: NCoreUIKit.ActivePalette["spaces"];
+    colors: NCoreUIKit.ActivePalette["colors"];
+};
+
+export type MenuLogoType = {
+    image?: () => ComponentType<ImageProps>;
+    imageUrl?: string;
+    title?: string;
+};
+
+export type MenuButton = {
+    isShowRedirectIcon?: NCoreUIKitIcon;
+    subButtons?: Array<MenuButton>;
+    isCollabsable?: boolean;
+    redirectMain?: string;
+    props?: IRowCardProps;
+    icon?: NCoreUIKitIcon;
+    redirectSub?: string;
+    isCollabs?: boolean;
+    title: string;
+};
+
+interface IMenuProps {
+    style?: StyleProp<ViewStyle> | Array<StyleProp<ViewStyle>>;
+    customTheme?: {
+        gapPropagation?: keyof NCoreUIKit.GapPropagationKey;
+        sharpness?: keyof NCoreUIKit.SharpnessKey;
+        paletteKey?: keyof NCoreUIKit.PaletteKey;
+        themeKey?: keyof NCoreUIKit.ThemeKey;
+    };
+    navigation: NCoreUIKit.Navigation;
+    isWorkWithSafeAreaView?: boolean;
+    isAutoClosedOnClosed?: boolean;
+    siteLogoProps?: ISiteLogoProps;
+    renderHeader?: () => ReactNode;
+    renderFooter?: () => ReactNode;
+    isWorkWithAnimation?: boolean;
+    isShowCollabsButton?: boolean;
+    isWorkWithPortal?: boolean;
+    buttons: Array<MenuButton>;
+    isWorkWithModal?: boolean;
+    modalProps?: IModalProps;
+    isCollabsable?: boolean;
+    isAutoClosed?: boolean;
+    onOpened?: (props: {
+        id?: string
+    }) => void;
+    onClosed?: (props: {
+        id?: string
+    }) => void;
+    portalName?: string;
+    isCollabs?: boolean;
+    logo?: MenuLogoType;
+    onClose?: (props: {
+        id?: string
+    }) => void;
+    close?: () => void;
+    onOpen?: (props: {
+        id?: string
+    }) => void;
+    id: string;
+};
+
+export type {
+    IMenuProps as default
+};

+ 4 - 0
src/components/index.ts

@@ -162,6 +162,10 @@ export {
     default as Menu
 } from "./menu";
 
+export {
+    default as MenuButton
+} from "./menu/components/menuButton";
+
 export {
     default as SiteLogo
 } from "./siteLogo";

+ 207 - 0
src/components/menu/components/menuButton/index.tsx

@@ -0,0 +1,207 @@
+import {
+    useLayoutEffect,
+    useEffect,
+    useState,
+    type FC,
+    useRef
+} from "react";
+import {
+    Platform,
+    Animated,
+    Easing,
+    View
+} from "react-native";
+import type {
+    MenuButton as MenuButtonType
+} from "./type";
+import type IMenuButton from "./type";
+import {
+    NCoreUIKitTheme
+} from "../../../../core/hooks";
+import {
+    ChevronRightIcon,
+    ChevronDownIcon,
+    ChevronLeftIcon
+} from "lucide-react-native";
+import RowCard from "../../../rowCard";
+
+const isPageActive = ((buttonItem: MenuButtonType, navigation: NCoreUIKit.Navigation) => {
+    const state = navigation.getState();
+    const currentRoute = state?.routes?.[state.index];
+
+    if(!currentRoute) return false;
+
+    if(currentRoute.state) {
+        const nestedRoute = currentRoute.state.routes?.[currentRoute.state.index];
+        return nestedRoute?.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+    } else if(currentRoute.name) {
+        return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+    }
+
+    return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
+});
+
+const MenuButton: FC<IMenuButton> = ({
+    updateMenuButtonCollabs,
+    closeAction,
+    buttonIndex,
+    buttonItem,
+    navigation,
+    depthMap,
+    id
+}) => {
+    const {
+        colors
+    } = NCoreUIKitTheme.useContext();
+
+    const [
+        measure,
+        setMeasure
+    ] = useState<null | number>(null);
+
+    const [
+        ,
+        setNavStateTracker
+    ] = useState(Date.now());
+
+    const animatedCollabs = useRef(new Animated.Value(0)).current;
+
+    useEffect(() => {
+        if (!navigation || typeof navigation.addListener !== "function") return;
+
+        const unsubscribeState = navigation.addListener("state", () => {
+            setNavStateTracker(Date.now());
+        });
+
+        const unsubscribeFocus = navigation.addListener("focus", () => {
+            setNavStateTracker(Date.now());
+        });
+
+        return () => {
+            if (unsubscribeState) unsubscribeState();
+            if (unsubscribeFocus) unsubscribeFocus();
+        };
+    }, [navigation]);
+
+    if(buttonItem.isCollabsable) {
+        useLayoutEffect(() => {
+            if(measure === null) return;
+
+            if(buttonItem.isCollabs) {
+                Animated.timing(animatedCollabs, {
+                    useNativeDriver: Platform.OS !== "web",
+                    toValue: Number(measure),
+                    easing: Easing.linear,
+                    duration: 150
+                }).start();
+            } else {
+                Animated.timing(animatedCollabs, {
+                    useNativeDriver: Platform.OS !== "web",
+                    easing: Easing.linear,
+                    duration: 150,
+                    toValue: 0
+                }).start();
+            }
+        }, [
+            buttonItem.isCollabs,
+            measure
+        ]);
+    }
+
+    const isPActive = isPageActive(buttonItem, navigation);
+
+    return <View
+        key={`menu-button-${id}-${buttonIndex}`}
+    >
+        <RowCard
+            {...buttonItem.props}
+            rightIcon={
+                buttonItem.isCollabsable || buttonItem.isShowRedirectIcon ?
+                    () => {
+                        if(buttonItem.isCollabsable) {
+                            if(buttonItem.isCollabs) {
+                                return <ChevronDownIcon
+                                    color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                    size={18}
+                                />;
+                            } else {
+                                return <ChevronLeftIcon
+                                    color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                    size={18}
+                                />;
+                            }
+                        } else {
+                            return <ChevronRightIcon
+                                color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
+                                size={18}
+                            />;
+                        }
+                    }
+                    :
+                    undefined
+            }
+            backgroundColor={isPActive ? "primary" : undefined}
+            titleColor={isPActive ? "onPrimary" : undefined}
+            iconColor={isPActive ? "onPrimary" : undefined}
+            title={buttonItem.title}
+            icon={buttonItem.icon}
+            onPress={() => {
+                if(buttonItem.isCollabsable) {
+                    updateMenuButtonCollabs({
+                        status: !buttonItem.isCollabs,
+                        buttonItem,
+                        depthMap
+                    });
+                } else {
+                    if(buttonItem.redirectMain || buttonItem.redirectSub) {
+                        if(buttonItem.redirectMain && buttonItem.redirectSub) {
+                            navigation.navigate(buttonItem.redirectMain, {
+                                screen: buttonItem.redirectSub
+                            });
+                        } else if(buttonItem.redirectMain) {
+                            navigation.navigate(buttonItem.redirectMain);
+                        }
+                    }
+
+                    if(buttonItem.props?.onPress) buttonItem.props.onPress();
+                }
+
+                if(closeAction && !buttonItem.isCollabsable) closeAction();
+            }}
+        />
+        {
+            buttonItem.subButtons && buttonItem.subButtons.length ?
+                <Animated.View
+                    onLayout={buttonItem.isCollabsable ? (event) => {
+                        const height = event.nativeEvent.layout.height;
+
+                        if(height !== 0 && measure === null) setMeasure(height);
+                    } : undefined}
+                    style={[
+                        {
+                            height: measure === null ? "auto" : animatedCollabs,
+                            overflow: "hidden"
+                        }
+                    ]}
+                >
+                    {buttonItem.subButtons.map((subButtonItem, subButtonIndex) => {
+                        return <MenuButton
+                            updateMenuButtonCollabs={updateMenuButtonCollabs}
+                            buttonIndex={subButtonIndex}
+                            buttonItem={subButtonItem}
+                            closeAction={closeAction}
+                            navigation={navigation}
+                            depthMap={[
+                                ...depthMap,
+                                subButtonIndex
+                            ]}
+                            id={id}
+                        />;
+                    })}
+                </Animated.View>
+                :
+                null
+        }
+    </View>;
+};
+export default MenuButton;

+ 33 - 0
src/components/menu/components/menuButton/type.ts

@@ -0,0 +1,33 @@
+import type {
+    NCoreUIKitIcon
+} from "../../../../types";
+import type IRowCardProps from "../../../rowCard/type";
+
+export type MenuButton = {
+    isShowRedirectIcon?: NCoreUIKitIcon;
+    subButtons?: Array<MenuButton>;
+    isCollabsable?: boolean;
+    redirectMain?: string;
+    props?: IRowCardProps;
+    icon?: NCoreUIKitIcon;
+    redirectSub?: string;
+    isCollabs?: boolean;
+    title: string;
+};
+
+interface IMenuButton {
+    updateMenuButtonCollabs: (props: {
+        depthMap: Array<number>;
+        buttonItem: MenuButton;
+        status: boolean;
+    }) => void;
+    navigation: NCoreUIKit.Navigation;
+    closeAction?: () => void;
+    depthMap: Array<number>;
+    buttonItem: MenuButton;
+    buttonIndex: number;
+    id: string;
+};
+export type {
+    IMenuButton as default
+};

+ 64 - 165
src/components/menu/index.tsx

@@ -13,35 +13,33 @@ import {
     Easing,
     View
 } from "react-native";
-import type {
-    MenuButton,
-    IMenuRef
+import {
+    type MenuButton as MenuButtonType
+} from "./components/menuButton/type";
+import {
+    type IMenuRef
 } from "./type";
 import type IMenuProps from "./type";
 import stylesheet, {
     useStyles
 } from "./stylesheet";
+import MenuButton from "./components/menuButton";
 import {
+    NCoreUIKitMenu,
     NCoreUIKitTheme
 } from "../../core/hooks";
 import type {
     RefForwardingComponent
 } from "../../types";
-import {
-    ChevronRight as ChevronRightIcon,
-    ChevronLeft as ChevronLeftIcon,
-    ChevronDown as ChevronDownIcon
-} from "lucide-react-native";
 import {
     SafeAreaView
 } from "react-native-safe-area-context";
 import {
     Portal
 } from "../../helpers/portalize";
+import Modal from "../modal";
 import Seperator from "../seperator";
 import SiteLogo from "../siteLogo";
-import RowCard from "../rowCard";
-import Modal from "../modal";
 
 const Menu: RefForwardingComponent<IMenuRef, IMenuProps> = ({
     isWorkWithSafeAreaView = true,
@@ -219,6 +217,53 @@ const Menu: RefForwardingComponent<IMenuRef, IMenuProps> = ({
         });
     };
 
+    const updateMenuButtonCollabs = ({
+        depthMap,
+        status
+    }: {
+        depthMap: Array<number>;
+        status: boolean;
+    }) => {
+        const state = NCoreUIKitMenu.get({
+            id
+        });
+
+        if(!state) {
+            return;
+        }
+
+        const recursivelyCatch = (item: Array<MenuButtonType>, index = 0): Array<MenuButtonType> => {
+            const tIndex = depthMap[index];
+
+            return item.map((c_item, c_index) => {
+                if(c_index !== tIndex) {
+                    return c_item;
+                }
+
+                if(index === depthMap.length - 1) {
+                    return {
+                        ...c_item,
+                        isCollabs: status
+                    };
+                }
+
+                return {
+                    ...c_item,
+                    subButtons: recursivelyCatch(c_item.subButtons as Array<MenuButtonType>, index + 1)
+                };
+            });
+        };
+
+        const newButtons = recursivelyCatch(state.buttons);
+
+        state.buttons = newButtons;
+
+        NCoreUIKitMenu.update({
+            menuData: state,
+            id: id
+        });
+    };
+
     const renderHeader = () => {
         if(RenderHeader) {
             return <RenderHeader/>;
@@ -242,167 +287,21 @@ const Menu: RefForwardingComponent<IMenuRef, IMenuProps> = ({
         </View>;
     };
 
-    const isPageActive = ((buttonItem: MenuButton) => {
-        const state = navigation.getState();
-        const currentRoute = state?.routes?.[state.index];
-
-        if(!currentRoute) return false;
-
-        if(currentRoute.state) {
-            const nestedRoute = currentRoute.state.routes?.[currentRoute.state.index];
-            return nestedRoute?.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
-        } else if(currentRoute.name) {
-            return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
-        }
-
-        return currentRoute.name === (buttonItem.redirectSub ?? buttonItem.redirectMain);
-    });
-
-    const renderMenuButton = (buttonItem: MenuButton, buttonIndex: number) => {
-        const [
-            isCollabs,
-            setIsCollabs
-        ] = useState(buttonItem.isCollabs);
-
-        const [
-            measure,
-            setMeasure
-        ] = useState<null | number>(null);
-
-        const [
-            ,
-            setNavStateTracker
-        ] = useState(Date.now());
-
-        const animatedCollabs = useRef(new Animated.Value(0)).current;
-
-        useEffect(() => {
-            if (!navigation || typeof navigation.addListener !== "function") return;
-
-            const unsubscribeState = navigation.addListener("state", () => {
-                setNavStateTracker(Date.now());
-            });
-
-            const unsubscribeFocus = navigation.addListener("focus", () => {
-                setNavStateTracker(Date.now());
-            });
-
-            return () => {
-                if (unsubscribeState) unsubscribeState();
-                if (unsubscribeFocus) unsubscribeFocus();
-            };
-        }, [navigation]);
-
-        if(buttonItem.isCollabsable) {
-            useLayoutEffect(() => {
-                if(measure === null) return;
-
-                if(isCollabs) {
-                    Animated.timing(animatedCollabs, {
-                        useNativeDriver: Platform.OS !== "web",
-                        toValue: Number(measure),
-                        easing: Easing.linear,
-                        duration: 150
-                    }).start();
-                } else {
-                    Animated.timing(animatedCollabs, {
-                        useNativeDriver: Platform.OS !== "web",
-                        easing: Easing.linear,
-                        duration: 150,
-                        toValue: 0
-                    }).start();
-                }
-            }, [isCollabs]);
-        }
-
-        const isPActive = isPageActive(buttonItem);
-
-        return <View
-            key={`menu-button-${id}-${buttonIndex}`}
-        >
-            <RowCard
-                {...buttonItem.props}
-                rightIcon={
-                    buttonItem.isCollabsable || buttonItem.isShowRedirectIcon ?
-                        () => {
-                            if(buttonItem.isCollabsable) {
-                                if(isCollabs) {
-                                    return <ChevronDownIcon
-                                        color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
-                                        size={18}
-                                    />;
-                                } else {
-                                    return <ChevronLeftIcon
-                                        color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
-                                        size={18}
-                                    />;
-                                }
-                            } else {
-                                return <ChevronRightIcon
-                                    color={isPActive ? colors.content.icon.onPrimary : colors.content.icon.low}
-                                    size={18}
-                                />;
-                            }
-                        }
-                        :
-                        undefined
-                }
-                backgroundColor={isPActive ? "primary" : undefined}
-                titleColor={isPActive ? "onPrimary" : undefined}
-                iconColor={isPActive ? "onPrimary" : undefined}
-                title={buttonItem.title}
-                icon={buttonItem.icon}
-                onPress={() => {
-                    if(buttonItem.isCollabsable) {
-                        setIsCollabs(!isCollabs);
-                    } else {
-                        if(buttonItem.redirectMain || buttonItem.redirectSub) {
-                            if(buttonItem.redirectMain && buttonItem.redirectSub) {
-                                navigation.navigate(buttonItem.redirectMain, {
-                                    screen: buttonItem.redirectSub
-                                });
-                            } else if(buttonItem.redirectMain) {
-                                navigation.navigate(buttonItem.redirectMain);
-                            }
-                        }
-
-                        if(buttonItem.props?.onPress) buttonItem.props.onPress();
-                    }
-
-                    if(closeAction && !buttonItem.isCollabsable) closeAction();
-                }}
-            />
-            {
-                buttonItem.subButtons && buttonItem.subButtons.length ?
-                    <Animated.View
-                        onLayout={buttonItem.isCollabsable ? (event) => {
-                            const height = event.nativeEvent.layout.height;
-
-                            if(height !== 0 && measure === null) setMeasure(height);
-                        } : undefined}
-                        style={[
-                            {
-                                height: measure === null ? "auto" : animatedCollabs
-                            }
-                        ]}
-                    >
-                        {buttonItem.subButtons.map((subButtonItem, subButtonIndex) => {
-                            return renderMenuButton(subButtonItem, subButtonIndex);
-                        })}
-                    </Animated.View>
-                    :
-                    null
-            }
-        </View>;
-    };
-
     const renderButtons = () => {
         if(!buttons || !buttons.length) {
             return null;
         }
 
         return buttons.map((buttonItem, buttonIndex) => {
-            return renderMenuButton(buttonItem, buttonIndex);
+            return <MenuButton
+                updateMenuButtonCollabs={updateMenuButtonCollabs}
+                closeAction={closeAction}
+                buttonIndex={buttonIndex}
+                depthMap={[buttonIndex]}
+                buttonItem={buttonItem}
+                navigation={navigation}
+                id={id}
+            />;
         });
     };
 

+ 1 - 1
src/components/menu/type.ts

@@ -80,7 +80,7 @@ interface IMenuProps {
     onOpen?: (props: {
         id?: string
     }) => void;
-    id?: string;
+    id: string;
 };
 
 export type {

+ 5 - 3
src/context/menu.tsx

@@ -21,7 +21,9 @@ class NCoreUIKitMenu extends NCoreContext<MenuContextType, ConfigType<MenuContex
         data?: Array<MenuDataType>
     }) {
         super({
+            get: () => undefined,
             unload: () => "",
+            update: () => {},
             close: () => {},
             load: () => "",
             open: () => {},
@@ -52,7 +54,7 @@ class NCoreUIKitMenu extends NCoreContext<MenuContextType, ConfigType<MenuContex
         index,
         id
     }: {
-        index: number;
+        index?: number;
         id?: string;
     }) => {
         const currentData = this.state.data;
@@ -67,7 +69,7 @@ class NCoreUIKitMenu extends NCoreContext<MenuContextType, ConfigType<MenuContex
             return currentData[index];
         }
 
-        return currentData;
+        return undefined;
     };
 
     update = ({
@@ -76,7 +78,7 @@ class NCoreUIKitMenu extends NCoreContext<MenuContextType, ConfigType<MenuContex
         id
     }: {
         menuData: MenuDataType;
-        index: number;
+        index?: number;
         id?: string;
     }) => {
         const currentData = this.state.data;

+ 1 - 0
src/index.tsx

@@ -31,6 +31,7 @@ export {
     AvatarGroup,
     SelectSheet,
     RadioButton,
+    MenuButton,
     SelectBox,
     StateCard,
     TextInput,

+ 9 - 0
src/types/menu.ts

@@ -10,6 +10,15 @@ export type MenuDataType = Omit<IMenuProps, "id"> & {
 
 export type MenuContextType = {
     load: (menuData: MenuDataType) => string;
+    get: (props?: {
+        index?: number;
+        id?: string;
+    }) => MenuDataType | undefined;
+    update: (props?: {
+        menuData: MenuDataType;
+        index?: number;
+        id?: string;
+    }) => void;
     data: Array<MenuDataType>;
     unload: (props?: {
         index?: number;