Skip to content

Commit

Permalink
feat(components): add drawer (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielsimao authored Jan 24, 2024
1 parent 8ca4409 commit b34fb3e
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/lucky-olives-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@interlay/ui": patch
---

feat(components): add drawer
7 changes: 4 additions & 3 deletions packages/components/src/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,10 +21,10 @@ type Props = {

type InheritAttrs = Omit<AriaDialogProps, keyof Props>;

type DialogProps = Props & InheritAttrs;
type DialogProps = Props & InheritAttrs & ElementTypeProp;

const Dialog = forwardRef<HTMLDivElement, DialogProps>(
({ 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
Expand All @@ -33,7 +34,7 @@ const Dialog = forwardRef<HTMLDivElement, DialogProps>(

return (
<DialogContext.Provider value={{ titleProps, size }}>
<StyledDialog ref={dialogRef} $size={size} {...mergeProps(props, dialogProps)}>
<StyledDialog ref={dialogRef} $size={size} as={elementType} {...mergeProps(props, dialogProps)} role={role}>
{onClose && (
<StyledCloseCTA aria-label='Dismiss' size={closeCTASize} variant='text' onPress={onClose}>
<XMark />
Expand Down
31 changes: 31 additions & 0 deletions packages/components/src/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Drawer>;

const Render = () => {
const [isOpen, setOpen] = useState(false);

return (
<>
<CTA onClick={() => setOpen(true)}>Open</CTA>
<Drawer isOpen={isOpen} onClose={() => setOpen(false)}>
Drawer
</Drawer>
</>
);
};

export const Default: StoryObj<DrawerProps> = {
render: Render
};
54 changes: 54 additions & 0 deletions packages/components/src/Drawer/Drawer.style.tsx
Original file line number Diff line number Diff line change
@@ -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<StyledModalProps>`
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)<StyledDialogProps>`
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 };
60 changes: 60 additions & 0 deletions packages/components/src/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -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<DrawerWrapperProps & DialogProps, keyof Props | 'size' | 'wrapperRef'>;

type DrawerProps = Props & InheritAttrs & ElementTypeProp;

const Drawer = forwardRef<HTMLDivElement, DrawerProps>(
(
{
children,
isDismissable = true,
isKeyboardDismissDisabled,
shouldCloseOnBlur,
container,
isOpen,
elementType = 'div',
...props
},
ref
): JSX.Element | null => {
const domRef = useDOMRef(ref);
const { onClose } = props;
const wrapperRef = useRef<HTMLDivElement>(null);

return (
<Overlay container={container} isOpen={isOpen} nodeRef={wrapperRef}>
<DrawerWrapper
ref={domRef}
isDismissable={isDismissable}
isKeyboardDismissDisabled={isKeyboardDismissDisabled}
isOpen={isOpen}
shouldCloseOnBlur={shouldCloseOnBlur}
wrapperRef={wrapperRef}
onClose={onClose}
>
<StyledDialog $isOpen={isOpen} {...props} elementType={elementType} role={undefined} onClose={undefined}>
{children}
</StyledDialog>
</DrawerWrapper>
</Overlay>
);
}
);

Drawer.displayName = 'Drawer';

export { Drawer };
export type { DrawerProps };
65 changes: 65 additions & 0 deletions packages/components/src/Drawer/DrawerWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
};

type InheritAttrs = Omit<AriaModalOverlayProps & AriaOverlayProps, keyof Props>;

type DrawerWrapperProps = Props & InheritAttrs;

const DrawerWrapper = forwardRef<HTMLDivElement, DrawerWrapperProps>(
(
{
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<HTMLElement>
);

return (
<div ref={wrapperRef}>
<Underlay {...underlayProps} isOpen={!!isOpen} />
<StyledModal ref={ref} $isOpen={isOpen} {...mergeProps(modalProps, props)}>
{children}
</StyledModal>
</div>
);
}
);

DrawerWrapper.displayName = 'DrawerWrapper';

export { DrawerWrapper };
export type { DrawerWrapperProps };
37 changes: 37 additions & 0 deletions packages/components/src/Drawer/__tests__/Drawer.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Drawer isOpen onClose={jest.fn}>
content
</Drawer>
);

expect(() => wrapper.unmount()).not.toThrow();
});

it('ref should be forwarded', () => {
const ref = createRef<HTMLDivElement>();

render(
<Drawer ref={ref} isOpen onClose={jest.fn}>
content
</Drawer>
);

expect(ref.current).not.toBeNull();
});

it('should pass a11y', async () => {
await testA11y(
<Drawer isOpen onClose={jest.fn}>
content
</Drawer>
);
});
});
2 changes: 2 additions & 0 deletions packages/components/src/Drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { DrawerProps } from './Drawer';
export { Drawer } from './Drawer';
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit b34fb3e

Please sign in to comment.