uikit-react-public 0.36.0 → 0.37.0

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 (32) hide show
  1. package/dist/components/Table/Table.context.d.ts +6 -0
  2. package/dist/components/Table/Table.d.ts +4 -3
  3. package/dist/components/Table/Table.stories.d.ts +3 -3
  4. package/dist/components/Table/Table.types.d.ts +6 -0
  5. package/dist/components/Table/index.d.ts +1 -1
  6. package/dist/components/Table/subcomponents/Cell/Cell.d.ts +8 -1
  7. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +5 -1
  8. package/dist/components/Table/subcomponents/Cell/CellContent.d.ts +3 -2
  9. package/dist/components/Table/subcomponents/HeadCell/HeadCell.d.ts +4 -2
  10. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +3 -1
  11. package/dist/components/Table/subcomponents/Row.d.ts +8 -1
  12. package/dist/index.js +6564 -6194
  13. package/lib/components/Snackbar/Snackbar.tsx +2 -3
  14. package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +2 -2
  15. package/lib/components/Table/Table.context.ts +12 -0
  16. package/lib/components/Table/Table.tsx +91 -12
  17. package/lib/components/Table/Table.types.ts +21 -0
  18. package/lib/components/Table/__tests__/Table.test.tsx +95 -1
  19. package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +54 -13
  20. package/lib/components/Table/index.ts +6 -1
  21. package/lib/components/Table/subcomponents/Body.tsx +16 -3
  22. package/lib/components/Table/subcomponents/Cell/Cell.tsx +193 -4
  23. package/lib/components/Table/subcomponents/Cell/CellContent.tsx +63 -3
  24. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +15 -10
  25. package/lib/components/Table/subcomponents/Head.tsx +14 -3
  26. package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +17 -4
  27. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +3 -3
  28. package/lib/components/Table/subcomponents/Row.tsx +195 -6
  29. package/lib/components/Table/subcomponents/SortIcon.tsx +4 -4
  30. package/lib/theme/__tests__/useTheme.test.tsx +2 -2
  31. package/lib/theme/useTheme.tsx +1 -1
  32. package/package.json +1 -1
@@ -40,7 +40,6 @@ const Snackbar = ({
40
40
  error: Icon.AlertTriangle,
41
41
  success: Icon.CheckCircle,
42
42
  }[variant];
43
- // Success token currently differs from Figma (#008E49).
44
43
  const backgroundColor = {
45
44
  default: theme.colour.fill.brandPrimary,
46
45
  error: theme.colour.fill.critical,
@@ -90,7 +89,7 @@ const Snackbar = ({
90
89
 
91
90
  const closeButtonStyle = css`
92
91
  &:hover {
93
- color: ${theme.colour.icon.brandInverse}; // todo: update to hover token when available
92
+ color: ${theme.colour.icon.brandInverseSecondaryHover};
94
93
  }
95
94
  `;
96
95
 
@@ -102,7 +101,7 @@ const Snackbar = ({
102
101
  cursor: pointer;
103
102
 
104
103
  &:hover {
105
- color: ${theme.colour.icon.brandInverse}; // todo: update to hover token when available
104
+ color: ${theme.colour.icon.brandInverseSecondaryHover};
106
105
  }
107
106
 
108
107
  &:focus-visible {
@@ -40,7 +40,7 @@ exports[`Snackbar > snapshot: custom test id 1`] = `
40
40
  class="css-1xjm0bq"
41
41
  >
42
42
  <button
43
- class="css-16c49qk"
43
+ class="css-1omltr1"
44
44
  data-testid="custom-test-id-undo-button"
45
45
  >
46
46
  <span
@@ -95,7 +95,7 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
95
95
  >
96
96
  <button
97
97
  aria-label="Dismiss notification"
98
- class="ucl-uikit-icon-button css-1kh68vi"
98
+ class="ucl-uikit-icon-button css-1s0isp9"
99
99
  data-testid="ucl-uikit-snackbar-close-button"
100
100
  >
101
101
  <svg
@@ -0,0 +1,12 @@
1
+ import { createContext, use } from 'react';
2
+ import type { TableColumnMetadata } from './Table.types';
3
+
4
+ export type TableContextValue = {
5
+ columns: TableColumnMetadata[];
6
+ };
7
+
8
+ const TableContext = createContext<TableContextValue>({ columns: [] });
9
+
10
+ export const TableContextProvider = TableContext.Provider;
11
+
12
+ export const useTableContext = () => use(TableContext);
@@ -1,10 +1,77 @@
1
+ import { Children, isValidElement } from 'react';
1
2
  import { css, cx } from '@emotion/css';
2
3
  import { Head, HeadCell, Body, Row, Cell } from './subcomponents';
3
4
  import { useTheme } from '../../theme';
4
- import type { TableProps } from './Table.types';
5
+ import { TableContextProvider } from './Table.context';
6
+ import type {
7
+ ColumnVariant,
8
+ NormalizedTableMobileRole,
9
+ TableColumnMetadata,
10
+ TableMobileRole,
11
+ TableProps,
12
+ } from './Table.types';
13
+ import type { TableHeadCellProps } from './subcomponents';
5
14
 
6
15
  const NAME = 'ucl-uikit-table';
7
16
 
17
+ const getTextFromChildren = (children: React.ReactNode): string => {
18
+ if (typeof children === 'string' || typeof children === 'number')
19
+ return String(children);
20
+
21
+ if (Array.isArray(children))
22
+ return children.map(getTextFromChildren).join('').trim();
23
+
24
+ if (isValidElement<{ children?: React.ReactNode }>(children))
25
+ return getTextFromChildren(children.props.children);
26
+
27
+ return '';
28
+ };
29
+
30
+ const normalizeMobileRole = (
31
+ mobileRole: TableMobileRole | undefined,
32
+ variant: ColumnVariant | undefined
33
+ ): NormalizedTableMobileRole => {
34
+ if (mobileRole === 'primaryStatus') return 'status';
35
+ if (mobileRole) return mobileRole;
36
+ if (variant === 'checkbox') return 'selection';
37
+ return 'field';
38
+ };
39
+
40
+ const getColumnsFromChildren = (
41
+ children: React.ReactNode
42
+ ): TableColumnMetadata[] => {
43
+ const head = Children.toArray(children).find(
44
+ (child) => isValidElement(child) && child.type === Head
45
+ );
46
+
47
+ if (!isValidElement<{ children?: React.ReactNode }>(head)) return [];
48
+
49
+ const columns = Children.toArray(head.props.children)
50
+ .filter(isValidElement<TableHeadCellProps>)
51
+ .map((cell) => ({
52
+ mobileRole: normalizeMobileRole(
53
+ cell.props.mobileRole,
54
+ cell.props.variant
55
+ ),
56
+ mobileLabel:
57
+ cell.props.mobileLabel ?? getTextFromChildren(cell.props.children),
58
+ }));
59
+
60
+ if (columns.some((column) => column.mobileRole === 'primary')) return columns;
61
+
62
+ const fallbackPrimaryIndex = columns.findIndex(
63
+ (column) => column.mobileRole === 'field'
64
+ );
65
+
66
+ if (fallbackPrimaryIndex === -1) return columns;
67
+
68
+ return columns.map((column, index) =>
69
+ index === fallbackPrimaryIndex
70
+ ? { ...column, mobileRole: 'primary' }
71
+ : column
72
+ );
73
+ };
74
+
8
75
  const Table = ({
9
76
  testId = NAME,
10
77
  className,
@@ -14,25 +81,37 @@ const Table = ({
14
81
  ...props
15
82
  }: TableProps) => {
16
83
  const [theme] = useTheme();
84
+ const columns = getColumnsFromChildren(children);
17
85
 
18
86
  const baseStyle = css`
19
- font-family: ${theme.font.family.primary};
20
- font-size: ${theme.font.size.f14};
87
+ width: 100%;
88
+ font-family: ${theme.typography.body.sm.fontFamily};
89
+ font-feature-settings: ${theme.typography.body.sm.fontSettings};
90
+ font-size: ${theme.typography.body.sm.fontSize}px;
91
+ font-weight: ${theme.typography.body.sm.fontWeight};
92
+ line-height: ${theme.typography.body.sm.lineHeight}%;
21
93
  border-collapse: collapse;
94
+
95
+ @media screen and (max-width: ${theme.breakpoints.tablet - 1}px) {
96
+ display: block;
97
+ border-collapse: separate;
98
+ }
22
99
  `;
23
100
 
24
101
  const style = cx(baseStyle, className, NAME);
25
102
 
26
103
  return (
27
- <table
28
- className={style}
29
- data-testid={testId}
30
- aria-label={ariaLabel}
31
- ref={ref}
32
- {...props}
33
- >
34
- {children}
35
- </table>
104
+ <TableContextProvider value={{ columns }}>
105
+ <table
106
+ className={style}
107
+ data-testid={testId}
108
+ aria-label={ariaLabel}
109
+ ref={ref}
110
+ {...props}
111
+ >
112
+ {children}
113
+ </table>
114
+ </TableContextProvider>
36
115
  );
37
116
  };
38
117
 
@@ -2,6 +2,27 @@ export type ColumnVariant = 'default' | 'numeric' | 'checkbox' | 'button';
2
2
 
3
3
  export type SortOrder = 'asc' | 'desc' | null;
4
4
 
5
+ export type TableMobileRole =
6
+ | 'selection'
7
+ | 'primaryMeta'
8
+ | 'primary'
9
+ | 'status'
10
+ | 'primaryStatus'
11
+ | 'field'
12
+ | 'primaryAction'
13
+ | 'contextMenu'
14
+ | 'hidden';
15
+
16
+ export type NormalizedTableMobileRole = Exclude<
17
+ TableMobileRole,
18
+ 'primaryStatus'
19
+ >;
20
+
21
+ export type TableColumnMetadata = {
22
+ mobileRole: NormalizedTableMobileRole;
23
+ mobileLabel?: string;
24
+ };
25
+
5
26
  export type SortPattern = {
6
27
  accessor: string | null;
7
28
  order: SortOrder;
@@ -1,6 +1,6 @@
1
1
  import { createRef } from 'react';
2
2
  import { describe, expect, test } from 'vitest';
3
- import { render, screen, within } from '@testing-library/react';
3
+ import { fireEvent, render, screen, within } from '@testing-library/react';
4
4
  import Table from '../Table';
5
5
  import { ThemeContextProvider } from '../../../theme/useTheme';
6
6
 
@@ -137,4 +137,98 @@ describe('Table', () => {
137
137
  screen.getByRole('table', { name: 'User list' })
138
138
  ).toBeInTheDocument();
139
139
  });
140
+
141
+ test('maps mobile column roles and labels from header cells to body cells', () => {
142
+ render(
143
+ <ThemeContextProvider>
144
+ <Table>
145
+ <Table.Head>
146
+ <Table.HeadCell
147
+ variant='checkbox'
148
+ mobileLabel='Select'
149
+ />
150
+ <Table.HeadCell mobileRole='primaryMeta'>Reference</Table.HeadCell>
151
+ <Table.HeadCell mobileRole='primary'>Course</Table.HeadCell>
152
+ <Table.HeadCell mobileRole='primaryStatus'>Status</Table.HeadCell>
153
+ <Table.HeadCell mobileRole='primaryAction'>Action</Table.HeadCell>
154
+ </Table.Head>
155
+ <Table.Body>
156
+ <Table.Row>
157
+ <Table.Cell
158
+ variant='checkbox'
159
+ checkboxProps={{ ariaLabel: 'Select Computer Science' }}
160
+ />
161
+ <Table.Cell>12345678</Table.Cell>
162
+ <Table.Cell>Computer Science BSc</Table.Cell>
163
+ <Table.Cell>Open</Table.Cell>
164
+ <Table.Cell>
165
+ <a href='/courses/computer-science'>View course</a>
166
+ </Table.Cell>
167
+ </Table.Row>
168
+ </Table.Body>
169
+ </Table>
170
+ </ThemeContextProvider>
171
+ );
172
+
173
+ const cells = screen.getAllByTestId('ucl-uikit-table__cell');
174
+
175
+ expect(cells[0]).toHaveAttribute('data-mobile-role', 'selection');
176
+ expect(cells[1]).toHaveAttribute('data-mobile-role', 'primaryMeta');
177
+ expect(cells[2]).toHaveAttribute('data-mobile-role', 'primary');
178
+ expect(cells[2]).toHaveAttribute('data-mobile-label', 'Course');
179
+ expect(cells[3]).toHaveAttribute('data-mobile-role', 'status');
180
+ expect(cells[4]).toHaveAttribute('data-mobile-role', 'primaryAction');
181
+ });
182
+
183
+ test('uses the first field column as the mobile primary when none is marked', () => {
184
+ render(
185
+ <ThemeContextProvider>
186
+ <Table>
187
+ <Table.Head>
188
+ <Table.HeadCell showSortIcon={false}>Name</Table.HeadCell>
189
+ <Table.HeadCell showSortIcon={false}>Email</Table.HeadCell>
190
+ </Table.Head>
191
+ <Table.Body>
192
+ <Table.Row>
193
+ <Table.Cell>Ada Lovelace</Table.Cell>
194
+ <Table.Cell>ada@example.com</Table.Cell>
195
+ </Table.Row>
196
+ </Table.Body>
197
+ </Table>
198
+ </ThemeContextProvider>
199
+ );
200
+
201
+ const cells = screen.getAllByTestId('ucl-uikit-table__cell');
202
+
203
+ expect(cells[0]).toHaveAttribute('data-mobile-role', 'primary');
204
+ expect(cells[1]).toHaveAttribute('data-mobile-role', 'field');
205
+ });
206
+
207
+ test('toggles row mobile expansion from the details disclosure button', () => {
208
+ render(
209
+ <ThemeContextProvider>
210
+ <Table>
211
+ <Table.Head>
212
+ <Table.HeadCell mobileRole='primary'>Name</Table.HeadCell>
213
+ <Table.HeadCell>Email</Table.HeadCell>
214
+ </Table.Head>
215
+ <Table.Body>
216
+ <Table.Row defaultMobileExpanded={false}>
217
+ <Table.Cell>Ada Lovelace</Table.Cell>
218
+ <Table.Cell>ada@example.com</Table.Cell>
219
+ </Table.Row>
220
+ </Table.Body>
221
+ </Table>
222
+ </ThemeContextProvider>
223
+ );
224
+
225
+ const row = screen.getByTestId('ucl-uikit-table__row');
226
+ const button = screen.getByRole('button', { name: 'Show details' });
227
+
228
+ expect(row).toHaveAttribute('data-mobile-expanded', 'false');
229
+
230
+ fireEvent.click(button);
231
+
232
+ expect(row).toHaveAttribute('data-mobile-expanded', 'true');
233
+ });
140
234
  });
@@ -2,18 +2,18 @@
2
2
 
3
3
  exports[`Table > Snapshot: basic table 1`] = `
4
4
  <table
5
- class="css-3bowfr ucl-uikit-table"
5
+ class="css-1mnnjq ucl-uikit-table"
6
6
  data-testid="ucl-uikit-table"
7
7
  >
8
8
  <thead
9
- class="css-3fy35g ucl-uikit-table__head"
9
+ class="css-1ux39q6 ucl-uikit-table__head"
10
10
  data-testid="ucl-uikit-table__head"
11
11
  >
12
12
  <tr
13
- class="css-7kbd2r"
13
+ class="css-1pr7kjy"
14
14
  >
15
15
  <th
16
- class="css-19iwbn3 ucl-uikit-table__head-cell"
16
+ class="css-x1n4gt ucl-uikit-table__head-cell"
17
17
  data-testid="ucl-uikit-table__head-cell"
18
18
  scope="col"
19
19
  >
@@ -64,7 +64,7 @@ exports[`Table > Snapshot: basic table 1`] = `
64
64
  </div>
65
65
  </th>
66
66
  <th
67
- class="css-19iwbn3 ucl-uikit-table__head-cell"
67
+ class="css-x1n4gt ucl-uikit-table__head-cell"
68
68
  data-testid="ucl-uikit-table__head-cell"
69
69
  scope="col"
70
70
  >
@@ -115,7 +115,7 @@ exports[`Table > Snapshot: basic table 1`] = `
115
115
  </div>
116
116
  </th>
117
117
  <th
118
- class="css-19iwbn3 ucl-uikit-table__head-cell"
118
+ class="css-x1n4gt ucl-uikit-table__head-cell"
119
119
  data-testid="ucl-uikit-table__head-cell"
120
120
  scope="col"
121
121
  >
@@ -168,46 +168,87 @@ exports[`Table > Snapshot: basic table 1`] = `
168
168
  </tr>
169
169
  </thead>
170
170
  <tbody
171
+ class="css-13y1iix ucl-uikit-table__body"
171
172
  data-testid="ucl-uikit-table__body"
172
173
  >
173
174
  <tr
174
175
  aria-selected="false"
175
- class="css-jetl3c ucl-uikit-table__row"
176
+ class="css-icxsp9 ucl-uikit-table__row"
177
+ data-mobile-expanded="true"
176
178
  data-testid="ucl-uikit-table__row"
177
179
  >
178
180
  <td
179
- class="css-u1gowl ucl-uikit-table__cell"
181
+ class="ucl-uikit-table__cell css-1bdqdyj"
182
+ data-mobile-label="Header Cell 1"
183
+ data-mobile-role="primary"
180
184
  data-testid="ucl-uikit-table__cell"
181
185
  >
182
186
  <div
183
- class="ucl-uikit-table__cell-content css-1ao6jhw"
187
+ class="ucl-uikit-table__cell-content css-1ugtgeo"
184
188
  data-testid="ucl-uikit-table__cell-content"
185
189
  >
186
190
  Test Cell 1
187
191
  </div>
188
192
  </td>
189
193
  <td
190
- class="css-u1gowl ucl-uikit-table__cell"
194
+ class="ucl-uikit-table__cell css-1yys4um"
195
+ data-mobile-label="Header Cell 2"
196
+ data-mobile-role="field"
191
197
  data-testid="ucl-uikit-table__cell"
192
198
  >
193
199
  <div
194
- class="ucl-uikit-table__cell-content css-1ao6jhw"
200
+ class="ucl-uikit-table__cell-content css-qn93h1"
195
201
  data-testid="ucl-uikit-table__cell-content"
196
202
  >
197
203
  Test Cell 2
198
204
  </div>
199
205
  </td>
200
206
  <td
201
- class="css-u1gowl ucl-uikit-table__cell"
207
+ class="ucl-uikit-table__cell css-501n1z"
208
+ data-mobile-label="Header Cell 3"
209
+ data-mobile-role="field"
202
210
  data-testid="ucl-uikit-table__cell"
203
211
  >
204
212
  <div
205
- class="ucl-uikit-table__cell-content css-1ao6jhw"
213
+ class="ucl-uikit-table__cell-content css-qn93h1"
206
214
  data-testid="ucl-uikit-table__cell-content"
207
215
  >
208
216
  Test Cell 3
209
217
  </div>
210
218
  </td>
219
+ <td
220
+ class="css-1nfo37q"
221
+ data-mobile-role="details"
222
+ >
223
+ <button
224
+ aria-expanded="true"
225
+ aria-label="Hide details"
226
+ class="css-1edts3y"
227
+ type="button"
228
+ >
229
+ <span>
230
+ Details
231
+ </span>
232
+ <svg
233
+ class="ucl-uikit-icon css-148hpxb"
234
+ data-testid="ucl-uikit-icon"
235
+ fill="none"
236
+ focusable="false"
237
+ height="20"
238
+ stroke="currentColor"
239
+ stroke-linecap="round"
240
+ stroke-linejoin="round"
241
+ stroke-width="2"
242
+ viewBox="0 0 24 24"
243
+ width="20"
244
+ xmlns="http://www.w3.org/2000/svg"
245
+ >
246
+ <path
247
+ d="m18 15-6-6-6 6"
248
+ />
249
+ </svg>
250
+ </button>
251
+ </td>
211
252
  </tr>
212
253
  </tbody>
213
254
  </table>
@@ -1,5 +1,10 @@
1
1
  export { default } from './Table';
2
- export type { TableProps, ColumnVariant } from './Table.types';
2
+ export type {
3
+ TableProps,
4
+ ColumnVariant,
5
+ TableMobileRole,
6
+ TableColumnMetadata,
7
+ } from './Table.types';
3
8
  export type {
4
9
  TableHeadProps,
5
10
  TableHeadCellProps,
@@ -1,12 +1,25 @@
1
- export interface TableBodyProps
2
- extends React.HTMLAttributes<HTMLTableSectionElement> {}
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ export interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement> {}
3
5
 
4
6
  const NAME = 'ucl-uikit-table__body';
5
7
 
6
8
  const Body = ({ className, children, ...props }: TableBodyProps) => {
9
+ const [theme] = useTheme();
10
+
11
+ const baseStyle = css`
12
+ @media screen and (max-width: ${theme.breakpoints.tablet - 1}px) {
13
+ display: block;
14
+ width: 100%;
15
+ }
16
+ `;
17
+
18
+ const style = cx(baseStyle, className, NAME);
19
+
7
20
  return (
8
21
  <tbody
9
- className={className}
22
+ className={style}
10
23
  data-testid={NAME}
11
24
  {...props}
12
25
  >