Skip to content

Commit d6e46ff

Browse files
lexitaylor-worklexi-taylor
authored andcommitted
initial commit
1 parent 3c5c640 commit d6e46ff

10 files changed

Lines changed: 412 additions & 2 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script>
6+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
7+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
8+
<script crossorigin="anonymous" src="/test-harness.js"></script>
9+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
10+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
11+
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
12+
</head>
13+
14+
<body>
15+
<main id="webchat"></main>
16+
<script type="text/babel">
17+
run(async function () {
18+
const {
19+
React,
20+
ReactDOM: { render },
21+
WebChat: { FluentThemeProvider, ReactWebChat }
22+
} = window; // Imports in UMD fashion.
23+
24+
const { directLine, store } = testHelpers.createDirectLineEmulator();
25+
26+
const App = () => <ReactWebChat directLine={directLine} store={store} />;
27+
28+
render(
29+
<FluentThemeProvider>
30+
<App />
31+
</FluentThemeProvider>,
32+
document.getElementById('webchat')
33+
);
34+
35+
await pageConditions.uiConnected();
36+
37+
await directLine.emulateIncomingActivity({
38+
type: 'event',
39+
name: 'webchat:activity-status/feedback',
40+
entities: [
41+
{
42+
"@type": "LikeAction",
43+
actionStatus: "PotentialActionStatus",
44+
target: {
45+
"@type": "EntryPoint",
46+
urlTemplate: "ms-directline://postback?interaction=like"
47+
}
48+
}
49+
],
50+
channelData: {
51+
feedbackLoop: {
52+
type: 'default'
53+
}
54+
}
55+
});
56+
57+
const { activity } = await directLine.actPostActivity(() =>
58+
Promise.resolve({ })
59+
);
60+
61+
expect(activity).toEqual(expect.objectContaining({
62+
type: 'invoke',
63+
name: 'message/submitAction',
64+
value: {
65+
actionName: 'feedback',
66+
actionValue: {
67+
reaction: 'like',
68+
feedback: {
69+
feedbackText: expect.any(String),
70+
}
71+
}
72+
}
73+
}));
74+
});
75+
</script>
76+
</body>
77+
</html>

packages/api/src/localization/en-US.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@
7676
"COPY_BUTTON_TEXT": "Copy",
7777
"COPY_BUTTON_COPIED_TEXT": "Copied",
7878
"_COPY_BUTTON_COPIED.comment": "After clicking on the copy button, this text will show briefly",
79+
"FEEDBACK_FORM_SUBMIT_BUTTON_LABEL": "Submit",
80+
"FEEDBACK_FORM_CANCEL_BUTTON_LABEL": "Cancel",
81+
"FEEDBACK_FORM_PLACEHOLDER": "Please provide your feedback",
82+
"FEEDBACK_FORM_TITLE": "What did you like?",
7983
"FILE_CONTENT_ALT": "'$1'",
8084
"FILE_CONTENT_DOWNLOADABLE_ALT": "Download file '$1'",
8185
"FILE_CONTENT_DOWNLOADABLE_WITH_SIZE_ALT": "Download file '$1' of size $2",

packages/component/src/Activity/private/Feedback.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ const Feedback = memo(({ actions, className }: Props) => {
3535
postActivityRef.current({
3636
entities: [selectedAction],
3737
name: 'webchat:activity-status/feedback',
38-
type: 'event'
38+
type: 'event',
39+
channelData: {
40+
feedbackLoop: { type: 'default' }
41+
}
3942
} as any),
4043
DEBOUNCE_TIMEOUT
4144
);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
:global(.webchat-fluent) .feedback-form {
2+
box-sizing: border-box;
3+
display: grid;
4+
gap: var(--webchat-spacingHorizontalXS);
5+
grid-template-rows: auto auto;
6+
position: relative;
7+
}
8+
9+
:global(.webchat-fluent) .feedback-form__body1 {
10+
font-family: var(--webchat-fontFamilyBase);
11+
font-style: normal;
12+
font-weight: 400;
13+
font-size: 14px;
14+
line-height: 20px;
15+
}
16+
17+
:global(.webchat-fluent) .feedback-form__caption1 {
18+
font-family: var(--webchat-fontFamilyBase);
19+
font-style: normal;
20+
font-weight: 400;
21+
font-size: 10px;
22+
line-height: 14px;
23+
color: #616161;
24+
}
25+
26+
:global(.webchat-fluent) .feedback-button__container {
27+
display: flex;
28+
flex-direction: row;
29+
gap: var(--webchat-spacingHorizontalS);
30+
}
31+
32+
:global(.webchat-fluent) .feedback-button__submit {
33+
background-color: var(--webchat-colorBrandForeground1);
34+
border: 1px solid var(--webchat-colorBrandForeground1);
35+
border-radius: var(--webchat-borderRadiusMedium);
36+
color: var(--webchat-colorNeutralBackground1);
37+
cursor: pointer;
38+
font-size: var(--webchat-fontSizeBase300);
39+
line-height: var(--webchat-lineHeightBase300);
40+
font-family: var(--webchat-fontFamilyBase);
41+
}
42+
43+
:global(.webchat-fluent) .feedback-button__submit:hover {
44+
background-color: var(--webchat-colorBrandForeground2Hover);
45+
}
46+
47+
:global(.webchat-fluent) .feedback-button__submit:active {
48+
background-color: var(--webchat-colorBrandForeground2Pressed);
49+
}
50+
51+
:global(.webchat-fluent) .feedback-button__submit:disabled {
52+
background-color: var(--webchat-colorNeutralBackground5);
53+
cursor: not-allowed;
54+
}
55+
56+
:global(.webchat-fluent) .feedback-button__cancel {
57+
background-color: white;
58+
border-radius: var(--webchat-borderRadiusMedium);
59+
cursor: pointer;
60+
font-size: var(--webchat-fontSizeBase300);
61+
line-height: var(--webchat-lineHeightBase300);
62+
border: 1px solid var(--webchat-colorNeutralBackground5);
63+
font-family: var(--webchat-fontFamilyBase);
64+
}
65+
66+
:global(.webchat-fluent) .feedback-button__cancel:hover {
67+
background-color: var(--webchat-colorNeutralBackground1Hover);
68+
}
69+
70+
:global(.webchat-fluent) .sendbox__sendbox {
71+
background-color: var(--webchat-colorNeutralBackground1);
72+
border-radius: var(--webchat-borderRadiusMedium);
73+
border: 1px solid var(--webchat-colorNeutralStroke1);
74+
display: grid;
75+
font-family: var(--webchat-fontFamilyBase);
76+
font-size: 14px;
77+
gap: 6px;
78+
line-height: 20px;
79+
padding: 8px;
80+
position: relative;
81+
82+
&:focus-within {
83+
border-color: var(--webchat-colorNeutralStroke1Selected);
84+
}
85+
86+
&::after {
87+
border-bottom-left-radius: var(--webchat-sendbox-border-radius);
88+
border-bottom-right-radius: var(--webchat-sendbox-border-radius);
89+
border-bottom: var(--webchat-strokeWidthThicker) solid var(--webchat-colorCompoundBrandForeground1Hover);
90+
bottom: -1px;
91+
clip-path: inset(calc(100% - var(--webchat-strokeWidthThicker)) 50% 0 50%);
92+
content: '';
93+
height: var(--webchat-sendbox-border-radius);
94+
left: -1px;
95+
position: absolute;
96+
right: -1px;
97+
transition: clip-path var(--webchat-durationUltraFast) var(--webchat-curveAccelerateMid);
98+
}
99+
100+
&:focus-within::after {
101+
clip-path: inset(calc(100% - var(--webchat-strokeWidthThicker)) 0 0 0);
102+
transition: clip-path var(--webchat-durationNormal) var(--webchat-curveDecelerateMid);
103+
}
104+
105+
> .sendbox__text-area--in-grid {
106+
grid-area: text-area;
107+
}
108+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { memo, useState, useMemo, useCallback } from 'react';
2+
import { hooks } from 'botframework-webchat-component';
3+
import { useStyles } from '../../styles';
4+
import styles from './FeedbackForm.module.css';
5+
import { type WebChatActivity, parseThing } from 'botframework-webchat-core';
6+
import StackedLayout from './private/StackedLayout';
7+
import TextArea from '../sendBox/TextArea';
8+
import cx from 'classnames';
9+
10+
const { useLocalizer, usePostActivity } = hooks;
11+
12+
const FeedbackOptions = {
13+
LikeAction: 'like',
14+
DislikeAction: 'dislike'
15+
} as const;
16+
17+
type FeedbackType = keyof typeof FeedbackOptions;
18+
19+
function FeedbackForm({ activity }: Readonly<{ activity: WebChatActivity }>) {
20+
const classNames = useStyles(styles);
21+
const localize = useLocalizer();
22+
const [feedback, setFeedback] = useState('');
23+
const postActivity = usePostActivity();
24+
25+
const graph = useMemo(() => activity.entities || [], [activity.entities]);
26+
27+
const messageThing = parseThing(graph[0] ?? {});
28+
29+
const reactionType = messageThing?.['@type'] as FeedbackType;
30+
31+
const handleSubmit = useCallback(
32+
event => {
33+
event.preventDefault();
34+
if (feedback) {
35+
postActivity({
36+
type: 'invoke',
37+
name: 'message/submitAction',
38+
value: {
39+
actionName: 'feedback',
40+
actionValue: {
41+
reaction: reactionType === 'LikeAction' ? FeedbackOptions.LikeAction : FeedbackOptions.DislikeAction,
42+
feedback: {
43+
feedbackText: feedback
44+
}
45+
}
46+
}
47+
} as any);
48+
setFeedback('');
49+
}
50+
},
51+
[feedback, postActivity, reactionType]
52+
);
53+
54+
const handleCancel = useCallback(() => {
55+
setFeedback('');
56+
}, []);
57+
58+
const handleChange = useCallback(
59+
event => {
60+
setFeedback(event.target.value);
61+
},
62+
[setFeedback]
63+
);
64+
65+
if (!reactionType) {
66+
return null;
67+
}
68+
69+
return (
70+
<StackedLayout activity={activity}>
71+
<div className={classNames['feedback-form']}>
72+
<span className={classNames['feedback-form__body1']}>{localize('FEEDBACK_FORM_TITLE')}</span>
73+
<form className={classNames['feedback-form']} onSubmit={handleSubmit}>
74+
<div className={cx(classNames['sendbox__sendbox'])}>
75+
<TextArea onInput={handleChange} placeholder={localize('FEEDBACK_FORM_PLACEHOLDER')} value={feedback} />
76+
</div>
77+
<span className={classNames['feedback-form__caption1']}>{'Test description'}</span>
78+
<div className={classNames['feedback-button__container']}>
79+
<button className={classNames['feedback-button__submit']} type="submit">
80+
{localize('FEEDBACK_FORM_SUBMIT_BUTTON_LABEL')}
81+
</button>
82+
<button className={classNames['feedback-button__cancel']} onClick={handleCancel} type="button">
83+
{localize('FEEDBACK_FORM_CANCEL_BUTTON_LABEL')}
84+
</button>
85+
</div>
86+
</form>
87+
</div>
88+
</StackedLayout>
89+
);
90+
}
91+
export default memo(FeedbackForm);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { default as ActivityDecorator } from './ActivityDecorator';
22
export { default as useActivityAuthor } from './private/useActivityAuthor';
3+
export { default as FeedbackForm } from './FeedbackForm';
4+
export { type FeedbackFormActivity, default as isFeedbackFormActivity } from './isFeedbackFormActivity';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type WebChatActivity } from 'botframework-webchat-core';
2+
3+
export type FeedbackFormActivity = WebChatActivity & {
4+
type: 'event';
5+
name: 'webchat:activity-status/feedback';
6+
channelData: {
7+
feedbackLoop: { type: 'default' };
8+
};
9+
};
10+
11+
export default function isFeedbackFormActivity(
12+
activity: undefined | WebChatActivity
13+
): activity is FeedbackFormActivity {
14+
return !!(
15+
activity &&
16+
activity.type === 'event' &&
17+
activity.name === 'webchat:activity-status/feedback' &&
18+
(activity.channelData as any)?.feedbackLoop?.type === 'default'
19+
);
20+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
&.webchat__stacked-layout {
2+
position: relative;
3+
/* This is to keep screen reader text in the destinated area. */
4+
5+
& .webchat__stacked-layout__attachment-row,
6+
& .webchat__stacked-layout__main,
7+
& .webchat__stacked-layout__message-row,
8+
& .webchat__stacked-layout__status {
9+
display: flex;
10+
}
11+
12+
& .webchat__stacked-layout__alignment-pad {
13+
flex-shrink: 0;
14+
}
15+
16+
& .webchat__stacked-layout__attachment {
17+
width: 100%;
18+
}
19+
20+
& .webchat__stacked-layout__avatar-gutter {
21+
display: flex;
22+
flex-direction: column;
23+
flex-shrink: 0;
24+
}
25+
26+
&.webchat__stacked-layout--from-user {
27+
& .webchat__stacked-layout__attachment-row,
28+
& .webchat__stacked-layout__main,
29+
& .webchat__stacked-layout__message-row,
30+
& .webchat__stacked-layout__status {
31+
flex-direction: row-reverse;
32+
}
33+
}
34+
35+
& .webchat__stacked-layout__content {
36+
flex: 1;
37+
38+
/* This is for bottom aligning an avatar with a message bubble shorter than the avatar. */
39+
/* Related to the test at activityGrouping.avatarMiddleware.atBottom.js. */
40+
display: flex;
41+
flex-direction: column;
42+
43+
/* This "overflow: hidden" is to make sure text overflow will get clipped correctly. */
44+
/* Related to the test at basic.js "long URLs with keep-all". */
45+
overflow: hidden;
46+
}
47+
48+
& .webchat__stacked-layout__nub-pad {
49+
flex-shrink: 0;
50+
}
51+
}
52+
53+
:global(.webchat-fluent) .webchat__card {
54+
padding: var(--webchat-spacingHorizontalS) var(--webchat-spacingHorizontalM);
55+
border-radius: var(--webchat-borderRadiusMedium);
56+
border: 1px solid var(--webchat-colorNeutralStroke1);
57+
}

0 commit comments

Comments
 (0)