6d964937db
补齐普通生图参考图来源菜单和画布选择流程 接入UI设计图与视频生成面板的提交链路 让生成引用上传目标支持多种生成面板 统一图片信息弹窗断言并补充相关测试 修复图标按钮浮层锚点ref与视频生成类型契约
157 lines
3.8 KiB
TypeScript
157 lines
3.8 KiB
TypeScript
import { forwardRef } from 'react';
|
|
import type {
|
|
ButtonHTMLAttributes,
|
|
HTMLAttributes,
|
|
KeyboardEvent,
|
|
LabelHTMLAttributes,
|
|
ReactNode,
|
|
Ref,
|
|
} from 'react';
|
|
|
|
type PlatformIconButtonBaseProps = {
|
|
label: string;
|
|
icon: ReactNode;
|
|
children?: ReactNode;
|
|
variant?: PlatformIconButtonVariant;
|
|
};
|
|
|
|
export type PlatformIconButtonVariant =
|
|
| 'platformIcon'
|
|
| 'surfaceFloating'
|
|
| 'darkMini';
|
|
|
|
type PlatformIconButtonButtonProps = Omit<
|
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
'aria-label' | 'children'
|
|
> &
|
|
PlatformIconButtonBaseProps & {
|
|
asChild?: false;
|
|
};
|
|
|
|
type PlatformIconButtonLabelProps = Omit<
|
|
LabelHTMLAttributes<HTMLLabelElement>,
|
|
'aria-label' | 'children'
|
|
> &
|
|
PlatformIconButtonBaseProps & {
|
|
asChild: 'label';
|
|
};
|
|
|
|
type PlatformIconButtonSpanButtonProps = Omit<
|
|
HTMLAttributes<HTMLSpanElement>,
|
|
'aria-label' | 'children' | 'role'
|
|
> &
|
|
PlatformIconButtonBaseProps & {
|
|
asChild: 'spanButton';
|
|
disabled?: boolean;
|
|
};
|
|
|
|
type PlatformIconButtonProps =
|
|
| PlatformIconButtonButtonProps
|
|
| PlatformIconButtonLabelProps
|
|
| PlatformIconButtonSpanButtonProps;
|
|
|
|
/**
|
|
* 平台通用图标动作按钮。
|
|
* 统一承接纯图标动作、图标上传 label 和带短标签的浮动图标动作。
|
|
*/
|
|
export const PlatformIconButton = forwardRef<
|
|
HTMLButtonElement | HTMLLabelElement | HTMLSpanElement,
|
|
PlatformIconButtonProps
|
|
>(function PlatformIconButton(
|
|
{
|
|
label,
|
|
icon,
|
|
children,
|
|
variant = 'platformIcon',
|
|
title,
|
|
className,
|
|
asChild,
|
|
...actionProps
|
|
},
|
|
ref,
|
|
) {
|
|
const variantClassName = {
|
|
platformIcon: 'platform-icon-button',
|
|
surfaceFloating:
|
|
'inline-flex items-center justify-center rounded-full border border-white/80 bg-white/94 text-[var(--platform-text-strong)] shadow-sm backdrop-blur transition hover:text-[var(--platform-accent)] disabled:cursor-not-allowed disabled:opacity-55',
|
|
darkMini:
|
|
'inline-flex items-center justify-center rounded-full border border-white/16 bg-black/55 text-white transition-colors hover:bg-black/70 disabled:cursor-not-allowed disabled:opacity-55',
|
|
}[variant];
|
|
|
|
const actionClassName = [variantClassName, className]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
|
|
if (asChild === 'label') {
|
|
return (
|
|
<label
|
|
{...(actionProps as LabelHTMLAttributes<HTMLLabelElement>)}
|
|
ref={ref as Ref<HTMLLabelElement>}
|
|
title={title}
|
|
className={actionClassName}
|
|
>
|
|
<span className="sr-only">{label}</span>
|
|
{icon}
|
|
{children}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
if (asChild === 'spanButton') {
|
|
const {
|
|
disabled,
|
|
onClick,
|
|
onKeyDown,
|
|
tabIndex,
|
|
...spanProps
|
|
} = actionProps as HTMLAttributes<HTMLSpanElement> & {
|
|
disabled?: boolean;
|
|
};
|
|
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => {
|
|
onKeyDown?.(event);
|
|
if (event.defaultPrevented || disabled) {
|
|
return;
|
|
}
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
event.currentTarget.click();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<span
|
|
{...spanProps}
|
|
ref={ref as Ref<HTMLSpanElement>}
|
|
aria-disabled={disabled}
|
|
aria-label={label}
|
|
className={actionClassName}
|
|
onClick={disabled ? undefined : onClick}
|
|
onKeyDown={handleKeyDown}
|
|
role="button"
|
|
tabIndex={disabled ? -1 : (tabIndex ?? 0)}
|
|
title={title}
|
|
>
|
|
{icon}
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const { type = 'button', ...buttonProps } =
|
|
actionProps as ButtonHTMLAttributes<HTMLButtonElement>;
|
|
|
|
return (
|
|
<button
|
|
{...buttonProps}
|
|
ref={ref as Ref<HTMLButtonElement>}
|
|
type={type}
|
|
aria-label={label}
|
|
title={title}
|
|
className={actionClassName}
|
|
>
|
|
{icon}
|
|
{children}
|
|
</button>
|
|
);
|
|
});
|