mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-24 04:15:54 +08:00
* fix(modal): fix layout of modal actions on safari mobile * fix(modal): fix vertical alignment on safari mobile * test(modal): update snapshots * docs: hide menu when modal active * docs(modal): add overlong case
107 lines
2.9 KiB
TypeScript
107 lines
2.9 KiB
TypeScript
import React, { useEffect, useMemo } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import usePortal from '../utils/use-portal'
|
|
import ModalTitle from './modal-title'
|
|
import ModalSubtitle from './modal-subtitle'
|
|
import ModalWrapper from './modal-wrapper'
|
|
import ModalContent from './modal-content'
|
|
import ModalAction from './modal-action'
|
|
import ModalActions from './modal-actions'
|
|
import Backdrop from '../shared/backdrop'
|
|
import { ModalConfig, ModalContext } from './modal-context'
|
|
import { pickChild } from '../utils/collections'
|
|
import useBodyScroll from '../utils/use-body-scroll'
|
|
import useCurrentState from '../utils/use-current-state'
|
|
|
|
interface Props {
|
|
disableBackdropClick?: boolean
|
|
onClose?: () => void
|
|
onOpen?: () => void
|
|
open?: boolean
|
|
width?: string
|
|
wrapClassName?: string
|
|
}
|
|
|
|
const defaultProps = {
|
|
width: '26rem',
|
|
wrapClassName: '',
|
|
disableBackdropClick: false,
|
|
}
|
|
|
|
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
|
export type ModalProps = Props & typeof defaultProps & NativeAttrs
|
|
|
|
const Modal: React.FC<React.PropsWithChildren<ModalProps>> = ({
|
|
children,
|
|
disableBackdropClick,
|
|
onClose,
|
|
onOpen,
|
|
open,
|
|
width: wrapperWidth,
|
|
wrapClassName,
|
|
}) => {
|
|
const portal = usePortal('modal')
|
|
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
|
|
const [visible, setVisible, visibleRef] = useCurrentState<boolean>(false)
|
|
const [withoutActionsChildren, ActionsChildren] = pickChild(children, ModalAction)
|
|
const hasActions = ActionsChildren && React.Children.count(ActionsChildren) > 0
|
|
|
|
const closeModal = () => {
|
|
onClose && onClose()
|
|
setVisible(false)
|
|
setBodyHidden(false)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (open === undefined) return
|
|
if (open) {
|
|
onOpen && onOpen()
|
|
}
|
|
if (!open && visibleRef.current) {
|
|
onClose && onClose()
|
|
}
|
|
|
|
setVisible(open)
|
|
setBodyHidden(open)
|
|
}, [open])
|
|
|
|
const closeFromBackdrop = () => {
|
|
if (disableBackdropClick && hasActions) return
|
|
closeModal()
|
|
}
|
|
|
|
const modalConfig: ModalConfig = useMemo(
|
|
() => ({
|
|
close: closeModal,
|
|
}),
|
|
[],
|
|
)
|
|
|
|
if (!portal) return null
|
|
return createPortal(
|
|
<ModalContext.Provider value={modalConfig}>
|
|
<Backdrop onClick={closeFromBackdrop} visible={visible} width={wrapperWidth}>
|
|
<ModalWrapper visible={visible} className={wrapClassName}>
|
|
{withoutActionsChildren}
|
|
{hasActions && <ModalActions>{ActionsChildren}</ModalActions>}
|
|
</ModalWrapper>
|
|
</Backdrop>
|
|
</ModalContext.Provider>,
|
|
portal,
|
|
)
|
|
}
|
|
|
|
type ModalComponent<P = {}> = React.FC<P> & {
|
|
Title: typeof ModalTitle
|
|
Subtitle: typeof ModalSubtitle
|
|
Content: typeof ModalContent
|
|
Action: typeof ModalAction
|
|
}
|
|
type ComponentProps = Partial<typeof defaultProps> &
|
|
Omit<Props, keyof typeof defaultProps> &
|
|
NativeAttrs
|
|
|
|
Modal.defaultProps = defaultProps
|
|
|
|
export default Modal as ModalComponent<ComponentProps>
|