Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/support use modal hooks #6739

Closed
1 change: 1 addition & 0 deletions src/components/modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type { ModalShowProps, ModalShowHandler } from './show'
export type { ModalAlertProps } from './alert'
export type { ModalConfirmProps } from './confirm'

export { useModal } from './use-modal'
export default attachPropertiesToComponent(Modal, {
show,
alert,
Expand Down
115 changes: 115 additions & 0 deletions src/components/modal/use-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useMemoizedFn } from 'ahooks'
import React from 'react'
import { mergeProps } from '../../utils/with-default-props'
import { getDefaultConfig } from '../config-provider'
import { usePortal } from '../portal-provider'
import { ModalAlertProps } from './alert'
import { ModalConfirmProps } from './confirm'
import { Modal } from './modal'
import { ModalShowProps } from './show'

export const closeFnSet = new Set<() => void>()

export const useModal = () => {
const { renderModalInPortal } = usePortal()

Check warning on line 14 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L14

Added line #L14 was not covered by tests

/**
* @description refer to Modal.show
* @see https://github.com/ant-design/ant-design-mobile/blob/master/src/components/modal/show.tsx
*/
const show = useMemoizedFn((props: ModalShowProps) => {
const handler = renderModalInPortal(

Check warning on line 21 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L20-L21

Added lines #L20 - L21 were not covered by tests
<Modal
{...props}
afterClose={() => {
closeFnSet.delete(handler.close)

Check warning on line 25 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L24-L25

Added lines #L24 - L25 were not covered by tests
props.afterClose?.()
}}
/>
)
closeFnSet.add(handler.close)
return handler

Check warning on line 31 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L30-L31

Added lines #L30 - L31 were not covered by tests
})

/**
* @description refer to Modal.confirm
* @see https://github.com/ant-design/ant-design-mobile/blob/master/src/components/modal/confirm.tsx
*/
const confirm = useMemoizedFn((p: ModalConfirmProps) => {
const defaultProps = {

Check warning on line 39 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L38-L39

Added lines #L38 - L39 were not covered by tests
confirmText: getDefaultConfig().locale.Modal.ok,
cancelText: getDefaultConfig().locale.common.cancel,
}
const props = mergeProps(defaultProps, p)

Check warning on line 43 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L43

Added line #L43 was not covered by tests

return new Promise<boolean>(resolve => {
show({

Check warning on line 46 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L45-L46

Added lines #L45 - L46 were not covered by tests
...props,
closeOnAction: true,
onClose: () => {

Check warning on line 49 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L49

Added line #L49 was not covered by tests
props.onClose?.()
resolve(false)

Check warning on line 51 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L51

Added line #L51 was not covered by tests
},
actions: [
{
key: 'confirm',
text: props.confirmText,
primary: true,
onClick: async () => {

Check warning on line 58 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L58

Added line #L58 was not covered by tests
await props.onConfirm?.()
resolve(true)

Check warning on line 60 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L60

Added line #L60 was not covered by tests
},
},
{
key: 'cancel',
text: props.cancelText,
onClick: async () => {

Check warning on line 66 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L66

Added line #L66 was not covered by tests
await props.onCancel?.()
resolve(false)

Check warning on line 68 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L68

Added line #L68 was not covered by tests
},
},
],
})
})
})

/**
* @description refer to Modal.alert
* @see https://github.com/ant-design/ant-design-mobile/blob/master/src/components/modal/alert.tsx
*/
const alert = useMemoizedFn((p: ModalAlertProps) => {
const defaultProps = {

Check warning on line 81 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L80-L81

Added lines #L80 - L81 were not covered by tests
confirmText: getDefaultConfig().locale.Modal.ok,
}
const props = mergeProps(defaultProps, p)
return new Promise<void>(resolve => {
show({

Check warning on line 86 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L84-L86

Added lines #L84 - L86 were not covered by tests
...props,
closeOnAction: true,
actions: [
{
key: 'confirm',
text: props.confirmText,
primary: true,
},
],
onAction: props.onConfirm,
onClose: () => {

Check warning on line 97 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L97

Added line #L97 was not covered by tests
props.onClose?.()
resolve()

Check warning on line 99 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L99

Added line #L99 was not covered by tests
},
})
})
})

const clear = useMemoizedFn(() => {
closeFnSet.forEach(close => close())

Check warning on line 106 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L105-L106

Added lines #L105 - L106 were not covered by tests
})

return {

Check warning on line 109 in src/components/modal/use-modal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/modal/use-modal.tsx#L109

Added line #L109 was not covered by tests
show,
clear,
confirm,
alert,
}
}
92 changes: 92 additions & 0 deletions src/components/portal-provider/demos/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
Button,
ConfigProvider,
PortalProvider,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure PortalProvider is a good concept. Maybe consider the antd PC style of App component:

https://ant.design/components/app

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In early version, antd PC provide useModal with holder & caller. But most user dont need that. So get App component for unique useHook register (like the PortalProvider but wish App can do more instead of portal inject only)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, does it mean that if I refer to app. the style will make more sense with ant design, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye. We can currently only provide App a dom without style but provide unique useXX ability. It maybe more easy to release.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it

Space,
useModal,
} from 'antd-mobile'
import enUS from 'antd-mobile/es/locales/en-US'
import { DemoBlock } from 'demos'
import React from 'react'

export default () => {
return (
<>
<DemoBlock title='中文'>
<ConfigProvider locale={enUS}>
{/* should be wrapped with `<PortalProvider />` */}
<PortalProvider>
<ComponentWantsToUseModal />
</PortalProvider>
</ConfigProvider>
</DemoBlock>
</>
)
}

const ComponentWantsToUseModal = () => {
const { show, confirm, alert, clear } = useModal()
return (
<Space direction='vertical'>
<Button
block
shape='rounded'
color='primary'
size='large'
onClick={() => {
show({
title: 'useModal show',
content: '🚀 LFG!',
closeOnAction: true,
showCloseButton: true,
onClose: () => console.log('❎ onClose'),
actions: [
{
key: 'confirm',
text: 'I get it',
},
],
})
}}
>
useModal show
</Button>
<Button
block
shape='rounded'
color='primary'
size='large'
onClick={() => {
confirm({
title: 'useModal confirm',
content: '🚀 LFG!',
showCloseButton: true,
onConfirm: () => console.log('🛫 confirm'),
onCancel: () => console.log('🫸 cancel'),
onClose: () => console.log('❎ onClose'),
})
}}
>
useModal confirm
</Button>
<Button
block
shape='rounded'
color='primary'
size='large'
onClick={() => {
alert({
title: 'useModal alert',
content: '🚀 LFG!',
showCloseButton: true,
confirmText: `Clear Modals`,
onConfirm: () => clear(),
onClose: () => console.log('❎ onClose'),
})
}}
>
useModal alert
</Button>
</Space>
)
}
19 changes: 19 additions & 0 deletions src/components/portal-provider/index.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# PortalProvider

Use `Modal` or `Popup` component imperatively with `hooks` style globally.

## When to use

- If you want to use `useModal` from `Modal` or `usePopup` `Popup` (`WIP 🚧 `) components

## Demos

<code src="./demos/demo1.tsx" ></code>

## PortalProvider

### Props

| Name | Description | Type | Default |
| -------- | ------------------------------------- | ----------- | ------- |
| children | Component who wants to use `useModal` | `ReactNode` | - |
7 changes: 7 additions & 0 deletions src/components/portal-provider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PortalProvider } from './portal-provider'

export type { PortalProviderProps } from './portal-provider'

export { usePortal } from './portal-provider'

export default PortalProvider
19 changes: 19 additions & 0 deletions src/components/portal-provider/index.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# PortalProvider 配置

让你能够像风一般自由地以 `hooks` 形式指令式调用 `Modal` 或者 `Popup` 等组件。

## 何时使用

- 假设您想试用 `useModal` 或者 `usePopup` (`🚧 假设本PR通过时`) 时需要 `PortalProvider`

## 示例

<code src="./demos/demo1.tsx" ></code>

## PortalProvider

### 属性

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | --- |
| children | 需要用到 `useModal` 的子组件 (一般是应用入口) | `Locale` | [zh-CN] | - |
127 changes: 127 additions & 0 deletions src/components/portal-provider/portal-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useMemoizedFn } from 'ahooks'
import React, {
createContext,
createRef,
PropsWithChildren,
useContext,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react'
import type { ImperativeHandler } from '../../utils/render-imperatively'

const PortalContext = createContext<PortalContextType | undefined>(undefined)

type ImperativeProps = {
visible?: boolean
onClose?: () => void
afterClose?: () => void
}

type TargetElement = React.ReactElement<ImperativeProps>

interface PortalContextType {
renderModalInPortal: (element: TargetElement) => ImperativeHandler
}

interface WrapperProps {
element: TargetElement
unmount: () => void
}

const Wrapper = React.forwardRef<ImperativeHandler, WrapperProps>(
({ element, unmount }, ref) => {
const [visible, setVisible] = useState(false)
const closedRef = useRef(false)
const [elementToRender, setElementToRender] = useState(element)
const keyRef = useRef(0)
useEffect(() => {

Check warning on line 39 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L34-L39

Added lines #L34 - L39 were not covered by tests
if (!closedRef.current) {
setVisible(true)
} else {
afterClose()

Check warning on line 43 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L41-L43

Added lines #L41 - L43 were not covered by tests
}
}, [])
function onClose() {
closedRef.current = true
setVisible(false)

Check warning on line 48 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L46-L48

Added lines #L46 - L48 were not covered by tests
elementToRender.props.onClose?.()
}
function afterClose() {
unmount()

Check warning on line 52 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L51-L52

Added lines #L51 - L52 were not covered by tests
elementToRender.props.afterClose?.()
}
useImperativeHandle(ref, () => ({

Check warning on line 55 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L55

Added line #L55 was not covered by tests
close: onClose,
replace: element => {
keyRef.current++

Check warning on line 58 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L57-L58

Added lines #L57 - L58 were not covered by tests
elementToRender.props.afterClose?.()
setElementToRender(element)

Check warning on line 60 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L60

Added line #L60 was not covered by tests
},
}))
return React.cloneElement(elementToRender, {

Check warning on line 63 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L63

Added line #L63 was not covered by tests
...elementToRender.props,
key: keyRef.current,
visible,
onClose,
afterClose,
})
}
)

export type PortalProviderProps = PropsWithChildren & {}

let wrapperId = 0

export const PortalProvider: React.FC<PortalProviderProps> = ({ children }) => {
const [portalElements, setPortalElements] = useState<TargetElement[]>([])
const wrapperRef = createRef<ImperativeHandler>()

Check warning on line 79 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L78-L79

Added lines #L78 - L79 were not covered by tests

const renderModalInPortal = useMemoizedFn((element: TargetElement) => {
const unmount = () => {
setPortalElements(elements =>
elements.filter(el => el.key !== wrappedElement.key)

Check warning on line 84 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L81-L84

Added lines #L81 - L84 were not covered by tests
)
}
const wrappedElement = (
<Wrapper

Check warning on line 88 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L88

Added line #L88 was not covered by tests
key={wrapperId++}
ref={wrapperRef}
element={element}
unmount={unmount}
/>
)
setPortalElements(elements => [...elements, wrappedElement])
return {
close: async () => {

Check warning on line 97 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L95-L97

Added lines #L95 - L97 were not covered by tests
if (!wrapperRef.current) {
// it means the wrapper is not mounted yet, call `unmount` directly
unmount()

Check warning on line 100 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L100

Added line #L100 was not covered by tests
// call `afterClose` to make sure the callback is called
element.props.afterClose?.()
} else {

Check warning on line 103 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L103

Added line #L103 was not covered by tests
wrapperRef.current?.close()
}
},
replace: element => {

Check warning on line 107 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L107

Added line #L107 was not covered by tests
wrapperRef.current?.replace(element)
},
isRendered: () => !!wrapperRef.current,

Check warning on line 110 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L110

Added line #L110 was not covered by tests
} as ImperativeHandler
})
return (

Check warning on line 113 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L113

Added line #L113 was not covered by tests
<PortalContext.Provider value={{ renderModalInPortal }}>
{children}
{portalElements}
</PortalContext.Provider>
)
}

export const usePortal = () => {
const context = useContext(PortalContext)

Check warning on line 122 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L122

Added line #L122 was not covered by tests
if (!context) {
throw new Error('usePortal must be used within a PortalProvider')

Check warning on line 124 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L124

Added line #L124 was not covered by tests
}
return context

Check warning on line 126 in src/components/portal-provider/portal-provider.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/portal-provider/portal-provider.tsx#L126

Added line #L126 was not covered by tests
}
5 changes: 5 additions & 0 deletions src/components/portal-provider/tests/portal-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('PortalProvider', () => {
test('WIP 🚧', () => {
tylerrrkd marked this conversation as resolved.
Show resolved Hide resolved
expect(true)
})
})
Loading
Loading