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.
- package/dist/botframework-webchat-fluent-theme.development.css.map +1 -0
- package/dist/botframework-webchat-fluent-theme.development.js +2384 -0
- package/dist/botframework-webchat-fluent-theme.development.js.map +1 -0
- package/dist/botframework-webchat-fluent-theme.production.min.css.map +1 -0
- package/dist/botframework-webchat-fluent-theme.production.min.js +6 -16
- package/dist/botframework-webchat-fluent-theme.production.min.js.map +1 -1
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1062 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1076 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -7
- package/src/bundle.ts +9 -2
- package/src/components/DropZone.tsx +3 -0
- package/src/components/SendBox.tsx +3 -0
- package/src/components/SuggestedActions.tsx +3 -0
- package/src/components/TelephoneKeypad.tsx +1 -0
- package/src/components/Theme.module.css +60 -0
- package/src/components/Theme.tsx +11 -0
- package/src/components/dropZone/index.module.css +23 -0
- package/src/components/dropZone/index.tsx +107 -0
- package/src/components/sendbox/AddAttachmentButton.module.css +10 -0
- package/src/components/sendbox/AddAttachmentButton.tsx +65 -0
- package/src/components/sendbox/Attachments.module.css +7 -0
- package/src/components/sendbox/Attachments.tsx +31 -0
- package/src/components/sendbox/ErrorMessage.module.css +9 -0
- package/src/components/sendbox/ErrorMessage.tsx +15 -0
- package/src/components/sendbox/TelephoneKeypadToolbarButton.tsx +30 -0
- package/src/components/sendbox/TextArea.module.css +61 -0
- package/src/components/sendbox/TextArea.tsx +85 -0
- package/src/components/sendbox/Toolbar.module.css +49 -0
- package/src/components/sendbox/Toolbar.tsx +64 -0
- package/src/components/sendbox/index.module.css +58 -0
- package/src/components/sendbox/index.tsx +169 -0
- package/src/components/sendbox/private/useSubmitError.ts +46 -0
- package/src/components/sendbox/private/useUniqueId.ts +13 -0
- package/src/components/suggestedActions/AccessibleButton.tsx +59 -0
- package/src/components/suggestedActions/SuggestedAction.module.css +34 -0
- package/src/components/suggestedActions/SuggestedAction.tsx +87 -0
- package/src/components/suggestedActions/index.module.css +23 -0
- package/src/components/suggestedActions/index.tsx +98 -0
- package/src/components/suggestedActions/private/computeSuggestedActionText.ts +21 -0
- package/src/components/telephoneKeypad/Provider.tsx +22 -0
- package/src/components/telephoneKeypad/Surrogate.tsx +13 -0
- package/src/components/telephoneKeypad/index.ts +6 -0
- package/src/components/telephoneKeypad/private/Button.module.css +62 -0
- package/src/components/telephoneKeypad/private/Button.tsx +45 -0
- package/src/components/telephoneKeypad/private/Context.ts +18 -0
- package/src/components/telephoneKeypad/private/TelephoneKeypad.module.css +30 -0
- package/src/components/telephoneKeypad/private/TelephoneKeypad.tsx +137 -0
- package/src/components/telephoneKeypad/types.ts +1 -0
- package/src/components/telephoneKeypad/useShown.ts +9 -0
- package/src/env.d.ts +7 -0
- package/src/external.umd/botframework-webchat-api.ts +3 -0
- package/src/external.umd/botframework-webchat-component.ts +4 -0
- package/src/external.umd/react.ts +1 -0
- package/src/icons/AddDocumentIcon.tsx +20 -0
- package/src/icons/AttachmentIcon.tsx +20 -0
- package/src/icons/SendIcon.tsx +20 -0
- package/src/icons/TelephoneKeypad.tsx +20 -0
- package/src/index.ts +5 -1
- package/src/private/FluentThemeProvider.tsx +11 -7
- package/src/styles/injectStyle.ts +9 -0
- package/src/styles/useStyles.ts +19 -0
- package/src/styles.ts +4 -0
- package/src/testIds.ts +21 -0
- package/src/tsconfig.json +2 -1
- package/src/types/PropsOf.ts +7 -0
- package/src/external/ThemeProvider.tsx +0 -16
- 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,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
|
+
}
|