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 | 属性 | 说明 | 类型 | 默认值 | 版本 |