@patternfly/chatbot 2.2.0-prerelease.44 → 2.2.0-prerelease.46

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.
Files changed (31) hide show
  1. package/dist/cjs/Message/Message.d.ts +15 -1
  2. package/dist/cjs/Message/Message.js +53 -34
  3. package/dist/cjs/Message/Message.test.js +46 -0
  4. package/dist/cjs/Message/MessageInput.d.ts +18 -0
  5. package/dist/cjs/Message/MessageInput.js +34 -0
  6. package/dist/cjs/SourcesCard/SourcesCard.d.ts +6 -1
  7. package/dist/cjs/SourcesCard/SourcesCard.js +14 -9
  8. package/dist/cjs/SourcesCard/SourcesCard.test.js +25 -11
  9. package/dist/css/main.css +7 -7
  10. package/dist/css/main.css.map +1 -1
  11. package/dist/esm/Message/Message.d.ts +15 -1
  12. package/dist/esm/Message/Message.js +53 -34
  13. package/dist/esm/Message/Message.test.js +46 -0
  14. package/dist/esm/Message/MessageInput.d.ts +18 -0
  15. package/dist/esm/Message/MessageInput.js +29 -0
  16. package/dist/esm/SourcesCard/SourcesCard.d.ts +6 -1
  17. package/dist/esm/SourcesCard/SourcesCard.js +15 -10
  18. package/dist/esm/SourcesCard/SourcesCard.test.js +25 -11
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +1 -1
  21. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +1 -1
  22. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +34 -5
  23. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +1 -1
  24. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +61 -14
  25. package/src/Message/Message.scss +4 -0
  26. package/src/Message/Message.test.tsx +48 -0
  27. package/src/Message/Message.tsx +107 -53
  28. package/src/Message/MessageInput.tsx +59 -0
  29. package/src/SourcesCard/SourcesCard.scss +3 -7
  30. package/src/SourcesCard/SourcesCard.test.tsx +30 -17
  31. package/src/SourcesCard/SourcesCard.tsx +41 -11
@@ -6,6 +6,7 @@ import { AlertActionLink, Form, FormGroup, Radio } from '@patternfly/react-core'
6
6
 
7
7
  export const UserMessageExample: React.FunctionComponent = () => {
8
8
  const [variant, setVariant] = React.useState('code');
9
+ const [isEditable, setIsEditable] = React.useState(true);
9
10
 
10
11
  /* eslint-disable indent */
11
12
  const renderContent = () => {
@@ -33,7 +34,7 @@ export const UserMessageExample: React.FunctionComponent = () => {
33
34
  case 'image':
34
35
  return image;
35
36
  default:
36
- return;
37
+ return '';
37
38
  }
38
39
  };
39
40
  /* eslint-enable indent */
@@ -175,88 +176,131 @@ _Italic text, formatted with single underscores_
175
176
  <FormGroup role="radiogroup" isInline fieldId="user-message-type" label="Message content type">
176
177
  <Radio
177
178
  isChecked={variant === 'code'}
178
- onChange={() => setVariant('code')}
179
+ onChange={() => {
180
+ setVariant('code');
181
+ setIsEditable(true);
182
+ }}
179
183
  name="user-message-type"
180
184
  label="Code"
181
185
  id="user-code"
182
186
  />
183
187
  <Radio
184
188
  isChecked={variant === 'inlineCode'}
185
- onChange={() => setVariant('inlineCode')}
189
+ onChange={() => {
190
+ setVariant('inlineCode');
191
+ setIsEditable(true);
192
+ }}
186
193
  name="user-message-type"
187
194
  label="Inline code"
188
195
  id="user-inline-code"
189
196
  />
190
197
  <Radio
191
198
  isChecked={variant === 'heading'}
192
- onChange={() => setVariant('heading')}
199
+ onChange={() => {
200
+ setVariant('heading');
201
+ setIsEditable(true);
202
+ }}
193
203
  name="user-message-type"
194
204
  label="Heading"
195
205
  id="user-heading"
196
206
  />
197
207
  <Radio
198
208
  isChecked={variant === 'blockQuotes'}
199
- onChange={() => setVariant('blockQuotes')}
209
+ onChange={() => {
210
+ setVariant('blockQuotes');
211
+ setIsEditable(true);
212
+ }}
200
213
  name="user-message-type"
201
214
  label="Block quote"
202
215
  id="user-block-quotes"
203
216
  />
204
217
  <Radio
205
218
  isChecked={variant === 'emphasis'}
206
- onChange={() => setVariant('emphasis')}
219
+ onChange={() => {
220
+ setVariant('emphasis');
221
+ setIsEditable(true);
222
+ }}
207
223
  name="user-message-type"
208
224
  label="Emphasis"
209
225
  id="user-emphasis"
210
226
  />
211
227
  <Radio
212
228
  isChecked={variant === 'link'}
213
- onChange={() => setVariant('link')}
229
+ onChange={() => {
230
+ setVariant('link');
231
+ setIsEditable(true);
232
+ }}
214
233
  name="user-message-type"
215
234
  label="Link"
216
235
  id="user-link"
217
236
  />
218
237
  <Radio
219
238
  isChecked={variant === 'unorderedList'}
220
- onChange={() => setVariant('unorderedList')}
239
+ onChange={() => {
240
+ setVariant('unorderedList');
241
+ setIsEditable(true);
242
+ }}
221
243
  name="user-message-type"
222
244
  label="Unordered list"
223
245
  id="user-unordered-list"
224
246
  />
225
247
  <Radio
226
248
  isChecked={variant === 'orderedList'}
227
- onChange={() => setVariant('orderedList')}
249
+ onChange={() => {
250
+ setVariant('orderedList');
251
+ setIsEditable(true);
252
+ }}
228
253
  name="user-message-type"
229
254
  label="Ordered list"
230
255
  id="user-ordered-list"
231
256
  />
232
257
  <Radio
233
258
  isChecked={variant === 'moreComplexList'}
234
- onChange={() => setVariant('moreComplexList')}
259
+ onChange={() => {
260
+ setVariant('moreComplexList');
261
+ setIsEditable(true);
262
+ }}
235
263
  name="user-message-type"
236
264
  label="More complex list"
237
265
  id="user-more-complex-list"
238
266
  />
239
267
  <Radio
240
268
  isChecked={variant === 'table'}
241
- onChange={() => setVariant('table')}
269
+ onChange={() => {
270
+ setVariant('table');
271
+ setIsEditable(true);
272
+ }}
242
273
  name="user-message-type"
243
274
  label="Table"
244
275
  id="user-table"
245
276
  />
246
277
  <Radio
247
278
  isChecked={variant === 'image'}
248
- onChange={() => setVariant('image')}
279
+ onChange={() => {
280
+ setVariant('image');
281
+ setIsEditable(true);
282
+ }}
249
283
  name="user-message-type"
250
284
  label="Image"
251
285
  id="user-image"
252
286
  />
253
287
  <Radio
254
288
  isChecked={variant === 'error'}
255
- onChange={() => setVariant('error')}
256
- name="user-message-error"
289
+ onChange={() => {
290
+ setVariant('error');
291
+ setIsEditable(true);
292
+ }}
293
+ name="user-message-type"
257
294
  label="Error"
258
295
  id="user-error"
259
296
  />
297
+ <Radio
298
+ isChecked={variant === 'editable'}
299
+ onChange={() => setVariant('editable')}
300
+ name="user-message-type"
301
+ label="Editable"
302
+ id="user-edit"
303
+ />
260
304
  </FormGroup>
261
305
  </Form>
262
306
  <Message
@@ -267,7 +311,10 @@ _Italic text, formatted with single underscores_
267
311
  tableProps={
268
312
  variant === 'table' ? { 'aria-label': 'App information and user roles for user messages' } : undefined
269
313
  }
314
+ isEditable={variant === 'editable' ? isEditable : false}
270
315
  error={variant === 'error' ? error : undefined}
316
+ onEditUpdate={() => setIsEditable(false)}
317
+ onEditCancel={() => setIsEditable(false)}
271
318
  />
272
319
  </>
273
320
  );
@@ -97,6 +97,10 @@
97
97
  flex-wrap: wrap;
98
98
  }
99
99
 
100
+ .pf-chatbot__message-edit-buttons {
101
+ --pf-v6-c-form__group--m-action--MarginBlockStart: 0;
102
+ }
103
+
100
104
  @import './MessageLoading';
101
105
  @import './CodeBlockMessage/CodeBlockMessage';
102
106
  @import './TextMessage/TextMessage';
@@ -815,4 +815,52 @@ describe('Message', () => {
815
815
  expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
816
816
  expect(screen.queryByText('Test')).toBeFalsy();
817
817
  });
818
+ it('should handle isEditable when there is message content', () => {
819
+ render(<Message avatar="./img" role="user" name="User" isEditable content="Test" />);
820
+ expect(screen.getByRole('textbox')).toBeTruthy();
821
+ expect(screen.getByRole('textbox')).toHaveValue('Test');
822
+ expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
823
+ expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
824
+ });
825
+ it('should handle isEditable when there is no message content', () => {
826
+ render(<Message avatar="./img" role="user" name="User" isEditable />);
827
+ expect(screen.getByRole('textbox')).toBeTruthy();
828
+ expect(screen.getByRole('textbox')).toHaveValue('');
829
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Edit prompt message...');
830
+ expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
831
+ expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
832
+ });
833
+ it('should be able to change edit placeholder', () => {
834
+ render(<Message avatar="./img" role="user" name="User" isEditable editPlaceholder="I am a placeholder" />);
835
+ expect(screen.getByRole('textbox')).toBeTruthy();
836
+ expect(screen.getByRole('textbox')).toHaveValue('');
837
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'I am a placeholder');
838
+ });
839
+ it('should be able to change updateWord', () => {
840
+ render(<Message avatar="./img" role="user" name="User" isEditable updateWord="Submit" />);
841
+ expect(screen.getByRole('button', { name: /Submit/i })).toBeTruthy();
842
+ });
843
+ it('should be able to change cancelWord', () => {
844
+ render(<Message avatar="./img" role="user" name="User" isEditable cancelWord="Don't submit" />);
845
+ expect(screen.getByRole('button', { name: /Don't submit/i })).toBeTruthy();
846
+ });
847
+ it('should be able to add onEditUpdate', async () => {
848
+ const spy = jest.fn();
849
+ render(<Message avatar="./img" role="user" name="User" isEditable onEditUpdate={spy} />);
850
+ await userEvent.click(screen.getByRole('button', { name: /Update/i }));
851
+ expect(spy).toHaveBeenCalledTimes(1);
852
+ });
853
+ it('should be able to add onEditCancel', async () => {
854
+ const spy = jest.fn();
855
+ render(<Message avatar="./img" role="user" name="User" isEditable onEditCancel={spy} />);
856
+ await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
857
+ expect(spy).toHaveBeenCalledTimes(1);
858
+ });
859
+ it('should be able to add editFormProps', () => {
860
+ const { container } = render(
861
+ <Message avatar="./img" role="user" name="User" isEditable editFormProps={{ className: 'test' }} />
862
+ );
863
+ const form = container.querySelector('form');
864
+ expect(form).toHaveClass('test');
865
+ });
818
866
  });
@@ -12,6 +12,7 @@ import {
12
12
  AvatarProps,
13
13
  ButtonProps,
14
14
  ContentVariants,
15
+ FormProps,
15
16
  Label,
16
17
  LabelGroupProps,
17
18
  Timestamp,
@@ -45,6 +46,7 @@ import rehypeSanitize from 'rehype-sanitize';
45
46
  import { PluggableList } from 'react-markdown/lib';
46
47
  import LinkMessage from './LinkMessage/LinkMessage';
47
48
  import ErrorMessage from './ErrorMessage/ErrorMessage';
49
+ import MessageInput from './MessageInput';
48
50
 
49
51
  export interface MessageAttachment {
50
52
  /** Name of file attached to the message */
@@ -148,6 +150,20 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
148
150
  error?: AlertProps;
149
151
  /** Props for links */
150
152
  linkProps?: ButtonProps;
153
+ /** Whether message is in edit mode */
154
+ isEditable?: boolean;
155
+ /** Placeholder for edit input */
156
+ editPlaceholder?: string;
157
+ /** Label for the English word "Update" used in edit mode. */
158
+ updateWord?: string;
159
+ /** Label for the English word "Cancel" used in edit mode. */
160
+ cancelWord?: string;
161
+ /** Callback function for when edit mode update button is clicked */
162
+ onEditUpdate?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
163
+ /** Callback functionf or when edit cancel update button is clicked */
164
+ onEditCancel?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
165
+ /** Props for edit form */
166
+ editFormProps?: FormProps;
151
167
  }
152
168
 
153
169
  export const MessageBase: React.FunctionComponent<MessageProps> = ({
@@ -178,8 +194,21 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
178
194
  additionalRehypePlugins = [],
179
195
  linkProps,
180
196
  error,
197
+ isEditable,
198
+ editPlaceholder = 'Edit prompt message...',
199
+ updateWord = 'Update',
200
+ cancelWord = 'Cancel',
201
+ onEditUpdate,
202
+ onEditCancel,
203
+ editFormProps,
181
204
  ...props
182
205
  }: MessageProps) => {
206
+ const [messageText, setMessageText] = React.useState(content);
207
+
208
+ React.useEffect(() => {
209
+ setMessageText(content);
210
+ }, [content]);
211
+
183
212
  const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
184
213
  let rehypePlugins: PluggableList = [rehypeUnwrapImages];
185
214
  if (openLinkInNewTab) {
@@ -197,6 +226,82 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
197
226
  // Keep timestamps consistent between Timestamp component and aria-label
198
227
  const date = new Date();
199
228
  const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
229
+
230
+ const renderMessage = () => {
231
+ if (isLoading) {
232
+ return <MessageLoading loadingWord={loadingWord} />;
233
+ }
234
+ if (isEditable) {
235
+ return (
236
+ <>
237
+ {beforeMainContent && <>{beforeMainContent}</>}
238
+ <MessageInput
239
+ content={content}
240
+ editPlaceholder={editPlaceholder}
241
+ updateWord={updateWord}
242
+ cancelWord={cancelWord}
243
+ onEditUpdate={(event, text) => {
244
+ onEditUpdate && onEditUpdate(event);
245
+ setMessageText(text);
246
+ }}
247
+ onEditCancel={onEditCancel}
248
+ {...editFormProps}
249
+ />
250
+ </>
251
+ );
252
+ }
253
+ return (
254
+ <>
255
+ {beforeMainContent && <>{beforeMainContent}</>}
256
+ {error ? (
257
+ <ErrorMessage {...error} />
258
+ ) : (
259
+ <Markdown
260
+ components={{
261
+ p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
262
+ code: ({ children, ...props }) => (
263
+ <CodeBlockMessage {...props} {...codeBlockProps}>
264
+ {children}
265
+ </CodeBlockMessage>
266
+ ),
267
+ h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
268
+ h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
269
+ h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
270
+ h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
271
+ h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
272
+ h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
273
+ blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
274
+ ul: (props) => <UnorderedListMessage {...props} />,
275
+ ol: (props) => <OrderedListMessage {...props} />,
276
+ li: (props) => <ListItemMessage {...props} />,
277
+ table: (props) => <TableMessage {...props} {...tableProps} />,
278
+ tbody: (props) => <TbodyMessage {...props} />,
279
+ thead: (props) => <TheadMessage {...props} />,
280
+ tr: (props) => <TrMessage {...props} />,
281
+ td: (props) => {
282
+ // Conflicts with Td type
283
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
284
+ const { width, ...rest } = props;
285
+ return <TdMessage {...rest} />;
286
+ },
287
+ th: (props) => <ThMessage {...props} />,
288
+ img: (props) => <ImageMessage {...props} />,
289
+ a: (props) => (
290
+ <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
291
+ {props.children}
292
+ </LinkMessage>
293
+ )
294
+ }}
295
+ remarkPlugins={[remarkGfm]}
296
+ rehypePlugins={rehypePlugins}
297
+ >
298
+ {messageText}
299
+ </Markdown>
300
+ )}
301
+ </>
302
+ );
303
+ };
304
+
200
305
  return (
201
306
  <section
202
307
  aria-label={`Message from ${role} - ${dateString}`}
@@ -229,59 +334,8 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
229
334
  </div>
230
335
  <div className="pf-chatbot__message-response">
231
336
  <div className="pf-chatbot__message-and-actions">
232
- {isLoading ? (
233
- <MessageLoading loadingWord={loadingWord} />
234
- ) : (
235
- <>
236
- {beforeMainContent && <>{beforeMainContent}</>}
237
- {error ? (
238
- <ErrorMessage {...error} />
239
- ) : (
240
- <Markdown
241
- components={{
242
- p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
243
- code: ({ children, ...props }) => (
244
- <CodeBlockMessage {...props} {...codeBlockProps}>
245
- {children}
246
- </CodeBlockMessage>
247
- ),
248
- h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
249
- h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
250
- h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
251
- h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
252
- h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
253
- h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
254
- blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
255
- ul: (props) => <UnorderedListMessage {...props} />,
256
- ol: (props) => <OrderedListMessage {...props} />,
257
- li: (props) => <ListItemMessage {...props} />,
258
- table: (props) => <TableMessage {...props} {...tableProps} />,
259
- tbody: (props) => <TbodyMessage {...props} />,
260
- thead: (props) => <TheadMessage {...props} />,
261
- tr: (props) => <TrMessage {...props} />,
262
- td: (props) => {
263
- // Conflicts with Td type
264
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
265
- const { width, ...rest } = props;
266
- return <TdMessage {...rest} />;
267
- },
268
- th: (props) => <ThMessage {...props} />,
269
- img: (props) => <ImageMessage {...props} />,
270
- a: (props) => (
271
- <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
272
- {props.children}
273
- </LinkMessage>
274
- )
275
- }}
276
- remarkPlugins={[remarkGfm]}
277
- rehypePlugins={rehypePlugins}
278
- >
279
- {content}
280
- </Markdown>
281
- )}
282
- {afterMainContent && <>{afterMainContent}</>}
283
- </>
284
- )}
337
+ {renderMessage()}
338
+ {afterMainContent && <>{afterMainContent}</>}
285
339
  {!isLoading && sources && <SourcesCard {...sources} />}
286
340
  {quickStarts && quickStarts.quickStart && (
287
341
  <QuickStartTile
@@ -0,0 +1,59 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Message Input
3
+ // ============================================================================
4
+
5
+ import React from 'react';
6
+ import { ActionGroup, Button, Form, FormProps, TextArea } from '@patternfly/react-core';
7
+
8
+ export interface MessageInputProps extends FormProps {
9
+ /** Placeholder for edit input */
10
+ editPlaceholder?: string;
11
+ /** Label for the English word "Update" used in edit mode. */
12
+ updateWord?: string;
13
+ /** Label for the English word "Cancel" used in edit mode. */
14
+ cancelWord?: string;
15
+ /** Callback function for when edit mode update button is clicked */
16
+ onEditUpdate?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, value: string) => void;
17
+ /** Callback functionf or when edit cancel update button is clicked */
18
+ onEditCancel?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
19
+ /** Message text */
20
+ content?: string;
21
+ }
22
+
23
+ const MessageInput: React.FunctionComponent<MessageInputProps> = ({
24
+ editPlaceholder = 'Edit prompt message...',
25
+ updateWord = 'Update',
26
+ cancelWord = 'Cancel',
27
+ onEditUpdate,
28
+ onEditCancel,
29
+ content,
30
+ ...props
31
+ }: MessageInputProps) => {
32
+ const [messageText, setMessageText] = React.useState(content ?? '');
33
+
34
+ const onChange = (event: React.FormEvent<HTMLTextAreaElement>, value: string) => {
35
+ setMessageText(value);
36
+ };
37
+
38
+ return (
39
+ <Form {...props}>
40
+ <TextArea
41
+ placeholder={editPlaceholder}
42
+ value={messageText}
43
+ onChange={onChange}
44
+ aria-label={editPlaceholder}
45
+ autoResize
46
+ />
47
+ <ActionGroup className="pf-chatbot__message-edit-buttons">
48
+ <Button variant="primary" onClick={(event) => onEditUpdate && onEditUpdate(event, messageText)}>
49
+ {updateWord}
50
+ </Button>
51
+ <Button variant="secondary" onClick={onEditCancel}>
52
+ {cancelWord}
53
+ </Button>
54
+ </ActionGroup>
55
+ </Form>
56
+ );
57
+ };
58
+
59
+ export default MessageInput;
@@ -16,7 +16,7 @@
16
16
  box-shadow: var(--pf-t--global--box-shadow--sm);
17
17
  }
18
18
 
19
- .pf-chatbot__sources-card-body {
19
+ .pf-chatbot__sources-card-body-text {
20
20
  display: block;
21
21
  display: -webkit-box;
22
22
  height: 2.8125rem;
@@ -25,11 +25,6 @@
25
25
  -webkit-box-orient: vertical;
26
26
  overflow: hidden;
27
27
  text-overflow: ellipsis;
28
- margin-bottom: var(--pf-t--global--spacer--md);
29
- }
30
-
31
- .pf-chatbot__sources-card-no-footer {
32
- margin-bottom: var(--pf-t--global--spacer--lg);
33
28
  }
34
29
 
35
30
  .pf-chatbot__sources-card-footer-container {
@@ -38,13 +33,14 @@
38
33
  var(--pf-t--global--spacer--sm) !important;
39
34
  .pf-chatbot__sources-card-footer {
40
35
  display: flex;
41
- justify-content: space-between;
42
36
  align-items: center;
43
37
 
44
38
  &-buttons {
45
39
  display: flex;
46
40
  gap: var(--pf-t--global--spacer--xs);
47
41
  align-items: center;
42
+ justify-content: space-between;
43
+ flex: 1;
48
44
 
49
45
  .pf-v6-c-button {
50
46
  border-radius: var(--pf-t--global--border--radius--pill);
@@ -11,7 +11,7 @@ describe('SourcesCard', () => {
11
11
  expect(screen.getByText('Source 1')).toBeTruthy();
12
12
  // no buttons or navigation when there is only 1 source
13
13
  expect(screen.queryByRole('button')).toBeFalsy();
14
- expect(screen.queryByText('1 of 1')).toBeFalsy();
14
+ expect(screen.queryByText('1/1')).toBeFalsy();
15
15
  });
16
16
 
17
17
  it('should render card correctly if one source with a title is passed in', () => {
@@ -48,7 +48,7 @@ describe('SourcesCard', () => {
48
48
  );
49
49
  expect(screen.getByText('2 sources')).toBeTruthy();
50
50
  expect(screen.getByText('How to make an apple pie')).toBeTruthy();
51
- expect(screen.getByText('1 of 2')).toBeTruthy();
51
+ expect(screen.getByText('1/2')).toBeTruthy();
52
52
  screen.getByRole('button', { name: /Go to previous page/i });
53
53
  screen.getByRole('button', { name: /Go to next page/i });
54
54
  });
@@ -63,12 +63,12 @@ describe('SourcesCard', () => {
63
63
  />
64
64
  );
65
65
  expect(screen.getByText('How to make an apple pie')).toBeTruthy();
66
- expect(screen.getByText('1 of 2')).toBeTruthy();
66
+ expect(screen.getByText('1/2')).toBeTruthy();
67
67
  expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
68
68
  await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
69
69
  expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
70
70
  expect(screen.getByText('How to make cookies')).toBeTruthy();
71
- expect(screen.getByText('2 of 2')).toBeTruthy();
71
+ expect(screen.getByText('2/2')).toBeTruthy();
72
72
  expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
73
73
  expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
74
74
  });
@@ -101,19 +101,6 @@ describe('SourcesCard', () => {
101
101
  expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
102
102
  });
103
103
 
104
- it('should change ofWord appropriately', () => {
105
- render(
106
- <SourcesCard
107
- sources={[
108
- { title: 'How to make an apple pie', link: '' },
109
- { title: 'How to make cookies', link: '' }
110
- ]}
111
- ofWord={'de'}
112
- />
113
- );
114
- expect(screen.getByText('1 de 2')).toBeTruthy();
115
- });
116
-
117
104
  it('should render navigation aria label appropriately', () => {
118
105
  render(
119
106
  <SourcesCard
@@ -230,4 +217,30 @@ describe('SourcesCard', () => {
230
217
  await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
231
218
  expect(spy).toHaveBeenCalledTimes(2);
232
219
  });
220
+
221
+ it('should handle showMore appropriately', async () => {
222
+ render(
223
+ <SourcesCard
224
+ sources={[
225
+ {
226
+ title: 'Getting started with Red Hat OpenShift',
227
+ link: '#',
228
+ body: 'Red Hat OpenShift on IBM Cloud is a managed offering to create your own cluster of compute hosts where you can deploy and manage containerized apps on IBM Cloud ...',
229
+ hasShowMore: true
230
+ },
231
+ {
232
+ title: 'Azure Red Hat OpenShift documentation',
233
+ link: '#',
234
+ body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
235
+ },
236
+ {
237
+ title: 'OKD Documentation: Home',
238
+ link: '#',
239
+ body: 'OKD is a distribution of Kubernetes optimized for continuous application development and multi-tenant deployment. OKD also serves as the upstream code base upon ...'
240
+ }
241
+ ]}
242
+ />
243
+ );
244
+ expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
245
+ });
233
246
  });