botframework-webchat-fluent-theme 4.17.0-main.20240411.647b269 → 4.17.0-main.20240416.4ff01ae

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 (72) hide show
  1. package/dist/botframework-webchat-fluent-theme.development.css.map +1 -0
  2. package/dist/botframework-webchat-fluent-theme.development.js +2384 -0
  3. package/dist/botframework-webchat-fluent-theme.development.js.map +1 -0
  4. package/dist/botframework-webchat-fluent-theme.production.min.css.map +1 -0
  5. package/dist/botframework-webchat-fluent-theme.production.min.js +6 -16
  6. package/dist/botframework-webchat-fluent-theme.production.min.js.map +1 -1
  7. package/dist/index.css.map +1 -0
  8. package/dist/index.d.mts +27 -0
  9. package/dist/index.d.ts +27 -0
  10. package/dist/index.js +1062 -16
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1076 -0
  13. package/dist/index.mjs.map +1 -1
  14. package/package.json +17 -7
  15. package/src/bundle.ts +9 -2
  16. package/src/components/DropZone.tsx +3 -0
  17. package/src/components/SendBox.tsx +3 -0
  18. package/src/components/SuggestedActions.tsx +3 -0
  19. package/src/components/TelephoneKeypad.tsx +1 -0
  20. package/src/components/Theme.module.css +60 -0
  21. package/src/components/Theme.tsx +11 -0
  22. package/src/components/dropZone/index.module.css +23 -0
  23. package/src/components/dropZone/index.tsx +107 -0
  24. package/src/components/sendbox/AddAttachmentButton.module.css +10 -0
  25. package/src/components/sendbox/AddAttachmentButton.tsx +65 -0
  26. package/src/components/sendbox/Attachments.module.css +7 -0
  27. package/src/components/sendbox/Attachments.tsx +31 -0
  28. package/src/components/sendbox/ErrorMessage.module.css +9 -0
  29. package/src/components/sendbox/ErrorMessage.tsx +15 -0
  30. package/src/components/sendbox/TelephoneKeypadToolbarButton.tsx +30 -0
  31. package/src/components/sendbox/TextArea.module.css +61 -0
  32. package/src/components/sendbox/TextArea.tsx +85 -0
  33. package/src/components/sendbox/Toolbar.module.css +49 -0
  34. package/src/components/sendbox/Toolbar.tsx +64 -0
  35. package/src/components/sendbox/index.module.css +58 -0
  36. package/src/components/sendbox/index.tsx +169 -0
  37. package/src/components/sendbox/private/useSubmitError.ts +46 -0
  38. package/src/components/sendbox/private/useUniqueId.ts +13 -0
  39. package/src/components/suggestedActions/AccessibleButton.tsx +59 -0
  40. package/src/components/suggestedActions/SuggestedAction.module.css +34 -0
  41. package/src/components/suggestedActions/SuggestedAction.tsx +87 -0
  42. package/src/components/suggestedActions/index.module.css +23 -0
  43. package/src/components/suggestedActions/index.tsx +98 -0
  44. package/src/components/suggestedActions/private/computeSuggestedActionText.ts +21 -0
  45. package/src/components/telephoneKeypad/Provider.tsx +22 -0
  46. package/src/components/telephoneKeypad/Surrogate.tsx +13 -0
  47. package/src/components/telephoneKeypad/index.ts +6 -0
  48. package/src/components/telephoneKeypad/private/Button.module.css +62 -0
  49. package/src/components/telephoneKeypad/private/Button.tsx +45 -0
  50. package/src/components/telephoneKeypad/private/Context.ts +18 -0
  51. package/src/components/telephoneKeypad/private/TelephoneKeypad.module.css +30 -0
  52. package/src/components/telephoneKeypad/private/TelephoneKeypad.tsx +137 -0
  53. package/src/components/telephoneKeypad/types.ts +1 -0
  54. package/src/components/telephoneKeypad/useShown.ts +9 -0
  55. package/src/env.d.ts +7 -0
  56. package/src/external.umd/botframework-webchat-api.ts +3 -0
  57. package/src/external.umd/botframework-webchat-component.ts +4 -0
  58. package/src/external.umd/react.ts +1 -0
  59. package/src/icons/AddDocumentIcon.tsx +20 -0
  60. package/src/icons/AttachmentIcon.tsx +20 -0
  61. package/src/icons/SendIcon.tsx +20 -0
  62. package/src/icons/TelephoneKeypad.tsx +20 -0
  63. package/src/index.ts +5 -1
  64. package/src/private/FluentThemeProvider.tsx +11 -7
  65. package/src/styles/injectStyle.ts +9 -0
  66. package/src/styles/useStyles.ts +19 -0
  67. package/src/styles.ts +4 -0
  68. package/src/testIds.ts +21 -0
  69. package/src/tsconfig.json +2 -1
  70. package/src/types/PropsOf.ts +7 -0
  71. package/src/external/ThemeProvider.tsx +0 -16
  72. package/src/private/SendBox.tsx +0 -7
@@ -0,0 +1,31 @@
1
+ import { hooks } from 'botframework-webchat-component';
2
+ import React, { memo } from 'react';
3
+ import styles from './Attachments.module.css';
4
+ import { useStyles } from '../../styles';
5
+
6
+ const { useLocalizer } = hooks;
7
+
8
+ const attachmentsPluralStringIds = {
9
+ one: 'TEXT_INPUT_ATTACHMENTS_ONE',
10
+ two: 'TEXT_INPUT_ATTACHMENTS_TWO',
11
+ few: 'TEXT_INPUT_ATTACHMENTS_FEW',
12
+ many: 'TEXT_INPUT_ATTACHMENTS_MANY',
13
+ other: 'TEXT_INPUT_ATTACHMENTS_OTHER'
14
+ };
15
+
16
+ function Attachments({
17
+ attachments
18
+ }: Readonly<{
19
+ readonly attachments: readonly Readonly<{ blob: Blob | File; thumbnailURL?: URL | undefined }>[];
20
+ }>) {
21
+ const classNames = useStyles(styles);
22
+ const localizeWithPlural = useLocalizer({ plural: true });
23
+
24
+ return attachments.length ? (
25
+ <div className={classNames['sendbox__attachment']}>
26
+ {localizeWithPlural(attachmentsPluralStringIds, attachments.length)}
27
+ </div>
28
+ ) : null;
29
+ }
30
+
31
+ export default memo(Attachments);
@@ -0,0 +1,9 @@
1
+ :global(.webchat-fluent) .sendbox__error-message {
2
+ color: transparent;
3
+ font-size: 0;
4
+ height: 0;
5
+ left: 0;
6
+ position: absolute;
7
+ top: 0;
8
+ width: 0;
9
+ }
@@ -0,0 +1,15 @@
1
+ import React, { memo } from 'react';
2
+ import styles from './ErrorMessage.module.css';
3
+ import { useStyles } from '../../styles';
4
+
5
+ function ErrorMessage(props: Readonly<{ id: string; error?: string | undefined }>) {
6
+ const classNames = useStyles(styles);
7
+ return (
8
+ // eslint-disable-next-line react/forbid-dom-props
9
+ <span className={classNames['sendbox__error-message']} id={props.id} role="alert">
10
+ {props.error}
11
+ </span>
12
+ );
13
+ }
14
+
15
+ export default memo(ErrorMessage);
@@ -0,0 +1,30 @@
1
+ import React, { memo, useCallback } from 'react';
2
+
3
+ import { hooks } from 'botframework-webchat-component';
4
+ import { TelephoneKeypadIcon } from '../../icons/TelephoneKeypad';
5
+ import testIds from '../../testIds';
6
+ import { useTelephoneKeypadShown } from '../TelephoneKeypad';
7
+ import { ToolbarButton } from './Toolbar';
8
+
9
+ const { useLocalizer } = hooks;
10
+
11
+ const TelephoneKeypadToolbarButton = memo(() => {
12
+ const [, setTelephoneKeypadShown] = useTelephoneKeypadShown();
13
+ const localize = useLocalizer();
14
+
15
+ const handleClick = useCallback(() => setTelephoneKeypadShown(shown => !shown), [setTelephoneKeypadShown]);
16
+
17
+ return (
18
+ <ToolbarButton
19
+ aria-label={localize('TEXT_INPUT_TELEPHONE_KEYPAD_BUTTON_ALT')}
20
+ data-testid={testIds.sendBoxTelephoneKeypadToolbarButton}
21
+ onClick={handleClick}
22
+ >
23
+ <TelephoneKeypadIcon />
24
+ </ToolbarButton>
25
+ );
26
+ });
27
+
28
+ TelephoneKeypadToolbarButton.displayName = 'SendBox.TelephoneKeypadToolbarButton';
29
+
30
+ export default TelephoneKeypadToolbarButton;
@@ -0,0 +1,61 @@
1
+ :global(.webchat-fluent) .sendbox__text-area {
2
+ display: grid;
3
+ grid-template-areas: 'main';
4
+ max-height: 200px;
5
+ overflow: hidden;
6
+ }
7
+
8
+ :global(.webchat-fluent) .sendbox__text-area--hidden {
9
+ /* TODO: Not perfect way of hiding the text box. */
10
+ height: 0;
11
+ visibility: collapse;
12
+ }
13
+
14
+ :global(.webchat-fluent) .sendbox__text-area-shared {
15
+ border: none;
16
+ font: inherit;
17
+ grid-area: main;
18
+ outline: inherit;
19
+ overflow-wrap: anywhere;
20
+ resize: inherit;
21
+ scrollbar-gutter: stable;
22
+ }
23
+
24
+ :global(.webchat-fluent) .sendbox__text-area-doppelganger {
25
+ overflow: hidden;
26
+ visibility: hidden;
27
+ white-space: pre-wrap;
28
+ }
29
+
30
+ :global(.webchat-fluent) .sendbox__text-area-input {
31
+ height: 100%;
32
+ padding: 0
33
+ }
34
+
35
+ :global(.webchat-fluent) .sendbox__text-area-input--scroll {
36
+ /* Edge uses -webkit-scrollbar if scrollbar-* is not set */
37
+ scrollbar-color: unset;
38
+ scrollbar-width: unset;
39
+ /* Firefox */
40
+ -moz-scrollbar-color: var(--webchat-colorNeutralBackground5) var(--webchat-colorNeutralForeground2);
41
+ -moz-scrollbar-width: thin;
42
+
43
+ /* Chrome, Edge, and Safari */
44
+ &::-webkit-scrollbar {
45
+ width: 8px
46
+ }
47
+
48
+ &::-webkit-scrollbar-track {
49
+ background-color: var(--webchat-colorNeutralBackground5);
50
+ border-radius: 16px
51
+ }
52
+
53
+ &::-webkit-scrollbar-thumb {
54
+ background-color: var(--webchat-colorNeutralForeground2);
55
+ border-radius: 16px
56
+ }
57
+
58
+ &::-webkit-scrollbar-corner {
59
+ background-color: var(--webchat-colorNeutralBackground5);
60
+ }
61
+ }
@@ -0,0 +1,85 @@
1
+ import cx from 'classnames';
2
+ import React, { forwardRef, useCallback, type FormEventHandler, type KeyboardEventHandler } from 'react';
3
+ import { useStyles } from '../../styles';
4
+ import styles from './TextArea.module.css';
5
+
6
+ const TextArea = forwardRef<
7
+ HTMLTextAreaElement,
8
+ Readonly<{
9
+ 'aria-label'?: string | undefined;
10
+ className?: string | undefined;
11
+ 'data-testid'?: string | undefined;
12
+
13
+ /**
14
+ * `true`, if the text area should be hidden but stay in the DOM, otherwise, `false`.
15
+ *
16
+ * Keeping the element in the DOM while making it invisible to users and PWDs is useful in these scenarios:
17
+ *
18
+ * - When the DTMF keypad is going away, we need to send focus to the text area before we unmount DTMF keypad,
19
+ * This ensures the flow of focus did not sent to document body
20
+ */
21
+ hidden?: boolean | undefined;
22
+ onInput?: FormEventHandler<HTMLTextAreaElement> | undefined;
23
+ placeholder?: string | undefined;
24
+ startRows?: number | undefined;
25
+ value?: string | undefined;
26
+ }>
27
+ >((props, ref) => {
28
+ const classNames = useStyles(styles);
29
+
30
+ const handleKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(event => {
31
+ // Shift+Enter adds a new line
32
+ // Enter requests related form submission
33
+ if (!event.shiftKey && event.key === 'Enter') {
34
+ event.preventDefault();
35
+
36
+ if ('form' in event.target && event.target.form instanceof HTMLFormElement) {
37
+ event.target?.form?.requestSubmit();
38
+ }
39
+ }
40
+ }, []);
41
+
42
+ return (
43
+ <div
44
+ className={cx(
45
+ classNames['sendbox__text-area'],
46
+ {
47
+ [classNames['sendbox__text-area--hidden']]: props.hidden
48
+ },
49
+ props.className
50
+ )}
51
+ role={props.hidden ? 'hidden' : undefined}
52
+ >
53
+ <div
54
+ className={cx(
55
+ classNames['sendbox__text-area-doppelganger'],
56
+ classNames['sendbox__text-area-shared'],
57
+ classNames['sendbox__text-area-input--scroll']
58
+ )}
59
+ >
60
+ {props.value || props.placeholder}{' '}
61
+ </div>
62
+ <textarea
63
+ aria-label={props['aria-label']}
64
+ className={cx(
65
+ classNames['sendbox__text-area-input'],
66
+ classNames['sendbox__text-area-shared'],
67
+ classNames['sendbox__text-area-input--scroll']
68
+ )}
69
+ data-testid={props['data-testid']}
70
+ onInput={props.onInput}
71
+ onKeyDown={handleKeyDown}
72
+ placeholder={props.placeholder}
73
+ ref={ref}
74
+ rows={props.startRows ?? 1}
75
+ // eslint-disable-next-line no-magic-numbers
76
+ tabIndex={props.hidden ? -1 : undefined}
77
+ value={props.value}
78
+ />
79
+ </div>
80
+ );
81
+ });
82
+
83
+ TextArea.displayName = 'TextArea';
84
+
85
+ export default TextArea;
@@ -0,0 +1,49 @@
1
+ :global(.webchat-fluent) .sendbox__toolbar {
2
+ display: flex;
3
+ gap: 4px;
4
+ margin-inline-start: auto;
5
+ }
6
+
7
+ :global(.webchat-fluent) .sendbox__toolbar-button {
8
+ align-items: center;
9
+ appearance: none;
10
+ aspect-ratio: 1;
11
+ background: transparent;
12
+ border-radius: var(--webchat-borderRadiusSmall);
13
+ border: none;
14
+ cursor: pointer;
15
+ display: flex;
16
+ justify-content: center;
17
+ padding: 3px;
18
+ width: 32px;
19
+
20
+ > svg {
21
+ font-size: 20px;
22
+ pointer-events: none;
23
+ }
24
+
25
+ @media (hover: hover) {
26
+ &:not([aria-disabled="true"]):hover {
27
+ background-color: var(--webchat-colorSubtleBackgroundHover);
28
+ color: var(--webchat-colorCompoundBrandForeground1Hover);
29
+ }
30
+ }
31
+ &:not([aria-disabled="true"]):active {
32
+ background-color: var(--webchat-colorSubtleBackgroundPressed);
33
+ color: var(--webchat-colorCompoundBrandForeground1Pressed);
34
+ }
35
+ &[aria-disabled="true"] {
36
+ color: var(--webchat-colorNeutralForegroundDisabled);
37
+ cursor: not-allowed;
38
+ }
39
+ }
40
+
41
+ :global(.webchat-fluent) .sendbox__toolbar-separator {
42
+ align-self: center;
43
+ border-inline-end: 1px solid var(--webchat-colorNeutralStroke2);
44
+ height: 28px;
45
+
46
+ &:first-child, &:last-child, &:only-child {
47
+ display: none
48
+ }
49
+ }
@@ -0,0 +1,64 @@
1
+ import cx from 'classnames';
2
+ import React, { memo, type MouseEventHandler, type ReactNode } from 'react';
3
+ import styles from './Toolbar.module.css';
4
+ import { useStyles } from '../../styles';
5
+
6
+ const preventDefaultHandler: MouseEventHandler<HTMLButtonElement> = event => event.preventDefault();
7
+
8
+ export const ToolbarButton = memo(
9
+ (
10
+ props: Readonly<{
11
+ 'aria-label'?: string | undefined;
12
+ children?: ReactNode | undefined;
13
+ className?: string | undefined;
14
+ 'data-testid'?: string | undefined;
15
+ disabled?: boolean | undefined;
16
+ onClick?: MouseEventHandler<HTMLButtonElement> | undefined;
17
+ type?: 'button' | 'submit' | undefined;
18
+ }>
19
+ ) => {
20
+ const classNames = useStyles(styles);
21
+
22
+ return (
23
+ <button
24
+ aria-label={props['aria-label']}
25
+ className={cx(classNames['sendbox__toolbar-button'], props.className)}
26
+ data-testid={props['data-testid']}
27
+ onClick={props.disabled ? preventDefaultHandler : props.onClick}
28
+ type={props.type === 'submit' ? 'submit' : 'button'}
29
+ {...(props.disabled && {
30
+ 'aria-disabled': 'true',
31
+ tabIndex: -1
32
+ })}
33
+ >
34
+ {props.children}
35
+ </button>
36
+ );
37
+ }
38
+ );
39
+
40
+ ToolbarButton.displayName = 'ToolbarButton';
41
+
42
+ export const Toolbar = memo((props: Readonly<{ children?: ReactNode | undefined; className?: string | undefined }>) => {
43
+ const classNames = useStyles(styles);
44
+
45
+ return <div className={cx(classNames['sendbox__toolbar'], props.className)}>{props.children}</div>;
46
+ });
47
+
48
+ Toolbar.displayName = 'Toolbar';
49
+
50
+ export const ToolbarSeparator = memo(
51
+ (props: Readonly<{ children?: ReactNode | undefined; className?: string | undefined }>) => {
52
+ const classNames = useStyles(styles);
53
+
54
+ return (
55
+ <div
56
+ aria-orientation="vertical"
57
+ className={cx(classNames['sendbox__toolbar-separator'], props.className)}
58
+ role="separator"
59
+ />
60
+ );
61
+ }
62
+ );
63
+
64
+ ToolbarSeparator.displayName = 'ToolbarSeparator';
@@ -0,0 +1,58 @@
1
+ :global(.webchat-fluent) .sendbox {
2
+ color: var(--webchat-colorNeutralForeground1);
3
+ font-family: var(--webchat-fontFamilyBase);
4
+ padding: 0 10px 10px;
5
+ text-rendering: optimizeLegibility;
6
+ }
7
+
8
+ :global(.webchat-fluent) .sendbox__sendbox {
9
+ background-color: var(--webchat-colorNeutralBackground1);
10
+ border-radius: var(--webchat-borderRadiusLarge);
11
+ border: 1px solid var(--webchat-colorNeutralStroke1);
12
+ display: grid;
13
+ font-family: var(--webchat-fontFamilyBase);
14
+ font-size: 14px;
15
+ gap: 6px;
16
+ line-height: 20px;
17
+ padding: 8px;
18
+ position: relative;
19
+
20
+ &:focus-within {
21
+ border-color: var(--webchat-colorNeutralStroke1Selected);
22
+ /* TODO clarify with design the color:
23
+ - Teams is using colorCompoundBrandForeground1
24
+ - Fluent is using colorCompoundBrandStroke
25
+ - we are using colorCompoundBrandForeground1Hover */
26
+ box-shadow: inset 0 -6px 0 -3px var(--webchat-colorCompoundBrandForeground1Hover);
27
+ }
28
+ }
29
+
30
+ :global(.webchat-fluent) .sendbox__sendbox-text {
31
+ background-color: transparent;
32
+ border: none;
33
+ flex: auto;
34
+ font-family: var(--webchat-fontFamilyBase);
35
+ font-size: 14px;
36
+ line-height: 20px;
37
+ outline: none;
38
+ padding: 4px 4px 0;
39
+ resize: none;
40
+ }
41
+
42
+ :global(.webchat-fluent) .sendbox__sendbox-controls {
43
+ align-items: center;
44
+ display: flex;
45
+ padding-inline-start: 4px;
46
+ }
47
+
48
+ :global(.webchat-fluent) .sendbox__text-counter {
49
+ color: var(--webchat-colorNeutralForeground4);
50
+ cursor: default;
51
+ font-family: var(--webchat-fontFamilyNumeric);
52
+ font-size: 10px;
53
+ line-height: 14px;
54
+ }
55
+
56
+ :global(.webchat-fluent) .sendbox__text-counter--error {
57
+ color: var(--webchat-colorStatusDangerForeground1);
58
+ }
@@ -0,0 +1,169 @@
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 testIds from '../../testIds';
7
+ import DropZone from '../DropZone';
8
+ import SuggestedActions from '../SuggestedActions';
9
+ import { TelephoneKeypadSurrogate, useTelephoneKeypadShown, type DTMF } from '../TelephoneKeypad';
10
+ import AddAttachmentButton from './AddAttachmentButton';
11
+ import Attachments from './Attachments';
12
+ import ErrorMessage from './ErrorMessage';
13
+ import TelephoneKeypadToolbarButton from './TelephoneKeypadToolbarButton';
14
+ import TextArea from './TextArea';
15
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from './Toolbar';
16
+ import useSubmitError from './private/useSubmitError';
17
+ import useUniqueId from './private/useUniqueId';
18
+ import styles from './index.module.css';
19
+ import { useStyles } from '../../styles';
20
+
21
+ const { useStyleOptions, useMakeThumbnail, useLocalizer, useSendBoxAttachments, useSendMessage } = hooks;
22
+
23
+ function SendBox(
24
+ props: Readonly<{
25
+ className?: string | undefined;
26
+ placeholder?: string | undefined;
27
+ }>
28
+ ) {
29
+ const inputRef = useRef<HTMLTextAreaElement>(null);
30
+ const [message, setMessage] = useState('');
31
+ const [attachments, setAttachments] = useSendBoxAttachments();
32
+ const [{ hideTelephoneKeypadButton, hideUploadButton, maxMessageLength }] = useStyleOptions();
33
+ const isMessageLengthExceeded = !!maxMessageLength && message.length > maxMessageLength;
34
+ const classNames = useStyles(styles);
35
+ const localize = useLocalizer();
36
+ const sendMessage = useSendMessage();
37
+ const makeThumbnail = useMakeThumbnail();
38
+ const errorMessageId = useUniqueId('sendbox__error-message-id');
39
+ const [errorRef, errorMessage] = useSubmitError({ message, attachments });
40
+ const [telephoneKeypadShown] = useTelephoneKeypadShown();
41
+
42
+ const attachmentsRef = useRefFrom(attachments);
43
+ const messageRef = useRefFrom(message);
44
+
45
+ const handleSendBoxClick = useCallback<MouseEventHandler>(
46
+ event => {
47
+ if ('tabIndex' in event.target && typeof event.target.tabIndex === 'number' && event.target.tabIndex >= 0) {
48
+ return;
49
+ }
50
+
51
+ // TODO: Should call `useFocus('sendBox')`.
52
+ inputRef.current?.focus();
53
+ },
54
+ [inputRef]
55
+ );
56
+
57
+ const handleMessageChange: React.FormEventHandler<HTMLTextAreaElement> = useCallback(
58
+ event => setMessage(event.currentTarget.value),
59
+ [setMessage]
60
+ );
61
+
62
+ const handleAddFiles = useCallback(
63
+ async (inputFiles: File[]) => {
64
+ const newAttachments = Object.freeze(
65
+ await Promise.all(
66
+ inputFiles.map(file =>
67
+ makeThumbnail(file).then(thumbnailURL =>
68
+ Object.freeze({
69
+ blob: file,
70
+ ...(thumbnailURL && { thumbnailURL })
71
+ })
72
+ )
73
+ )
74
+ )
75
+ );
76
+
77
+ setAttachments(newAttachments);
78
+
79
+ // TODO: Currently in the UX, we have no way to remove attachments.
80
+ // Keep concatenating doesn't make sense in current UX.
81
+ // When end-user can remove attachment, we should enable the code again.
82
+ // setAttachments(attachments => attachments.concat(newAttachments));
83
+ },
84
+ [makeThumbnail, setAttachments]
85
+ );
86
+
87
+ const handleFormSubmit: FormEventHandler<HTMLFormElement> = useCallback(
88
+ event => {
89
+ event.preventDefault();
90
+
91
+ if (errorRef.current !== 'empty' && !isMessageLengthExceeded) {
92
+ sendMessage(messageRef.current, undefined, { attachments: attachmentsRef.current });
93
+
94
+ setMessage('');
95
+ setAttachments([]);
96
+ }
97
+
98
+ // TODO: Should call `useFocus('sendBox')`.
99
+ inputRef.current?.focus();
100
+ },
101
+ [attachmentsRef, messageRef, sendMessage, setAttachments, setMessage, isMessageLengthExceeded, errorRef, inputRef]
102
+ );
103
+
104
+ const handleTelephoneKeypadButtonClick = useCallback(
105
+ // TODO: We need more official way of sending DTMF.
106
+ (dtmf: DTMF) => sendMessage(`/DTMF ${dtmf}`),
107
+ [sendMessage]
108
+ );
109
+
110
+ const aria = {
111
+ 'aria-invalid': 'false' as const,
112
+ ...(errorMessage && {
113
+ 'aria-invalid': 'true' as const,
114
+ 'aria-errormessage': errorMessageId
115
+ })
116
+ };
117
+
118
+ return (
119
+ <form {...aria} className={cx(classNames['sendbox'], props.className)} onSubmit={handleFormSubmit}>
120
+ <SuggestedActions />
121
+ <div className={cx(classNames['sendbox__sendbox'])} onClickCapture={handleSendBoxClick}>
122
+ <TelephoneKeypadSurrogate
123
+ autoFocus={true}
124
+ isHorizontal={false}
125
+ onButtonClick={handleTelephoneKeypadButtonClick}
126
+ />
127
+ <TextArea
128
+ aria-label={isMessageLengthExceeded ? localize('TEXT_INPUT_LENGTH_EXCEEDED_ALT') : localize('TEXT_INPUT_ALT')}
129
+ className={classNames['sendbox__sendbox-text']}
130
+ data-testid={testIds.sendBoxTextBox}
131
+ hidden={telephoneKeypadShown}
132
+ onInput={handleMessageChange}
133
+ placeholder={props.placeholder ?? localize('TEXT_INPUT_PLACEHOLDER')}
134
+ ref={inputRef}
135
+ value={message}
136
+ />
137
+ <Attachments attachments={attachments} />
138
+ <div className={cx(classNames['sendbox__sendbox-controls'])}>
139
+ {maxMessageLength && (
140
+ <div
141
+ className={cx(classNames['sendbox__text-counter'], {
142
+ [classNames['sendbox__text-counter--error']]: isMessageLengthExceeded
143
+ })}
144
+ >
145
+ {`${message.length}/${maxMessageLength}`}
146
+ </div>
147
+ )}
148
+ <Toolbar>
149
+ {!hideTelephoneKeypadButton && <TelephoneKeypadToolbarButton />}
150
+ {!hideUploadButton && <AddAttachmentButton onFilesAdded={handleAddFiles} />}
151
+ <ToolbarSeparator />
152
+ <ToolbarButton
153
+ aria-label={localize('TEXT_INPUT_SEND_BUTTON_ALT')}
154
+ data-testid={testIds.sendBoxSendButton}
155
+ disabled={isMessageLengthExceeded}
156
+ type="submit"
157
+ >
158
+ <SendIcon />
159
+ </ToolbarButton>
160
+ </Toolbar>
161
+ </div>
162
+ <DropZone onFilesAdded={handleAddFiles} />
163
+ <ErrorMessage error={errorMessage} id={errorMessageId} />
164
+ </div>
165
+ </form>
166
+ );
167
+ }
168
+
169
+ 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
+ }