@instructure/ui-table 9.7.2 → 9.8.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 (96) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/es/Table/Body/index.js +13 -5
  3. package/es/Table/Body/props.js +2 -5
  4. package/es/Table/Body/styles.js +0 -2
  5. package/es/Table/Cell/index.js +3 -2
  6. package/es/Table/Cell/props.js +1 -2
  7. package/es/Table/ColHeader/props.js +2 -1
  8. package/es/Table/Head/index.js +40 -30
  9. package/es/Table/Head/props.js +1 -2
  10. package/es/Table/Head/styles.js +0 -2
  11. package/es/Table/Row/index.js +21 -18
  12. package/es/Table/Row/props.js +2 -5
  13. package/es/Table/Row/styles.js +4 -6
  14. package/es/Table/RowHeader/index.js +3 -2
  15. package/es/Table/RowHeader/props.js +1 -2
  16. package/es/Table/TableContext.js +34 -0
  17. package/es/Table/__new-tests__/Table.test.js +66 -2
  18. package/es/Table/index.js +10 -6
  19. package/es/index.js +2 -1
  20. package/lib/Table/Body/index.js +13 -5
  21. package/lib/Table/Body/props.js +2 -5
  22. package/lib/Table/Body/styles.js +0 -2
  23. package/lib/Table/Cell/index.js +3 -2
  24. package/lib/Table/Cell/props.js +1 -2
  25. package/lib/Table/ColHeader/props.js +2 -1
  26. package/lib/Table/Head/index.js +39 -30
  27. package/lib/Table/Head/props.js +1 -2
  28. package/lib/Table/Head/styles.js +0 -2
  29. package/lib/Table/Row/index.js +18 -17
  30. package/lib/Table/Row/props.js +2 -5
  31. package/lib/Table/Row/styles.js +4 -6
  32. package/lib/Table/RowHeader/index.js +3 -2
  33. package/lib/Table/RowHeader/props.js +1 -2
  34. package/lib/Table/TableContext.js +39 -0
  35. package/lib/Table/__new-tests__/Table.test.js +66 -2
  36. package/lib/Table/index.js +10 -6
  37. package/lib/index.js +8 -1
  38. package/package.json +17 -17
  39. package/src/Table/Body/index.tsx +14 -7
  40. package/src/Table/Body/props.ts +6 -18
  41. package/src/Table/Body/styles.ts +0 -2
  42. package/src/Table/Cell/index.tsx +6 -3
  43. package/src/Table/Cell/props.ts +7 -9
  44. package/src/Table/ColHeader/props.ts +9 -3
  45. package/src/Table/Head/index.tsx +40 -40
  46. package/src/Table/Head/props.ts +20 -10
  47. package/src/Table/Head/styles.ts +0 -2
  48. package/src/Table/README.md +1788 -546
  49. package/src/Table/Row/index.tsx +21 -23
  50. package/src/Table/Row/props.ts +7 -19
  51. package/src/Table/Row/styles.ts +5 -6
  52. package/src/Table/RowHeader/index.tsx +6 -4
  53. package/src/Table/RowHeader/props.ts +1 -3
  54. package/src/Table/TableContext.ts +54 -0
  55. package/src/Table/__new-tests__/Table.test.tsx +95 -2
  56. package/src/Table/index.tsx +32 -28
  57. package/src/Table/props.ts +8 -28
  58. package/src/index.ts +1 -0
  59. package/tsconfig.build.tsbuildinfo +1 -1
  60. package/types/Table/Body/index.d.ts +6 -13
  61. package/types/Table/Body/index.d.ts.map +1 -1
  62. package/types/Table/Body/props.d.ts +4 -5
  63. package/types/Table/Body/props.d.ts.map +1 -1
  64. package/types/Table/Body/styles.d.ts +0 -2
  65. package/types/Table/Body/styles.d.ts.map +1 -1
  66. package/types/Table/Cell/index.d.ts +4 -3
  67. package/types/Table/Cell/index.d.ts.map +1 -1
  68. package/types/Table/Cell/props.d.ts +6 -2
  69. package/types/Table/Cell/props.d.ts.map +1 -1
  70. package/types/Table/ColHeader/index.d.ts +2 -0
  71. package/types/Table/ColHeader/index.d.ts.map +1 -1
  72. package/types/Table/ColHeader/props.d.ts +7 -3
  73. package/types/Table/ColHeader/props.d.ts.map +1 -1
  74. package/types/Table/Head/index.d.ts +15 -5
  75. package/types/Table/Head/index.d.ts.map +1 -1
  76. package/types/Table/Head/props.d.ts +19 -4
  77. package/types/Table/Head/props.d.ts.map +1 -1
  78. package/types/Table/Head/styles.d.ts +0 -2
  79. package/types/Table/Head/styles.d.ts.map +1 -1
  80. package/types/Table/Row/index.d.ts +6 -13
  81. package/types/Table/Row/index.d.ts.map +1 -1
  82. package/types/Table/Row/props.d.ts +5 -6
  83. package/types/Table/Row/props.d.ts.map +1 -1
  84. package/types/Table/Row/styles.d.ts +5 -2
  85. package/types/Table/Row/styles.d.ts.map +1 -1
  86. package/types/Table/RowHeader/index.d.ts +4 -3
  87. package/types/Table/RowHeader/index.d.ts.map +1 -1
  88. package/types/Table/RowHeader/props.d.ts +0 -1
  89. package/types/Table/RowHeader/props.d.ts.map +1 -1
  90. package/types/Table/TableContext.d.ts +24 -0
  91. package/types/Table/TableContext.d.ts.map +1 -0
  92. package/types/Table/index.d.ts.map +1 -1
  93. package/types/Table/props.d.ts +10 -22
  94. package/types/Table/props.d.ts.map +1 -1
  95. package/types/index.d.ts +1 -0
  96. package/types/index.d.ts.map +1 -1
@@ -11,33 +11,112 @@ In stacked layout, column header is rendered in each cell, but not in row header
11
11
  > exceed the bounds of the table cell, use `fixed` or `stacked`, together with the [Text](#Text) component:
12
12
  > `<Text wrap="break-word">[long string]</Text>`.
13
13
 
14
- ```javascript
15
- ---
16
- type: example
17
- ---
18
- class Example extends React.Component {
19
- state = {
20
- layout: 'auto',
21
- hover: false,
22
- }
14
+ - ```javascript
15
+ class Example extends React.Component {
16
+ state = {
17
+ layout: 'auto',
18
+ hover: false
19
+ }
23
20
 
24
- handleChange = (field, value) => {
25
- this.setState({
26
- [field]: value,
27
- })
21
+ handleChange = (field, value) => {
22
+ this.setState({
23
+ [field]: value
24
+ })
25
+ }
26
+
27
+ renderOptions() {
28
+ const { layout, hover } = this.state
29
+
30
+ return (
31
+ <Flex alignItems="start">
32
+ <Flex.Item margin="small">
33
+ <RadioInputGroup
34
+ name="layout"
35
+ description="layout"
36
+ value={layout}
37
+ onChange={(e, value) => this.handleChange('layout', value)}
38
+ >
39
+ <RadioInput label="auto" value="auto" />
40
+ <RadioInput label="fixed" value="fixed" />
41
+ <RadioInput label="stacked" value="stacked" />
42
+ </RadioInputGroup>
43
+ </Flex.Item>
44
+ <Flex.Item margin="small">
45
+ <Checkbox
46
+ label="hover"
47
+ checked={hover}
48
+ onChange={(e, value) => this.handleChange('hover', !hover)}
49
+ />
50
+ </Flex.Item>
51
+ </Flex>
52
+ )
53
+ }
54
+
55
+ render() {
56
+ const { layout, hover } = this.state
57
+
58
+ return (
59
+ <div>
60
+ {this.renderOptions()}
61
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
62
+ <Table.Head>
63
+ <Table.Row>
64
+ <Table.ColHeader id="Rank">Rank</Table.ColHeader>
65
+ <Table.ColHeader id="Title">Title</Table.ColHeader>
66
+ <Table.ColHeader id="Year">Year</Table.ColHeader>
67
+ <Table.ColHeader id="Rating">Rating</Table.ColHeader>
68
+ </Table.Row>
69
+ </Table.Head>
70
+ <Table.Body>
71
+ <Table.Row>
72
+ <Table.RowHeader>1</Table.RowHeader>
73
+ <Table.Cell>The Shawshank Redemption</Table.Cell>
74
+ <Table.Cell>1994</Table.Cell>
75
+ <Table.Cell>9.3</Table.Cell>
76
+ </Table.Row>
77
+ <Table.Row>
78
+ <Table.RowHeader>2</Table.RowHeader>
79
+ <Table.Cell>The Godfather</Table.Cell>
80
+ <Table.Cell>1972</Table.Cell>
81
+ <Table.Cell>9.2</Table.Cell>
82
+ </Table.Row>
83
+ <Table.Row>
84
+ <Table.RowHeader>3</Table.RowHeader>
85
+ <Table.Cell>The Godfather: Part II</Table.Cell>
86
+ <Table.Cell>1974</Table.Cell>
87
+ <Table.Cell>9.0</Table.Cell>
88
+ </Table.Row>
89
+ </Table.Body>
90
+ </Table>
91
+ </div>
92
+ )
93
+ }
28
94
  }
29
95
 
30
- renderOptions () {
31
- const { layout, hover } = this.state
96
+ render(<Example />)
97
+ ```
32
98
 
33
- return (
99
+ - ```javascript
100
+ const Example = () => {
101
+ const [layout, setLayout] = useState('auto')
102
+ const [hover, setHover] = useState(false)
103
+
104
+ const handleChange = (field, value) => {
105
+ if (field === 'layout') {
106
+ setLayout(value)
107
+ } else if (field === 'hover') {
108
+ setHover(value)
109
+ }
110
+ }
111
+
112
+ const renderOptions = () => (
34
113
  <Flex alignItems="start">
35
114
  <Flex.Item margin="small">
36
115
  <RadioInputGroup
37
116
  name="layout"
38
117
  description="layout"
39
118
  value={layout}
40
- onChange={(e, value) => this.handleChange('layout', value)}
119
+ onChange={(e, value) => handleChange('layout', value)}
41
120
  >
42
121
  <RadioInput label="auto" value="auto" />
43
122
  <RadioInput label="fixed" value="fixed" />
@@ -48,24 +127,16 @@ class Example extends React.Component {
48
127
  <Checkbox
49
128
  label="hover"
50
129
  checked={hover}
51
- onChange={(e, value) => this.handleChange('hover', !hover)}
130
+ onChange={(e, value) => handleChange('hover', !hover)}
52
131
  />
53
132
  </Flex.Item>
54
133
  </Flex>
55
134
  )
56
- }
57
-
58
- render() {
59
- const { layout, hover } = this.state
60
135
 
61
136
  return (
62
137
  <div>
63
- {this.renderOptions()}
64
- <Table
65
- caption='Top rated movies'
66
- layout={layout}
67
- hover={hover}
68
- >
138
+ {renderOptions()}
139
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
69
140
  <Table.Head>
70
141
  <Table.Row>
71
142
  <Table.ColHeader id="Rank">Rank</Table.ColHeader>
@@ -98,41 +169,154 @@ class Example extends React.Component {
98
169
  </div>
99
170
  )
100
171
  }
101
- }
102
172
 
103
- render(<Example />)
104
- ```
173
+ render(<Example />)
174
+ ```
105
175
 
106
176
  ### Column width and alignment
107
177
 
108
178
  Each column (`ColHeader`) can have a custom width, and each cell (`ColHeader`, `RowHeader` or `Cell`)
109
179
  can be aligned differently.
110
180
 
111
- ```javascript
112
- ---
113
- type: example
114
- ---
115
- class Example extends React.Component {
116
- render() {
117
- const { headers, rows } = this.props
181
+ - ```javascript
182
+ class Example extends React.Component {
183
+ render() {
184
+ const { headers, rows } = this.props
185
+
186
+ return (
187
+ <Responsive
188
+ query={{
189
+ small: { maxWidth: '40rem' },
190
+ large: { minWidth: '41rem' }
191
+ }}
192
+ props={{
193
+ small: { layout: 'stacked' },
194
+ large: { layout: 'fixed' }
195
+ }}
196
+ >
197
+ {({ layout }) => (
198
+ <div>
199
+ <Table caption="Top rated movies" layout={layout}>
200
+ <Table.Head>
201
+ <Table.Row>
202
+ {(headers || []).map(({ id, text, width, textAlign }) => (
203
+ <Table.ColHeader
204
+ key={id}
205
+ id={id}
206
+ width={width}
207
+ textAlign={textAlign}
208
+ >
209
+ {text}
210
+ </Table.ColHeader>
211
+ ))}
212
+ </Table.Row>
213
+ </Table.Head>
214
+ <Table.Body>
215
+ {rows.map((row) => (
216
+ <Table.Row key={row.id}>
217
+ {headers.map(({ id, renderCell, textAlign }) => (
218
+ <Table.Cell
219
+ key={id}
220
+ textAlign={layout === 'stacked' ? 'start' : textAlign}
221
+ >
222
+ {renderCell ? renderCell(row[id], layout) : row[id]}
223
+ </Table.Cell>
224
+ ))}
225
+ </Table.Row>
226
+ ))}
227
+ </Table.Body>
228
+ </Table>
229
+ </div>
230
+ )}
231
+ </Responsive>
232
+ )
233
+ }
234
+ }
235
+
236
+ const renderSummary = (summary, layout) =>
237
+ layout === 'stacked' ? (
238
+ summary
239
+ ) : (
240
+ <TruncateText truncate="word" ellipsis="...">
241
+ {summary}
242
+ </TruncateText>
243
+ )
244
+
245
+ render(
246
+ <Example
247
+ headers={[
248
+ {
249
+ id: 'Title',
250
+ text: 'Title',
251
+ width: '25%',
252
+ textAlign: 'start'
253
+ },
254
+ {
255
+ id: 'Year',
256
+ text: 'Year',
257
+ width: '15%',
258
+ textAlign: 'start'
259
+ },
260
+ {
261
+ id: 'Summary',
262
+ text: 'Summary',
263
+ width: '40%',
264
+ renderCell: renderSummary,
265
+ textAlign: 'start'
266
+ },
267
+ {
268
+ id: 'BoxOffice',
269
+ text: 'Box Office',
270
+ width: '20%',
271
+ textAlign: 'end'
272
+ }
273
+ ]}
274
+ rows={[
275
+ {
276
+ id: '1',
277
+ Title: 'The Shawshank Redemption',
278
+ Year: 1994,
279
+ Summary:
280
+ 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.',
281
+ BoxOffice: '$28,341,469'
282
+ },
283
+ {
284
+ id: '2',
285
+ Title: 'The Godfather',
286
+ Year: 1972,
287
+ Summary:
288
+ 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.',
289
+ BoxOffice: '$133,698,921'
290
+ },
291
+ {
292
+ id: '3',
293
+ Title: 'The Godfather: Part II',
294
+ Year: 1974,
295
+ Summary:
296
+ 'The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.',
297
+ BoxOffice: '$47,542,841'
298
+ }
299
+ ]}
300
+ />
301
+ )
302
+ ```
118
303
 
304
+ - ```javascript
305
+ const Example = ({ headers, rows }) => {
119
306
  return (
120
307
  <Responsive
121
308
  query={{
122
309
  small: { maxWidth: '40rem' },
123
- large: { minWidth: '41rem' },
310
+ large: { minWidth: '41rem' }
124
311
  }}
125
312
  props={{
126
313
  small: { layout: 'stacked' },
127
- large: { layout: 'fixed' },
314
+ large: { layout: 'fixed' }
128
315
  }}
129
316
  >
130
317
  {({ layout }) => (
131
318
  <div>
132
- <Table
133
- caption='Top rated movies'
134
- layout={layout}
135
- >
319
+ <Table caption="Top rated movies" layout={layout}>
136
320
  <Table.Head>
137
321
  <Table.Row>
138
322
  {(headers || []).map(({ id, text, width, textAlign }) => (
@@ -144,7 +328,7 @@ class Example extends React.Component {
144
328
  >
145
329
  {text}
146
330
  </Table.ColHeader>
147
- ))}
331
+ ))}
148
332
  </Table.Row>
149
333
  </Table.Head>
150
334
  <Table.Body>
@@ -167,74 +351,74 @@ class Example extends React.Component {
167
351
  </Responsive>
168
352
  )
169
353
  }
170
- }
171
-
172
- const renderSummary = (summary, layout) => (layout === 'stacked')
173
- ? summary
174
- : (
175
- <TruncateText
176
- truncate="word"
177
- ellipsis="..."
178
- >
179
- {summary}
180
- </TruncateText>
181
- )
182
354
 
183
- render(
184
- <Example
185
- headers={[
186
- {
187
- id: 'Title',
188
- text: 'Title',
189
- width: '25%',
190
- textAlign: 'start',
191
- },
192
- {
193
- id: 'Year',
194
- text: 'Year',
195
- width: '15%',
196
- textAlign: 'start',
197
- },
198
- {
199
- id: 'Summary',
200
- text: 'Summary',
201
- width: '40%',
202
- renderCell: renderSummary,
203
- textAlign: 'start',
204
- },
205
- {
206
- id: 'BoxOffice',
207
- text: 'Box Office',
208
- width: '20%',
209
- textAlign: 'end',
210
- },
211
- ]}
212
- rows={[
213
- {
214
- id: '1',
215
- Title: 'The Shawshank Redemption',
216
- Year: 1994,
217
- Summary: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.',
218
- BoxOffice: '$28,341,469',
219
- },
220
- {
221
- id: '2',
222
- Title: 'The Godfather',
223
- Year: 1972,
224
- Summary: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.',
225
- BoxOffice: '$133,698,921',
226
- },
227
- {
228
- id: '3',
229
- Title: 'The Godfather: Part II',
230
- Year: 1974,
231
- Summary: 'The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.',
232
- BoxOffice: '$47,542,841',
233
- },
234
- ]}
235
- />
236
- )
237
- ```
355
+ const renderSummary = (summary, layout) =>
356
+ layout === 'stacked' ? (
357
+ summary
358
+ ) : (
359
+ <TruncateText truncate="word" ellipsis="...">
360
+ {summary}
361
+ </TruncateText>
362
+ )
363
+
364
+ render(
365
+ <Example
366
+ headers={[
367
+ {
368
+ id: 'Title',
369
+ text: 'Title',
370
+ width: '25%',
371
+ textAlign: 'start'
372
+ },
373
+ {
374
+ id: 'Year',
375
+ text: 'Year',
376
+ width: '15%',
377
+ textAlign: 'start'
378
+ },
379
+ {
380
+ id: 'Summary',
381
+ text: 'Summary',
382
+ width: '40%',
383
+ renderCell: renderSummary,
384
+ textAlign: 'start'
385
+ },
386
+ {
387
+ id: 'BoxOffice',
388
+ text: 'Box Office',
389
+ width: '20%',
390
+ textAlign: 'end'
391
+ }
392
+ ]}
393
+ rows={[
394
+ {
395
+ id: '1',
396
+ Title: 'The Shawshank Redemption',
397
+ Year: 1994,
398
+ Summary:
399
+ 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.',
400
+ BoxOffice: '$28,341,469'
401
+ },
402
+ {
403
+ id: '2',
404
+ Title: 'The Godfather',
405
+ Year: 1972,
406
+ Summary:
407
+ 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.',
408
+ BoxOffice: '$133,698,921'
409
+ },
410
+ {
411
+ id: '3',
412
+ Title: 'The Godfather: Part II',
413
+ Year: 1974,
414
+ Summary:
415
+ 'The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.',
416
+ BoxOffice: '$47,542,841'
417
+ }
418
+ ]}
419
+ />
420
+ )
421
+ ```
238
422
 
239
423
  ### A sortable table using our Responsive component
240
424
 
@@ -242,56 +426,293 @@ Resize the window to see how column headers transition into a `Select` for sorti
242
426
 
243
427
  By default, the options in the `Select` for sorting in stacked layout are generated from the `id` property of the `Table.ColHeader` components. If you want to display custom strings, use the `stackedSortByLabel` property.
244
428
 
245
- ```javascript
246
- ---
247
- type: example
248
- ---
249
- class SortableTable extends React.Component {
250
- constructor (props) {
251
- super(props)
252
- const { headers } = props
429
+ - ```javascript
430
+ class SortableTable extends React.Component {
431
+ constructor(props) {
432
+ super(props)
433
+ const { headers } = props
434
+
435
+ const initialColWidth = {}
436
+ headers.forEach((header) => {
437
+ initialColWidth[header.id] = 'start'
438
+ })
439
+
440
+ this.state = {
441
+ sortBy: headers && headers[0] && headers[0].id,
442
+ ascending: true,
443
+ colTextAligns: initialColWidth
444
+ }
445
+ }
446
+
447
+ handleSort = (event, { id }) => {
448
+ const { sortBy, ascending } = this.state
449
+
450
+ if (id === sortBy) {
451
+ this.setState({
452
+ ascending: !ascending
453
+ })
454
+ } else {
455
+ this.setState({
456
+ sortBy: id,
457
+ ascending: true
458
+ })
459
+ }
460
+ }
461
+
462
+ handleColTextAlignChange(id, value) {
463
+ this.setState((state) => ({
464
+ colTextAligns: {
465
+ ...state.colTextAligns,
466
+ [id]: value
467
+ }
468
+ }))
469
+ }
253
470
 
471
+ renderHeaderRow(direction) {
472
+ const { headers } = this.props
473
+ const { colTextAligns, sortBy } = this.state
474
+
475
+ return (
476
+ <Table.Row>
477
+ {(headers || []).map(({ id, text, width }) => (
478
+ <Table.ColHeader
479
+ key={id}
480
+ id={id}
481
+ width={width}
482
+ {...(direction && {
483
+ textAlign: colTextAligns[id],
484
+ stackedSortByLabel: text,
485
+ onRequestSort: this.handleSort,
486
+ sortDirection: id === sortBy ? direction : 'none'
487
+ })}
488
+ >
489
+ {text}
490
+ </Table.ColHeader>
491
+ ))}
492
+ </Table.Row>
493
+ )
494
+ }
495
+
496
+ renderOptions() {
497
+ const { headers } = this.props
498
+ const { colTextAligns } = this.state
499
+
500
+ return (
501
+ <ToggleGroup
502
+ size="small"
503
+ toggleLabel="Set text-align for columns"
504
+ summary="Set text-align for columns"
505
+ background="default"
506
+ >
507
+ <Table caption="Set text-align for columns">
508
+ <Table.Head>{this.renderHeaderRow()}</Table.Head>
509
+ <Table.Body>
510
+ <Table.Row>
511
+ {Object.entries(colTextAligns).map(([headerId, textAlign]) => {
512
+ return (
513
+ <Table.Cell key={headerId}>
514
+ <RadioInputGroup
515
+ description={
516
+ <ScreenReaderContent>
517
+ Set text-align for column: {headerId}
518
+ </ScreenReaderContent>
519
+ }
520
+ name={`columnTextAlign_${headerId}`}
521
+ value={textAlign}
522
+ margin="0 0 small"
523
+ size="small"
524
+ onChange={(e, value) =>
525
+ this.handleColTextAlignChange(headerId, value)
526
+ }
527
+ >
528
+ <RadioInput label="start" value="start" />
529
+ <RadioInput label="center" value="center" />
530
+ <RadioInput label="end" value="end" />
531
+ </RadioInputGroup>
532
+ </Table.Cell>
533
+ )
534
+ })}
535
+ </Table.Row>
536
+ </Table.Body>
537
+ </Table>
538
+ </ToggleGroup>
539
+ )
540
+ }
541
+
542
+ render() {
543
+ const { caption, headers, rows } = this.props
544
+ const { sortBy, ascending, colTextAligns } = this.state
545
+ const direction = ascending ? 'ascending' : 'descending'
546
+ const sortedRows = [...(rows || [])].sort((a, b) => {
547
+ if (a[sortBy] < b[sortBy]) {
548
+ return -1
549
+ }
550
+ if (a[sortBy] > b[sortBy]) {
551
+ return 1
552
+ }
553
+ return 0
554
+ })
555
+
556
+ if (!ascending) {
557
+ sortedRows.reverse()
558
+ }
559
+ return (
560
+ <Responsive
561
+ query={{
562
+ small: { maxWidth: '40rem' },
563
+ large: { minWidth: '41rem' }
564
+ }}
565
+ props={{
566
+ small: { layout: 'stacked' },
567
+ large: { layout: 'auto' }
568
+ }}
569
+ >
570
+ {(props) => (
571
+ <div>
572
+ {props.layout !== 'stacked' && (
573
+ <View display="block" margin="0 0 medium">
574
+ {this.renderOptions()}
575
+ </View>
576
+ )}
577
+
578
+ <Table
579
+ caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
580
+ {...props}
581
+ >
582
+ <Table.Head renderSortLabel="Sort by">
583
+ {this.renderHeaderRow(direction)}
584
+ </Table.Head>
585
+ <Table.Body>
586
+ {sortedRows.map((row) => (
587
+ <Table.Row key={row.id}>
588
+ {headers.map(({ id, renderCell }) => (
589
+ <Table.Cell key={id} textAlign={colTextAligns[id]}>
590
+ {renderCell ? renderCell(row[id]) : row[id]}
591
+ </Table.Cell>
592
+ ))}
593
+ </Table.Row>
594
+ ))}
595
+ </Table.Body>
596
+ </Table>
597
+ <Alert
598
+ liveRegion={() => document.getElementById('flash-messages')}
599
+ liveRegionPoliteness="polite"
600
+ screenReaderOnly
601
+ >
602
+ {`Sorted by ${sortBy} in ${direction} order`}
603
+ </Alert>
604
+ </div>
605
+ )}
606
+ </Responsive>
607
+ )
608
+ }
609
+ }
610
+
611
+ render(
612
+ <SortableTable
613
+ caption="Top rated movies"
614
+ headers={[
615
+ {
616
+ id: 'rank',
617
+ text: 'Rank',
618
+ width: '15%'
619
+ },
620
+ {
621
+ id: 'title',
622
+ text: 'Title',
623
+ width: '55%'
624
+ },
625
+ {
626
+ id: 'year',
627
+ text: 'Year',
628
+ width: '15%'
629
+ },
630
+ {
631
+ id: 'rating',
632
+ text: 'Rating',
633
+ width: '15%',
634
+ renderCell: (rating) => rating.toFixed(1)
635
+ }
636
+ ]}
637
+ rows={[
638
+ {
639
+ id: '1',
640
+ rank: 1,
641
+ title: 'The Shawshank Redemption',
642
+ year: 1994,
643
+ rating: 9.3
644
+ },
645
+ {
646
+ id: '2',
647
+ rank: 2,
648
+ title: 'The Godfather',
649
+ year: 1972,
650
+ rating: 9.2
651
+ },
652
+ {
653
+ id: '3',
654
+ rank: 3,
655
+ title: 'The Godfather: Part II',
656
+ year: 1974,
657
+ rating: 9.0
658
+ },
659
+ {
660
+ id: '4',
661
+ rank: 4,
662
+ title: 'The Dark Knight',
663
+ year: 2008,
664
+ rating: 9.0
665
+ },
666
+ {
667
+ id: '5',
668
+ rank: 5,
669
+ title: '12 Angry Men',
670
+ year: 1957,
671
+ rating: 8.9
672
+ }
673
+ ]}
674
+ />
675
+ )
676
+ ```
677
+
678
+ - ```javascript
679
+ const SortableTable = ({ caption, headers, rows }) => {
254
680
  const initialColWidth = {}
255
681
  headers.forEach((header) => {
256
682
  initialColWidth[header.id] = 'start'
257
683
  })
258
684
 
259
- this.state = {
260
- sortBy: headers && headers[0] && headers[0].id,
261
- ascending: true,
262
- colTextAligns: initialColWidth
263
- }
264
- }
685
+ const [sortBy, setSortBy] = useState(headers && headers[0] && headers[0].id)
686
+ const [ascending, setAscending] = useState(true)
687
+ const [colTextAligns, setColTextAligns] = useState(initialColWidth)
265
688
 
266
- handleSort = (event, { id }) => {
267
- const { sortBy, ascending } = this.state
689
+ const sortedRows = useMemo(() => {
690
+ if (!sortBy) return rows
268
691
 
269
- if (id === sortBy) {
270
- this.setState({
271
- ascending: !ascending,
272
- })
273
- } else {
274
- this.setState({
275
- sortBy: id,
276
- ascending: true,
692
+ const sorted = [...rows].sort((a, b) => {
693
+ return a[sortBy] > b[sortBy] ? 1 : a[sortBy] < b[sortBy] ? -1 : 0
277
694
  })
278
- }
279
- }
280
695
 
281
- handleColTextAlignChange(id, value) {
282
- this.setState(state => ({
283
- colTextAligns: {
284
- ...state.colTextAligns,
285
- [id]: value
696
+ return ascending ? sorted : sorted.reverse()
697
+ }, [sortBy, ascending, rows])
698
+
699
+ const handleSort = (event, { id }) => {
700
+ if (id === sortBy) {
701
+ setAscending(!ascending)
702
+ } else {
703
+ setSortBy(id)
704
+ setAscending(true)
286
705
  }
287
- }))
288
- }
706
+ }
289
707
 
290
- renderHeaderRow(direction) {
291
- const { headers } = this.props
292
- const { colTextAligns , sortBy } = this.state
708
+ const handleColTextAlignChange = (id, value) => {
709
+ setColTextAligns((prevState) => ({
710
+ ...prevState,
711
+ [id]: value
712
+ }))
713
+ }
293
714
 
294
- return (
715
+ const renderHeaderRow = (direction) => (
295
716
  <Table.Row>
296
717
  {(headers || []).map(({ id, text, width }) => (
297
718
  <Table.ColHeader
@@ -301,7 +722,7 @@ class SortableTable extends React.Component {
301
722
  {...(direction && {
302
723
  textAlign: colTextAligns[id],
303
724
  stackedSortByLabel: text,
304
- onRequestSort: this.handleSort,
725
+ onRequestSort: handleSort,
305
726
  sortDirection: id === sortBy ? direction : 'none'
306
727
  })}
307
728
  >
@@ -310,126 +731,64 @@ class SortableTable extends React.Component {
310
731
  ))}
311
732
  </Table.Row>
312
733
  )
313
- }
314
-
315
- renderOptions () {
316
- const { headers } = this.props
317
- const { colTextAligns } = this.state
318
734
 
319
- return (
735
+ const renderOptions = () => (
320
736
  <ToggleGroup
321
737
  size="small"
322
738
  toggleLabel="Set text-align for columns"
323
739
  summary="Set text-align for columns"
324
740
  background="default"
325
741
  >
326
- <Table caption='Set text-align for columns'>
327
- <Table.Head>
328
- {this.renderHeaderRow()}
329
- </Table.Head>
742
+ <Table caption="Set text-align for columns">
743
+ <Table.Head>{renderHeaderRow()}</Table.Head>
330
744
  <Table.Body>
331
745
  <Table.Row>
332
- {Object.entries(colTextAligns).map(([headerId, textAlign]) => {
333
- return (
334
- <Table.Cell
335
- key={headerId}
336
- width={headers.find(header => header.id === headerId).width}
746
+ {Object.entries(colTextAligns).map(([headerId, textAlign]) => (
747
+ <Table.Cell key={headerId}>
748
+ <RadioInputGroup
749
+ description={
750
+ <ScreenReaderContent>
751
+ Set text-align for column: {headerId}
752
+ </ScreenReaderContent>
753
+ }
754
+ name={`columnTextAlign_${headerId}`}
755
+ value={textAlign}
756
+ margin="0 0 small"
757
+ size="small"
758
+ onChange={(e, value) =>
759
+ handleColTextAlignChange(headerId, value)
760
+ }
337
761
  >
338
- <RadioInputGroup
339
- description={
340
- <ScreenReaderContent>
341
- Set text-align for column: {headerId}
342
- </ScreenReaderContent>
343
- }
344
- name={`columnTextAlign_${headerId}`}
345
- value={textAlign}
346
- margin="0 0 small"
347
- size="small"
348
- onChange={
349
- (e, value) => this.handleColTextAlignChange(headerId, value)
350
- }
351
- >
352
- <RadioInput label="start" value="start" />
353
- <RadioInput label="center" value="center" />
354
- <RadioInput label="end" value="end" />
355
- </RadioInputGroup>
356
- </Table.Cell>
357
- )
358
- })}
762
+ <RadioInput label="start" value="start" />
763
+ <RadioInput label="center" value="center" />
764
+ <RadioInput label="end" value="end" />
765
+ </RadioInputGroup>
766
+ </Table.Cell>
767
+ ))}
359
768
  </Table.Row>
360
769
  </Table.Body>
361
770
  </Table>
362
771
  </ToggleGroup>
363
772
  )
364
773
 
365
- return (
366
- <FormField id="columnTextAlign" label="Set column text-align">
367
- <Flex margin="0 0 medium">
368
- {Object.entries(colTextAligns).map(([headerId, textAlign]) => {
369
- return (
370
- <Flex.Item
371
- key={headerId}
372
- width={headers.find(header => header.id === headerId).width}
373
- >
374
- <RadioInputGroup
375
- description={
376
- <ScreenReaderContent>
377
- Column {headerId}textAlign
378
- </ScreenReaderContent>
379
- }
380
- name={`Column "${headerId}" textAlign`}
381
- value={textAlign}
382
- margin="0 0 small"
383
- size="small"
384
- onChange={
385
- (e, value) => this.handleColTextAlignChange(headerId, value)
386
- }
387
- >
388
- <RadioInput label="start" value="start" />
389
- <RadioInput label="center" value="center" />
390
- <RadioInput label="end" value="end" />
391
- </RadioInputGroup>
392
- </Flex.Item>
393
- )
394
- })}
395
- </Flex>
396
- </FormField>
397
- )
398
- }
399
-
400
- render() {
401
- const { caption, headers, rows } = this.props
402
- const { sortBy, ascending, colTextAligns } = this.state
403
774
  const direction = ascending ? 'ascending' : 'descending'
404
- const sortedRows = [...(rows || [])].sort((a, b) => {
405
- if (a[sortBy] < b[sortBy]) {
406
- return -1
407
- }
408
- if (a[sortBy] > b[sortBy]) {
409
- return 1
410
- }
411
- return 0
412
- })
413
775
 
414
- if (!ascending) {
415
- sortedRows.reverse()
416
- }
417
776
  return (
418
777
  <Responsive
419
778
  query={{
420
779
  small: { maxWidth: '40rem' },
421
- large: { minWidth: '41rem' },
780
+ large: { minWidth: '41rem' }
422
781
  }}
423
782
  props={{
424
783
  small: { layout: 'stacked' },
425
- large: { layout: 'auto' },
784
+ large: { layout: 'auto' }
426
785
  }}
427
786
  >
428
787
  {(props) => (
429
788
  <div>
430
789
  {props.layout !== 'stacked' && (
431
790
  <View display="block" margin="0 0 medium">
432
- {this.renderOptions()}
791
+ {renderOptions()}
433
792
  </View>
434
793
  )}
435
794
 
@@ -438,7 +797,7 @@ class SortableTable extends React.Component {
438
797
  {...props}
439
798
  >
440
799
  <Table.Head renderSortLabel="Sort by">
441
- {this.renderHeaderRow(direction)}
800
+ {renderHeaderRow(direction)}
442
801
  </Table.Head>
443
802
  <Table.Body>
444
803
  {sortedRows.map((row) => (
@@ -464,118 +823,446 @@ class SortableTable extends React.Component {
464
823
  </Responsive>
465
824
  )
466
825
  }
467
- }
468
-
469
- render(
470
- <SortableTable
471
- caption="Top rated movies"
472
- headers={[
473
- {
474
- id: 'rank',
475
- text: 'Rank',
476
- width: '15%',
477
- },
478
- {
479
- id: 'title',
480
- text: 'Title',
481
- width: '55%',
482
- },
483
- {
484
- id: 'year',
485
- text: 'Year',
486
- width: '15%',
487
- },
488
- {
489
- id: 'rating',
490
- text: 'Rating',
491
- width: '15%',
492
- renderCell: (rating) => rating.toFixed(1),
493
- },
494
- ]}
495
- rows={[
496
- {
497
- id: '1',
498
- rank: 1,
499
- title: 'The Shawshank Redemption',
500
- year: 1994,
501
- rating: 9.3,
502
- },
503
- {
504
- id: '2',
505
- rank: 2,
506
- title: 'The Godfather',
507
- year: 1972,
508
- rating: 9.2,
509
- },
510
- {
511
- id: '3',
512
- rank: 3,
513
- title: 'The Godfather: Part II',
514
- year: 1974,
515
- rating: 9.0,
516
- },
517
- {
518
- id: '4',
519
- rank: 4,
520
- title: 'The Dark Knight',
521
- year: 2008,
522
- rating: 9.0,
523
- },
524
- {
525
- id: '5',
526
- rank: 5,
527
- title: '12 Angry Men',
528
- year: 1957,
529
- rating: 8.9,
530
- },
531
- ]}
532
- />
533
- )
534
- ```
826
+
827
+ render(
828
+ <SortableTable
829
+ caption="Top rated movies"
830
+ headers={[
831
+ {
832
+ id: 'rank',
833
+ text: 'Rank',
834
+ width: '15%'
835
+ },
836
+ {
837
+ id: 'title',
838
+ text: 'Title',
839
+ width: '55%'
840
+ },
841
+ {
842
+ id: 'year',
843
+ text: 'Year',
844
+ width: '15%'
845
+ },
846
+ {
847
+ id: 'rating',
848
+ text: 'Rating',
849
+ width: '15%',
850
+ renderCell: (rating) => rating.toFixed(1)
851
+ }
852
+ ]}
853
+ rows={[
854
+ {
855
+ id: '1',
856
+ rank: 1,
857
+ title: 'The Shawshank Redemption',
858
+ year: 1994,
859
+ rating: 9.3
860
+ },
861
+ {
862
+ id: '2',
863
+ rank: 2,
864
+ title: 'The Godfather',
865
+ year: 1972,
866
+ rating: 9.2
867
+ },
868
+ {
869
+ id: '3',
870
+ rank: 3,
871
+ title: 'The Godfather: Part II',
872
+ year: 1974,
873
+ rating: 9.0
874
+ },
875
+ {
876
+ id: '4',
877
+ rank: 4,
878
+ title: 'The Dark Knight',
879
+ year: 2008,
880
+ rating: 9.0
881
+ },
882
+ {
883
+ id: '5',
884
+ rank: 5,
885
+ title: '12 Angry Men',
886
+ year: 1957,
887
+ rating: 8.9
888
+ }
889
+ ]}
890
+ />
891
+ )
892
+ ```
535
893
 
536
894
  ### A sortable table with selection and pagination
537
895
 
538
896
  The composition order is important. `SelectableTable` -> `PaginatedTable` -> `SortableTable`, so
539
897
  that selection does not re-paginate or re-sort the table, and pagination does not re-sort the table.
540
898
 
541
- ```javascript
542
- ---
543
- type: example
544
- ---
545
- class SelectableTable extends React.Component {
546
- constructor(props) {
547
- super(props)
548
- this.state = {
549
- selected: new Set()
899
+ - ```javascript
900
+ class SelectableTable extends React.Component {
901
+ constructor(props) {
902
+ super(props)
903
+ this.state = {
904
+ selected: new Set()
905
+ }
906
+ }
907
+
908
+ handleSelectAll = (allSelected) => {
909
+ const { rowIds } = this.props
910
+
911
+ this.setState({
912
+ selected: allSelected ? new Set() : new Set(rowIds)
913
+ })
914
+ }
915
+
916
+ handleSelectRow = (rowSelected, rowId) => {
917
+ const { selected } = this.state
918
+ const copy = new Set(selected)
919
+ if (rowSelected) {
920
+ copy.delete(rowId)
921
+ } else {
922
+ copy.add(rowId)
923
+ }
924
+
925
+ this.setState({
926
+ selected: copy
927
+ })
928
+ }
929
+
930
+ render() {
931
+ const { caption, headers, rows, onSort, sortBy, ascending, rowIds } =
932
+ this.props
933
+ const { selected } = this.state
934
+ const allSelected =
935
+ selected.size > 0 && rowIds.every((id) => selected.has(id))
936
+ const someSelected = selected.size > 0 && !allSelected
937
+ const direction = ascending ? 'ascending' : 'descending'
938
+
939
+ return (
940
+ <Responsive
941
+ query={{
942
+ small: { maxWidth: '40rem' },
943
+ large: { minWidth: '41rem' }
944
+ }}
945
+ props={{
946
+ small: { layout: 'stacked' },
947
+ large: { layout: 'auto' }
948
+ }}
949
+ >
950
+ {(props) => (
951
+ <div>
952
+ <View as="div" padding="small" background="primary-inverse">
953
+ {`${selected.size} of ${rowIds.length} selected`}
954
+ </View>
955
+ <Table
956
+ caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
957
+ {...props}
958
+ >
959
+ <Table.Head
960
+ renderSortLabel={
961
+ <ScreenReaderContent>Sort by</ScreenReaderContent>
962
+ }
963
+ >
964
+ <Table.Row>
965
+ <Table.ColHeader id="select">
966
+ <Checkbox
967
+ label={
968
+ <ScreenReaderContent>Select all</ScreenReaderContent>
969
+ }
970
+ onChange={() => this.handleSelectAll(allSelected)}
971
+ checked={allSelected}
972
+ indeterminate={someSelected}
973
+ />
974
+ </Table.ColHeader>
975
+ {(headers || []).map(({ id, text, width }) => (
976
+ <Table.ColHeader
977
+ key={id}
978
+ id={id}
979
+ width={width}
980
+ onRequestSort={onSort}
981
+ sortDirection={id === sortBy ? direction : 'none'}
982
+ >
983
+ {text}
984
+ </Table.ColHeader>
985
+ ))}
986
+ </Table.Row>
987
+ </Table.Head>
988
+ <Table.Body>
989
+ {(rows || []).map((row) => {
990
+ const rowSelected = selected.has(row.id)
991
+
992
+ return (
993
+ <Table.Row key={row.id}>
994
+ <Table.RowHeader>
995
+ <Checkbox
996
+ label={
997
+ <ScreenReaderContent>
998
+ Select row
999
+ </ScreenReaderContent>
1000
+ }
1001
+ onChange={() =>
1002
+ this.handleSelectRow(rowSelected, row.id)
1003
+ }
1004
+ checked={rowSelected}
1005
+ />
1006
+ </Table.RowHeader>
1007
+ {(headers || []).map(({ id, renderCell }) => (
1008
+ <Table.Cell key={id}>
1009
+ {renderCell ? renderCell(row[id]) : row[id]}
1010
+ </Table.Cell>
1011
+ ))}
1012
+ </Table.Row>
1013
+ )
1014
+ })}
1015
+ </Table.Body>
1016
+ </Table>
1017
+ <Alert
1018
+ liveRegion={() => document.getElementById('flash-messages')}
1019
+ liveRegionPoliteness="polite"
1020
+ screenReaderOnly
1021
+ >
1022
+ {`${selected.size} of ${rowIds.length} selected`}
1023
+ </Alert>
1024
+ </div>
1025
+ )}
1026
+ </Responsive>
1027
+ )
550
1028
  }
551
1029
  }
552
1030
 
553
- handleSelectAll = (allSelected) => {
554
- const { rowIds } = this.props
1031
+ class PaginatedTable extends React.Component {
1032
+ constructor(props) {
1033
+ super(props)
1034
+ this.state = {
1035
+ page: 0
1036
+ }
1037
+ }
555
1038
 
556
- this.setState({
557
- selected: allSelected ? new Set() : new Set(rowIds),
558
- })
1039
+ handleClick = (page) => {
1040
+ this.setState({
1041
+ page
1042
+ })
1043
+ }
1044
+
1045
+ handleSort = (event, options) => {
1046
+ const { onSort } = this.props
1047
+
1048
+ this.setState({
1049
+ page: 0
1050
+ })
1051
+ onSort(event, options)
1052
+ }
1053
+
1054
+ render() {
1055
+ const { caption, headers, rows, sortBy, ascending, perPage } = this.props
1056
+ const { page } = this.state
1057
+ const startIndex = page * perPage
1058
+ const slicedRows = rows.slice(startIndex, startIndex + perPage)
1059
+ const pageCount = perPage && Math.ceil(rows.length / perPage)
1060
+
1061
+ return (
1062
+ <div>
1063
+ <SelectableTable
1064
+ caption={caption}
1065
+ headers={headers}
1066
+ rows={slicedRows}
1067
+ onSort={this.handleSort}
1068
+ sortBy={sortBy}
1069
+ ascending={ascending}
1070
+ rowIds={rows.map((row) => row.id)}
1071
+ />
1072
+ {pageCount > 1 && (
1073
+ <Pagination
1074
+ variant="compact"
1075
+ labelNext="Next Page"
1076
+ labelPrev="Previous Page"
1077
+ margin="large"
1078
+ >
1079
+ {Array.from(Array(pageCount), (item, index) => (
1080
+ <Pagination.Page
1081
+ key={index}
1082
+ onClick={() => this.handleClick(index)}
1083
+ current={index === page}
1084
+ >
1085
+ {index + 1}
1086
+ </Pagination.Page>
1087
+ ))}
1088
+ </Pagination>
1089
+ )}
1090
+ <Alert
1091
+ liveRegion={() => document.getElementById('flash-messages')}
1092
+ liveRegionPoliteness="polite"
1093
+ screenReaderOnly
1094
+ >
1095
+ {`Table page ${page + 1} of ${pageCount}`}
1096
+ </Alert>
1097
+ </div>
1098
+ )
1099
+ }
559
1100
  }
560
1101
 
561
- handleSelectRow = (rowSelected, rowId) => {
562
- const { selected } = this.state
563
- const copy = new Set(selected)
564
- if (rowSelected) {
565
- copy.delete(rowId)
566
- } else {
567
- copy.add(rowId)
1102
+ class SortableTable extends React.Component {
1103
+ constructor(props) {
1104
+ super(props)
1105
+ const { headers } = props
1106
+
1107
+ this.state = {
1108
+ sortBy: headers && headers[0] && headers[0].id,
1109
+ ascending: true
1110
+ }
568
1111
  }
569
1112
 
570
- this.setState({
571
- selected: copy,
572
- })
1113
+ handleSort = (event, { id }) => {
1114
+ const { sortBy, ascending } = this.state
1115
+
1116
+ if (id === sortBy) {
1117
+ this.setState({
1118
+ ascending: !ascending
1119
+ })
1120
+ } else {
1121
+ this.setState({
1122
+ sortBy: id,
1123
+ ascending: true
1124
+ })
1125
+ }
1126
+ }
1127
+
1128
+ render() {
1129
+ const { caption, headers, rows, perPage } = this.props
1130
+ const { sortBy, ascending } = this.state
1131
+ const sortedRows = [...rows].sort((a, b) => {
1132
+ if (a[sortBy] < b[sortBy]) {
1133
+ return -1
1134
+ }
1135
+ if (a[sortBy] > b[sortBy]) {
1136
+ return 1
1137
+ }
1138
+ return 0
1139
+ })
1140
+
1141
+ if (!ascending) {
1142
+ sortedRows.reverse()
1143
+ }
1144
+ return (
1145
+ <div>
1146
+ <PaginatedTable
1147
+ caption={caption}
1148
+ headers={headers}
1149
+ rows={sortedRows}
1150
+ onSort={this.handleSort}
1151
+ sortBy={sortBy}
1152
+ ascending={ascending}
1153
+ perPage={perPage}
1154
+ />
1155
+ <Alert
1156
+ liveRegion={() => document.getElementById('flash-messages')}
1157
+ liveRegionPoliteness="polite"
1158
+ screenReaderOnly
1159
+ >
1160
+ {`Sorted by ${sortBy} in ${
1161
+ ascending ? 'ascending' : 'descending'
1162
+ } order`}
1163
+ </Alert>
1164
+ </div>
1165
+ )
1166
+ }
573
1167
  }
574
1168
 
575
- render() {
576
- const { caption, headers, rows, onSort, sortBy, ascending, rowIds } = this.props
577
- const { selected } = this.state
578
- const allSelected = selected.size > 0 && rowIds.every((id) => selected.has(id))
1169
+ const renderRating = (rating) => (
1170
+ <Rating label="Rating" valueNow={rating} valueMax={10} iconCount={5} />
1171
+ )
1172
+
1173
+ render(
1174
+ <SortableTable
1175
+ caption="Top rated movies"
1176
+ headers={[
1177
+ {
1178
+ id: 'Rank',
1179
+ text: 'Rank'
1180
+ },
1181
+ {
1182
+ id: 'Title',
1183
+ text: 'Title',
1184
+ width: '40%'
1185
+ },
1186
+ {
1187
+ id: 'Year',
1188
+ text: 'Year'
1189
+ },
1190
+ {
1191
+ id: 'Rating',
1192
+ text: 'Rating',
1193
+ renderCell: renderRating
1194
+ }
1195
+ ]}
1196
+ rows={[
1197
+ {
1198
+ id: '1',
1199
+ Rank: 1,
1200
+ Title: 'The Shawshank Redemption',
1201
+ Year: 1994,
1202
+ Rating: 9.3
1203
+ },
1204
+ {
1205
+ id: '2',
1206
+ Rank: 2,
1207
+ Title: 'The Godfather',
1208
+ Year: 1972,
1209
+ Rating: 9.2
1210
+ },
1211
+ {
1212
+ id: '3',
1213
+ Rank: 3,
1214
+ Title: 'The Godfather: Part II',
1215
+ Year: 1974,
1216
+ Rating: 9.0
1217
+ },
1218
+ {
1219
+ id: '4',
1220
+ Rank: 4,
1221
+ Title: 'The Dark Knight',
1222
+ Year: 2008,
1223
+ Rating: 9.0
1224
+ },
1225
+ {
1226
+ id: '5',
1227
+ Rank: 5,
1228
+ Title: '12 Angry Men',
1229
+ Year: 1957,
1230
+ Rating: 8.9
1231
+ }
1232
+ ]}
1233
+ perPage={3}
1234
+ />
1235
+ )
1236
+ ```
1237
+
1238
+ - ```javascript
1239
+ const SelectableTable = ({
1240
+ caption,
1241
+ headers,
1242
+ rows,
1243
+ onSort,
1244
+ sortBy,
1245
+ ascending,
1246
+ rowIds
1247
+ }) => {
1248
+ const [selected, setSelected] = useState(new Set())
1249
+
1250
+ const handleSelectAll = (allSelected) => {
1251
+ setSelected(allSelected ? new Set() : new Set(rowIds))
1252
+ }
1253
+
1254
+ const handleSelectRow = (rowSelected, rowId) => {
1255
+ const copy = new Set(selected)
1256
+ if (rowSelected) {
1257
+ copy.delete(rowId)
1258
+ } else {
1259
+ copy.add(rowId)
1260
+ }
1261
+ setSelected(copy)
1262
+ }
1263
+
1264
+ const allSelected =
1265
+ selected.size > 0 && rowIds.every((id) => selected.has(id))
579
1266
  const someSelected = selected.size > 0 && !allSelected
580
1267
  const direction = ascending ? 'ascending' : 'descending'
581
1268
 
@@ -583,47 +1270,49 @@ class SelectableTable extends React.Component {
583
1270
  <Responsive
584
1271
  query={{
585
1272
  small: { maxWidth: '40rem' },
586
- large: { minWidth: '41rem' },
1273
+ large: { minWidth: '41rem' }
587
1274
  }}
588
1275
  props={{
589
1276
  small: { layout: 'stacked' },
590
- large: { layout: 'auto' },
1277
+ large: { layout: 'auto' }
591
1278
  }}
592
1279
  >
593
1280
  {(props) => (
594
1281
  <div>
595
- <View
596
- as="div"
597
- padding="small"
598
- background="primary-inverse"
599
- >
1282
+ <View as="div" padding="small" background="primary-inverse">
600
1283
  {`${selected.size} of ${rowIds.length} selected`}
601
1284
  </View>
602
1285
  <Table
603
1286
  caption={`${caption}: sorted by ${sortBy} in ${direction} order`}
604
1287
  {...props}
605
1288
  >
606
- <Table.Head renderSortLabel={<ScreenReaderContent>Sort by</ScreenReaderContent>}>
1289
+ <Table.Head
1290
+ renderSortLabel={
1291
+ <ScreenReaderContent>Sort by</ScreenReaderContent>
1292
+ }
1293
+ >
607
1294
  <Table.Row>
608
1295
  <Table.ColHeader id="select">
609
1296
  <Checkbox
610
- label={<ScreenReaderContent>Select all</ScreenReaderContent>}
611
- onChange={() => this.handleSelectAll(allSelected)}
1297
+ label={
1298
+ <ScreenReaderContent>Select all</ScreenReaderContent>
1299
+ }
1300
+ onChange={() => handleSelectAll(allSelected)}
612
1301
  checked={allSelected}
613
1302
  indeterminate={someSelected}
614
1303
  />
615
1304
  </Table.ColHeader>
616
1305
  {(headers || []).map(({ id, text, width }) => (
617
- <Table.ColHeader
618
- key={id}
619
- id={id}
620
- width={width}
621
- onRequestSort={onSort}
622
- sortDirection={id === sortBy ? direction : 'none'}
623
- >
624
- {text}
625
- </Table.ColHeader>
626
- ))}
1306
+ <Table.ColHeader
1307
+ key={id}
1308
+ id={id}
1309
+ width={width}
1310
+ onRequestSort={onSort}
1311
+ sortDirection={id === sortBy ? direction : 'none'}
1312
+ >
1313
+ {text}
1314
+ </Table.ColHeader>
1315
+ ))}
627
1316
  </Table.Row>
628
1317
  </Table.Head>
629
1318
  <Table.Body>
@@ -634,8 +1323,12 @@ class SelectableTable extends React.Component {
634
1323
  <Table.Row key={row.id}>
635
1324
  <Table.RowHeader>
636
1325
  <Checkbox
637
- label={<ScreenReaderContent>Select row</ScreenReaderContent>}
638
- onChange={() => this.handleSelectRow(rowSelected, row.id)}
1326
+ label={
1327
+ <ScreenReaderContent>
1328
+ Select row
1329
+ </ScreenReaderContent>
1330
+ }
1331
+ onChange={() => handleSelectRow(rowSelected, row.id)}
639
1332
  checked={rowSelected}
640
1333
  />
641
1334
  </Table.RowHeader>
@@ -661,34 +1354,27 @@ class SelectableTable extends React.Component {
661
1354
  </Responsive>
662
1355
  )
663
1356
  }
664
- }
665
1357
 
666
- class PaginatedTable extends React.Component {
667
- constructor(props) {
668
- super(props)
669
- this.state = {
670
- page: 0,
1358
+ const PaginatedTable = ({
1359
+ caption,
1360
+ headers,
1361
+ rows,
1362
+ onSort,
1363
+ sortBy,
1364
+ ascending,
1365
+ perPage
1366
+ }) => {
1367
+ const [page, setPage] = useState(0)
1368
+
1369
+ const handleClick = (page) => {
1370
+ setPage(page)
671
1371
  }
672
- }
673
1372
 
674
- handleClick = (page) => {
675
- this.setState({
676
- page,
677
- })
678
- }
679
-
680
- handleSort = (event, options) => {
681
- const { onSort } = this.props
682
-
683
- this.setState({
684
- page: 0,
685
- })
686
- onSort(event, options)
687
- }
1373
+ const handleSort = (event, options) => {
1374
+ setPage(0)
1375
+ onSort(event, options)
1376
+ }
688
1377
 
689
- render() {
690
- const { caption, headers, rows, sortBy, ascending, perPage } = this.props
691
- const { page } = this.state
692
1378
  const startIndex = page * perPage
693
1379
  const slicedRows = rows.slice(startIndex, startIndex + perPage)
694
1380
  const pageCount = perPage && Math.ceil(rows.length / perPage)
@@ -699,22 +1385,22 @@ class PaginatedTable extends React.Component {
699
1385
  caption={caption}
700
1386
  headers={headers}
701
1387
  rows={slicedRows}
702
- onSort={this.handleSort}
1388
+ onSort={handleSort}
703
1389
  sortBy={sortBy}
704
1390
  ascending={ascending}
705
1391
  rowIds={rows.map((row) => row.id)}
706
1392
  />
707
1393
  {pageCount > 1 && (
708
1394
  <Pagination
709
- variant='compact'
710
- labelNext='Next Page'
711
- labelPrev='Previous Page'
712
- margin='large'
1395
+ variant="compact"
1396
+ labelNext="Next Page"
1397
+ labelPrev="Previous Page"
1398
+ margin="large"
713
1399
  >
714
1400
  {Array.from(Array(pageCount), (item, index) => (
715
1401
  <Pagination.Page
716
1402
  key={index}
717
- onClick={() => this.handleClick(index)}
1403
+ onClick={() => handleClick(index)}
718
1404
  current={index === page}
719
1405
  >
720
1406
  {index + 1}
@@ -732,57 +1418,37 @@ class PaginatedTable extends React.Component {
732
1418
  </div>
733
1419
  )
734
1420
  }
735
- }
736
-
737
- class SortableTable extends React.Component {
738
- constructor (props) {
739
- super(props)
740
- const { headers } = props
741
1421
 
742
- this.state = {
743
- sortBy: headers && headers[0] && headers[0].id,
744
- ascending: true,
745
- }
746
- }
1422
+ const SortableTable = ({ caption, headers, rows, perPage }) => {
1423
+ const [sortBy, setSortBy] = useState(headers && headers[0] && headers[0].id)
1424
+ const [ascending, setAscending] = useState(true)
747
1425
 
748
- handleSort = (event, { id }) => {
749
- const { sortBy, ascending } = this.state
1426
+ const sortedRows = useMemo(() => {
1427
+ if (!sortBy) return rows
750
1428
 
751
- if (id === sortBy) {
752
- this.setState({
753
- ascending: !ascending,
754
- })
755
- } else {
756
- this.setState({
757
- sortBy: id,
758
- ascending: true,
1429
+ const sorted = [...rows].sort((a, b) => {
1430
+ return a[sortBy] > b[sortBy] ? 1 : a[sortBy] < b[sortBy] ? -1 : 0
759
1431
  })
760
- }
761
- }
762
1432
 
763
- render() {
764
- const { caption, headers, rows, perPage } = this.props
765
- const { sortBy, ascending } = this.state
766
- const sortedRows = [...rows].sort((a, b) => {
767
- if (a[sortBy] < b[sortBy]) {
768
- return -1
769
- }
770
- if (a[sortBy] > b[sortBy]) {
771
- return 1
772
- }
773
- return 0
774
- })
1433
+ return ascending ? sorted : sorted.reverse()
1434
+ }, [sortBy, ascending, rows])
775
1435
 
776
- if (!ascending) {
777
- sortedRows.reverse()
1436
+ const handleSort = (event, { id }) => {
1437
+ if (id === sortBy) {
1438
+ setAscending(!ascending)
1439
+ } else {
1440
+ setSortBy(id)
1441
+ setAscending(true)
1442
+ }
778
1443
  }
1444
+
779
1445
  return (
780
1446
  <div>
781
1447
  <PaginatedTable
782
1448
  caption={caption}
783
1449
  headers={headers}
784
1450
  rows={sortedRows}
785
- onSort={this.handleSort}
1451
+ onSort={handleSort}
786
1452
  sortBy={sortBy}
787
1453
  ascending={ascending}
788
1454
  perPage={perPage}
@@ -792,139 +1458,223 @@ class SortableTable extends React.Component {
792
1458
  liveRegionPoliteness="polite"
793
1459
  screenReaderOnly
794
1460
  >
795
- {`Sorted by ${sortBy} in ${ascending ? 'ascending' : 'descending'} order`}
1461
+ {`Sorted by ${sortBy} in ${
1462
+ ascending ? 'ascending' : 'descending'
1463
+ } order`}
796
1464
  </Alert>
797
1465
  </div>
798
1466
  )
799
1467
  }
800
- }
801
-
802
- const renderRating = (rating) => (
803
- <Rating
804
- label='Rating'
805
- valueNow={rating}
806
- valueMax={10}
807
- iconCount={5}
808
- />
809
- )
810
-
811
- render(
812
- <SortableTable
813
- caption="Top rated movies"
814
- headers={[
815
- {
816
- id: 'Rank',
817
- text: 'Rank',
818
- },
819
- {
820
- id: 'Title',
821
- text: 'Title',
822
- width: '40%',
823
- },
824
- {
825
- id: 'Year',
826
- text: 'Year',
827
- },
828
- {
829
- id: 'Rating',
830
- text: 'Rating',
831
- renderCell: renderRating,
832
- },
833
- ]}
834
- rows={[
835
- {
836
- id: '1',
837
- Rank: 1,
838
- Title: 'The Shawshank Redemption',
839
- Year: 1994,
840
- Rating: 9.3,
841
- },
842
- {
843
- id: '2',
844
- Rank: 2,
845
- Title: 'The Godfather',
846
- Year: 1972,
847
- Rating: 9.2,
848
- },
849
- {
850
- id: '3',
851
- Rank: 3,
852
- Title: 'The Godfather: Part II',
853
- Year: 1974,
854
- Rating: 9.0,
855
- },
856
- {
857
- id: '4',
858
- Rank: 4,
859
- Title: 'The Dark Knight',
860
- Year: 2008,
861
- Rating: 9.0,
862
- },
863
- {
864
- id: '5',
865
- Rank: 5,
866
- Title: '12 Angry Men',
867
- Year: 1957,
868
- Rating: 8.9,
869
- },
870
- ]}
871
- perPage={3}
872
- />
873
- )
874
- ```
1468
+
1469
+ const renderRating = (rating) => (
1470
+ <Rating label="Rating" valueNow={rating} valueMax={10} iconCount={5} />
1471
+ )
1472
+
1473
+ render(
1474
+ <SortableTable
1475
+ caption="Top rated movies"
1476
+ headers={[
1477
+ {
1478
+ id: 'Rank',
1479
+ text: 'Rank'
1480
+ },
1481
+ {
1482
+ id: 'Title',
1483
+ text: 'Title',
1484
+ width: '40%'
1485
+ },
1486
+ {
1487
+ id: 'Year',
1488
+ text: 'Year'
1489
+ },
1490
+ {
1491
+ id: 'Rating',
1492
+ text: 'Rating',
1493
+ renderCell: renderRating
1494
+ }
1495
+ ]}
1496
+ rows={[
1497
+ {
1498
+ id: '1',
1499
+ Rank: 1,
1500
+ Title: 'The Shawshank Redemption',
1501
+ Year: 1994,
1502
+ Rating: 9.3
1503
+ },
1504
+ {
1505
+ id: '2',
1506
+ Rank: 2,
1507
+ Title: 'The Godfather',
1508
+ Year: 1972,
1509
+ Rating: 9.2
1510
+ },
1511
+ {
1512
+ id: '3',
1513
+ Rank: 3,
1514
+ Title: 'The Godfather: Part II',
1515
+ Year: 1974,
1516
+ Rating: 9.0
1517
+ },
1518
+ {
1519
+ id: '4',
1520
+ Rank: 4,
1521
+ Title: 'The Dark Knight',
1522
+ Year: 2008,
1523
+ Rating: 9.0
1524
+ },
1525
+ {
1526
+ id: '5',
1527
+ Rank: 5,
1528
+ Title: '12 Angry Men',
1529
+ Year: 1957,
1530
+ Rating: 8.9
1531
+ }
1532
+ ]}
1533
+ perPage={3}
1534
+ />
1535
+ )
1536
+ ```
875
1537
 
876
1538
  ### Using Custom Components as Children
877
1539
 
878
- In some cases you might want to use custom components in a `Table`, e.g. a HOC for `Table.Row` or `Table.Cell`. This is generally not recommended but sometimes it could be beneficial for codesplitting or writing cleaner code for larger and more complex Tables. In those cases you have to pay attention to always pass down the appropriate props manually.
1540
+ In some cases you might want to use custom components in a `Table`, e.g. a HOC for `Table.Row` or `Table.Cell`. This is generally not recommended, but sometimes it could be beneficial for codesplitting or writing cleaner code for larger and more complex Tables.
879
1541
 
880
- ```javascript
881
- ---
882
- type: example
883
- ---
884
- class CustomTableCell extends React.Component {
885
- render () {
886
- return (
887
- <Table.Cell {...this.props}>{this.props.children}</Table.Cell>
888
- )
1542
+ > Do not replace `Table.Body` and `Table.Head` with custom components
1543
+
1544
+ Wrapper HOCs are simple, just return the original component:
1545
+
1546
+ - ```javascript
1547
+ class CustomTableCell extends React.Component {
1548
+ render() {
1549
+ return <Table.Cell {...this.props}>{this.props.children}</Table.Cell>
1550
+ }
889
1551
  }
890
- }
891
1552
 
892
- class CustomTableRow extends React.Component {
893
- render () {
894
- return (
1553
+ class CustomTableRow extends React.Component {
1554
+ render() {
1555
+ return (
895
1556
  <Table.Row {...this.props}>
896
1557
  <Table.RowHeader>1</Table.RowHeader>
897
1558
  <Table.Cell>The Shawshank Redemption</Table.Cell>
898
1559
  <Table.Cell>1994</Table.Cell>
899
1560
  <CustomTableCell>9.3</CustomTableCell>
900
1561
  </Table.Row>
901
- )
1562
+ )
1563
+ }
902
1564
  }
903
- }
904
1565
 
905
- class Example extends React.Component {
906
- state = {
907
- layout: 'auto',
908
- hover: false,
909
- }
1566
+ class Example extends React.Component {
1567
+ state = {
1568
+ layout: 'auto',
1569
+ hover: false
1570
+ }
910
1571
 
911
- handleChange = (field, value) => {
912
- this.setState({
913
- [field]: value,
914
- })
1572
+ handleChange = (field, value) => {
1573
+ this.setState({
1574
+ [field]: value
1575
+ })
1576
+ }
1577
+
1578
+ renderOptions() {
1579
+ const { layout, hover } = this.state
1580
+
1581
+ return (
1582
+ <Flex alignItems="start">
1583
+ <Flex.Item margin="small">
1584
+ <RadioInputGroup
1585
+ name="layout2"
1586
+ description="layout2"
1587
+ value={layout}
1588
+ onChange={(e, value) => this.handleChange('layout', value)}
1589
+ >
1590
+ <RadioInput label="auto" value="auto" />
1591
+ <RadioInput label="fixed" value="fixed" />
1592
+ <RadioInput label="stacked" value="stacked" />
1593
+ </RadioInputGroup>
1594
+ </Flex.Item>
1595
+ <Flex.Item margin="small">
1596
+ <Checkbox
1597
+ label="hover"
1598
+ checked={hover}
1599
+ onChange={(e, value) => this.handleChange('hover', !hover)}
1600
+ />
1601
+ </Flex.Item>
1602
+ </Flex>
1603
+ )
1604
+ }
1605
+
1606
+ render() {
1607
+ const { layout, hover } = this.state
1608
+ return (
1609
+ <div>
1610
+ {this.renderOptions()}
1611
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
1612
+ <Table.Head>
1613
+ <Table.Row>
1614
+ <Table.ColHeader id="Rank">Rank</Table.ColHeader>
1615
+ <Table.ColHeader id="Title">Title</Table.ColHeader>
1616
+ <Table.ColHeader id="Year">Year</Table.ColHeader>
1617
+ <Table.ColHeader id="Rating">Rating</Table.ColHeader>
1618
+ </Table.Row>
1619
+ </Table.Head>
1620
+ <Table.Body>
1621
+ <CustomTableRow />
1622
+ <Table.Row>
1623
+ <Table.RowHeader>2</Table.RowHeader>
1624
+ <Table.Cell>The Godfather</Table.Cell>
1625
+ <Table.Cell>1972</Table.Cell>
1626
+ <Table.Cell>9.2</Table.Cell>
1627
+ </Table.Row>
1628
+ <Table.Row>
1629
+ <Table.RowHeader>3</Table.RowHeader>
1630
+ <Table.Cell>The Godfather: Part II</Table.Cell>
1631
+ <Table.Cell>1974</Table.Cell>
1632
+ <Table.Cell>9.0</Table.Cell>
1633
+ </Table.Row>
1634
+ </Table.Body>
1635
+ </Table>
1636
+ </div>
1637
+ )
1638
+ }
915
1639
  }
916
1640
 
917
- renderOptions () {
918
- const { layout, hover } = this.state
1641
+ render(<Example />)
1642
+ ```
919
1643
 
920
- return (
1644
+ - ```javascript
1645
+ const CustomTableCell = ({ children, ...props }) => (
1646
+ <Table.Cell {...props}>{children}</Table.Cell>
1647
+ )
1648
+
1649
+ const CustomTableRow = ({ children, ...props }) => (
1650
+ <Table.Row {...props}>
1651
+ <Table.RowHeader>1</Table.RowHeader>
1652
+ <Table.Cell>The Shawshank Redemption</Table.Cell>
1653
+ <Table.Cell>1994</Table.Cell>
1654
+ <CustomTableCell>9.3</CustomTableCell>
1655
+ </Table.Row>
1656
+ )
1657
+
1658
+ const Example = () => {
1659
+ const [layout, setLayout] = useState('auto')
1660
+ const [hover, setHover] = useState(false)
1661
+
1662
+ const handleChange = (field, value) => {
1663
+ if (field === 'layout') {
1664
+ setLayout(value)
1665
+ } else if (field === 'hover') {
1666
+ setHover(!hover)
1667
+ }
1668
+ }
1669
+
1670
+ const renderOptions = () => (
921
1671
  <Flex alignItems="start">
922
1672
  <Flex.Item margin="small">
923
1673
  <RadioInputGroup
924
1674
  name="layout2"
925
1675
  description="layout2"
926
1676
  value={layout}
927
- onChange={(e, value) => this.handleChange('layout', value)}
1677
+ onChange={(e, value) => handleChange('layout', value)}
928
1678
  >
929
1679
  <RadioInput label="auto" value="auto" />
930
1680
  <RadioInput label="fixed" value="fixed" />
@@ -935,24 +1685,16 @@ class Example extends React.Component {
935
1685
  <Checkbox
936
1686
  label="hover"
937
1687
  checked={hover}
938
- onChange={(e, value) => this.handleChange('hover', !hover)}
1688
+ onChange={(e, value) => handleChange('hover', !hover)}
939
1689
  />
940
1690
  </Flex.Item>
941
1691
  </Flex>
942
1692
  )
943
- }
944
-
945
- render() {
946
- const { layout, hover } = this.state
947
1693
 
948
1694
  return (
949
1695
  <div>
950
- {this.renderOptions()}
951
- <Table
952
- caption='Top rated movies'
953
- layout={layout}
954
- hover={hover}
955
- >
1696
+ {renderOptions()}
1697
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
956
1698
  <Table.Head>
957
1699
  <Table.Row>
958
1700
  <Table.ColHeader id="Rank">Rank</Table.ColHeader>
@@ -962,7 +1704,7 @@ class Example extends React.Component {
962
1704
  </Table.Row>
963
1705
  </Table.Head>
964
1706
  <Table.Body>
965
- <CustomTableRow/>
1707
+ <CustomTableRow />
966
1708
  <Table.Row>
967
1709
  <Table.RowHeader>2</Table.RowHeader>
968
1710
  <Table.Cell>The Godfather</Table.Cell>
@@ -980,10 +1722,510 @@ class Example extends React.Component {
980
1722
  </div>
981
1723
  )
982
1724
  }
983
- }
984
1725
 
985
- render(<Example />)
986
- ```
1726
+ render(<Example />)
1727
+ ```
1728
+
1729
+ #### Fully custom components
1730
+
1731
+ If you want to use fully custom components you have to pay attention to the following:
1732
+
1733
+ - Render them as the appropriate HTML Table tags (`tr`, `th`, ...)
1734
+ - Read the `hover` prop from `TableContext` to customize hover behaviour
1735
+ - A11y: Row header cells must have the `scope='row'` HTML attribute
1736
+ - A11y: Column header cells must have the `scope='col'` and `aria-sort` (if sortable) HTML attribute
1737
+
1738
+ Basic fully custom table:
1739
+
1740
+ - ```javascript
1741
+ class CustomTableCell extends React.Component {
1742
+ render() {
1743
+ return <td>{this.props.children}</td>
1744
+ }
1745
+ }
1746
+
1747
+ class CustomTableRow extends React.Component {
1748
+ static contextType = TableContext
1749
+ state = { isHovered: false }
1750
+
1751
+ toggleHoverOff = () => {
1752
+ this.setState({ isHovered: false })
1753
+ }
1754
+ toggleHoverOn = () => {
1755
+ this.setState({ isHovered: true })
1756
+ }
1757
+
1758
+ render() {
1759
+ const rowStyle =
1760
+ this.context.hover && this.state.isHovered
1761
+ ? { backgroundColor: 'SeaGreen' }
1762
+ : { backgroundColor: 'white' }
1763
+ return (
1764
+ <tr
1765
+ style={rowStyle}
1766
+ onMouseOver={this.toggleHoverOn}
1767
+ onMouseOut={this.toggleHoverOff}
1768
+ >
1769
+ {this.props.children}
1770
+ </tr>
1771
+ )
1772
+ }
1773
+ }
1774
+
1775
+ class Example extends React.Component {
1776
+ state = {
1777
+ layout: 'auto',
1778
+ hover: false
1779
+ }
1780
+
1781
+ handleChange = (field, value) => {
1782
+ this.setState({
1783
+ [field]: value
1784
+ })
1785
+ }
1786
+
1787
+ renderOptions() {
1788
+ const { layout, hover } = this.state
1789
+
1790
+ return (
1791
+ <Flex alignItems="start">
1792
+ <Flex.Item margin="small">
1793
+ <RadioInputGroup
1794
+ name="Layout"
1795
+ description="Layout"
1796
+ value={layout}
1797
+ onChange={(e, value) => this.handleChange('layout', value)}
1798
+ >
1799
+ <RadioInput label="auto" value="auto" />
1800
+ <RadioInput label="fixed" value="fixed" />
1801
+ </RadioInputGroup>
1802
+ </Flex.Item>
1803
+ <Flex.Item margin="small">
1804
+ <Checkbox
1805
+ label="hover"
1806
+ checked={hover}
1807
+ onChange={(e, value) => this.handleChange('hover', !hover)}
1808
+ />
1809
+ </Flex.Item>
1810
+ </Flex>
1811
+ )
1812
+ }
1813
+
1814
+ render() {
1815
+ const { layout, hover } = this.state
1816
+
1817
+ return (
1818
+ <div>
1819
+ {this.renderOptions()}
1820
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
1821
+ <Table.Head>
1822
+ <CustomTableRow>
1823
+ <CustomTableCell scope="col">Rank</CustomTableCell>
1824
+ <CustomTableCell scope="col">Title</CustomTableCell>
1825
+ <CustomTableCell scope="col">Year</CustomTableCell>
1826
+ <CustomTableCell scope="col">Rating</CustomTableCell>
1827
+ </CustomTableRow>
1828
+ </Table.Head>
1829
+ <Table.Body>
1830
+ <CustomTableRow>
1831
+ <CustomTableCell scope="row">1</CustomTableCell>
1832
+ <CustomTableCell>The Godfather</CustomTableCell>
1833
+ <CustomTableCell>1972</CustomTableCell>
1834
+ <CustomTableCell>9.2</CustomTableCell>
1835
+ </CustomTableRow>
1836
+ <CustomTableRow>
1837
+ <CustomTableCell scope="row">2</CustomTableCell>
1838
+ <CustomTableCell>The Godfather: Part II</CustomTableCell>
1839
+ <CustomTableCell>1974</CustomTableCell>
1840
+ <CustomTableCell>9.0</CustomTableCell>
1841
+ </CustomTableRow>
1842
+ </Table.Body>
1843
+ </Table>
1844
+ </div>
1845
+ )
1846
+ }
1847
+ }
1848
+
1849
+ render(<Example />)
1850
+ ```
1851
+
1852
+ - ```javascript
1853
+ const CustomTableCell = ({ children, ...props }) => (
1854
+ <td {...props}>{children}</td>
1855
+ )
1856
+
1857
+ const CustomTableRow = ({ children, ...props }) => {
1858
+ const { hover } = useContext(TableContext)
1859
+ const [isHovered, setIsHovered] = useState(false)
1860
+
1861
+ const rowStyle =
1862
+ hover && isHovered
1863
+ ? { backgroundColor: 'SeaGreen' }
1864
+ : { backgroundColor: 'white' }
1865
+
1866
+ return (
1867
+ <tr
1868
+ style={rowStyle}
1869
+ onMouseOver={() => setIsHovered(true)}
1870
+ onMouseOut={() => setIsHovered(false)}
1871
+ >
1872
+ {children}
1873
+ </tr>
1874
+ )
1875
+ }
1876
+
1877
+ const Example = () => {
1878
+ const [layout, setLayout] = useState('auto')
1879
+ const [hover, setHover] = useState(false)
1880
+
1881
+ const handleChange = (field, value) => {
1882
+ if (field === 'layout') {
1883
+ setLayout(value)
1884
+ } else if (field === 'hover') {
1885
+ setHover(!hover)
1886
+ }
1887
+ }
1888
+
1889
+ const renderOptions = () => (
1890
+ <Flex alignItems="start">
1891
+ <Flex.Item margin="small">
1892
+ <RadioInputGroup
1893
+ name="Layout"
1894
+ description="Layout"
1895
+ value={layout}
1896
+ onChange={(e, value) => handleChange('layout', value)}
1897
+ >
1898
+ <RadioInput label="auto" value="auto" />
1899
+ <RadioInput label="fixed" value="fixed" />
1900
+ </RadioInputGroup>
1901
+ </Flex.Item>
1902
+ <Flex.Item margin="small">
1903
+ <Checkbox
1904
+ label="hover"
1905
+ checked={hover}
1906
+ onChange={(e, value) => handleChange('hover', !hover)}
1907
+ />
1908
+ </Flex.Item>
1909
+ </Flex>
1910
+ )
1911
+
1912
+ return (
1913
+ <div>
1914
+ {renderOptions()}
1915
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
1916
+ <Table.Head>
1917
+ <CustomTableRow>
1918
+ <CustomTableCell scope="col">Rank</CustomTableCell>
1919
+ <CustomTableCell scope="col">Title</CustomTableCell>
1920
+ <CustomTableCell scope="col">Year</CustomTableCell>
1921
+ <CustomTableCell scope="col">Rating</CustomTableCell>
1922
+ </CustomTableRow>
1923
+ </Table.Head>
1924
+ <Table.Body>
1925
+ <CustomTableRow>
1926
+ <CustomTableCell scope="row">1</CustomTableCell>
1927
+ <CustomTableCell>The Godfather</CustomTableCell>
1928
+ <CustomTableCell>1972</CustomTableCell>
1929
+ <CustomTableCell>9.2</CustomTableCell>
1930
+ </CustomTableRow>
1931
+ <CustomTableRow>
1932
+ <CustomTableCell scope="row">2</CustomTableCell>
1933
+ <CustomTableCell>The Godfather: Part II</CustomTableCell>
1934
+ <CustomTableCell>1974</CustomTableCell>
1935
+ <CustomTableCell>9.0</CustomTableCell>
1936
+ </CustomTableRow>
1937
+ </Table.Body>
1938
+ </Table>
1939
+ </div>
1940
+ )
1941
+ }
1942
+
1943
+ render(<Example />)
1944
+ ```
1945
+
1946
+ #### Fully custom components with `stacked` layout
1947
+
1948
+ This layout for small screens displays the table as a list. To accomplish this the headers are passed down to cells (in `TableContext`), so they can display what column they are rendering.
1949
+ In this layout for accessibility not render HTML table tags, just plain DOM elements (e.g. `div`) and use the appropriate ARIA role to signify that it's actually a `Table` (e.g. [`cell`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/cell_role), [`row`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/row_role), [`rowheader`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/rowheader_role)).
1950
+ Also you need the following props on the components:
1951
+
1952
+ ##### Table rows
1953
+
1954
+ - It should read the `headers` array from `TableContext` and pass its nth element to its nth child (if they have such prop).
1955
+
1956
+ ##### The children of the first row in `Table.Head` (`Table.ColHeader` by default)
1957
+
1958
+ - If the table is sortable the Table needs `id`, `onRequestSort`, `sortDirection` and `stackedSortByLabel` props to render a `Select` to choose how to sort the `Table` (see the props of `Table.ColHeader` for types)
1959
+
1960
+ ##### Table cells
1961
+
1962
+ - It needs to have an optional `header` prop and should display its value so the user knows which column the cell's value belongs to (you can read whether the table is using `stacked` layout from `TableContext`.
1963
+
1964
+ Custom table with `stacked` layout support:
1965
+
1966
+ - ```javascript
1967
+ class CustomTableCell extends React.Component {
1968
+ static contextType = TableContext
1969
+
1970
+ render() {
1971
+ const isStacked = this.context.isStacked
1972
+ if (isStacked) {
1973
+ let headerTxt
1974
+ if (typeof this.props.header === 'function') {
1975
+ headerTxt = React.createElement(this.props.header)
1976
+ } else {
1977
+ headerTxt = this.props.header
1978
+ }
1979
+ return (
1980
+ <div role="cell">
1981
+ {headerTxt && headerTxt}
1982
+ {headerTxt && ': '}
1983
+ {this.props.children}
1984
+ </div>
1985
+ )
1986
+ }
1987
+ return <td>{this.props.children}</td>
1988
+ }
1989
+ }
1990
+
1991
+ class CustomTableRow extends React.Component {
1992
+ static contextType = TableContext
1993
+ state = { isHovered: false }
1994
+
1995
+ toggleHoverOff = () => {
1996
+ this.setState({ isHovered: false })
1997
+ }
1998
+ toggleHoverOn = () => {
1999
+ this.setState({ isHovered: true })
2000
+ }
2001
+
2002
+ render() {
2003
+ const { hover, headers, isStacked } = this.context
2004
+ const Tag = isStacked ? 'div' : 'tr'
2005
+ const rowStyle =
2006
+ hover && this.state.isHovered
2007
+ ? { backgroundColor: 'SeaGreen' }
2008
+ : { backgroundColor: 'white' }
2009
+
2010
+ return (
2011
+ <Tag
2012
+ style={rowStyle}
2013
+ role={isStacked ? 'row' : undefined}
2014
+ onMouseOver={this.toggleHoverOn}
2015
+ onMouseOut={this.toggleHoverOff}
2016
+ >
2017
+ {React.Children.toArray(this.props.children)
2018
+ .filter(React.isValidElement)
2019
+ .map((child, index) => {
2020
+ return React.cloneElement(child, {
2021
+ key: child.props.name,
2022
+ // used by `CustomTableCell` to render its column title in `stacked` layout
2023
+ header: headers && headers[index]
2024
+ })
2025
+ })}
2026
+ </Tag>
2027
+ )
2028
+ }
2029
+ }
2030
+
2031
+ class Example extends React.Component {
2032
+ state = {
2033
+ layout: 'auto',
2034
+ hover: false
2035
+ }
2036
+
2037
+ handleChange = (field, value) => {
2038
+ this.setState({
2039
+ [field]: value
2040
+ })
2041
+ }
2042
+
2043
+ renderOptions() {
2044
+ const { layout, hover } = this.state
2045
+
2046
+ return (
2047
+ <Flex alignItems="start">
2048
+ <Flex.Item margin="small">
2049
+ <RadioInputGroup
2050
+ name="customStackedLayout"
2051
+ description="Layout"
2052
+ value={layout}
2053
+ onChange={(e, value) => this.handleChange('layout', value)}
2054
+ >
2055
+ <RadioInput label="auto" value="auto" />
2056
+ <RadioInput label="fixed" value="fixed" />
2057
+ <RadioInput label="stacked" value="stacked" />
2058
+ </RadioInputGroup>
2059
+ </Flex.Item>
2060
+ <Flex.Item margin="small">
2061
+ <Checkbox
2062
+ label="hover"
2063
+ checked={hover}
2064
+ onChange={(e, value) => this.handleChange('hover', !hover)}
2065
+ />
2066
+ </Flex.Item>
2067
+ </Flex>
2068
+ )
2069
+ }
2070
+
2071
+ render() {
2072
+ const { layout, hover } = this.state
2073
+
2074
+ return (
2075
+ <div>
2076
+ {this.renderOptions()}
2077
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
2078
+ <Table.Head>
2079
+ <CustomTableRow>
2080
+ <CustomTableCell scope="col">Rank</CustomTableCell>
2081
+ <CustomTableCell scope="col">Title</CustomTableCell>
2082
+ <CustomTableCell scope="col">Year</CustomTableCell>
2083
+ <CustomTableCell scope="col">Rating</CustomTableCell>
2084
+ </CustomTableRow>
2085
+ </Table.Head>
2086
+ <Table.Body>
2087
+ <CustomTableRow>
2088
+ <CustomTableCell scope="row">1</CustomTableCell>
2089
+ <CustomTableCell>The Godfather</CustomTableCell>
2090
+ <CustomTableCell>1972</CustomTableCell>
2091
+ <CustomTableCell>9.2</CustomTableCell>
2092
+ </CustomTableRow>
2093
+ <CustomTableRow>
2094
+ <CustomTableCell scope="row">2</CustomTableCell>
2095
+ <CustomTableCell>The Godfather: Part II</CustomTableCell>
2096
+ <CustomTableCell>1974</CustomTableCell>
2097
+ <CustomTableCell>9.0</CustomTableCell>
2098
+ </CustomTableRow>
2099
+ </Table.Body>
2100
+ </Table>
2101
+ </div>
2102
+ )
2103
+ }
2104
+ }
2105
+
2106
+ render(<Example />)
2107
+ ```
2108
+
2109
+ - ```javascript
2110
+ const CustomTableCell = ({ children, header }) => {
2111
+ const { isStacked } = useContext(TableContext)
2112
+ if (isStacked) {
2113
+ let headerTxt
2114
+ if (typeof header === 'function') {
2115
+ headerTxt = React.createElement(header)
2116
+ } else {
2117
+ headerTxt = header
2118
+ }
2119
+ return (
2120
+ <div role="cell">
2121
+ {headerTxt && headerTxt}
2122
+ {headerTxt && ': '}
2123
+ {children}
2124
+ </div>
2125
+ )
2126
+ }
2127
+ return <td>{children}</td>
2128
+ }
2129
+
2130
+ const CustomTableRow = ({ children }) => {
2131
+ const { hover, headers, isStacked } = useContext(TableContext)
2132
+ const [isHovered, setIsHovered] = useState(false)
2133
+
2134
+ const Tag = isStacked ? 'div' : 'tr'
2135
+ const rowStyle =
2136
+ hover && isHovered
2137
+ ? { backgroundColor: 'SeaGreen' }
2138
+ : { backgroundColor: 'white' }
2139
+
2140
+ return (
2141
+ <Tag
2142
+ style={rowStyle}
2143
+ role={isStacked ? 'row' : undefined}
2144
+ onMouseOver={() => setIsHovered(true)}
2145
+ onMouseOut={() => setIsHovered(false)}
2146
+ >
2147
+ {React.Children.toArray(children)
2148
+ .filter(React.isValidElement)
2149
+ .map((child, index) => {
2150
+ return React.cloneElement(child, {
2151
+ key: child.props.name,
2152
+ // used by `CustomTableCell` to render its column title in `stacked` layout
2153
+ header: headers && headers[index]
2154
+ })
2155
+ })}
2156
+ </Tag>
2157
+ )
2158
+ }
2159
+
2160
+ const Example = () => {
2161
+ const [layout, setLayout] = useState('auto')
2162
+ const [hover, setHover] = useState(false)
2163
+
2164
+ const handleChange = (field, value) => {
2165
+ if (field === 'layout') {
2166
+ setLayout(value)
2167
+ } else if (field === 'hover') {
2168
+ setHover(!hover)
2169
+ }
2170
+ }
2171
+
2172
+ const renderOptions = () => (
2173
+ <Flex alignItems="start">
2174
+ <Flex.Item margin="small">
2175
+ <RadioInputGroup
2176
+ name="customStackedLayout"
2177
+ description="Layout"
2178
+ value={layout}
2179
+ onChange={(e, value) => handleChange('layout', value)}
2180
+ >
2181
+ <RadioInput label="auto" value="auto" />
2182
+ <RadioInput label="fixed" value="fixed" />
2183
+ <RadioInput label="stacked" value="stacked" />
2184
+ </RadioInputGroup>
2185
+ </Flex.Item>
2186
+ <Flex.Item margin="small">
2187
+ <Checkbox
2188
+ label="hover"
2189
+ checked={hover}
2190
+ onChange={(e, value) => handleChange('hover', !hover)}
2191
+ />
2192
+ </Flex.Item>
2193
+ </Flex>
2194
+ )
2195
+
2196
+ return (
2197
+ <div>
2198
+ {renderOptions()}
2199
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
2200
+ <Table.Head>
2201
+ <CustomTableRow>
2202
+ <CustomTableCell scope="col">Rank</CustomTableCell>
2203
+ <CustomTableCell scope="col">Title</CustomTableCell>
2204
+ <CustomTableCell scope="col">Year</CustomTableCell>
2205
+ <CustomTableCell scope="col">Rating</CustomTableCell>
2206
+ </CustomTableRow>
2207
+ </Table.Head>
2208
+ <Table.Body>
2209
+ <CustomTableRow>
2210
+ <CustomTableCell scope="row">1</CustomTableCell>
2211
+ <CustomTableCell>The Godfather</CustomTableCell>
2212
+ <CustomTableCell>1972</CustomTableCell>
2213
+ <CustomTableCell>9.2</CustomTableCell>
2214
+ </CustomTableRow>
2215
+ <CustomTableRow>
2216
+ <CustomTableCell scope="row">2</CustomTableCell>
2217
+ <CustomTableCell>The Godfather: Part II</CustomTableCell>
2218
+ <CustomTableCell>1974</CustomTableCell>
2219
+ <CustomTableCell>9.0</CustomTableCell>
2220
+ </CustomTableRow>
2221
+ </Table.Body>
2222
+ </Table>
2223
+ </div>
2224
+ )
2225
+ }
2226
+
2227
+ render(<Example />)
2228
+ ```
987
2229
 
988
2230
  ### Guidelines
989
2231