From b2b10059db0bb1d1b7b25559378de7d62707a3cc Mon Sep 17 00:00:00 2001
From: liuzaijiang <530604689@qq.com>
Date: Fri, 14 Oct 2022 10:31:51 +0800
Subject: [PATCH] feat(comp:spin): add IxSpinProvider
---
packages/components/index.ts | 3 +-
.../__snapshots__/spinProvider.spec.ts.snap | 3 +
.../spin/__tests__/spinProvider.spec.ts | 101 ++++++++++
packages/components/spin/demo/Basic.vue | 2 +-
packages/components/spin/demo/UseSpin.md | 19 ++
packages/components/spin/demo/UseSpin.vue | 65 +++++++
packages/components/spin/docs/Api.zh.md | 29 +++
packages/components/spin/index.ts | 23 ++-
packages/components/spin/src/Spin.tsx | 6 +-
packages/components/spin/src/SpinProvider.tsx | 177 ++++++++++++++++++
packages/components/spin/src/token.ts | 11 ++
packages/components/spin/src/types.ts | 40 ++++
packages/components/spin/src/useSpin.ts | 22 +++
packages/components/spin/style/index.less | 31 ++-
packages/site/src/App.vue | 114 +++++------
15 files changed, 579 insertions(+), 67 deletions(-)
create mode 100644 packages/components/spin/__tests__/__snapshots__/spinProvider.spec.ts.snap
create mode 100644 packages/components/spin/__tests__/spinProvider.spec.ts
create mode 100644 packages/components/spin/demo/UseSpin.md
create mode 100644 packages/components/spin/demo/UseSpin.vue
create mode 100644 packages/components/spin/src/SpinProvider.tsx
create mode 100644 packages/components/spin/src/token.ts
create mode 100644 packages/components/spin/src/useSpin.ts
diff --git a/packages/components/index.ts b/packages/components/index.ts
index ebb54ff1f..825a3872e 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -51,7 +51,7 @@ import { IxSelect, IxSelectOption, IxSelectOptionGroup, IxSelectPanel } from '@i
import { IxSkeleton } from '@idux/components/skeleton'
import { IxSlider } from '@idux/components/slider'
import { IxSpace } from '@idux/components/space'
-import { IxSpin } from '@idux/components/spin'
+import { IxSpin, IxSpinProvider } from '@idux/components/spin'
import { IxStatistic } from '@idux/components/statistic'
import { IxStepper, IxStepperItem } from '@idux/components/stepper'
import { IxSwitch } from '@idux/components/switch'
@@ -146,6 +146,7 @@ const components = [
IxSlider,
IxSpace,
IxSpin,
+ IxSpinProvider,
IxStatistic,
IxStepper,
IxStepperItem,
diff --git a/packages/components/spin/__tests__/__snapshots__/spinProvider.spec.ts.snap b/packages/components/spin/__tests__/__snapshots__/spinProvider.spec.ts.snap
new file mode 100644
index 000000000..c79d824c0
--- /dev/null
+++ b/packages/components/spin/__tests__/__snapshots__/spinProvider.spec.ts.snap
@@ -0,0 +1,3 @@
+// Vitest Snapshot v1
+
+exports[`SpinProvider > basic > render work 1`] = `"
content
"`;
diff --git a/packages/components/spin/__tests__/spinProvider.spec.ts b/packages/components/spin/__tests__/spinProvider.spec.ts
new file mode 100644
index 000000000..cee05b5cb
--- /dev/null
+++ b/packages/components/spin/__tests__/spinProvider.spec.ts
@@ -0,0 +1,101 @@
+import { MountingOptions, VueWrapper, flushPromises, mount } from '@vue/test-utils'
+
+import { renderWork } from '@tests'
+
+import SpinProvider from '../src/SpinProvider'
+import { SpinProviderInstance, SpinProviderProps } from '../src/types'
+
+describe('SpinProvider', () => {
+ const SpinProviderMount = (options?: MountingOptions) => {
+ return mount(SpinProvider, { ...options }) as VueWrapper
+ }
+
+ const tip = 'This is a tip'
+
+ const newTip = 'This is a newTip'
+
+ describe('basic', () => {
+ renderWork(SpinProvider, { slots: { default: 'content' } })
+
+ test('update and destroy and destroyAll work', async () => {
+ const wrapper = SpinProviderMount()
+
+ document.body.innerHTML = 'This is a content
'
+
+ wrapper.vm.open({ tip, target: '.target' })
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(1)
+
+ expect(document.querySelector('.target')?.classList.contains('ix-spin-target-container')).toBe(true)
+
+ expect(document.querySelector('.ix-spin-spinner-tip')!.textContent).toBe(tip)
+
+ wrapper.vm.update({
+ tip: newTip,
+ target: '.target',
+ })
+
+ await flushPromises()
+
+ expect(document.querySelector('.ix-spin-spinner-tip')!.textContent).toBe(newTip)
+
+ wrapper.vm.open({ tip })
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(2)
+
+ wrapper.vm.destroy()
+
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(1)
+
+ // // will not generate spins repeatedly
+ wrapper.vm.open({ tip, target: '.target' })
+
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(1)
+
+ wrapper.vm.destroyAll()
+
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(0)
+ })
+ })
+
+ describe('spinRef', () => {
+ test.skip('update and destroy work', async () => {
+ const wrapper = SpinProviderMount()
+ const spinProviderRef = wrapper.vm.open({ tip })
+ await flushPromises()
+
+ expect(document.body.classList.contains('ix-spin-target-container')).toBe(true)
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(1)
+
+ expect(document.querySelectorAll('.ix-spin')[0].style.zIndex).toBe('2000')
+
+ expect(document.querySelector('.ix-spin-spinner-tip')!.textContent).toBe(tip)
+
+ spinProviderRef.update({
+ tip: 'new text',
+ zIndex: 3000,
+ })
+
+ await flushPromises()
+
+ expect(document.querySelector('.ix-spin-spinner-tip')!.textContent).toBe('new text')
+
+ expect(document.querySelectorAll('.ix-spin')[0].style.zIndex).toBe('3000')
+
+ spinProviderRef.destroy()
+
+ await flushPromises()
+
+ expect(document.querySelectorAll('.ix-spin').length).toBe(0)
+ })
+ })
+})
diff --git a/packages/components/spin/demo/Basic.vue b/packages/components/spin/demo/Basic.vue
index 034a3d157..033db32ac 100644
--- a/packages/components/spin/demo/Basic.vue
+++ b/packages/components/spin/demo/Basic.vue
@@ -1,7 +1,7 @@
- content
+ 1212
切换状态
diff --git a/packages/components/spin/demo/UseSpin.md b/packages/components/spin/demo/UseSpin.md
new file mode 100644
index 000000000..c3f502bfe
--- /dev/null
+++ b/packages/components/spin/demo/UseSpin.md
@@ -0,0 +1,19 @@
+---
+order: 4
+title:
+ zh: UseSpin
+ en: UseSpin
+hidden: true
+---
+
+## zh
+
+你可以通过`useSpin`来灵活的使用`spin`组件,前提是需要把子组件包裹在`IxSpinProvider`里面
+
+``` html
+
+
+
+```
+
+## en
diff --git a/packages/components/spin/demo/UseSpin.vue b/packages/components/spin/demo/UseSpin.vue
new file mode 100644
index 000000000..29c5f4715
--- /dev/null
+++ b/packages/components/spin/demo/UseSpin.vue
@@ -0,0 +1,65 @@
+
+
+
+
+ Open
+ Update
+ Destroy
+ fullScreen
+ DestroyAll
+
+
+
+
+
+
diff --git a/packages/components/spin/docs/Api.zh.md b/packages/components/spin/docs/Api.zh.md
index f4843a597..6a1863a02 100644
--- a/packages/components/spin/docs/Api.zh.md
+++ b/packages/components/spin/docs/Api.zh.md
@@ -21,3 +21,32 @@
|名称 | 说明 | 参数类型 | 备注 |
| --- | --- | --- | --- |
|`default` | 需要遮罩的内容区域 | - | - |
+
+### IxSpinProvider
+
+#### IxSpinProviderMethods
+
+| 名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+| `open` | 打开 | `(options: SpinOptions) => SpinRef` | `target`不传,默认为`target`为`body` |
+| `update` | 更新 | `(options: SpinOptions) => void` | `target`不传,默认为`target`为`body` |
+| `destroy` | 销毁 | `(target?: TargetType \| TargetType[]) => void` | `target`不传,默认为`target`为`body` |
+| `destroyAll` | 销毁全部 | `() => void` | - |
+
+``` ts
+export type SpinOptions = Partial<
+ Omit
& {
+ tip: string
+ target: string | HTMLElement | (() => string | HTMLElement)
+ zIndex: number
+ }
+>
+
+export type SpinRefUpdateOptions = Omit
+
+export interface SpinRef {
+ update: (options: SpinRefUpdateOptions) => void
+ destroy: () => void
+}
+
+```
diff --git a/packages/components/spin/index.ts b/packages/components/spin/index.ts
index 6f73285ee..6a2b40e61 100644
--- a/packages/components/spin/index.ts
+++ b/packages/components/spin/index.ts
@@ -5,12 +5,29 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { SpinComponent } from './src/types'
+import type { SpinComponent, SpinProviderComponent } from './src/types'
import Spin from './src/Spin'
+import SpinProvider from './src/SpinProvider'
const IxSpin = Spin as unknown as SpinComponent
+const IxSpinProvider = SpinProvider as unknown as SpinProviderComponent
-export { IxSpin }
+export { IxSpin, IxSpinProvider }
-export type { SpinInstance, SpinComponent, SpinPublicProps as SpinProps, SpinTipAlignType, SpinSize } from './src/types'
+export { useSpin } from './src/useSpin'
+
+export type {
+ SpinInstance,
+ SpinComponent,
+ SpinPublicProps as SpinProps,
+ SpinProviderInstance,
+ SpinProviderComponent,
+ SpinProviderPublicProps as SpinProviderProps,
+ SpinProviderRef,
+ SpinRef,
+ SpinOptions,
+ SpinRefUpdateOptions,
+ SpinTipAlignType,
+ SpinSize,
+} from './src/types'
diff --git a/packages/components/spin/src/Spin.tsx b/packages/components/spin/src/Spin.tsx
index 1a32549d5..957417a59 100644
--- a/packages/components/spin/src/Spin.tsx
+++ b/packages/components/spin/src/Spin.tsx
@@ -40,7 +40,7 @@ export default defineComponent({
const { size, strokeWidth, radius } = useSize(props, spinConfig)
const hasDefaultSlot = computed(() => hasSlot(slots))
- const mregedIcon = computed(() => props.icon ?? spinConfig.icon)
+ const mergedIcon = computed(() => props.icon ?? spinConfig.icon)
const mergedTip = computed(() => props.tip ?? spinConfig.tip)
const { spinnerClassName, containerClassName } = useClasses(
@@ -73,11 +73,11 @@ export default defineComponent({
return {slots.icon()}
}
- if (mregedIcon.value) {
+ if (mergedIcon.value) {
const iconStyle = normalizeStyle(props.duration && { animationDuration: `${props.duration}s` })
return (
-
+
)
}
diff --git a/packages/components/spin/src/SpinProvider.tsx b/packages/components/spin/src/SpinProvider.tsx
new file mode 100644
index 000000000..a9353ad23
--- /dev/null
+++ b/packages/components/spin/src/SpinProvider.tsx
@@ -0,0 +1,177 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { ComputedRef, VNode, computed, defineComponent, normalizeStyle, provide, ref } from 'vue'
+
+import { CdkPortal, type PortalTargetType } from '@idux/cdk/portal'
+import { addClass, convertArray, convertCssPixel, removeClass } from '@idux/cdk/utils'
+import { useGlobalConfig } from '@idux/components/config'
+import { convertTarget } from '@idux/components/utils'
+
+import IxSpin from './Spin'
+import { spinProviderToken } from './token'
+import { type SpinMergedOptions, type SpinOptions, SpinRef, SpinRefUpdateOptions, spinProviderProps } from './types'
+
+const BASE_ZINDEX = 2000
+
+export default defineComponent({
+ name: 'IxSpinProvider',
+ inheritAttrs: false,
+ props: spinProviderProps,
+ setup(_, { expose, slots }) {
+ const common = useGlobalConfig('common')
+ const mergedPrefixCls = computed(() => `${common.prefixCls}-spin`)
+
+ const { spins, open, update, destroy, destroyAll } = useSpin(mergedPrefixCls)
+
+ const apis = { open, update, destroy, destroyAll }
+
+ provide(spinProviderToken, apis)
+ expose(apis)
+
+ return () => {
+ const children: VNode[] = []
+
+ spins.value.forEach((spinOptions, index) => {
+ const { spinning, target, width, height, isFullScreen, hasScroll, tip, zIndex } = spinOptions
+ const BaseZindex = zIndex ?? BASE_ZINDEX + index
+ const style = normalizeStyle({
+ width: !isFullScreen && hasScroll && convertCssPixel(width),
+ height: !isFullScreen && hasScroll && convertCssPixel(height),
+ position: isFullScreen ? 'fixed' : '',
+ zIndex: BaseZindex,
+ })
+ spinning &&
+ children.push(
+
+
+ ,
+ )
+ })
+
+ return (
+
+ {children}
+ {slots.default?.()}
+
+ )
+ }
+ },
+})
+
+function useSpin(mergedPrefixCls: ComputedRef) {
+ const spins = ref([])
+
+ const _convertTarget = (target: PortalTargetType) => {
+ const targetElement = convertTarget(target)
+ return targetElement === window ? null : (targetElement as HTMLElement)
+ }
+
+ const elementHasScroll = (element: HTMLElement) => {
+ return element.clientHeight < element.scrollHeight || element.clientWidth < element.scrollWidth
+ }
+
+ const deleteClass = (targetElement: HTMLElement) => {
+ removeClass(targetElement, [
+ `${mergedPrefixCls.value}-target-container`,
+ `${mergedPrefixCls.value}-target-container-relative`,
+ `${mergedPrefixCls.value}-target-container-has-scroll`,
+ ])
+ }
+
+ const getCurrIndex = (target: PortalTargetType) => {
+ return spins.value.findIndex(spin => spin.target === target)
+ }
+
+ const add = (item: SpinMergedOptions) => {
+ const target = item.target
+ const currIndex = getCurrIndex(target)
+ if (currIndex !== -1) {
+ spins.value.splice(currIndex, 1, item)
+ }
+
+ spins.value.push(item)
+ }
+
+ const update = (options: SpinOptions) => {
+ const { target = 'body' } = options
+ const currIndex = getCurrIndex(target)
+ if (currIndex !== -1) {
+ const newItem = { ...spins.value[currIndex], ...options }
+ spins.value.splice(currIndex, 1, newItem)
+ }
+ }
+
+ const destroy = (target: PortalTargetType | PortalTargetType[] = 'body') => {
+ const targets = convertArray(target)
+ targets.forEach(target => {
+ const currIndex = getCurrIndex(target)
+ if (currIndex !== -1) {
+ const item = spins.value.splice(currIndex, 1)
+ const targetElement = _convertTarget(item[0].target)
+ targetElement && deleteClass(targetElement)
+ }
+ })
+ }
+
+ const destroyAll = () => {
+ destroy(spins.value.map(item => item.target))
+ spins.value = []
+ }
+
+ const open = (options: SpinOptions): SpinRef => {
+ const { target = 'body', ...reset } = options
+
+ if (getCurrIndex(target) === -1) {
+ const targetElement = convertTarget(target) as HTMLElement
+ const hasScroll = elementHasScroll(targetElement)
+ const isFullScreen = targetElement === document.body
+ const staticPosition = getComputedStyle(targetElement).position === 'static'
+
+ addClass(
+ targetElement,
+ [
+ `${mergedPrefixCls.value}-target-container`,
+ staticPosition ? `${mergedPrefixCls.value}-target-container-relative` : '',
+ !isFullScreen && hasScroll ? `${mergedPrefixCls.value}-target-container-has-scroll` : '',
+ ].filter(Boolean),
+ )
+
+ add({
+ spinning: true,
+ target,
+ hasScroll,
+ isFullScreen: targetElement === document.body,
+ staticPosition,
+ // 当存在滚动条且需要遮住全部除了全屏外(fixed可以解决)
+ // 其余几乎无此真实场景,所以不考虑resize时动态赋值width和height,目前也没办法解决
+ width: targetElement.scrollWidth,
+ height: targetElement.scrollHeight,
+ ...reset,
+ })
+ }
+
+ const ref = {
+ target,
+ update: (options: SpinRefUpdateOptions) =>
+ update({
+ target,
+ ...options,
+ }),
+ destroy: () => destroy(target),
+ }
+ return ref
+ }
+
+ return {
+ spins,
+ open,
+ update,
+ destroy,
+ destroyAll,
+ }
+}
diff --git a/packages/components/spin/src/token.ts b/packages/components/spin/src/token.ts
new file mode 100644
index 000000000..0a0eca3f0
--- /dev/null
+++ b/packages/components/spin/src/token.ts
@@ -0,0 +1,11 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import type { SpinProviderRef } from './types'
+import type { InjectionKey } from 'vue'
+
+export const spinProviderToken: InjectionKey = Symbol('spinProviderToken')
diff --git a/packages/components/spin/src/types.ts b/packages/components/spin/src/types.ts
index 46fbab987..eb4c21bc5 100644
--- a/packages/components/spin/src/types.ts
+++ b/packages/components/spin/src/types.ts
@@ -5,6 +5,7 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
+import type { PortalTargetType } from '@idux/cdk/portal'
import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils'
import type { DefineComponent, HTMLAttributes, PropType } from 'vue'
@@ -29,7 +30,46 @@ export const spinProps = {
size: String as PropType<'lg' | 'md' | 'sm'>,
} as const
+export const spinProviderProps = {}
+
+export type SpinOptions = Partial<
+ Omit & {
+ tip: string
+ target: PortalTargetType
+ zIndex: number
+ }
+>
+
+export type SpinRefUpdateOptions = Omit
+
+export interface SpinMergedOptions extends SpinOptions {
+ target: PortalTargetType
+ spinning: boolean
+ width: number
+ height: number
+ hasScroll: boolean
+ isFullScreen: boolean
+ staticPosition: boolean
+}
+
+export interface SpinRef {
+ update: (options: SpinOptions) => void
+ destroy: () => void
+}
+
+export interface SpinProviderRef {
+ open: (options: SpinOptions) => SpinRef
+ update: (options: SpinOptions) => void
+ destroy: (target?: PortalTargetType | PortalTargetType[]) => void
+ destroyAll: () => void
+}
+
export type SpinProps = ExtractInnerPropTypes
export type SpinPublicProps = ExtractPublicPropTypes
export type SpinComponent = DefineComponent & SpinPublicProps>
export type SpinInstance = InstanceType>
+
+export type SpinProviderProps = ExtractInnerPropTypes
+export type SpinProviderPublicProps = ExtractPublicPropTypes
+export type SpinProviderComponent = DefineComponent
+export type SpinProviderInstance = InstanceType>
diff --git a/packages/components/spin/src/useSpin.ts b/packages/components/spin/src/useSpin.ts
new file mode 100644
index 000000000..214bd4285
--- /dev/null
+++ b/packages/components/spin/src/useSpin.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import type { SpinProviderRef } from './types'
+
+import { inject } from 'vue'
+
+import { throwError } from '@idux/cdk/utils'
+
+import { spinProviderToken } from './token'
+
+export const useSpin = (): SpinProviderRef => {
+ const spinProviderRef = inject(spinProviderToken, null)
+ if (spinProviderRef === null) {
+ return throwError('components/spin', ' not found.')
+ }
+ return spinProviderRef
+}
diff --git a/packages/components/spin/style/index.less b/packages/components/spin/style/index.less
index e5f383e80..1eb63ca53 100644
--- a/packages/components/spin/style/index.less
+++ b/packages/components/spin/style/index.less
@@ -1,13 +1,13 @@
@import '../../style/mixins/reset.less';
-.spin-mask() {
- position: absolute;
+.spin-mask(@position: absolute) {
+ position: @position;
top: 0;
left: 0;
right: 0;
bottom: 0;
user-select: none;
-}
+};
.spin-size(@size) {
&-@{size} {
@@ -32,6 +32,31 @@
position: relative;
+ &-target-container {
+ user-select: none;
+ clear: both;
+ &-relative {
+ position: relative;
+ }
+ &-has-scroll {
+ >.@{spin-prefix} {
+ >.@{spin-prefix}-spinner {
+ position: sticky;
+ top: 50%;
+ left: 50%;
+ }
+ }
+ }
+ >.@{spin-prefix} {
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ position: absolute;
+ background-color: hsl(0deg 0% 100% / 40%);
+ }
+ }
+
&-spinner {
.spin-mask();
diff --git a/packages/site/src/App.vue b/packages/site/src/App.vue
index af1cda067..e55c659ce 100644
--- a/packages/site/src/App.vue
+++ b/packages/site/src/App.vue
@@ -1,62 +1,64 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-