diff --git a/components/_util/__tests__/hooks.test.tsx b/components/_util/__tests__/hooks.test.tsx
new file mode 100644
index 00000000..adda89f3
--- /dev/null
+++ b/components/_util/__tests__/hooks.test.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { render } from '../../../tests/utils';
+import useProxyImperativeHandle from '../hooks/use-proxy-imperative-handle';
+
+const TestComponent = React.forwardRef((_, ref) => {
+ const divRef = React.useRef(null);
+ useProxyImperativeHandle(ref, () => ({
+ nativeElement: divRef.current!,
+ testMethod: () => 'testMethod called',
+ testProperty: 'testProperty',
+ }));
+ return
Hello World!
;
+});
+
+describe('useProxyImperativeHandle', () => {
+ it('should correctly proxy the nativeElement and init methods', () => {
+ const ref = React.createRef();
+ render();
+ expect(ref.current).toBeDefined();
+ expect(ref.current?.testMethod()).toBe('testMethod called');
+ expect(ref.current?.testProperty).toBe('testProperty');
+ expect(ref.current?.nativeElement).toBeDefined();
+ expect(ref.current?.focus === ref.current?.nativeElement.focus).toBeTruthy();
+ });
+});
diff --git a/components/_util/hooks/use-proxy-imperative-handle.ts b/components/_util/hooks/use-proxy-imperative-handle.ts
new file mode 100644
index 00000000..08143490
--- /dev/null
+++ b/components/_util/hooks/use-proxy-imperative-handle.ts
@@ -0,0 +1,25 @@
+// Proxy the dom ref with `{ nativeElement, otherFn }` type
+// ref: https://github.com/ant-design/ant-design/discussions/45242
+
+import { useImperativeHandle } from 'react';
+import type { Ref } from 'react';
+
+export default function useProxyImperativeHandle<
+ NativeELementType extends HTMLElement,
+ ReturnRefType extends { nativeElement: NativeELementType },
+>(ref: Ref | undefined, init: () => ReturnRefType) {
+ return useImperativeHandle(ref, () => {
+ const refObj = init();
+ const { nativeElement } = refObj;
+
+ return new Proxy(nativeElement, {
+ get(obj: any, prop: any) {
+ if ((refObj as any)[prop]) {
+ return (refObj as any)[prop];
+ }
+
+ return Reflect.get(obj, prop);
+ },
+ });
+ });
+}
diff --git a/components/attachments/demo/with-sender.tsx b/components/attachments/demo/with-sender.tsx
index 9cd70e6d..602db393 100644
--- a/components/attachments/demo/with-sender.tsx
+++ b/components/attachments/demo/with-sender.tsx
@@ -1,6 +1,6 @@
import { CloudUploadOutlined, LinkOutlined } from '@ant-design/icons';
import { Attachments, AttachmentsProps, Sender } from '@ant-design/x';
-import { App, Badge, Button, Flex, type GetProp, Typography } from 'antd';
+import { App, Badge, Button, Flex, type GetProp, type GetRef, Typography } from 'antd';
import React from 'react';
const Demo = () => {
@@ -10,7 +10,7 @@ const Demo = () => {
const { notification } = App.useApp();
- const senderRef = React.useRef(null);
+ const senderRef = React.useRef>(null);
const senderHeader = (
{
description: 'Click or drag files to this area to upload',
}
}
- getDropContainer={() => senderRef.current}
+ getDropContainer={() => senderRef.current?.nativeElement}
/>
);
diff --git a/components/sender/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/sender/__tests__/__snapshots__/demo-extend.test.ts.snap
index f049d9ca..7581ba0d 100644
--- a/components/sender/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/sender/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -297,6 +297,106 @@ exports[`renders components/sender/demo/basic.tsx extend context correctly 1`] =
exports[`renders components/sender/demo/basic.tsx extend context correctly 2`] = `[]`;
+exports[`renders components/sender/demo/focus.tsx extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders components/sender/demo/focus.tsx extend context correctly 2`] = `[]`;
+
exports[`renders components/sender/demo/header.tsx extend context correctly 1`] = `
`;
+exports[`renders components/sender/demo/focus.tsx correctly 1`] = `
+
+
+
+
+
+
+
+
+`;
+
exports[`renders components/sender/demo/header.tsx correctly 1`] = `
{
+ const senderRef = useRef
>(null);
+
+ const senderProps = {
+ defaultValue: 'Hello, welcome to use Ant Design X!',
+ ref: senderRef,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/components/sender/demo/paste-image.tsx b/components/sender/demo/paste-image.tsx
index ce2f5d00..b570c6ad 100644
--- a/components/sender/demo/paste-image.tsx
+++ b/components/sender/demo/paste-image.tsx
@@ -10,7 +10,7 @@ const Demo = () => {
const attachmentsRef = React.useRef>(null);
- const senderRef = React.useRef(null);
+ const senderRef = React.useRef>(null);
const senderHeader = (
{
description: 'Click or drag files to this area to upload',
}
}
- getDropContainer={() => senderRef.current}
+ getDropContainer={() => senderRef.current?.nativeElement}
/>
);
diff --git a/components/sender/index.en-US.md b/components/sender/index.en-US.md
index 422fdcbd..6d89ac45 100644
--- a/components/sender/index.en-US.md
+++ b/components/sender/index.en-US.md
@@ -25,6 +25,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
Reference
Adjust style
Paste image
+Focus
## API
@@ -61,6 +62,14 @@ type SpeechConfig = {
};
```
+### Sender Ref
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| nativeElement | Outer container | `HTMLDivElement` | - | - |
+| focus | Set focus | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | - | - |
+| blur | Remove focus | () => void | - | - |
+
### Sender.Header
| Property | Description | Type | Default | Version |
diff --git a/components/sender/index.tsx b/components/sender/index.tsx
index be3faf83..119a35a5 100644
--- a/components/sender/index.tsx
+++ b/components/sender/index.tsx
@@ -1,10 +1,10 @@
-import { type ButtonProps, Flex, type GetProps, Input } from 'antd';
+import { Flex, Input } from 'antd';
import classnames from 'classnames';
-
-import { useComposeRef, useMergedState } from 'rc-util';
+import { useMergedState } from 'rc-util';
import pickAttrs from 'rc-util/lib/pickAttrs';
import getValue from 'rc-util/lib/utils/get';
import React from 'react';
+import useProxyImperativeHandle from '../_util/hooks/use-proxy-imperative-handle';
import useXComponentConfig from '../_util/hooks/use-x-component-config';
import { useXProviderContext } from '../x-provider';
import SenderHeader, { SendHeaderContext } from './SenderHeader';
@@ -13,10 +13,12 @@ import ClearButton from './components/ClearButton';
import LoadingButton from './components/LoadingButton';
import SendButton from './components/SendButton';
import SpeechButton from './components/SpeechButton';
-import type { CustomizeComponent, SubmitType } from './interface';
import useStyle from './style';
import useSpeech, { type AllowSpeech } from './useSpeech';
+import type { InputRef as AntdInputRef, ButtonProps, GetProps } from 'antd';
+import type { CustomizeComponent, SubmitType } from './interface';
+
type TextareaProps = GetProps;
export interface SenderComponents {
@@ -71,6 +73,10 @@ export interface SenderProps extends Pick;
+
function getComponent(
components: SenderComponents | undefined,
path: string[],
@@ -79,7 +85,7 @@ function getComponent(
return getValue(components, path) || defaultComponent;
}
-function Sender(props: SenderProps, ref: React.Ref) {
+const ForwardSender = React.forwardRef((props, ref) => {
const {
prefixCls: customizePrefixCls,
styles = {},
@@ -114,9 +120,13 @@ function Sender(props: SenderProps, ref: React.Ref) {
// ============================= Refs =============================
const containerRef = React.useRef(null);
- const inputRef = React.useRef(null);
+ const inputRef = React.useRef(null);
- const mergedContainerRef = useComposeRef(ref, containerRef);
+ useProxyImperativeHandle(ref, () => ({
+ nativeElement: containerRef.current!,
+ focus: inputRef.current?.focus!,
+ blur: inputRef.current?.blur!,
+ }));
// ======================= Component Config =======================
const contextConfig = useXComponentConfig('sender');
@@ -267,11 +277,7 @@ function Sender(props: SenderProps, ref: React.Ref) {
// ============================ Render ============================
return wrapCSSVar(
-
+
{/* Header */}
{header && (
{header}
@@ -346,18 +352,18 @@ function Sender(props: SenderProps, ref: React.Ref) {
,
);
-}
+});
-const ForwardSender = React.forwardRef(Sender) as React.ForwardRefExoticComponent<
- SenderProps & React.RefAttributes
-> & {
+type CompoundedSender = typeof ForwardSender & {
Header: typeof SenderHeader;
};
+const Sender = ForwardSender as CompoundedSender;
+
if (process.env.NODE_ENV !== 'production') {
- ForwardSender.displayName = 'Sender';
+ Sender.displayName = 'Sender';
}
-ForwardSender.Header = SenderHeader;
+Sender.Header = SenderHeader;
-export default ForwardSender;
+export default Sender;
diff --git a/components/sender/index.zh-CN.md b/components/sender/index.zh-CN.md
index dfb521d8..299687d4 100644
--- a/components/sender/index.zh-CN.md
+++ b/components/sender/index.zh-CN.md
@@ -26,6 +26,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*cOfrS4fVkOMAAA
引用
调整样式
黏贴图片
+聚焦
## API
@@ -62,6 +63,14 @@ type SpeechConfig = {
};
```
+#### Sender Ref
+
+| 属性 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| nativeElement | 外层容器 | `HTMLDivElement` | - | - |
+| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | - | - |
+| blur | 取消焦点 | () => void | - | - |
+
### Sender.Header
| 属性 | 说明 | 类型 | 默认值 | 版本 |