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,59 @@
|
|
|
1
|
+
import React, { MouseEventHandler, ReactNode, forwardRef, memo, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const preventDefaultHandler: MouseEventHandler<HTMLButtonElement> = event => event.preventDefault();
|
|
4
|
+
|
|
5
|
+
type AccessibleButtonProps = Readonly<{
|
|
6
|
+
className?: string | undefined;
|
|
7
|
+
'aria-hidden'?: boolean;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
11
|
+
tabIndex?: number;
|
|
12
|
+
type: 'button';
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
// Differences between <button> and <AccessibleButton>:
|
|
16
|
+
// - Disable behavior
|
|
17
|
+
// - When the widget is disabled
|
|
18
|
+
// - Set "aria-disabled" attribute to "true"
|
|
19
|
+
// - Set "readonly" attribute
|
|
20
|
+
// - Set "tabIndex" to -1
|
|
21
|
+
// - Remove "onClick" handler
|
|
22
|
+
// - Why this is needed
|
|
23
|
+
// - Browser compatibility: when the widget is disabled, different browser send focus to different places
|
|
24
|
+
// - When the widget become disabled, it's reasonable to keep the focus on the same widget for an extended period of time
|
|
25
|
+
// - When the user presses TAB after the current widget is disabled, it should jump to the next non-disabled widget
|
|
26
|
+
|
|
27
|
+
// Developers using this accessible widget will need to:
|
|
28
|
+
// - Style the disabled widget themselves, using CSS query `:disabled, [aria-disabled="true"] {}`
|
|
29
|
+
// - Modify all code that check disabled through "disabled" attribute to use aria-disabled="true" instead
|
|
30
|
+
// - aria-disabled="true" is the source of truth
|
|
31
|
+
// - If the widget is contained by a <form>, the developer need to filter out some `onSubmit` event caused by this widget
|
|
32
|
+
|
|
33
|
+
const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
|
|
34
|
+
({ 'aria-hidden': ariaHidden, children, disabled, onClick, tabIndex, ...props }, forwardedRef) => {
|
|
35
|
+
const targetRef = useRef<HTMLButtonElement>(null);
|
|
36
|
+
|
|
37
|
+
const ref = forwardedRef || targetRef;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
aria-disabled={disabled ? 'true' : 'false'}
|
|
42
|
+
aria-hidden={ariaHidden}
|
|
43
|
+
onClick={disabled ? preventDefaultHandler : onClick}
|
|
44
|
+
ref={ref}
|
|
45
|
+
tabIndex={tabIndex}
|
|
46
|
+
{...(disabled && {
|
|
47
|
+
'aria-disabled': 'true',
|
|
48
|
+
tabIndex: -1
|
|
49
|
+
})}
|
|
50
|
+
{...props}
|
|
51
|
+
type="button"
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export default memo(AccessibleButton);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
:global(.webchat-fluent) .suggested-action {
|
|
2
|
+
align-items: center;
|
|
3
|
+
background: transparent;
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
border: 1px solid var(--webchat-colorBrandStroke2);
|
|
6
|
+
cursor: pointer;
|
|
7
|
+
display: flex;
|
|
8
|
+
font-size: 12px;
|
|
9
|
+
gap: 4px;
|
|
10
|
+
padding: 4px 8px 4px;
|
|
11
|
+
text-align: start;
|
|
12
|
+
transition: all .15s ease-out;
|
|
13
|
+
|
|
14
|
+
@media (hover: hover) {
|
|
15
|
+
&:not([aria-disabled="true"]):hover {
|
|
16
|
+
background-color: var(--webchat-colorBrandBackground2Hover);
|
|
17
|
+
color: var(--webchat-colorBrandForeground2Hover)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
&:not([aria-disabled="true"]):active {
|
|
21
|
+
background-color: var(--webchat-colorBrandBackground2Pressed);
|
|
22
|
+
color: var(--webchat-colorBrandForeground2Pressed)
|
|
23
|
+
}
|
|
24
|
+
&[aria-disabled="true"] {
|
|
25
|
+
color: var(--webchat-colorNeutralForegroundDisabled);
|
|
26
|
+
cursor: not-allowed
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:global(.webchat-fluent) .suggested-action__image {
|
|
31
|
+
font-size: 12px;
|
|
32
|
+
height: 1em;
|
|
33
|
+
width: 1em;
|
|
34
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { hooks } from 'botframework-webchat-component';
|
|
2
|
+
import { type DirectLineCardAction } from 'botframework-webchat-core';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import React, { MouseEventHandler, memo, useCallback, useRef } from 'react';
|
|
5
|
+
import styles from './SuggestedAction.module.css';
|
|
6
|
+
import { useStyles } from '../../styles';
|
|
7
|
+
import AccessibleButton from './AccessibleButton';
|
|
8
|
+
|
|
9
|
+
const { useDisabled, useFocus, usePerformCardAction, useScrollToEnd, useStyleSet, useSuggestedActions } = hooks;
|
|
10
|
+
|
|
11
|
+
type SuggestedActionProps = Readonly<{
|
|
12
|
+
buttonText: string | undefined;
|
|
13
|
+
className?: string | undefined;
|
|
14
|
+
displayText?: string | undefined;
|
|
15
|
+
image?: string | undefined;
|
|
16
|
+
imageAlt?: string | undefined;
|
|
17
|
+
itemIndex: number;
|
|
18
|
+
text?: string | undefined;
|
|
19
|
+
type?:
|
|
20
|
+
| 'call'
|
|
21
|
+
| 'downloadFile'
|
|
22
|
+
| 'imBack'
|
|
23
|
+
| 'messageBack'
|
|
24
|
+
| 'openUrl'
|
|
25
|
+
| 'playAudio'
|
|
26
|
+
| 'playVideo'
|
|
27
|
+
| 'postBack'
|
|
28
|
+
| 'showImage'
|
|
29
|
+
| 'signin';
|
|
30
|
+
value?: any;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
function SuggestedAction({
|
|
34
|
+
buttonText,
|
|
35
|
+
className,
|
|
36
|
+
displayText,
|
|
37
|
+
image,
|
|
38
|
+
imageAlt,
|
|
39
|
+
text,
|
|
40
|
+
type,
|
|
41
|
+
value
|
|
42
|
+
}: SuggestedActionProps) {
|
|
43
|
+
const [_, setSuggestedActions] = useSuggestedActions();
|
|
44
|
+
const [{ suggestedAction: suggestedActionStyleSet }] = useStyleSet();
|
|
45
|
+
const [disabled] = useDisabled();
|
|
46
|
+
const focus = useFocus();
|
|
47
|
+
const focusRef = useRef<HTMLButtonElement>(null);
|
|
48
|
+
const performCardAction = usePerformCardAction();
|
|
49
|
+
const classNames = useStyles(styles);
|
|
50
|
+
const scrollToEnd = useScrollToEnd();
|
|
51
|
+
|
|
52
|
+
const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>(
|
|
53
|
+
({ target }) => {
|
|
54
|
+
(async function () {
|
|
55
|
+
// We need to focus to the send box before we are performing this card action.
|
|
56
|
+
// The will make sure the focus is always on Web Chat.
|
|
57
|
+
// Otherwise, the focus may momentarily send to `document.body` and screen reader will be confused.
|
|
58
|
+
await focus('sendBoxWithoutKeyboard');
|
|
59
|
+
|
|
60
|
+
// TODO: [P3] #XXX We should not destruct DirectLineCardAction into React props and pass them in. It makes typings difficult.
|
|
61
|
+
// Instead, we should pass a "cardAction" props.
|
|
62
|
+
performCardAction({ displayText, text, type, value } as DirectLineCardAction, { target });
|
|
63
|
+
|
|
64
|
+
// Since "openUrl" action do not submit, the suggested action buttons do not hide after click.
|
|
65
|
+
type === 'openUrl' && setSuggestedActions([]);
|
|
66
|
+
|
|
67
|
+
scrollToEnd();
|
|
68
|
+
})();
|
|
69
|
+
},
|
|
70
|
+
[displayText, focus, performCardAction, scrollToEnd, setSuggestedActions, text, type, value]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<AccessibleButton
|
|
75
|
+
className={cx(classNames['suggested-action'], suggestedActionStyleSet + '', (className || '') + '')}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
onClick={handleClick}
|
|
78
|
+
ref={focusRef}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
{image && <img alt={imageAlt} className={classNames['suggested-action__image']} src={image} />}
|
|
82
|
+
<span>{buttonText}</span>
|
|
83
|
+
</AccessibleButton>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default memo(SuggestedAction);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
:global(.webchat-fluent) .suggested-actions {
|
|
3
|
+
align-items: flex-end;
|
|
4
|
+
align-self: flex-end;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 8px;
|
|
8
|
+
|
|
9
|
+
&:not(:empty) {
|
|
10
|
+
padding-block-end: 8px;
|
|
11
|
+
padding-inline-start: 4px
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&.suggested-actions--flow {
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
flex-wrap: wrap;
|
|
17
|
+
justify-content: flex-end;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&.suggested-actions--stacked {
|
|
21
|
+
flex-direction: column
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { hooks } from 'botframework-webchat-component';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import React, { memo, type ReactNode } from 'react';
|
|
4
|
+
import SuggestedAction from './SuggestedAction';
|
|
5
|
+
import computeSuggestedActionText from './private/computeSuggestedActionText';
|
|
6
|
+
import styles from './index.module.css';
|
|
7
|
+
import { useStyles } from '../../styles';
|
|
8
|
+
|
|
9
|
+
const { useLocalizer, useStyleOptions, useStyleSet, useSuggestedActions } = hooks;
|
|
10
|
+
|
|
11
|
+
function SuggestedActionStackedOrFlowContainer(
|
|
12
|
+
props: Readonly<{
|
|
13
|
+
'aria-label'?: string | undefined;
|
|
14
|
+
children?: ReactNode | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}>
|
|
17
|
+
) {
|
|
18
|
+
const [{ suggestedActionLayout }] = useStyleOptions();
|
|
19
|
+
const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet();
|
|
20
|
+
const classNames = useStyles(styles);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
aria-label={props['aria-label']}
|
|
25
|
+
aria-live="polite"
|
|
26
|
+
aria-orientation="vertical"
|
|
27
|
+
className={cx(
|
|
28
|
+
classNames['suggested-actions'],
|
|
29
|
+
suggestedActionsStyleSet + '',
|
|
30
|
+
{
|
|
31
|
+
[classNames['suggested-actions--flow']]: suggestedActionLayout === 'flow',
|
|
32
|
+
[classNames['suggested-actions--stacked']]: suggestedActionLayout !== 'flow'
|
|
33
|
+
},
|
|
34
|
+
props.className
|
|
35
|
+
)}
|
|
36
|
+
role="toolbar"
|
|
37
|
+
>
|
|
38
|
+
{!!props.children && !!React.Children.count(props.children) && props.children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SuggestedActions() {
|
|
44
|
+
const classNames = useStyles(styles);
|
|
45
|
+
const localize = useLocalizer();
|
|
46
|
+
const [suggestedActions] = useSuggestedActions();
|
|
47
|
+
const children = suggestedActions.map((cardAction, index) => {
|
|
48
|
+
const { displayText, image, imageAltText, text, type, value } = cardAction as {
|
|
49
|
+
displayText?: string;
|
|
50
|
+
image?: string;
|
|
51
|
+
imageAltText?: string;
|
|
52
|
+
text?: string;
|
|
53
|
+
type:
|
|
54
|
+
| 'call'
|
|
55
|
+
| 'downloadFile'
|
|
56
|
+
| 'imBack'
|
|
57
|
+
| 'messageBack'
|
|
58
|
+
| 'openUrl'
|
|
59
|
+
| 'playAudio'
|
|
60
|
+
| 'playVideo'
|
|
61
|
+
| 'postBack'
|
|
62
|
+
| 'showImage'
|
|
63
|
+
| 'signin';
|
|
64
|
+
value?: { [key: string]: any } | string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (!suggestedActions?.length) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SuggestedAction
|
|
73
|
+
buttonText={computeSuggestedActionText(cardAction)}
|
|
74
|
+
displayText={displayText}
|
|
75
|
+
image={image}
|
|
76
|
+
// Image alt text should use `imageAltText` field and fallback to `text` field.
|
|
77
|
+
// https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#image-alt-text
|
|
78
|
+
imageAlt={image && (imageAltText || text)}
|
|
79
|
+
itemIndex={index}
|
|
80
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
81
|
+
key={index}
|
|
82
|
+
text={text}
|
|
83
|
+
type={type}
|
|
84
|
+
value={value}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
return (
|
|
89
|
+
<SuggestedActionStackedOrFlowContainer
|
|
90
|
+
aria-label={localize('SUGGESTED_ACTIONS_LABEL_ALT')}
|
|
91
|
+
className={classNames['suggested-actions']}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</SuggestedActionStackedOrFlowContainer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default memo(SuggestedActions);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DirectLineCardAction } from 'botframework-webchat-core';
|
|
2
|
+
|
|
3
|
+
// Please refer to this article to find out how to compute the "button text" for suggested action.
|
|
4
|
+
// https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#card-action
|
|
5
|
+
export default function computeSuggestedActionText(cardAction: DirectLineCardAction) {
|
|
6
|
+
// "CardAction" must contains at least image or title.
|
|
7
|
+
const { title } = cardAction as { title?: string };
|
|
8
|
+
const { type, value } = cardAction;
|
|
9
|
+
|
|
10
|
+
if (type === 'messageBack') {
|
|
11
|
+
return title || cardAction.displayText;
|
|
12
|
+
} else if (title) {
|
|
13
|
+
return title;
|
|
14
|
+
} else if (typeof value === 'string') {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// TODO: [P1] This is copied from botframework-webchat-component. Think about what we should do to eliminate duplications.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { memo, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import Context from './private/Context';
|
|
4
|
+
|
|
5
|
+
type Props = Readonly<{ children?: ReactNode | undefined }>;
|
|
6
|
+
|
|
7
|
+
const Provider = memo(({ children }: Props) => {
|
|
8
|
+
const [shown, setShown] = useState(false);
|
|
9
|
+
|
|
10
|
+
const context = useMemo(
|
|
11
|
+
() =>
|
|
12
|
+
Object.freeze({
|
|
13
|
+
setShown,
|
|
14
|
+
shown
|
|
15
|
+
}),
|
|
16
|
+
[setShown, shown]
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return <Context.Provider value={context}>{children}</Context.Provider>;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default Provider;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { PropsOf } from '../../types/PropsOf';
|
|
4
|
+
import TelephoneKeypad from './private/TelephoneKeypad';
|
|
5
|
+
import useShown from './useShown';
|
|
6
|
+
|
|
7
|
+
type Props = PropsOf<typeof TelephoneKeypad>;
|
|
8
|
+
|
|
9
|
+
const TelephoneKeypadSurrogate = memo((props: Props) => (useShown()[0] ? <TelephoneKeypad {...props} /> : false));
|
|
10
|
+
|
|
11
|
+
TelephoneKeypadSurrogate.displayName = 'TelephoneKeypad.Surrogate';
|
|
12
|
+
|
|
13
|
+
export default TelephoneKeypadSurrogate;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import TelephoneKeypadProvider from './Provider';
|
|
2
|
+
import TelephoneKeypadSurrogate from './Surrogate';
|
|
3
|
+
import { type DTMF } from './types';
|
|
4
|
+
import useTelephoneKeypadShown from './useShown';
|
|
5
|
+
|
|
6
|
+
export { TelephoneKeypadProvider, TelephoneKeypadSurrogate, useTelephoneKeypadShown, type DTMF };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
:global(.webchat-fluent) .telephone-keypad__button {
|
|
3
|
+
-webkit-user-select: none;
|
|
4
|
+
align-items: center;
|
|
5
|
+
appearance: none;
|
|
6
|
+
/* backgroundColor: isDarkTheme() || isHighContrastTheme() ? black : white, */
|
|
7
|
+
background-color: White;
|
|
8
|
+
border-radius: 100%;
|
|
9
|
+
|
|
10
|
+
/* Whitelabel styles */
|
|
11
|
+
/* border: `solid 1px ${isHighContrastTheme() ? white : isDarkTheme() ? gray160 : gray40}`, */
|
|
12
|
+
/* color: inherit; */
|
|
13
|
+
|
|
14
|
+
border: solid 1px var(--webchat-colorNeutralStroke1);
|
|
15
|
+
color: var(--webchat-colorGray200);
|
|
16
|
+
font-weight: var(--webchat-fontWeightSemibold);
|
|
17
|
+
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
height: 60px;
|
|
22
|
+
opacity: 0.7;
|
|
23
|
+
padding: 0;
|
|
24
|
+
position: relative;
|
|
25
|
+
touch-action: none;
|
|
26
|
+
user-select: none;
|
|
27
|
+
width: 60px;
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
/* backgroundColor: isHighContrastTheme() ? gray210 : isDarkTheme() ? gray150 : gray30 */
|
|
31
|
+
background-color: var(--webchat-colorGray30)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:global(.webchat-fluent) .telephone-keypad__button__ruby {
|
|
36
|
+
/* color: isHighContrastTheme() ? white : isDarkTheme() ? gray40 : gray160, */
|
|
37
|
+
color: var(--webchat-colorGray190);
|
|
38
|
+
font-size: 10px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:global(.webchat-fluent) .telephone-keypad__button__text {
|
|
42
|
+
font-size: 24px;
|
|
43
|
+
margin-top: 8px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:global(.webchat-fluent) .telephone-keypad--horizontal {
|
|
47
|
+
& .telephone-keypad__button {
|
|
48
|
+
height: 32px;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
margin: 8px 4px;
|
|
51
|
+
width: 32px;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
.telephone-keypad__button__ruby {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
& .telephone-keypad__button__text {
|
|
59
|
+
font-size: 20px;
|
|
60
|
+
margin-top: 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { forwardRef, memo, useCallback, type Ref } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useRefFrom } from 'use-ref-from';
|
|
4
|
+
|
|
5
|
+
import { type DTMF } from '../types';
|
|
6
|
+
|
|
7
|
+
import styles from './Button.module.css';
|
|
8
|
+
import { useStyles } from '../../../styles';
|
|
9
|
+
|
|
10
|
+
type Props = Readonly<{
|
|
11
|
+
button: DTMF;
|
|
12
|
+
['data-testid']?: string | undefined;
|
|
13
|
+
onClick?: (() => void) | undefined;
|
|
14
|
+
ruby?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
const Button = memo(
|
|
18
|
+
// As we are all TypeScript, internal components do not need propTypes.
|
|
19
|
+
// eslint-disable-next-line react/prop-types
|
|
20
|
+
forwardRef(({ button, 'data-testid': dataTestId, onClick, ruby }: Props, ref: Ref<HTMLButtonElement>) => {
|
|
21
|
+
const classNames = useStyles(styles);
|
|
22
|
+
const onClickRef = useRefFrom(onClick);
|
|
23
|
+
|
|
24
|
+
const handleClick = useCallback(() => onClickRef.current?.(), [onClickRef]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
className={classNames['telephone-keypad__button']}
|
|
29
|
+
data-testid={dataTestId}
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
ref={ref}
|
|
32
|
+
type="button"
|
|
33
|
+
>
|
|
34
|
+
<span className={classNames['telephone-keypad__button__text']}>
|
|
35
|
+
{button === 'star' ? '\u2217' : button === 'pound' ? '#' : button}
|
|
36
|
+
</span>
|
|
37
|
+
{!!ruby && <ruby className={classNames['telephone-keypad__button__ruby']}>{ruby}</ruby>}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
Button.displayName = 'TelephoneKeypad.Button';
|
|
44
|
+
|
|
45
|
+
export default Button;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
|
|
3
|
+
type ContextType = Readonly<{
|
|
4
|
+
setShown: Dispatch<SetStateAction<boolean>>;
|
|
5
|
+
shown: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
const Context = createContext<ContextType>(
|
|
9
|
+
new Proxy({} as ContextType, {
|
|
10
|
+
get() {
|
|
11
|
+
throw new Error('botframework-webchat: This hook can only used under its corresponding <Provider>.');
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
Context.displayName = 'TelephoneKeypad.Context';
|
|
17
|
+
|
|
18
|
+
export default Context;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
:global(.webchat-fluent) .telephone-keypad {
|
|
3
|
+
/* Commented out whitelabel styles for now. */
|
|
4
|
+
/* background: getHighContrastDarkThemeColor(highContrastColor: black, darkThemeColor: gray190, string, defaultColor: gray10), */
|
|
5
|
+
/* borderRadius: '8px 8px 0px 0px; */
|
|
6
|
+
/* boxShadow: '-3px 0px 7px 0px rgba(0, 0, 0, 0.13), -0.6px 0px 1.8px 0px rgba(0, 0, 0, 0.10)', */
|
|
7
|
+
|
|
8
|
+
align-items: center;
|
|
9
|
+
background: var(--webchat-colorNeutralBackground1);
|
|
10
|
+
/* border: isHighContrastTheme() ? `1px solid ${white}` : none; */
|
|
11
|
+
border: none;
|
|
12
|
+
border-radius: var(--webchat-borderRadiusXLarge);
|
|
13
|
+
/* boxShadow: var(--shadow16); */
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
font-family: var(--webchat-fontFamilyBase);
|
|
17
|
+
justify-content: center;
|
|
18
|
+
/* margin: var(--spacingHorizontalMNudge)' */
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:global(.webchat-fluent) .telephone-keypad__box {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
display: grid;
|
|
24
|
+
gap: 16px;
|
|
25
|
+
grid-template-columns: repeat(3, 1fr);
|
|
26
|
+
grid-template-rows: repeat(4, 1fr);
|
|
27
|
+
justify-items: center;
|
|
28
|
+
padding: 16px;
|
|
29
|
+
width: 100%;
|
|
30
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { KeyboardEventHandler, memo, useCallback, useEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
import { useRefFrom } from 'use-ref-from';
|
|
3
|
+
|
|
4
|
+
import Button from './Button';
|
|
5
|
+
// import HorizontalDialPadController from './HorizontalDialPadController';
|
|
6
|
+
import testIds from '../../../testIds';
|
|
7
|
+
import { type DTMF } from '../types';
|
|
8
|
+
import useShown from '../useShown';
|
|
9
|
+
import styles from './TelephoneKeypad.module.css';
|
|
10
|
+
import { useStyles } from '../../../styles';
|
|
11
|
+
|
|
12
|
+
type Props = Readonly<{
|
|
13
|
+
autoFocus?: boolean | undefined;
|
|
14
|
+
isHorizontal: boolean;
|
|
15
|
+
onButtonClick: (button: DTMF) => void;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
const Orientation = memo(
|
|
19
|
+
({ children, isHorizontal }: Readonly<{ children?: ReactNode | undefined; isHorizontal: boolean }>) => {
|
|
20
|
+
const classNames = useStyles(styles);
|
|
21
|
+
|
|
22
|
+
return isHorizontal ? (
|
|
23
|
+
// <HorizontalDialPadController>{children}</HorizontalDialPadController>
|
|
24
|
+
false
|
|
25
|
+
) : (
|
|
26
|
+
<div className={classNames['telephone-keypad__box']}>{children}</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
Orientation.displayName = 'TelephoneKeypad:Orientation';
|
|
32
|
+
|
|
33
|
+
const TelephoneKeypad = memo(({ autoFocus, onButtonClick, isHorizontal }: Props) => {
|
|
34
|
+
const autoFocusRef = useRefFrom(autoFocus);
|
|
35
|
+
const classNames = useStyles(styles);
|
|
36
|
+
const firstButtonRef = useRef<HTMLButtonElement>(null);
|
|
37
|
+
const onButtonClickRef = useRefFrom(onButtonClick);
|
|
38
|
+
const [, setShown] = useShown();
|
|
39
|
+
|
|
40
|
+
const handleButton1Click = useCallback(() => onButtonClickRef.current?.('1'), [onButtonClickRef]);
|
|
41
|
+
const handleButton2Click = useCallback(() => onButtonClickRef.current?.('2'), [onButtonClickRef]);
|
|
42
|
+
const handleButton3Click = useCallback(() => onButtonClickRef.current?.('3'), [onButtonClickRef]);
|
|
43
|
+
const handleButton4Click = useCallback(() => onButtonClickRef.current?.('4'), [onButtonClickRef]);
|
|
44
|
+
const handleButton5Click = useCallback(() => onButtonClickRef.current?.('5'), [onButtonClickRef]);
|
|
45
|
+
const handleButton6Click = useCallback(() => onButtonClickRef.current?.('6'), [onButtonClickRef]);
|
|
46
|
+
const handleButton7Click = useCallback(() => onButtonClickRef.current?.('7'), [onButtonClickRef]);
|
|
47
|
+
const handleButton8Click = useCallback(() => onButtonClickRef.current?.('8'), [onButtonClickRef]);
|
|
48
|
+
const handleButton9Click = useCallback(() => onButtonClickRef.current?.('9'), [onButtonClickRef]);
|
|
49
|
+
const handleButton0Click = useCallback(() => onButtonClickRef.current?.('0'), [onButtonClickRef]);
|
|
50
|
+
const handleButtonStarClick = useCallback(() => onButtonClickRef.current?.('star'), [onButtonClickRef]);
|
|
51
|
+
const handleButtonPoundClick = useCallback(() => onButtonClickRef.current?.('pound'), [onButtonClickRef]);
|
|
52
|
+
const handleKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
53
|
+
event => {
|
|
54
|
+
if (event.key === 'Escape') {
|
|
55
|
+
// TODO: Should send focus to the send box.
|
|
56
|
+
setShown(false);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[setShown]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
autoFocusRef.current && firstButtonRef.current?.focus();
|
|
64
|
+
}, [autoFocusRef, firstButtonRef]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className={classNames['telephone-keypad']} onKeyDown={handleKeyDown}>
|
|
68
|
+
<Orientation isHorizontal={isHorizontal}>
|
|
69
|
+
<Button
|
|
70
|
+
button="1"
|
|
71
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton1}
|
|
72
|
+
onClick={handleButton1Click}
|
|
73
|
+
ref={firstButtonRef}
|
|
74
|
+
/>
|
|
75
|
+
<Button
|
|
76
|
+
button="2"
|
|
77
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton2}
|
|
78
|
+
onClick={handleButton2Click}
|
|
79
|
+
ruby="ABC"
|
|
80
|
+
/>
|
|
81
|
+
<Button
|
|
82
|
+
button="3"
|
|
83
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton3}
|
|
84
|
+
onClick={handleButton3Click}
|
|
85
|
+
ruby="DEF"
|
|
86
|
+
/>
|
|
87
|
+
<Button
|
|
88
|
+
button="4"
|
|
89
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton4}
|
|
90
|
+
onClick={handleButton4Click}
|
|
91
|
+
ruby="GHI"
|
|
92
|
+
/>
|
|
93
|
+
<Button
|
|
94
|
+
button="5"
|
|
95
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton5}
|
|
96
|
+
onClick={handleButton5Click}
|
|
97
|
+
ruby="JKL"
|
|
98
|
+
/>
|
|
99
|
+
<Button
|
|
100
|
+
button="6"
|
|
101
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton6}
|
|
102
|
+
onClick={handleButton6Click}
|
|
103
|
+
ruby="MNO"
|
|
104
|
+
/>
|
|
105
|
+
<Button
|
|
106
|
+
button="7"
|
|
107
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton7}
|
|
108
|
+
onClick={handleButton7Click}
|
|
109
|
+
ruby="PQRS"
|
|
110
|
+
/>
|
|
111
|
+
<Button
|
|
112
|
+
button="8"
|
|
113
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton8}
|
|
114
|
+
onClick={handleButton8Click}
|
|
115
|
+
ruby="TUV"
|
|
116
|
+
/>
|
|
117
|
+
<Button
|
|
118
|
+
button="9"
|
|
119
|
+
data-testid={testIds.sendBoxTelephoneKeypadButton9}
|
|
120
|
+
onClick={handleButton9Click}
|
|
121
|
+
ruby="WXYZ"
|
|
122
|
+
/>
|
|
123
|
+
<Button button="star" data-testid={testIds.sendBoxTelephoneKeypadButtonStar} onClick={handleButtonStarClick} />
|
|
124
|
+
<Button button="0" data-testid={testIds.sendBoxTelephoneKeypadButton0} onClick={handleButton0Click} ruby="+" />
|
|
125
|
+
<Button
|
|
126
|
+
button="pound"
|
|
127
|
+
data-testid={testIds.sendBoxTelephoneKeypadButtonPound}
|
|
128
|
+
onClick={handleButtonPoundClick}
|
|
129
|
+
/>
|
|
130
|
+
</Orientation>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
TelephoneKeypad.displayName = 'TelephoneKeypad';
|
|
136
|
+
|
|
137
|
+
export default TelephoneKeypad;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type DTMF = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' | 'star' | 'pound';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext, useMemo, type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
|
|
3
|
+
import Context from './private/Context';
|
|
4
|
+
|
|
5
|
+
export default function useShown(): readonly [boolean, Dispatch<SetStateAction<boolean>>] {
|
|
6
|
+
const { setShown, shown } = useContext(Context);
|
|
7
|
+
|
|
8
|
+
return useMemo(() => Object.freeze([shown, setShown]), [shown, setShown]);
|
|
9
|
+
}
|