From a25904a4accee1c1e667d787741d8809994fb51d Mon Sep 17 00:00:00 2001 From: kodai3 Date: Fri, 3 May 2024 15:44:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20MessageHalfModal?= =?UTF-8?q?=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: kodai3 --- .../MessageHalfModal.module.css | 139 ++++++++++++++++++ .../MessageHalfModal/MessageHalfModal.tsx | 120 +++++++++++++++ src/index.ts | 1 + src/stories/MessageHalfModal.stories.tsx | 75 ++++++++++ 4 files changed, 335 insertions(+) create mode 100644 src/components/MessageHalfModal/MessageHalfModal.module.css create mode 100644 src/components/MessageHalfModal/MessageHalfModal.tsx create mode 100644 src/stories/MessageHalfModal.stories.tsx diff --git a/src/components/MessageHalfModal/MessageHalfModal.module.css b/src/components/MessageHalfModal/MessageHalfModal.module.css new file mode 100644 index 00000000..32f4bb6d --- /dev/null +++ b/src/components/MessageHalfModal/MessageHalfModal.module.css @@ -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); +} diff --git a/src/components/MessageHalfModal/MessageHalfModal.tsx b/src/components/MessageHalfModal/MessageHalfModal.tsx new file mode 100644 index 00000000..a07d35c2 --- /dev/null +++ b/src/components/MessageHalfModal/MessageHalfModal.tsx @@ -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> = ({ + children, + onClose, + header, + closeLabel = '閉じる', + overlayOpacity = 'normal', + showClose = true, + open = true, + isStatic = false, + fullscreen = false, + bodyScroll = true, +}) => { + const opacityClassName = opacityToClassName(overlayOpacity); + + return ( + + + + + + +
+ {header && {header}} +
{children}
+
+ {showClose && ( + + )} +
+
+
+
+
+ ); +}; diff --git a/src/index.ts b/src/index.ts index 15854014..e0e080e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/stories/MessageHalfModal.stories.tsx b/src/stories/MessageHalfModal.stories.tsx new file mode 100644 index 00000000..bb2b9350 --- /dev/null +++ b/src/stories/MessageHalfModal.stories.tsx @@ -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; + +type Story = StoryObj; + +const defaultArgs = { + header: 'モーダル', + children: 'body', +}; + +export const Default: Story = { + render: (args) => { + const [open, setOpen] = useState(true); + + const onClose = useCallback(() => { + setOpen(false); + }, []); + + return ( + <> + + + + ); + }, + args: defaultArgs, +}; + +export const Fullscreen: Story = { + render: (args) => { + const [open, setOpen] = useState(false); + + const onClose = useCallback(() => { + setOpen(false); + }, []); + + return ( + <> + + + + ); + }, + args: defaultArgs, +}; + +export const NoCloseButton: Story = { + render: (args) => { + const [open, setOpen] = useState(false); + + const onClose = useCallback(() => { + setOpen(false); + }, []); + + return ( + <> + + + + ); + }, + args: defaultArgs, +};