-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 🎸 add MessageHalfModal component
Signed-off-by: kodai3 <k3dai.su3@gmail.com>
- Loading branch information
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
139 changes: 139 additions & 0 deletions
139
src/components/MessageHalfModal/MessageHalfModal.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |