跳到主要内容

Tooltip

Tooltip 预览

规划中

浮动 Tooltip(跟随选中元素的悬浮操作条)目前尚未在 @ydesign/react-editor 中提供

当前版本所有元素操作都集中在顶部的 <Toolbar />。如果你想要"选中文字之后,在文字旁边弹出一个小工具条"这类 Figma/Canva 式的交互,本页说明当前可用的替代方案以及我们未来的计划。


为什么需要 Tooltip?

顶部 Toolbar 是一个水平长条,优点是可见性高、每一项的位置稳定;缺点是:

  • 离被操作的元素较远,视线要频繁上下移动
  • 小屏 / 多栏布局下横向空间紧张,被迫折叠或滚动
  • 对常用的"改颜色、改字号、微调一下大小"等高频操作不够轻量

浮动 Tooltip 的价值就在于:把高频操作直接浮在元素旁边,用完即收。

目前可用的替代方案

在 Ydesign 原生 Tooltip 发布前,你可以通过以下两种方式实现类似体验:

方案 1:自定义浮层监听 selection:changed

思路:用一个常驻的绝对定位 React 组件,监听 Editor 的选中事件 + 画布变换,计算元素在屏幕上的坐标,然后在该位置渲染自己的工具条。

import { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';

const FloatingTooltip = observer(({ store }) => {
const [rect, setRect] = useState<DOMRect | null>(null);
const el = store.selectedElements[0];

useEffect(() => {
if (!el || !store.editor) {
setRect(null);
return;
}

const canvas = store.editor.customCanvas.canvas;
const update = () => {
const bounding = el.getBoundingRect(); // Fabric API
const vpt = canvas.viewportTransform!;
const zoom = vpt[0];
setRect(new DOMRect(bounding.left * zoom + vpt[4], bounding.top * zoom + vpt[5], bounding.width * zoom, bounding.height * zoom));
};

update();
// 画布每次渲染之后同步位置
canvas.on('after:render', update);
return () => {
canvas.off('after:render', update);
};
}, [el, store.editor]);

if (!el || !rect) return null;

return (
<div
style={{
position: 'absolute',
left: rect.left,
top: rect.top - 48, // 元素上方 48px
zIndex: 100,
background: '#fff',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
borderRadius: 8,
padding: '4px 8px',
display: 'flex',
gap: 4,
}}
>
{/* 这里放你的高频操作:颜色、字号、对齐… */}
<button onClick={() => store.set({ fontWeight: 'bold' }, el)}>B</button>
<button onClick={() => store.set({ fontStyle: 'italic' }, el)}>I</button>
<input type="color" value={el.fill as string} onChange={e => store.set({ fill: e.target.value }, el)} />
</div>
);
});

// 在 Workspace 的父容器里和 <Workspace /> 并列渲染即可
<div style={{ position: 'relative', flex: 1 }}>
<Workspace store={store} />
<FloatingTooltip store={store} />
</div>;

💡 关键是 canvas.on('after:render', update) —— Fabric 每次重绘后都会触发,这保证了缩放、平移、画布尺寸变化时浮层会实时跟随

方案 2:借用 Antd 的 Popover 当成浮层容器

如果你只想在元素点击后弹出一个小窗(而不是一直跟随),直接复用 Antd 的 Popover

import { Popover } from 'antd';
import { observer } from 'mobx-react-lite';

const ElementPopover = observer(({ store, anchorRef, children }) => {
const visible = store.selectedElementsIds.length > 0;

return (
<Popover open={visible} placement="top" trigger="click" content={children} getPopupContainer={() => anchorRef.current}>
{/* 锚点元素 */}
<div ref={anchorRef} style={{ position: 'absolute' /* 坐标同上 */ }} />
</Popover>
);
});

这种方式开发成本最低,但动画和定位会比较"浏览器默认"。


规划中的原生 Tooltip 组件

我们计划引入一个官方 <Tooltip /> 组件,以插件式的方式挂到 <Workspace /> 旁,实现:

  • 自动定位:基于 Fabric getBoundingRect + 视口变换,实时跟随
  • 避让策略:靠近画布边缘时自动翻转到下方 / 侧边
  • 类型分化:和 Toolbar 一样,按选中元素类型切换内容(TextTooltip / ImageTooltip / …)
  • 可定制:通过 components prop 替换每种类型的 tooltip,或整体禁用
  • 多选合并:同时选中多个元素时显示 ManyTooltip

预期 API(草案)

import { Tooltip } from '@ydesign/react-editor/canvas/tooltip';
import Workspace from '@ydesign/react-editor/canvas/workspace';

<div style={{ position: 'relative', flex: 1 }}>
<Workspace store={store} />
<Tooltip store={store} />
</div>;

带类型定制与一键关闭:

const MyImageTooltip = ({ store, element }) => (
<div>
<button onClick={() => store.editor?.imageCropHandler.cropImg.onEnterCrop(element)}>裁剪</button>
</div>
);

// 只自定义图片类型;禁用文字浮层
<Tooltip
store={store}
components={{
image: MyImageTooltip,
textbox: () => null,
}}
/>;

路线图

  • M1 — 基础浮层容器 + 自动跟随(单选)
  • M2 — 内置 TextTooltip / ImageTooltip / FigureTooltip
  • M3 — 多选合并(ManyTooltip
  • M4 — 画布边界避让策略
  • M5 — 与顶部 <Toolbar /> 的"同步隐藏"配置(避免功能冗余)

有兴趣参与设计或内测,欢迎在 GitHub Discussions 留言。


与 Polotno Tooltip 的区别

Polotno 的 <Tooltip /> 组件基于 Konva + Blueprint 实现,和 Ydesign(Fabric + Antd)的底层完全不同,因此:

  • 坐标计算方式不一样:Polotno 用 Konva 节点 API;Ydesign 用 Fabric 的 getBoundingRect + viewportTransform
  • 类型替换粒度不一样:Polotno 可以深入到 TextFill 级别;Ydesign 预期按元素类型级别替换(与 Toolbar 对齐)
  • 无法 API 兼容迁移:Polotno 代码不能直接搬过来用

如果你正从 Polotno 迁移一个 tooltip 方案,建议先用上面的"方案 1"手动接,后续切换到原生 <Tooltip /> 时再重构。


下一步