Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

为 Bubble 和 BubbleList 组件的 footer 属性增加可控的 hover 样式 #489

Open
chenluda opened this issue Jan 21, 2025 · 6 comments
Assignees
Labels
enhancement New feature or request

Comments

@chenluda
Copy link

需求动机

问题描述

  1. Bubble 组件中实现 footer 的 hover 效果。虽然可以直接使用 CSS 样式来实现,但在 BubbleList 组件中,使用 CSS 样式可能会导致样式污染。因此,希望在 BubbleBubbleList 组件中增加一个属性,以便能够更灵活地控制 footer 的 hover 样式;
  2. Bubble 组件中 footer 建议默认在 typeingloading 完后再加载。

解决方案

建议在 BubbleBubbleList 组件中增加属性,允许自定义 footer 的 hover 样式。

预期效果

  • Bubble 组件中,footer 部分能够根据 footerDisplayMode 属性设置显示 hover 效果。
  • BubbleList 组件中,可以根据 hoverableBubbleFooter 属性设置符合条件的 Bubble,其 footer 部分的 hover 效果。

Image

提议的 API 是什么样的?

Bubble:

属性 说明 类型 默认值
footerDisplayMode 设置气泡 footer hover 属性 always/hover always
  • bubble/Bubble.js
const footerClassName = classnames(`${prefixCls}-footer`, contextConfig.classNames.footer, classNames.footer, {
  [`${prefixCls}-footer-hover`]: footerDisplayMode === 'hover', 
});
  • bubble/style/index.js
[`& ${componentCls}-footer-hover`]: {
  opacity: 0,
  transition: 'opacity 0.3s ease',
},
[`&:hover ${componentCls}-footer-hover`]: {
  opacity: 1, 
},

Bubble.List:

属性 说明 类型 默认值
hoverableBubbleFooter 设置对话列表中气泡 hover 属性 latest/all/none/与 role 匹配 none
  • bubble/BubbleList.js
const getFooterDisplayMode = (index, role) => {
  if (hoverableBubbleFooter === 'latest') {
    return index === displayData.length - 1 ? 'always' : 'hover';
  } else if (hoverableBubbleFooter === 'all') {
    return 'hover';
  }

  if (roles && roles[hoverableBubbleFooter]) {
    return role === hoverableBubbleFooter ? 'hover' : 'always';
  }

  return 'always';
};
@YumoImer
Copy link
Collaborator

非常感谢!我看了后有几个点在犹豫:

  1. header 其实也有类似的场景,比如「hover 后展示消息时间」这样的场景,应该被一起考虑进去
  2. 后续我们会做移动支持,会有 tap 这样的触发方式
  3. 命名的方式尽可能参考 antd 已有的 API 设计

@chenluda
Copy link
Author

感谢回复,视野局限于当前需求,确实没有考虑全面。

目前思考了两点:

  1. 个人觉得 header footer 、特别是未来可能新增的 extra 区域,都是需要添加添加不同的触发事件来控制内容的显示与隐藏的。
  2. 时间戳可以单独做个组件,网页端还可以配合着锚点来做链接。

我这边尝试实现了个版本:

1:首先实现 Bubble 组件 Semantic DOM 基本的触发操作。

1.1 定义 SemanticContentProps

  • 在 interface.d.ts 中,定义了 SemanticTrigger 和 SemanticContentProps,用于描述触发行为和内容属性:

interface.d.ts

...
export type SemanticTrigger='never'|'always'|'hover'|'click';
export interface SemanticContentProps {
    /**
     * @default always
     */
    trigger?: SemanticTrigger;
    content: React.ReactNode;
}
...
export interface BubbleProps<ContentType extends BubbleContentType=string> extends Omit<React.HTMLAttributes<HTMLDivElement>,'content'> {
    prefixCls?: string;
    ...
    header?: SemanticContentProps|React.ReactNode;
    footer?: SemanticContentProps|React.ReactNode;
}

1.2 实现 useSemanticContent Hook

  • 通过 useSemanticContent Hook,统一处理 header 和 footer 的渲染逻辑和样式类名:
/**
 * Return semantic content rendering logic and classNames.
 */
const useSemanticContent = (semanticProps, type, prefixCls, disableTrigger = false) => {
  const isSemanticProps = typeof semanticProps === 'object' && semanticProps !== null && 'trigger' in semanticProps;

  const trigger = disableTrigger ? undefined : isSemanticProps ? semanticProps.trigger : 'always';
  const content = isSemanticProps ? semanticProps.content : semanticProps;

  const resolvedContent = trigger === 'never' ? null : content;

  const className = classnames(`${prefixCls}-${type}`, {
    [`${prefixCls}-${type}-hover`]: trigger === 'hover',
    [`${prefixCls}-${type}-click`]: trigger === 'click',
    [`${prefixCls}-${type}-visible`]: trigger === 'always',
  });

  return {
    content: resolvedContent,
    className,
    trigger,
  };
};

export default useSemanticContent;

2:给 BubbleList 组件新增 triggerRange 接口,动态控制列表中不同气泡 Semantic DOM 的触发行为。

triggerRange 用于控制 Bubble 组件中 Semantic DOM 的触发行为是否失效(即强制变为 always)。

  • 「triggerRange = 'latest'」 表示最新气泡中的 Semantic DOM 的 trigger 失效,强制变为 always。其他气泡的 trigger 行为保持不变。

  • 「triggerRange = 'all'」 表示所有气泡中的 Semantic DOM 的 trigger 失效,强制变为 always。

  • 「triggerRange 与 roles 匹配」 如果 triggerRange 的值与某个 role 匹配(即 triggerRange === bubble.role),则匹配上的气泡中的 Semantic DOM 的 trigger 失效,强制变为 always。其他气泡的 trigger 行为保持不变。

  • 「triggerRange 未匹配任何 role」 如果 triggerRange 的值未与任何 role 匹配,则所有气泡中的 Semantic DOM 的 trigger 行为正常生效(即按照 SemanticContentProps 中定义的 trigger 行为执行)。

hook/useListData.js

const shouldDisableTrigger = (triggerRange, isLatest, roleMatch) => {
  if (triggerRange === 'latest') {
    return isLatest; // 仅最新气泡的 trigger 失效
  }
  if (triggerRange === 'all') {
    return true; // 所有气泡的 trigger 失效
  }
  if (triggerRange) {
    return roleMatch; // 匹配 role 的气泡的 trigger 失效
  }
  return false; // 默认情况下,trigger 行为正常生效
};

这样组合起来有很多玩法:

  • 最新气泡的 Semantic DOM 的 trigger 失效,强制显示;

  • 历史气泡的 trigger 行为保持不变。

1:历史气泡的 header 不显示,footer hover 触发

<Bubble.List
  roles={roles}
  triggerRange="latest"
  items={messages.map((message, index) => {
    return {
      header: {
        trigger: "never",
        content: headerContent
      },

      footer: {
        trigger: "hover",
        content: footerContent
        ),
      },
      key: index,
      role: message.isBot ? "bot" : "user",
      content: message.text,
      loading: loading,
    };
  })}
/>

Image

2:历史气泡的 header 显示,footer click 触发

<Bubble.List
  roles={roles}
  triggerRange="latest"
  items={messages.map((message, index) => {
    return {
      header: headerContent,

      footer: {
        trigger: "hover",
        content: footerContent
        ),
      },
      key: index,
      role: message.isBot ? "bot" : "user",
      content: message.text,
      loading: loading,
    };
  })}
/>

Image

@YumoImer
Copy link
Collaborator

个人觉得 header 、 footer 、特别是未来可能新增的 #437 区域,都是需要添加添加不同的触发事件来控制内容的显示与隐藏的。

赞!extra 这点确实我没想到~

@YumoImer
Copy link
Collaborator

2:给 BubbleList 组件新增 triggerRange 接口,动态控制列表中不同气泡 Semantic DOM 的触发行为。

这点我有个想法:不新增 triggerRange 参数,通过已有属性 roles 扩展来支持:

// before
export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType) => RoleType);
// now
export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType, index: number) => RoleType);

这样还可以顺手把 #475 支持了,😄

你觉得如何?

@chenluda
Copy link
Author

triggerRange 参数确实冗余,删了后给 roles 属性扩展 index,玩法更丰富了。

1:给 roles 属性扩展 index

  • BubbleList.d.ts
#17 export type RolesType = Record<string, RoleType> | ((bubbleDataP: BubbleDataType, index: number) => RoleType);
  • hooks/useListData.js
export default function useListData(items, roles) {
  const getRoleBubbleProps = React.useCallback((bubble, index) => {
    if (typeof roles === 'function') {
      return roles(bubble, index);
    }
    if (roles) {
      return roles[bubble.role] || {};
    }
    return {};
  }, [roles]);
  return React.useMemo(() => (items || []).map((bubbleData, i) => {
    const mergedKey = bubbleData.key ?? `preset_${i}`;
    return {
      ...getRoleBubbleProps(bubbleData, i),
      ...bubbleData,
      key: mergedKey
    };
  }), [items, getRoleBubbleProps]);
}

2:最新气泡的 Semantic DOM 正常显示,历史气泡按 trigger 触发

const roles = (bubble, index) => {
  const isLatest = index == (messages.length - 1).toString();
  switch (bubble.role) {
    case "bot":
      return {
        placement: "start",
        avatar: { icon: <UserOutlined />, style: fooAvatar },
        typing: { step: 2, interval: 50 },
        header: {
          trigger: isLatest ? "always" : "never",
          content: botHeaderContent
        },
        footer: {
          trigger: isLatest ? "always" : "hover",
          content: footerHeaderContent
        },
      };
    case "user":
      return {
        placement: "end",
        avatar: { icon: <UserOutlined />, style: barAvatar },
        header: {
          trigger: "never",
          content: userHeaderContent
        },
        footer: {
          trigger: "hover",
          content: userFooterContent
        },
      };
  }
};
<Bubble.List
  ref={listRef}
  className="bubble-list"
  roles={roles}
  items={messages.map((msg, index) => ({
    key: index,
    role: msg.role,
    content: msg.text,
    time: msg.time,
  }))}
/>

Image

@YumoImer
Copy link
Collaborator

看起来没什么问题了,RFC 之吧,这个 issue 我指派给你,感谢参与!

@YumoImer YumoImer added the enhancement New feature or request label Jan 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants