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,71 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import { useStyles } from '../styles';
3
+
4
+ const styles = {
5
+ 'webchat-fluent__theme': {
6
+ display: 'contents',
7
+
8
+ '--webchat-colorNeutralForeground1': 'var(--colorNeutralForeground1, #242424)',
9
+ '--webchat-colorNeutralForeground2': 'var(--colorNeutralForeground2, #424242)',
10
+ '--webchat-colorNeutralForeground4': 'var(--colorNeutralForeground4, #707070)',
11
+
12
+ '--webchat-colorNeutralForegroundDisabled': 'var(--colorNeutralForegroundDisabled, #bdbdbd)',
13
+
14
+ '--webchat-colorNeutralBackground1': 'var(--colorNeutralBackground1, #ffffff)',
15
+ '--webchat-colorNeutralBackground4': 'var(--colorNeutralBackground4, #f0f0f0)',
16
+ '--webchat-colorNeutralBackground5': 'var(--colorNeutralBackground5, #ebebeb)',
17
+
18
+ '--webchat-colorSubtleBackgroundHover': 'var(--colorSubtleBackgroundHover, #f5f5f5)',
19
+ '--webchat-colorSubtleBackgroundPressed': 'var(--colorSubtleBackgroundPressed, #e0e0e0)',
20
+
21
+ '--webchat-colorNeutralStroke1': 'var(--colorNeutralStroke1, #d1d1d1)',
22
+ '--webchat-colorNeutralStroke2': 'var(--colorNeutralStroke2, #e0e0e0)',
23
+ '--webchat-colorNeutralStroke1Selected': 'var(--colorNeutralStroke1Selected, #bdbdbd)',
24
+
25
+ '--webchat-colorBrandStroke2': 'var(--colorBrandStroke2, #9edcf7)',
26
+
27
+ '--webchat-colorBrandForeground2Hover': 'var(--colorBrandForeground2Hover, #015a7a)',
28
+ '--webchat-colorBrandForeground2Pressed': 'var(--colorBrandForeground2Pressed, #01384d)',
29
+
30
+ '--webchat-colorBrandBackground2Hover': 'var(--colorBrandBackground2Hover, #bee7fa)',
31
+ '--webchat-colorBrandBackground2Pressed': 'var(--colorBrandBackground2Pressed, #7fd2f5)',
32
+
33
+ '--webchat-colorCompoundBrandForeground1': 'var(--colorCompoundBrandForeground1, #077fab)',
34
+
35
+ '--webchat-colorCompoundBrandForeground1Hover': 'var(--colorCompoundBrandForeground1Hover, #02729c)',
36
+ '--webchat-colorCompoundBrandForeground1Pressed': 'var(--colorCompoundBrandForeground1Pressed, #01678c)',
37
+
38
+ '--webchat-colorStatusDangerForeground1': 'var(--colorStatusDangerForeground1, #b10e1c)',
39
+
40
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/colors.ts
41
+ '--webchat-colorGray30': 'var(--colorGray30, #edebe9)',
42
+ '--webchat-colorGray160': 'var(--colorGray160, #323130)',
43
+ '--webchat-colorGray200': 'var(--colorGray200, #1b1a19)',
44
+
45
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/borderRadius.ts
46
+ '--webchat-borderRadiusSmall': 'var(--borderRadiusSmall, 2px)',
47
+ '--webchat-borderRadiusLarge': 'var(--borderRadiusLarge, 6px)',
48
+ '--webchat-borderRadiusXLarge': 'var(--borderRadiusXLarge, 8px)',
49
+
50
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/utils/shadows.ts
51
+ '--webchat-shadow16':
52
+ 'var(--shadow16, 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108))',
53
+
54
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts
55
+ '--webchat-spacingHorizontalMNudge': 'var(--spacingHorizontalMNudge, 10px)',
56
+
57
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts
58
+ '--webchat-fontFamilyBase': `var(--fontFamilyBase, 'Segoe UI)', 'Segoe UI Web (West European))', -apple-system,
59
+ BlinkMacSystemFont, Roboto, 'Helvetica Neue)', sans-serif)`,
60
+ '--webchat-fontFamilyNumeric': `var(--fontFamilyNumeric, Bahnschrift, 'Segoe UI)', 'Segoe UI Web (West European))',
61
+ -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue)', sans-serif)`,
62
+
63
+ // https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts
64
+ '--webchat-fontWeightSemibold': 'var(--fontWeightSemibold, 600)'
65
+ }
66
+ };
67
+
68
+ export default function WebchatTheme(props: Readonly<{ readonly children: ReactNode | undefined }>) {
69
+ const classNames = useStyles(styles);
70
+ return <div className={classNames['webchat-fluent__theme']}>{props.children}</div>;
71
+ }
@@ -0,0 +1,132 @@
1
+ import { hooks } from 'botframework-webchat-api';
2
+ import cx from 'classnames';
3
+ import React, { memo, useCallback, useEffect, useRef, useState, type DragEventHandler } from 'react';
4
+ import { useRefFrom } from 'use-ref-from';
5
+
6
+ import { AddDocumentIcon } from '../../icons/AddDocumentIcon';
7
+ import { useStyles } from '../../styles';
8
+ import testIds from '../../testIds';
9
+
10
+ const { useLocalizer } = hooks;
11
+
12
+ const styles = {
13
+ 'webchat-fluent__sendbox__attachment-drop-zone': {
14
+ backgroundColor: 'var(--webchat-colorNeutralBackground4)',
15
+ borderRadius: 'inherit',
16
+ cursor: 'copy',
17
+ display: 'grid',
18
+ gap: '8px',
19
+ inset: '0',
20
+ placeContent: 'center',
21
+ placeItems: 'center',
22
+ position: 'absolute'
23
+ },
24
+
25
+ 'webchat-fluent__sendbox__attachment-drop-zone--droppable': {
26
+ backgroundColor: '#e00',
27
+ color: 'White'
28
+ },
29
+
30
+ 'webchat-fluent__sendbox__attachment-drop-zone-icon': {
31
+ height: '36px',
32
+ // Set "pointer-events: none" to ignore dragging over the icon. Otherwise, when dragging over the icon, it would disable the "--droppable" modifier.
33
+ pointerEvents: 'none',
34
+ width: '36px'
35
+ }
36
+ };
37
+
38
+ const handleDragOver: DragEventHandler<HTMLDivElement> = event => {
39
+ // This is for preventing the browser from opening the dropped file in a new tab.
40
+ event.preventDefault();
41
+ };
42
+
43
+ // Notes: For files dragging from outside of browser, it only tell us if it is a "File" instead of "text/plain" or "text/uri-list".
44
+ // For images dragging inside of browser, it only tell us that it is "text/plain", "text/uri-list" and "text/html". But not "image/*".
45
+ // So we cannot whitelist what is droppable.
46
+ // We are using case-insensitive of type "files" so we can drag in WebDriver.
47
+ const isFilesTransferEvent = (event: DragEvent) =>
48
+ !!event.dataTransfer?.types?.some(type => type.toLowerCase() === 'files');
49
+
50
+ function isDescendantOf(target: Node, ancestor: Node): boolean {
51
+ let current = target.parentNode;
52
+
53
+ while (current) {
54
+ if (current === ancestor) {
55
+ return true;
56
+ }
57
+
58
+ current = current.parentNode;
59
+ }
60
+
61
+ return false;
62
+ }
63
+
64
+ const DropZone = (props: { readonly onFilesAdded: (files: File[]) => void }) => {
65
+ const [dropZoneState, setDropZoneState] = useState<false | 'visible' | 'droppable'>(false);
66
+ const classNames = useStyles(styles);
67
+ const dropZoneRef = useRef<HTMLDivElement>(null);
68
+ const localize = useLocalizer();
69
+ const onFilesAddedRef = useRefFrom(props.onFilesAdded);
70
+
71
+ useEffect(() => {
72
+ let entranceCounter = 0;
73
+
74
+ const handleDragEnter = (event: DragEvent) => {
75
+ entranceCounter++;
76
+
77
+ if (isFilesTransferEvent(event)) {
78
+ setDropZoneState(
79
+ dropZoneRef.current &&
80
+ (event.target === dropZoneRef.current ||
81
+ (event.target instanceof HTMLElement && isDescendantOf(event.target, dropZoneRef.current)))
82
+ ? 'droppable'
83
+ : 'visible'
84
+ );
85
+ }
86
+ };
87
+
88
+ const handleDragLeave = () => --entranceCounter <= 0 && setDropZoneState(false);
89
+
90
+ document.addEventListener('dragenter', handleDragEnter, false);
91
+ document.addEventListener('dragleave', handleDragLeave, false);
92
+
93
+ return () => {
94
+ document.removeEventListener('dragenter', handleDragEnter);
95
+ document.removeEventListener('dragleave', handleDragLeave);
96
+ };
97
+ }, [setDropZoneState]);
98
+
99
+ const handleDrop = useCallback<DragEventHandler<HTMLDivElement>>(
100
+ event => {
101
+ event.preventDefault();
102
+
103
+ setDropZoneState(false);
104
+
105
+ if (!isFilesTransferEvent(event.nativeEvent)) {
106
+ return;
107
+ }
108
+
109
+ onFilesAddedRef.current([...event.dataTransfer.files]);
110
+ },
111
+ [onFilesAddedRef, setDropZoneState]
112
+ );
113
+
114
+ return dropZoneState ? (
115
+ <div
116
+ className={cx(classNames['webchat-fluent__sendbox__attachment-drop-zone'], {
117
+ [classNames['webchat-fluent__sendbox__attachment-drop-zone--droppable']]: dropZoneState === 'droppable'
118
+ })}
119
+ data-testid={testIds.sendBoxDropZone}
120
+ onDragOver={handleDragOver}
121
+ onDrop={handleDrop}
122
+ ref={dropZoneRef}
123
+ >
124
+ <AddDocumentIcon className={classNames['webchat-fluent__sendbox__attachment-drop-zone-icon']} />
125
+ {localize('TEXT_INPUT_DROP_ZONE')}
126
+ </div>
127
+ ) : null;
128
+ };
129
+
130
+ DropZone.displayName = 'DropZone';
131
+
132
+ export default memo(DropZone);
@@ -0,0 +1,77 @@
1
+ import { hooks } from 'botframework-webchat-api';
2
+ import React, { useCallback, useRef, type ChangeEventHandler, memo } from 'react';
3
+ import { useRefFrom } from 'use-ref-from';
4
+ import { AttachmentIcon } from '../../icons/AttachmentIcon';
5
+ import { useStyles } from '../../styles';
6
+ import testIds from '../../testIds';
7
+ import { ToolbarButton } from './Toolbar';
8
+
9
+ const { useLocalizer, useStyleOptions } = hooks;
10
+
11
+ const styles = {
12
+ 'webchat-fluent__sendbox__add-attachment': {
13
+ display: 'grid'
14
+ },
15
+
16
+ 'webchat-fluent__sendbox__add-attachment-input': {
17
+ fontSize: 0,
18
+ height: 0,
19
+ opacity: 0,
20
+ width: 0
21
+ }
22
+ };
23
+
24
+ function AddAttachmentButton(
25
+ props: Readonly<{
26
+ disabled?: boolean | undefined;
27
+ onFilesAdded: ((files: File[]) => void) | undefined;
28
+ }>
29
+ ) {
30
+ const inputRef = useRef<HTMLInputElement>(null);
31
+ const classNames = useStyles(styles);
32
+ const localize = useLocalizer();
33
+ const [{ uploadAccept, uploadMultiple }] = useStyleOptions();
34
+ const onFilesAddedRef = useRefFrom(props.onFilesAdded);
35
+
36
+ const handleClick = useCallback(() => inputRef.current?.click(), [inputRef]);
37
+
38
+ const handleFileChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
39
+ ({ target: { files } }) => {
40
+ if (files) {
41
+ onFilesAddedRef.current?.([...files]);
42
+
43
+ if (inputRef.current) {
44
+ inputRef.current.value = '';
45
+ }
46
+ }
47
+ },
48
+ [inputRef, onFilesAddedRef]
49
+ );
50
+
51
+ return (
52
+ <div className={classNames['webchat-fluent__sendbox__add-attachment']}>
53
+ <input
54
+ accept={uploadAccept}
55
+ aria-disabled={props.disabled}
56
+ aria-hidden="true"
57
+ className={classNames['webchat-fluent__sendbox__add-attachment-input']}
58
+ multiple={uploadMultiple}
59
+ onInput={props.disabled ? undefined : handleFileChange}
60
+ readOnly={props.disabled}
61
+ ref={inputRef}
62
+ role="button"
63
+ tabIndex={-1}
64
+ type="file"
65
+ />
66
+ <ToolbarButton
67
+ aria-label={localize('TEXT_INPUT_UPLOAD_BUTTON_ALT')}
68
+ data-testid={testIds.sendBoxUploadButton}
69
+ onClick={handleClick}
70
+ >
71
+ <AttachmentIcon />
72
+ </ToolbarButton>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export default memo(AddAttachmentButton);
@@ -0,0 +1,40 @@
1
+ import { hooks } from 'botframework-webchat-api';
2
+ import React, { memo } from 'react';
3
+ import { useStyles } from '../../styles';
4
+
5
+ const { useLocalizer } = hooks;
6
+
7
+ const styles = {
8
+ 'webchat-fluent__sendbox__attachment': {
9
+ border: '1px solid var(--webchat-colorNeutralStroke1)',
10
+ borderRadius: 'var(--webchat-borderRadiusLarge)',
11
+ cursor: 'default',
12
+ padding: '6px 8px',
13
+ width: 'fit-content'
14
+ }
15
+ };
16
+
17
+ const attachmentsPluralStringIds = {
18
+ one: 'TEXT_INPUT_ATTACHMENTS_ONE',
19
+ two: 'TEXT_INPUT_ATTACHMENTS_TWO',
20
+ few: 'TEXT_INPUT_ATTACHMENTS_FEW',
21
+ many: 'TEXT_INPUT_ATTACHMENTS_MANY',
22
+ other: 'TEXT_INPUT_ATTACHMENTS_OTHER'
23
+ };
24
+
25
+ function Attachments({
26
+ attachments
27
+ }: Readonly<{
28
+ readonly attachments: readonly Readonly<{ blob: Blob | File; thumbnailURL?: URL | undefined }>[];
29
+ }>) {
30
+ const classNames = useStyles(styles);
31
+ const localizeWithPlural = useLocalizer({ plural: true });
32
+
33
+ return attachments.length ? (
34
+ <div className={classNames['webchat-fluent__sendbox__attachment']}>
35
+ {localizeWithPlural(attachmentsPluralStringIds, attachments.length)}
36
+ </div>
37
+ ) : null;
38
+ }
39
+
40
+ export default memo(Attachments);
@@ -0,0 +1,26 @@
1
+ import React, { memo } from 'react';
2
+ import { useStyles } from '../../styles';
3
+
4
+ const styles = {
5
+ 'webchat-fluent___sendbox__error-message': {
6
+ fontSize: 0,
7
+ height: 0,
8
+ width: 0,
9
+ position: 'absolute',
10
+ top: 0,
11
+ left: 0,
12
+ color: 'transparent'
13
+ }
14
+ };
15
+
16
+ function ErrorMessage(props: Readonly<{ id: string; error?: string | undefined }>) {
17
+ const classNames = useStyles(styles);
18
+ return (
19
+ // eslint-disable-next-line react/forbid-dom-props
20
+ <span className={classNames['webchat-fluent___sendbox__error-message']} id={props.id} role="alert">
21
+ {props.error}
22
+ </span>
23
+ );
24
+ }
25
+
26
+ export default memo(ErrorMessage);
@@ -0,0 +1,30 @@
1
+ import React, { memo, useCallback } from 'react';
2
+
3
+ import { hooks } from 'botframework-webchat-api';
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,146 @@
1
+ import cx from 'classnames';
2
+ import React, { forwardRef, useCallback, type FormEventHandler, type KeyboardEventHandler } from 'react';
3
+
4
+ import { useStyles } from '../../styles';
5
+
6
+ const styles = {
7
+ 'webchat-fluent__sendbox__text-area': {
8
+ display: 'grid',
9
+ gridTemplateAreas: `'main'`,
10
+ maxHeight: '200px',
11
+ overflow: 'hidden'
12
+ },
13
+
14
+ 'webchat-fluent__sendbox__text-area--hidden': {
15
+ // TODO: Not perfect way of hiding the text box.
16
+ height: 0,
17
+ visibility: 'collapse'
18
+ },
19
+
20
+ 'webchat-fluent__sendbox__text-area-shared': {
21
+ border: 'none',
22
+ font: 'inherit',
23
+ gridArea: 'main',
24
+ outline: 'inherit',
25
+ overflowWrap: 'anywhere',
26
+ resize: 'inherit',
27
+ scrollbarGutter: 'stable'
28
+ },
29
+
30
+ 'webchat-fluent__sendbox__text-area-doppelganger': {
31
+ overflow: 'hidden',
32
+ visibility: 'hidden',
33
+ whiteSpace: 'pre-wrap'
34
+ },
35
+
36
+ 'webchat-fluent__sendbox__text-area-input': {
37
+ height: '100%',
38
+ padding: 0
39
+ },
40
+
41
+ 'webchat-fluent__sendbox__text-area-input--scroll': {
42
+ /* Firefox */
43
+ MozScrollbarColor: 'var(--webchat-colorNeutralBackground5) var(--webchat-colorNeutralForeground2)',
44
+ MozScrollbarWidth: 'thin',
45
+
46
+ /* Chrome, Edge, and Safari */
47
+ '&::-webkit-scrollbar': {
48
+ width: '8px'
49
+ },
50
+
51
+ '&::-webkit-scrollbar-track': {
52
+ backgroundColor: ' var(--webchat-colorNeutralBackground5)',
53
+ borderRadius: '16px'
54
+ },
55
+
56
+ '&::-webkit-scrollbar-thumb': {
57
+ backgroundColor: 'var(--webchat-colorNeutralForeground2)',
58
+ borderRadius: '16px'
59
+ },
60
+
61
+ '&::-webkit-scrollbar-corner': {
62
+ backgroundColor: 'var(--webchat-colorNeutralBackground5)'
63
+ }
64
+ }
65
+ };
66
+
67
+ const TextArea = forwardRef<
68
+ HTMLTextAreaElement,
69
+ Readonly<{
70
+ 'aria-label'?: string | undefined;
71
+ className?: string | undefined;
72
+ 'data-testid'?: string | undefined;
73
+
74
+ /**
75
+ * `true`, if the text area should be hidden but stay in the DOM, otherwise, `false`.
76
+ *
77
+ * Keeping the element in the DOM while making it invisible to users and PWDs is useful in these scenarios:
78
+ *
79
+ * - When the DTMF keypad is going away, we need to send focus to the text area before we unmount DTMF keypad,
80
+ * This ensures the flow of focus did not sent to document body
81
+ */
82
+ hidden?: boolean | undefined;
83
+ onInput?: FormEventHandler<HTMLTextAreaElement> | undefined;
84
+ placeholder?: string | undefined;
85
+ startRows?: number | undefined;
86
+ value?: string | undefined;
87
+ }>
88
+ >((props, ref) => {
89
+ const classNames = useStyles(styles);
90
+
91
+ const handleKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(event => {
92
+ // Shift+Enter adds a new line
93
+ // Enter requests related form submission
94
+ if (!event.shiftKey && event.key === 'Enter') {
95
+ event.preventDefault();
96
+
97
+ if ('form' in event.target && event.target.form instanceof HTMLFormElement) {
98
+ event.target?.form?.requestSubmit();
99
+ }
100
+ }
101
+ }, []);
102
+
103
+ return (
104
+ <div
105
+ className={cx(
106
+ classNames['webchat-fluent__sendbox__text-area'],
107
+ {
108
+ [classNames['webchat-fluent__sendbox__text-area--hidden']]: props.hidden
109
+ },
110
+ props.className
111
+ )}
112
+ role={props.hidden ? 'hidden' : undefined}
113
+ >
114
+ <div
115
+ className={cx(
116
+ classNames['webchat-fluent__sendbox__text-area-doppelganger'],
117
+ classNames['webchat-fluent__sendbox__text-area-shared'],
118
+ classNames['webchat-fluent__sendbox__text-area-input--scroll']
119
+ )}
120
+ >
121
+ {props.value || props.placeholder}{' '}
122
+ </div>
123
+ <textarea
124
+ aria-label={props['aria-label']}
125
+ className={cx(
126
+ classNames['webchat-fluent__sendbox__text-area-input'],
127
+ classNames['webchat-fluent__sendbox__text-area-shared'],
128
+ classNames['webchat-fluent__sendbox__text-area-input--scroll']
129
+ )}
130
+ data-testid={props['data-testid']}
131
+ onInput={props.onInput}
132
+ onKeyDown={handleKeyDown}
133
+ placeholder={props.placeholder}
134
+ ref={ref}
135
+ rows={props.startRows ?? 1}
136
+ // eslint-disable-next-line no-magic-numbers
137
+ tabIndex={props.hidden ? -1 : undefined}
138
+ value={props.value}
139
+ />
140
+ </div>
141
+ );
142
+ });
143
+
144
+ TextArea.displayName = 'TextArea';
145
+
146
+ export default TextArea;
@@ -0,0 +1,115 @@
1
+ import cx from 'classnames';
2
+ import React, { memo, type MouseEventHandler, type ReactNode } from 'react';
3
+ import { useStyles } from '../../styles';
4
+
5
+ const styles = {
6
+ 'webchat-fluent__sendbox__toolbar': {
7
+ display: 'flex',
8
+ gap: '4px',
9
+ marginInlineStart: 'auto'
10
+ },
11
+
12
+ 'webchat-fluent__sendbox__toolbar-button': {
13
+ alignItems: 'center',
14
+ appearance: 'none',
15
+ aspectRatio: '1',
16
+ background: 'transparent',
17
+ border: 'none',
18
+ borderRadius: 'var(--webchat-borderRadiusSmall)',
19
+ cursor: 'pointer',
20
+ display: 'flex',
21
+ justifyContent: 'center',
22
+ padding: '3px',
23
+ width: '32px',
24
+
25
+ '> svg': {
26
+ fontSize: '20px',
27
+ pointerEvents: 'none'
28
+ },
29
+
30
+ '@media (hover: hover)': {
31
+ '&:not([aria-disabled="true"]):hover': {
32
+ backgroundColor: 'var(--webchat-colorSubtleBackgroundHover)',
33
+ color: 'var(--webchat-colorCompoundBrandForeground1Hover)'
34
+ }
35
+ },
36
+ '&:not([aria-disabled="true"]):active': {
37
+ backgroundColor: 'var(--webchat-colorSubtleBackgroundPressed)',
38
+ color: 'var(--webchat-colorCompoundBrandForeground1Pressed)'
39
+ },
40
+ '&[aria-disabled="true"]': {
41
+ color: ' var(--webchat-colorNeutralForegroundDisabled)',
42
+ cursor: 'not-allowed'
43
+ }
44
+ },
45
+
46
+ 'webchat-fluent__sendbox__toolbar-separator': {
47
+ alignSelf: 'center',
48
+ borderInlineEnd: '1px solid var(--webchat-colorNeutralStroke2)',
49
+ height: '28px',
50
+
51
+ '&:first-child, &:last-child, &:only-child': {
52
+ display: 'none'
53
+ }
54
+ }
55
+ };
56
+
57
+ const preventDefaultHandler: MouseEventHandler<HTMLButtonElement> = event => event.preventDefault();
58
+
59
+ export const ToolbarButton = memo(
60
+ (
61
+ props: Readonly<{
62
+ 'aria-label'?: string | undefined;
63
+ children?: ReactNode | undefined;
64
+ className?: string | undefined;
65
+ 'data-testid'?: string | undefined;
66
+ disabled?: boolean | undefined;
67
+ onClick?: MouseEventHandler<HTMLButtonElement> | undefined;
68
+ type?: 'button' | 'submit' | undefined;
69
+ }>
70
+ ) => {
71
+ const classNames = useStyles(styles);
72
+
73
+ return (
74
+ <button
75
+ aria-label={props['aria-label']}
76
+ className={cx(classNames['webchat-fluent__sendbox__toolbar-button'], props.className)}
77
+ data-testid={props['data-testid']}
78
+ onClick={props.disabled ? preventDefaultHandler : props.onClick}
79
+ type={props.type === 'submit' ? 'submit' : 'button'}
80
+ {...(props.disabled && {
81
+ 'aria-disabled': 'true',
82
+ tabIndex: -1
83
+ })}
84
+ >
85
+ {props.children}
86
+ </button>
87
+ );
88
+ }
89
+ );
90
+
91
+ ToolbarButton.displayName = 'ToolbarButton';
92
+
93
+ export const Toolbar = memo((props: Readonly<{ children?: ReactNode | undefined; className?: string | undefined }>) => {
94
+ const classNames = useStyles(styles);
95
+
96
+ return <div className={cx(classNames['webchat-fluent__sendbox__toolbar'], props.className)}>{props.children}</div>;
97
+ });
98
+
99
+ Toolbar.displayName = 'Toolbar';
100
+
101
+ export const ToolbarSeparator = memo(
102
+ (props: Readonly<{ children?: ReactNode | undefined; className?: string | undefined }>) => {
103
+ const classNames = useStyles(styles);
104
+
105
+ return (
106
+ <div
107
+ aria-orientation="vertical"
108
+ className={cx(classNames['webchat-fluent__sendbox__toolbar-separator'], props.className)}
109
+ role="separator"
110
+ />
111
+ );
112
+ }
113
+ );
114
+
115
+ ToolbarSeparator.displayName = 'ToolbarSeparator';