mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 19:46:32 +08:00
refactor: move component into a single package (#898)
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { cloneElement, Children, forwardRef } from 'react';
|
||||
import { StyledButton } from './styles';
|
||||
|
||||
import { ButtonProps } from './interface';
|
||||
import { getSize } from './utils';
|
||||
import { Loading } from './Loading';
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
shape = 'default',
|
||||
icon,
|
||||
iconPosition = 'start',
|
||||
type = 'default',
|
||||
children,
|
||||
bold = false,
|
||||
loading = false,
|
||||
noBorder = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { iconSize } = getSize(size);
|
||||
|
||||
const iconElement =
|
||||
icon &&
|
||||
cloneElement(Children.only(icon), {
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
className: `affine-button-icon ${icon.props.className ?? ''}`,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
shape={shape}
|
||||
hoverBackground={hoverBackground}
|
||||
hoverColor={hoverColor}
|
||||
hoverStyle={hoverStyle}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
type={type}
|
||||
bold={bold}
|
||||
noBorder={noBorder}
|
||||
{...props}
|
||||
>
|
||||
{loading ? (
|
||||
<Loading type={type}></Loading>
|
||||
) : (
|
||||
<>
|
||||
{iconPosition === 'start' && iconElement}
|
||||
{children && <span>{children}</span>}
|
||||
{iconPosition === 'end' && iconElement}
|
||||
</>
|
||||
)}
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
HTMLAttributes,
|
||||
cloneElement,
|
||||
ReactElement,
|
||||
Children,
|
||||
CSSProperties,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
import { StyledIconButton } from './styles';
|
||||
|
||||
const SIZE_SMALL = 'small' as const;
|
||||
const SIZE_MIDDLE = 'middle' as const;
|
||||
const SIZE_NORMAL = 'normal' as const;
|
||||
// TODO: IconButton should merge into Button, but it has not been designed yet
|
||||
const SIZE_CONFIG = {
|
||||
[SIZE_SMALL]: {
|
||||
iconSize: 16,
|
||||
areaSize: 24,
|
||||
},
|
||||
[SIZE_MIDDLE]: {
|
||||
iconSize: 20,
|
||||
areaSize: 28,
|
||||
},
|
||||
[SIZE_NORMAL]: {
|
||||
iconSize: 24,
|
||||
areaSize: 32,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type IconButtonProps = {
|
||||
size?:
|
||||
| typeof SIZE_SMALL
|
||||
| typeof SIZE_MIDDLE
|
||||
| typeof SIZE_NORMAL
|
||||
| [number, number];
|
||||
iconSize?:
|
||||
| typeof SIZE_SMALL
|
||||
| typeof SIZE_MIDDLE
|
||||
| typeof SIZE_NORMAL
|
||||
| [number, number];
|
||||
disabled?: boolean;
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
|
||||
darker?: boolean;
|
||||
} & HTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
size = 'normal',
|
||||
iconSize = 'normal',
|
||||
disabled = false,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [width, height] = Array.isArray(size)
|
||||
? size
|
||||
: [SIZE_CONFIG[size]['areaSize'], SIZE_CONFIG[size]['areaSize']];
|
||||
const [iconWidth, iconHeight] = Array.isArray(iconSize)
|
||||
? iconSize
|
||||
: [SIZE_CONFIG[iconSize]['iconSize'], SIZE_CONFIG[iconSize]['iconSize']];
|
||||
|
||||
return (
|
||||
<StyledIconButton
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
width={width}
|
||||
height={height}
|
||||
borderRadius={iconWidth / 4}
|
||||
{...props}
|
||||
>
|
||||
{cloneElement(Children.only(children), {
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
})}
|
||||
</StyledIconButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
IconButton.displayName = 'IconButton';
|
||||
|
||||
export default IconButton;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { styled } from '../../styles';
|
||||
import { ButtonProps } from './interface';
|
||||
import { getButtonColors } from './utils';
|
||||
|
||||
export const LoadingContainer = styled('div')<Pick<ButtonProps, 'type'>>(
|
||||
({ theme, type = 'default' }) => {
|
||||
const { color } = getButtonColors(theme, type, false);
|
||||
return `
|
||||
margin: 0px auto;
|
||||
width: 38px;
|
||||
text-align: center;
|
||||
.load {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: ${color};
|
||||
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
/* Prevent first frame from flickering when animation starts */
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
.load1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.load2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0);
|
||||
} 40% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
export const Loading = ({ type }: Pick<ButtonProps, 'type'>) => {
|
||||
return (
|
||||
<LoadingContainer type={type} className="load-container">
|
||||
<div className="load load1"></div>
|
||||
<div className="load load2"></div>
|
||||
<div className="load"></div>
|
||||
</LoadingContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { cloneElement, Children, forwardRef } from 'react';
|
||||
import { StyledTextButton } from './styles';
|
||||
|
||||
import { ButtonProps } from './interface';
|
||||
import { getSize } from './utils';
|
||||
|
||||
export const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
shape = 'default',
|
||||
icon,
|
||||
type = 'default',
|
||||
children,
|
||||
bold = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { iconSize } = getSize(size);
|
||||
|
||||
return (
|
||||
<StyledTextButton
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
shape={shape}
|
||||
hoverBackground={hoverBackground}
|
||||
hoverColor={hoverColor}
|
||||
hoverStyle={hoverStyle}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
type={type}
|
||||
bold={bold}
|
||||
{...props}
|
||||
>
|
||||
{icon &&
|
||||
cloneElement(Children.only(icon), {
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
className: `affine-button-icon ${icon.props.className ?? ''}`,
|
||||
})}
|
||||
{children && <span>{children}</span>}
|
||||
</StyledTextButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
TextButton.displayName = 'TextButton';
|
||||
|
||||
export default TextButton;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './IconButton';
|
||||
export * from './Button';
|
||||
export * from './TextButton';
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
CSSProperties,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
|
||||
export const SIZE_SMALL = 'small' as const;
|
||||
export const SIZE_MIDDLE = 'middle' as const;
|
||||
export const SIZE_DEFAULT = 'default' as const;
|
||||
|
||||
export type ButtonProps = PropsWithChildren &
|
||||
Omit<HTMLAttributes<HTMLButtonElement>, 'type'> & {
|
||||
size?: typeof SIZE_SMALL | typeof SIZE_MIDDLE | typeof SIZE_DEFAULT;
|
||||
disabled?: boolean;
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: CSSProperties['color'];
|
||||
hoverStyle?: CSSProperties;
|
||||
icon?: ReactElement;
|
||||
iconPosition?: 'start' | 'end';
|
||||
shape?: 'default' | 'round' | 'circle';
|
||||
type?: 'primary' | 'light' | 'warning' | 'danger' | 'default';
|
||||
bold?: boolean;
|
||||
loading?: boolean;
|
||||
noBorder?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
import { absoluteCenter, displayInlineFlex, styled } from '../../styles';
|
||||
import { CSSProperties } from 'react';
|
||||
import { ButtonProps } from './interface';
|
||||
import { getSize, getButtonColors } from './utils';
|
||||
|
||||
export const StyledIconButton = styled('button', {
|
||||
shouldForwardProp: prop => {
|
||||
return ![
|
||||
'borderRadius',
|
||||
'top',
|
||||
'right',
|
||||
'width',
|
||||
'height',
|
||||
'hoverBackground',
|
||||
'hoverColor',
|
||||
'hoverStyle',
|
||||
'darker',
|
||||
].includes(prop);
|
||||
},
|
||||
})<{
|
||||
width: number;
|
||||
height: number;
|
||||
borderRadius: number;
|
||||
disabled?: boolean;
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
// In some cases, button is in a normal hover status, it should be darkened
|
||||
darker?: boolean;
|
||||
}>(
|
||||
({
|
||||
theme,
|
||||
width,
|
||||
height,
|
||||
disabled,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
darker = false,
|
||||
}) => {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
color: theme.colors.iconColor,
|
||||
...displayInlineFlex('center', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
transition: 'background .15s',
|
||||
|
||||
// TODO: we need to add @emotion/babel-plugin
|
||||
'::after': {
|
||||
content: '""',
|
||||
width,
|
||||
height,
|
||||
borderRadius: width / 5,
|
||||
transition: 'background .15s',
|
||||
...absoluteCenter({ horizontal: true, vertical: true }),
|
||||
},
|
||||
|
||||
svg: {
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
color: hoverColor ?? theme.colors.primaryColor,
|
||||
'::after': {
|
||||
background:
|
||||
hoverBackground ?? darker
|
||||
? theme.colors.innerHoverBackground
|
||||
: theme.colors.hoverBackground,
|
||||
},
|
||||
...(hoverStyle ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledTextButton = styled('button', {
|
||||
shouldForwardProp: prop => {
|
||||
return ![
|
||||
'borderRadius',
|
||||
'top',
|
||||
'right',
|
||||
'width',
|
||||
'height',
|
||||
'hoverBackground',
|
||||
'hoverColor',
|
||||
'hoverStyle',
|
||||
'bold',
|
||||
].includes(prop);
|
||||
},
|
||||
})<
|
||||
Pick<
|
||||
ButtonProps,
|
||||
| 'size'
|
||||
| 'disabled'
|
||||
| 'hoverBackground'
|
||||
| 'hoverColor'
|
||||
| 'hoverStyle'
|
||||
| 'shape'
|
||||
| 'type'
|
||||
| 'bold'
|
||||
>
|
||||
>(
|
||||
({
|
||||
theme,
|
||||
size = 'default',
|
||||
disabled,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
bold = false,
|
||||
shape = 'default',
|
||||
// TODO: Implement type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type = 'default',
|
||||
}) => {
|
||||
const { fontSize, borderRadius, padding, height } = getSize(size);
|
||||
console.log('size', size, height);
|
||||
|
||||
return {
|
||||
height,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding,
|
||||
...displayInlineFlex('flex-start', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
transition: 'background .15s',
|
||||
// TODO: Implement circle shape
|
||||
borderRadius: shape === 'default' ? borderRadius : height / 2,
|
||||
fontSize,
|
||||
fontWeight: bold ? '500' : '400',
|
||||
|
||||
':hover': {
|
||||
color: hoverColor ?? theme.colors.primaryColor,
|
||||
background: hoverBackground ?? theme.colors.hoverBackground,
|
||||
...(hoverStyle ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledButton = styled('button', {
|
||||
shouldForwardProp: prop => {
|
||||
return ![
|
||||
'hoverBackground',
|
||||
'shape',
|
||||
'hoverColor',
|
||||
'hoverStyle',
|
||||
'type',
|
||||
'bold',
|
||||
'noBorder',
|
||||
].includes(prop);
|
||||
},
|
||||
})<
|
||||
Pick<
|
||||
ButtonProps,
|
||||
| 'size'
|
||||
| 'disabled'
|
||||
| 'hoverBackground'
|
||||
| 'hoverColor'
|
||||
| 'hoverStyle'
|
||||
| 'shape'
|
||||
| 'type'
|
||||
| 'bold'
|
||||
| 'noBorder'
|
||||
>
|
||||
>(
|
||||
({
|
||||
theme,
|
||||
size = 'default',
|
||||
disabled,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
bold = false,
|
||||
shape = 'default',
|
||||
type = 'default',
|
||||
noBorder = false,
|
||||
}) => {
|
||||
const { fontSize, borderRadius, padding, height } = getSize(size);
|
||||
|
||||
return {
|
||||
height,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding,
|
||||
border: noBorder ? 'none' : '1px solid',
|
||||
...displayInlineFlex('center', 'center'),
|
||||
position: 'relative',
|
||||
// TODO: disabled color is not decided
|
||||
...(disabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none',
|
||||
color: theme.colors.borderColor,
|
||||
}
|
||||
: {}),
|
||||
transition: 'background .15s',
|
||||
// TODO: Implement circle shape
|
||||
borderRadius: shape === 'default' ? borderRadius : height / 2,
|
||||
fontSize,
|
||||
fontWeight: bold ? '500' : '400',
|
||||
'.affine-button-icon': {
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
'.affine-button-icon__fixed': {
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
'>span': {
|
||||
marginLeft: '5px',
|
||||
width: '100%',
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
...getButtonColors(theme, type, {
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
}),
|
||||
|
||||
// TODO: disabled hover should be implemented
|
||||
//
|
||||
// ':hover': {
|
||||
// color: hoverColor ?? theme.colors.primaryColor,
|
||||
// background: hoverBackground ?? theme.colors.hoverBackground,
|
||||
// '.affine-button-icon':{
|
||||
//
|
||||
// }
|
||||
// ...(hoverStyle ?? {}),
|
||||
// },
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,117 @@
|
||||
import { AffineTheme } from '../../styles';
|
||||
import {
|
||||
SIZE_SMALL,
|
||||
SIZE_MIDDLE,
|
||||
SIZE_DEFAULT,
|
||||
ButtonProps,
|
||||
} from './interface';
|
||||
|
||||
// TODO: Designer is not sure about the size, Now, is just use default size
|
||||
export const SIZE_CONFIG = {
|
||||
[SIZE_SMALL]: {
|
||||
iconSize: 16,
|
||||
fontSize: 16,
|
||||
borderRadius: 6,
|
||||
height: 26,
|
||||
padding: 24,
|
||||
},
|
||||
[SIZE_MIDDLE]: {
|
||||
iconSize: 20,
|
||||
fontSize: 16,
|
||||
borderRadius: 6,
|
||||
height: 32,
|
||||
padding: 24,
|
||||
},
|
||||
[SIZE_DEFAULT]: {
|
||||
iconSize: 24,
|
||||
fontSize: 16,
|
||||
height: 38,
|
||||
padding: 24,
|
||||
borderRadius: 6,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const getSize = (
|
||||
size: typeof SIZE_SMALL | typeof SIZE_MIDDLE | typeof SIZE_DEFAULT
|
||||
) => {
|
||||
return SIZE_CONFIG[size];
|
||||
};
|
||||
|
||||
export const getButtonColors = (
|
||||
theme: AffineTheme,
|
||||
type: ButtonProps['type'],
|
||||
disabled: boolean,
|
||||
extend?: {
|
||||
hoverBackground: ButtonProps['hoverBackground'];
|
||||
hoverColor: ButtonProps['hoverColor'];
|
||||
hoverStyle: ButtonProps['hoverStyle'];
|
||||
}
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
return {
|
||||
background: theme.colors.primaryColor,
|
||||
color: '#fff',
|
||||
borderColor: theme.colors.primaryColor,
|
||||
'.affine-button-icon': {
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
case 'light':
|
||||
return {
|
||||
background: theme.colors.hoverBackground,
|
||||
color: disabled ? theme.colors.disableColor : theme.colors.primaryColor,
|
||||
borderColor: theme.colors.hoverBackground,
|
||||
'.affine-button-icon': {
|
||||
borderColor: theme.colors.primaryColor,
|
||||
},
|
||||
':hover': {
|
||||
borderColor: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
background: theme.colors.warningBackground,
|
||||
color: theme.colors.warningColor,
|
||||
borderColor: theme.colors.warningBackground,
|
||||
'.affine-button-icon': {
|
||||
color: theme.colors.warningColor,
|
||||
},
|
||||
':hover': {
|
||||
borderColor: theme.colors.warningColor,
|
||||
color: extend?.hoverColor,
|
||||
background: extend?.hoverBackground,
|
||||
...extend?.hoverStyle,
|
||||
},
|
||||
};
|
||||
case 'danger':
|
||||
return {
|
||||
background: theme.colors.errorBackground,
|
||||
color: theme.colors.errorColor,
|
||||
borderColor: theme.colors.errorBackground,
|
||||
'.affine-button-icon': {
|
||||
color: theme.colors.errorColor,
|
||||
},
|
||||
':hover': {
|
||||
borderColor: theme.colors.errorColor,
|
||||
color: extend?.hoverColor,
|
||||
background: extend?.hoverBackground,
|
||||
...extend?.hoverStyle,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: theme.colors.popoverColor,
|
||||
borderColor: theme.colors.borderColor,
|
||||
':hover': {
|
||||
borderColor: theme.colors.primaryColor,
|
||||
color: extend?.hoverColor ?? theme.colors.primaryColor,
|
||||
'.affine-button-icon': {
|
||||
color: extend?.hoverColor ?? theme.colors.primaryColor,
|
||||
background: extend?.hoverBackground,
|
||||
...extend?.hoverStyle,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user