diff --git a/example/storybook-nativewind/.storybook/preview.js b/example/storybook-nativewind/.storybook/preview.js index 501a40e4f..459028f22 100644 --- a/example/storybook-nativewind/.storybook/preview.js +++ b/example/storybook-nativewind/.storybook/preview.js @@ -78,7 +78,15 @@ export const parameters = { 'Textarea', ], 'Overlay', - ['AlertDialog', 'Menu', 'Modal', 'Popover', 'Portal', 'Tooltip'], + [ + 'AlertDialog', + 'Drawer', + 'Menu', + 'Modal', + 'Popover', + 'Portal', + 'Tooltip', + ], 'Disclosure', ['Actionsheet', 'Accordion', 'BottomSheet'], 'Media And Icons', diff --git a/example/storybook-nativewind/src/components/Drawer/Drawer.stories.tsx b/example/storybook-nativewind/src/components/Drawer/Drawer.stories.tsx new file mode 100644 index 000000000..0f94e7043 --- /dev/null +++ b/example/storybook-nativewind/src/components/Drawer/Drawer.stories.tsx @@ -0,0 +1,33 @@ +import type { ComponentMeta } from '@storybook/react-native'; +import Drawer from './Drawer'; + +const DrawerMeta: ComponentMeta = { + title: 'stories/Drawer', + component: Drawer, + parameters: { + layout: 'fullscreen', + }, + // metaInfo is required for figma generation + // @ts-ignore + metaInfo: { + componentDescription: `A Drawer component provides a slide-in panel from any screen edge, with customizable size and anchor positions, featuring smooth animations for opening and closing. It adapts dynamically based on screen dimensions.`, + }, + args: { + size: 'sm', + anchor: 'left', + }, + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg', 'full'], + }, + anchor: { + control: 'select', + options: ['left', 'right', 'top', 'bottom'], + }, + }, +}; + +export default DrawerMeta; + +export { Drawer }; diff --git a/example/storybook-nativewind/src/components/Drawer/Drawer.tsx b/example/storybook-nativewind/src/components/Drawer/Drawer.tsx new file mode 100644 index 000000000..2d6f4f7ec --- /dev/null +++ b/example/storybook-nativewind/src/components/Drawer/Drawer.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Button, ButtonText } from '@/components/ui/button'; +import { + Drawer, + DrawerBackdrop, + DrawerBody, + DrawerContent, + DrawerFooter, + DrawerHeader, +} from '@/components/ui/drawer'; +import { Heading } from '@/components/ui/heading'; +import { Text } from '@/components/ui/text'; + +const DrawerBasic = ({ ...props }: any) => { + const [showDrawer, setShowDrawer] = React.useState(false); + + return ( + <> + + { + setShowDrawer(false); + }} + {...props} + > + + + + Heading + + + + This is a sentence. + + + + + + + + + ); +}; + +export default DrawerBasic; diff --git a/example/storybook-nativewind/src/components/Drawer/index.nw.stories.mdx b/example/storybook-nativewind/src/components/Drawer/index.nw.stories.mdx new file mode 100644 index 000000000..ab689e395 --- /dev/null +++ b/example/storybook-nativewind/src/components/Drawer/index.nw.stories.mdx @@ -0,0 +1,963 @@ +--- +title: Drawer | gluestack-ui | Installation, Usage, and API + +description: A sliding panel that emerges from the edge of the screen to reveal additional content or navigation options.. +pageTitle: Drawer + +pageDescription: A sliding panel that emerges from the edge of the screen to reveal additional content or navigation options.. +showHeader: true +--- + +import { anchorBlockIdFromId, Meta } from '@storybook/addon-docs'; + + + +import { + Drawer, + DrawerBackdrop, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + CheckboxGroup, + Checkbox, + CheckboxIndicator, + CheckboxIcon, + CheckboxLabel, + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb, + HStack, + VStack, + Heading, + Text, + Divider, + CheckIcon, + CircleIcon, + StarIcon, + PhoneIcon, + Icon, + Button, + ButtonText, + ButtonIcon, + Avatar, + AvatarFallbackText, + AvatarImage, + Pressable, +} from '../../core-components/nativewind'; +import { + User, + Home, + ShoppingCart, + Wallet, + LogOut, +} from 'lucide-react-native'; +import { transformedCode } from '../../utils'; +import { + CodePreview, + Table, + TableContainer, + InlineCode, + Tabs, + CollapsibleCode +} from '@gluestack/design-system'; +import Wrapper from '../../core-components/nativewind/Wrapper'; +import AnatomyImage from '../../extra-components/nativewind/AnatomyImage'; + +This is an illustration of **Drawer** component. + + + + + { + setShowDrawer(false); + }} + {...props} + > + + + + Heading + + + + This is a sentence. + + + + + + + + + ); + }; + `, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Drawer, + DrawerBackdrop, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + Button, + ButtonText, + Heading, + Text, + }, + argsType: { + size: { + control: 'select', + options: ['sm', 'md', 'lg', 'full'], + default: 'sm', + }, + anchor: { + control: 'select', + options: ['left', 'right', 'top', 'bottom'], + default: 'left', + }, + }, + }} + /> + + +
+ +## Installation + + + + + CLI + + + Manual + + + + +<> + +### Run the following command: + ```bash + npx gluestack-ui add drawer + ``` + + + +<> + +### Step 1: Install the following dependencies: +```bash +npm i @gluestack-ui/modal @legendapp/motion +``` +> Note: At present, we have integrated the `@legendapp/motion` for animation. You have the option to remove this and implement your own custom animation wrapper. + +### Step 2: Copy and paste the following code into your project. + + +```jsx +%%-- File: core-components/nativewind/drawer/index.tsx --%% +``` + + +### Step 3: Update the import paths to match your project setup. + + + + + +## API Reference + +To use this component in your project, include the following import statement in your file. + +```jsx +import { Drawer, DrawerBackdrop, DrawerContent, DrawerHeader, DrawerCloseButton, DrawerBody, DrawerFooter } from '@/components/ui/drawer'; +``` + + + +```jsx +export default () => ( + + + + + + + + + + +); +``` + +### Component Props + +This section provides a comprehensive reference list for the component props, detailing descriptions, properties, types, and default behavior for easy project integration. + +#### Drawer + +Contains all View related layout style props and actions. It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view#props) component. + + + + + + + + Prop + + + Type + + + Default + + + Description + + + + + + + + isOpen + + + + boolean + + + - + + + {`If true, the drawer will open. Useful for controllable state behavior.`} + + + + + + onClose + + + + {`() => any`} + + + - + + + {`Callback invoked when the drawer is closed.`} + + + + + + defaultIsOpen + + + + boolean + + + - + + + {`Specifies the default open state of the Drawer`} + + + + + + initialFocusRef + + + + {`React.RefObject`} + + + - + + + {`The ref of element to receive focus when the drawer opens.`} + + + + + + finalFocusRef + + + + {`React.RefObject`} + + + - + + + {`The ref of element to receive focus when the drawer closes`} + + + + + + avoidKeyboard + + + + boolean + + + - + + + {`If true, the Drawer will avoid the keyboard.`} + + + + + + closeOnOverlayClick + + + + boolean + + + - + + + {`If true, the Drawer will close when the overlay is clicked.`} + + + + + + isKeyboardDismissable + + + + boolean + + + - + + + {`If true, the keyboard can dismiss the Drawer`} + + + + + + children + + + + any + + + - + + + {`The content to display inside the Drawer`} + + + +
+
+
+ +#### DrawerBackdrop + +It is React Native's [Pressable](https://reactnative.dev/docs/pressable#props) component, created using [@legendapp/motion's](https://legendapp.com/open-source/motion/) `createMotionAnimatedComponent` function to add animation to the component. You can use any declarative animation library you prefer. + +#### DrawerContent + +It is [@legendapp/motion's](https://legendapp.com/open-source/motion/) [Motion.View](https://legendapp.com/open-source/motion/overview/) component. You can use any declarative animation library you prefer. + +<> + + + + + + Prop + + + Type + + + Default + + + Description + + + + + + + + focusable + + + + boolean + + + false + + + {`If true, Drawer Content will be focusable.`} + + + +
+
+ + +#### DrawerHeader + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +#### DrawerCloseButton + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +#### DrawerBody + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component + +#### DrawerFooter + +It inherits all the properties of React Native's [View](https://reactnative.dev/docs/view) component. + +### Props + +Modal component is created using View component from react-native. It extends all the props supported by [React Native View](https://reactnative.dev/docs/view#props), [utility props](/ui/docs/styling/utility-and-sx-props) and the props mentioned below. + +#### Modal + +<> + + + + + + Name + + + Value + + + Default + + + + + + + + size + + + + xs | sm | md | lg | full + + + sm + + + + + + anchor + + + + left | right | top | bottom + + + left + + + +
+
+ + +### Examples + +The Examples section provides visual representations of the different variants of the component, allowing you to quickly and easily determine which one best fits your needs. Simply copy the code and integrate it into your project. + +#### Filter + + + + + { + setShowDrawer(false); + }} + > + + + + FILTERS + + + + + + Categories + + + { + setCategories(keys); + }} + > + + + + + + + Tops + + + (143,234) + + + + + + + + Bottoms + + + (5,431,234) + + + + + + + + Shoes + + + (98,234) + + + + + + + + Accessories + + + (91,234) + + + + + + + Brands + + { + setBrands(keys); + }} + > + + + + + + + Allen Solly + + + (1,234) + + + + + + + + Adidas + + + (524) + + + + + + + + Nike + + + (931) + + + + + + + + Marks & Spencer + + + (1,241) + + + + + + + Price Range + + + + + + + + + + + 0 + 10,000 + + + + Colour + + { + setColors(keys); + }} + > + + + + + + + + + White + + + + (23,673) + + + + + + + + + + Black + + + + (54,657) + + + + + + + + + + Red + + + + (45,678) + + + + + + + + + + Brown + + + + (13,672) + + + + + + + + + + ); + } + `, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Drawer, + DrawerBackdrop, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + Button, + ButtonText, + Heading, + Text, + CheckIcon, + CircleIcon, + Icon, + VStack, + HStack, + Divider, + CheckboxGroup, + Checkbox, + CheckboxIndicator, + CheckboxIcon, + CheckboxLabel, + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb, + }, + argsType: {}, + }} + /> + + +#### Sidebar Menu + + + + + { + setShowDrawer(false); + }} + > + + + + + User Image + + + + User Name + + abc@gmail.com + + + + + + + {/* User is imported from 'lucide-react-native' */} + + My Profile + + + {/* Home is imported from 'lucide-react-native' */} + + Saved Address + + + {/* ShoppingCart is imported from 'lucide-react-native' */} + + Orders + + + {/* Wallet is imported from 'lucide-react-native' */} + + Saved Cards + + + + Review App + + + + Contact Us + + + + + + + + + ); + } + `, + transformCode: (code) => { + return transformedCode(code, 'function', 'App'); + }, + scope: { + Wrapper, + Drawer, + DrawerBackdrop, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerFooter, + Button, + ButtonText, + ButtonIcon, + Text, + HStack, + VStack, + Pressable, + Divider, + Avatar, + AvatarFallbackText, + AvatarImage, + User, + Home, + ShoppingCart, + Wallet, + LogOut, + Icon, + StarIcon, + PhoneIcon + }, + argsType: {}, + }} + /> + diff --git a/example/storybook-nativewind/src/components/Modal/index.nw.stories.mdx b/example/storybook-nativewind/src/components/Modal/index.nw.stories.mdx index cfbe028af..303efbeff 100644 --- a/example/storybook-nativewind/src/components/Modal/index.nw.stories.mdx +++ b/example/storybook-nativewind/src/components/Modal/index.nw.stories.mdx @@ -212,7 +212,7 @@ npm i @gluestack-ui/modal @legendapp/motion To use this component in your project, include the following import statement in your file. ```jsx -import { Modal } from '@/components/ui/modal'; +import { Modal, ModalBackdrop, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter } from '@/components/ui/modal'; ``` diff --git a/example/storybook-nativewind/src/core-components/nativewind/drawer/index.tsx b/example/storybook-nativewind/src/core-components/nativewind/drawer/index.tsx new file mode 100644 index 000000000..09bc3aee3 --- /dev/null +++ b/example/storybook-nativewind/src/core-components/nativewind/drawer/index.tsx @@ -0,0 +1,354 @@ +'use client'; +import React from 'react'; +import { createModal as createDrawer } from '@gluestack-ui/modal'; +import { + Pressable, + View, + ScrollView, + Platform, + Dimensions, +} from 'react-native'; +import { + Motion, + AnimatePresence, + createMotionAnimatedComponent, +} from '@legendapp/motion'; +import { tva } from '@gluestack-ui/nativewind-utils/tva'; +import { + withStyleContext, + useStyleContext, +} from '@gluestack-ui/nativewind-utils/withStyleContext'; +import { withStyleContextAndStates } from '@gluestack-ui/nativewind-utils/withStyleContextAndStates'; +import { cssInterop } from 'nativewind'; +import type { VariantProps } from '@gluestack-ui/nativewind-utils'; + +const AnimatedPressable = createMotionAnimatedComponent(Pressable); +const SCOPE = 'MODAL'; +const screenWidth = Dimensions.get('window').width; +const screenHeight = Dimensions.get('window').height; +const sizes: { [key: string]: number } = { + sm: 0.25, + md: 0.5, + lg: 0.75, + full: 1, +}; + +const UIDrawer = createDrawer({ + Root: + Platform.OS === 'web' + ? withStyleContext(View, SCOPE) + : withStyleContextAndStates(View, SCOPE), + Backdrop: AnimatedPressable, + Content: Motion.View, + Body: ScrollView, + CloseButton: Pressable, + Footer: View, + Header: View, + AnimatePresence: AnimatePresence, +}); +cssInterop(UIDrawer, { className: 'style' }); +cssInterop(UIDrawer.Backdrop, { className: 'style' }); +cssInterop(UIDrawer.Content, { className: 'style' }); +cssInterop(UIDrawer.CloseButton, { className: 'style' }); +cssInterop(UIDrawer.Header, { className: 'style' }); +cssInterop(UIDrawer.Body, { + className: 'style', + contentContainerClassName: 'contentContainerStyle', + indicatorClassName: 'indicatorStyle', +}); +cssInterop(UIDrawer.Footer, { className: 'style' }); + +const drawerStyle = tva({ + base: 'w-full h-full web:pointer-events-none relative', + variants: { + size: { + sm: '', + md: '', + lg: '', + full: '', + }, + anchor: { + left: 'items-start', + right: 'items-end', + top: 'justify-start', + bottom: 'justify-end', + }, + }, +}); + +const drawerBackdropStyle = tva({ + base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default', +}); + +const drawerContentStyle = tva({ + base: 'bg-background-0 overflow-scroll border border-outline-100 p-6 absolute', + parentVariants: { + size: { + sm: 'w-1/4', + md: 'w-1/2', + lg: 'w-3/4', + full: 'w-full', + }, + anchor: { + left: 'h-full', + right: 'h-full', + top: 'w-full', + bottom: 'w-full', + }, + }, + parentCompoundVariants: [ + { + anchor: 'top', + size: 'sm', + class: 'h-1/4', + }, + { + anchor: 'top', + size: 'md', + class: 'h-1/2', + }, + { + anchor: 'top', + size: 'lg', + class: 'h-3/4', + }, + { + anchor: 'top', + size: 'full', + class: 'h-full', + }, + { + anchor: 'bottom', + size: 'sm', + class: 'h-1/4', + }, + { + anchor: 'bottom', + size: 'md', + class: 'h-1/2', + }, + { + anchor: 'bottom', + size: 'lg', + class: 'h-3/4', + }, + { + anchor: 'bottom', + size: 'full', + class: 'h-full', + }, + ], +}); + +const drawerCloseButtonStyle = tva({ + base: 'z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer', +}); + +const drawerHeaderStyle = tva({ + base: 'justify-between items-center flex-row', +}); + +const drawerBodyStyle = tva({ + base: 'mt-4 mb-6 shrink-0', +}); + +const drawerFooterStyle = tva({ + base: 'flex-row justify-end items-center', +}); + +type IDrawerProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerBackdropProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerContentProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerHeaderProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerBodyProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerFooterProps = React.ComponentProps & + VariantProps & { className?: string }; + +type IDrawerCloseButtonProps = React.ComponentProps< + typeof UIDrawer.CloseButton +> & + VariantProps & { className?: string }; + +const Drawer = React.forwardRef< + React.ElementRef, + IDrawerProps +>(({ className, size = 'sm', anchor = 'left', ...props }, ref) => { + return ( + + ); +}); + +const DrawerBackdrop = React.forwardRef< + React.ElementRef, + IDrawerBackdropProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +const DrawerContent = React.forwardRef< + React.ElementRef, + IDrawerContentProps +>(({ className, ...props }, ref) => { + const { size: parentSize, anchor: parentAnchor } = useStyleContext(SCOPE); + + const drawerHeight = screenHeight * (sizes[parentSize] || sizes.md); + const drawerWidth = screenWidth * (sizes[parentSize] || sizes.md); + + const isHorizontal = parentAnchor === 'left' || parentAnchor === 'right'; + + const initialObj = isHorizontal + ? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth } + : { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }; + + const animateObj = isHorizontal ? { x: 0 } : { y: 0 }; + + const exitObj = isHorizontal + ? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth } + : { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }; + + const customClass = isHorizontal + ? `top-0 ${parentAnchor === 'left' ? 'left-0' : 'right-0'}` + : `left-0 ${parentAnchor === 'top' ? 'top-0' : 'bottom-0'}`; + + return ( + + ); +}); + +const DrawerHeader = React.forwardRef< + React.ElementRef, + IDrawerHeaderProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +const DrawerBody = React.forwardRef< + React.ElementRef, + IDrawerBodyProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +const DrawerFooter = React.forwardRef< + React.ElementRef, + IDrawerFooterProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +const DrawerCloseButton = React.forwardRef< + React.ElementRef, + IDrawerCloseButtonProps +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +Drawer.displayName = 'Drawer'; +DrawerBackdrop.displayName = 'DrawerBackdrop'; +DrawerContent.displayName = 'DrawerContent'; +DrawerHeader.displayName = 'DrawerHeader'; +DrawerBody.displayName = 'DrawerBody'; +DrawerFooter.displayName = 'DrawerFooter'; +DrawerCloseButton.displayName = 'DrawerCloseButton'; + +export { + Drawer, + DrawerBackdrop, + DrawerContent, + DrawerCloseButton, + DrawerHeader, + DrawerBody, + DrawerFooter, +}; diff --git a/example/storybook-nativewind/src/core-components/nativewind/index.ts b/example/storybook-nativewind/src/core-components/nativewind/index.ts index 3fadb525f..23ee62e6a 100644 --- a/example/storybook-nativewind/src/core-components/nativewind/index.ts +++ b/example/storybook-nativewind/src/core-components/nativewind/index.ts @@ -50,3 +50,4 @@ export * from './refresh-control'; export * from './image-background'; export * from './skeleton'; export * from './bottomsheet'; +export * from './drawer';