mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-29 12:45:32 +08:00
feat: initial
This commit is contained in:
12
components/modal/index.ts
Normal file
12
components/modal/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import Modal from './modal'
|
||||
import ModalTitle from './modal-title'
|
||||
import ModalSubtitle from './modal-subtitle'
|
||||
import ModalContent from './modal-content'
|
||||
import ModalAction from './modal-action'
|
||||
|
||||
Modal.Title = ModalTitle
|
||||
Modal.Subtitle = ModalSubtitle
|
||||
Modal.Content = ModalContent
|
||||
Modal.Action = ModalAction
|
||||
|
||||
export default Modal
|
||||
78
components/modal/modal-action.tsx
Normal file
78
components/modal/modal-action.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { MouseEvent, useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import { useModalContext } from './modal-context'
|
||||
|
||||
type ModalActionEvent = MouseEvent<HTMLButtonElement> & {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
passive?: boolean
|
||||
disabled?: boolean
|
||||
onClick?: (event: ModalActionEvent) => void
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
passive: false,
|
||||
disabled: false,
|
||||
onClick: (event: ModalActionEvent) => event.close && event.close(),
|
||||
}
|
||||
|
||||
export type ModalActionProps = Props & typeof defaultProps & React.ButtonHTMLAttributes<any>
|
||||
|
||||
const ModalAction: React.FC<ModalActionProps> = React.memo(({
|
||||
className, children, onClick, passive, disabled, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const { close } = useModalContext()
|
||||
const clickHandler = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (disabled) return
|
||||
const actionEvent = Object.assign({}, event, {
|
||||
close: close,
|
||||
})
|
||||
onClick(actionEvent)
|
||||
}
|
||||
|
||||
const color = useMemo(() => {
|
||||
return passive || disabled ? theme.palette.accents_5 : theme.palette.foreground
|
||||
}, [theme.palette, passive, disabled])
|
||||
|
||||
const bgColor = useMemo(() => {
|
||||
return disabled ? theme.palette.accents_1 : theme.palette.background
|
||||
}, [theme.palette, disabled])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className={className} onClick={clickHandler} {...props}>{children}</button>
|
||||
<style jsx>{`
|
||||
button {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
transition: all 200ms ease-in-out 0s;
|
||||
border: none;
|
||||
color: ${color};
|
||||
background-color: ${bgColor};
|
||||
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: ${disabled ? color : theme.palette.foreground};
|
||||
background-color: ${disabled ? bgColor : theme.palette.accents_1};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(ModalAction, defaultProps)
|
||||
42
components/modal/modal-actions.tsx
Normal file
42
components/modal/modal-actions.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
|
||||
const ModalActions: React.FC<React.PropsWithChildren<{}>> = React.memo(({
|
||||
children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<>
|
||||
<div />
|
||||
<footer {...props}>
|
||||
{children}
|
||||
|
||||
</footer>
|
||||
<style jsx>{`
|
||||
footer {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 3.625rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-top: 1px solid ${theme.palette.border};
|
||||
border-bottom-left-radius: ${theme.layout.radius};
|
||||
border-bottom-right-radius: ${theme.layout.radius};
|
||||
}
|
||||
|
||||
footer > :global(button+button) {
|
||||
border-left: 1px solid ${theme.palette.border};
|
||||
}
|
||||
|
||||
div {
|
||||
height: 3.625rem;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default ModalActions
|
||||
37
components/modal/modal-content.tsx
Normal file
37
components/modal/modal-content.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type ModalContentProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLElement>
|
||||
|
||||
const ModalContent: React.FC<ModalContentProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`content ${className}`} {...props}>{children}</div>
|
||||
<style jsx>{`
|
||||
.content {
|
||||
margin: 0;
|
||||
padding: ${theme.layout.gap} 0 ${theme.layout.gapHalf} 0;
|
||||
}
|
||||
|
||||
.content :global(p) {
|
||||
margin: 0;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(ModalContent, defaultProps)
|
||||
15
components/modal/modal-context.ts
Normal file
15
components/modal/modal-context.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface ModalConfig {
|
||||
close: () => void
|
||||
open: () => void
|
||||
}
|
||||
|
||||
const defaultContext = {
|
||||
close: () => {},
|
||||
open: () => {},
|
||||
}
|
||||
|
||||
export const ModalContext = React.createContext<ModalConfig>(defaultContext)
|
||||
|
||||
export const useModalContext = (): ModalConfig => React.useContext<ModalConfig>(ModalContext)
|
||||
42
components/modal/modal-subtitle.tsx
Normal file
42
components/modal/modal-subtitle.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type ModalSubtitleProps = Props & typeof defaultProps & React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const ModalSubtitle: React.FC<ModalSubtitleProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className={className} {...props}>{children}</p>
|
||||
<style jsx>{`
|
||||
p {
|
||||
font-size: .875rem;
|
||||
line-height: 1.6;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
text-transform: uppercase;
|
||||
color: ${theme.palette.accents_5};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(ModalSubtitle, defaultProps)
|
||||
42
components/modal/modal-title.tsx
Normal file
42
components/modal/modal-title.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: ''
|
||||
}
|
||||
|
||||
export type ModalTitleProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const ModalTitle: React.FC<ModalTitleProps> = React.memo(({
|
||||
className, children, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className={className} {...props}>{children}</h2>
|
||||
<style jsx>{`
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.6;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
text-transform: uppercase;
|
||||
color: ${theme.palette.foreground};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(ModalTitle, defaultProps)
|
||||
72
components/modal/modal-wrapper.tsx
Normal file
72
components/modal/modal-wrapper.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import CSSTransition from '../shared/css-transition'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
width?: string
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
width: '26rem',
|
||||
visible: false,
|
||||
}
|
||||
|
||||
export type ModalWrapperProps = Props & typeof defaultProps
|
||||
|
||||
const ModalWrapper: React.FC<React.PropsWithChildren<ModalWrapperProps>> = React.memo(({
|
||||
className, width, children, visible, ...props
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<CSSTransition name="wrapper" visible={visible} clearTime={300}>
|
||||
<div className={`wrapper ${className}`} {...props}>
|
||||
{children}
|
||||
<style jsx>{`
|
||||
.wrapper {
|
||||
max-width: 90%;
|
||||
width: ${width};
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-color: ${theme.palette.background};
|
||||
color: ${theme.palette.foreground};
|
||||
border-radius: ${theme.layout.radius};
|
||||
padding: ${theme.layout.gap};
|
||||
opacity: 0;
|
||||
transform: translate3d(0px, -40px, 0px);
|
||||
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.35s cubic-bezier(0.4, 0, 0.2, 1) 0s;
|
||||
}
|
||||
|
||||
.wrapper-enter {
|
||||
opacity: 0;
|
||||
transform: translate3d(0px, -40px, 0px);
|
||||
}
|
||||
|
||||
.wrapper-enter-active {
|
||||
opacity: 1;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
|
||||
.wrapper-leave {
|
||||
opacity: 1;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
|
||||
.wrapper-leave-active {
|
||||
opacity: 0;
|
||||
transform: translate3d(0px, -50px, 0px);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(ModalWrapper, defaultProps)
|
||||
87
components/modal/modal.tsx
Normal file
87
components/modal/modal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } 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'
|
||||
|
||||
interface Props {
|
||||
disableBackdropClick?: boolean
|
||||
onClose: () => void
|
||||
onOpen: () => void
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
disableBackdropClick: false,
|
||||
onClose: () => {},
|
||||
onOpen: () => {},
|
||||
open: false,
|
||||
}
|
||||
|
||||
export type ModalProps = Props & typeof defaultProps & React.HTMLAttributes<any>
|
||||
|
||||
const Modal: React.FC<React.PropsWithChildren<ModalProps>> = React.memo(({
|
||||
children, disableBackdropClick, onClose, onOpen, open
|
||||
}) => {
|
||||
const portal = usePortal('modal')
|
||||
const [visible, setVisible] = useState<boolean>(open)
|
||||
const [withoutActionsChildren, ActionsChildren] = pickChild(children, ModalAction)
|
||||
const hasActions = ActionsChildren && React.Children.count(ActionsChildren) > 0
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setVisible(false)
|
||||
onClose && onClose()
|
||||
}, [open])
|
||||
const openModal = useCallback(() => {
|
||||
setVisible(true)
|
||||
onOpen && onOpen()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(open)
|
||||
}, [open])
|
||||
|
||||
const closeFromBackdrop = useCallback(() => {
|
||||
if (disableBackdropClick && hasActions) return
|
||||
closeModal()
|
||||
}, [disableBackdropClick])
|
||||
|
||||
const modalConfig: ModalConfig = useMemo(() => ({
|
||||
close: closeModal,
|
||||
open: openModal,
|
||||
}), [])
|
||||
|
||||
if (!portal) return null
|
||||
return createPortal(
|
||||
(
|
||||
<ModalContext.Provider value={modalConfig}>
|
||||
<Backdrop onClick={closeFromBackdrop} visible={visible} offsetY={25}>
|
||||
<ModalWrapper visible={visible}>
|
||||
{withoutActionsChildren}
|
||||
{hasActions && <ModalActions>{ActionsChildren}</ModalActions>}
|
||||
</ModalWrapper>
|
||||
</Backdrop>
|
||||
</ModalContext.Provider>
|
||||
), portal
|
||||
)
|
||||
})
|
||||
|
||||
Modal.defaultProps = defaultProps
|
||||
|
||||
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>
|
||||
|
||||
export default Modal as ModalComponent<ComponentProps>
|
||||
26
components/modal/use-modal.ts
Normal file
26
components/modal/use-modal.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
||||
import useCurrentState from 'components/utils/use-current-state'
|
||||
|
||||
const useModal = (initialVisible: boolean = false): {
|
||||
visible: boolean
|
||||
setVisible: Dispatch<SetStateAction<boolean>>
|
||||
currentRef: MutableRefObject<boolean>
|
||||
bindings: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
} => {
|
||||
const [visible, setVisible, currentRef] = useCurrentState<boolean>(initialVisible)
|
||||
|
||||
return {
|
||||
visible,
|
||||
setVisible,
|
||||
currentRef,
|
||||
bindings: {
|
||||
open: visible,
|
||||
onClose: () => setVisible(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useModal
|
||||
Reference in New Issue
Block a user