Skip to content

feat: spin support cssvar #8215

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

Open
wants to merge 1 commit into
base: feat-4.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions components/spin/Progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { defineComponent, ref, computed, watchEffect } from 'vue';

export interface ProgressProps {
prefixCls: string;
percent: number;
}

const viewSize = 100;
const borderWidth = viewSize / 5;
const radius = viewSize / 2 - borderWidth / 2;
const circumference = radius * 2 * Math.PI;
const position = 50;

const CustomCircle = defineComponent({
compatConfig: { MODE: 3 },
inheritAttrs: false,
props: {
dotClassName: String,
style: Object,
hasCircleCls: Boolean,
},
setup(props) {
const cStyle = computed(() => props.style || {});

return () => (
<circle
class={[
`${props.dotClassName}-circle`,
{
[`${props.dotClassName}-circle-bg`]: props.hasCircleCls,
},
]}
r={radius}
cx={position}
cy={position}
stroke-width={borderWidth}
style={cStyle.value}
/>
);
},
});

export default defineComponent({
compatConfig: { MODE: 3 },
name: 'Progress',
inheritAttrs: false,
props: {
percent: Number,
prefixCls: String,
},
setup(props) {
const dotClassName = `${props.prefixCls}-dot`;
const holderClassName = `${dotClassName}-holder`;
const hideClassName = `${holderClassName}-hidden`;

const render = ref(false);

// ==================== Visible =====================
watchEffect(() => {
if (props.percent !== 0) {
render.value = true;
}
});

// ==================== Progress ====================
const safePtg = computed(() => Math.max(Math.min(props.percent, 100), 0));

const circleStyle = computed(() => ({
strokeDashoffset: `${circumference / 4}`,
strokeDasharray: `${(circumference * safePtg.value) / 100} ${
(circumference * (100 - safePtg.value)) / 100
}`,
}));

// ===================== Render =====================
return () => {
if (!render.value) {
return null;
}

return (
<span
class={[holderClassName, `${dotClassName}-progress`, safePtg.value <= 0 && hideClassName]}
>
<svg
viewBox={`0 0 ${viewSize} ${viewSize}`}
{...({
role: 'progressbar',
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-valuenow': safePtg.value,
} as any)}
>
<CustomCircle dotClassName={dotClassName} hasCircleCls={true} />
<CustomCircle dotClassName={dotClassName} style={circleStyle.value} />
</svg>
</span>
);
};
},
});
104 changes: 85 additions & 19 deletions components/spin/Spin.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import type { VNode, ExtractPropTypes, PropType } from 'vue';
import { onBeforeUnmount, cloneVNode, isVNode, defineComponent, shallowRef, watch } from 'vue';
import {
onBeforeUnmount,
cloneVNode,
isVNode,
defineComponent,
shallowRef,
watch,
computed,
} from 'vue';
import { debounce } from 'throttle-debounce';
import PropTypes from '../_util/vue-types';
import { filterEmpty, getPropsSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import useStyle from './style';
import useConfigInject from '../config-provider/hooks/useConfigInject';
import useCSSVarCls from '../config-provider/hooks/useCssVarCls';
import Progress from './Progress';
import usePercent from './usePercent';

export type SpinSize = 'small' | 'default' | 'large';
export const spinProps = () => ({
Expand All @@ -16,6 +27,8 @@ export const spinProps = () => ({
tip: PropTypes.any,
delay: Number,
indicator: PropTypes.any,
fullscreen: Boolean,
percent: [Number, String] as PropType<number | 'auto'>,
});

export type SpinProps = Partial<ExtractPropTypes<ReturnType<typeof spinProps>>>;
Expand All @@ -40,11 +53,16 @@ export default defineComponent({
size: 'default',
spinning: true,
wrapperClassName: '',
fullscreen: false,
}),
setup(props, { attrs, slots }) {
const { prefixCls, size, direction } = useConfigInject('spin', props);
const [wrapSSR, hashId] = useStyle(prefixCls);
const rootCls = useCSSVarCls(prefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
const sSpinning = shallowRef(props.spinning && !shouldDelay(props.spinning, props.delay));

const mergedPercent = computed(() => usePercent(sSpinning.value, props.percent));

let updateSpinning: any;
watch(
[() => props.spinning, () => props.delay],
Expand All @@ -63,6 +81,7 @@ export default defineComponent({
onBeforeUnmount(() => {
updateSpinning?.cancel();
});

return () => {
const { class: cls, ...divProps } = attrs;
const { tip = slots.tip?.() } = props;
Expand All @@ -78,8 +97,11 @@ export default defineComponent({
[cls as string]: !!cls,
};

function renderIndicator(prefixCls: string) {
function renderIndicator(prefixCls: string, percent: number) {
const dotClassName = `${prefixCls}-dot`;
const holderClassName = `${dotClassName}-holder`;
const hideClassName = `${holderClassName}-hidden`;

let indicator = getPropsSlot(slots, props, 'indicator');
// should not be render default indicator when indicator value is null
if (indicator === null) {
Expand All @@ -89,43 +111,87 @@ export default defineComponent({
indicator = indicator.length === 1 ? indicator[0] : indicator;
}
if (isVNode(indicator)) {
return cloneVNode(indicator, { class: dotClassName });
return cloneVNode(indicator, { class: dotClassName, percent });
}

if (defaultIndicator && isVNode(defaultIndicator())) {
return cloneVNode(defaultIndicator(), { class: dotClassName });
return cloneVNode(defaultIndicator(), { class: dotClassName, percent });
}

return (
<span class={`${dotClassName} ${prefixCls}-dot-spin`}>
<i class={`${prefixCls}-dot-item`} />
<i class={`${prefixCls}-dot-item`} />
<i class={`${prefixCls}-dot-item`} />
<i class={`${prefixCls}-dot-item`} />
</span>
<>
<span class={[holderClassName, percent > 0 && hideClassName]}>
<span class={[dotClassName, `${prefixCls}-dot-spin`]}>
{[1, 2, 3, 4].map(i => (
<i class={`${prefixCls}-dot-item`} key={i} />
))}
</span>
</span>
{props.percent && <Progress prefixCls={prefixCls} percent={percent} />}
</>
);
}
const spinElement = (
<div {...divProps} class={spinClassName} aria-live="polite" aria-busy={sSpinning.value}>
{renderIndicator(prefixCls.value)}
{tip ? <div class={`${prefixCls.value}-text`}>{tip}</div> : null}
<div
{...divProps}
key="loading"
class={[spinClassName, rootCls.value, cssVarCls.value]}
aria-live="polite"
aria-busy={sSpinning.value}
>
{renderIndicator(prefixCls.value, mergedPercent.value.value)}
{tip ? (
<div class={[`${prefixCls.value}-text`, hashId.value, rootCls.value, cssVarCls.value]}>
{tip}
</div>
) : null}
</div>
);
if (children && filterEmpty(children).length) {
if (children && filterEmpty(children).length && !props.fullscreen) {
const containerClassName = {
[`${prefixCls.value}-container`]: true,
[`${prefixCls.value}-blur`]: sSpinning.value,
[rootCls.value]: true,
[cssVarCls.value]: true,
[hashId.value]: true,
};
return wrapSSR(
<div class={[`${prefixCls.value}-nested-loading`, props.wrapperClassName, hashId.value]}>
{sSpinning.value && <div key="loading">{spinElement}</div>}
return wrapCSSVar(
<div
class={[
`${prefixCls.value}-nested-loading`,
props.wrapperClassName,
hashId.value,
rootCls.value,
cssVarCls.value,
]}
>
{sSpinning.value && spinElement}
<div class={containerClassName} key="container">
{children}
</div>
</div>,
);
}
return wrapSSR(spinElement);

if (props.fullscreen) {
return wrapCSSVar(
<div
class={[
`${prefixCls.value}-fullscreen`,
{
[`${prefixCls.value}-fullscreen-show`]: sSpinning.value,
},
hashId.value,
rootCls.value,
cssVarCls.value,
]}
>
{spinElement}
</div>,
);
}

return wrapCSSVar(spinElement);
};
},
});
25 changes: 24 additions & 1 deletion components/spin/demo/custom-indicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,38 @@ Use custom loading indicator.
</docs>

<template>
<a-spin :indicator="indicator" />
<a-flex align="center" gap="middle">
<a-spin :indicator="smallIndicator" />
<a-spin :indicator="indicator" />
<a-spin :indicator="largeIndicator" />
<a-spin :indicator="customIndicator" />
</a-flex>
</template>
<script lang="ts" setup>
import { LoadingOutlined } from '@ant-design/icons-vue';
import { h } from 'vue';
const smallIndicator = h(LoadingOutlined, {
style: {
fontSize: '16px',
},
spin: true,
});
const indicator = h(LoadingOutlined, {
style: {
fontSize: '24px',
},
spin: true,
});
const largeIndicator = h(LoadingOutlined, {
style: {
fontSize: '36px',
},
spin: true,
});
const customIndicator = h(LoadingOutlined, {
style: {
fontSize: '48px',
},
spin: true,
});
</script>
51 changes: 51 additions & 0 deletions components/spin/demo/fullscreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<docs>
---
order: 8
title:
zh-CN: 全屏
en-US: fullscreen
---

## zh-CN

`fullscreen` 属性非常适合创建流畅的页面加载器。它添加了半透明覆盖层,并在其中心放置了一个旋转加载符号。

## en-US

The `fullscreen` mode is perfect for creating page loaders. It adds a dimmed overlay with a centered spinner.

</docs>

<template>
<a-button @click="showLoader">Show fullscreen</a-button>
<a-spin :spinning="spinning" :percent="percent" fullscreen />
</template>

<script setup>
import { ref, onUnmounted } from 'vue';

const spinning = ref(false);
const percent = ref(0);
let interval = null;

const showLoader = () => {
spinning.value = true;
let ptg = -10;

interval = setInterval(() => {
ptg += 5;
percent.value = ptg;

if (ptg > 120) {
if (interval) clearInterval(interval);
spinning.value = false;
percent.value = 0;
}
}, 100);
};

// 组件卸载时清理定时器
onUnmounted(() => {
if (interval) clearInterval(interval);
});
</script>
6 changes: 6 additions & 0 deletions components/spin/demo/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<tip />
<delay />
<custom-indicator />
<fullscreen />
<percent />
</demo-sort>
</template>
<script lang="ts">
Expand All @@ -16,6 +18,8 @@ import Inside from './inside.vue';
import Nested from './nested.vue';
import Tip from './tip.vue';
import Delay from './delay.vue';
import Fullscreen from './fullscreen.vue';
import Percent from './percent.vue';
import CustomIndicator from './custom-indicator.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
Expand All @@ -31,6 +35,8 @@ export default defineComponent({
Tip,
Delay,
CustomIndicator,
Fullscreen,
Percent,
},
setup() {
return {};
Expand Down
Loading