Skip to content

Commit 96819cd

Browse files
author
Marc Lundgren
committed
Refactor Composer component to use forwardRef and add focusSendBoxInput method; update type exports for ComposerRef
1 parent 84c85cf commit 96819cd

File tree

5 files changed

+142
-113
lines changed

5 files changed

+142
-113
lines changed

packages/bundle/src/FullComposer.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import { Components } from 'botframework-webchat-component';
22
import PropTypes from 'prop-types';
3-
import React from 'react';
3+
import React, { forwardRef } from 'react';
44

55
import AddFullBundle from './AddFullBundle';
66

77
import type { AddFullBundleProps } from './AddFullBundle';
8-
import type { ComposerProps } from 'botframework-webchat-component';
9-
import type { FC } from 'react';
8+
import type { ComposerProps, ComposerRef } from 'botframework-webchat-component';
109

1110
const { Composer } = Components;
1211

1312
type FullComposerProps = ComposerProps & AddFullBundleProps;
1413

15-
const FullComposer: FC<FullComposerProps> = props => (
14+
const FullComposer = forwardRef<ComposerRef, FullComposerProps>((props, ref) => (
1615
<AddFullBundle {...props}>
1716
{extraProps => (
18-
<Composer {...props} {...extraProps}>
17+
<Composer ref={ref} {...props} {...extraProps}>
1918
{/* We need to spread, thus, we cannot we destructuring assignment. */}
20-
{/* eslint-disable-next-line react/destructuring-assignment */}
2119
{props.children}
2220
</Composer>
2321
)}
2422
</AddFullBundle>
25-
);
23+
));
2624

2725
FullComposer.defaultProps = {
2826
...Composer.defaultProps,

packages/bundle/src/index-minimal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import ReactWebChat, {
1919
withEmoji
2020
} from 'botframework-webchat-component';
2121

22+
export type { ComposerRef } from 'botframework-webchat-component';
23+
2224
import addVersion from './addVersion';
2325
import coreRenderWebChat from './renderWebChat';
2426
import createBrowserWebSpeechPonyfillFactory from './createBrowserWebSpeechPonyfillFactory';

packages/bundle/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// window['WebChat'] is required for TypeScript
33

44
export * from './index-minimal';
5+
export type { ComposerRef } from './index-minimal';
56

67
import { Components as MinimalComponents, hooks, version, withEmoji } from './index-minimal';
78
import AdaptiveCardContent from './adaptiveCards/Attachment/AdaptiveCardContent';

packages/component/src/Composer.tsx

Lines changed: 132 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { singleToArray } from 'botframework-webchat-core';
44
import classNames from 'classnames';
55
import MarkdownIt from 'markdown-it';
66
import PropTypes from 'prop-types';
7-
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
7+
import React, { forwardRef, Fragment, memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
88
import { Composer as SayComposer } from 'react-say';
99
import createStyleSet from './Styles/createStyleSet';
1010

@@ -18,6 +18,7 @@ import {
1818
import UITracker from './hooks/internal/UITracker';
1919
import WebChatUIContext from './hooks/internal/WebChatUIContext';
2020
import useStyleSet from './hooks/useStyleSet';
21+
import useFocus from './hooks/useFocus';
2122
import createDefaultActivityMiddleware from './Middleware/Activity/createCoreMiddleware';
2223
import createDefaultActivityStatusMiddleware from './Middleware/ActivityStatus/createCoreMiddleware';
2324
import createDefaultAttachmentForScreenReaderMiddleware from './Middleware/AttachmentForScreenReader/createCoreMiddleware';
@@ -40,8 +41,28 @@ import type { ContextOf } from './types/ContextOf';
4041
import { type FocusSendBoxInit } from './types/internal/FocusSendBoxInit';
4142
import { type FocusTranscriptInit } from './types/internal/FocusTranscriptInit';
4243

44+
export type ComposerRef = {
45+
focusSendBoxInput: () => Promise<void>;
46+
};
47+
4348
const { useGetActivityByKey, useReferenceGrammarID, useStyleOptions } = hooks;
4449

50+
const ComposerWithRef = forwardRef<ComposerRef, { readonly children: ReactNode }>(({ children }, ref) => {
51+
const focus = useFocus();
52+
53+
useImperativeHandle(
54+
ref,
55+
() => ({
56+
focusSendBoxInput: async () => {
57+
await focus('sendBox');
58+
}
59+
}),
60+
[focus]
61+
);
62+
63+
return <Fragment>{children}</Fragment>;
64+
});
65+
4566
const node_env = process.env.node_env || process.env.NODE_ENV;
4667

4768
const emotionPool = {};
@@ -282,109 +303,116 @@ ComposerCore.propTypes = {
282303

283304
type ComposerProps = APIComposerProps & ComposerCoreProps;
284305

285-
const Composer: FC<ComposerProps> = ({
286-
activityMiddleware,
287-
activityStatusMiddleware,
288-
attachmentForScreenReaderMiddleware,
289-
attachmentMiddleware,
290-
avatarMiddleware,
291-
cardActionMiddleware,
292-
children,
293-
extraStyleSet,
294-
renderMarkdown,
295-
scrollToEndButtonMiddleware,
296-
styleSet,
297-
suggestedActionsAccessKey,
298-
toastMiddleware,
299-
typingIndicatorMiddleware,
300-
webSpeechPonyfillFactory,
301-
...composerProps
302-
}) => {
303-
const { nonce, onTelemetry } = composerProps;
304-
305-
const patchedActivityMiddleware = useMemo(
306-
() => [...singleToArray(activityMiddleware), ...createDefaultActivityMiddleware()],
307-
[activityMiddleware]
308-
);
309-
310-
const patchedActivityStatusMiddleware = useMemo(
311-
() => [...singleToArray(activityStatusMiddleware), ...createDefaultActivityStatusMiddleware()],
312-
[activityStatusMiddleware]
313-
);
314-
315-
const patchedAttachmentForScreenReaderMiddleware = useMemo(
316-
() => [
317-
...singleToArray(attachmentForScreenReaderMiddleware),
318-
...createDefaultAttachmentForScreenReaderMiddleware()
319-
],
320-
[attachmentForScreenReaderMiddleware]
321-
);
322-
323-
const patchedAttachmentMiddleware = useMemo(
324-
() => [...singleToArray(attachmentMiddleware), ...createDefaultAttachmentMiddleware()],
325-
[attachmentMiddleware]
326-
);
327-
328-
const patchedAvatarMiddleware = useMemo(
329-
() => [...singleToArray(avatarMiddleware), ...createDefaultAvatarMiddleware()],
330-
[avatarMiddleware]
331-
);
332-
333-
const patchedCardActionMiddleware = useMemo(
334-
() => [...singleToArray(cardActionMiddleware), ...createDefaultCardActionMiddleware()],
335-
[cardActionMiddleware]
336-
);
337-
338-
const patchedToastMiddleware = useMemo(
339-
() => [...singleToArray(toastMiddleware), ...createDefaultToastMiddleware()],
340-
[toastMiddleware]
341-
);
342-
343-
const patchedTypingIndicatorMiddleware = useMemo(
344-
() => [...singleToArray(typingIndicatorMiddleware), ...createDefaultTypingIndicatorMiddleware()],
345-
[typingIndicatorMiddleware]
346-
);
347-
348-
const defaultScrollToEndButtonMiddleware = useMemo(() => createDefaultScrollToEndButtonMiddleware(), []);
349-
350-
const patchedScrollToEndButtonMiddleware = useMemo(
351-
() => [...singleToArray(scrollToEndButtonMiddleware), ...defaultScrollToEndButtonMiddleware],
352-
[defaultScrollToEndButtonMiddleware, scrollToEndButtonMiddleware]
353-
);
354-
355-
return (
356-
<APIComposer
357-
activityMiddleware={patchedActivityMiddleware}
358-
activityStatusMiddleware={patchedActivityStatusMiddleware}
359-
attachmentForScreenReaderMiddleware={patchedAttachmentForScreenReaderMiddleware}
360-
attachmentMiddleware={patchedAttachmentMiddleware}
361-
avatarMiddleware={patchedAvatarMiddleware}
362-
cardActionMiddleware={patchedCardActionMiddleware}
363-
downscaleImageToDataURL={downscaleImageToDataURL}
364-
// Under dev server of create-react-app, "NODE_ENV" will be set to "development".
365-
internalErrorBoxClass={node_env === 'development' ? ErrorBox : undefined}
366-
nonce={nonce}
367-
scrollToEndButtonMiddleware={patchedScrollToEndButtonMiddleware}
368-
toastMiddleware={patchedToastMiddleware}
369-
typingIndicatorMiddleware={patchedTypingIndicatorMiddleware}
370-
{...composerProps}
371-
>
372-
<ActivityTreeComposer>
373-
<ComposerCore
374-
extraStyleSet={extraStyleSet}
375-
nonce={nonce}
376-
renderMarkdown={renderMarkdown}
377-
styleSet={styleSet}
378-
suggestedActionsAccessKey={suggestedActionsAccessKey}
379-
webSpeechPonyfillFactory={webSpeechPonyfillFactory}
380-
>
381-
{children}
382-
{onTelemetry && <UITracker />}
383-
</ComposerCore>
384-
</ActivityTreeComposer>
385-
</APIComposer>
386-
);
387-
};
306+
const Composer = forwardRef<ComposerRef, ComposerProps>(
307+
(
308+
{
309+
activityMiddleware,
310+
activityStatusMiddleware,
311+
attachmentForScreenReaderMiddleware,
312+
attachmentMiddleware,
313+
avatarMiddleware,
314+
cardActionMiddleware,
315+
children,
316+
extraStyleSet,
317+
renderMarkdown,
318+
scrollToEndButtonMiddleware,
319+
styleSet,
320+
suggestedActionsAccessKey,
321+
toastMiddleware,
322+
typingIndicatorMiddleware,
323+
webSpeechPonyfillFactory,
324+
...composerProps
325+
},
326+
ref
327+
) => {
328+
const { nonce, onTelemetry } = composerProps;
329+
330+
const patchedActivityMiddleware = useMemo(
331+
() => [...singleToArray(activityMiddleware), ...createDefaultActivityMiddleware()],
332+
[activityMiddleware]
333+
);
334+
335+
const patchedActivityStatusMiddleware = useMemo(
336+
() => [...singleToArray(activityStatusMiddleware), ...createDefaultActivityStatusMiddleware()],
337+
[activityStatusMiddleware]
338+
);
339+
340+
const patchedAttachmentForScreenReaderMiddleware = useMemo(
341+
() => [
342+
...singleToArray(attachmentForScreenReaderMiddleware),
343+
...createDefaultAttachmentForScreenReaderMiddleware()
344+
],
345+
[attachmentForScreenReaderMiddleware]
346+
);
347+
348+
const patchedAttachmentMiddleware = useMemo(
349+
() => [...singleToArray(attachmentMiddleware), ...createDefaultAttachmentMiddleware()],
350+
[attachmentMiddleware]
351+
);
352+
353+
const patchedAvatarMiddleware = useMemo(
354+
() => [...singleToArray(avatarMiddleware), ...createDefaultAvatarMiddleware()],
355+
[avatarMiddleware]
356+
);
357+
358+
const patchedCardActionMiddleware = useMemo(
359+
() => [...singleToArray(cardActionMiddleware), ...createDefaultCardActionMiddleware()],
360+
[cardActionMiddleware]
361+
);
362+
363+
const patchedToastMiddleware = useMemo(
364+
() => [...singleToArray(toastMiddleware), ...createDefaultToastMiddleware()],
365+
[toastMiddleware]
366+
);
367+
368+
const patchedTypingIndicatorMiddleware = useMemo(
369+
() => [...singleToArray(typingIndicatorMiddleware), ...createDefaultTypingIndicatorMiddleware()],
370+
[typingIndicatorMiddleware]
371+
);
372+
373+
const defaultScrollToEndButtonMiddleware = useMemo(() => createDefaultScrollToEndButtonMiddleware(), []);
374+
375+
const patchedScrollToEndButtonMiddleware = useMemo(
376+
() => [...singleToArray(scrollToEndButtonMiddleware), ...defaultScrollToEndButtonMiddleware],
377+
[defaultScrollToEndButtonMiddleware, scrollToEndButtonMiddleware]
378+
);
379+
380+
return (
381+
<APIComposer
382+
activityMiddleware={patchedActivityMiddleware}
383+
activityStatusMiddleware={patchedActivityStatusMiddleware}
384+
attachmentForScreenReaderMiddleware={patchedAttachmentForScreenReaderMiddleware}
385+
attachmentMiddleware={patchedAttachmentMiddleware}
386+
avatarMiddleware={patchedAvatarMiddleware}
387+
cardActionMiddleware={patchedCardActionMiddleware}
388+
downscaleImageToDataURL={downscaleImageToDataURL}
389+
// Under dev server of create-react-app, "NODE_ENV" will be set to "development".
390+
internalErrorBoxClass={node_env === 'development' ? ErrorBox : undefined}
391+
nonce={nonce}
392+
scrollToEndButtonMiddleware={patchedScrollToEndButtonMiddleware}
393+
toastMiddleware={patchedToastMiddleware}
394+
typingIndicatorMiddleware={patchedTypingIndicatorMiddleware}
395+
{...composerProps}
396+
>
397+
<ActivityTreeComposer>
398+
<ComposerCore
399+
extraStyleSet={extraStyleSet}
400+
nonce={nonce}
401+
renderMarkdown={renderMarkdown}
402+
styleSet={styleSet}
403+
suggestedActionsAccessKey={suggestedActionsAccessKey}
404+
webSpeechPonyfillFactory={webSpeechPonyfillFactory}
405+
>
406+
<ComposerWithRef ref={ref}>
407+
{children}
408+
{onTelemetry && <UITracker />}
409+
</ComposerWithRef>
410+
</ComposerCore>
411+
</ActivityTreeComposer>
412+
</APIComposer>
413+
);
414+
}
415+
);
388416

389417
Composer.defaultProps = {
390418
...APIComposer.defaultProps,

packages/component/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { concatMiddleware, hooks as apiHooks, localize } from 'botframework-webc
22

33
import ReactWebChat, { ReactWebChatProps } from './ReactWebChat';
44

5-
import Composer, { ComposerProps } from './Composer';
5+
import Composer, { ComposerProps, ComposerRef } from './Composer';
66

77
import AccessKeySinkSurface from './Utils/AccessKeySink/Surface';
88

@@ -118,4 +118,4 @@ export {
118118
withEmoji
119119
};
120120

121-
export type { BasicWebChatProps, ComposerProps, ReactWebChatProps };
121+
export type { BasicWebChatProps, ComposerProps, ComposerRef, ReactWebChatProps };

0 commit comments

Comments
 (0)