Skip to content

Commit

Permalink
feat(comp:modal): add spin support for modal (#1974)
Browse files Browse the repository at this point in the history
  • Loading branch information
sallerli1 authored Jul 30, 2024
1 parent 9bdfe41 commit 32ac59c
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 32 deletions.
8 changes: 6 additions & 2 deletions packages/components/_private/footer/src/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ export default defineComponent({
cancelButton && buttonProps.push(cancelButton)
}
children = buttonProps.map(item => {
const { text, ...rest } = item
const { text, disabled, ...rest } = item
const _text = isFunction(text) ? text() : text
return <IxButton {...rest}>{_text}</IxButton>
return (
<IxButton disabled={disabled || props.disabled} {...rest}>
{_text}
</IxButton>
)
})
}

Expand Down
4 changes: 4 additions & 0 deletions packages/components/_private/footer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const footerProps = {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: undefined,
},
footer: [Boolean, Array, Object, Function] as PropType<boolean | FooterButtonProps[] | VNode | (() => VNodeChild)>,
ok: Function as PropType<(evt?: Event | unknown) => Promise<void> | void>,
okButton: Object as PropType<ButtonProps>,
Expand Down
1 change: 1 addition & 0 deletions packages/components/config/src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export const defaultConfig: GlobalConfig = {
closeOnEsc: true,
mask: true,
maskClosable: false,
spinWithFullModal: false,
},
notification: {
destroyOnHover: false,
Expand Down
1 change: 1 addition & 0 deletions packages/components/config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ export interface ModalConfig {
icon?: Partial<Record<ModalType, string | VNode | (() => VNodeChild)>>
mask: boolean
maskClosable: boolean
spinWithFullModal: boolean
/**
* @deprecated
*/
Expand Down
14 changes: 14 additions & 0 deletions packages/components/modal/demo/Loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title:
zh: 加载中状态
en: Loading status
order: 3
---

## zh

加载中状态。

## en

Loading status.
43 changes: 43 additions & 0 deletions packages/components/modal/demo/Loading.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<IxSpace vertical>
<IxButton mode="primary" @click="showModal">Open Modal with async logic</IxButton>
<IxSpace>
<span>spinWithFullModal: </span>
<IxSwitch v-model:checked="spinWithFullModal"></IxSwitch>
</IxSpace>
</IxSpace>

<IxModal
v-model:visible="visible"
:spin="spin"
:spinWithFullModal="spinWithFullModal"
header="Loading status"
:closable="!spin"
:mask-closable="!spin"
title="This is title"
type="confirm"
@ok="onOk"
>
<p>Some contents...</p>
</IxModal>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const spin = ref(false)
const spinWithFullModal = ref(false)
const onOk = () =>
new Promise(resolve => {
spin.value = true
setTimeout(() => {
resolve(true)
spin.value = false
}, 3000)
})
const showModal = () => {
visible.value = true
}
</script>
15 changes: 15 additions & 0 deletions packages/components/modal/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
| `offset` | 对话框偏移量 | `number \| string` | `128` | - | 为顶部偏移量,仅在`centered=false` 时生效 |
| `okButton` | 确认按钮的属性 | `ButtonProps` | - | - | - |
| `okText` | 确认按钮的文本 | `string` | `确定` | - | - |
| `spin` | 弹窗是否是加载中 | `boolean \| SpinProps` | - | - | 当弹窗在加载中时,默认会禁用掉`footer`中的按钮 |
| `spinWithFullModal` | `spin` 是否覆盖整个弹窗 | `boolean` | `false` || - |
| `title` | 对话框次标题 | `string \| VNode \| (() => VNodeChild) \| #title` | - | - |`type` 不为 `default` 时有效 |
| `type` | 对话框类型 | `'default' \| 'confirm' \| 'info' \| 'success' \| 'warning' \| 'error'` | `default` | - | - |
| `width` | 对话框宽度 | `string \| number` | - | - | `default` 类型默认宽度 `480px`, 其他类型默认宽度 `400` |
Expand Down Expand Up @@ -55,6 +57,19 @@ export interface ModalButtonProps extends ButtonProps {
| `cancel` | 手动触发当前取消按钮 | `(evt?: Event \| unknown) => Promise<void>` | - | - | `evt` 参数将传给 `onCancel` 回调 |
| `ok` | 手动触发当前确定按钮 | `(evt?: Event \| unknown) => Promise<void>` | - | - | `evt` 参数将传给 `onOk` 回调 |

#### ModalSlots

| 名称 | 说明 | 参数类型 | 备注 |
| --- | --- | --- | --- |
| `default` | 弹窗内容 | - | - |
| `title` | 弹窗内容标题 | - | - |
| `icon` | 弹窗图标 | - | - |
| `header` | 弹窗的header | - | - |
| `footer` | 弹窗的footer | `FooterProps` | - |
| `closeIcon` | 弹窗header的关闭图标 | - | - |
| `spinContent` | spin的内容 | - | - |
| `spinIcon` | spin的图标 | - | - |

### IxModalProvider

如果你想通过 `useModal` 来创建对话框,则你需要把组件包裹在 `IxModalProvider` 内部,因为这样才不会丢失应用的上下文信息。
Expand Down
12 changes: 12 additions & 0 deletions packages/components/modal/src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { SpinProps } from '@idux/components/spin'

import {
type ComputedRef,
computed,
Expand All @@ -18,6 +20,8 @@ import {
watch,
} from 'vue'

import { isBoolean } from 'lodash-es'

import { CdkPortal } from '@idux/cdk/portal'
import { BlockScrollStrategy, type ScrollStrategy } from '@idux/cdk/scroll'
import { callEmit, isPromise, useControlledProp } from '@idux/cdk/utils'
Expand All @@ -44,6 +48,8 @@ export default defineComponent({
const config = useGlobalConfig('modal')
const mergedPrefixCls = computed(() => `${common.prefixCls}-modal`)
const mergedPortalTarget = usePortalTarget(props, config, common, mergedPrefixCls)
const mergedSpin = computed(() => convertSpinProps(props.spin))
const mergedSpinWithFullModal = computed(() => props.spinWithFullModal ?? config.spinWithFullModal)

const mask = computed(() => props.mask ?? config.mask)
const { visible, setVisible, animatedVisible, mergedVisible } = useVisible(props)
Expand All @@ -58,6 +64,8 @@ export default defineComponent({
locale,
config,
mergedPrefixCls,
mergedSpin,
mergedSpinWithFullModal,
visible,
animatedVisible,
mergedVisible,
Expand Down Expand Up @@ -177,3 +185,7 @@ function useTrigger(props: ModalProps, setVisible: (value: boolean) => void) {

return { cancelLoading, okLoading, open, close, cancel, ok }
}

function convertSpinProps(spin: boolean | SpinProps | undefined) {
return isBoolean(spin) ? { spinning: spin } : spin
}
51 changes: 36 additions & 15 deletions packages/components/modal/src/ModalBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { computed, defineComponent, inject } from 'vue'
import { isFunction, isString } from 'lodash-es'

import { IxIcon } from '@idux/components/icon'
import { IxSpin } from '@idux/components/spin'

import { modalToken } from './token'

Expand All @@ -27,7 +28,7 @@ const defaultIconTypes = {

export default defineComponent({
setup() {
const { props, slots, config, mergedPrefixCls } = inject(modalToken)!
const { props, slots, config, mergedPrefixCls, mergedSpin, mergedSpinWithFullModal } = inject(modalToken)!
const isDefault = computed(() => props.type === 'default')
const iconName = computed(() => {
const { icon, type } = props
Expand All @@ -38,22 +39,42 @@ export default defineComponent({
const prefixCls = `${mergedPrefixCls.value}-body`
const defaultNode = slots.default?.() ?? props.__content_node

let bodyNode: VNode

if (isDefault.value) {
return <div class={prefixCls}>{defaultNode}</div>
}
const classes = `${prefixCls} ${prefixCls}-${props.type}`
const iconNode = renderIcon(prefixCls, slots.icon, iconName.value)
const titleNode = renderTitle(prefixCls, slots.title, props.title)

return (
<div class={classes}>
{iconNode}
<div class={[`${prefixCls}-content`, defaultNode ? '' : `${prefixCls}-content-only-title`]}>
{titleNode}
{defaultNode}
bodyNode = <div class={prefixCls}>{defaultNode}</div>
} else {
const classes = `${prefixCls} ${prefixCls}-${props.type}`
const iconNode = renderIcon(prefixCls, slots.icon, iconName.value)
const titleNode = renderTitle(prefixCls, slots.title, props.title)

bodyNode = (
<div class={classes}>
{iconNode}
<div class={[`${prefixCls}-content`, defaultNode ? '' : `${prefixCls}-content-only-title`]}>
{titleNode}
{defaultNode}
</div>
</div>
</div>
)
)
}

const spinProps = mergedSpin.value

if (!mergedSpinWithFullModal.value && spinProps) {
const spinSlots = {
default: slots.spinContent,
icon: slots.spinIcon,
}

return (
<IxSpin v-slots={spinSlots} {...spinProps}>
{bodyNode}
</IxSpin>
)
}

return bodyNode
}
},
})
Expand Down
58 changes: 43 additions & 15 deletions packages/components/modal/src/ModalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { callEmit, convertCssPixel, getOffset } from '@idux/cdk/utils'
import { ɵFooter } from '@idux/components/_private/footer'
import { ɵHeader } from '@idux/components/_private/header'
import { type ModalConfig } from '@idux/components/config'
import { IxSpin } from '@idux/components/spin'
import { useThemeToken } from '@idux/components/theme'

import ModalBody from './ModalBody'
Expand All @@ -45,6 +46,8 @@ export default defineComponent({
visible,
animatedVisible,
mergedVisible,
mergedSpin,
mergedSpinWithFullModal,
cancelLoading,
okLoading,
currentZIndex,
Expand Down Expand Up @@ -130,7 +133,7 @@ export default defineComponent({

onMounted(() => watchVisibleChange(props, wrapperRef, sentinelStartRef, movableRef, mask))

return () => {
const renderContent = () => {
const prefixCls = mergedPrefixCls.value
const okButton = { size: 'md', ...props.okButton } as const
const cancelButton = { size: 'md', ...props.cancelButton } as const
Expand All @@ -139,7 +142,15 @@ export default defineComponent({
header: slots.header,
closeIcon: slots.closeIcon,
}
const contentNodes = [

const spinSlots = {
default: slots.spinContent,
icon: slots.spinIcon,
}

const spinProps = mergedSpin.value

const children = [
<ɵHeader
ref={headerRef}
class={`${prefixCls}-header`}
Expand All @@ -159,6 +170,7 @@ export default defineComponent({
cancelLoading={cancelLoading.value}
cancelText={cancelText.value}
cancelVisible={cancelVisible.value}
disabled={spinProps?.spinning}
footer={props.footer}
ok={handleOk}
okButton={okButton}
Expand All @@ -167,6 +179,34 @@ export default defineComponent({
></ɵFooter>,
]

const contentNode = props.draggable ? (
<CdkDndMovable
ref={movableRef}
class={`${prefixCls}-content`}
dragHandle={headerRef.value}
boundary={wrapperRef.value}
mode="immediate"
>
{children}
</CdkDndMovable>
) : (
<div class={`${prefixCls}-content`}>{children}</div>
)

if (mergedSpinWithFullModal.value && spinProps) {
return (
<IxSpin v-slots={spinSlots} {...spinProps}>
{contentNode}
</IxSpin>
)
}

return contentNode
}

return () => {
const prefixCls = mergedPrefixCls.value

return (
<div
v-show={mergedVisible.value}
Expand Down Expand Up @@ -195,19 +235,7 @@ export default defineComponent({
{...attrs}
>
<div ref={sentinelStartRef} tabindex={0} class={`${prefixCls}-sentinel`} aria-hidden={true}></div>
{props.draggable ? (
<CdkDndMovable
ref={movableRef}
class={`${prefixCls}-content`}
dragHandle={headerRef.value}
boundary={wrapperRef.value}
mode="immediate"
>
{contentNodes}
</CdkDndMovable>
) : (
<div class={`${prefixCls}-content`}>{contentNodes}</div>
)}
{renderContent()}
<div ref={sentinelEndRef} tabindex={0} class={`${prefixCls}-sentinel`} aria-hidden={true}></div>
</div>
</Transition>
Expand Down
3 changes: 3 additions & 0 deletions packages/components/modal/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { ModalBindings, ModalProps, ModalProviderRef } from './types'
import type { CommonConfig, ModalConfig } from '@idux/components/config'
import type { Locale } from '@idux/components/locales'
import type { SpinProps } from '@idux/components/spin'
import type { ComputedRef, InjectionKey, Ref, Slots } from 'vue'

export interface ModalContext {
Expand All @@ -20,6 +21,8 @@ export interface ModalContext {
visible: ComputedRef<boolean>
animatedVisible: Ref<boolean | undefined>
mergedVisible: ComputedRef<boolean>
mergedSpin: ComputedRef<SpinProps | undefined>
mergedSpinWithFullModal: ComputedRef<boolean>
cancelLoading: Ref<boolean>
okLoading: Ref<boolean>
currentZIndex: ComputedRef<number>
Expand Down
3 changes: 3 additions & 0 deletions packages/components/modal/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey }
import type { ɵFooterButtonProps } from '@idux/components/_private/footer'
import type { ButtonProps } from '@idux/components/button'
import type { HeaderProps } from '@idux/components/header'
import type { SpinProps } from '@idux/components/spin'
import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild, VNodeProps } from 'vue'

export type ModalType = 'default' | 'confirm' | 'info' | 'success' | 'warning' | 'error'
Expand Down Expand Up @@ -85,6 +86,8 @@ export const modalProps = {
okButton: Object as PropType<ButtonProps>,
okText: String,
scrollStrategy: Object as PropType<ScrollStrategy>,
spin: { type: [Boolean, Object] as PropType<boolean | SpinProps>, default: undefined },
spinWithFullModal: { type: Boolean, default: undefined },
title: [String, Object, Function] as PropType<string | VNode | (() => VNodeChild)>,
type: {
type: String as PropType<ModalType>,
Expand Down

0 comments on commit 32ac59c

Please sign in to comment.