botframework-webchat-fluent-theme 4.17.0-main.20240411.647b269 → 4.17.0-main.20240411.ff34969

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/botframework-webchat-fluent-theme.development.js +4081 -0
  2. package/dist/botframework-webchat-fluent-theme.development.js.map +1 -0
  3. package/dist/botframework-webchat-fluent-theme.production.min.js +8 -16
  4. package/dist/botframework-webchat-fluent-theme.production.min.js.map +1 -1
  5. package/dist/index.js +1350 -16
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +13 -4
  9. package/src/bundle.ts +9 -2
  10. package/src/components/DropZone.tsx +3 -0
  11. package/src/components/SendBox.tsx +3 -0
  12. package/src/components/SuggestedActions.tsx +3 -0
  13. package/src/components/TelephoneKeypad.tsx +1 -0
  14. package/src/components/Theme.tsx +71 -0
  15. package/src/components/dropZone/index.tsx +132 -0
  16. package/src/components/sendbox/AddAttachmentButton.tsx +77 -0
  17. package/src/components/sendbox/Attachments.tsx +40 -0
  18. package/src/components/sendbox/ErrorMessage.tsx +26 -0
  19. package/src/components/sendbox/TelephoneKeypadToolbarButton.tsx +30 -0
  20. package/src/components/sendbox/TextArea.tsx +146 -0
  21. package/src/components/sendbox/Toolbar.tsx +115 -0
  22. package/src/components/sendbox/index.tsx +234 -0
  23. package/src/components/sendbox/private/useSubmitError.ts +46 -0
  24. package/src/components/sendbox/private/useUniqueId.ts +13 -0
  25. package/src/components/suggestedActions/AccessibleButton.tsx +59 -0
  26. package/src/components/suggestedActions/SuggestedAction.tsx +129 -0
  27. package/src/components/suggestedActions/index.tsx +103 -0
  28. package/src/components/suggestedActions/private/computeSuggestedActionText.ts +21 -0
  29. package/src/components/telephoneKeypad/Provider.tsx +22 -0
  30. package/src/components/telephoneKeypad/Surrogate.tsx +13 -0
  31. package/src/components/telephoneKeypad/index.ts +6 -0
  32. package/src/components/telephoneKeypad/private/Button.tsx +107 -0
  33. package/src/components/telephoneKeypad/private/Context.ts +18 -0
  34. package/src/components/telephoneKeypad/private/TelephoneKeypad.tsx +168 -0
  35. package/src/components/telephoneKeypad/types.ts +1 -0
  36. package/src/components/telephoneKeypad/useShown.ts +9 -0
  37. package/src/external.umd/botframework-webchat-api.ts +3 -0
  38. package/src/external.umd/botframework-webchat-component.ts +4 -0
  39. package/src/external.umd/react.ts +1 -0
  40. package/src/icons/AddDocumentIcon.tsx +20 -0
  41. package/src/icons/AttachmentIcon.tsx +20 -0
  42. package/src/icons/SendIcon.tsx +20 -0
  43. package/src/icons/TelephoneKeypad.tsx +20 -0
  44. package/src/index.ts +2 -1
  45. package/src/private/FluentThemeProvider.tsx +11 -7
  46. package/src/private/useStyleToEmotionObject.ts +32 -0
  47. package/src/styles/index.ts +15 -0
  48. package/src/testIds.ts +21 -0
  49. package/src/types/PropsOf.ts +7 -0
  50. package/src/external/ThemeProvider.tsx +0 -16
  51. package/src/private/SendBox.tsx +0 -7
@@ -0,0 +1,234 @@
1
+ import { hooks } from 'botframework-webchat-component';
2
+ import cx from 'classnames';
3
+ import React, { memo, useCallback, useRef, useState, type FormEventHandler, type MouseEventHandler } from 'react';
4
+ import { useRefFrom } from 'use-ref-from';
5
+ import { SendIcon } from '../../icons/SendIcon';
6
+ import { useStyles } from '../../styles';
7
+ import testIds from '../../testIds';
8
+ import DropZone from '../DropZone';
9
+ import SuggestedActions from '../SuggestedActions';
10
+ import { TelephoneKeypadSurrogate, useTelephoneKeypadShown, type DTMF } from '../TelephoneKeypad';
11
+ import AddAttachmentButton from './AddAttachmentButton';
12
+ import Attachments from './Attachments';
13
+ import ErrorMessage from './ErrorMessage';
14
+ import TelephoneKeypadToolbarButton from './TelephoneKeypadToolbarButton';
15
+ import TextArea from './TextArea';
16
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from './Toolbar';
17
+ import useSubmitError from './private/useSubmitError';
18
+ import useUniqueId from './private/useUniqueId';
19
+
20
+ const { useStyleOptions, useMakeThumbnail, useLocalizer, useSendBoxAttachments, useSendMessage } = hooks;
21
+
22
+ const styles = {
23
+ 'webchat-fluent__sendbox': {
24
+ color: 'var(--webchat-colorNeutralForeground1)',
25
+ fontFamily: 'var(--webchat-fontFamilyBase)',
26
+ padding: '0 10px 10px',
27
+ textRendering: 'optimizeLegibility'
28
+ },
29
+
30
+ 'webchat-fluent__sendbox__sendbox': {
31
+ backgroundColor: 'var(--webchat-colorNeutralBackground1)',
32
+ border: '1px solid var(--webchat-colorNeutralStroke1)',
33
+ borderRadius: 'var(--webchat-borderRadiusLarge)',
34
+ display: 'grid',
35
+ fontFamily: 'var(--webchat-fontFamilyBase)',
36
+ fontSize: '14px',
37
+ gap: '6px',
38
+ lineHeight: '20px',
39
+ padding: '8px',
40
+ position: 'relative',
41
+
42
+ '&:focus-within': {
43
+ borderColor: 'var(--webchat-colorNeutralStroke1Selected)',
44
+ // TODO clarify with design the color:
45
+ // - Teams is using colorCompoundBrandForeground1
46
+ // - Fluent is using colorCompoundBrandStroke
47
+ // - we are using colorCompoundBrandForeground1Hover
48
+ boxShadow: 'inset 0 -6px 0 -3px var(--webchat-colorCompoundBrandForeground1Hover)'
49
+ }
50
+ },
51
+
52
+ 'webchat-fluent__sendbox__sendbox-text': {
53
+ backgroundColor: 'transparent',
54
+ border: 'none',
55
+ flex: 'auto',
56
+ fontFamily: 'var(--webchat-fontFamilyBase)',
57
+ fontSize: '14px',
58
+ lineHeight: '20px',
59
+ outline: 'none',
60
+ padding: '4px 4px 0',
61
+ resize: 'none'
62
+ },
63
+
64
+ 'webchat-fluent__sendbox__sendbox-controls': {
65
+ alignItems: 'center',
66
+ display: 'flex',
67
+ paddingInlineStart: '4px'
68
+ },
69
+
70
+ 'webchat-fluent__sendbox__text-counter': {
71
+ color: 'var(--webchat-colorNeutralForeground4)',
72
+ cursor: 'default',
73
+ fontFamily: 'var(--webchat-fontFamilyNumeric)',
74
+ fontSize: '10px',
75
+ lineHeight: '14px'
76
+ },
77
+
78
+ 'webchat-fluent__sendbox__text-counter--error': {
79
+ color: 'var(--webchat-colorStatusDangerForeground1)'
80
+ }
81
+ };
82
+
83
+ function SendBox(
84
+ props: Readonly<{
85
+ className?: string | undefined;
86
+ placeholder?: string | undefined;
87
+ }>
88
+ ) {
89
+ const inputRef = useRef<HTMLTextAreaElement>(null);
90
+ const [message, setMessage] = useState('');
91
+ const [attachments, setAttachments] = useSendBoxAttachments();
92
+ const [{ maxMessageLength }] = useStyleOptions();
93
+ const isMessageLengthExceeded = !!maxMessageLength && message.length > maxMessageLength;
94
+ const classNames = useStyles(styles);
95
+ const localize = useLocalizer();
96
+ const sendMessage = useSendMessage();
97
+ const makeThumbnail = useMakeThumbnail();
98
+ const errorMessageId = useUniqueId('webchat-fluent__sendbox__error-message-id');
99
+ const [errorRef, errorMessage] = useSubmitError({ message, attachments });
100
+ const [telephoneKeypadShown, setTelephoneKeypadShown] = useTelephoneKeypadShown();
101
+
102
+ const attachmentsRef = useRefFrom(attachments);
103
+ const messageRef = useRefFrom(message);
104
+
105
+ const handleSendBoxClick = useCallback<MouseEventHandler>(
106
+ event => {
107
+ if ('tabIndex' in event.target && typeof event.target.tabIndex === 'number' && event.target.tabIndex >= 0) {
108
+ return;
109
+ }
110
+
111
+ // TODO: Should call `useFocus('sendBox')`.
112
+ inputRef.current?.focus();
113
+ },
114
+ [inputRef]
115
+ );
116
+
117
+ const handleMessageChange: React.FormEventHandler<HTMLTextAreaElement> = useCallback(
118
+ event => setMessage(event.currentTarget.value),
119
+ [setMessage]
120
+ );
121
+
122
+ const handleAddFiles = useCallback(
123
+ async (inputFiles: File[]) => {
124
+ const newAttachments = Object.freeze(
125
+ await Promise.all(
126
+ inputFiles.map(file =>
127
+ makeThumbnail(file).then(thumbnailURL =>
128
+ Object.freeze({
129
+ blob: file,
130
+ ...(thumbnailURL && { thumbnailURL })
131
+ })
132
+ )
133
+ )
134
+ )
135
+ );
136
+
137
+ setAttachments(newAttachments);
138
+
139
+ // TODO: Currently in the UX, we have no way to remove attachments.
140
+ // Keep concatenating doesn't make sense in current UX.
141
+ // When end-user can remove attachment, we should enable the code again.
142
+ // setAttachments(attachments => attachments.concat(newAttachments));
143
+ },
144
+ [makeThumbnail, setAttachments]
145
+ );
146
+
147
+ const handleFormSubmit: FormEventHandler<HTMLFormElement> = useCallback(
148
+ event => {
149
+ event.preventDefault();
150
+
151
+ if (errorRef.current !== 'empty' && !isMessageLengthExceeded) {
152
+ sendMessage(messageRef.current, undefined, { attachments: attachmentsRef.current });
153
+
154
+ setMessage('');
155
+ setAttachments([]);
156
+ }
157
+
158
+ // TODO: Should call `useFocus('sendBox')`.
159
+ inputRef.current?.focus();
160
+ },
161
+ [attachmentsRef, messageRef, sendMessage, setAttachments, setMessage, isMessageLengthExceeded, errorRef, inputRef]
162
+ );
163
+
164
+ const handleTelephoneKeypadButtonClick = useCallback(
165
+ (dtmf: DTMF) => {
166
+ // TODO: We need more official way of sending DTMF.
167
+ sendMessage(`/DTMF ${dtmf}`);
168
+
169
+ // TODO: In the future when we work on input modality, it should manage the focus in a better way.
170
+ setTelephoneKeypadShown(false);
171
+ },
172
+ [sendMessage, setTelephoneKeypadShown]
173
+ );
174
+
175
+ const aria = {
176
+ 'aria-invalid': 'false' as const,
177
+ ...(errorMessage && {
178
+ 'aria-invalid': 'true' as const,
179
+ 'aria-errormessage': errorMessageId
180
+ })
181
+ };
182
+
183
+ return (
184
+ <form {...aria} className={cx(classNames['webchat-fluent__sendbox'], props.className)} onSubmit={handleFormSubmit}>
185
+ <SuggestedActions />
186
+ <div className={cx(classNames['webchat-fluent__sendbox__sendbox'])} onClickCapture={handleSendBoxClick}>
187
+ <TelephoneKeypadSurrogate
188
+ autoFocus={true}
189
+ isHorizontal={false}
190
+ onButtonClick={handleTelephoneKeypadButtonClick}
191
+ />
192
+ <TextArea
193
+ aria-label={isMessageLengthExceeded ? localize('TEXT_INPUT_LENGTH_EXCEEDED_ALT') : localize('TEXT_INPUT_ALT')}
194
+ className={classNames['webchat-fluent__sendbox__sendbox-text']}
195
+ data-testid={testIds.sendBoxTextBox}
196
+ hidden={telephoneKeypadShown}
197
+ onInput={handleMessageChange}
198
+ placeholder={props.placeholder ?? localize('TEXT_INPUT_PLACEHOLDER')}
199
+ ref={inputRef}
200
+ value={message}
201
+ />
202
+ <Attachments attachments={attachments} />
203
+ <div className={cx(classNames['webchat-fluent__sendbox__sendbox-controls'])}>
204
+ {maxMessageLength && (
205
+ <div
206
+ className={cx(classNames['webchat-fluent__sendbox__text-counter'], {
207
+ [classNames['webchat-fluent__sendbox__text-counter--error']]: isMessageLengthExceeded
208
+ })}
209
+ >
210
+ {`${message.length}/${maxMessageLength}`}
211
+ </div>
212
+ )}
213
+ <Toolbar>
214
+ <TelephoneKeypadToolbarButton />
215
+ <AddAttachmentButton onFilesAdded={handleAddFiles} />
216
+ <ToolbarSeparator />
217
+ <ToolbarButton
218
+ aria-label={localize('TEXT_INPUT_SEND_BUTTON_ALT')}
219
+ data-testid={testIds.sendBoxSendButton}
220
+ disabled={isMessageLengthExceeded}
221
+ type="submit"
222
+ >
223
+ <SendIcon />
224
+ </ToolbarButton>
225
+ </Toolbar>
226
+ </div>
227
+ <DropZone onFilesAdded={handleAddFiles} />
228
+ <ErrorMessage error={errorMessage} id={errorMessageId} />
229
+ </div>
230
+ </form>
231
+ );
232
+ }
233
+
234
+ export default memo(SendBox);
@@ -0,0 +1,46 @@
1
+ import { hooks } from 'botframework-webchat-component';
2
+ import { RefObject, useMemo } from 'react';
3
+ import { useRefFrom } from 'use-ref-from';
4
+
5
+ const { useConnectivityStatus, useLocalizer } = hooks;
6
+
7
+ type ErrorMessageStringMap = ReadonlyMap<SendError, string>;
8
+
9
+ type SendError = 'empty' | 'offline';
10
+
11
+ const useSubmitError = ({
12
+ attachments,
13
+ message
14
+ }: Readonly<{
15
+ attachments: readonly Readonly<{ blob: Blob | File; thumbnailURL?: URL | undefined }>[];
16
+ message: string;
17
+ }>) => {
18
+ const [connectivityStatus] = useConnectivityStatus();
19
+ const localize = useLocalizer();
20
+
21
+ const submitErrorRef = useRefFrom<'empty' | 'offline' | undefined>(
22
+ connectivityStatus !== 'connected' && connectivityStatus !== 'reconnected'
23
+ ? 'offline'
24
+ : !message && !attachments.length
25
+ ? 'empty'
26
+ : undefined
27
+ );
28
+
29
+ const errorMessageStringMap = useMemo<ErrorMessageStringMap>(
30
+ () =>
31
+ Object.freeze(
32
+ new Map<SendError, string>()
33
+ .set('empty', localize('SEND_BOX_IS_EMPTY_TOOLTIP_ALT'))
34
+ // TODO: [P0] We should add a new string for "Cannot send message while offline."
35
+ .set('offline', localize('CONNECTIVITY_STATUS_ALT_FATAL'))
36
+ ),
37
+ [localize]
38
+ );
39
+
40
+ return useMemo<Readonly<[RefObject<SendError | undefined>, string | undefined]>>(
41
+ () => Object.freeze([submitErrorRef, submitErrorRef.current && errorMessageStringMap.get(submitErrorRef.current)]),
42
+ [errorMessageStringMap, submitErrorRef]
43
+ );
44
+ };
45
+
46
+ export default useSubmitError;
@@ -0,0 +1,13 @@
1
+ /* eslint no-magic-numbers: ["error", { "ignore": [2, 5, 36] }] */
2
+
3
+ import { useMemo } from 'react';
4
+ // TODO: fix math-random fails to import crypto
5
+ // import random from 'math-random';
6
+
7
+ export default function useUniqueId(prefix?: string): string {
8
+ const id = useMemo(() => Math.random().toString(36).substr(2, 5), []);
9
+
10
+ prefix = prefix ? `${prefix}--` : '';
11
+
12
+ return `${prefix}${id}`;
13
+ }
@@ -0,0 +1,59 @@
1
+ import React, { MouseEventHandler, ReactNode, forwardRef, memo, useRef } from 'react';
2
+
3
+ const preventDefaultHandler: MouseEventHandler<HTMLButtonElement> = event => event.preventDefault();
4
+
5
+ type AccessibleButtonProps = Readonly<{
6
+ className?: string | undefined;
7
+ 'aria-hidden'?: boolean;
8
+ children?: ReactNode;
9
+ disabled?: boolean;
10
+ onClick?: MouseEventHandler<HTMLButtonElement>;
11
+ tabIndex?: number;
12
+ type: 'button';
13
+ }>;
14
+
15
+ // Differences between <button> and <AccessibleButton>:
16
+ // - Disable behavior
17
+ // - When the widget is disabled
18
+ // - Set "aria-disabled" attribute to "true"
19
+ // - Set "readonly" attribute
20
+ // - Set "tabIndex" to -1
21
+ // - Remove "onClick" handler
22
+ // - Why this is needed
23
+ // - Browser compatibility: when the widget is disabled, different browser send focus to different places
24
+ // - When the widget become disabled, it's reasonable to keep the focus on the same widget for an extended period of time
25
+ // - When the user presses TAB after the current widget is disabled, it should jump to the next non-disabled widget
26
+
27
+ // Developers using this accessible widget will need to:
28
+ // - Style the disabled widget themselves, using CSS query `:disabled, [aria-disabled="true"] {}`
29
+ // - Modify all code that check disabled through "disabled" attribute to use aria-disabled="true" instead
30
+ // - aria-disabled="true" is the source of truth
31
+ // - If the widget is contained by a <form>, the developer need to filter out some `onSubmit` event caused by this widget
32
+
33
+ const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
34
+ ({ 'aria-hidden': ariaHidden, children, disabled, onClick, tabIndex, ...props }, forwardedRef) => {
35
+ const targetRef = useRef<HTMLButtonElement>(null);
36
+
37
+ const ref = forwardedRef || targetRef;
38
+
39
+ return (
40
+ <button
41
+ aria-disabled={disabled ? 'true' : 'false'}
42
+ aria-hidden={ariaHidden}
43
+ onClick={disabled ? preventDefaultHandler : onClick}
44
+ ref={ref}
45
+ tabIndex={tabIndex}
46
+ {...(disabled && {
47
+ 'aria-disabled': 'true',
48
+ tabIndex: -1
49
+ })}
50
+ {...props}
51
+ type="button"
52
+ >
53
+ {children}
54
+ </button>
55
+ );
56
+ }
57
+ );
58
+
59
+ export default memo(AccessibleButton);
@@ -0,0 +1,129 @@
1
+ import { hooks } from 'botframework-webchat-component';
2
+ import { type DirectLineCardAction } from 'botframework-webchat-core';
3
+ import cx from 'classnames';
4
+ import React, { MouseEventHandler, memo, useCallback, useRef } from 'react';
5
+ import AccessibleButton from './AccessibleButton';
6
+ import { useStyles } from '../../styles';
7
+
8
+ const { useScrollToEnd, useStyleSet, usePerformCardAction, useFocus, useSuggestedActions, useDisabled } = hooks;
9
+
10
+ type SuggestedActionProps = Readonly<{
11
+ buttonText: string | undefined;
12
+ className?: string | undefined;
13
+ displayText?: string | undefined;
14
+ image?: string | undefined;
15
+ imageAlt?: string | undefined;
16
+ itemIndex: number;
17
+ text?: string | undefined;
18
+ type?:
19
+ | 'call'
20
+ | 'downloadFile'
21
+ | 'imBack'
22
+ | 'messageBack'
23
+ | 'openUrl'
24
+ | 'playAudio'
25
+ | 'playVideo'
26
+ | 'postBack'
27
+ | 'showImage'
28
+ | 'signin';
29
+ value?: any;
30
+ }>;
31
+
32
+ const styles = {
33
+ 'webchat-fluent__suggested-action': {
34
+ background: 'transparent',
35
+ border: '1px solid var(--webchat-colorBrandStroke2)',
36
+ borderRadius: '8px',
37
+ cursor: 'pointer',
38
+ fontSize: '12px',
39
+ lineHeight: '14px',
40
+ padding: '6px 8px 4px',
41
+ textAlign: 'start',
42
+ display: 'flex',
43
+ gap: '4px',
44
+ alignItems: 'center',
45
+ transition: 'all .15s ease-out',
46
+
47
+ '@media (hover: hover)': {
48
+ '&:not([aria-disabled="true"]):hover': {
49
+ backgroundColor: 'var(--webchat-colorBrandBackground2Hover)',
50
+ color: 'var(--webchat-colorBrandForeground2Hover)'
51
+ }
52
+ },
53
+ '&:not([aria-disabled="true"]):active': {
54
+ backgroundColor: 'var(--webchat-colorBrandBackground2Pressed)',
55
+ color: 'var(--webchat-colorBrandForeground2Pressed)'
56
+ },
57
+ '&[aria-disabled="true"]': {
58
+ color: ' var(--webchat-colorNeutralForegroundDisabled)',
59
+ cursor: 'not-allowed'
60
+ }
61
+ },
62
+
63
+ 'webchat-fluent__suggested-action__image': {
64
+ width: '1em',
65
+ height: '1em',
66
+ fontSize: '20px',
67
+ translate: '0 -1px'
68
+ }
69
+ };
70
+
71
+ function SuggestedAction({
72
+ buttonText,
73
+ className,
74
+ displayText,
75
+ image,
76
+ imageAlt,
77
+ text,
78
+ type,
79
+ value
80
+ }: SuggestedActionProps) {
81
+ const [_, setSuggestedActions] = useSuggestedActions();
82
+ const [{ suggestedAction: suggestedActionStyleSet }] = useStyleSet();
83
+ const [disabled] = useDisabled();
84
+ const focus = useFocus();
85
+ const focusRef = useRef<HTMLButtonElement>(null);
86
+ const performCardAction = usePerformCardAction();
87
+ const classNames = useStyles(styles);
88
+ const scrollToEnd = useScrollToEnd();
89
+
90
+ const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>(
91
+ ({ target }) => {
92
+ (async function () {
93
+ // We need to focus to the send box before we are performing this card action.
94
+ // The will make sure the focus is always on Web Chat.
95
+ // Otherwise, the focus may momentarily send to `document.body` and screen reader will be confused.
96
+ await focus('sendBoxWithoutKeyboard');
97
+
98
+ // TODO: [P3] #XXX We should not destruct DirectLineCardAction into React props and pass them in. It makes typings difficult.
99
+ // Instead, we should pass a "cardAction" props.
100
+ performCardAction({ displayText, text, type, value } as DirectLineCardAction, { target });
101
+
102
+ // Since "openUrl" action do not submit, the suggested action buttons do not hide after click.
103
+ type === 'openUrl' && setSuggestedActions([]);
104
+
105
+ scrollToEnd();
106
+ })();
107
+ },
108
+ [displayText, focus, performCardAction, scrollToEnd, setSuggestedActions, text, type, value]
109
+ );
110
+
111
+ return (
112
+ <AccessibleButton
113
+ className={cx(
114
+ classNames['webchat-fluent__suggested-action'],
115
+ suggestedActionStyleSet + '',
116
+ (className || '') + ''
117
+ )}
118
+ disabled={disabled}
119
+ onClick={handleClick}
120
+ ref={focusRef}
121
+ type="button"
122
+ >
123
+ {image && <img alt={imageAlt} className={classNames['webchat-fluent__suggested-action__image']} src={image} />}
124
+ <span>{buttonText}</span>
125
+ </AccessibleButton>
126
+ );
127
+ }
128
+
129
+ export default memo(SuggestedAction);
@@ -0,0 +1,103 @@
1
+ import { hooks } from 'botframework-webchat-component';
2
+ import cx from 'classnames';
3
+ import React, { memo, type ReactNode } from 'react';
4
+ import { useStyles } from '../../styles';
5
+ import computeSuggestedActionText from './private/computeSuggestedActionText';
6
+ import SuggestedAction from './SuggestedAction';
7
+
8
+ const { useLocalizer, useStyleSet, useSuggestedActions } = hooks;
9
+
10
+ const styles = {
11
+ 'webchat-fluent__suggested-actions': {
12
+ alignItems: 'flex-end',
13
+ alignSelf: 'flex-end',
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ gap: '8px',
17
+
18
+ '&:not(:empty)': {
19
+ paddingBlockEnd: '8px',
20
+ paddingInlineStart: '4px'
21
+ }
22
+ }
23
+ };
24
+
25
+ function SuggestedActionStackedContainer(
26
+ props: Readonly<{
27
+ 'aria-label'?: string | undefined;
28
+ children?: ReactNode | undefined;
29
+ className?: string | undefined;
30
+ }>
31
+ ) {
32
+ const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet();
33
+ const classNames = useStyles(styles);
34
+
35
+ return (
36
+ <div
37
+ aria-label={props['aria-label']}
38
+ aria-live="polite"
39
+ aria-orientation="vertical"
40
+ className={cx(classNames['webchat-fluent__suggested-actions'], suggestedActionsStyleSet + '', props.className)}
41
+ role="toolbar"
42
+ >
43
+ {!!props.children && !!React.Children.count(props.children) && props.children}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ function SuggestedActions() {
49
+ const classNames = useStyles(styles);
50
+ const localize = useLocalizer();
51
+ const [suggestedActions] = useSuggestedActions();
52
+ const children = suggestedActions.map((cardAction, index) => {
53
+ const { displayText, image, imageAltText, text, type, value } = cardAction as {
54
+ displayText?: string;
55
+ image?: string;
56
+ imageAltText?: string;
57
+ text?: string;
58
+ type:
59
+ | 'call'
60
+ | 'downloadFile'
61
+ | 'imBack'
62
+ | 'messageBack'
63
+ | 'openUrl'
64
+ | 'playAudio'
65
+ | 'playVideo'
66
+ | 'postBack'
67
+ | 'showImage'
68
+ | 'signin';
69
+ value?: { [key: string]: any } | string;
70
+ };
71
+
72
+ if (!suggestedActions?.length) {
73
+ return null;
74
+ }
75
+
76
+ return (
77
+ <SuggestedAction
78
+ buttonText={computeSuggestedActionText(cardAction)}
79
+ displayText={displayText}
80
+ image={image}
81
+ // Image alt text should use `imageAltText` field and fallback to `text` field.
82
+ // https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#image-alt-text
83
+ imageAlt={image && (imageAltText || text)}
84
+ itemIndex={index}
85
+ // eslint-disable-next-line react/no-array-index-key
86
+ key={index}
87
+ text={text}
88
+ type={type}
89
+ value={value}
90
+ />
91
+ );
92
+ });
93
+ return (
94
+ <SuggestedActionStackedContainer
95
+ aria-label={localize('SUGGESTED_ACTIONS_LABEL_ALT')}
96
+ className={classNames['webchat-fluent__suggested-actions']}
97
+ >
98
+ {children}
99
+ </SuggestedActionStackedContainer>
100
+ );
101
+ }
102
+
103
+ export default memo(SuggestedActions);
@@ -0,0 +1,21 @@
1
+ import type { DirectLineCardAction } from 'botframework-webchat-core';
2
+
3
+ // Please refer to this article to find out how to compute the "button text" for suggested action.
4
+ // https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#card-action
5
+ export default function computeSuggestedActionText(cardAction: DirectLineCardAction) {
6
+ // "CardAction" must contains at least image or title.
7
+ const { title } = cardAction as { title?: string };
8
+ const { type, value } = cardAction;
9
+
10
+ if (type === 'messageBack') {
11
+ return title || cardAction.displayText;
12
+ } else if (title) {
13
+ return title;
14
+ } else if (typeof value === 'string') {
15
+ return value;
16
+ }
17
+
18
+ return JSON.stringify(value);
19
+ }
20
+
21
+ // TODO: [P1] This is copied from botframework-webchat-component. Think about what we should do to eliminate duplications.
@@ -0,0 +1,22 @@
1
+ import React, { memo, useMemo, useState, type ReactNode } from 'react';
2
+
3
+ import Context from './private/Context';
4
+
5
+ type Props = Readonly<{ children?: ReactNode | undefined }>;
6
+
7
+ const Provider = memo(({ children }: Props) => {
8
+ const [shown, setShown] = useState(false);
9
+
10
+ const context = useMemo(
11
+ () =>
12
+ Object.freeze({
13
+ setShown,
14
+ shown
15
+ }),
16
+ [setShown, shown]
17
+ );
18
+
19
+ return <Context.Provider value={context}>{children}</Context.Provider>;
20
+ });
21
+
22
+ export default Provider;
@@ -0,0 +1,13 @@
1
+ import React, { memo } from 'react';
2
+
3
+ import type { PropsOf } from '../../types/PropsOf';
4
+ import TelephoneKeypad from './private/TelephoneKeypad';
5
+ import useShown from './useShown';
6
+
7
+ type Props = PropsOf<typeof TelephoneKeypad>;
8
+
9
+ const TelephoneKeypadSurrogate = memo((props: Props) => (useShown()[0] ? <TelephoneKeypad {...props} /> : false));
10
+
11
+ TelephoneKeypadSurrogate.displayName = 'TelephoneKeypad.Surrogate';
12
+
13
+ export default TelephoneKeypadSurrogate;
@@ -0,0 +1,6 @@
1
+ import TelephoneKeypadProvider from './Provider';
2
+ import TelephoneKeypadSurrogate from './Surrogate';
3
+ import { type DTMF } from './types';
4
+ import useTelephoneKeypadShown from './useShown';
5
+
6
+ export { TelephoneKeypadProvider, TelephoneKeypadSurrogate, useTelephoneKeypadShown, type DTMF };