Skip to content

Commit

Permalink
feat(Modal): add basic feature
Browse files Browse the repository at this point in the history
  • Loading branch information
shervinchen committed Nov 8, 2023
1 parent f553dd5 commit 07b7247
Show file tree
Hide file tree
Showing 30 changed files with 612 additions and 38 deletions.
16 changes: 16 additions & 0 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ import {
DemoPopoverHideArrow,
DemoPopoverPlacement,
} from './popover';
import {
DemoModalDefault,
DemoModalNotCloseOnOverlayClick,
DemoModalWidth,
} from './modal';

const Container: FC<PropsWithChildren<{ title: string }>> = ({
title,
Expand Down Expand Up @@ -309,6 +314,17 @@ function App() {
<DemoPopoverControlled />
</Wrapper>
</Container>
<Container title="Modal">
<Wrapper title="Default">
<DemoModalDefault />
</Wrapper>
<Wrapper title="Not Close On Overlay Click">
<DemoModalNotCloseOnOverlayClick />
</Wrapper>
<Wrapper title="Width">
<DemoModalWidth />
</Wrapper>
</Container>
</div>
);
}
Expand Down
97 changes: 97 additions & 0 deletions demo/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useState } from 'react';
import Unit from '../Unit';
import { Button, Modal } from '@/packages';

export function DemoModalDefault() {
const [visible, setVisible] = useState(false);

const openModal = () => setVisible(true);

const closeModal = () => {
setVisible(false);
};

return (
<Unit layout="row">
<>
<Button onClick={openModal}>Open Modal</Button>
<Modal visible={visible} onClose={closeModal}>
<Modal.Header>Modal Title</Modal.Header>
<Modal.Body>
<div>This is a modal.</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={closeModal}>Cancel</Button>
<Button type="primary" onClick={closeModal}>
Confirm
</Button>
</Modal.Footer>
</Modal>
</>
</Unit>
);
}

export function DemoModalNotCloseOnOverlayClick() {
const [visible, setVisible] = useState(false);

const openModal = () => setVisible(true);

const closeModal = () => {
setVisible(false);
};

return (
<Unit layout="row">
<>
<Button onClick={openModal}>Open Modal</Button>
<Modal
visible={visible}
onClose={closeModal}
closeOnOverlayClick={false}
>
<Modal.Header>Modal Title</Modal.Header>
<Modal.Body>
<div>This is a modal.</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={closeModal}>Cancel</Button>
<Button type="primary" onClick={closeModal}>
Confirm
</Button>
</Modal.Footer>
</Modal>
</>
</Unit>
);
}

export function DemoModalWidth() {
const [visible, setVisible] = useState(false);

const openModal = () => setVisible(true);

const closeModal = () => {
setVisible(false);
};

return (
<Unit layout="row">
<>
<Button onClick={openModal}>Open Modal</Button>
<Modal visible={visible} onClose={closeModal} width="400px">
<Modal.Header>Modal Title</Modal.Header>
<Modal.Body>
<div>This is a modal.</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={closeModal}>Cancel</Button>
<Button type="primary" onClick={closeModal}>
Confirm
</Button>
</Modal.Footer>
</Modal>
</>
</Unit>
);
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"files": [
"dist",
"package.json",
"README.md"
"README.md",
"LICENSE"
],
"scripts": {
"build:clear": "rm -rf ./dist",
Expand Down Expand Up @@ -125,6 +126,7 @@
"dependencies": {
"classnames": "^2.3.2",
"react-feather": "^2.0.10",
"react-remove-scroll": "^2.5.7",
"styled-jsx": "^5.1.2"
}
}
53 changes: 53 additions & 0 deletions packages/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { FC, PropsWithChildren, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { ModalProps } from './Modal.types';
import { useControlled, usePortal } from '../utils/hooks';
import Overlay from '../Overlay';
import ModalWrapper from './ModalWrapper';
import { ModalConfig, ModalContext } from './modal-context';
import ModalCloseButton from './ModalCloseButton';

const Modal: FC<PropsWithChildren<ModalProps>> = ({
visible,
width = '540px',
closeOnOverlayClick = true,
onClose,
children,
...restProps
}) => {
const portal = usePortal('modal');
const [internalValue, setInternalValue] = useControlled<boolean>({
defaultValue: false,
value: visible,
});

const closeModal = useCallback(() => {
setInternalValue(false);
onClose?.();
}, [onClose, setInternalValue]);

const modalConfig: ModalConfig = useMemo(
() => ({
width,
closeOnOverlayClick,
closeModal,
}),
[closeModal, width, closeOnOverlayClick]
);

if (!portal) return null;

return createPortal(
<ModalContext.Provider value={modalConfig}>
<Overlay visible={internalValue}>
<ModalWrapper visible={internalValue} {...restProps}>
<ModalCloseButton />
{children}
</ModalWrapper>
</Overlay>
</ModalContext.Provider>,
portal
);
};

export default Modal;
15 changes: 15 additions & 0 deletions packages/Modal/Modal.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { HTMLAttributes } from 'react';

interface BaseModalProps {
visible?: boolean;
width?: string;
closeOnOverlayClick?: boolean;
onClose?: () => void;
}

type NativeModalProps = Omit<
HTMLAttributes<HTMLDivElement>,
keyof BaseModalProps
>;

export type ModalProps = BaseModalProps & NativeModalProps;
18 changes: 18 additions & 0 deletions packages/Modal/ModalBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { FC, PropsWithChildren } from 'react';

const ModalBody: FC<PropsWithChildren> = ({ children, ...restProps }) => {
return (
<div className="raw-modal-body" {...restProps}>
{children}
<style jsx>{`
.raw-modal-body {
flex: 1 1 0%;
padding: 8px 24px;
font-size: 16px;
}
`}</style>
</div>
);
};

export default ModalBody;
41 changes: 41 additions & 0 deletions packages/Modal/ModalCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { X } from 'react-feather';
import { useTheme } from '../Theme';
import { useModalContext } from './modal-context';

const ModalCloseButton = () => {
const theme = useTheme();
const { closeModal } = useModalContext();

return (
<button className="raw-modal-close-button" onClick={closeModal}>
<X size={20} />
<style jsx>{`
.raw-modal-close-button {
position: absolute;
top: 16px;
right: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: ${theme.palette.accents7};
transition-property: color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 0.15s;
flex-shrink: 0;
padding: 0;
background: transparent;
appearance: none;
border: none;
outline: none;
}
.raw-modal-close-button:hover {
color: ${theme.palette.foreground};
}
`}</style>
</button>
);
};

export default ModalCloseButton;
20 changes: 20 additions & 0 deletions packages/Modal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { FC, PropsWithChildren } from 'react';

const ModalFooter: FC<PropsWithChildren> = ({ children, ...restProps }) => {
return (
<footer className="raw-modal-footer" {...restProps}>
{children}
<style jsx>{`
.raw-modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 16px 24px;
gap: 16px;
}
`}</style>
</footer>
);
};

export default ModalFooter;
19 changes: 19 additions & 0 deletions packages/Modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { FC, PropsWithChildren } from 'react';

const ModalHeader: FC<PropsWithChildren> = ({ children, ...restProps }) => {
return (
<header className="raw-modal-header" {...restProps}>
{children}
<style jsx>{`
.raw-modal-header {
flex: 0 1 0%;
padding: 16px 24px;
font-weight: 600;
font-size: 24px;
}
`}</style>
</header>
);
};

export default ModalHeader;
Loading

0 comments on commit 07b7247

Please sign in to comment.