diff --git a/src/loading/Loading.tsx b/src/loading/Loading.tsx new file mode 100644 index 0000000000..82f155ec04 --- /dev/null +++ b/src/loading/Loading.tsx @@ -0,0 +1,67 @@ +import React, { useMemo, forwardRef, useRef } from 'react'; +import classNames from 'classnames'; +import useDebounceLoading from '../utils/hooks/useDebounceLoading'; +import composeRef from '../utils/composeRef'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { LoadingProps } from './interface'; +import Wheel from './Wheel'; + +const Loading = forwardRef((props, ref) => { + const { + prefixCls: customizePrefixCls, + loading = true, + delay = 0, + indicator, + titlePosition = 'bottom', + title, + size = 'large', + className, + style, + children, + blurColor = 'white', + } = props; + const prefixCls = usePrefixCls('loading-new', customizePrefixCls); + const shouldLoading = useDebounceLoading(loading, delay); + const loadingRef = useRef(null); + + const loadingElementAndTitle: JSX.Element = useMemo( + () => + shouldLoading ? ( +
+ + {title && ( + {title} + )} +
+ ) : ( + <>{null} + ), + [className, customizePrefixCls, indicator, prefixCls, ref, shouldLoading, size, style, title, titlePosition] + ); + + const result: JSX.Element = useMemo(() => { + if (children) { + return ( +
+ {loadingElementAndTitle} +
+ {children} +
+
+ ); + } + return loadingElementAndTitle; + }, [blurColor, children, loadingElementAndTitle, prefixCls, shouldLoading, className]); + + return result; +}); + +export default Loading; diff --git a/src/loading/Wheel.tsx b/src/loading/Wheel.tsx new file mode 100644 index 0000000000..3cc0410e39 --- /dev/null +++ b/src/loading/Wheel.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import { WheelProps } from './interface'; + +const Wheel: React.FC = (props: WheelProps) => { + const { indicator, prefixCls: customizePrefixCls } = props; + const prefixCls = usePrefixCls('loading-new', customizePrefixCls); + if (indicator) { + return {indicator}; + } + return ( +
+ {[1, 2, 3, 4].map((item) => ( +
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ); +}; + +export default Wheel; diff --git a/src/loading/demos/Loading.stories.tsx b/src/loading/demos/Loading.stories.tsx new file mode 100644 index 0000000000..d65149adeb --- /dev/null +++ b/src/loading/demos/Loading.stories.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { withDesign } from 'storybook-addon-designs'; +import { LoadingOutlined } from '@gio-design/icons'; +import Loading from '../index'; +import Button from '../../button'; +import Tabs, { TabPane } from '../../components/tabs'; +import { LoadingProps } from '../interface'; +import '../style'; + +export default { + title: 'Upgraded/Loading', + component: Loading, + decorators: [withDesign], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/kP3A6S2fLUGVVMBgDuUx0f/GrowingIO-Design-Components?node-id=889%3A1142', + allowFullscreen: true, + }, + docs: { + page: null, + }, + }, +} as Meta; + +const defaultTabs = ( + + +
    +
  • Event 1
  • +
  • Event 1
  • +
  • Event 1
  • +
  • Event 1
  • +
+
+ +
    +
  • Event 2
  • +
  • Event 2
  • +
  • Event 2
  • +
  • Event 2
  • +
+
+ +
    +
  • Event 3
  • +
  • Event 3
  • +
  • Event 3
  • +
  • Event 3
  • +
+
+
+); + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + loading: true, + titlePosition: 'right', + autoCenter: true, +}; + +export const Container = Template.bind({}); +Container.args = { + children: defaultTabs, +}; + +export const Indicator = Template.bind({}); + +Indicator.args = { + indicator: , + title: false, +}; + +Indicator.story = { + parameters: { + design: { + url: 'https://www.figma.com/file/kP3A6S2fLUGVVMBgDuUx0f/GrowingIO-Design-Components?node-id=889%3A1159', + }, + }, +}; + +interface DelayProps { + delay: number; +} + +const DelayTemplate: Story = (args) => { + const { delay } = args; + const [loading, setLoading] = React.useState(true); + return ( + <> + + {defaultTabs} + +
+ + + ); +}; + +export const Delay = DelayTemplate.bind({}); +Delay.args = { + delay: 1000, +}; + +const SizeTemplate: Story = () => { + const styles = { height: 100, border: '1px solid #818181', marginBottom: 8 }; + return ( + <> +
+ +
+
+ +
+
+ +
+ + ); +}; + +export const Size = SizeTemplate.bind({}); +Size.args = {}; diff --git a/src/loading/index.tsx b/src/loading/index.tsx new file mode 100644 index 0000000000..d8584c083d --- /dev/null +++ b/src/loading/index.tsx @@ -0,0 +1,4 @@ +import Loading from './Loading'; + +export { LoadingProps } from './interface'; +export default Loading; diff --git a/src/loading/interface.ts b/src/loading/interface.ts new file mode 100644 index 0000000000..10a933492f --- /dev/null +++ b/src/loading/interface.ts @@ -0,0 +1,51 @@ +export interface LoadingProps { + /** + 是否为加载中状态 + */ + loading?: boolean; + /** + 描述文案相对于指示符号的位置 + */ + titlePosition?: 'right' | 'bottom'; + /** + 自定义描述文案 + */ + title?: false | string; + /** + 延迟显示加载效果的时间(防止闪烁) + */ + delay?: number; + /** + 替换`class`类前缀 + */ + prefixCls?: string; + /** + 设置被包裹的元素 + */ + children?: React.ReactElement[] | React.ReactElement; + /** + 自定义 `className` + */ + className?: string; + /** + 自定义样式 + */ + style?: React.CSSProperties; + /** + 设置默认指示符号大小 + */ + size?: 'small' | 'middle' | 'large'; + /** + 自定义指示符号 + */ + indicator?: React.ReactElement; + /** + 设置模糊蒙层颜色 + */ + blurColor?: 'white' | 'black'; +} + +export interface WheelProps { + indicator?: React.ReactElement; + prefixCls?: string; +} diff --git a/src/loading/style/index.less b/src/loading/style/index.less new file mode 100644 index 0000000000..e0dfe5be1e --- /dev/null +++ b/src/loading/style/index.less @@ -0,0 +1,342 @@ +@import '../../stylesheet/index.less'; + +@loading-prefix-cls: ~'@{component-prefix}-loading-new'; + +@color-1: #1248e9; +@color-2: #88a6ff; +@color-3: #1248e9; +@color-4: #99b1f9; + +@container-rotate-timing: 1600ms; +@ring-fill-unfill-rotate-timing: 4800ms; +@ring-line-count: 4; +@ring-left-spin: (@ring-fill-unfill-rotate-timing / @ring-line-count); +@ring-right-spin: (@ring-fill-unfill-rotate-timing / @ring-line-count); + +@keyframes container-rotate { + to { + transform: rotate(360deg); + } +} + +@keyframes fill-unfill-rotate { + 12.5% { + transform: rotate(135deg); + } + 25% { + transform: rotate(270deg); + } + 37.5% { + transform: rotate(405deg); + } + 50% { + transform: rotate(540deg); + } + 62.5% { + transform: rotate(675deg); + } + 75% { + transform: rotate(810deg); + } + 87.5% { + transform: rotate(945deg); + } + to { + transform: rotate(1080deg); + } +} + +@keyframes left-spin { + 0% { + transform: rotate(130deg); + } + 50% { + transform: rotate(-5deg); + } + to { + transform: rotate(130deg); + } +} +@keyframes right-spin { + 0% { + transform: rotate(-130deg); + } + 50% { + transform: rotate(5deg); + } + to { + transform: rotate(-130deg); + } +} + +@keyframes line-1-fade-in-out { + 0% { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 89% { + opacity: 0; + } + 90% { + opacity: 1; + } + to { + opacity: 1; + } +} + +@keyframes line-2-fade-in-out { + 0% { + opacity: 0; + } + 15% { + opacity: 0; + } + 25% { + opacity: 1; + } + 50% { + opacity: 1; + } + 51% { + opacity: 0; + } +} + +@keyframes line-3-fade-in-out { + 0% { + opacity: 0; + } + 40% { + opacity: 0; + } + 50% { + opacity: 1; + } + 75% { + opacity: 1; + } + 76% { + opacity: 0; + } +} + +@keyframes line-4-fade-in-out { + 0% { + opacity: 0; + } + 65% { + opacity: 0; + } + 75% { + opacity: 1; + } + 90% { + opacity: 1; + } + to { + opacity: 0; + } +} + +.@{loading-prefix-cls} { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + line-height: 0; + text-align: center; + + &-wrapper-loading { + position: relative; + height: 100%; + } + + &-wrapper-loading > & { + position: absolute; + top: 50%; + z-index: @zindex-loading; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + transform: translateY(-50%); + } + + &-container { + position: relative; + height: 100%; + &-loading { + cursor: not-allowed; + pointer-events: none; + } + &-loading::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-loading; + content: ''; + } + &-blur-white::after { + background-color: @color-background-loading-blur-white; + } + &-blur-black::after { + background-color: @color-background-loading-blur-black; + } + } + + &-ring { + display: inline-block; + animation: container-rotate @container-rotate-timing linear infinite; + &-line { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + + &-1 { + border-color: @color-1; + animation: fill-unfill-rotate @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both, + line-1-fade-in-out @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-2 { + border-color: @color-2; + animation: fill-unfill-rotate @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both, + line-2-fade-in-out @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-3 { + border-color: @color-3; + animation: fill-unfill-rotate @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both, + line-3-fade-in-out @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-4 { + border-color: @color-4; + animation: fill-unfill-rotate @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both, + line-4-fade-in-out @ring-fill-unfill-rotate-timing cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-cog { + position: relative; + display: inline-block; + width: 50%; + height: 100%; + overflow: hidden; + border-color: inherit; + &-inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-sizing: border-box; + width: 200%; + height: 100%; + border-color: inherit; + border-style: solid; + border-bottom-color: transparent; + border-radius: 50%; + animation: none; + + &-left { + border-right-color: transparent; + transform: rotate(129deg); + animation: left-spin @ring-left-spin cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-right { + left: -100%; + border-left-color: transparent; + transform: rotate(-129deg); + animation: right-spin @ring-right-spin cubic-bezier(0.4, 0, 0.2, 1) infinite both; + } + + &-center { + left: -450%; + width: 1000%; + } + } + } + + &-ticker { + position: absolute; + top: 0; + left: 45%; + box-sizing: border-box; + width: 10%; + height: 100%; + overflow: hidden; + border-color: inherit; + } + } + } + + &-large &-ring { + width: 50px; + height: 50px; + &-line-cog-inner { + border-width: 3.5px; + } + } + + &-middle &-ring { + width: 40px; + height: 40px; + &-line-cog-inner { + border-width: 3px; + } + } + + &-small &-ring { + width: 30px; + height: 30px; + &-line-cog-inner { + border-width: 2px; + } + } + + &-small &-title-right { + height: 30px; + } + + &-middle &-title-right { + height: 40px; + } + + &-large &-title-right { + height: 50px; + } + + &-title { + color: @color-text-loading; + font-size: @size-font-14; + &-bottom { + display: block; + height: 14px; + margin-top: 16px; + } + &-right { + display: inline-block; + height: 100%; + margin-left: 16px; + line-height: 100%; + vertical-align: middle; + } + } + + &-indicator { + display: inline-block; + vertical-align: middle; + } +} diff --git a/src/loading/style/index.ts b/src/loading/style/index.ts new file mode 100644 index 0000000000..d74e52ee9f --- /dev/null +++ b/src/loading/style/index.ts @@ -0,0 +1 @@ +import './index.less';