diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx index c913c184..fe25366e 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx @@ -1,176 +1,71 @@ import React from 'react'; import Message from '@patternfly/chatbot/dist/dynamic/Message'; import patternflyAvatar from './patternfly_avatar.jpg'; +import { Checkbox, FormGroup, Stack } from '@patternfly/react-core'; export const MessageWithFeedbackExample: React.FunctionComponent = () => { - const [showUserFeedbackForm, setShowUserFeedbackForm] = React.useState(false); - const [showCompletionForm, setShowCompletionForm] = React.useState(false); - const [launchButton, setLaunchButton] = React.useState(); - const positiveRef = React.useRef(null); - const negativeRef = React.useRef(null); - const feedbackId = 'user-feedback-form'; - const completeId = 'user-feedback-received'; - - const getCurrentCard = () => { - if (showUserFeedbackForm) { - return feedbackId; - } - if (showCompletionForm) { - return completeId; - } - }; - - const isExpanded = showUserFeedbackForm || showCompletionForm; - - const focusLaunchButton = () => { - if (launchButton === 'positive') { - positiveRef.current?.focus(); - } - if (launchButton === 'negative') { - negativeRef.current?.focus(); - } - }; + const [hasCloseButton, setHasCloseButton] = React.useState(true); + const [hasTextArea, setHasTextArea] = React.useState(false); return ( <> - { - setShowUserFeedbackForm(true); - setShowCompletionForm(false); - setLaunchButton('positive'); - }, - /* These are important for accessibility */ - 'aria-expanded': isExpanded, - 'aria-controls': getCurrentCard(), - isClicked: launchButton === 'positive', - ref: positiveRef - }, - negative: { - onClick: () => { - setShowUserFeedbackForm(true); - setShowCompletionForm(false); - setLaunchButton('negative'); - }, - /* These are important for accessibility */ - 'aria-expanded': isExpanded, - 'aria-controls': getCurrentCard(), - isClicked: launchButton === 'negative', - ref: negativeRef - } - }} - userFeedbackForm={ - showUserFeedbackForm - ? /* eslint-disable indent */ - { - quickResponses: [ - { id: '1', content: 'Correct' }, - { id: '2', content: 'Easy to understand' }, - { id: '3', content: 'Complete' } - ], - onSubmit: (quickResponse, additionalFeedback) => { - alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`); - setShowUserFeedbackForm(false); - setShowCompletionForm(true); - focusLaunchButton(); - }, - hasTextArea: true, - onClose: () => { - setShowUserFeedbackForm(false); - focusLaunchButton(); - }, - id: feedbackId - } - : undefined - /* eslint-enable indent */ - } - userFeedbackComplete={ - showCompletionForm - ? /* eslint-disable indent */ - { - onClose: () => { - setShowCompletionForm(false); - focusLaunchButton(); - }, - id: completeId - } - : undefined - /* eslint-enable indent */ - } - /> - - alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), - hasTextArea: true, - // eslint-disable-next-line no-console - onClose: () => console.log('closed feedback form'), - focusOnLoad: false - }} - /> - alert(`Selected ${quickResponse}`), + + + { + setHasTextArea(!hasTextArea); + }} + name="basic-inline-radio" + label="Has text area" + id="has-text-area" + /> + + + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), + hasTextArea, + // eslint-disable-next-line no-console + onClose: () => console.log('closed feedback form'), + focusOnLoad: false + }} + /> + + + + { + setHasCloseButton(!hasCloseButton); + }} + name="basic-inline-radio" + label="Has close button" + id="has-close" + /> + + console.log('closed feedback form'), - focusOnLoad: false - }} - /> - - alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`), - focusOnLoad: false - }} - /> - console.log('closed completion message'), focusOnLoad: false }} - /> - + userFeedbackComplete={{ + // eslint-disable-next-line no-console + onClose: hasCloseButton ? () => console.log('closed completion message') : undefined, + focusOnLoad: false + }} + /> + ); }; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index 76a76186..6e5914a3 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -102,33 +102,29 @@ You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is ### Message feedback -When a user selects a positive or negative [message action](#message-actions), you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button or be [configured to time out automatically](/patternfly-ai/chatbot/messages#message-feedback-with-timeouts). +When a user selects a positive or negative [message action](#message-actions), you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button and the thank-you card can be [configured to time out automatically](/patternfly-ai/chatbot/messages#message-feedback-with-timeouts).

-The message feedback card will immediately receive focus by default, but you remove this behavior by passing `focusOnLoad: false` to the `` (as shown in the following examples). For better usability, you should generally keep the default focus behavior. +You can see a demo of the full feedback flow [in the demos section](http://localhost:8006/patternfly-ai/chatbot/messages/demo#message-feedback) +

+The message feedback cards will immediately receive focus by default, but you remove this behavior by passing `focusOnLoad: false` to the `` (as shown in the following examples). For better usability, you should generally keep the default focus behavior.

The following examples demonstrate: -- A full feedback flow, which accepts written feedback submission and displays the thank you card. - A basic card. - A basic card without text input. -- A card without a close button. - Thank-you cards, with and without a close button. -The full feedback flow example also demonstrates how to handle focus appropriately for accessibility. The card will be focused when it appears in the DOM. When the card closes, place the focus back on the launching button. You can also add `aria-expanded` and `aria-controls` attributes to the feedback buttons to provide additional context on what the button controls. -

-It is also important to announce when new content appears onscreen for accessibility purposes. If you set `isLiveRegion` to true on ``, it will make appropriate announcements for you when the feedback card appears. - ```js file="./MessageWithFeedback.tsx" ``` ### Message feedback with timeouts -The feedback card and thank you message can be configured to time out and automatically close after a period of time. The default time out period is 8000 ms, but it can be customized via `timeout`. +The feedback thank you message can be configured to time out and automatically close after a period of time. The default time out period is 8000 ms, but it can be customized via `timeout`.

The card will not dismiss within the default time if a user is hovering over it or if it has keyboard focus. Instead, it will dismiss after they remove focus, via `timeoutAnimation`, which is 3000 ms by default. You can adjust this duration and set an `onTimeout` callback, as well as optional `onMouseEnter` and `onMouseLeave` callbacks.

-For accessibility purposes, be sure to announce when new content appears onscreen. If you set `isLiveRegion` to `true` for a ``, it will make appropriate announcements for you when the feedback card appears. +For accessibility purposes, be sure to announce when new content appears onscreen. `isLiveRegion` is set to true by default on `` soit will make appropriate announcements for you when the thank-you card appears. ```js file="./MessageWithFeedbackTimeout.tsx" diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md index 98f6205f..17ef7df7 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md @@ -47,6 +47,20 @@ import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; ## Demos +### Message feedback + +When a user selects a positive or negative message action, you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button and the thank-you card can be configured to time out automatically. + +The following example demonstrates a full feedback flow, which accepts written feedback submission and displays a thank you card. + +It also demonstrates how to handle focus appropriately for accessibility. The card will be focused when it appears in the DOM. When the card closes, place the focus back on the launching button. You can also add aria-expanded and aria-controls attributes to the feedback buttons to provide additional context on what the button controls. + +It is also important to announce when new content appears onscreen for accessibility purposes. isLiveRegion is set to true by default on so it will make appropriate announcements for you when the feedback card appears. + +```js file="./Feedback.tsx" + +``` + ### Attach via upload button in message bar This demo displays unique attachment features, including: diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx new file mode 100644 index 00000000..e0ecb8c8 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; + +export const MessageWithFeedbackExample: React.FunctionComponent = () => { + const [showUserFeedbackForm, setShowUserFeedbackForm] = React.useState(false); + const [showCompletionForm, setShowCompletionForm] = React.useState(false); + const [launchButton, setLaunchButton] = React.useState(); + const positiveRef = React.useRef(null); + const negativeRef = React.useRef(null); + const feedbackId = 'user-feedback-form'; + const completeId = 'user-feedback-received'; + + const getCurrentCard = () => { + if (showUserFeedbackForm) { + return feedbackId; + } + if (showCompletionForm) { + return completeId; + } + }; + + const isExpanded = showUserFeedbackForm || showCompletionForm; + + const focusLaunchButton = () => { + if (launchButton === 'positive') { + positiveRef.current?.focus(); + } + if (launchButton === 'negative') { + negativeRef.current?.focus(); + } + }; + + return ( + { + setShowUserFeedbackForm(true); + setShowCompletionForm(false); + setLaunchButton('positive'); + }, + /* These are important for accessibility */ + 'aria-expanded': isExpanded, + 'aria-controls': getCurrentCard(), + ref: positiveRef + }, + negative: { + onClick: () => { + setShowUserFeedbackForm(true); + setShowCompletionForm(false); + setLaunchButton('negative'); + }, + /* These are important for accessibility */ + 'aria-expanded': isExpanded, + 'aria-controls': getCurrentCard(), + ref: negativeRef + } + }} + userFeedbackForm={ + showUserFeedbackForm + ? /* eslint-disable indent */ + { + quickResponses: [ + { id: '1', content: 'Correct' }, + { id: '2', content: 'Easy to understand' }, + { id: '3', content: 'Complete' } + ], + onSubmit: (quickResponse, additionalFeedback) => { + alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`); + setShowUserFeedbackForm(false); + setShowCompletionForm(true); + focusLaunchButton(); + }, + hasTextArea: true, + onClose: () => { + setShowUserFeedbackForm(false); + focusLaunchButton(); + }, + id: feedbackId + } + : undefined + /* eslint-enable indent */ + } + userFeedbackComplete={ + showCompletionForm + ? /* eslint-disable indent */ + { + onClose: () => { + setShowCompletionForm(false); + focusLaunchButton(); + }, + id: completeId + } + : undefined + /* eslint-enable indent */ + } + /> + ); +}; diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx index c3f8b217..484666e3 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx @@ -28,7 +28,13 @@ export const ChatbotConversationHistoryDropdown: React.FunctionComponent) => ( - + (
- + + diff --git a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.test.tsx b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.test.tsx index 5a57bb1d..beb5bbd1 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.test.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.test.tsx @@ -6,47 +6,47 @@ import UserFeedbackComplete from './UserFeedbackComplete'; describe('UserFeedbackComplete', () => { it('should render correctly', () => { - render(); + render(); expect(screen.getByText('Thank you')).toBeTruthy(); screen.getByText(/You have successfully sent your feedback!/i); screen.getByText(/Thank you for responding./i); expect(screen.queryByRole('button', { name: /Close/i })).toBeFalsy(); }); it('should render different title correctly', () => { - render(); + render(); expect(screen.getByText('Thanks!')).toBeTruthy(); screen.getByText(/You have successfully sent your feedback!/i); screen.getByText(/Thank you for responding./i); }); it('should render different body correctly', () => { - render(); + render(); expect(screen.getByText('Thank you')).toBeTruthy(); screen.getByText(/Feedback received!/i); }); it('should handle onClose correctly', async () => { const spy = jest.fn(); - render(); - const closeButton = screen.getByRole('button', { name: 'Close' }); + render(); + const closeButton = screen.getByRole('button', { name: 'Close feedback for message received at 12/12/12' }); expect(closeButton).toBeTruthy(); await userEvent.click(closeButton); expect(spy).toHaveBeenCalledTimes(1); }); it('should be able to change close button aria label', () => { const spy = jest.fn(); - render(); + render(); expect(screen.getByRole('button', { name: /Ima button/i })).toBeTruthy(); }); it('should handle className', async () => { - render(); + render(); expect(screen.getByTestId('card')).toHaveClass('test'); }); it('should apply id', async () => { - render(); + render(); expect(screen.getByTestId('card').parentElement).toHaveAttribute('id', 'test'); }); it('renders with no timeout by default', () => { jest.useFakeTimers(); - render(); + render(); act(() => { jest.advanceTimersByTime(8000); }); @@ -55,7 +55,7 @@ describe('UserFeedbackComplete', () => { }); it('should handle timeout correctly after 8000ms when timeout = true', async () => { jest.useFakeTimers(); - render(); + render(); act(() => { jest.advanceTimersByTime(7999); }); @@ -68,7 +68,7 @@ describe('UserFeedbackComplete', () => { }); it('should handle timeout correctly when timeout = numeric value', async () => { jest.useFakeTimers(); - render(); + render(); act(() => { jest.advanceTimersByTime(299); }); @@ -84,7 +84,7 @@ describe('UserFeedbackComplete', () => { advanceTimers: (delay) => jest.advanceTimersByTime(delay) }); jest.useFakeTimers(); - render(); + render(); expect(screen.getByText('Thank you')).toBeTruthy(); await user.click(screen.getByTestId('card')); act(() => { @@ -98,7 +98,7 @@ describe('UserFeedbackComplete', () => { advanceTimers: (delay) => jest.advanceTimersByTime(delay) }); jest.useFakeTimers(); - render(); + render(); const card = screen.getByTestId('card'); await user.hover(card); act(() => { @@ -115,7 +115,7 @@ describe('UserFeedbackComplete', () => { render(
- +
); const card = screen.getByTestId('card'); @@ -139,7 +139,7 @@ describe('UserFeedbackComplete', () => { render(
- +
); const card = screen.getByTestId('card'); @@ -163,7 +163,7 @@ describe('UserFeedbackComplete', () => { render(
- +
); const card = screen.getByTestId('card'); @@ -181,7 +181,7 @@ describe('UserFeedbackComplete', () => { it('does not call the onTimeout callback before the timeout period has expired', () => { const spy = jest.fn(); jest.useFakeTimers(); - render(); + render(); act(() => { jest.advanceTimersByTime(7999); }); @@ -191,7 +191,7 @@ describe('UserFeedbackComplete', () => { it('calls the onTimeout callback after the timeout period has expired', () => { jest.useFakeTimers(); const spy = jest.fn(); - render(); + render(); act(() => { jest.advanceTimersByTime(8000); }); @@ -200,14 +200,14 @@ describe('UserFeedbackComplete', () => { }); it('renders without aria-live and aria-atomic attributes by default', () => { - render(); + render(); const card = screen.getByTestId('card').parentElement; expect(card).not.toHaveAttribute('aria-live'); expect(card).not.toHaveAttribute('aria-atomic'); }); it('has an aria-live value of polite and aria-atomic value of false when isLiveRegion = true', () => { - render(); + render(); const card = screen.getByTestId('card').parentElement; expect(card).toHaveAttribute('aria-live', 'polite'); expect(card).toHaveAttribute('aria-atomic', 'false'); @@ -218,7 +218,7 @@ describe('UserFeedbackComplete', () => { render(
- +
); const card = screen.getByTestId('card'); @@ -232,7 +232,7 @@ describe('UserFeedbackComplete', () => { render(
- +
); const card = screen.getByTestId('card'); @@ -242,11 +242,11 @@ describe('UserFeedbackComplete', () => { expect(spy).toHaveBeenCalledTimes(1); }); it('should focus on load by default', () => { - render(); + render(); expect(screen.getByTestId('card').parentElement).toHaveFocus(); }); it('should not focus on load if focusOnLoad = false', () => { - render(); + render(); expect(screen.getByTestId('card').parentElement).not.toHaveFocus(); }); }); diff --git a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx index 860e84ba..99368275 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx @@ -42,6 +42,8 @@ export interface UserFeedbackCompleteProps extends Omit, OUIAP id?: string; /** Whether to focus card on load */ focusOnLoad?: boolean; + /** Timestamp passed in by Message for more context in aria announcements */ + timestamp?: string; } const UserFeedbackComplete: React.FunctionComponent = ({ @@ -50,11 +52,12 @@ const UserFeedbackComplete: React.FunctionComponent = body = `You have successfully sent your feedback! Thank you for responding.`, + timestamp, timeout = false, timeoutAnimation = 3000, onTimeout, onClose, - closeButtonAriaLabel = 'Close', + closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`, onMouseEnter, onMouseLeave, ouiaId, @@ -145,7 +148,13 @@ const UserFeedbackComplete: React.FunctionComponent = } : undefined + /* eslint-disable indent */ + onClose + ? { + actions: + } + : undefined + /* eslint-enable indent */ } >
diff --git a/packages/module/src/MessageBar/AttachButton.tsx b/packages/module/src/MessageBar/AttachButton.tsx index 432dfddf..1f89be5b 100644 --- a/packages/module/src/MessageBar/AttachButton.tsx +++ b/packages/module/src/MessageBar/AttachButton.tsx @@ -55,6 +55,8 @@ const AttachButtonBase: React.FunctionComponent = ({ exitDelay={tooltipProps?.exitDelay || 0} distance={tooltipProps?.distance || 8} animationDuration={tooltipProps?.animationDuration || 0} + // prevents VO announcements of both aria label and tooltip + aria="none" {...tooltipProps} >