Skip to content

Commit 9cea659

Browse files
authored
Merge pull request patternfly#425 from rebeccaalpert/tables
feat(TableMessage): Add support for PatternFly tables
2 parents 1cda849 + 23962de commit 9cea659

File tree

15 files changed

+373
-9
lines changed

15 files changed

+373
-9
lines changed

package-lock.json

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"puppeteer-cluster": "^0.24.0"
9191
},
9292
"dependencies": {
93+
"@patternfly/react-table": "^6.1.0",
9394
"dompurify": "^3.2.0",
9495
"react-dropzone": "^14.2.3"
9596
},

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const BotMessageExample: React.FunctionComponent = () => {
2828
return inlineCode;
2929
case 'link':
3030
return link;
31+
case 'table':
32+
return table;
3133
default:
3234
return;
3335
}
@@ -125,6 +127,15 @@ _Italic text, formatted with single underscores_
125127

126128
const inlineCode = `Here is an inline code - \`() => void\``;
127129

130+
const table = `To customize your table, you can use [PatternFly TableProps](/components/table#table)
131+
132+
| Version | GA date | User role
133+
|-|-|-|
134+
| 2.5 | September 30, 2024 | Administrator |
135+
| 2.5 | June 27, 2023 | Editor |
136+
| 3.0 | April 1, 2025 | Administrator
137+
`;
138+
128139
return (
129140
<>
130141
<Message name="Bot" role="bot" avatar={patternflyAvatar} content={`Text-based message from a bot named "Bot"`} />
@@ -216,9 +227,24 @@ _Italic text, formatted with single underscores_
216227
label="More complex list"
217228
id="more-complex-list"
218229
/>
230+
<Radio
231+
isChecked={variant === 'table'}
232+
onChange={() => setVariant('table')}
233+
name="bot-message-type"
234+
label="Table"
235+
id="table"
236+
/>
219237
</FormGroup>
220238
</Form>
221-
<Message name="Bot" role="bot" avatar={patternflyAvatar} content={renderContent()} />
239+
<Message
240+
name="Bot"
241+
role="bot"
242+
avatar={patternflyAvatar}
243+
content={renderContent()}
244+
tableProps={
245+
variant === 'table' ? { 'aria-label': 'App information and user roles for bot messages' } : undefined
246+
}
247+
/>
222248
</>
223249
);
224250
};

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const UserMessageExample: React.FunctionComponent = () => {
2828
return moreComplexList;
2929
case 'link':
3030
return link;
31+
case 'table':
32+
return table;
3133
default:
3234
return;
3335
}
@@ -125,6 +127,15 @@ _Italic text, formatted with single underscores_
125127

126128
const inlineCode = `Here is an inline code - \`() => void\``;
127129

130+
const table = `To customize your table, you can use [PatternFly TableProps](/components/table#table)
131+
132+
| Version | GA date | User role
133+
|-|-|-|
134+
| 2.5 | September 30, 2024 | Administrator |
135+
| 2.5 | June 27, 2023 | Editor |
136+
| 3.0 | April 1, 2025 | Administrator
137+
`;
138+
128139
return (
129140
<>
130141
<Message
@@ -206,9 +217,24 @@ _Italic text, formatted with single underscores_
206217
label="More complex list"
207218
id="user-more-complex-list"
208219
/>
220+
<Radio
221+
isChecked={variant === 'table'}
222+
onChange={() => setVariant('table')}
223+
name="user-message-type"
224+
label="Table"
225+
id="user-table"
226+
/>
209227
</FormGroup>
210228
</Form>
211-
<Message name="User" role="user" content={renderContent()} avatar={userAvatar} />
229+
<Message
230+
name="User"
231+
role="user"
232+
content={renderContent()}
233+
avatar={userAvatar}
234+
tableProps={
235+
variant === 'table' ? { 'aria-label': 'App information and user roles for user messages' } : undefined
236+
}
237+
/>
212238
</>
213239
);
214240
};

packages/module/src/Message/Message.test.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,55 @@ const HEADING = `
8888
const BLOCK_QUOTES = `> Blockquotes can also be nested...
8989
>> ...by using additional greater-than signs (>) right next to each other...
9090
> > > ...or with spaces between each sign.`;
91+
const TABLE = `
92+
93+
| Column 1 | Column 2 |
94+
|-|-|
95+
| Cell 1 | Cell 2 |
96+
| Cell 3 | Cell 4 |
97+
98+
`;
99+
100+
const ONE_COLUMN_TABLE = `
101+
102+
| Column 1 |
103+
|-|
104+
| Cell 1 |
105+
| Cell 2 |
106+
107+
`;
108+
109+
const ONE_CELL_TABLE = `
110+
111+
| Column 1 |
112+
|-|
113+
| Cell 1 |
114+
115+
`;
116+
117+
const HEADERLESS_TABLE = `
118+
119+
| |
120+
|-|
121+
| Cell 1 |
122+
123+
`;
124+
125+
const CHILDLESS_TABLE = `
126+
127+
| Column 1 |
128+
|-|
129+
| |
130+
131+
`;
132+
133+
const EMPTY_TABLE = `
134+
135+
| |
136+
|-|
137+
| |
138+
139+
`;
91140

92141
const checkListItemsRendered = () => {
93142
const items = ['Item 1', 'Item 2', 'Item 3'];
@@ -528,4 +577,54 @@ describe('Message', () => {
528577
expect(screen.getByRole('heading', { name: /h5 Heading/i })).toBeTruthy();
529578
expect(screen.getByRole('heading', { name: /h6 Heading/i })).toBeTruthy();
530579
});
580+
it('should render table correctly', () => {
581+
render(<Message avatar="./img" role="user" name="User" content={TABLE} />);
582+
expect(screen.getByRole('row', { name: /Column 1 Column 2/i })).toBeTruthy();
583+
expect(screen.getByRole('row', { name: /Cell 1 Cell 2/i })).toBeTruthy();
584+
expect(screen.getByRole('row', { name: /Cell 3 Cell 4/i })).toBeTruthy();
585+
expect(screen.getByRole('columnheader', { name: /Column 1/i })).toBeTruthy();
586+
expect(screen.getByRole('columnheader', { name: /Column 2/i })).toBeTruthy();
587+
expect(screen.getByRole('cell', { name: /Cell 1/i })).toBeTruthy();
588+
expect(screen.getByRole('cell', { name: /Cell 2/i })).toBeTruthy();
589+
expect(screen.getByRole('cell', { name: /Cell 3/i })).toBeTruthy();
590+
expect(screen.getByRole('cell', { name: /Cell 4/i })).toBeTruthy();
591+
});
592+
it('should render table data labels correctly for mobile breakpoint', () => {
593+
render(<Message avatar="./img" role="user" name="User" content={TABLE} />);
594+
expect(screen.getByRole('row', { name: /Cell 1 Cell 2/i })).toHaveAttribute('extraHeaders', 'Column 1,Column 2');
595+
expect(screen.getByRole('row', { name: /Cell 3 Cell 4/i })).toHaveAttribute('extraHeaders', 'Column 1,Column 2');
596+
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
597+
expect(screen.getByRole('cell', { name: /Cell 2/i })).toHaveAttribute('data-label', 'Column 2');
598+
expect(screen.getByRole('cell', { name: /Cell 3/i })).toHaveAttribute('data-label', 'Column 1');
599+
expect(screen.getByRole('cell', { name: /Cell 4/i })).toHaveAttribute('data-label', 'Column 2');
600+
});
601+
it('should render table data labels correctly for mobile breakpoint for one column table', () => {
602+
render(<Message avatar="./img" role="user" name="User" content={ONE_COLUMN_TABLE} />);
603+
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', 'Column 1');
604+
expect(screen.getByRole('row', { name: /Cell 2/i })).toHaveAttribute('extraHeaders', 'Column 1');
605+
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
606+
expect(screen.getByRole('cell', { name: /Cell 2/i })).toHaveAttribute('data-label', 'Column 1');
607+
});
608+
it('should render table data labels correctly for mobile breakpoint for one cell table', () => {
609+
render(<Message avatar="./img" role="user" name="User" content={ONE_CELL_TABLE} />);
610+
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', 'Column 1');
611+
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
612+
});
613+
it('should render table data labels correctly for mobile breakpoint for headerless', () => {
614+
render(<Message avatar="./img" role="user" name="User" content={HEADERLESS_TABLE} />);
615+
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', '');
616+
expect(screen.getByRole('cell', { name: /Cell 1/i })).not.toHaveAttribute('data-label');
617+
});
618+
it('should render table data labels correctly for mobile breakpoint for childless', () => {
619+
render(<Message avatar="./img" role="user" name="User" content={CHILDLESS_TABLE} />);
620+
expect(screen.getByRole('cell')).not.toHaveAttribute('extraHeaders', 'Column 1');
621+
});
622+
it('should render table data labels correctly for mobile breakpoint for empty', () => {
623+
render(<Message avatar="./img" role="user" name="User" content={EMPTY_TABLE} />);
624+
expect(screen.getByRole('cell')).not.toHaveAttribute('extraHeaders', '');
625+
});
626+
it('should render custom table aria label correctly', () => {
627+
render(<Message avatar="./img" role="user" name="User" content={TABLE} tableProps={{ 'aria-label': 'Test' }} />);
628+
expect(screen.getByRole('grid', { name: /Test/i })).toBeTruthy();
629+
});
531630
});

packages/module/src/Message/Message.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ import { QuickStart, QuickstartAction } from './QuickStarts/types';
2929
import QuickResponse from './QuickResponse/QuickResponse';
3030
import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback';
3131
import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete';
32+
import TableMessage from './TableMessage/TableMessage';
33+
import TrMessage from './TableMessage/TrMessage';
34+
import TdMessage from './TableMessage/TdMessage';
35+
import TbodyMessage from './TableMessage/TbodyMessage';
36+
import TheadMessage from './TableMessage/TheadMessage';
37+
import ThMessage from './TableMessage/ThMessage';
38+
import { TableProps } from '@patternfly/react-table';
3239

3340
export interface MessageAttachment {
3441
/** Name of file attached to the message */
@@ -109,6 +116,8 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
109116
isLiveRegion?: boolean;
110117
/** Ref applied to message */
111118
innerRef?: React.Ref<HTMLDivElement>;
119+
/** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */
120+
tableProps?: Required<Pick<TableProps, 'aria-label'>> & TableProps;
112121
}
113122

114123
export const MessageBase: React.FunctionComponent<MessageProps> = ({
@@ -133,6 +142,7 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
133142
userFeedbackComplete,
134143
isLiveRegion = true,
135144
innerRef,
145+
tableProps,
136146
...props
137147
}: MessageProps) => {
138148
let avatarClassName;
@@ -187,16 +197,27 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
187197
{children}
188198
</CodeBlockMessage>
189199
),
190-
ul: UnorderedListMessage,
191-
ol: (props) => <OrderedListMessage {...props} />,
192-
li: ListItemMessage,
193200
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
194201
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
195202
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
196203
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
197204
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
198205
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
199-
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />
206+
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
207+
ul: (props) => <UnorderedListMessage {...props} />,
208+
ol: (props) => <OrderedListMessage {...props} />,
209+
li: (props) => <ListItemMessage {...props} />,
210+
table: (props) => <TableMessage {...props} {...tableProps} />,
211+
tbody: (props) => <TbodyMessage {...props} />,
212+
thead: (props) => <TheadMessage {...props} />,
213+
tr: (props) => <TrMessage {...props} />,
214+
td: (props) => {
215+
// Conflicts with Td type
216+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
217+
const { width, ...rest } = props;
218+
return <TdMessage {...rest} />;
219+
},
220+
th: (props) => <ThMessage {...props} />
200221
}}
201222
remarkPlugins={[remarkGfm]}
202223
>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.pf-chatbot__message-table {
2+
border-radius: var(--pf-t--global--border--radius--small);
3+
--pf-v6-c-table--BackgroundColor: var(--pf-t--global--background--color--tertiary--default);
4+
--pf-v6-c-table--BorderColor: var(--pf-t--global--border--color--default);
5+
padding: 0 var(--pf-t--global--spacer--lg) 0 var(--pf-t--global--spacer--lg);
6+
7+
&.pf-m-grid.pf-v6-c-table tbody:where(.pf-v6-c-table__tbody):first-of-type {
8+
border-block-start: 0;
9+
}
10+
11+
tbody {
12+
border-radius: var(--pf-t--global--border--radius--small);
13+
}
14+
15+
tr {
16+
--pf-v6-c-table__tr--responsive--PaddingInlineEnd: 0;
17+
--pf-v6-c-table__tr--responsive--PaddingInlineStart: 0;
18+
}
19+
20+
.pf-v6-c-table__tr:last-of-type {
21+
border-block-end: 0;
22+
}
23+
}

0 commit comments

Comments
 (0)