-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Feedback buttons should not squash other buttons #5480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
compulim
merged 31 commits into
microsoft:main
from
compulim:fix-feedback-form-with-copy-button
May 19, 2025
Merged
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
a50284a
Fixed feedback buttons shoud not squash other buttons
compulim acc9bc1
Update PR number
compulim 2dc97d0
Redo <form> and support Escape key
compulim 14392c0
Move to valibot
compulim 6e9650d
Add sub-entry
compulim a762859
Move entry
compulim 78bbf5f
Use <input type="checkbox"> for like/dislike button
compulim 3fec125
Update style names
compulim 59a109a
Rename
compulim a8cf162
Trying useHooks()
compulim e42723b
Hide feedback bar when no actions
compulim 31596d7
Fix checkbox vs. button
compulim 8f86296
Fix test
compulim bcff76b
Fix tests
compulim 2382609
Fix test
compulim abbcd59
Fix flaky test
compulim 7ead9bb
Use radio button and hide role for image
compulim dcc87ff
Revert to checkbox
compulim e1a4d89
Support button vs. radio
compulim eace768
Fix checkbox
compulim 85f1a60
Fix test
compulim a372226
Fix test
compulim 768442a
Fix test
compulim 99fb10d
Use valibot
compulim ce5803d
Sort
compulim 40e9552
Sort
compulim 2c3d844
Sort
compulim 8d15b12
Remove comment
compulim e495547
Add UserReview
compulim 3118563
Add sub entry
compulim bed66d7
Use reviewAspect instead
compulim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
149 changes: 149 additions & 0 deletions
149
__tests__/html2/feedbackForm/behavior.resetByEscapeKey.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <link href="/assets/index.css" rel="stylesheet" type="text/css" /> | ||
| <script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script> | ||
| <script crossorigin="anonymous" src="/test-harness.js"></script> | ||
| <script crossorigin="anonymous" src="/test-page-object.js"></script> | ||
| <script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script> | ||
| </head> | ||
| <body> | ||
| <main id="webchat"></main> | ||
| <script type="importmap"> | ||
| { | ||
| "imports": { | ||
| "@testduet/wait-for": "https://esm.sh/@testduet/wait-for" | ||
| } | ||
| } | ||
| </script> | ||
| <script type="module"> | ||
| import { waitFor } from '@testduet/wait-for'; | ||
|
|
||
| run(async function () { | ||
| const { | ||
| WebChat: { renderWebChat, testIds } | ||
| } = window; // Imports in UMD fashion. | ||
|
|
||
| const { directLine, store } = testHelpers.createDirectLineEmulator(); | ||
|
|
||
| const styleOptions = { | ||
| feedbackActionsPlacement: 'activity-actions' | ||
| }; | ||
|
|
||
| renderWebChat({ directLine, store, styleOptions }, document.getElementById('webchat')); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity({ | ||
| text: 'Pariatur dolore culpa cupidatat proident reprehenderit id fugiat exercitation est.', | ||
| type: 'message' | ||
| }); | ||
|
|
||
| await directLine.emulateIncomingActivity({ | ||
| channelData: { | ||
| feedbackLoop: { | ||
| type: 'default' | ||
| } | ||
| }, | ||
| entities: [ | ||
| { | ||
| '@context': 'https://schema.org', | ||
| '@id': '', | ||
| '@type': 'Message', | ||
| type: 'https://schema.org/Message', | ||
| potentialAction: [ | ||
| { | ||
| '@type': 'LikeAction', | ||
| actionStatus: 'PotentialActionStatus', | ||
| target: { | ||
| '@type': 'EntryPoint', | ||
| urlTemplate: 'ms-directline://postback?interaction=like' | ||
| } | ||
| }, | ||
| { | ||
| '@type': 'DislikeAction', | ||
| actionStatus: 'PotentialActionStatus', | ||
| result: { | ||
| '@type': 'Review', | ||
| reviewBody: "I don't like it.", | ||
| 'reviewBody-input': { | ||
| '@type': 'PropertyValueSpecification', | ||
| valueMinLength: 3, | ||
| valueName: 'reason' | ||
| } | ||
| }, | ||
| target: { | ||
| '@type': 'EntryPoint', | ||
| urlTemplate: 'ms-directline://postback?interaction=dislike{&reason}' | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| text: `Occaecat cillum amet ipsum amet pariatur proident commodo eiusmod cupidatat voluptate.`, | ||
| type: 'message' | ||
| }); | ||
|
|
||
| await directLine.emulateIncomingActivity({ | ||
| text: 'Mollit est aliqua magna cillum ullamco officia cupidatat voluptate enim.', | ||
| type: 'message' | ||
| }); | ||
|
|
||
| await pageConditions.numActivitiesShown(3); | ||
|
|
||
| // WHEN: Feedback form is opened. | ||
| document.querySelector(`[data-testid="${testIds.sendBoxTextBox}"]`).focus(); | ||
| await host.sendShiftTab(3); | ||
| await host.sendKeys('UP', 'ENTER'); | ||
| await host.sendTab(); | ||
| await host.sendKeys('ENTER'); | ||
|
|
||
| console.log(document.querySelectorAll(`[data-testid="${testIds.feedbackButton}"]`)); | ||
|
|
||
| // THEN: The dislike button should be pressed. | ||
| expect( | ||
| Array.from(document.querySelectorAll(`[data-testid="${testIds.feedbackButton}"]`)).map(element => | ||
| element.getAttribute('aria-pressed') | ||
| ) | ||
| ).toEqual(['false', 'true']); | ||
|
|
||
| // THEN: It should focus on the feedback box. | ||
| await waitFor(() => | ||
| expect(document.activeElement).toBe(document.querySelector(`[data-testid="${testIds.feedbackSendBox}"]`)) | ||
| ); | ||
|
|
||
| // THEN: Should match the snapshot. | ||
| await host.snapshot('local'); | ||
|
|
||
| // WHEN: ESCAPE key is pressed. | ||
| await host.sendKeys('ESCAPE'); | ||
|
|
||
| // THEN: It should hide the feedback form. | ||
| () => expect(document.querySelector(`[data-testid="${testIds.feedbackSendBox}"]`)).toBeFalsy(); | ||
|
|
||
| // THEN: It should unselect all feedback buttons. | ||
| expect( | ||
| Array.from(document.querySelectorAll(`[data-testid="${testIds.feedbackButton}"]`)).map(element => | ||
| element.getAttribute('aria-pressed') | ||
| ) | ||
| ).toEqual(['false', 'false']); | ||
|
|
||
| // THEN: Should focus on the dislike button. | ||
| await waitFor(() => | ||
| expect(document.activeElement).toBe( | ||
| document.querySelectorAll(`[data-testid="${testIds.feedbackButton}"]`).item(1) | ||
| ) | ||
| ); | ||
|
|
||
| // THEN: Should match the snapshot. | ||
| await host.snapshot('local'); | ||
|
|
||
| // WHEN: ESCAPE key is pressed again. | ||
| await host.sendKeys('ESCAPE'); | ||
|
|
||
| // THEN: Should focus on the activity. | ||
| await expect(pageElements.activeActivity()).toBe(pageElements.activities()[1]); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
Binary file added
BIN
+40.1 KB
__tests__/html2/feedbackForm/behavior.resetByEscapeKey.html.snap-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+29.6 KB
__tests__/html2/feedbackForm/behavior.resetByEscapeKey.html.snap-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
Binary file added
BIN
+12.3 KB
__tests__/html2/feedbackForm/feedback.activity.dismiss.html.snap-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+14.2 KB
__tests__/html2/feedbackForm/feedback.activity.dismiss.html.snap-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+14.1 KB
__tests__/html2/feedbackForm/feedback.activity.dismiss.html.snap-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+12.8 KB
__tests__/html2/feedbackForm/feedback.activity.dismiss.html.snap-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
101 changes: 101 additions & 0 deletions
101
__tests__/html2/feedbackForm/layout.withCopyButton.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <link href="/assets/index.css" rel="stylesheet" type="text/css" /> | ||
| <script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone@7.8.7/babel.min.js"></script> | ||
| <script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script> | ||
| <script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script> | ||
| <script crossorigin="anonymous" src="/test-harness.js"></script> | ||
| <script crossorigin="anonymous" src="/test-page-object.js"></script> | ||
| <script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script> | ||
| <script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script> | ||
| </head> | ||
| <body> | ||
| <main id="webchat" style="position: relative"></main> | ||
| <script type="text/babel"> | ||
| run(async function () { | ||
| const { | ||
| React, | ||
| ReactDOM: { render }, | ||
| WebChat: { FluentThemeProvider, ReactWebChat, testIds } | ||
| } = window; // Imports in UMD fashion. | ||
|
|
||
| const { directLine, store } = testHelpers.createDirectLineEmulator(); | ||
|
|
||
| const styleOptions = { | ||
| feedbackActionsPlacement: 'activity-actions' | ||
| }; | ||
|
|
||
| render( | ||
| <FluentThemeProvider> | ||
| <ReactWebChat directLine={directLine} store={store} styleOptions={styleOptions} /> | ||
| </FluentThemeProvider>, | ||
| document.getElementById('webchat') | ||
| ); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity({ | ||
| channelData: { | ||
| feedbackLoop: { | ||
| type: 'default' | ||
| } | ||
| }, | ||
| entities: [ | ||
| { | ||
| '@context': 'https://schema.org', | ||
| '@id': '', | ||
| '@type': 'Message', | ||
| type: 'https://schema.org/Message', | ||
| keywords: ['AllowCopy'], | ||
| potentialAction: [ | ||
| { | ||
| '@type': 'LikeAction', | ||
| actionStatus: 'PotentialActionStatus', | ||
| target: { | ||
| '@type': 'EntryPoint', | ||
| urlTemplate: 'ms-directline://postback?interaction=like' | ||
| } | ||
| }, | ||
| { | ||
| '@type': 'DislikeAction', | ||
| actionStatus: 'PotentialActionStatus', | ||
| result: { | ||
| '@type': 'Review', | ||
| reviewBody: "I don't like it.", | ||
| 'reviewBody-input': { | ||
| '@type': 'PropertyValueSpecification', | ||
| valueMinLength: 3, | ||
| valueName: 'reason' | ||
| } | ||
| }, | ||
| target: { | ||
| '@type': 'EntryPoint', | ||
| urlTemplate: 'ms-directline://postback?interaction=dislike{&reason}' | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| text: `Occaecat cillum amet ipsum amet pariatur proident commodo eiusmod cupidatat voluptate.`, | ||
| type: 'message' | ||
| }); | ||
|
|
||
| await pageConditions.numActivitiesShown(1); | ||
|
|
||
| // THEN: It should match the snapshot. | ||
| await host.snapshot('local'); | ||
|
|
||
| // WHEN: Feedback form is opened. | ||
| document.querySelector(`[data-testid="${testIds.sendBoxTextBox}"]`).focus(); | ||
| await host.sendShiftTab(2); | ||
| await host.sendKeys('ENTER'); | ||
| await host.sendTab(); | ||
| await host.sendKeys('ENTER'); | ||
|
|
||
| // THEN: It should match the snapshot. | ||
| await host.snapshot('local'); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
Oops, something went wrong.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,4 +129,4 @@ | |
| </script> | ||
| </body> | ||
|
|
||
| </html> | ||
| </html> | ||
Binary file modified
BIN
-1 Byte
(100%)
__tests__/html2/fluentTheme/feedbackForm.noDisclaimer.html.snap-1.png
Oops, something went wrong.
70 changes: 56 additions & 14 deletions
70
packages/component/src/ActivityFeedback/ActivityFeedback.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,69 @@ | ||
| import { WebChatActivity } from 'botframework-webchat-core'; | ||
| import React, { memo } from 'react'; | ||
| import classNames from 'classnames'; | ||
| import React, { memo, useCallback, type FormEventHandler, type KeyboardEventHandler } from 'react'; | ||
| import { Extract, wrapWith } from 'react-wrap-with'; | ||
| import { useRefFrom } from 'use-ref-from'; | ||
|
|
||
| import useStyleSet from '../hooks/useStyleSet'; | ||
| import FeedbackLoopWithMessage from './private/FeedbackLoopWithMessage'; | ||
| import FeedbackLoopWithoutMessage from './private/FeedbackLoopWithoutMessage'; | ||
| import ActivityFeedbackComposer from './providers/ActivityFeedbackComposer'; | ||
| import useFeedbackText from './providers/useFeedbackText'; | ||
| import useFocusAction from './providers/useFocusAction'; | ||
| import useSelectedAction from './providers/useSelectedAction'; | ||
| import useShouldShowFeedbackForm from './providers/useShouldShowFeedbackForm'; | ||
| import useSubmitCallback from './providers/useSubmitCallback'; | ||
|
|
||
| function InternalActivityFeedback() { | ||
| const [{ feedbackForm }] = useStyleSet(); | ||
| const [feedbackText, setFeedbackText] = useFeedbackText(); | ||
| const [selectedAction, setSelectedAction] = useSelectedAction(); | ||
| const [shouldShowFeedbackForm] = useShouldShowFeedbackForm(); | ||
| const focusAction = useFocusAction(); | ||
| const submit = useSubmitCallback(); | ||
|
|
||
| const feedbackTextRef = useRefFrom(feedbackText); | ||
| const selectedActionRef = useRefFrom(selectedAction); | ||
|
|
||
| const InternalActivityFeedback = memo(() => | ||
| useShouldShowFeedbackForm()[0] ? <FeedbackLoopWithMessage /> : <FeedbackLoopWithoutMessage /> | ||
| ); | ||
| const handleReset = useCallback<FormEventHandler<HTMLFormElement>>(() => { | ||
| focusAction(selectedActionRef.current); | ||
|
|
||
| InternalActivityFeedback.displayName = 'InternalActivityFeedback'; | ||
| setFeedbackText(undefined); | ||
| setSelectedAction(undefined); | ||
| }, [focusAction, selectedActionRef, setFeedbackText, setSelectedAction]); | ||
|
|
||
| type ActivityFeedbackProps = Readonly<{ | ||
| activity: WebChatActivity; | ||
| }>; | ||
| const handleSubmit = useCallback<FormEventHandler<HTMLFormElement>>( | ||
| event => { | ||
| event.preventDefault(); | ||
|
|
||
| submit(selectedActionRef.current, feedbackTextRef.current); | ||
| }, | ||
| [feedbackTextRef, selectedActionRef, submit] | ||
| ); | ||
|
|
||
| const handleKeyDown = useCallback<KeyboardEventHandler<HTMLFormElement>>( | ||
| event => { | ||
| if (event.key === 'Escape' && selectedActionRef.current) { | ||
| event.stopPropagation(); | ||
| event.currentTarget.reset(); | ||
| } | ||
| }, | ||
| [selectedActionRef] | ||
| ); | ||
|
|
||
| function ActivityFeedback({ activity }: ActivityFeedbackProps) { | ||
| return ( | ||
| <ActivityFeedbackComposer activity={activity}> | ||
| <InternalActivityFeedback /> | ||
| </ActivityFeedbackComposer> | ||
| <form | ||
| className={classNames('webchat__feedback-form-real', feedbackForm + '')} | ||
| onKeyDown={handleKeyDown} | ||
| onReset={handleReset} | ||
| onSubmit={handleSubmit} | ||
| > | ||
| {shouldShowFeedbackForm ? <FeedbackLoopWithMessage /> : <FeedbackLoopWithoutMessage />} | ||
| </form> | ||
| ); | ||
| } | ||
|
|
||
| const ActivityFeedback = wrapWith(ActivityFeedbackComposer, { activity: Extract })(InternalActivityFeedback); | ||
|
|
||
| ActivityFeedback.displayName = 'ActivityFeedback'; | ||
|
|
||
| export default memo(ActivityFeedback); | ||
| export { type ActivityFeedbackProps }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.