From b34fb3e19195a747d9907ab578abd2fc18d8e670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Wed, 24 Jan 2024 13:54:12 +0000 Subject: [PATCH] feat(components): add drawer (#50) --- .changeset/lucky-olives-try.md | 5 ++ packages/components/src/Dialog/Dialog.tsx | 7 +- .../components/src/Drawer/Drawer.stories.tsx | 31 +++++++++ .../components/src/Drawer/Drawer.style.tsx | 54 +++++++++++++++ packages/components/src/Drawer/Drawer.tsx | 60 +++++++++++++++++ .../components/src/Drawer/DrawerWrapper.tsx | 65 +++++++++++++++++++ .../src/Drawer/__tests__/Drawer.test.tsx | 37 +++++++++++ packages/components/src/Drawer/index.tsx | 2 + packages/components/src/index.ts | 2 + 9 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 .changeset/lucky-olives-try.md create mode 100644 packages/components/src/Drawer/Drawer.stories.tsx create mode 100644 packages/components/src/Drawer/Drawer.style.tsx create mode 100644 packages/components/src/Drawer/Drawer.tsx create mode 100644 packages/components/src/Drawer/DrawerWrapper.tsx create mode 100644 packages/components/src/Drawer/__tests__/Drawer.test.tsx create mode 100644 packages/components/src/Drawer/index.tsx diff --git a/.changeset/lucky-olives-try.md b/.changeset/lucky-olives-try.md new file mode 100644 index 000000000..19c710092 --- /dev/null +++ b/.changeset/lucky-olives-try.md @@ -0,0 +1,5 @@ +--- +"@interlay/ui": patch +--- + +feat(components): add drawer diff --git a/packages/components/src/Dialog/Dialog.tsx b/packages/components/src/Dialog/Dialog.tsx index c6b2e5021..8eb4fcb6f 100644 --- a/packages/components/src/Dialog/Dialog.tsx +++ b/packages/components/src/Dialog/Dialog.tsx @@ -6,6 +6,7 @@ import { XMark } from '@interlay/icons'; import { useDOMRef } from '@interlay/hooks'; import { CTASizes, Sizes } from '../../../core/theme/src'; +import { ElementTypeProp } from '../utils/types'; import { StyledCloseCTA, StyledDialog } from './Dialog.style'; import { DialogContext } from './DialogContext'; @@ -20,10 +21,10 @@ type Props = { type InheritAttrs = Omit; -type DialogProps = Props & InheritAttrs; +type DialogProps = Props & InheritAttrs & ElementTypeProp; const Dialog = forwardRef( - ({ children, onClose, size = 'medium', ...props }, ref): JSX.Element => { + ({ children, onClose, size = 'medium', elementType, role = 'dialog', ...props }, ref): JSX.Element => { const dialogRef = useDOMRef(ref); // Get props for the dialog and its title @@ -33,7 +34,7 @@ const Dialog = forwardRef( return ( - + {onClose && ( diff --git a/packages/components/src/Drawer/Drawer.stories.tsx b/packages/components/src/Drawer/Drawer.stories.tsx new file mode 100644 index 000000000..bc31a0a2a --- /dev/null +++ b/packages/components/src/Drawer/Drawer.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; + +import { CTA } from '..'; + +import { Drawer, DrawerProps } from '.'; + +export default { + title: 'Overlays/Drawer', + component: Drawer, + parameters: { + layout: 'centered' + } +} as Meta; + +const Render = () => { + const [isOpen, setOpen] = useState(false); + + return ( + <> + setOpen(true)}>Open + setOpen(false)}> + Drawer + + + ); +}; + +export const Default: StoryObj = { + render: Render +}; diff --git a/packages/components/src/Drawer/Drawer.style.tsx b/packages/components/src/Drawer/Drawer.style.tsx new file mode 100644 index 000000000..d624d0fc8 --- /dev/null +++ b/packages/components/src/Drawer/Drawer.style.tsx @@ -0,0 +1,54 @@ +import styled from 'styled-components'; +import { theme } from '@interlay/theme'; + +import { overlayCSS } from '../utils/overlay'; +import { Dialog } from '../Dialog'; + +type StyledModalProps = { + $isOpen?: boolean; +}; + +type StyledDialogProps = { + $isOpen?: boolean; +}; + +const StyledModal = styled.div` + transform: ${({ $isOpen }) => ($isOpen ? 'translateX(100%)' : 'translateX(0%)')}; + ${({ $isOpen }) => overlayCSS(!!$isOpen)} + + visibility: visible; + pointer-events: auto; + + outline: none; + opacity: 1; + + overflow-y: scroll; + z-index: ${theme.modal.zIndex}; + position: fixed; + top: 0; + bottom: 0; + left: auto; + right: 100%; + + height: 100%; + background: ${theme.colors.bgPrimary}; + + transition: transform + ${({ $isOpen }) => ($isOpen ? theme.transition.duration.duration250 : theme.transition.duration.duration100)}ms + ease-in-out; +`; + +const StyledDialog = styled(Dialog)` + pointer-events: ${({ $isOpen }) => !$isOpen && 'none'}; + background: none; + border: none; + border-radius: 0px; + width: 300px; + display: flex; + flex-direction: column; + position: relative; + outline: none; + padding: ${theme.spacing.spacing4}; +`; + +export { StyledDialog, StyledModal }; diff --git a/packages/components/src/Drawer/Drawer.tsx b/packages/components/src/Drawer/Drawer.tsx new file mode 100644 index 000000000..63271e3fb --- /dev/null +++ b/packages/components/src/Drawer/Drawer.tsx @@ -0,0 +1,60 @@ +import { useDOMRef } from '@interlay/hooks'; +import { forwardRef, useRef } from 'react'; + +import { DialogProps } from '../Dialog'; +import { Overlay } from '../Overlay'; +import { ElementTypeProp } from '../utils/types'; + +import { StyledDialog } from './Drawer.style'; +import { DrawerWrapper, DrawerWrapperProps } from './DrawerWrapper'; + +type Props = { + container?: Element; +}; + +type InheritAttrs = Omit; + +type DrawerProps = Props & InheritAttrs & ElementTypeProp; + +const Drawer = forwardRef( + ( + { + children, + isDismissable = true, + isKeyboardDismissDisabled, + shouldCloseOnBlur, + container, + isOpen, + elementType = 'div', + ...props + }, + ref + ): JSX.Element | null => { + const domRef = useDOMRef(ref); + const { onClose } = props; + const wrapperRef = useRef(null); + + return ( + + + + {children} + + + + ); + } +); + +Drawer.displayName = 'Drawer'; + +export { Drawer }; +export type { DrawerProps }; diff --git a/packages/components/src/Drawer/DrawerWrapper.tsx b/packages/components/src/Drawer/DrawerWrapper.tsx new file mode 100644 index 000000000..a52960b6a --- /dev/null +++ b/packages/components/src/Drawer/DrawerWrapper.tsx @@ -0,0 +1,65 @@ +import { AriaModalOverlayProps, AriaOverlayProps, useModalOverlay } from '@react-aria/overlays'; +import { mergeProps } from '@react-aria/utils'; +import { OverlayTriggerState } from '@react-stately/overlays'; +import { forwardRef, ReactNode, RefObject } from 'react'; + +import { Underlay } from '../Overlay/Underlay'; + +import { StyledModal } from './Drawer.style'; + +type Props = { + children: ReactNode; + isOpen?: boolean; + onClose: () => void; + wrapperRef: RefObject; +}; + +type InheritAttrs = Omit; + +type DrawerWrapperProps = Props & InheritAttrs; + +const DrawerWrapper = forwardRef( + ( + { + children, + isDismissable = true, + onClose, + isKeyboardDismissDisabled, + isOpen, + shouldCloseOnInteractOutside, + shouldCloseOnBlur, + wrapperRef, + ...props + }, + ref + ): JSX.Element | null => { + // Handle interacting outside the dialog and pressing + // the Escape key to close the modal. + const { modalProps, underlayProps } = useModalOverlay( + { + isDismissable, + isKeyboardDismissDisabled, + shouldCloseOnInteractOutside, + shouldCloseOnBlur, + ...props + } as AriaOverlayProps, + // These are the only props needed + { isOpen: !!isOpen, close: onClose } as OverlayTriggerState, + ref as RefObject + ); + + return ( +
+ + + {children} + +
+ ); + } +); + +DrawerWrapper.displayName = 'DrawerWrapper'; + +export { DrawerWrapper }; +export type { DrawerWrapperProps }; diff --git a/packages/components/src/Drawer/__tests__/Drawer.test.tsx b/packages/components/src/Drawer/__tests__/Drawer.test.tsx new file mode 100644 index 000000000..77eda62ce --- /dev/null +++ b/packages/components/src/Drawer/__tests__/Drawer.test.tsx @@ -0,0 +1,37 @@ +import { render } from '@testing-library/react'; +import { createRef } from 'react'; +import { testA11y } from '@interlay/test-utils'; + +import { Drawer } from '..'; + +describe('Drawer', () => { + it('should render correctly', () => { + const wrapper = render( + + content + + ); + + expect(() => wrapper.unmount()).not.toThrow(); + }); + + it('ref should be forwarded', () => { + const ref = createRef(); + + render( + + content + + ); + + expect(ref.current).not.toBeNull(); + }); + + it('should pass a11y', async () => { + await testA11y( + + content + + ); + }); +}); diff --git a/packages/components/src/Drawer/index.tsx b/packages/components/src/Drawer/index.tsx new file mode 100644 index 000000000..31d63f1f6 --- /dev/null +++ b/packages/components/src/Drawer/index.tsx @@ -0,0 +1,2 @@ +export type { DrawerProps } from './Drawer'; +export { Drawer } from './Drawer'; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index e69567ea9..8abfaa974 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -47,6 +47,8 @@ export type { SelectProps } from './Select'; export { Item, Select } from './Select'; export type { SliderProps } from './Slider'; export { Slider } from './Slider'; +export type { DrawerProps } from './Drawer'; +export { Drawer } from './Drawer'; export type { StackProps } from './Stack'; export { Stack } from './Stack'; export type { SwitchProps } from './Switch';