@instructure/ui-table 10.2.1 → 10.2.2-pr-snapshot-1726164252153

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.
@@ -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
+ }
118
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
+ ```
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
+ }
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
+ }
253
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
-
674
- handleClick = (page) => {
675
- this.setState({
676
- page,
677
- })
678
- }
679
-
680
- handleSort = (event, options) => {
681
- const { onSort } = this.props
682
1372
 
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
1421
 
737
- class SortableTable extends React.Component {
738
- constructor (props) {
739
- super(props)
740
- const { headers } = props
741
-
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,86 +1458,82 @@ 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
 
@@ -881,55 +1543,138 @@ In some cases you might want to use custom components in a `Table`, e.g. a HOC f
881
1543
 
882
1544
  Wrapper HOCs are simple, just return the original component:
883
1545
 
884
- ```javascript
885
- ---
886
- type: example
887
- ---
888
-
889
- class CustomTableCell extends React.Component {
890
- render () {
891
- return (
892
- <Table.Cell {...this.props}>{this.props.children}</Table.Cell>
893
- )
1546
+ - ```javascript
1547
+ class CustomTableCell extends React.Component {
1548
+ render() {
1549
+ return <Table.Cell {...this.props}>{this.props.children}</Table.Cell>
1550
+ }
894
1551
  }
895
- }
896
1552
 
897
- class CustomTableRow extends React.Component {
898
- render () {
899
- return (
1553
+ class CustomTableRow extends React.Component {
1554
+ render() {
1555
+ return (
900
1556
  <Table.Row {...this.props}>
901
1557
  <Table.RowHeader>1</Table.RowHeader>
902
1558
  <Table.Cell>The Shawshank Redemption</Table.Cell>
903
1559
  <Table.Cell>1994</Table.Cell>
904
1560
  <CustomTableCell>9.3</CustomTableCell>
905
1561
  </Table.Row>
906
- )
1562
+ )
1563
+ }
907
1564
  }
908
- }
909
1565
 
910
- class Example extends React.Component {
911
- state = {
912
- layout: 'auto',
913
- hover: false,
914
- }
1566
+ class Example extends React.Component {
1567
+ state = {
1568
+ layout: 'auto',
1569
+ hover: false
1570
+ }
915
1571
 
916
- handleChange = (field, value) => {
917
- this.setState({
918
- [field]: value,
919
- })
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
+ }
920
1639
  }
921
1640
 
922
- renderOptions () {
923
- const { layout, hover } = this.state
1641
+ render(<Example />)
1642
+ ```
924
1643
 
925
- 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 = () => (
926
1671
  <Flex alignItems="start">
927
1672
  <Flex.Item margin="small">
928
1673
  <RadioInputGroup
929
1674
  name="layout2"
930
1675
  description="layout2"
931
1676
  value={layout}
932
- onChange={(e, value) => this.handleChange('layout', value)}
1677
+ onChange={(e, value) => handleChange('layout', value)}
933
1678
  >
934
1679
  <RadioInput label="auto" value="auto" />
935
1680
  <RadioInput label="fixed" value="fixed" />
@@ -940,23 +1685,16 @@ class Example extends React.Component {
940
1685
  <Checkbox
941
1686
  label="hover"
942
1687
  checked={hover}
943
- onChange={(e, value) => this.handleChange('hover', !hover)}
1688
+ onChange={(e, value) => handleChange('hover', !hover)}
944
1689
  />
945
1690
  </Flex.Item>
946
1691
  </Flex>
947
1692
  )
948
- }
949
1693
 
950
- render() {
951
- const { layout, hover } = this.state
952
1694
  return (
953
1695
  <div>
954
- {this.renderOptions()}
955
- <Table
956
- caption='Top rated movies'
957
- layout={layout}
958
- hover={hover}
959
- >
1696
+ {renderOptions()}
1697
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
960
1698
  <Table.Head>
961
1699
  <Table.Row>
962
1700
  <Table.ColHeader id="Rank">Rank</Table.ColHeader>
@@ -966,7 +1704,7 @@ class Example extends React.Component {
966
1704
  </Table.Row>
967
1705
  </Table.Head>
968
1706
  <Table.Body>
969
- <CustomTableRow/>
1707
+ <CustomTableRow />
970
1708
  <Table.Row>
971
1709
  <Table.RowHeader>2</Table.RowHeader>
972
1710
  <Table.Cell>The Godfather</Table.Cell>
@@ -984,10 +1722,9 @@ class Example extends React.Component {
984
1722
  </div>
985
1723
  )
986
1724
  }
987
- }
988
1725
 
989
- render(<Example />)
990
- ```
1726
+ render(<Example />)
1727
+ ```
991
1728
 
992
1729
  #### Fully custom components
993
1730
 
@@ -1000,64 +1737,163 @@ If you want to use fully custom components you have to pay attention to the foll
1000
1737
 
1001
1738
  Basic fully custom table:
1002
1739
 
1003
- ```javascript
1004
- ---
1005
- type: example
1006
- ---
1007
-
1008
- class CustomTableCell extends React.Component {
1009
- render() {
1010
- return <td>{this.props.children}</td>
1740
+ - ```javascript
1741
+ class CustomTableCell extends React.Component {
1742
+ render() {
1743
+ return <td>{this.props.children}</td>
1744
+ }
1011
1745
  }
1012
- }
1013
1746
 
1014
- class CustomTableRow extends React.Component {
1015
- static contextType = TableContext
1016
- state = { isHovered: false }
1747
+ class CustomTableRow extends React.Component {
1748
+ static contextType = TableContext
1749
+ state = { isHovered: false }
1017
1750
 
1018
- toggleHoverOff = () => {
1019
- this.setState({isHovered: false})
1020
- }
1021
- toggleHoverOn = () => {
1022
- this.setState({isHovered: true})
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
+ }
1023
1773
  }
1024
- render() {
1025
- // super ugly way to change CSS on hover
1026
- let css = {backgroundColor: 'white'}
1027
- if (this.context.hover && this.state.isHovered) {
1028
- css = {backgroundColor: 'SeaGreen'}
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
+ })
1029
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
+
1030
1866
  return (
1031
- <tr style={css} onMouseOver={this.toggleHoverOn} onMouseOut={this.toggleHoverOff}>
1032
- {this.props.children}
1867
+ <tr
1868
+ style={rowStyle}
1869
+ onMouseOver={() => setIsHovered(true)}
1870
+ onMouseOut={() => setIsHovered(false)}
1871
+ >
1872
+ {children}
1033
1873
  </tr>
1034
1874
  )
1035
1875
  }
1036
- }
1037
1876
 
1038
- class Example extends React.Component {
1039
- state = {
1040
- layout: 'auto',
1041
- hover: false,
1042
- }
1877
+ const Example = () => {
1878
+ const [layout, setLayout] = useState('auto')
1879
+ const [hover, setHover] = useState(false)
1043
1880
 
1044
- handleChange = (field, value) => {
1045
- this.setState({
1046
- [field]: value,
1047
- })
1048
- }
1049
-
1050
- renderOptions () {
1051
- const { layout, hover } = this.state
1881
+ const handleChange = (field, value) => {
1882
+ if (field === 'layout') {
1883
+ setLayout(value)
1884
+ } else if (field === 'hover') {
1885
+ setHover(!hover)
1886
+ }
1887
+ }
1052
1888
 
1053
- return (
1889
+ const renderOptions = () => (
1054
1890
  <Flex alignItems="start">
1055
1891
  <Flex.Item margin="small">
1056
1892
  <RadioInputGroup
1057
1893
  name="Layout"
1058
1894
  description="Layout"
1059
1895
  value={layout}
1060
- onChange={(e, value) => this.handleChange('layout', value)}
1896
+ onChange={(e, value) => handleChange('layout', value)}
1061
1897
  >
1062
1898
  <RadioInput label="auto" value="auto" />
1063
1899
  <RadioInput label="fixed" value="fixed" />
@@ -1067,41 +1903,33 @@ class Example extends React.Component {
1067
1903
  <Checkbox
1068
1904
  label="hover"
1069
1905
  checked={hover}
1070
- onChange={(e, value) => this.handleChange('hover', !hover)}
1906
+ onChange={(e, value) => handleChange('hover', !hover)}
1071
1907
  />
1072
1908
  </Flex.Item>
1073
1909
  </Flex>
1074
1910
  )
1075
- }
1076
-
1077
- render() {
1078
- const { layout, hover } = this.state
1079
1911
 
1080
1912
  return (
1081
1913
  <div>
1082
- {this.renderOptions()}
1083
- <Table
1084
- caption='Top rated movies'
1085
- layout={layout}
1086
- hover={hover}
1087
- >
1914
+ {renderOptions()}
1915
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
1088
1916
  <Table.Head>
1089
1917
  <CustomTableRow>
1090
- <CustomTableCell scope='col'>Rank</CustomTableCell>
1091
- <CustomTableCell scope='col'>Title</CustomTableCell>
1092
- <CustomTableCell scope='col'>Year</CustomTableCell>
1093
- <CustomTableCell scope='col'>Rating</CustomTableCell>
1918
+ <CustomTableCell scope="col">Rank</CustomTableCell>
1919
+ <CustomTableCell scope="col">Title</CustomTableCell>
1920
+ <CustomTableCell scope="col">Year</CustomTableCell>
1921
+ <CustomTableCell scope="col">Rating</CustomTableCell>
1094
1922
  </CustomTableRow>
1095
1923
  </Table.Head>
1096
1924
  <Table.Body>
1097
1925
  <CustomTableRow>
1098
- <CustomTableCell scope='row'>1</CustomTableCell>
1926
+ <CustomTableCell scope="row">1</CustomTableCell>
1099
1927
  <CustomTableCell>The Godfather</CustomTableCell>
1100
1928
  <CustomTableCell>1972</CustomTableCell>
1101
1929
  <CustomTableCell>9.2</CustomTableCell>
1102
1930
  </CustomTableRow>
1103
1931
  <CustomTableRow>
1104
- <CustomTableCell scope='row'>2</CustomTableCell>
1932
+ <CustomTableCell scope="row">2</CustomTableCell>
1105
1933
  <CustomTableCell>The Godfather: Part II</CustomTableCell>
1106
1934
  <CustomTableCell>1974</CustomTableCell>
1107
1935
  <CustomTableCell>9.0</CustomTableCell>
@@ -1111,10 +1939,9 @@ class Example extends React.Component {
1111
1939
  </div>
1112
1940
  )
1113
1941
  }
1114
- }
1115
1942
 
1116
- render(<Example />)
1117
- ```
1943
+ render(<Example />)
1944
+ ```
1118
1945
 
1119
1946
  #### Fully custom components with `stacked` layout
1120
1947
 
@@ -1136,56 +1963,188 @@ Also you need the following props on the components:
1136
1963
 
1137
1964
  Custom table with `stacked` layout support:
1138
1965
 
1139
- ```javascript
1140
- ---
1141
- type: example
1142
- ---
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
+ }
1143
2042
 
1144
- class CustomTableCell extends React.Component {
1145
- static contextType = TableContext
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
+ }
1146
2070
 
1147
- render() {
1148
- const isStacked = this.context.isStacked
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)
1149
2112
  if (isStacked) {
1150
2113
  let headerTxt
1151
- if (typeof this.props.header === 'function') {
1152
- headerTxt = React.createElement(this.props.header)
2114
+ if (typeof header === 'function') {
2115
+ headerTxt = React.createElement(header)
1153
2116
  } else {
1154
- headerTxt = this.props.header
2117
+ headerTxt = header
1155
2118
  }
1156
- return <div role="cell">
1157
- {headerTxt && headerTxt}
1158
- {headerTxt && ': '}
1159
- {this.props.children}
1160
- </div>
2119
+ return (
2120
+ <div role="cell">
2121
+ {headerTxt && headerTxt}
2122
+ {headerTxt && ': '}
2123
+ {children}
2124
+ </div>
2125
+ )
1161
2126
  }
1162
- return <td>{this.props.children}</td>
2127
+ return <td>{children}</td>
1163
2128
  }
1164
- }
1165
2129
 
1166
- class CustomTableRow extends React.Component {
1167
- static contextType = TableContext
1168
- state = { isHovered: false }
2130
+ const CustomTableRow = ({ children }) => {
2131
+ const { hover, headers, isStacked } = useContext(TableContext)
2132
+ const [isHovered, setIsHovered] = useState(false)
1169
2133
 
1170
- toggleHoverOff = () => {
1171
- this.setState({isHovered: false})
1172
- }
1173
- toggleHoverOn = () => {
1174
- this.setState({isHovered: true})
1175
- }
1176
- render() {
1177
- // super ugly way to change CSS on hover
1178
- let css = {backgroundColor: 'white'}
1179
- if (this.context.hover && this.state.isHovered) {
1180
- css = {backgroundColor: 'SeaGreen'}
1181
- }
1182
- const headers = this.context.headers
1183
- const isStacked = this.context.isStacked
1184
2134
  const Tag = isStacked ? 'div' : 'tr'
2135
+ const rowStyle =
2136
+ hover && isHovered
2137
+ ? { backgroundColor: 'SeaGreen' }
2138
+ : { backgroundColor: 'white' }
2139
+
1185
2140
  return (
1186
- <Tag style={css} role={isStacked ? 'row' : undefined}
1187
- onMouseOver={this.toggleHoverOn} onMouseOut={this.toggleHoverOff}>
1188
- {React.Children.toArray(this.props.children)
2141
+ <Tag
2142
+ style={rowStyle}
2143
+ role={isStacked ? 'row' : undefined}
2144
+ onMouseOver={() => setIsHovered(true)}
2145
+ onMouseOut={() => setIsHovered(false)}
2146
+ >
2147
+ {React.Children.toArray(children)
1189
2148
  .filter(React.isValidElement)
1190
2149
  .map((child, index) => {
1191
2150
  return React.cloneElement(child, {
@@ -1193,36 +2152,31 @@ class CustomTableRow extends React.Component {
1193
2152
  // used by `CustomTableCell` to render its column title in `stacked` layout
1194
2153
  header: headers && headers[index]
1195
2154
  })
1196
- })
1197
- }
2155
+ })}
1198
2156
  </Tag>
1199
2157
  )
1200
2158
  }
1201
- }
1202
2159
 
1203
- class Example extends React.Component {
1204
- state = {
1205
- layout: 'auto',
1206
- hover: false,
1207
- }
1208
-
1209
- handleChange = (field, value) => {
1210
- this.setState({
1211
- [field]: value,
1212
- })
1213
- }
2160
+ const Example = () => {
2161
+ const [layout, setLayout] = useState('auto')
2162
+ const [hover, setHover] = useState(false)
1214
2163
 
1215
- renderOptions () {
1216
- const { layout, hover } = this.state
2164
+ const handleChange = (field, value) => {
2165
+ if (field === 'layout') {
2166
+ setLayout(value)
2167
+ } else if (field === 'hover') {
2168
+ setHover(!hover)
2169
+ }
2170
+ }
1217
2171
 
1218
- return (
2172
+ const renderOptions = () => (
1219
2173
  <Flex alignItems="start">
1220
2174
  <Flex.Item margin="small">
1221
2175
  <RadioInputGroup
1222
2176
  name="customStackedLayout"
1223
2177
  description="Layout"
1224
2178
  value={layout}
1225
- onChange={(e, value) => this.handleChange('layout', value)}
2179
+ onChange={(e, value) => handleChange('layout', value)}
1226
2180
  >
1227
2181
  <RadioInput label="auto" value="auto" />
1228
2182
  <RadioInput label="fixed" value="fixed" />
@@ -1233,41 +2187,33 @@ class Example extends React.Component {
1233
2187
  <Checkbox
1234
2188
  label="hover"
1235
2189
  checked={hover}
1236
- onChange={(e, value) => this.handleChange('hover', !hover)}
2190
+ onChange={(e, value) => handleChange('hover', !hover)}
1237
2191
  />
1238
2192
  </Flex.Item>
1239
2193
  </Flex>
1240
2194
  )
1241
- }
1242
-
1243
- render() {
1244
- const { layout, hover } = this.state
1245
2195
 
1246
2196
  return (
1247
2197
  <div>
1248
- {this.renderOptions()}
1249
- <Table
1250
- caption='Top rated movies'
1251
- layout={layout}
1252
- hover={hover}
1253
- >
2198
+ {renderOptions()}
2199
+ <Table caption="Top rated movies" layout={layout} hover={hover}>
1254
2200
  <Table.Head>
1255
2201
  <CustomTableRow>
1256
- <CustomTableCell scope='col'>Rank</CustomTableCell>
1257
- <CustomTableCell scope='col'>Title</CustomTableCell>
1258
- <CustomTableCell scope='col'>Year</CustomTableCell>
1259
- <CustomTableCell scope='col'>Rating</CustomTableCell>
2202
+ <CustomTableCell scope="col">Rank</CustomTableCell>
2203
+ <CustomTableCell scope="col">Title</CustomTableCell>
2204
+ <CustomTableCell scope="col">Year</CustomTableCell>
2205
+ <CustomTableCell scope="col">Rating</CustomTableCell>
1260
2206
  </CustomTableRow>
1261
2207
  </Table.Head>
1262
2208
  <Table.Body>
1263
2209
  <CustomTableRow>
1264
- <CustomTableCell scope='row'>1</CustomTableCell>
2210
+ <CustomTableCell scope="row">1</CustomTableCell>
1265
2211
  <CustomTableCell>The Godfather</CustomTableCell>
1266
2212
  <CustomTableCell>1972</CustomTableCell>
1267
2213
  <CustomTableCell>9.2</CustomTableCell>
1268
2214
  </CustomTableRow>
1269
2215
  <CustomTableRow>
1270
- <CustomTableCell scope='row'>2</CustomTableCell>
2216
+ <CustomTableCell scope="row">2</CustomTableCell>
1271
2217
  <CustomTableCell>The Godfather: Part II</CustomTableCell>
1272
2218
  <CustomTableCell>1974</CustomTableCell>
1273
2219
  <CustomTableCell>9.0</CustomTableCell>
@@ -1277,10 +2223,9 @@ class Example extends React.Component {
1277
2223
  </div>
1278
2224
  )
1279
2225
  }
1280
- }
1281
2226
 
1282
- render(<Example />)
1283
- ```
2227
+ render(<Example />)
2228
+ ```
1284
2229
 
1285
2230
  ### Guidelines
1286
2231