@patternfly/chatbot 6.4.0-prerelease.10 → 6.4.0-prerelease.12

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.
@@ -1,5 +1,5 @@
1
1
  import type { FunctionComponent } from 'react';
2
- import { CardProps, LabelGroupProps, OUIAProps } from '@patternfly/react-core';
2
+ import { ActionGroupProps, ButtonProps, CardBodyProps, CardHeaderProps, CardProps, FormProps, LabelGroupProps, OUIAProps, TextAreaProps } from '@patternfly/react-core';
3
3
  import QuickResponse from '../QuickResponse/QuickResponse';
4
4
  export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProps {
5
5
  /** Additional classes for the pagination navigation container. */
@@ -34,6 +34,20 @@ export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProp
34
34
  focusOnLoad?: boolean;
35
35
  /** Timestamp passed in by Message for more context in aria announcements */
36
36
  timestamp?: string;
37
+ /** Additional props passed to submit button */
38
+ submitButtonProps?: ButtonProps;
39
+ /** Additional props passed to card header */
40
+ cardHeaderProps?: CardHeaderProps;
41
+ /** Additional props passed to card body */
42
+ cardBodyProps?: CardBodyProps;
43
+ /** Additional props passed to title heading */
44
+ headingLevelProps?: React.HTMLAttributes<HTMLHeadingElement>;
45
+ /** Additional props passed to form */
46
+ formProps?: FormProps;
47
+ /** Additional props passed to text area */
48
+ textAreaProps?: TextAreaProps;
49
+ /** Additional props passed to action group */
50
+ actionGroupProps?: ActionGroupProps;
37
51
  }
38
52
  declare const UserFeedback: FunctionComponent<UserFeedbackProps>;
39
53
  export default UserFeedback;
@@ -21,7 +21,7 @@ const react_core_1 = require("@patternfly/react-core");
21
21
  const QuickResponse_1 = __importDefault(require("../QuickResponse/QuickResponse"));
22
22
  const CloseButton_1 = __importDefault(require("./CloseButton"));
23
23
  const UserFeedback = (_a) => {
24
- var { className, timestamp, title = 'Why did you choose this rating?', hasTextArea, textAreaAriaLabel = `Provide optional additional feedback for message received at ${timestamp}`, textAreaPlaceholder = 'Provide optional additional feedback', onTextAreaChange, submitWord = 'Submit', quickResponses, quickResponseContainerProps = { 'aria-label': `Quick feedback for message received at ${timestamp}` }, onSubmit, onClose, closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`, id, headingLevel: HeadingLevel = 'h1', focusOnLoad = true, isCompact } = _a, props = __rest(_a, ["className", "timestamp", "title", "hasTextArea", "textAreaAriaLabel", "textAreaPlaceholder", "onTextAreaChange", "submitWord", "quickResponses", "quickResponseContainerProps", "onSubmit", "onClose", "closeButtonAriaLabel", "id", "headingLevel", "focusOnLoad", "isCompact"]);
24
+ var { className, timestamp, title = 'Why did you choose this rating?', hasTextArea, textAreaAriaLabel = `Provide optional additional feedback for message received at ${timestamp}`, textAreaPlaceholder = 'Provide optional additional feedback', onTextAreaChange, submitWord = 'Submit', quickResponses, quickResponseContainerProps = { 'aria-label': `Quick feedback for message received at ${timestamp}` }, onSubmit, onClose, closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`, id, headingLevel: HeadingLevel = 'h1', focusOnLoad = true, isCompact, children, cardHeaderProps, cardBodyProps, headingLevelProps, formProps, textAreaProps, actionGroupProps, submitButtonProps } = _a, props = __rest(_a, ["className", "timestamp", "title", "hasTextArea", "textAreaAriaLabel", "textAreaPlaceholder", "onTextAreaChange", "submitWord", "quickResponses", "quickResponseContainerProps", "onSubmit", "onClose", "closeButtonAriaLabel", "id", "headingLevel", "focusOnLoad", "isCompact", "children", "cardHeaderProps", "cardBodyProps", "headingLevelProps", "formProps", "textAreaProps", "actionGroupProps", "submitButtonProps"]);
25
25
  const [selectedResponse, setSelectedResponse] = (0, react_1.useState)();
26
26
  const [value, setValue] = (0, react_1.useState)('');
27
27
  const divRef = (0, react_1.useRef)(null);
@@ -33,11 +33,11 @@ const UserFeedback = (_a) => {
33
33
  }, []);
34
34
  return (
35
35
  /* card does not have ref forwarding; hence wrapper div */
36
- (0, jsx_runtime_1.jsx)("div", { ref: divRef, id: id, tabIndex: 0, "aria-label": title, children: (0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: isCompact, className: `pf-chatbot__feedback-card ${className ? className : ''}` }, props, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardHeader, { actions: {
36
+ (0, jsx_runtime_1.jsx)("div", { ref: divRef, id: id, tabIndex: 0, "aria-label": title, children: (0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: isCompact, className: `pf-chatbot__feedback-card ${className ? className : ''}` }, props, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardHeader, Object.assign({ actions: {
37
37
  actions: (0, jsx_runtime_1.jsx)(CloseButton_1.default, { onClose: onClose, ariaLabel: closeButtonAriaLabel })
38
- }, children: (0, jsx_runtime_1.jsx)(HeadingLevel, { className: "pf-chatbot__feedback-card-title", children: title }) }), (0, jsx_runtime_1.jsx)(react_core_1.CardBody, { children: (0, jsx_runtime_1.jsxs)(react_core_1.Form, { className: `pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}`, children: [quickResponses && ((0, jsx_runtime_1.jsx)(QuickResponse_1.default, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, onSelect: (id) => setSelectedResponse(id), isCompact: isCompact })), hasTextArea && ((0, jsx_runtime_1.jsx)(react_core_1.TextArea, { value: value, onChange: (_event, value) => {
38
+ } }, cardHeaderProps, { children: (0, jsx_runtime_1.jsx)(HeadingLevel, Object.assign({ className: "pf-chatbot__feedback-card-title" }, headingLevelProps, { children: title })) })), (0, jsx_runtime_1.jsx)(react_core_1.CardBody, Object.assign({}, cardBodyProps, { children: (0, jsx_runtime_1.jsxs)(react_core_1.Form, Object.assign({ className: `pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}` }, formProps, { children: [quickResponses && ((0, jsx_runtime_1.jsx)(QuickResponse_1.default, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, onSelect: (id) => setSelectedResponse(id), isCompact: isCompact })), hasTextArea && ((0, jsx_runtime_1.jsx)(react_core_1.TextArea, Object.assign({ value: value, onChange: (_event, value) => {
39
39
  setValue(value);
40
40
  onTextAreaChange && onTextAreaChange(_event, value);
41
- }, placeholder: textAreaPlaceholder, "aria-label": textAreaAriaLabel, resizeOrientation: "vertical" })), (0, jsx_runtime_1.jsx)(react_core_1.ActionGroup, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, { onClick: () => onSubmit(selectedResponse, value), children: submitWord }) })] }) })] })) }));
41
+ }, placeholder: textAreaPlaceholder, "aria-label": textAreaAriaLabel, resizeOrientation: "vertical" }, textAreaProps))), children, (0, jsx_runtime_1.jsx)(react_core_1.ActionGroup, Object.assign({}, actionGroupProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ onClick: () => onSubmit(selectedResponse, value) }, submitButtonProps, { children: submitWord })) }))] })) }))] })) }));
42
42
  };
43
43
  exports.default = UserFeedback;
@@ -134,4 +134,48 @@ describe('UserFeedback', () => {
134
134
  (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, "data-testid": "card", isCompact: true }));
135
135
  expect(react_1.screen.getByTestId('card')).toHaveClass('pf-m-compact');
136
136
  });
137
+ it('should pass buttonProps to submit button', () => {
138
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, submitButtonProps: { variant: 'secondary', isDisabled: true } }));
139
+ const submitButton = react_1.screen.getByRole('button', { name: /Submit/i });
140
+ expect(submitButton).toHaveClass('pf-v6-c-button pf-m-secondary');
141
+ expect(submitButton).toBeDisabled();
142
+ });
143
+ it('should pass cardHeaderProps to card header', () => {
144
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, cardHeaderProps: { 'data-testid': 'card-header', className: 'custom-header' } }));
145
+ const cardHeader = react_1.screen.getByTestId('card-header');
146
+ expect(cardHeader).toHaveClass('custom-header');
147
+ });
148
+ it('should pass cardBodyProps to card body', () => {
149
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, cardBodyProps: { 'data-testid': 'card-body', className: 'custom-body' } }));
150
+ const cardBody = react_1.screen.getByTestId('card-body');
151
+ expect(cardBody).toHaveClass('custom-body');
152
+ });
153
+ it('should pass headingLevelProps to title heading', () => {
154
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, headingLevelProps: { className: 'custom-heading', id: 'feedback-title' } }));
155
+ const heading = react_1.screen.getByRole('heading', { level: 1, name: /Why did you choose this rating?/i });
156
+ expect(heading).toHaveClass('custom-heading');
157
+ expect(heading).toHaveAttribute('id', 'feedback-title');
158
+ });
159
+ it('should pass formProps to form', () => {
160
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, formProps: { 'data-testid': 'feedback-form', className: 'custom-form' } }));
161
+ const form = react_1.screen.getByTestId('feedback-form');
162
+ expect(form).toHaveClass('custom-form');
163
+ });
164
+ it('should pass textAreaProps to text area when hasTextArea is true', () => {
165
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, hasTextArea: true, textAreaProps: { 'data-testid': 'custom-textarea', rows: 5 } }));
166
+ const textArea = react_1.screen.getByTestId('custom-textarea');
167
+ expect(textArea).toHaveAttribute('rows', '5');
168
+ expect(textArea).toHaveAttribute('data-testid', 'custom-textarea');
169
+ });
170
+ it('should pass actionGroupProps to action group', () => {
171
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, actionGroupProps: { 'data-testid': 'action-group', className: 'custom-actions' } }));
172
+ const actionGroup = react_1.screen.getByTestId('action-group');
173
+ expect(actionGroup).toHaveClass('custom-actions');
174
+ });
175
+ it('should render children', () => {
176
+ (0, react_1.render)((0, jsx_runtime_1.jsxs)(UserFeedback_1.default, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "custom-content", children: "Custom feedback content" }), (0, jsx_runtime_1.jsx)("p", { children: "Additional paragraph" })] }));
177
+ expect(react_1.screen.getByTestId('custom-content')).toBeInTheDocument();
178
+ expect(react_1.screen.getByText('Custom feedback content')).toBeInTheDocument();
179
+ expect(react_1.screen.getByText('Additional paragraph')).toBeInTheDocument();
180
+ });
137
181
  });
@@ -1,5 +1,5 @@
1
1
  import type { FunctionComponent } from 'react';
2
- import { CardProps, LabelGroupProps, OUIAProps } from '@patternfly/react-core';
2
+ import { ActionGroupProps, ButtonProps, CardBodyProps, CardHeaderProps, CardProps, FormProps, LabelGroupProps, OUIAProps, TextAreaProps } from '@patternfly/react-core';
3
3
  import QuickResponse from '../QuickResponse/QuickResponse';
4
4
  export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProps {
5
5
  /** Additional classes for the pagination navigation container. */
@@ -34,6 +34,20 @@ export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProp
34
34
  focusOnLoad?: boolean;
35
35
  /** Timestamp passed in by Message for more context in aria announcements */
36
36
  timestamp?: string;
37
+ /** Additional props passed to submit button */
38
+ submitButtonProps?: ButtonProps;
39
+ /** Additional props passed to card header */
40
+ cardHeaderProps?: CardHeaderProps;
41
+ /** Additional props passed to card body */
42
+ cardBodyProps?: CardBodyProps;
43
+ /** Additional props passed to title heading */
44
+ headingLevelProps?: React.HTMLAttributes<HTMLHeadingElement>;
45
+ /** Additional props passed to form */
46
+ formProps?: FormProps;
47
+ /** Additional props passed to text area */
48
+ textAreaProps?: TextAreaProps;
49
+ /** Additional props passed to action group */
50
+ actionGroupProps?: ActionGroupProps;
37
51
  }
38
52
  declare const UserFeedback: FunctionComponent<UserFeedbackProps>;
39
53
  export default UserFeedback;
@@ -16,7 +16,7 @@ import { ActionGroup, Button, Card, CardBody, CardHeader, Form, TextArea } from
16
16
  import QuickResponse from '../QuickResponse/QuickResponse';
17
17
  import CloseButton from './CloseButton';
18
18
  const UserFeedback = (_a) => {
19
- var { className, timestamp, title = 'Why did you choose this rating?', hasTextArea, textAreaAriaLabel = `Provide optional additional feedback for message received at ${timestamp}`, textAreaPlaceholder = 'Provide optional additional feedback', onTextAreaChange, submitWord = 'Submit', quickResponses, quickResponseContainerProps = { 'aria-label': `Quick feedback for message received at ${timestamp}` }, onSubmit, onClose, closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`, id, headingLevel: HeadingLevel = 'h1', focusOnLoad = true, isCompact } = _a, props = __rest(_a, ["className", "timestamp", "title", "hasTextArea", "textAreaAriaLabel", "textAreaPlaceholder", "onTextAreaChange", "submitWord", "quickResponses", "quickResponseContainerProps", "onSubmit", "onClose", "closeButtonAriaLabel", "id", "headingLevel", "focusOnLoad", "isCompact"]);
19
+ var { className, timestamp, title = 'Why did you choose this rating?', hasTextArea, textAreaAriaLabel = `Provide optional additional feedback for message received at ${timestamp}`, textAreaPlaceholder = 'Provide optional additional feedback', onTextAreaChange, submitWord = 'Submit', quickResponses, quickResponseContainerProps = { 'aria-label': `Quick feedback for message received at ${timestamp}` }, onSubmit, onClose, closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`, id, headingLevel: HeadingLevel = 'h1', focusOnLoad = true, isCompact, children, cardHeaderProps, cardBodyProps, headingLevelProps, formProps, textAreaProps, actionGroupProps, submitButtonProps } = _a, props = __rest(_a, ["className", "timestamp", "title", "hasTextArea", "textAreaAriaLabel", "textAreaPlaceholder", "onTextAreaChange", "submitWord", "quickResponses", "quickResponseContainerProps", "onSubmit", "onClose", "closeButtonAriaLabel", "id", "headingLevel", "focusOnLoad", "isCompact", "children", "cardHeaderProps", "cardBodyProps", "headingLevelProps", "formProps", "textAreaProps", "actionGroupProps", "submitButtonProps"]);
20
20
  const [selectedResponse, setSelectedResponse] = useState();
21
21
  const [value, setValue] = useState('');
22
22
  const divRef = useRef(null);
@@ -28,11 +28,11 @@ const UserFeedback = (_a) => {
28
28
  }, []);
29
29
  return (
30
30
  /* card does not have ref forwarding; hence wrapper div */
31
- _jsx("div", { ref: divRef, id: id, tabIndex: 0, "aria-label": title, children: _jsxs(Card, Object.assign({ isCompact: isCompact, className: `pf-chatbot__feedback-card ${className ? className : ''}` }, props, { children: [_jsx(CardHeader, { actions: {
31
+ _jsx("div", { ref: divRef, id: id, tabIndex: 0, "aria-label": title, children: _jsxs(Card, Object.assign({ isCompact: isCompact, className: `pf-chatbot__feedback-card ${className ? className : ''}` }, props, { children: [_jsx(CardHeader, Object.assign({ actions: {
32
32
  actions: _jsx(CloseButton, { onClose: onClose, ariaLabel: closeButtonAriaLabel })
33
- }, children: _jsx(HeadingLevel, { className: "pf-chatbot__feedback-card-title", children: title }) }), _jsx(CardBody, { children: _jsxs(Form, { className: `pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}`, children: [quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, onSelect: (id) => setSelectedResponse(id), isCompact: isCompact })), hasTextArea && (_jsx(TextArea, { value: value, onChange: (_event, value) => {
33
+ } }, cardHeaderProps, { children: _jsx(HeadingLevel, Object.assign({ className: "pf-chatbot__feedback-card-title" }, headingLevelProps, { children: title })) })), _jsx(CardBody, Object.assign({}, cardBodyProps, { children: _jsxs(Form, Object.assign({ className: `pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}` }, formProps, { children: [quickResponses && (_jsx(QuickResponse, { quickResponses: quickResponses, quickResponseContainerProps: quickResponseContainerProps, onSelect: (id) => setSelectedResponse(id), isCompact: isCompact })), hasTextArea && (_jsx(TextArea, Object.assign({ value: value, onChange: (_event, value) => {
34
34
  setValue(value);
35
35
  onTextAreaChange && onTextAreaChange(_event, value);
36
- }, placeholder: textAreaPlaceholder, "aria-label": textAreaAriaLabel, resizeOrientation: "vertical" })), _jsx(ActionGroup, { children: _jsx(Button, { onClick: () => onSubmit(selectedResponse, value), children: submitWord }) })] }) })] })) }));
36
+ }, placeholder: textAreaPlaceholder, "aria-label": textAreaAriaLabel, resizeOrientation: "vertical" }, textAreaProps))), children, _jsx(ActionGroup, Object.assign({}, actionGroupProps, { children: _jsx(Button, Object.assign({ onClick: () => onSubmit(selectedResponse, value) }, submitButtonProps, { children: submitWord })) }))] })) }))] })) }));
37
37
  };
38
38
  export default UserFeedback;
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { jsx as _jsx } from "react/jsx-runtime";
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { render, screen } from '@testing-library/react';
12
12
  import '@testing-library/jest-dom';
13
13
  import userEvent from '@testing-library/user-event';
@@ -129,4 +129,48 @@ describe('UserFeedback', () => {
129
129
  render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, "data-testid": "card", isCompact: true }));
130
130
  expect(screen.getByTestId('card')).toHaveClass('pf-m-compact');
131
131
  });
132
+ it('should pass buttonProps to submit button', () => {
133
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, submitButtonProps: { variant: 'secondary', isDisabled: true } }));
134
+ const submitButton = screen.getByRole('button', { name: /Submit/i });
135
+ expect(submitButton).toHaveClass('pf-v6-c-button pf-m-secondary');
136
+ expect(submitButton).toBeDisabled();
137
+ });
138
+ it('should pass cardHeaderProps to card header', () => {
139
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, cardHeaderProps: { 'data-testid': 'card-header', className: 'custom-header' } }));
140
+ const cardHeader = screen.getByTestId('card-header');
141
+ expect(cardHeader).toHaveClass('custom-header');
142
+ });
143
+ it('should pass cardBodyProps to card body', () => {
144
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, cardBodyProps: { 'data-testid': 'card-body', className: 'custom-body' } }));
145
+ const cardBody = screen.getByTestId('card-body');
146
+ expect(cardBody).toHaveClass('custom-body');
147
+ });
148
+ it('should pass headingLevelProps to title heading', () => {
149
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, headingLevelProps: { className: 'custom-heading', id: 'feedback-title' } }));
150
+ const heading = screen.getByRole('heading', { level: 1, name: /Why did you choose this rating?/i });
151
+ expect(heading).toHaveClass('custom-heading');
152
+ expect(heading).toHaveAttribute('id', 'feedback-title');
153
+ });
154
+ it('should pass formProps to form', () => {
155
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, formProps: { 'data-testid': 'feedback-form', className: 'custom-form' } }));
156
+ const form = screen.getByTestId('feedback-form');
157
+ expect(form).toHaveClass('custom-form');
158
+ });
159
+ it('should pass textAreaProps to text area when hasTextArea is true', () => {
160
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, hasTextArea: true, textAreaProps: { 'data-testid': 'custom-textarea', rows: 5 } }));
161
+ const textArea = screen.getByTestId('custom-textarea');
162
+ expect(textArea).toHaveAttribute('rows', '5');
163
+ expect(textArea).toHaveAttribute('data-testid', 'custom-textarea');
164
+ });
165
+ it('should pass actionGroupProps to action group', () => {
166
+ render(_jsx(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, actionGroupProps: { 'data-testid': 'action-group', className: 'custom-actions' } }));
167
+ const actionGroup = screen.getByTestId('action-group');
168
+ expect(actionGroup).toHaveClass('custom-actions');
169
+ });
170
+ it('should render children', () => {
171
+ render(_jsxs(UserFeedback, { timestamp: "12/12/12", onClose: jest.fn, onSubmit: jest.fn, quickResponses: MOCK_RESPONSES, children: [_jsx("div", { "data-testid": "custom-content", children: "Custom feedback content" }), _jsx("p", { children: "Additional paragraph" })] }));
172
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument();
173
+ expect(screen.getByText('Custom feedback content')).toBeInTheDocument();
174
+ expect(screen.getByText('Additional paragraph')).toBeInTheDocument();
175
+ });
132
176
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.4.0-prerelease.10",
3
+ "version": "6.4.0-prerelease.12",
4
4
  "description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,104 +1,130 @@
1
1
  import { useState, FunctionComponent } from 'react';
2
2
  import Message from '@patternfly/chatbot/dist/dynamic/Message';
3
3
  import patternflyAvatar from './patternfly_avatar.jpg';
4
- import { Checkbox, FormGroup, Stack } from '@patternfly/react-core';
4
+ import { Checkbox, FormGroup, Flex, FlexItem } from '@patternfly/react-core';
5
5
 
6
6
  export const MessageWithFeedbackExample: FunctionComponent = () => {
7
7
  const [hasCloseButton, setHasCloseButton] = useState(false);
8
8
  const [hasTextArea, setHasTextArea] = useState(false);
9
+ const [hasChildren, setHasChildren] = useState(false);
10
+
11
+ const children = <>Do not share any personal or other sensitive information in your feedback.</>;
9
12
 
10
13
  return (
11
14
  <>
12
- <Stack hasGutter>
13
- <FormGroup role="radiogroup" isInline fieldId="feedback-card" label="Variant">
14
- <Checkbox
15
- isChecked={hasTextArea}
16
- onChange={() => {
17
- setHasTextArea(!hasTextArea);
15
+ <Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsMd' }}>
16
+ <FlexItem>
17
+ <FormGroup role="radiogroup" isInline isStack fieldId="feedback-card" label="Variant">
18
+ <Checkbox
19
+ isChecked={hasTextArea}
20
+ onChange={() => {
21
+ setHasTextArea(!hasTextArea);
22
+ }}
23
+ name="feedback-card-with-text-area"
24
+ label="Has text area"
25
+ id="has-text-area"
26
+ />
27
+ <Checkbox
28
+ isChecked={hasChildren}
29
+ onChange={() => {
30
+ setHasChildren(!hasChildren);
31
+ }}
32
+ name="feedback-card-with-children"
33
+ label="Has additional content"
34
+ id="has-children"
35
+ />
36
+ </FormGroup>
37
+ </FlexItem>
38
+ <FlexItem>
39
+ <Message
40
+ name="Bot"
41
+ role="bot"
42
+ avatar={patternflyAvatar}
43
+ content="This is a message with the feedback card:"
44
+ userFeedbackForm={{
45
+ quickResponses: [
46
+ { id: '1', content: 'Helpful information' },
47
+ { id: '2', content: 'Easy to understand' },
48
+ { id: '3', content: 'Resolved my issue' }
49
+ ],
50
+ onSubmit: (quickResponse, additionalFeedback) =>
51
+ alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`),
52
+ hasTextArea,
53
+ children: hasChildren ? children : undefined,
54
+ // eslint-disable-next-line no-console
55
+ onClose: () => console.log('closed feedback form'),
56
+ focusOnLoad: false
18
57
  }}
19
- name="basic-inline-radio"
20
- label="Has text area"
21
- id="has-text-area"
22
58
  />
23
- </FormGroup>
24
- <Message
25
- name="Bot"
26
- role="bot"
27
- avatar={patternflyAvatar}
28
- content="This is a message with the feedback card:"
29
- userFeedbackForm={{
30
- quickResponses: [
31
- { id: '1', content: 'Helpful information' },
32
- { id: '2', content: 'Easy to understand' },
33
- { id: '3', content: 'Resolved my issue' }
34
- ],
35
- onSubmit: (quickResponse, additionalFeedback) =>
36
- alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`),
37
- hasTextArea,
38
- // eslint-disable-next-line no-console
39
- onClose: () => console.log('closed feedback form'),
40
- focusOnLoad: false
41
- }}
42
- />
43
- <Message
44
- name="Bot"
45
- role="bot"
46
- avatar={patternflyAvatar}
47
- content="This is a compact message with the feedback card:"
48
- userFeedbackForm={{
49
- quickResponses: [
50
- { id: '1', content: 'Helpful information' },
51
- { id: '2', content: 'Easy to understand' },
52
- { id: '3', content: 'Resolved my issue' }
53
- ],
54
- onSubmit: (quickResponse, additionalFeedback) =>
55
- alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`),
56
- hasTextArea,
57
- // eslint-disable-next-line no-console
58
- onClose: () => console.log('closed feedback form'),
59
- focusOnLoad: false
60
- }}
61
- isCompact
62
- />
63
- </Stack>
64
- <Stack hasGutter>
65
- <FormGroup role="radiogroup" isInline fieldId="feedback-thank-you" label="Variant">
66
- <Checkbox
67
- isChecked={hasCloseButton}
68
- onChange={() => {
69
- setHasCloseButton(!hasCloseButton);
59
+ </FlexItem>
60
+ <FlexItem>
61
+ <Message
62
+ name="Bot"
63
+ role="bot"
64
+ avatar={patternflyAvatar}
65
+ content="This is a compact message with the feedback card:"
66
+ userFeedbackForm={{
67
+ quickResponses: [
68
+ { id: '1', content: 'Helpful information' },
69
+ { id: '2', content: 'Easy to understand' },
70
+ { id: '3', content: 'Resolved my issue' }
71
+ ],
72
+ onSubmit: (quickResponse, additionalFeedback) =>
73
+ alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`),
74
+ hasTextArea,
75
+ children: hasChildren ? children : undefined,
76
+ // eslint-disable-next-line no-console
77
+ onClose: () => console.log('closed feedback form'),
78
+ focusOnLoad: false
70
79
  }}
71
- name="basic-inline-radio"
72
- label="Has close button"
73
- id="has-close"
80
+ isCompact
74
81
  />
75
- </FormGroup>
76
- <Message
77
- name="Bot"
78
- role="bot"
79
- avatar={patternflyAvatar}
80
- content="This is a thank-you message, which is displayed once the feedback card is submitted:"
81
- // eslint-disable-next-line no-console
82
- userFeedbackComplete={{
82
+ </FlexItem>
83
+ </Flex>
84
+ <Flex direction={{ default: 'column' }}>
85
+ <FlexItem>
86
+ <FormGroup role="radiogroup" isInline fieldId="feedback-thank-you" label="Variant">
87
+ <Checkbox
88
+ isChecked={hasCloseButton}
89
+ onChange={() => {
90
+ setHasCloseButton(!hasCloseButton);
91
+ }}
92
+ name="basic-inline-radio"
93
+ label="Has close button"
94
+ id="has-close"
95
+ />
96
+ </FormGroup>
97
+ </FlexItem>
98
+ <FlexItem>
99
+ <Message
100
+ name="Bot"
101
+ role="bot"
102
+ avatar={patternflyAvatar}
103
+ content="This is a thank-you message, which is displayed once the feedback card is submitted:"
83
104
  // eslint-disable-next-line no-console
84
- onClose: hasCloseButton ? () => console.log('closed completion message') : undefined,
85
- focusOnLoad: false
86
- }}
87
- />
88
- <Message
89
- name="Bot"
90
- role="bot"
91
- avatar={patternflyAvatar}
92
- content="This is a compact thank-you message, which is displayed once the feedback card is submitted:"
93
- // eslint-disable-next-line no-console
94
- userFeedbackComplete={{
105
+ userFeedbackComplete={{
106
+ // eslint-disable-next-line no-console
107
+ onClose: hasCloseButton ? () => console.log('closed completion message') : undefined,
108
+ focusOnLoad: false
109
+ }}
110
+ />
111
+ </FlexItem>
112
+ <FlexItem>
113
+ <Message
114
+ name="Bot"
115
+ role="bot"
116
+ avatar={patternflyAvatar}
117
+ content="This is a compact thank-you message, which is displayed once the feedback card is submitted:"
95
118
  // eslint-disable-next-line no-console
96
- onClose: hasCloseButton ? () => console.log('closed completion message') : undefined,
97
- focusOnLoad: false
98
- }}
99
- isCompact
100
- />
101
- </Stack>
119
+ userFeedbackComplete={{
120
+ // eslint-disable-next-line no-console
121
+ onClose: hasCloseButton ? () => console.log('closed completion message') : undefined,
122
+ focusOnLoad: false
123
+ }}
124
+ isCompact
125
+ />
126
+ </FlexItem>
127
+ </Flex>
102
128
  </>
103
129
  );
104
130
  };
@@ -34,10 +34,7 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
34
34
  import MessageDivider from '@patternfly/chatbot/dist/dynamic/MessageDivider';
35
35
  import { rehypeCodeBlockToggle } from '@patternfly/chatbot/dist/esm/Message/Plugins/rehypeCodeBlockToggle';
36
36
  import SourcesCard from '@patternfly/chatbot/dist/dynamic/SourcesCard';
37
- import { RobotIcon } from '@patternfly/react-icons/dist/esm/icons/robot-icon';
38
- import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon';
39
- import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon';
40
- import RedoIcon from '@patternfly/react-icons/dist/esm/icons/redo-icon';
37
+ import { ArrowCircleDownIcon, ArrowRightIcon, CheckCircleIcon, CubeIcon, CubesIcon, DownloadIcon, InfoCircleIcon, RedoIcon, RobotIcon } from '@patternfly/react-icons';
41
38
  import patternflyAvatar from './patternfly_avatar.jpg';
42
39
  import AttachmentEdit from '@patternfly/chatbot/dist/dynamic/AttachmentEdit';
43
40
  import FileDetails from '@patternfly/chatbot/dist/dynamic/FileDetails';
@@ -1,8 +1,46 @@
1
- import { Fragment, FunctionComponent } from 'react';
2
-
1
+ import { Fragment, FunctionComponent, useState, useEffect } from 'react';
3
2
  import Message from '@patternfly/chatbot/dist/dynamic/Message';
4
3
  import userAvatar from './user_avatar.svg';
5
- import { Alert, Badge, Button, Card, CardBody, CardFooter, CardTitle } from '@patternfly/react-core';
4
+ import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
5
+ import {
6
+ Accordion,
7
+ AccordionContent,
8
+ AccordionItem,
9
+ AccordionToggle,
10
+ Alert,
11
+ Badge,
12
+ Button,
13
+ ButtonVariant,
14
+ Card,
15
+ CardBody,
16
+ CardExpandableContent,
17
+ CardFooter,
18
+ CardHeader,
19
+ CardTitle,
20
+ CodeBlock,
21
+ CodeBlockCode,
22
+ Content,
23
+ ContentVariants,
24
+ DescriptionList,
25
+ DescriptionListDescription,
26
+ DescriptionListGroup,
27
+ DescriptionListTerm,
28
+ Flex,
29
+ FlexItem,
30
+ HelperText,
31
+ HelperTextItem,
32
+ Icon,
33
+ Progress,
34
+ ProgressMeasureLocation,
35
+ ExpandableSection,
36
+ ExpandableSectionToggle,
37
+ Label,
38
+ Tab,
39
+ Tabs,
40
+ TabTitleText,
41
+ Spinner
42
+ } from '@patternfly/react-core';
43
+ import { ArrowCircleDownIcon, ArrowRightIcon, CheckCircleIcon, CubeIcon, CubesIcon } from '@patternfly/react-icons';
6
44
 
7
45
  const UserActionEndContent = () => {
8
46
  // eslint-disable-next-line no-console
@@ -36,6 +74,543 @@ const BeforeMainContent = () => (
36
74
  </div>
37
75
  );
38
76
 
77
+ interface Stage {
78
+ id: string;
79
+ name: string;
80
+ startProgress: number;
81
+ endProgress: number;
82
+ }
83
+
84
+ const LiveProgressSummaryCard = () => {
85
+ const [isCardExpanded, setIsCardExpanded] = useState(false);
86
+ const [isAccordionExpanded, setIsAccordionExpanded] = useState('installing-toggle');
87
+ const [progress, setProgress] = useState(15);
88
+ const [isSimulationRunning, setIsSimulationRunning] = useState(false);
89
+ const [userManuallyExpandedAccordion, setUserManuallyExpandedAccordion] = useState(false);
90
+
91
+ const stages: Stage[] = [
92
+ {
93
+ id: 'installing-toggle',
94
+ name: 'Installing cluster bootstrap',
95
+ startProgress: 0,
96
+ endProgress: 25
97
+ },
98
+ {
99
+ id: 'setup-toggle',
100
+ name: 'Control plane setup',
101
+ startProgress: 25,
102
+ endProgress: 45
103
+ },
104
+ {
105
+ id: 'deploying-toggle',
106
+ name: 'Deploying cluster operators',
107
+ startProgress: 45,
108
+ endProgress: 85
109
+ },
110
+ {
111
+ id: 'finalizing-toggle',
112
+ name: 'Finalizing installation',
113
+ startProgress: 85,
114
+ endProgress: 100
115
+ }
116
+ ];
117
+
118
+ const getCurrentStage = () =>
119
+ stages.find((stage) => progress >= stage.startProgress && progress < stage.endProgress) ||
120
+ stages[stages.length - 1];
121
+
122
+ const getTimeRemaining = () => {
123
+ const remainingProgress = 100 - progress;
124
+ const estimatedMinutes = Math.max(1, Math.round((remainingProgress / 100) * 30)); // 30 minutes total simulation
125
+ return `About ${estimatedMinutes} minute${estimatedMinutes !== 1 ? 's' : ''} remaining`;
126
+ };
127
+
128
+ const getCurrentStageName = () => {
129
+ const currentStage = getCurrentStage();
130
+ return currentStage.name;
131
+ };
132
+
133
+ const getStageStatus = (stage: Stage) => {
134
+ if (progress >= stage.endProgress) {
135
+ return 'completed';
136
+ }
137
+ if (progress >= stage.startProgress) {
138
+ return 'in-progress';
139
+ }
140
+ return 'pending';
141
+ };
142
+
143
+ const renderStageIcon = (stage: Stage) => {
144
+ const status = getStageStatus(stage);
145
+
146
+ if (status === 'completed') {
147
+ return <CheckCircleIcon color="green" aria-label="Complete" />;
148
+ } else if (status === 'in-progress') {
149
+ return <Spinner size="sm" aria-valuetext="In progress" />;
150
+ } else {
151
+ return <div style={{ width: 'var(--pf-t--global--spacer--md)' }}>{/* Empty space for pending stages */}</div>;
152
+ }
153
+ };
154
+
155
+ // Auto-increment progress when simulation is running
156
+ useEffect(() => {
157
+ let interval;
158
+
159
+ if (isSimulationRunning && progress < 100) {
160
+ interval = setInterval(() => {
161
+ setProgress((prev) => {
162
+ const increment = Math.random() * 2 + 0.5; // Random increment between 0.5-2.5%
163
+ const newProgress = Math.min(prev + increment, 100);
164
+
165
+ // Stop simulation when complete
166
+ if (newProgress >= 100) {
167
+ setIsSimulationRunning(false);
168
+ setUserManuallyExpandedAccordion(false); // Reset manual override when simulation completes
169
+ }
170
+
171
+ return newProgress;
172
+ });
173
+ }, 800);
174
+ }
175
+
176
+ return () => {
177
+ if (interval) {
178
+ clearInterval(interval);
179
+ }
180
+ };
181
+ }, [isSimulationRunning, progress]);
182
+
183
+ // Auto-expand accordion to show current stage (only if user hasn't manually overridden)
184
+ useEffect(() => {
185
+ if (isSimulationRunning && !userManuallyExpandedAccordion) {
186
+ setIsAccordionExpanded(getCurrentStage().id);
187
+ }
188
+ }, [progress, isSimulationRunning, userManuallyExpandedAccordion]);
189
+
190
+ const onExpandCard = (_event: React.MouseEvent) => {
191
+ setIsCardExpanded(!isCardExpanded);
192
+ };
193
+
194
+ const onExpandAccordion = (id: string) => {
195
+ setUserManuallyExpandedAccordion(true);
196
+
197
+ if (id === isAccordionExpanded) {
198
+ setIsAccordionExpanded('');
199
+ } else {
200
+ setIsAccordionExpanded(id);
201
+ }
202
+ };
203
+
204
+ const getStageContent = (stage: Stage) => {
205
+ const status = getStageStatus(stage);
206
+
207
+ if (status === 'in-progress') {
208
+ switch (stage.id) {
209
+ case 'installing-toggle':
210
+ return `Installing bootstrap node...
211
+ Installing etcd cluster...
212
+ Installing control plane...`;
213
+ case 'setup-toggle':
214
+ return `Configuring cluster networking...
215
+ Setting up OpenShift API server...
216
+ Configuring authentication...
217
+ Setting up cluster operators...`;
218
+ case 'deploying-toggle':
219
+ return `Deploying openshift-apiserver operator...
220
+ Deploying openshift-sdn operator...
221
+ Deploying openshift-ingress operator...
222
+ `;
223
+ case 'finalizing-toggle':
224
+ return `Finalizing cluster configuration...
225
+ Running post-installation tasks...
226
+ Setting up cluster console...`;
227
+ }
228
+ }
229
+ if (status === 'pending') {
230
+ return 'Processing...';
231
+ }
232
+ return 'Complete!';
233
+ };
234
+
235
+ const renderCodeBlock = (stage: Stage) => (
236
+ <CodeBlock
237
+ style={
238
+ {
239
+ '--pf-v6-c-code-block--BackgroundColor': 'var(--pf-t--color--gray--95)',
240
+ '--pf-v6-c-code-block--BorderRadius': 'var(--pf-t--global--border--radius--small)'
241
+ } as React.CSSProperties
242
+ }
243
+ >
244
+ <CodeBlockCode>{getStageContent(stage)}</CodeBlockCode>
245
+ </CodeBlock>
246
+ );
247
+
248
+ return (
249
+ <>
250
+ <Flex className="pf-v6-u-mt-lg" spaceItems={{ default: 'spaceItemsSm' }}>
251
+ <FlexItem>
252
+ <Button
253
+ variant="primary"
254
+ size="sm"
255
+ onClick={() => {
256
+ setProgress(15);
257
+ setIsSimulationRunning(true);
258
+ setUserManuallyExpandedAccordion(false);
259
+ }}
260
+ isDisabled={isSimulationRunning}
261
+ >
262
+ {isSimulationRunning ? 'Simulation running...' : 'Start simulation of cluster installation progress'}
263
+ </Button>
264
+ </FlexItem>
265
+ <FlexItem>
266
+ <Button
267
+ variant="secondary"
268
+ size="sm"
269
+ onClick={() => {
270
+ setIsSimulationRunning(false);
271
+ setProgress(15);
272
+ setIsAccordionExpanded('installing-toggle');
273
+ setUserManuallyExpandedAccordion(false);
274
+ }}
275
+ >
276
+ Reset
277
+ </Button>
278
+ </FlexItem>
279
+ </Flex>
280
+ <Card ouiaId="LiveProgressSummaryCard" isExpanded={isCardExpanded}>
281
+ <CardHeader
282
+ onExpand={onExpandCard}
283
+ isToggleRightAligned
284
+ toggleButtonProps={{
285
+ id: 'toggle-button1',
286
+ 'aria-label': 'Details',
287
+ 'aria-labelledby': 'expandable-card-title toggle-button1',
288
+ 'aria-expanded': isCardExpanded
289
+ }}
290
+ >
291
+ <DescriptionList>
292
+ <DescriptionListGroup
293
+ style={
294
+ {
295
+ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--md)'
296
+ } as React.CSSProperties
297
+ }
298
+ >
299
+ <DescriptionListTerm id="title-outside-progress-example-label">
300
+ OpenShift cluster installation
301
+ </DescriptionListTerm>
302
+ <DescriptionListDescription>
303
+ <Progress
304
+ aria-labelledby="title-outside-progress-example-label"
305
+ value={progress}
306
+ measureLocation={ProgressMeasureLocation.outside}
307
+ style={{ '--pf-v6-c-progress--GridGap': 'var(--pf-t--global--spacer--sm' } as React.CSSProperties}
308
+ helperText={
309
+ <Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
310
+ <FlexItem>
311
+ <HelperText>
312
+ <HelperTextItem>
313
+ {progress >= 100 ? 'Installation complete!' : getCurrentStageName()}
314
+ </HelperTextItem>
315
+ </HelperText>
316
+ </FlexItem>
317
+ <FlexItem>
318
+ <HelperText>
319
+ {/* Progress was getting announced on VoiceOver constantly - this helps avoid that */}
320
+ <HelperTextItem aria-live="off">
321
+ {progress >= 100 ? 'Completed' : getTimeRemaining()}
322
+ </HelperTextItem>
323
+ </HelperText>
324
+ </FlexItem>
325
+ </Flex>
326
+ }
327
+ />
328
+ </DescriptionListDescription>
329
+ </DescriptionListGroup>
330
+ </DescriptionList>
331
+ </CardHeader>
332
+ <CardExpandableContent>
333
+ <CardBody>
334
+ <hr className="pf-v6-u-mb-md" />
335
+ <Accordion
336
+ style={
337
+ {
338
+ '--pf-v6-c-accordion__expandable-content-body--PaddingBlockStart': 'var(--pf-t--global--spacer--md)',
339
+ '--pf-v6-c-accordion__expandable-content-body--PaddingBlockEnd': '0',
340
+ '--pf-v6-c-accordion__expandable-content-body--PaddingInlineStart': '0',
341
+ '--pf-v6-c-accordion__expandable-content-body--PaddingInlineEnd': '0',
342
+ '--pf-v6-c-accordion__expandable-content--BackgroundColor': 'initial',
343
+ '--pf-v6-c-accordion__expandable-content--Color': '#fff'
344
+ } as React.CSSProperties
345
+ }
346
+ >
347
+ {stages.map((stage) => (
348
+ <AccordionItem key={stage.id} isExpanded={isAccordionExpanded === stage.id}>
349
+ <AccordionToggle
350
+ onClick={() => {
351
+ onExpandAccordion(stage.id);
352
+ }}
353
+ id={stage.id}
354
+ >
355
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
356
+ <FlexItem>{renderStageIcon(stage)}</FlexItem>
357
+ <FlexItem>{stage.name}</FlexItem>
358
+ </Flex>
359
+ </AccordionToggle>
360
+ <AccordionContent id={stage.id.replace('-toggle', '')}>{renderCodeBlock(stage)}</AccordionContent>
361
+ </AccordionItem>
362
+ ))}
363
+ </Accordion>
364
+ </CardBody>
365
+ </CardExpandableContent>
366
+ <CardFooter>
367
+ <Button isBlock variant="tertiary" icon={<ArrowRightIcon />} iconPosition="end">
368
+ Open in console
369
+ </Button>
370
+ </CardFooter>
371
+ </Card>
372
+ </>
373
+ );
374
+ };
375
+
376
+ const VersionSelectorCard = () => {
377
+ const [activeTabKey, setActiveTabKey] = useState<string | number>(0);
378
+ const [isCardSelected, setIsCardSelected] = useState('');
379
+ const [isExpanded, setIsExpanded] = useState(false);
380
+ const id1 = '4.20';
381
+ const id2 = '4.19';
382
+ const id3 = '4.18';
383
+ const id4 = '4.17';
384
+
385
+ const onChange = (event: React.FormEvent<HTMLInputElement>) => {
386
+ setIsCardSelected(event.currentTarget.id);
387
+ };
388
+
389
+ const onToggleExpandableSection = (isExpanded: boolean) => {
390
+ setIsExpanded(isExpanded);
391
+ };
392
+
393
+ const handleTabClick = (_event: any, tabIndex: string | number) => {
394
+ setActiveTabKey(tabIndex);
395
+ };
396
+
397
+ const contentId = 'detached-expandable-section-content';
398
+ const toggleId = 'detached-expandable-section-toggle';
399
+
400
+ const generateTabContent = (title: string, subtitle: string) => (
401
+ <Flex direction={{ default: 'column' }} alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapMd' }}>
402
+ <FlexItem alignSelf={{ default: 'alignSelfCenter' }} className="pf-v6-u-mt-md pf-v6-u-text-align-center">
403
+ <div className="pf-v6-u-font-weight-bold">{title}</div>
404
+ <div>{subtitle}</div>
405
+ </FlexItem>
406
+ <FlexItem alignSelf={{ default: 'alignSelfStretch' }}>
407
+ <Card id="4.20-card" isSelectable isSelected={isCardSelected === id1}>
408
+ <CardHeader
409
+ selectableActions={{
410
+ selectableActionId: id1,
411
+ selectableActionAriaLabelledby: '4.20-card',
412
+ name: 'version',
413
+ variant: 'single',
414
+ onChange,
415
+ isHidden: true
416
+ }}
417
+ >
418
+ <CardTitle>
419
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
420
+ <FlexItem>4.20.0-ec.3</FlexItem>
421
+ <FlexItem>
422
+ <Label isCompact color={isCardSelected === id1 ? 'blue' : undefined}>
423
+ Preview
424
+ </Label>
425
+ </FlexItem>
426
+ </Flex>
427
+ </CardTitle>
428
+ </CardHeader>
429
+ <CardBody>Developer preview • Not for production</CardBody>
430
+ </Card>
431
+ </FlexItem>
432
+ <FlexItem alignSelf={{ default: 'alignSelfStretch' }}>
433
+ <Card id="4.19-card" isSelectable isSelected={isCardSelected === id2}>
434
+ <CardHeader
435
+ selectableActions={{
436
+ selectableActionId: id2,
437
+ selectableActionAriaLabelledby: '4.19-card',
438
+ name: 'version',
439
+ variant: 'single',
440
+ onChange,
441
+ isHidden: true
442
+ }}
443
+ >
444
+ <CardTitle>
445
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
446
+ <FlexItem>4.19.2</FlexItem>
447
+ <FlexItem>
448
+ <Label isCompact color={isCardSelected === id2 ? 'blue' : undefined}>
449
+ Latest
450
+ </Label>
451
+ </FlexItem>
452
+ </Flex>
453
+ </CardTitle>
454
+ </CardHeader>
455
+ <CardBody>Newest features • 18-month support • Recommended</CardBody>
456
+ </Card>
457
+ </FlexItem>
458
+ <FlexItem alignSelf={{ default: 'alignSelfStretch' }}>
459
+ <Card id="4.18-card" isSelectable isSelected={isCardSelected === id3}>
460
+ <CardHeader
461
+ selectableActions={{
462
+ selectableActionId: id3,
463
+ selectableActionAriaLabelledby: '4.18-card',
464
+ name: 'version',
465
+ variant: 'single',
466
+ onChange,
467
+ isHidden: true
468
+ }}
469
+ >
470
+ <CardTitle>4.18.19</CardTitle>
471
+ </CardHeader>
472
+ <CardBody>Previous stable • Full support</CardBody>
473
+ </Card>
474
+ <ExpandableSection isExpanded={isExpanded} isDetached toggleId={toggleId} contentId={contentId}>
475
+ <Flex
476
+ direction={{ default: 'column' }}
477
+ alignItems={{ default: 'alignItemsCenter' }}
478
+ gap={{ default: 'gapMd' }}
479
+ >
480
+ <FlexItem alignSelf={{ default: 'alignSelfStretch' }}>
481
+ <Card className="pf-v6-u-mt-md" id="4.17-card" isSelectable isSelected={isCardSelected === id4}>
482
+ <CardHeader
483
+ selectableActions={{
484
+ selectableActionId: id4,
485
+ selectableActionAriaLabelledby: '4.17-card',
486
+ name: 'version',
487
+ variant: 'single',
488
+ onChange,
489
+ isHidden: true
490
+ }}
491
+ >
492
+ <CardTitle>
493
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
494
+ <FlexItem>4.17.34</FlexItem>
495
+ <FlexItem>
496
+ <Label isCompact color={isCardSelected === id4 ? 'blue' : undefined}>
497
+ Maintenance
498
+ </Label>
499
+ </FlexItem>
500
+ </Flex>
501
+ </CardTitle>
502
+ </CardHeader>
503
+ <CardBody>Maintenance support phase</CardBody>
504
+ </Card>
505
+ </FlexItem>
506
+ </Flex>
507
+ </ExpandableSection>
508
+ </FlexItem>
509
+ <FlexItem>
510
+ <ExpandableSectionToggle
511
+ isExpanded={isExpanded}
512
+ onToggle={onToggleExpandableSection}
513
+ toggleId={toggleId}
514
+ contentId={contentId}
515
+ direction="up"
516
+ >
517
+ {isExpanded ? 'Hide older versions' : 'Show older versions'}
518
+ </ExpandableSectionToggle>
519
+ </FlexItem>
520
+ </Flex>
521
+ );
522
+
523
+ return (
524
+ <Card ouiaId="VersionSelectorCard">
525
+ <CardBody
526
+ style={{ '--pf-v6-c-card--child--PaddingBlockEnd': 'var(--pf-t--global--spacer--md)' } as React.CSSProperties}
527
+ >
528
+ <Tabs activeKey={activeTabKey} onSelect={handleTabClick} aria-label="Architecture" role="region" isFilled>
529
+ <Tab
530
+ eventKey={0}
531
+ title={
532
+ <TabTitleText>
533
+ <Flex spaceItems={{ default: 'spaceItemsXs' }}>
534
+ <FlexItem>
535
+ <CubeIcon />
536
+ </FlexItem>
537
+ <FlexItem>Single arch</FlexItem>
538
+ </Flex>
539
+ </TabTitleText>
540
+ }
541
+ >
542
+ {generateTabContent('x86_64 Intel/AMD only', 'Standard deployments • Most common choice')}
543
+ </Tab>
544
+ <Tab
545
+ eventKey={1}
546
+ title={
547
+ <TabTitleText>
548
+ <Flex spaceItems={{ default: 'spaceItemsXs' }}>
549
+ <FlexItem>
550
+ <CubesIcon />
551
+ </FlexItem>
552
+ <FlexItem>Multi arch</FlexItem>
553
+ </Flex>
554
+ </TabTitleText>
555
+ }
556
+ >
557
+ {generateTabContent('Multi arch', 'Standard deployments')}
558
+ </Tab>
559
+ </Tabs>
560
+ </CardBody>
561
+ <CardFooter>
562
+ <Button isBlock isDisabled={isCardSelected === ''}>
563
+ Continue with selections
564
+ </Button>
565
+ </CardFooter>
566
+ </Card>
567
+ );
568
+ };
569
+
570
+ const DownloadCard = () => (
571
+ <Card>
572
+ <CardHeader isToggleRightAligned>
573
+ <CardTitle>
574
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
575
+ <FlexItem>
576
+ <Icon size="lg" status="success">
577
+ <CheckCircleIcon />
578
+ </Icon>
579
+ </FlexItem>
580
+ <FlexItem>Your discovery ISO is ready</FlexItem>
581
+ </Flex>
582
+ </CardTitle>
583
+ </CardHeader>
584
+ <CardBody>
585
+ <Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsLg' }}>
586
+ <FlexItem>
587
+ <Content component={ContentVariants.p}>
588
+ To begin adding hosts to your bare metal cluster, you first need to boot them with the generated Discovery
589
+ ISO. This allows the installation program to see and manage your hardware.
590
+ </Content>
591
+ </FlexItem>
592
+
593
+ <Flex direction={{ default: 'column' }} spaceItems={{ default: 'spaceItemsSm' }}>
594
+ <FlexItem>
595
+ <Button variant={ButtonVariant.primary} icon={<ArrowCircleDownIcon />} isBlock>
596
+ Download Discovery ISO
597
+ </Button>
598
+ </FlexItem>
599
+
600
+ <FlexItem alignSelf={{ default: 'alignSelfCenter' }}>
601
+ <Content component={ContentVariants.small}>1.2 GB • Expires in 24 hours</Content>
602
+ </FlexItem>
603
+ </Flex>
604
+ </Flex>
605
+ </CardBody>
606
+ <CardFooter>
607
+ <Content component={ContentVariants.small}>
608
+ <strong>Next step:</strong> After downloading, boot your bare metal hosts from this ISO image.
609
+ </Content>
610
+ </CardFooter>
611
+ </Card>
612
+ );
613
+
39
614
  export const UserMessageWithExtraContent: FunctionComponent = () => (
40
615
  <>
41
616
  <Message
@@ -50,5 +625,34 @@ export const UserMessageWithExtraContent: FunctionComponent = () => (
50
625
  endContent: <UserActionEndContent />
51
626
  }}
52
627
  />
628
+ <Message
629
+ avatar={patternflyAvatar}
630
+ name="Bot"
631
+ role="bot"
632
+ content="This is a message with a live progress summmary card."
633
+ timestamp="1 hour ago"
634
+ extraContent={{
635
+ afterMainContent: <LiveProgressSummaryCard />
636
+ }}
637
+ />
638
+ <Message
639
+ avatar={patternflyAvatar}
640
+ name="Bot"
641
+ role="bot"
642
+ content="This is a message with a version selector card."
643
+ timestamp="1 hour ago"
644
+ extraContent={{
645
+ afterMainContent: <VersionSelectorCard />
646
+ }}
647
+ />
648
+ <Message
649
+ avatar={patternflyAvatar}
650
+ name="Bot"
651
+ role="bot"
652
+ content="All set! I've finished building the Discovery ISO. The next step is to download it and boot your hosts, which you can do using the summary card I've prepared for you:"
653
+ extraContent={{
654
+ endContent: <DownloadCard />
655
+ }}
656
+ />
53
657
  </>
54
658
  );
@@ -245,4 +245,111 @@ describe('UserFeedback', () => {
245
245
  );
246
246
  expect(screen.getByTestId('card')).toHaveClass('pf-m-compact');
247
247
  });
248
+ it('should pass buttonProps to submit button', () => {
249
+ render(
250
+ <UserFeedback
251
+ timestamp="12/12/12"
252
+ onClose={jest.fn}
253
+ onSubmit={jest.fn}
254
+ quickResponses={MOCK_RESPONSES}
255
+ submitButtonProps={{ variant: 'secondary', isDisabled: true }}
256
+ />
257
+ );
258
+ const submitButton = screen.getByRole('button', { name: /Submit/i });
259
+ expect(submitButton).toHaveClass('pf-v6-c-button pf-m-secondary');
260
+ expect(submitButton).toBeDisabled();
261
+ });
262
+ it('should pass cardHeaderProps to card header', () => {
263
+ render(
264
+ <UserFeedback
265
+ timestamp="12/12/12"
266
+ onClose={jest.fn}
267
+ onSubmit={jest.fn}
268
+ quickResponses={MOCK_RESPONSES}
269
+ cardHeaderProps={{ 'data-testid': 'card-header', className: 'custom-header' } as any}
270
+ />
271
+ );
272
+ const cardHeader = screen.getByTestId('card-header');
273
+ expect(cardHeader).toHaveClass('custom-header');
274
+ });
275
+ it('should pass cardBodyProps to card body', () => {
276
+ render(
277
+ <UserFeedback
278
+ timestamp="12/12/12"
279
+ onClose={jest.fn}
280
+ onSubmit={jest.fn}
281
+ quickResponses={MOCK_RESPONSES}
282
+ cardBodyProps={{ 'data-testid': 'card-body', className: 'custom-body' } as any}
283
+ />
284
+ );
285
+ const cardBody = screen.getByTestId('card-body');
286
+ expect(cardBody).toHaveClass('custom-body');
287
+ });
288
+ it('should pass headingLevelProps to title heading', () => {
289
+ render(
290
+ <UserFeedback
291
+ timestamp="12/12/12"
292
+ onClose={jest.fn}
293
+ onSubmit={jest.fn}
294
+ quickResponses={MOCK_RESPONSES}
295
+ headingLevelProps={{ className: 'custom-heading', id: 'feedback-title' }}
296
+ />
297
+ );
298
+ const heading = screen.getByRole('heading', { level: 1, name: /Why did you choose this rating?/i });
299
+ expect(heading).toHaveClass('custom-heading');
300
+ expect(heading).toHaveAttribute('id', 'feedback-title');
301
+ });
302
+
303
+ it('should pass formProps to form', () => {
304
+ render(
305
+ <UserFeedback
306
+ timestamp="12/12/12"
307
+ onClose={jest.fn}
308
+ onSubmit={jest.fn}
309
+ quickResponses={MOCK_RESPONSES}
310
+ formProps={{ 'data-testid': 'feedback-form', className: 'custom-form' } as any}
311
+ />
312
+ );
313
+ const form = screen.getByTestId('feedback-form');
314
+ expect(form).toHaveClass('custom-form');
315
+ });
316
+ it('should pass textAreaProps to text area when hasTextArea is true', () => {
317
+ render(
318
+ <UserFeedback
319
+ timestamp="12/12/12"
320
+ onClose={jest.fn}
321
+ onSubmit={jest.fn}
322
+ quickResponses={MOCK_RESPONSES}
323
+ hasTextArea
324
+ textAreaProps={{ 'data-testid': 'custom-textarea', rows: 5 } as any}
325
+ />
326
+ );
327
+ const textArea = screen.getByTestId('custom-textarea');
328
+ expect(textArea).toHaveAttribute('rows', '5');
329
+ expect(textArea).toHaveAttribute('data-testid', 'custom-textarea');
330
+ });
331
+ it('should pass actionGroupProps to action group', () => {
332
+ render(
333
+ <UserFeedback
334
+ timestamp="12/12/12"
335
+ onClose={jest.fn}
336
+ onSubmit={jest.fn}
337
+ quickResponses={MOCK_RESPONSES}
338
+ actionGroupProps={{ 'data-testid': 'action-group', className: 'custom-actions' } as any}
339
+ />
340
+ );
341
+ const actionGroup = screen.getByTestId('action-group');
342
+ expect(actionGroup).toHaveClass('custom-actions');
343
+ });
344
+ it('should render children', () => {
345
+ render(
346
+ <UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={jest.fn} quickResponses={MOCK_RESPONSES}>
347
+ <div data-testid="custom-content">Custom feedback content</div>
348
+ <p>Additional paragraph</p>
349
+ </UserFeedback>
350
+ );
351
+ expect(screen.getByTestId('custom-content')).toBeInTheDocument();
352
+ expect(screen.getByText('Custom feedback content')).toBeInTheDocument();
353
+ expect(screen.getByText('Additional paragraph')).toBeInTheDocument();
354
+ });
248
355
  });
@@ -8,15 +8,21 @@ import { useState, useRef, useEffect } from 'react';
8
8
  // Import PatternFly components
9
9
  import {
10
10
  ActionGroup,
11
+ ActionGroupProps,
11
12
  Button,
13
+ ButtonProps,
12
14
  Card,
13
15
  CardBody,
16
+ CardBodyProps,
14
17
  CardHeader,
18
+ CardHeaderProps,
15
19
  CardProps,
16
20
  Form,
21
+ FormProps,
17
22
  LabelGroupProps,
18
23
  OUIAProps,
19
- TextArea
24
+ TextArea,
25
+ TextAreaProps
20
26
  } from '@patternfly/react-core';
21
27
  import QuickResponse from '../QuickResponse/QuickResponse';
22
28
  import CloseButton from './CloseButton';
@@ -54,6 +60,20 @@ export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProp
54
60
  focusOnLoad?: boolean;
55
61
  /** Timestamp passed in by Message for more context in aria announcements */
56
62
  timestamp?: string;
63
+ /** Additional props passed to submit button */
64
+ submitButtonProps?: ButtonProps;
65
+ /** Additional props passed to card header */
66
+ cardHeaderProps?: CardHeaderProps;
67
+ /** Additional props passed to card body */
68
+ cardBodyProps?: CardBodyProps;
69
+ /** Additional props passed to title heading */
70
+ headingLevelProps?: React.HTMLAttributes<HTMLHeadingElement>;
71
+ /** Additional props passed to form */
72
+ formProps?: FormProps;
73
+ /** Additional props passed to text area */
74
+ textAreaProps?: TextAreaProps;
75
+ /** Additional props passed to action group */
76
+ actionGroupProps?: ActionGroupProps;
57
77
  }
58
78
 
59
79
  const UserFeedback: FunctionComponent<UserFeedbackProps> = ({
@@ -74,6 +94,14 @@ const UserFeedback: FunctionComponent<UserFeedbackProps> = ({
74
94
  headingLevel: HeadingLevel = 'h1',
75
95
  focusOnLoad = true,
76
96
  isCompact,
97
+ children,
98
+ cardHeaderProps,
99
+ cardBodyProps,
100
+ headingLevelProps,
101
+ formProps,
102
+ textAreaProps,
103
+ actionGroupProps,
104
+ submitButtonProps,
77
105
  ...props
78
106
  }: UserFeedbackProps) => {
79
107
  const [selectedResponse, setSelectedResponse] = useState<string>();
@@ -94,11 +122,14 @@ const UserFeedback: FunctionComponent<UserFeedbackProps> = ({
94
122
  actions={{
95
123
  actions: <CloseButton onClose={onClose} ariaLabel={closeButtonAriaLabel} />
96
124
  }}
125
+ {...cardHeaderProps}
97
126
  >
98
- <HeadingLevel className="pf-chatbot__feedback-card-title">{title}</HeadingLevel>
127
+ <HeadingLevel className="pf-chatbot__feedback-card-title" {...headingLevelProps}>
128
+ {title}
129
+ </HeadingLevel>
99
130
  </CardHeader>
100
- <CardBody>
101
- <Form className={`pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}`}>
131
+ <CardBody {...cardBodyProps}>
132
+ <Form className={`pf-chatbot__feedback-card-form ${isCompact ? 'pf-m-compact' : ''}`} {...formProps}>
102
133
  {quickResponses && (
103
134
  <QuickResponse
104
135
  quickResponses={quickResponses}
@@ -117,10 +148,14 @@ const UserFeedback: FunctionComponent<UserFeedbackProps> = ({
117
148
  placeholder={textAreaPlaceholder}
118
149
  aria-label={textAreaAriaLabel}
119
150
  resizeOrientation="vertical"
151
+ {...textAreaProps}
120
152
  />
121
153
  )}
122
- <ActionGroup>
123
- <Button onClick={() => onSubmit(selectedResponse, value)}>{submitWord}</Button>
154
+ {children}
155
+ <ActionGroup {...actionGroupProps}>
156
+ <Button onClick={() => onSubmit(selectedResponse, value)} {...submitButtonProps}>
157
+ {submitWord}
158
+ </Button>
124
159
  </ActionGroup>
125
160
  </Form>
126
161
  </CardBody>