botframework-webchat-fluent-theme 4.17.1 → 4.18.1-hotfix.20260127.b53acdf
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.
- package/dist/botframework-webchat-fluent-theme.css.map +1 -1
- package/dist/botframework-webchat-fluent-theme.d.mts +23 -5
- package/dist/botframework-webchat-fluent-theme.d.ts +23 -5
- package/dist/botframework-webchat-fluent-theme.development.css.map +1 -1
- package/dist/botframework-webchat-fluent-theme.development.js +11 -1
- package/dist/botframework-webchat-fluent-theme.development.js.map +1 -1
- package/dist/botframework-webchat-fluent-theme.js +1 -1
- package/dist/botframework-webchat-fluent-theme.js.map +1 -1
- package/dist/botframework-webchat-fluent-theme.mjs +1 -1
- package/dist/botframework-webchat-fluent-theme.mjs.map +1 -1
- package/dist/botframework-webchat-fluent-theme.production.min.css.map +1 -1
- package/dist/botframework-webchat-fluent-theme.production.min.js +11 -1
- package/dist/botframework-webchat-fluent-theme.production.min.js.map +1 -1
- package/package.json +27 -26
- package/src/bundle.ts +2 -0
- package/src/components/activity/ActivityDecorator.module.css +432 -0
- package/src/components/activity/ActivityDecorator.tsx +26 -0
- package/src/components/activity/ActivityLoader.module.css +10 -0
- package/src/components/activity/ActivityLoader.tsx +21 -0
- package/src/components/activity/CopilotMessageHeader.module.css +38 -0
- package/src/components/activity/CopilotMessageHeader.tsx +49 -0
- package/src/components/activity/index.ts +2 -0
- package/src/components/activity/private/isAIGeneratedActivity.ts +5 -0
- package/src/components/activity/private/useActivityAuthor.ts +16 -0
- package/src/components/activity/private/useActivityStyleOptions.ts +19 -0
- package/src/components/assets/AssetComposer.tsx +37 -0
- package/src/components/assets/AssetName.ts +1 -0
- package/src/components/assets/SlidingDots.tsx +61 -0
- package/src/components/assets/private/Context.ts +24 -0
- package/src/components/assets/private/useAssetURL.ts +12 -0
- package/src/components/assets/private/useContext.ts +8 -0
- package/src/components/dropZone/DropZone.module.css +3 -4
- package/src/components/dropZone/DropZone.tsx +41 -6
- package/src/components/linerActivity/index.ts +2 -0
- package/src/components/linerActivity/private/LinerActivity.tsx +20 -0
- package/src/components/linerActivity/private/LinerMessageActivity.module.css +28 -0
- package/src/components/linerActivity/private/isLinerMessageActivity.ts +7 -0
- package/src/components/preChatActivity/PreChatMessageActivity.module.css +56 -0
- package/src/components/preChatActivity/PreChatMessageActivity.tsx +60 -0
- package/src/components/preChatActivity/StarterPromptsCardAction.module.css +126 -0
- package/src/components/preChatActivity/StarterPromptsCardAction.tsx +75 -0
- package/src/components/preChatActivity/StarterPromptsToolbar.module.css +18 -0
- package/src/components/preChatActivity/StarterPromptsToolbar.tsx +47 -0
- package/src/components/preChatActivity/index.tsx +2 -0
- package/src/components/preChatActivity/isPreChatMessageActivity.ts +7 -0
- package/src/components/sendBox/Attachments.module.css +1 -1
- package/src/components/sendBox/Attachments.tsx +5 -4
- package/src/components/sendBox/ErrorMessage.tsx +15 -4
- package/src/components/sendBox/SendBox.module.css +21 -6
- package/src/components/sendBox/SendBox.tsx +95 -56
- package/src/components/sendBox/TextArea.module.css +27 -8
- package/src/components/sendBox/TextArea.tsx +60 -31
- package/src/components/sendBox/Toolbar.module.css +15 -7
- package/src/components/sendBox/Toolbar.tsx +17 -7
- package/src/components/sendBox/index.tsx +1 -1
- package/src/components/sendBox/private/useSubmitError.ts +17 -4
- package/src/components/sendBox/private/useTranscriptNavigation.ts +53 -0
- package/src/components/suggestedActions/AccessibleButton.tsx +15 -13
- package/src/components/suggestedActions/SuggestedAction.module.css +14 -13
- package/src/components/suggestedActions/SuggestedAction.tsx +7 -4
- package/src/components/suggestedActions/SuggestedActions.module.css +2 -3
- package/src/components/suggestedActions/SuggestedActions.tsx +49 -46
- package/src/components/telephoneKeypad/private/Button.module.css +2 -3
- package/src/components/telephoneKeypad/private/Button.tsx +1 -5
- package/src/components/telephoneKeypad/private/TelephoneKeypad.module.css +0 -1
- package/src/components/telephoneKeypad/private/TelephoneKeypad.tsx +4 -8
- package/src/components/telephoneKeypad/types.ts +1 -1
- package/src/components/theme/Theme.module.css +665 -15
- package/src/components/theme/Theme.tsx +4 -3
- package/src/components/typingIndicator/SlidingDotsTypingIndicator.module.css +12 -0
- package/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx +19 -0
- package/src/env.d.ts +1 -7
- package/src/external.umd/botframework-webchat-api/decorator.ts +1 -0
- package/src/external.umd/botframework-webchat-component/decorator.ts +1 -0
- package/src/external.umd/botframework-webchat-component/index.ts +5 -0
- package/src/external.umd/botframework-webchat-component/internal.ts +1 -0
- package/src/icons/AddDocumentIcon.tsx +8 -16
- package/src/icons/AttachmentIcon.tsx +8 -16
- package/src/icons/InfoSmallIcon.tsx +7 -15
- package/src/icons/SendIcon.tsx +7 -15
- package/src/icons/TelephoneKeypadIcon.tsx +7 -15
- package/src/index.ts +2 -4
- package/src/private/FluentThemeProvider.tsx +91 -10
- package/src/private/VariantComposer.ts +29 -0
- package/src/private/createComposer.tsx +16 -0
- package/src/private/useVariants.ts +7 -0
- package/src/styles/createStyles.ts +5 -0
- package/src/styles/index.ts +3 -2
- package/src/styles/useStyles.ts +2 -19
- package/src/styles/useVariantClassName.ts +16 -0
- package/src/testIds.ts +3 -0
- package/src/tsconfig.json +12 -10
- package/src/types/PropsOf.ts +2 -5
- package/src/external.umd/botframework-webchat-component.ts +0 -4
- package/src/styles/injectStyle.ts +0 -9
- /package/src/external.umd/{botframework-webchat-api.ts → botframework-webchat-api/index.ts} +0 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Components, hooks } from 'botframework-webchat-component';
|
|
2
|
+
import { type DirectLineCardAction } from 'botframework-webchat-core';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import React, { memo, useCallback, useMemo } from 'react';
|
|
5
|
+
import { useRefFrom } from 'use-ref-from';
|
|
6
|
+
import { useStyles } from '../../styles/index.js';
|
|
7
|
+
import testIds from '../../testIds.js';
|
|
8
|
+
import styles from './StarterPromptsCardAction.module.css';
|
|
9
|
+
|
|
10
|
+
const { useFocus, useRenderMarkdownAsHTML, useSendBoxValue, useUIState } = hooks;
|
|
11
|
+
const { MonochromeImageMasker } = Components;
|
|
12
|
+
|
|
13
|
+
type Props = Readonly<{
|
|
14
|
+
className?: string | undefined;
|
|
15
|
+
messageBackAction?: (DirectLineCardAction & { type: 'messageBack' }) | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
const StarterPromptsCardAction = ({ className, messageBackAction }: Props) => {
|
|
19
|
+
const [_, setSendBoxValue] = useSendBoxValue();
|
|
20
|
+
const [uiState] = useUIState();
|
|
21
|
+
const classNames = useStyles(styles);
|
|
22
|
+
const focus = useFocus();
|
|
23
|
+
const inputTextRef = useRefFrom(messageBackAction?.displayText || messageBackAction?.text || '');
|
|
24
|
+
const renderMarkdownAsHTML = useRenderMarkdownAsHTML('message activity');
|
|
25
|
+
const subtitleHTML = useMemo<{ __html: string } | undefined>(
|
|
26
|
+
() => (renderMarkdownAsHTML ? { __html: renderMarkdownAsHTML(messageBackAction?.text || '') } : undefined),
|
|
27
|
+
[messageBackAction?.text, renderMarkdownAsHTML]
|
|
28
|
+
);
|
|
29
|
+
const disabled = uiState === 'disabled';
|
|
30
|
+
const title = messageBackAction && 'title' in messageBackAction && messageBackAction.title;
|
|
31
|
+
|
|
32
|
+
// Every starter prompt card action must have "title" field.
|
|
33
|
+
const shouldShowBlueprint = uiState === 'blueprint' || !title;
|
|
34
|
+
|
|
35
|
+
const handleClick = useCallback(() => {
|
|
36
|
+
setSendBoxValue(inputTextRef.current);
|
|
37
|
+
|
|
38
|
+
// Focus on the send box after the value is "pasted."
|
|
39
|
+
focus('sendBox');
|
|
40
|
+
}, [focus, inputTextRef, setSendBoxValue]);
|
|
41
|
+
|
|
42
|
+
return shouldShowBlueprint ? (
|
|
43
|
+
<div
|
|
44
|
+
className={cx(className, classNames['pre-chat-message-activity__card-action-box'])}
|
|
45
|
+
data-testid={testIds.preChatMessageActivityStarterPromptsCardAction}
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
<button
|
|
49
|
+
aria-disabled={disabled ? true : undefined}
|
|
50
|
+
className={cx(className, classNames['pre-chat-message-activity__card-action-box'])}
|
|
51
|
+
data-testid={testIds.preChatMessageActivityStarterPromptsCardAction}
|
|
52
|
+
onClick={disabled ? undefined : handleClick}
|
|
53
|
+
// eslint-disable-next-line no-magic-numbers
|
|
54
|
+
tabIndex={disabled ? -1 : undefined}
|
|
55
|
+
type="button"
|
|
56
|
+
>
|
|
57
|
+
<div className={classNames['pre-chat-message-activity__card-action-title']}>{title}</div>
|
|
58
|
+
{'image' in messageBackAction && messageBackAction.image && (
|
|
59
|
+
<MonochromeImageMasker
|
|
60
|
+
className={classNames['pre-chat-message-activity__card-action-image']}
|
|
61
|
+
src={messageBackAction.image}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
<div
|
|
65
|
+
className={classNames['pre-chat-message-activity__card-action-subtitle']}
|
|
66
|
+
// eslint-disable-next-line react/no-danger
|
|
67
|
+
dangerouslySetInnerHTML={subtitleHTML}
|
|
68
|
+
/>
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
StarterPromptsCardAction.displayName = 'StarterPromptsCardAction';
|
|
74
|
+
|
|
75
|
+
export default memo(StarterPromptsCardAction);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
:global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar {
|
|
2
|
+
container-name: webchat-container;
|
|
3
|
+
container-type: inline-size;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
:global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar-grid {
|
|
7
|
+
display: grid;
|
|
8
|
+
gap: var(--webchat-spacingHorizontalM);
|
|
9
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* TODO: What is the good width to show as 3 columns? Web Chat, by default, has a bubble max width of 480px. */
|
|
14
|
+
@container webchat-container (width <= 480px) {
|
|
15
|
+
:global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar-grid {
|
|
16
|
+
grid-template-columns: 1fr;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { hooks } from 'botframework-webchat-api';
|
|
2
|
+
import { type DirectLineCardAction } from 'botframework-webchat-core';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import React, { Fragment, memo } from 'react';
|
|
5
|
+
import { useStyles } from '../../styles/index.js';
|
|
6
|
+
import StarterPromptsCardAction from './StarterPromptsCardAction.js';
|
|
7
|
+
import styles from './StarterPromptsToolbar.module.css';
|
|
8
|
+
|
|
9
|
+
const { useUIState } = hooks;
|
|
10
|
+
|
|
11
|
+
type Props = Readonly<{
|
|
12
|
+
cardActions: readonly DirectLineCardAction[];
|
|
13
|
+
className?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
const StarterPromptsToolbar = ({ cardActions, className }: Props) => {
|
|
17
|
+
const classNames = useStyles(styles);
|
|
18
|
+
const [uiState] = useUIState();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
// TODO: Accessibility-wise, this should be role="toolbar" with keyboard navigation.
|
|
22
|
+
<div className={cx(className, classNames['pre-chat-message-activity__card-action-toolbar'])}>
|
|
23
|
+
<div className={classNames['pre-chat-message-activity__card-action-toolbar-grid']}>
|
|
24
|
+
{uiState === 'blueprint' ? (
|
|
25
|
+
<Fragment>
|
|
26
|
+
<StarterPromptsCardAction />
|
|
27
|
+
<StarterPromptsCardAction />
|
|
28
|
+
<StarterPromptsCardAction />
|
|
29
|
+
</Fragment>
|
|
30
|
+
) : (
|
|
31
|
+
cardActions
|
|
32
|
+
.filter<DirectLineCardAction & { type: 'messageBack' }>(
|
|
33
|
+
(card: DirectLineCardAction): card is DirectLineCardAction & { type: 'messageBack' } =>
|
|
34
|
+
card.type === 'messageBack'
|
|
35
|
+
)
|
|
36
|
+
// There is no other usable keys in card actions.
|
|
37
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
38
|
+
.map((cardAction, index) => <StarterPromptsCardAction key={index} messageBackAction={cardAction} />)
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
StarterPromptsToolbar.displayName = 'StarterPromptsToolbar';
|
|
46
|
+
|
|
47
|
+
export default memo(StarterPromptsToolbar);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getOrgSchemaMessage, type WebChatActivity } from 'botframework-webchat-core';
|
|
2
|
+
|
|
3
|
+
export default function isPreChatMessageActivity(
|
|
4
|
+
activity: undefined | WebChatActivity
|
|
5
|
+
): activity is WebChatActivity & { type: 'message' } {
|
|
6
|
+
return !!(activity && getOrgSchemaMessage(activity?.entities || [])?.keywords?.includes('PreChatMessage'));
|
|
7
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { hooks } from 'botframework-webchat-component';
|
|
2
|
-
import React, { memo } from 'react';
|
|
3
2
|
import cx from 'classnames';
|
|
4
|
-
import
|
|
3
|
+
import React, { memo } from 'react';
|
|
5
4
|
import { useStyles } from '../../styles';
|
|
5
|
+
import styles from './Attachments.module.css';
|
|
6
6
|
|
|
7
|
-
const { useLocalizer } = hooks;
|
|
7
|
+
const { useLocalizer, useUIState } = hooks;
|
|
8
8
|
|
|
9
9
|
const attachmentsPluralStringIds = {
|
|
10
10
|
one: 'TEXT_INPUT_ATTACHMENTS_ONE',
|
|
@@ -21,10 +21,11 @@ function Attachments({
|
|
|
21
21
|
readonly attachments: readonly Readonly<{ blob: Blob | File; thumbnailURL?: URL | undefined }>[];
|
|
22
22
|
readonly className?: string | undefined;
|
|
23
23
|
}>) {
|
|
24
|
+
const [uiState] = useUIState();
|
|
24
25
|
const classNames = useStyles(styles);
|
|
25
26
|
const localizeWithPlural = useLocalizer({ plural: true });
|
|
26
27
|
|
|
27
|
-
return attachments.length ? (
|
|
28
|
+
return uiState !== 'blueprint' && attachments.length ? (
|
|
28
29
|
<div className={cx(classNames['sendbox__attachment'], className)}>
|
|
29
30
|
{localizeWithPlural(attachmentsPluralStringIds, attachments.length)}
|
|
30
31
|
</div>
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import { useLiveRegion } from 'botframework-webchat-component/internal';
|
|
1
2
|
import React, { memo } from 'react';
|
|
2
|
-
import styles from './ErrorMessage.module.css';
|
|
3
3
|
import { useStyles } from '../../styles';
|
|
4
|
+
import styles from './ErrorMessage.module.css';
|
|
5
|
+
|
|
6
|
+
type ErrorMessageProps = Readonly<{
|
|
7
|
+
error?: string | undefined;
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
4
10
|
|
|
5
|
-
function ErrorMessage(
|
|
11
|
+
function ErrorMessage({ error, id }: ErrorMessageProps) {
|
|
6
12
|
const classNames = useStyles(styles);
|
|
13
|
+
|
|
14
|
+
useLiveRegion(() => error && <div className="sendbox__error-message__status">{error}</div>, [error]);
|
|
15
|
+
|
|
7
16
|
return (
|
|
8
17
|
// eslint-disable-next-line react/forbid-dom-props
|
|
9
|
-
<span className={classNames['sendbox__error-message']} id={
|
|
10
|
-
{
|
|
18
|
+
<span className={classNames['sendbox__error-message']} id={id}>
|
|
19
|
+
{error}
|
|
11
20
|
</span>
|
|
12
21
|
);
|
|
13
22
|
}
|
|
14
23
|
|
|
24
|
+
ErrorMessage.displayName = 'ErrorMessage';
|
|
25
|
+
|
|
15
26
|
export default memo(ErrorMessage);
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
:global(.webchat-fluent) .sendbox {
|
|
2
|
+
--webchat__sendbox--padding: var(--webchat__padding--sendbox);
|
|
3
|
+
|
|
2
4
|
color: var(--webchat-colorNeutralForeground1);
|
|
3
5
|
font-family: var(--webchat-fontFamilyBase);
|
|
4
|
-
padding:
|
|
6
|
+
padding: var(--webchat__sendbox--padding);
|
|
5
7
|
text-rendering: optimizeLegibility;
|
|
6
8
|
|
|
7
9
|
--webchat-sendbox-attachment-area-active: ;
|
|
8
10
|
--webchat-sendbox-border-radius: var(--webchat-borderRadiusLarge);
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
/* Copilot variant */
|
|
14
|
+
:global(.webchat-fluent) .sendbox.variant-copilot {
|
|
15
|
+
.sendbox__text-counter:not(.sendbox__text-counter--error) {
|
|
16
|
+
visibility: hidden;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
:global(.webchat-fluent) .sendbox__sendbox {
|
|
12
21
|
background-color: var(--webchat-colorNeutralBackground1);
|
|
13
22
|
border-radius: var(--webchat-sendbox-border-radius);
|
|
@@ -19,14 +28,13 @@
|
|
|
19
28
|
grid-template:
|
|
20
29
|
[telephone-keypad-start] 'text-area' [telephone-keypad-end]
|
|
21
30
|
var(--webchat-sendbox-attachment-area-active)
|
|
22
|
-
'controls' / [telephone-keypad] 1fr
|
|
23
|
-
;
|
|
31
|
+
'controls' / [telephone-keypad] 1fr;
|
|
24
32
|
line-height: 20px;
|
|
25
33
|
padding: 8px;
|
|
26
34
|
position: relative;
|
|
27
35
|
|
|
28
36
|
&:has(.sendbox__attachment--in-grid) {
|
|
29
|
-
--webchat-sendbox-attachment-area-active: 'attachment'
|
|
37
|
+
--webchat-sendbox-attachment-area-active: 'attachment';
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
&:focus-within {
|
|
@@ -39,7 +47,7 @@
|
|
|
39
47
|
border-bottom: var(--webchat-strokeWidthThicker) solid var(--webchat-colorCompoundBrandForeground1Hover);
|
|
40
48
|
bottom: -1px;
|
|
41
49
|
clip-path: inset(calc(100% - var(--webchat-strokeWidthThicker)) 50% 0 50%);
|
|
42
|
-
content:
|
|
50
|
+
content: '';
|
|
43
51
|
height: var(--webchat-sendbox-border-radius);
|
|
44
52
|
left: -1px;
|
|
45
53
|
position: absolute;
|
|
@@ -77,8 +85,15 @@
|
|
|
77
85
|
font-size: 14px;
|
|
78
86
|
line-height: 20px;
|
|
79
87
|
outline: none;
|
|
80
|
-
|
|
88
|
+
margin: var(--webchat-spacingVerticalXS) var(--webchat-spacingHorizontalXS) var(--webchat-spacingVerticalNone);
|
|
81
89
|
resize: none;
|
|
90
|
+
|
|
91
|
+
/* Prevent zoom on focus on iOS */
|
|
92
|
+
@media only screen and (hover: none) and (pointer: coarse) {
|
|
93
|
+
&:focus-within {
|
|
94
|
+
font-size: 16px;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
:global(.webchat-fluent) .sendbox__sendbox-controls {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import { hooks
|
|
1
|
+
import { hooks } from 'botframework-webchat-component';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
memo,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type FormEventHandler,
|
|
10
|
+
type MouseEventHandler
|
|
11
|
+
} from 'react';
|
|
4
12
|
import { useRefFrom } from 'use-ref-from';
|
|
5
13
|
import { SendIcon } from '../../icons';
|
|
14
|
+
import { useStyles, useVariantClassName } from '../../styles';
|
|
6
15
|
import testIds from '../../testIds';
|
|
7
16
|
import { DropZone } from '../dropZone';
|
|
8
17
|
import { SuggestedActions } from '../suggestedActions';
|
|
@@ -10,13 +19,13 @@ import { TelephoneKeypadSurrogate, useTelephoneKeypadShown, type DTMF } from '..
|
|
|
10
19
|
import AddAttachmentButton from './AddAttachmentButton';
|
|
11
20
|
import Attachments from './Attachments';
|
|
12
21
|
import ErrorMessage from './ErrorMessage';
|
|
13
|
-
import TelephoneKeypadToolbarButton from './TelephoneKeypadToolbarButton';
|
|
14
|
-
import TextArea from './TextArea';
|
|
15
|
-
import { Toolbar, ToolbarButton, ToolbarSeparator } from './Toolbar';
|
|
16
22
|
import useSubmitError from './private/useSubmitError';
|
|
23
|
+
import useTranscriptNavigation from './private/useTranscriptNavigation';
|
|
17
24
|
import useUniqueId from './private/useUniqueId';
|
|
18
25
|
import styles from './SendBox.module.css';
|
|
19
|
-
import
|
|
26
|
+
import TelephoneKeypadToolbarButton from './TelephoneKeypadToolbarButton';
|
|
27
|
+
import TextArea from './TextArea';
|
|
28
|
+
import { Toolbar, ToolbarButton, ToolbarSeparator } from './Toolbar';
|
|
20
29
|
|
|
21
30
|
const {
|
|
22
31
|
useFocus,
|
|
@@ -24,57 +33,55 @@ const {
|
|
|
24
33
|
useMakeThumbnail,
|
|
25
34
|
useRegisterFocusSendBox,
|
|
26
35
|
useSendBoxAttachments,
|
|
36
|
+
useSendBoxValue,
|
|
27
37
|
useSendMessage,
|
|
28
|
-
useStyleOptions
|
|
38
|
+
useStyleOptions,
|
|
39
|
+
useUIState
|
|
29
40
|
} = hooks;
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const [attachments, setAttachments] = useSendBoxAttachments();
|
|
42
|
+
type Props = Readonly<{
|
|
43
|
+
className?: string | undefined;
|
|
44
|
+
completion?: ReactNode | undefined;
|
|
45
|
+
isPrimary?: boolean | undefined;
|
|
46
|
+
placeholder?: string | undefined;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
function SendBox(props: Props) {
|
|
40
50
|
const [{ hideTelephoneKeypadButton, hideUploadButton, maxMessageLength }] = useStyleOptions();
|
|
41
|
-
const
|
|
51
|
+
const [attachments, setAttachments] = useSendBoxAttachments();
|
|
52
|
+
const [globalMessage, setGlobalMessage] = useSendBoxValue();
|
|
53
|
+
const [localMessage, setLocalMessage] = useState('');
|
|
54
|
+
const [telephoneKeypadShown] = useTelephoneKeypadShown();
|
|
55
|
+
const [uiState] = useUIState();
|
|
42
56
|
const classNames = useStyles(styles);
|
|
57
|
+
const variantClassName = useVariantClassName(styles);
|
|
58
|
+
const errorMessageId = useUniqueId('sendbox__error-message-id');
|
|
59
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
43
60
|
const localize = useLocalizer();
|
|
44
|
-
const sendMessage = useSendMessage();
|
|
45
61
|
const makeThumbnail = useMakeThumbnail();
|
|
46
|
-
const
|
|
47
|
-
const [errorRef, errorMessage] = useSubmitError({ message, attachments });
|
|
48
|
-
const [telephoneKeypadShown] = useTelephoneKeypadShown();
|
|
62
|
+
const sendMessage = useSendMessage();
|
|
49
63
|
const setFocus = useFocus();
|
|
50
64
|
|
|
65
|
+
const message = props.isPrimary ? globalMessage : localMessage;
|
|
66
|
+
const setMessage = props.isPrimary ? setGlobalMessage : setLocalMessage;
|
|
67
|
+
const isBlueprint = uiState === 'blueprint';
|
|
68
|
+
|
|
69
|
+
const [errorMessage, commitLatestError] = useSubmitError({ message, attachments });
|
|
70
|
+
const isMessageLengthExceeded = !!maxMessageLength && message.length > maxMessageLength;
|
|
71
|
+
const shouldShowMessageLength =
|
|
72
|
+
!isBlueprint && !telephoneKeypadShown && maxMessageLength && isFinite(maxMessageLength);
|
|
73
|
+
const shouldShowTelephoneKeypad = !isBlueprint && telephoneKeypadShown;
|
|
74
|
+
|
|
51
75
|
useRegisterFocusSendBox(
|
|
52
76
|
useCallback(
|
|
53
|
-
({ noKeyboard
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
inputRef.current?.setAttribute('readonly', 'true');
|
|
62
|
-
// TODO: [P2] We should update this logic to handle quickly-successive `focusCallback`.
|
|
63
|
-
// If a succeeding `focusCallback` is being called, the `setTimeout` should run immediately.
|
|
64
|
-
// Or the second `focusCallback` should not set `readonly` to `true`.
|
|
65
|
-
// eslint-disable-next-line no-restricted-globals
|
|
66
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
67
|
-
inputRef.current?.focus();
|
|
68
|
-
if (typeof previousReadOnly !== 'string') {
|
|
69
|
-
inputRef.current?.removeAttribute('readonly');
|
|
70
|
-
} else {
|
|
71
|
-
inputRef.current?.setAttribute('readonly', previousReadOnly);
|
|
72
|
-
}
|
|
73
|
-
})()
|
|
74
|
-
);
|
|
75
|
-
} else {
|
|
76
|
-
inputRef.current?.focus();
|
|
77
|
-
}
|
|
77
|
+
({ noKeyboard }) => {
|
|
78
|
+
const { current } = inputRef;
|
|
79
|
+
|
|
80
|
+
// Setting `inputMode` to `none` temporarily to suppress soft keyboard in iOS.
|
|
81
|
+
// We will revert the change once the end-user tap on the send box.
|
|
82
|
+
// This code path is only triggered when the user press "send" button to send the message, instead of pressing ENTER key.
|
|
83
|
+
noKeyboard && current?.setAttribute('inputmode', 'none');
|
|
84
|
+
current?.focus();
|
|
78
85
|
},
|
|
79
86
|
[inputRef]
|
|
80
87
|
)
|
|
@@ -124,11 +131,14 @@ function SendBox(
|
|
|
124
131
|
[makeThumbnail, setAttachments]
|
|
125
132
|
);
|
|
126
133
|
|
|
134
|
+
const handleClick = useCallback(({ currentTarget }) => currentTarget.removeAttribute('inputmode'), []);
|
|
135
|
+
|
|
127
136
|
const handleFormSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
|
128
137
|
event => {
|
|
129
138
|
event.preventDefault();
|
|
139
|
+
const error = commitLatestError();
|
|
130
140
|
|
|
131
|
-
if (
|
|
141
|
+
if (error !== 'empty' && !isMessageLengthExceeded) {
|
|
132
142
|
sendMessage(messageRef.current, undefined, { attachments: attachmentsRef.current });
|
|
133
143
|
|
|
134
144
|
setMessage('');
|
|
@@ -137,32 +147,55 @@ function SendBox(
|
|
|
137
147
|
|
|
138
148
|
setFocus('sendBox');
|
|
139
149
|
},
|
|
140
|
-
[
|
|
150
|
+
[
|
|
151
|
+
commitLatestError,
|
|
152
|
+
isMessageLengthExceeded,
|
|
153
|
+
setFocus,
|
|
154
|
+
sendMessage,
|
|
155
|
+
setMessage,
|
|
156
|
+
messageRef,
|
|
157
|
+
attachmentsRef,
|
|
158
|
+
setAttachments
|
|
159
|
+
]
|
|
141
160
|
);
|
|
142
161
|
|
|
143
162
|
const handleTelephoneKeypadButtonClick = useCallback(
|
|
144
163
|
// TODO: We need more official way of sending DTMF.
|
|
145
|
-
(dtmf: DTMF) => sendMessage(`/
|
|
164
|
+
(dtmf: DTMF) => sendMessage(`/DTMFKey ${dtmf}`),
|
|
146
165
|
[sendMessage]
|
|
147
166
|
);
|
|
148
167
|
|
|
168
|
+
const handleTranscriptNavigation = useTranscriptNavigation();
|
|
169
|
+
|
|
149
170
|
const aria = {
|
|
150
171
|
'aria-invalid': 'false' as const,
|
|
151
172
|
...(errorMessage && {
|
|
152
|
-
'aria-
|
|
153
|
-
'aria-errormessage': errorMessageId
|
|
173
|
+
'aria-describedby': errorMessageId,
|
|
174
|
+
'aria-errormessage': errorMessageId,
|
|
175
|
+
'aria-invalid': 'true' as const
|
|
154
176
|
})
|
|
155
177
|
};
|
|
156
178
|
|
|
157
179
|
return (
|
|
158
|
-
<form
|
|
180
|
+
<form
|
|
181
|
+
{...aria}
|
|
182
|
+
className={cx(classNames['sendbox'], variantClassName, props.className)}
|
|
183
|
+
data-testid={testIds.sendBoxContainer}
|
|
184
|
+
onSubmit={handleFormSubmit}
|
|
185
|
+
>
|
|
159
186
|
<SuggestedActions />
|
|
160
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
className={cx(classNames['sendbox__sendbox'])}
|
|
189
|
+
onClickCapture={handleSendBoxClick}
|
|
190
|
+
onKeyDown={handleTranscriptNavigation}
|
|
191
|
+
>
|
|
161
192
|
<TextArea
|
|
162
193
|
aria-label={isMessageLengthExceeded ? localize('TEXT_INPUT_LENGTH_EXCEEDED_ALT') : localize('TEXT_INPUT_ALT')}
|
|
163
194
|
className={cx(classNames['sendbox__sendbox-text'], classNames['sendbox__text-area--in-grid'])}
|
|
195
|
+
completion={props.completion}
|
|
164
196
|
data-testid={testIds.sendBoxTextBox}
|
|
165
|
-
hidden={
|
|
197
|
+
hidden={shouldShowTelephoneKeypad}
|
|
198
|
+
onClick={handleClick}
|
|
166
199
|
onInput={handleMessageChange}
|
|
167
200
|
placeholder={props.placeholder ?? localize('TEXT_INPUT_PLACEHOLDER')}
|
|
168
201
|
ref={inputRef}
|
|
@@ -176,7 +209,7 @@ function SendBox(
|
|
|
176
209
|
/>
|
|
177
210
|
<Attachments attachments={attachments} className={classNames['sendbox__attachment--in-grid']} />
|
|
178
211
|
<div className={cx(classNames['sendbox__sendbox-controls'], classNames['sendbox__sendbox-controls--in-grid'])}>
|
|
179
|
-
{
|
|
212
|
+
{shouldShowMessageLength && (
|
|
180
213
|
<div
|
|
181
214
|
className={cx(classNames['sendbox__text-counter'], {
|
|
182
215
|
[classNames['sendbox__text-counter--error']]: isMessageLengthExceeded
|
|
@@ -192,7 +225,7 @@ function SendBox(
|
|
|
192
225
|
<ToolbarButton
|
|
193
226
|
aria-label={localize('TEXT_INPUT_SEND_BUTTON_ALT')}
|
|
194
227
|
data-testid={testIds.sendBoxSendButton}
|
|
195
|
-
disabled={isMessageLengthExceeded ||
|
|
228
|
+
disabled={isMessageLengthExceeded || shouldShowTelephoneKeypad}
|
|
196
229
|
type="submit"
|
|
197
230
|
>
|
|
198
231
|
<SendIcon />
|
|
@@ -206,4 +239,10 @@ function SendBox(
|
|
|
206
239
|
);
|
|
207
240
|
}
|
|
208
241
|
|
|
242
|
+
const PrimarySendBox = memo((props: Exclude<Props, 'primary'>) => <SendBox {...props} isPrimary={true} />);
|
|
243
|
+
|
|
244
|
+
PrimarySendBox.displayName = 'PrimarySendBox';
|
|
245
|
+
|
|
209
246
|
export default memo(SendBox);
|
|
247
|
+
|
|
248
|
+
export { PrimarySendBox };
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
display: grid;
|
|
3
3
|
grid-template-areas: 'main';
|
|
4
4
|
max-height: 200px;
|
|
5
|
-
overflow:
|
|
5
|
+
overflow: auto;
|
|
6
|
+
scrollbar-gutter: stable;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
:global(.webchat-fluent) .sendbox__text-area--hidden {
|
|
@@ -11,6 +12,22 @@
|
|
|
11
12
|
visibility: collapse;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
:global(.webchat-fluent) .sendbox__text-area--in-completion {
|
|
16
|
+
.sendbox__text-area-doppelganger {
|
|
17
|
+
visibility: unset;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sendbox__text-area-input {
|
|
21
|
+
background-color: transparent;
|
|
22
|
+
caret-color: var(--webchat-colorNeutralForeground1);
|
|
23
|
+
color: transparent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
textarea::placeholder {
|
|
27
|
+
color: transparent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
:global(.webchat-fluent) .sendbox__text-area-shared {
|
|
15
32
|
border: none;
|
|
16
33
|
font: inherit;
|
|
@@ -18,11 +35,12 @@
|
|
|
18
35
|
outline: inherit;
|
|
19
36
|
overflow-wrap: anywhere;
|
|
20
37
|
resize: inherit;
|
|
21
|
-
scrollbar-gutter: stable;
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
:global(.webchat-fluent) .sendbox__text-area-doppelganger {
|
|
25
|
-
overflow:
|
|
41
|
+
overflow: visible;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
user-select: none;
|
|
26
44
|
visibility: hidden;
|
|
27
45
|
white-space: pre-wrap;
|
|
28
46
|
}
|
|
@@ -30,11 +48,12 @@
|
|
|
30
48
|
:global(.webchat-fluent) .sendbox__text-area-input {
|
|
31
49
|
background-color: inherit;
|
|
32
50
|
color: currentColor;
|
|
33
|
-
|
|
51
|
+
contain: size;
|
|
52
|
+
overflow: hidden;
|
|
34
53
|
padding: 0;
|
|
35
54
|
}
|
|
36
55
|
|
|
37
|
-
:global(.webchat-fluent) .sendbox__text-area
|
|
56
|
+
:global(.webchat-fluent) .sendbox__text-area--scroll {
|
|
38
57
|
/* Edge uses -webkit-scrollbar if scrollbar-* is not set */
|
|
39
58
|
scrollbar-color: unset;
|
|
40
59
|
scrollbar-width: unset;
|
|
@@ -44,17 +63,17 @@
|
|
|
44
63
|
|
|
45
64
|
/* Chrome, Edge, and Safari */
|
|
46
65
|
&::-webkit-scrollbar {
|
|
47
|
-
width: 8px
|
|
66
|
+
width: 8px;
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
&::-webkit-scrollbar-track {
|
|
51
70
|
background-color: var(--webchat-colorNeutralBackground5);
|
|
52
|
-
border-radius: 16px
|
|
71
|
+
border-radius: 16px;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
&::-webkit-scrollbar-thumb {
|
|
56
75
|
background-color: var(--webchat-colorNeutralForeground2);
|
|
57
|
-
border-radius: 16px
|
|
76
|
+
border-radius: 16px;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
&::-webkit-scrollbar-corner {
|