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

[Feat]支持 Sender 组件 content 自定义渲染,以支持类似的 speech 交互变体需求或者支持其他的content自定义渲染需求 #354

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions components/sender/demo/CustomContent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

通过 `content` 自定义内容组件,从而实现自定义内容渲染。

## en-US

Customize the content component to achieve custom content rendering.
149 changes: 149 additions & 0 deletions components/sender/demo/CustomContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Sender } from '@ant-design/x';
import { App } from 'antd';
import React, { useState } from 'react';
import styled, { keyframes } from 'styled-components';

interface CustomContentProps {
innerValue?: string;
onValueChange?: (newValue: string) => void;
onSubmit?: () => void;
loading?: boolean;
disabled?: boolean;
}

const waveAnimation = keyframes`
0% {
transform: scaleY(0.5);
}
50% {
transform: scaleY(1.2);
}
100% {
transform: scaleY(0.5);
}
`;

const AudioWave = styled.div`
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
`;

const WaveBar = styled.div<{ delay: number }>`
background-color: white;
width: 6px;
height: 24px;
border-radius: 4px;
animation: ${waveAnimation} 1s ease-in-out infinite;
animation-delay: ${({ delay }) => delay}s;
`;

const RecordingText = styled.div`
font-size: 14px;
color: white;
margin-top: 8px;
text-align: center;
`;

const CustomSpeechBox = styled.div<{ recording: boolean }>`
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
width: 100%;
height: 50px;
background: linear-gradient(180deg, #f7f7f7, #e5e5e5);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;

${({ recording }) =>
recording &&
`
background: linear-gradient(180deg, #d3e5ff, #b3d4ff);
`}
`;

const SpeechContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;

const Dots = styled.div`
font-size: 18px;
color: black;
font-weight: bold;
text-align: center;
`;

const SpeechContentText = styled.p`
margin: 4px 0 0;
font-size: 14px;
color: black;
font-weight: 500;
text-align: center;
`;

const Demo: React.FC = () => {
const { message } = App.useApp();
const [recording, setRecording] = useState(false);
const [value, setValue] = useState<string>('');

return (
<Sender
value={value}
components={{
content: ({ onValueChange, onSubmit }: CustomContentProps) => (
<CustomSpeechBox
recording={recording}
onClick={() => {
if (recording) {
message.success('录音结束');
onValueChange?.('');
onSubmit?.();
} else {
message.info('开始录音');
onValueChange?.('录音内容...');
}
setRecording(!recording);
}}
>
<SpeechContent>
{recording ? (
<div style={{ textAlign: 'center' }}>
<AudioWave>
<WaveBar delay={0} />
<WaveBar delay={0.2} />
<WaveBar delay={0.4} />
<WaveBar delay={0.6} />
<WaveBar delay={0.8} />
</AudioWave>
<RecordingText>正在听...</RecordingText>
</div>
) : (
<>
<Dots>· · · ·</Dots>
<SpeechContentText>可以开始说话了</SpeechContentText>
</>
)}
</SpeechContent>
</CustomSpeechBox>
),
}}
actions={() => null}
onChange={setValue}
onSubmit={() => {
message.success('语音消息已发送!');
}}
/>
);
};
Comment on lines +90 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

需要增强错误处理和可访问性

主要问题:

  1. 缺少错误处理机制
  2. 缺少键盘操作支持
  3. 缺少适当的 ARIA 属性
  4. 缺少加载和禁用状态的处理

建议进行以下改进:

 const Demo: React.FC = () => {
   const { message } = App.useApp();
   const [recording, setRecording] = useState(false);
   const [value, setValue] = useState<string>('');
+  const [error, setError] = useState<string | null>(null);

   return (
     <Sender
       value={value}
       components={{
         content: ({ onValueChange, onSubmit, loading, disabled }: CustomContentProps) => (
           <CustomSpeechBox
             recording={recording}
+            role="button"
+            tabIndex={0}
+            aria-label={recording ? "正在录音" : "点击开始录音"}
+            aria-disabled={disabled}
+            disabled={disabled}
+            onKeyDown={(e) => {
+              if (e.key === 'Enter' || e.key === ' ') {
+                e.preventDefault();
+                if (!disabled && !loading) {
+                  // 触发点击事件
+                  e.currentTarget.click();
+                }
+              }
+            }}
             onClick={() => {
+              if (disabled || loading) return;
+
               if (recording) {
                 message.success('录音结束');
                 onValueChange?.('');
                 onSubmit?.();
               } else {
                 message.info('开始录音');
                 onValueChange?.('录音内容...');
               }
               setRecording(!recording);
             }}
           >
             <SpeechContent>
               {loading ? (
+                <LoadingIndicator />
+              ) : disabled ? (
+                <DisabledState />
+              ) : recording ? (
                 // ... 现有的录音状态UI
               ) : (
                 // ... 现有的非录音状态UI
               )}
             </SpeechContent>
           </CustomSpeechBox>
         ),
       }}
       actions={() => null}
       onChange={setValue}
       onSubmit={() => {
         message.success('语音消息已发送!');
       }}
     />
   );
 };

Committable suggestion skipped: line range outside the PR's diff.


export default () => (
<App>
<Demo />
</App>
);
1 change: 1 addition & 0 deletions components/sender/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/speech.tsx">Speech input</code>
<code src="./demo/speech-custom.tsx">Custom speech input</code>
<code src="./demo/actions.tsx">Custom actions</code>
<code src="./demo/CustomContent.tsx">Custom content</code>
<code src="./demo/header.tsx">Header panel</code>
<code src="./demo/header-fixed.tsx">Reference</code>
<code src="./demo/send-style.tsx">Adjust style</code>
Expand Down
39 changes: 19 additions & 20 deletions components/sender/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type TextareaProps = GetProps<typeof Input.TextArea>;

export interface SenderComponents {
input?: CustomizeComponent<TextareaProps>;
/**Sender Content CustomizeComponent*/
content?: CustomizeComponent;
}

export type ActionsRender = (
Expand Down Expand Up @@ -261,6 +263,9 @@ function Sender(props: SenderProps, ref: React.Ref<HTMLDivElement>) {
} else if (actions) {
actionNode = actions;
}
// ============================ CustomContent ============================
// 获取自定义的CustomContent组件(默认使用一个空的<div/>占位)
const CustomContent = getComponent(components, ['content'], () => <div />);

// ============================ Render ============================
return wrapCSSVar(
Expand Down Expand Up @@ -289,26 +294,20 @@ function Sender(props: SenderProps, ref: React.Ref<HTMLDivElement>) {
</div>
)}

{/* Input */}
<InputTextArea
{...inputProps}
disabled={disabled}
style={{ ...contextConfig.styles.input, ...styles.input }}
className={classnames(inputCls, contextConfig.classNames.input, classNames.input)}
autoSize={{ maxRows: 8 }}
value={innerValue}
onChange={(e) => {
triggerValueChange((e.target as HTMLTextAreaElement).value);
triggerSpeech(true);
}}
onPressEnter={onInternalKeyPress}
onCompositionStart={onInternalCompositionStart}
onCompositionEnd={onInternalCompositionEnd}
onKeyDown={onKeyDown}
onPaste={onInternalPaste}
variant="borderless"
readOnly={readOnly}
/>
{/* 自定义 content 渲染 */}
{CustomContent ? (
<CustomContent />
) : (
<InputTextArea
{...inputProps}
disabled={disabled}
value={innerValue}
onChange={(e) => triggerValueChange((e.target as HTMLTextAreaElement).value)}
onPressEnter={onInternalKeyPress}
variant="borderless"
readOnly={readOnly}
/>
)}
Comment on lines +297 to +310
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

建议:改进条件渲染逻辑

当前的条件渲染逻辑可以更加简洁,同时建议添加错误边界处理。

-        {/* 自定义 content 渲染 */}
-        {CustomContent ? (
-          <CustomContent />
-        ) : (
-          <InputTextArea
-            {...inputProps}
-            disabled={disabled}
-            value={innerValue}
-            onChange={(e) => triggerValueChange((e.target as HTMLTextAreaElement).value)}
-            onPressEnter={onInternalKeyPress}
-            variant="borderless"
-            readOnly={readOnly}
-          />
-        )}
+        {/* 自定义 content 渲染 */}
+        <React.Suspense fallback={null}>
+          {CustomContent ? (
+            <ErrorBoundary>
+              <CustomContent />
+            </ErrorBoundary>
+          ) : (
+            <InputTextArea
+              {...inputProps}
+              disabled={disabled}
+              value={innerValue}
+              onChange={(e) => triggerValueChange((e.target as HTMLTextAreaElement).value)}
+              onPressEnter={onInternalKeyPress}
+              variant="borderless"
+              readOnly={readOnly}
+            />
+          )}
+        </React.Suspense>

Committable suggestion skipped: line range outside the PR's diff.


{/* Action List */}
<div
Expand Down
1 change: 1 addition & 0 deletions components/sender/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
<code src="./demo/speech.tsx">语音输入</code>
<code src="./demo/speech-custom.tsx">自定义语音输入</code>
<code src="./demo/actions.tsx">自定义按钮</code>
<code src="./demo/CustomContent.tsx">自定义内容</code>
<code src="./demo/header.tsx">展开面板</code>
<code src="./demo/header-fixed.tsx">引用</code>
<code src="./demo/send-style.tsx">调整样式</code>
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
"@babel/runtime": "^7.25.6",
"classnames": "^2.5.1",
"rc-motion": "^2.9.2",
"rc-util": "^5.43.0"
"rc-util": "^5.43.0",
"styled-components": "^6.1.13"
},
"devDependencies": {
"@ant-design/tools": "^18.0.2",
Expand Down Expand Up @@ -150,6 +151,7 @@
"@types/react-resizable": "^3.0.8",
"@types/semver": "^7.5.8",
"@types/spinnies": "^0.5.3",
"@types/styled-components": "^5.1.34",
"@types/tar": "^6.1.13",
"@types/throttle-debounce": "^5.0.2",
"@types/warning": "^3.0.3",
Expand Down
Loading