Skip to content

Commit

Permalink
feat: 🎸 add MessageHalfModal component
Browse files Browse the repository at this point in the history
Signed-off-by: kodai3 <k3dai.su3@gmail.com>
  • Loading branch information
kodai3 committed May 3, 2024
1 parent 54d8c85 commit a25904a
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
139 changes: 139 additions & 0 deletions src/components/MessageHalfModal/MessageHalfModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: var(--z-index-modal);
overflow: hidden;
}

.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

.normalOverlay {
background: rgb(0 0 0 / 50%);
}

.darkerOverlay {
background: rgb(0 0 0 / 80%);
}

.contents {
padding: 0 var(--size-spacing-md);
}

.modalBody {
position: fixed;
bottom: 0;
left: 50%;
display: flex;
flex-direction: column;
gap: var(--size-spacing-lg);
width: 100%;
max-width: 600px;
max-height: calc(100% - 24px);
padding: var(--size-spacing-lg) 0;
margin: 0 auto;
overflow-y: auto;
background: #fff;
border-radius: 12px;
border-end-start-radius: 0;
border-end-end-radius: 0;
transform: translate3d(-50%, 0, 0);
}

.modalBody.bodyScroll {
overflow-y: auto;
}

.modalBody.headerLess {
padding-top: var(--size-spacing-xl);
}

.modalBody.fullscreen {
height: calc(100% - 24px);
}

.modalBody.fullscreen.contents {
height: 100%;
min-height: 400px;
overflow: hidden;
}

.header {
font-size: var(--text-heading-xs-size);
font-weight: bold;
line-height: var(--text-heading-xs-line);
text-align: center;
white-space: pre-wrap;
}

.buttonContainer {
display: grid;
gap: var(--size-spacing-md);
padding: 0 var(--size-spacing-md);
}

.overlayEnter {
transition-timing-function: ease-out;
transition-duration: 300ms;
transition-property: opacity;
}

.overlayEnterFrom {
opacity: 0;
}

.overlayEnterTo {
opacity: 1;
}

.overlayLeave {
transition-timing-function: ease-in;
transition-duration: 200ms;
}

.overlayLeaveFrom {
opacity: 1;
}

.overlayLeaveTo {
opacity: 0;
}

.panelEnter {
transition-timing-function: ease-out;
transition-duration: 300ms;
transition-property: transform, opacity;
}

.panelEnterFrom {
opacity: 0;
transform: translate3d(-50%, 100%, 0);
}

.panelEnterTo {
opacity: 1;
transform: translate3d(-50%, 0, 0);
}

.panelLeave {
transition-timing-function: ease-in;
transition-duration: 200ms;
}

.panelLeaveFrom {
opacity: 1;
transform: translate3d(-50%, 0, 0);
}

.panelLeaveTo {
opacity: 0;
transform: translate3d(-50%, 100%, 0);
}
120 changes: 120 additions & 0 deletions src/components/MessageHalfModal/MessageHalfModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use client';

import { Dialog, Transition } from '@headlessui/react';
import { clsx } from 'clsx';
import { FC, Fragment, PropsWithChildren } from 'react';
import styles from './MessageHalfModal.module.css';
import { opacityToClassName } from '../../utils/style';
import { Button } from '../Button/Button';

type Opacity = 'normal' | 'darker';

type BaseProps = {
/**
* 閉じるアクションが実行された場合のコールバック
*/
onClose: () => void;
/**
* ヘッダーに表示する見出しテキスト
*/
header?: string;

/**
* 閉じるボタンのラベル
* @default 閉じる
*/
closeLabel?: string;
/**
* オーバーレイの透過度
* @default normal
*/
overlayOpacity?: Opacity;
/**
* 閉じるボタンを表示するかどうか
* @default true
*/
showClose?: boolean;
/**
* モーダルを開くかどうか
* @default true
*/
open?: boolean;
/**
* openを無視してモーダルを開いたままにするかどうか。アニメーションライブラリとの連携で、ActionHalfModal自身が開閉に関与しない場合に使用
* @default false
*/
isStatic?: boolean;
/**
* モーダルをフルスクリーンで表示するかどうか
* @default false
*/
fullscreen?: boolean;
/**
* モーダルボディ部分のスクロールを許可するかどうか
* @default true
*/
bodyScroll?: boolean;
};

type Props = BaseProps;

export const MessageHalfModal: FC<PropsWithChildren<Props>> = ({
children,
onClose,
header,
closeLabel = '閉じる',
overlayOpacity = 'normal',
showClose = true,
open = true,
isStatic = false,
fullscreen = false,
bodyScroll = true,
}) => {
const opacityClassName = opacityToClassName(overlayOpacity);

return (
<Transition show={open}>
<Dialog static={isStatic} onClose={onClose} className={clsx(styles.modal, fullscreen && styles.fullscreen)}>
<Transition.Child
as={Fragment}
enter={styles.overlayEnter}
enterFrom={styles.overlayEnterFrom}
enterTo={styles.overlayEnterTo}
leave={styles.overlayLeave}
leaveFrom={styles.overlayLeaveFrom}
leaveTo={styles.overlayLeaveTo}
>
<Dialog.Overlay className={clsx(styles.overlay, styles[opacityClassName])} />
</Transition.Child>
<Transition.Child
as={Fragment}
enter={styles.panelEnter}
enterFrom={styles.panelEnterFrom}
enterTo={styles.panelEnterTo}
leave={styles.panelLeave}
leaveFrom={styles.panelLeaveFrom}
leaveTo={styles.panelLeaveTo}
>
<div
className={clsx(
styles.modalBody,
!header && styles.headerLess,
fullscreen && styles.fullscreen,
bodyScroll && styles.bodyScroll,
)}
>
{header && <Dialog.Title className={styles.header}>{header}</Dialog.Title>}
<div className={styles.contents}>{children}</div>
<div className={styles.buttonContainer}>
{showClose && (
<Button variant="primary" onClick={onClose} aria-label={closeLabel}>
{closeLabel}
</Button>
)}
</div>
</div>
</Transition.Child>
</Dialog>
</Transition>
);
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { CheckboxGroup } from './components/CheckboxGroup/CheckboxGroup';
export { Input } from './components/Input/Input';
export { Label } from './components/Label/Label';
export { LinkCard } from './components/LinkCard/LinkCard';
export { MessageHalfModal } from './components/MessageHalfModal/MessageHalfModal';
export { MessageModal } from './components/MessageModal/MessageModal';
export { RadioButton } from './components/RadioButton/RadioButton';
export { RadioCard } from './components/RadioCard/RadioCard';
Expand Down
75 changes: 75 additions & 0 deletions src/stories/MessageHalfModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Meta, StoryObj } from '@storybook/react';
import { useCallback, useState } from 'react';
import { MessageHalfModal } from '..';

export default {
title: 'Modal/MessageHalfModal',
component: MessageHalfModal,
} satisfies Meta<typeof MessageHalfModal>;

type Story = StoryObj<typeof MessageHalfModal>;

const defaultArgs = {
header: 'モーダル',
children: 'body',
};

export const Default: Story = {
render: (args) => {
const [open, setOpen] = useState(true);

const onClose = useCallback(() => {
setOpen(false);
}, []);

return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open Modal
</button>
<MessageHalfModal {...args} open={open} onClose={onClose} />
</>
);
},
args: defaultArgs,
};

export const Fullscreen: Story = {
render: (args) => {
const [open, setOpen] = useState(false);

const onClose = useCallback(() => {
setOpen(false);
}, []);

return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open Modal
</button>
<MessageHalfModal {...args} open={open} onClose={onClose} fullscreen />
</>
);
},
args: defaultArgs,
};

export const NoCloseButton: Story = {
render: (args) => {
const [open, setOpen] = useState(false);

const onClose = useCallback(() => {
setOpen(false);
}, []);

return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open Modal
</button>
<MessageHalfModal showClose={false} {...args} open={open} onClose={onClose} />
</>
);
},
args: defaultArgs,
};

0 comments on commit a25904a

Please sign in to comment.