Compare commits

...

18 Commits

Author SHA1 Message Date
二货机器人 920864e303 chore: skip error type 2 years ago
二货机器人 77f22d5d0d chore: adjust color alg 2 years ago
二货机器人 4b7967eeb9 chore: merge feature 2 years ago
二货机器人 75929d07d5 chore: bump rc-rate 2 years ago
二货机器人 78630f28fc chore: wave effect it 2 years ago
二货机器人 6e4a6df77c chore: adjust layer 2 years ago
二货机器人 985e0c73d0 Merge remote-tracking branch 'origin/feature' into happy 2 years ago
二货机器人 cb5f4ddb51 chore: move css selector from cls to data attr 2 years ago
二货机器人 7d243d23a6 fix: dot count 2 years ago
二货机器人 6eb620f079 chore: radio wave 2 years ago
二货机器人 b6b7ab562b chore: checkbox use wave 2 years ago
二货机器人 f08c980eb9 chore: fix compile 2 years ago
二货机器人 8a729d6642 chore: effect of it 2 years ago
二货机器人 192c9332fa flower 2 years ago
二货机器人 e21f44e56f center lines 2 years ago
二货机器人 966ba02bf9 motion it 2 years ago
二货机器人 7e4139815e chore: init wrapper 2 years ago
二货机器人 1d7af30f28 chore: init happy 2 years ago
  1. 16
      .dumi/theme/common/ThemeSwitch/index.tsx
  2. 35
      .dumi/theme/layouts/GlobalLayout.tsx
  3. 1
      .dumi/theme/locales/en-US.json
  4. 1
      .dumi/theme/locales/zh-CN.json
  5. 83
      components/_util/wave/WaveEffect.tsx
  6. 6
      components/_util/wave/index.ts
  7. 9
      components/_util/wave/useWave.ts
  8. 7
      components/badge/demo/ribbon.tsx
  9. 40
      components/checkbox/Checkbox.tsx
  10. 42
      components/checkbox/style/index.ts
  11. 26
      components/config-provider/context.ts
  12. 5
      components/config-provider/index.tsx
  13. 8
      components/progress/utils.ts
  14. 30
      components/radio/radio.tsx
  15. 19
      components/radio/style/index.tsx
  16. 4
      components/theme/index.ts
  17. 6
      components/theme/internal.ts
  18. 9
      components/theme/themesToken/happy/index.tsx
  19. 100
      components/theme/themesToken/happy/style/wave.ts
  20. 253
      components/theme/themesToken/happy/wave.tsx

16
.dumi/theme/common/ThemeSwitch/index.tsx

@ -2,12 +2,12 @@ import React from 'react';
import { FloatButton } from 'antd';
import { FormattedMessage, Link, useLocation } from 'dumi';
import { DarkTheme, CompactTheme } from 'antd-token-previewer/es/icons';
import { BgColorsOutlined } from '@ant-design/icons';
import { BgColorsOutlined, SmileOutlined } from '@ant-design/icons';
import useSiteToken from '../../../hooks/useSiteToken';
import { getLocalizedPathname, isZhCN } from '../../utils';
import ThemeIcon from './ThemeIcon';
export type ThemeName = 'light' | 'dark' | 'compact';
export type ThemeName = 'light' | 'dark' | 'compact' | 'happy';
export type ThemeSwitchProps = {
value?: ThemeName[];
@ -53,6 +53,18 @@ const ThemeSwitch: React.FC<ThemeSwitchProps> = (props: ThemeSwitchProps) => {
}}
tooltip={<FormattedMessage id="app.theme.switch.compact" />}
/>
<FloatButton
icon={<SmileOutlined />}
type={value.includes('happy') ? 'primary' : 'default'}
onClick={() => {
if (value.includes('happy')) {
onChange(value.filter((theme) => theme !== 'happy'));
} else {
onChange([...value, 'happy']);
}
}}
tooltip={<FormattedMessage id="app.theme.switch.happy" />}
/>
</FloatButton.Group>
);
};

35
.dumi/theme/layouts/GlobalLayout.tsx

@ -5,17 +5,17 @@ import {
parentSelectorLinter,
StyleProvider,
} from '@ant-design/cssinjs';
import { App, ConfigProvider, theme as antdTheme } from 'antd';
import { theme as antdTheme, App, ConfigProvider } from 'antd';
import type { DirectionType } from 'antd/es/config-provider';
import { createSearchParams, useOutlet, useSearchParams } from 'dumi';
import React, { useCallback, useEffect, useMemo } from 'react';
import SiteThemeProvider from '../SiteThemeProvider';
import useLayoutState from '../../hooks/useLayoutState';
import useLocation from '../../hooks/useLocation';
import type { ThemeName } from '../common/ThemeSwitch';
import ThemeSwitch from '../common/ThemeSwitch';
import SiteThemeProvider from '../SiteThemeProvider';
import type { SiteContextProps } from '../slots/SiteContext';
import SiteContext from '../slots/SiteContext';
import useLayoutState from '../../hooks/useLayoutState';
type Entries<T> = { [K in keyof T]: [K, T[K]] }[keyof T][];
type SiteState = Partial<Omit<SiteContextProps, 'updateSiteContext'>>;
@ -28,15 +28,20 @@ if (typeof global !== 'undefined') {
}
const getAlgorithm = (themes: ThemeName[] = []) =>
themes.map((theme) => {
if (theme === 'dark') {
return antdTheme.darkAlgorithm;
}
if (theme === 'compact') {
return antdTheme.compactAlgorithm;
}
return antdTheme.defaultAlgorithm;
});
themes
.map((theme) => {
if (theme === 'dark') {
return antdTheme.darkAlgorithm;
}
if (theme === 'compact') {
return antdTheme.compactAlgorithm;
}
if (theme === 'happy') {
return null;
}
return antdTheme.defaultAlgorithm;
})
.filter((algorithm) => algorithm);
const GlobalLayout: React.FC = () => {
const outlet = useOutlet();
@ -107,6 +112,10 @@ const GlobalLayout: React.FC = () => {
[isMobile, direction, updateSiteConfig, theme],
);
// Happy Work Theme
const components = theme.includes('happy') ? antdTheme.happyThemeToken.token : null;
const wave = theme.includes('happy') ? antdTheme.happyThemeToken.wave : null;
return (
<StyleProvider
cache={styleCache}
@ -116,7 +125,9 @@ const GlobalLayout: React.FC = () => {
<ConfigProvider
theme={{
algorithm: getAlgorithm(theme),
components,
}}
wave={wave}
>
<SiteThemeProvider>
<App>

1
.dumi/theme/locales/en-US.json

@ -3,6 +3,7 @@
"app.theme.switch.default": "Default theme",
"app.theme.switch.dark": "Dark theme",
"app.theme.switch.compact": "Compact theme",
"app.theme.switch.happy": "Happy Work theme",
"app.header.search": "Search...",
"app.header.menu.documentation": "Docs",
"app.header.menu.more": "More",

1
.dumi/theme/locales/zh-CN.json

@ -3,6 +3,7 @@
"app.theme.switch.default": "默认主题",
"app.theme.switch.dark": "暗黑主题",
"app.theme.switch.compact": "紧凑主题",
"app.theme.switch.happy": "快乐工作主题",
"app.header.search": "全文本搜索...",
"app.header.menu.documentation": "文档",
"app.header.menu.more": "更多",

83
components/_util/wave/WaveEffect.tsx

@ -1,21 +1,22 @@
import * as React from 'react';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import raf from 'rc-util/lib/raf';
import { render, unmount } from 'rc-util/lib/React/render';
import classNames from 'classnames';
import raf from 'rc-util/lib/raf';
import * as React from 'react';
import type { ConfigProviderProps } from '../../config-provider';
import type { useToken } from '../../theme/internal';
import { getTargetWaveColor } from './util';
function validateNum(value: number) {
return Number.isNaN(value) ? 0 : value;
}
export interface WaveEffectProps {
className: string;
target: HTMLElement;
export interface WaveEffectProps extends Omit<WaveWrapperProps, 'token'> {
onFinish: VoidFunction;
}
const WaveEffect: React.FC<WaveEffectProps> = (props) => {
const { className, target } = props;
const { className, target, onFinish } = props;
const divRef = React.useRef<HTMLDivElement>(null);
const [color, setWaveColor] = React.useState<string | null>(null);
@ -41,15 +42,15 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
}
function syncPos() {
const nodeStyle = getComputedStyle(target);
const targetStyle = getComputedStyle(target);
// Get wave color from target
setWaveColor(getTargetWaveColor(target));
const isStatic = nodeStyle.position === 'static';
const isStatic = targetStyle.position === 'static';
// Rect
const { borderLeftWidth, borderTopWidth } = nodeStyle;
const { borderLeftWidth, borderTopWidth } = targetStyle;
setLeft(isStatic ? target.offsetLeft : validateNum(-parseFloat(borderLeftWidth)));
setTop(isStatic ? target.offsetTop : validateNum(-parseFloat(borderTopWidth)));
setWidth(target.offsetWidth);
@ -61,7 +62,7 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
borderTopRightRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
} = nodeStyle;
} = targetStyle;
setBorderRadius(
[
@ -110,10 +111,7 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
motionDeadline={5000}
onAppearEnd={(_, event) => {
if (event.deadline || (event as TransitionEvent).propertyName === 'opacity') {
const holder = divRef.current?.parentElement!;
unmount(holder).then(() => {
holder.parentElement?.removeChild(holder);
});
onFinish();
}
return false;
}}
@ -125,13 +123,60 @@ const WaveEffect: React.FC<WaveEffectProps> = (props) => {
);
};
export default function showWaveEffect(node: HTMLElement, className: string) {
export interface WaveWrapperProps {
className: string;
currentTarget: HTMLElement;
/**
* In most case same as `currentTarget`.
* Only when `currentTarget` has children element with `.antd-wave-target` className.
* `target` will be the classNamed one.
*/
target: HTMLElement;
token: ReturnType<typeof useToken>;
holder: HTMLElement;
}
function WaveWrapper(props: WaveWrapperProps) {
const { holder } = props;
function onFinish() {
unmount(holder).then(() => {
holder.parentElement?.removeChild(holder);
});
}
return <WaveEffect {...props} onFinish={onFinish} />;
}
export default function showWaveEffect(
node: HTMLElement,
className: string,
token: ReturnType<typeof useToken>,
wave: ConfigProviderProps['wave'],
) {
// Target
const target = node.querySelector<HTMLElement>('.antd-wave-target') || node;
if (wave?.render) {
wave?.render({ target, currentTarget: node, token });
return;
}
// Create holder
const holder = document.createElement('div');
holder.style.position = 'absolute';
holder.style.left = `0px`;
holder.style.top = `0px`;
node?.insertBefore(holder, node?.firstChild);
render(<WaveEffect target={node} className={className} />, holder);
target?.insertBefore(holder, target?.firstChild);
render(
<WaveWrapper
currentTarget={node}
target={target}
className={className}
token={token}
holder={holder}
/>,
holder,
);
}

6
components/_util/wave/index.ts

@ -7,6 +7,7 @@ import { ConfigContext } from '../../config-provider';
import { cloneElement } from '../reactNode';
import useStyle from './style';
import useWave from './useWave';
import { useToken } from '../../theme/internal';
export interface WaveProps {
disabled?: boolean;
@ -15,15 +16,16 @@ export interface WaveProps {
const Wave: React.FC<WaveProps> = (props) => {
const { children, disabled } = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const { getPrefixCls, wave } = useContext<ConfigConsumerProps>(ConfigContext);
const containerRef = useRef<HTMLElement>(null);
// ============================== Style ===============================
const prefixCls = getPrefixCls('wave');
const [, hashId] = useStyle(prefixCls);
const token = useToken();
// =============================== Wave ===============================
const showWave = useWave(containerRef, classNames(prefixCls, hashId));
const showWave = useWave(containerRef, classNames(prefixCls, hashId), token, wave);
// ============================== Effect ==============================
React.useEffect(() => {

9
components/_util/wave/useWave.ts

@ -1,14 +1,19 @@
import useEvent from 'rc-util/lib/hooks/useEvent';
import type { ConfigProviderProps } from '../../config-provider';
import showWaveEffect from './WaveEffect';
import type { useToken } from '../../theme/internal';
export default function useWave(
nodeRef: React.RefObject<HTMLElement>,
className: string,
token: ReturnType<typeof useToken>,
wave: ConfigProviderProps['wave'],
): VoidFunction {
function showWave() {
const node = nodeRef.current!;
showWaveEffect(node, className);
showWaveEffect(node, className, token, wave);
}
return showWave;
return useEvent(showWave);
}

7
components/badge/demo/ribbon.tsx

@ -8,12 +8,15 @@ const App: React.FC = () => (
and raises the spyglass.
</Card>
</Badge.Ribbon>
<Badge.Ribbon text={
<Badge.Ribbon
text={
<div>
Hippies <br />
Happy
</div>
} color="pink">
}
color="pink"
>
<Card title="Pushes open the window" size="small">
and raises the spyglass.
</Card>

40
components/checkbox/Checkbox.tsx

@ -9,6 +9,7 @@ import { GroupContext } from './Group';
import DisabledContext from '../config-provider/DisabledContext';
import useStyle from './style';
import Wave from '../_util/wave';
export interface AbstractCheckboxProps<T> {
prefixCls?: string;
@ -126,28 +127,33 @@ const InternalCheckbox: React.ForwardRefRenderFunction<CheckboxRef, CheckboxProp
{
[`${prefixCls}-indeterminate`]: indeterminate,
},
'antd-wave-target',
hashId,
);
const ariaChecked = indeterminate ? 'mixed' : undefined;
/* eslint-disable jsx-a11y/label-has-associated-control */
return wrapSSR(
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={classString}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<RcCheckbox
aria-checked={ariaChecked}
{...checkboxProps}
prefixCls={prefixCls}
className={checkboxClass}
disabled={mergedDisabled}
ref={ref}
/>
{children !== undefined && <span>{children}</span>}
</label>,
<Wave>
<label
className={classString}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<RcCheckbox
aria-checked={ariaChecked}
{...checkboxProps}
prefixCls={prefixCls}
className={checkboxClass}
disabled={mergedDisabled}
ref={ref}
/>
{children !== undefined && <span>{children}</span>}
</label>
</Wave>,
);
/* eslint-enable */
};
const Checkbox = React.forwardRef<unknown, CheckboxProps>(InternalCheckbox);

42
components/checkbox/style/index.ts

@ -1,4 +1,3 @@
import { Keyframes } from '@ant-design/cssinjs';
import { genFocusOutline, resetComponent } from '../../style';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
@ -10,19 +9,6 @@ interface CheckboxToken extends FullToken<'Checkbox'> {
checkboxSize: number;
}
// ============================== Motion ==============================
const antCheckboxEffect = new Keyframes('antCheckboxEffect', {
'0%': {
transform: 'scale(1)',
opacity: 0.5,
},
'100%': {
transform: 'scale(1.6)',
opacity: 0,
},
});
// ============================== Styles ==============================
export const genCheckboxStyle: GenerateStyle<CheckboxToken> = (token) => {
const { checkboxCls } = token;
@ -75,6 +61,7 @@ export const genCheckboxStyle: GenerateStyle<CheckboxToken> = (token) => {
whiteSpace: 'nowrap',
lineHeight: 1,
cursor: 'pointer',
borderRadius: token.borderRadiusSM,
alignSelf: 'start',
// https://github.com/ant-design/ant-design/issues/41564
@ -167,13 +154,7 @@ export const genCheckboxStyle: GenerateStyle<CheckboxToken> = (token) => {
// ===================== Hover =====================
{
// Wrapper
[`${wrapperCls}:hover ${checkboxCls}:after`]: {
visibility: 'visible',
},
// Wrapper & Wrapper > Checkbox
[`
${wrapperCls}:not(${wrapperCls}-disabled),
${checkboxCls}:not(${checkboxCls}-disabled)
@ -208,24 +189,6 @@ export const genCheckboxStyle: GenerateStyle<CheckboxToken> = (token) => {
transition: `all ${token.motionDurationMid} ${token.motionEaseOutBack} ${token.motionDurationFast}`,
},
},
// Checked Effect
'&:after': {
position: 'absolute',
top: 0,
insetInlineStart: 0,
width: '100%',
height: '100%',
borderRadius: token.borderRadiusSM,
visibility: 'hidden',
border: `${token.lineWidthBold}px solid ${token.colorPrimary}`,
animationName: antCheckboxEffect,
animationDuration: token.motionDurationSlow,
animationTimingFunction: 'ease-in-out',
animationFillMode: 'backwards',
content: '""',
transition: `all ${token.motionDurationSlow}`,
},
},
[`
@ -236,9 +199,6 @@ export const genCheckboxStyle: GenerateStyle<CheckboxToken> = (token) => {
backgroundColor: token.colorPrimaryHover,
borderColor: 'transparent',
},
[`&:hover ${checkboxCls}:after`]: {
borderColor: token.colorPrimaryHover,
},
},
},

26
components/config-provider/context.ts

@ -1,11 +1,11 @@
import * as React from 'react';
import type { DerivativeFunc } from '@ant-design/cssinjs';
import * as React from 'react';
import type { Options } from 'scroll-into-view-if-needed';
import type { ConfigProviderProps } from '.';
import type { RequiredMark } from '../form/Form';
import type { Locale } from '../locale';
import type { AliasToken, MapToken, OverrideToken, SeedToken } from '../theme/interface';
import type { RenderEmptyHandler } from './defaultRenderEmpty';
import type { SizeType } from './SizeContext';
export const defaultIconPrefixCls = 'anticon';
@ -35,8 +35,11 @@ export interface ThemeConfig {
}
export type PopupOverflow = 'viewport' | 'scroll';
export interface ConfigConsumerProps {
export interface ConfigConsumerProps
extends Pick<
ConfigProviderProps,
'input' | 'pagination' | 'space' | 'form' | 'select' | 'pageHeader' | 'wave'
> {
getTargetContainer?: () => HTMLElement;
getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
rootPrefixCls?: string;
@ -45,20 +48,8 @@ export interface ConfigConsumerProps {
renderEmpty?: RenderEmptyHandler;
csp?: CSPConfig;
autoInsertSpaceInButton?: boolean;
input?: {
autoComplete?: string;
};
pagination?: {
showSizeChanger?: boolean;
};
locale?: Locale;
pageHeader?: {
ghost: boolean;
};
direction?: DirectionType;
space?: {
size?: SizeType | number;
};
virtual?: boolean;
popupMatchSelectWidth?: boolean;
popupOverflow?: PopupOverflow;
@ -68,9 +59,6 @@ export interface ConfigConsumerProps {
scrollToFirstError?: Options | boolean;
};
theme?: ThemeConfig;
select?: {
showSearch?: boolean;
};
}
const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => {

5
components/config-provider/index.tsx

@ -33,6 +33,7 @@ import useTheme from './hooks/useTheme';
import type { SizeType } from './SizeContext';
import SizeContext, { SizeContextProvider } from './SizeContext';
import useStyle from './style';
import type { WaveWrapperProps } from '../_util/wave/WaveEffect';
/**
* Since too many feedback using static method like `Modal.confirm` not getting theme,
@ -85,6 +86,7 @@ const PASSED_PROPS: Exclude<keyof ConfigConsumerProps, 'rootPrefixCls' | 'getPre
'pagination',
'form',
'select',
'wave',
];
export interface ConfigProviderProps {
@ -111,6 +113,9 @@ export interface ConfigProviderProps {
pagination?: {
showSizeChanger?: boolean;
};
wave?: {
render?: (props: Omit<WaveWrapperProps, 'className' | 'wave' | 'holder'>) => void;
};
locale?: Locale;
pageHeader?: {
ghost: boolean;

8
components/progress/utils.ts

@ -35,10 +35,10 @@ export const getPercentage = ({ percent, success, successPercent }: ProgressProp
return [realSuccessPercent, validProgress(validProgress(percent) - realSuccessPercent)];
};
export const getStrokeColor = ({ success = {}, strokeColor }: Partial<CircleProps>): (
| string
| Record<PropertyKey, string>
)[] => {
export const getStrokeColor = ({
success = {},
strokeColor,
}: Partial<CircleProps>): (string | Record<PropertyKey, string>)[] => {
const { strokeColor: successColor } = success;
return [successColor || presetPrimaryColors.green, strokeColor || null!];
};

30
components/radio/radio.tsx

@ -7,6 +7,7 @@ import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import { FormItemInputContext } from '../form/context';
import warning from '../_util/warning';
import Wave from '../_util/wave';
import RadioGroupContext, { RadioOptionTypeContext } from './context';
import type { RadioChangeEvent, RadioProps } from './interface';
@ -71,18 +72,27 @@ const InternalRadio: React.ForwardRefRenderFunction<CheckboxRef, RadioProps> = (
hashId,
);
/* eslint-disable jsx-a11y/label-has-associated-control */
return wrapSSR(
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox {...radioProps} type="radio" prefixCls={prefixCls} ref={mergedRef} />
{children !== undefined ? <span>{children}</span> : null}
</label>,
<Wave>
<label
className={wrapperClassString}
style={style}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<RcCheckbox
{...radioProps}
type="radio"
prefixCls={prefixCls}
ref={mergedRef}
className={classNames(radioProps.className, 'antd-wave-target')}
/>
{children !== undefined ? <span>{children}</span> : null}
</label>
</Wave>,
);
/* eslint-enable */
};
const Radio = React.forwardRef<CheckboxRef, RadioProps>(InternalRadio);

19
components/radio/style/index.tsx

@ -1,4 +1,3 @@
import { Keyframes } from '@ant-design/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import { genFocusOutline, resetComponent } from '../../style';
@ -30,11 +29,6 @@ interface RadioToken extends FullToken<'Radio'> {
}
// ============================== Styles ==============================
const antRadioEffect = new Keyframes('antRadioEffect', {
'0%': { transform: 'scale(1)', opacity: 0.5 },
'100%': { transform: 'scale(1.6)', opacity: 0 },
});
// styles from RadioGroup only
const getGroupRadioStyle: GenerateStyle<RadioToken> = (token) => {
const { componentCls, antCls } = token;
@ -71,7 +65,6 @@ const getRadioBasicStyle: GenerateStyle<RadioToken> = (token) => {
radioSize,
motionDurationSlow,
motionDurationMid,
motionEaseInOut,
motionEaseInOutCirc,
radioButtonBg,
colorBorder,
@ -91,7 +84,6 @@ const getRadioBasicStyle: GenerateStyle<RadioToken> = (token) => {
return {
[`${componentCls}-wrapper`]: {
...resetComponent(token),
position: 'relative',
display: 'inline-flex',
alignItems: 'baseline',
marginInlineStart: 0,
@ -125,10 +117,6 @@ const getRadioBasicStyle: GenerateStyle<RadioToken> = (token) => {
border: `${lineWidth}px ${lineType} ${radioCheckedColor}`,
borderRadius: '50%',
visibility: 'hidden',
animationName: antRadioEffect,
animationDuration: motionDurationSlow,
animationTimingFunction: motionEaseInOut,
animationFillMode: 'both',
content: '""',
},
@ -139,6 +127,7 @@ const getRadioBasicStyle: GenerateStyle<RadioToken> = (token) => {
outline: 'none',
cursor: 'pointer',
alignSelf: 'center',
borderRadius: '50%',
},
[`${componentCls}-wrapper:hover &,
@ -193,9 +182,11 @@ const getRadioBasicStyle: GenerateStyle<RadioToken> = (token) => {
[`${componentCls}-input`]: {
position: 'absolute',
insetBlockStart: 0,
insetInlineEnd: 0,
insetBlockEnd: 0,
insetInlineStart: 0,
width: 0,
height: 0,
padding: 0,
margin: 0,
zIndex: 1,
cursor: 'pointer',
opacity: 0,

4
components/theme/index.ts

@ -4,6 +4,7 @@ import type { GlobalToken } from './interface';
import defaultAlgorithm from './themes/default';
import darkAlgorithm from './themes/dark';
import compactAlgorithm from './themes/compact';
import happyThemeToken from './themesToken/happy';
// ZombieJ: We export as object to user but array in internal.
// This is used to minimize the bundle size for antd package but safe to refactor as object also.
@ -28,4 +29,7 @@ export default {
defaultAlgorithm,
darkAlgorithm,
compactAlgorithm,
// TODO: Remove this after to be third part package
happyThemeToken,
};

6
components/theme/internal.ts

@ -59,7 +59,11 @@ export const DesignTokenContext = React.createContext<{
}>(defaultConfig);
// ================================== Hook ==================================
export function useToken(): [Theme<SeedToken, MapToken>, GlobalToken, string] {
export function useToken(): [
theme: Theme<SeedToken, MapToken>,
globalToken: GlobalToken,
hashId: string,
] {
const {
token: rootDesignToken,
hashed,

9
components/theme/themesToken/happy/index.tsx

@ -0,0 +1,9 @@
import type { OverrideToken } from '../../interface';
import wave from './wave';
const token: OverrideToken = {};
export default {
token,
wave,
};

100
components/theme/themesToken/happy/style/wave.ts

@ -0,0 +1,100 @@
import { Keyframes, useStyleRegister } from '@ant-design/cssinjs';
import type { UseTokenType } from '../wave';
export const TARGET_ATTR = 'data-happy-wave-target';
const antWaveTargetEffect = new Keyframes('antWaveTargetEffect', {
'0%': {
transform: 'scale(1)',
},
'10%': {
transform: 'scale(1.1)',
},
'35%': {
transform: 'scale(0.94)',
},
'60%': {
transform: 'scale(1.05)',
},
'85%': {
transform: 'scale(0.97)',
},
'100%': {
transform: 'scale(1)',
},
});
const antWaveDotEffect = new Keyframes('antWaveDotEffect', {
'0%': {
opacity: 0,
left: `var(--start-x)`,
top: `var(--start-y)`,
width: `var(--start-size)`,
height: `var(--start-size)`,
background: `var(--background)`,
border: `var(--border)`,
},
'25%': {
opacity: 1,
},
'50%': {
opacity: 0.8,
},
'100%': {
opacity: 0,
left: `var(--end-x)`,
top: `var(--end-y)`,
width: `var(--end-size)`,
height: `var(--end-size)`,
background: `var(--background)`,
border: `var(--border)`,
},
});
export default function useStyle(tokenConfig: UseTokenType) {
const [theme, token, hashId] = tokenConfig;
useStyleRegister({ theme, token, hashId, path: ['Customize', 'Wave', 'Effect'] }, () => {
const { motionDurationSlow } = token;
const prefixCls = '.happy-wave';
return [
{
// ======================== Target ========================
[`[${TARGET_ATTR}], & [${TARGET_ATTR}]`]: {
animationName: antWaveTargetEffect,
animationDuration: `0.45s`,
animationTimingFunction: 'ease-in-out',
animationFillMode: 'backwards',
},
// ========================= Dots =========================
[prefixCls]: {
position: 'fixed',
pointerEvents: 'none',
zIndex: 999,
[`${prefixCls}-dot`]: {
boxSizing: 'border-box',
position: 'absolute',
borderRadius: '100%',
opacity: 0,
transform: 'translate(-50%, -50%)',
// Start Position
zIndex: 999,
// =================== Basic Motion ===================
'&.happy-in-out': {
animationName: antWaveDotEffect,
animationDuration: motionDurationSlow,
animationTimingFunction: 'linear',
animationFillMode: 'backwards',
},
},
},
},
];
});
}

253
components/theme/themesToken/happy/wave.tsx

@ -0,0 +1,253 @@
import { TinyColor } from '@ctrl/tinycolor';
import classNames from 'classnames';
import { CSSMotionList } from 'rc-motion';
import { render, unmount } from 'rc-util/lib/React/render';
import raf from 'rc-util/lib/raf';
import * as React from 'react';
import type { ConfigProviderProps } from '../../../config-provider';
import useWaveStyle, { TARGET_ATTR } from './style/wave';
export type WaveRender = Required<Required<ConfigProviderProps>['wave']>['render'];
export type UseTokenType = Parameters<WaveRender>[0]['token'];
interface HappyWaveProps {
target: HTMLElement;
token: UseTokenType;
onFinish: VoidFunction;
}
const DOT_COUNT = 7;
const DOT_COUNT_LG = 10;
interface DotInfo {
key: number;
startSize: string;
endSize: string;
type: 'fill' | 'outlined';
color: string;
startX: string;
startY: string;
endX: string;
endY: string;
}
function inRange(x: number, y: number, left: number, top: number, right: number, bottom: number) {
return x >= left && x <= right && y >= top && y <= bottom;
}
function HappyWave({ target, token, onFinish }: HappyWaveProps) {
const prefixCls = 'happy-wave';
const dotPrefixCls = `${prefixCls}-dot`;
const [dots, setDots] = React.useState<DotInfo[] | null>(null);
const [left, setLeft] = React.useState(0);
const [top, setTop] = React.useState(0);
const [, globalToken, hashId] = token;
useWaveStyle(token);
// ========================= Dots =========================
React.useEffect(() => {
const id = raf(() => {
if (['-dangerous', '-error'].some((skipCls) => target.className.includes(skipCls))) {
return;
}
const rect = target.getBoundingClientRect();
const { width, height } = rect;
setLeft(rect.left + width / 2);
setTop(rect.top + height / 2);
setDots([]);
const minSize = Math.min(width, height);
const maxSize = Math.max(width, height);
const halfMinSize = minSize / 2;
const halfMaxSize = maxSize / 2;
const halfWidth = width / 2;
const halfHeight = height / 2;
const OFFSET_MIN = 15;
const OFFSET_MAX = 30;
const halfOffsetMinWidth = halfWidth + OFFSET_MIN;
const halfOffsetMinHeight = halfHeight + OFFSET_MIN;
const halfOffsetMaxWidth = halfWidth + OFFSET_MAX;
const halfOffsetMaxHeight = halfHeight + OFFSET_MAX;
const dotCount = minSize >= 20 ? DOT_COUNT_LG : DOT_COUNT;
// Delay to start dot motion
setTimeout(() => {
const offsetAngle = Math.random() * 360;
// Color
const { colorPrimary } = globalToken;
const colorHsv = new TinyColor(colorPrimary).toHsv();
colorHsv.h -= 30;
const colorPrimaryWeak = new TinyColor(colorHsv).toHexString();
setDots(
new Array(dotCount).fill(null).map((_, index) => {
const rotate: number = 360 / dotCount;
const randomAngle = offsetAngle + rotate * index;
// Get start XY (Which should align the rect edge)
let startX = 0;
let startY = 0;
for (let startDist = halfMinSize - 1; startDist <= halfMaxSize; startDist += 1) {
const x = Math.cos((randomAngle * Math.PI) / 180) * startDist;
const y = Math.sin((randomAngle * Math.PI) / 180) * startDist;
if (!inRange(x, y, -halfWidth, -halfHeight, halfWidth, halfHeight)) {
break;
}
startX = x;
startY = y;
}
// Get end XY
let endX = startX;
let endY = startY;
let endDist = halfMinSize;
const endHalfWidth =
Math.random() * (halfOffsetMaxWidth - halfOffsetMinWidth) + halfOffsetMinWidth;
const endHalfHeight =
Math.random() * (halfOffsetMaxHeight - halfOffsetMinHeight) + halfOffsetMinHeight;
do {
endX = Math.cos((randomAngle * Math.PI) / 180) * endDist;
endY = Math.sin((randomAngle * Math.PI) / 180) * endDist;
endDist += 1;
} while (
inRange(endX, endY, -endHalfWidth, -endHalfHeight, endHalfWidth, endHalfHeight)
);
let size = Math.random() * 3 + 3;
if (height >= 20) {
size = Math.random() * 4 + 6;
}
return {
key: index + 1,
startX: `${startX}px`,
startY: `${startY}px`,
endX: `${endX}px`,
endY: `${endY}px`,
startSize: `${size}px`,
endSize: `${Math.random() > 0.75 ? size : 0}px`,
type: Math.random() > 0.6 ? 'outlined' : 'fill',
color: Math.random() > 0.5 ? colorPrimary : colorPrimaryWeak,
};
}),
);
}, 50);
target.setAttribute(TARGET_ATTR, 'true');
});
return () => {
raf.cancel(id);
};
}, []);
// ======================== Clean =========================
React.useEffect(() => {
const id = setTimeout(() => {
target.removeAttribute(TARGET_ATTR);
onFinish();
}, 600);
return () => {
clearTimeout(id);
};
}, []);
// ======================== Render ========================
if (!dots) {
return null;
}
return (
<div
className={classNames(prefixCls, hashId)}
style={{
left,
top,
}}
>
<CSSMotionList component={false} keys={dots} motionAppear motionName="happy-in-out">
{({
className: motionCls,
style: motionStyle,
key,
startX,
startY,
endX,
endY,
startSize,
endSize,
type,
color,
}) => {
const name = `${dotPrefixCls}-${key}`;
const dotCls = classNames(dotPrefixCls, motionCls, name);
const dotStyle: Record<string, string> = {
[`--start-x`]: startX,
[`--start-y`]: startY,
[`--end-x`]: endX,
[`--end-y`]: endY,
[`--start-size`]: startSize,
[`--end-size`]: endSize,
};
if (type === 'fill') {
dotStyle['--background'] = color;
} else {
dotStyle['--border'] = `1px solid ${color}`;
}
return (
<div
className={dotCls}
style={{
...motionStyle,
...dotStyle,
}}
/>
);
}}
</CSSMotionList>
</div>
);
}
const renderWave: WaveRender = (props) => {
// Create holder
const holder = document.createElement('div');
holder.style.position = 'absolute';
holder.style.left = `0px`;
holder.style.top = `0px`;
document.body.appendChild(holder);
render(
<HappyWave
{...props}
onFinish={() => {
unmount(holder).then(() => {
holder.parentElement?.removeChild(holder);
});
}}
/>,
holder,
);
};
const Wave: ConfigProviderProps['wave'] = {
render: renderWave,
};
export default Wave;
Loading…
Cancel
Save