ferns-ui 0.25.3 → 0.26.1

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 (90) hide show
  1. package/dist/ActionSheet.d.ts +1 -1
  2. package/dist/ActionSheet.js +1 -2
  3. package/dist/ActionSheet.js.map +1 -1
  4. package/dist/Box.js +1 -1
  5. package/dist/Box.js.map +1 -1
  6. package/dist/CheckBox.d.ts +1 -4
  7. package/dist/CheckBox.js +33 -20
  8. package/dist/CheckBox.js.map +1 -1
  9. package/dist/Common.d.ts +44 -10
  10. package/dist/Common.js.map +1 -1
  11. package/dist/DateTimeField.android.d.ts +1 -1
  12. package/dist/DateTimeField.android.js +58 -16
  13. package/dist/DateTimeField.android.js.map +1 -1
  14. package/dist/DateTimeField.d.ts +2 -2
  15. package/dist/DateTimeField.ios.d.ts +1 -1
  16. package/dist/DateTimeField.ios.js +43 -12
  17. package/dist/DateTimeField.ios.js.map +1 -1
  18. package/dist/DateTimeField.js.map +1 -1
  19. package/dist/Field.js +2 -2
  20. package/dist/Field.js.map +1 -1
  21. package/dist/Mask.d.ts +2 -3
  22. package/dist/MediaQuery.d.ts +1 -0
  23. package/dist/MediaQuery.js +16 -0
  24. package/dist/MediaQuery.js.map +1 -1
  25. package/dist/Page.js.map +1 -1
  26. package/dist/SegmentedControl.js +6 -2
  27. package/dist/SegmentedControl.js.map +1 -1
  28. package/dist/SelectList.d.ts +1 -1
  29. package/dist/SelectList.js +9 -8
  30. package/dist/SelectList.js.map +1 -1
  31. package/dist/SplitPage.d.ts +3 -21
  32. package/dist/SplitPage.js +78 -23
  33. package/dist/SplitPage.js.map +1 -1
  34. package/dist/SplitPage.native.d.ts +3 -0
  35. package/dist/SplitPage.native.js +75 -0
  36. package/dist/SplitPage.native.js.map +1 -0
  37. package/dist/Table.d.ts +21 -0
  38. package/dist/Table.js +31 -0
  39. package/dist/Table.js.map +1 -0
  40. package/dist/TableHeader.d.ts +20 -0
  41. package/dist/TableHeader.js +11 -0
  42. package/dist/TableHeader.js.map +1 -0
  43. package/dist/TableHeaderCell.d.ts +15 -0
  44. package/dist/TableHeaderCell.js +36 -0
  45. package/dist/TableHeaderCell.js.map +1 -0
  46. package/dist/TableRow.d.ts +24 -0
  47. package/dist/TableRow.js +30 -0
  48. package/dist/TableRow.js.map +1 -0
  49. package/dist/TapToEdit.js +21 -16
  50. package/dist/TapToEdit.js.map +1 -1
  51. package/dist/TextField.js +11 -6
  52. package/dist/TextField.js.map +1 -1
  53. package/dist/Utilities.d.ts +1 -0
  54. package/dist/Utilities.js +18 -2
  55. package/dist/Utilities.js.map +1 -1
  56. package/dist/WithLabel.d.ts +3 -3
  57. package/dist/WithLabel.js +3 -0
  58. package/dist/WithLabel.js.map +1 -1
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +5 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/tableContext.d.ts +18 -0
  63. package/dist/tableContext.js +16 -0
  64. package/dist/tableContext.js.map +1 -0
  65. package/package.json +2 -1
  66. package/src/ActionSheet.tsx +2 -3
  67. package/src/Box.tsx +1 -0
  68. package/src/CheckBox.tsx +85 -60
  69. package/src/Common.ts +49 -10
  70. package/src/DateTimeField.android.tsx +71 -26
  71. package/src/DateTimeField.ios.tsx +55 -13
  72. package/src/DateTimeField.tsx +2 -2
  73. package/src/Field.tsx +4 -4
  74. package/src/Mask.tsx +2 -2
  75. package/src/MediaQuery.ts +14 -0
  76. package/src/Page.tsx +0 -1
  77. package/src/SegmentedControl.tsx +3 -3
  78. package/src/SelectList.tsx +9 -2
  79. package/src/SplitPage.native.tsx +156 -0
  80. package/src/SplitPage.tsx +136 -47
  81. package/src/Table.tsx +69 -0
  82. package/src/TableHeader.tsx +33 -0
  83. package/src/TableHeaderCell.tsx +76 -0
  84. package/src/TableRow.tsx +87 -0
  85. package/src/TapToEdit.tsx +21 -22
  86. package/src/TextField.tsx +26 -17
  87. package/src/Utilities.tsx +24 -3
  88. package/src/WithLabel.tsx +6 -3
  89. package/src/index.tsx +10 -5
  90. package/src/tableContext.tsx +43 -0
@@ -0,0 +1,156 @@
1
+ // TODO: Update SplitPage native to have desktop UX for tablet sized screens
2
+ import React, {Children, useCallback, useEffect, useState} from "react";
3
+ import {Dimensions, ListRenderItemInfo, View} from "react-native";
4
+ import {SwiperFlatList} from "react-native-swiper-flatlist";
5
+
6
+ import {Box} from "./Box";
7
+ import {SplitPageProps} from "./Common";
8
+ import {FlatList} from "./FlatList";
9
+ import {IconButton} from "./IconButton";
10
+ import {Spinner} from "./Spinner";
11
+ import {Unifier} from "./Unifier";
12
+
13
+ export const SplitPage = ({
14
+ children,
15
+ loading = false,
16
+ color,
17
+ keyboardOffset,
18
+ renderListViewItem,
19
+ renderListViewHeader,
20
+ renderContent,
21
+ onSelectionChange = () => {},
22
+ listViewData,
23
+ listViewExtraData,
24
+ bottomNavBarHeight,
25
+ showItemList,
26
+ }: SplitPageProps) => {
27
+ const [selectedId, setSelectedId] = useState<number | undefined>(undefined);
28
+
29
+ const elementArray = Children.toArray(children);
30
+ const {width} = Dimensions.get("window");
31
+
32
+ const onItemSelect = useCallback(
33
+ (item: ListRenderItemInfo<any>) => {
34
+ setSelectedId(item.index);
35
+ onSelectionChange(item);
36
+ },
37
+ [onSelectionChange]
38
+ );
39
+
40
+ const onItemDeselect = useCallback(() => {
41
+ setSelectedId(undefined);
42
+ onSelectionChange(undefined);
43
+ }, [onSelectionChange]);
44
+
45
+ useEffect(() => {
46
+ if (showItemList) {
47
+ onItemDeselect();
48
+ }
49
+ }, [showItemList, onItemDeselect]);
50
+
51
+ if (!children && !renderContent) {
52
+ console.warn("A child node is required");
53
+ return null;
54
+ }
55
+
56
+ const renderItem = (itemInfo: ListRenderItemInfo<any>) => {
57
+ return (
58
+ <Box
59
+ onClick={() => {
60
+ Unifier.utils.haptic();
61
+ onItemSelect(itemInfo);
62
+ }}
63
+ >
64
+ {renderListViewItem(itemInfo)}
65
+ </Box>
66
+ );
67
+ };
68
+
69
+ const renderList = () => {
70
+ if (selectedId !== undefined) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <View
76
+ style={{
77
+ width: "100%",
78
+ maxWidth: "100%",
79
+ flexGrow: 1,
80
+ flexShrink: 0,
81
+ display: "flex",
82
+ flexDirection: "column",
83
+ paddingBottom: bottomNavBarHeight,
84
+ }}
85
+ >
86
+ {renderListViewHeader && renderListViewHeader()}
87
+ <FlatList
88
+ data={listViewData}
89
+ extraData={listViewExtraData}
90
+ keyExtractor={(item) => item.id}
91
+ renderItem={renderItem}
92
+ />
93
+ </View>
94
+ );
95
+ };
96
+
97
+ const renderListContent = () => {
98
+ if (selectedId === undefined) {
99
+ return null;
100
+ }
101
+ return (
102
+ <Box flex="grow" padding={2}>
103
+ <Box width="100%">
104
+ <IconButton
105
+ accessibilityLabel="close"
106
+ icon="times"
107
+ iconColor="darkGray"
108
+ onClick={() => onItemDeselect()}
109
+ />
110
+ </Box>
111
+ {renderContent && renderContent(selectedId)}
112
+ </Box>
113
+ );
114
+ };
115
+
116
+ const renderChildrenContent = () => {
117
+ if (selectedId === undefined) {
118
+ return null;
119
+ }
120
+ return (
121
+ <SwiperFlatList
122
+ nestedScrollEnabled
123
+ renderAll
124
+ showPagination={elementArray.length > 1}
125
+ style={{width: "100%"}}
126
+ >
127
+ {elementArray.map((element, i) => {
128
+ return (
129
+ <View
130
+ key={i}
131
+ style={{width, height: elementArray.length > 1 ? "95%" : "100%", padding: 4}}
132
+ >
133
+ {element}
134
+ </View>
135
+ );
136
+ })}
137
+ </SwiperFlatList>
138
+ );
139
+ };
140
+
141
+ const renderMainContent = renderContent ? renderListContent() : renderChildrenContent();
142
+
143
+ return (
144
+ <Box
145
+ avoidKeyboard
146
+ color={color || "lightGray"}
147
+ flex="grow"
148
+ height="100%"
149
+ keyboardOffset={keyboardOffset}
150
+ width="100%"
151
+ >
152
+ {loading === true && <Spinner color={Unifier.theme.darkGray as any} size="md" />}
153
+ {selectedId === undefined ? renderList() : renderMainContent}
154
+ </Box>
155
+ );
156
+ };
package/src/SplitPage.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import React, {Children, ReactChild, ReactElement, useState} from "react";
2
- import {ListRenderItemInfo, ScrollView, View} from "react-native";
1
+ import React, {Children, useCallback, useEffect, useState} from "react";
2
+ import {Dimensions, ListRenderItemInfo, ScrollView, View} from "react-native";
3
+ import {SwiperFlatList} from "react-native-swiper-flatlist";
3
4
 
4
5
  import {Box} from "./Box";
5
- import {Color, SPACING} from "./Common";
6
+ import {SplitPageProps} from "./Common";
6
7
  import {FlatList} from "./FlatList";
7
8
  import {IconButton} from "./IconButton";
8
9
  import {mediaQueryLargerThan} from "./MediaQuery";
@@ -10,24 +11,6 @@ import {SegmentedControl} from "./SegmentedControl";
10
11
  import {Spinner} from "./Spinner";
11
12
  import {Unifier} from "./Unifier";
12
13
 
13
- interface SplitPageProps {
14
- children?: ReactChild | ReactChild[] | null;
15
- tabs?: string[];
16
- // TODO: figure out navigation
17
- navigation?: any;
18
- loading?: boolean;
19
- color?: Color;
20
- keyboardOffset?: number;
21
- renderListViewItem: (itemInfo: ListRenderItemInfo<any>) => ReactElement | null;
22
- renderListViewHeader?: () => ReactElement | null;
23
- renderContent?: (index?: number) => ReactElement | ReactElement[] | null;
24
- listViewData: any[];
25
- listViewExtraData?: any;
26
- listViewWidth?: number;
27
- renderChild?: () => ReactChild;
28
- selectLimit?: number;
29
- }
30
-
31
14
  // A component for rendering a list on one side and a details view on the right for large screens,
32
15
  // and a scrollable list where clicking an item takes you the details view.
33
16
  export const SplitPage = ({
@@ -39,16 +22,41 @@ export const SplitPage = ({
39
22
  renderListViewItem,
40
23
  renderListViewHeader,
41
24
  renderContent,
25
+ onSelectionChange = () => {},
42
26
  listViewData,
43
27
  listViewExtraData,
44
28
  listViewWidth,
29
+ bottomNavBarHeight,
30
+ showItemList,
45
31
  selectLimit,
46
32
  }: SplitPageProps) => {
47
33
  const [selectedId, setSelectedId] = useState<number | undefined>(undefined);
48
34
  const [activeTabs, setActiveTabs] = useState<number[]>(tabs.length > 2 ? [0, 1] : []);
35
+ const {width} = Dimensions.get("window");
36
+
37
+ const isMobileDevice = !mediaQueryLargerThan("sm");
49
38
 
50
39
  const elementArray = Children.toArray(children);
51
40
 
41
+ const onItemSelect = useCallback(
42
+ (item: ListRenderItemInfo<any>) => {
43
+ setSelectedId(item.index);
44
+ onSelectionChange(item);
45
+ },
46
+ [onSelectionChange]
47
+ );
48
+
49
+ const onItemDeselect = useCallback(() => {
50
+ setSelectedId(undefined);
51
+ onSelectionChange(undefined);
52
+ }, [onSelectionChange]);
53
+
54
+ useEffect(() => {
55
+ if (showItemList) {
56
+ onItemDeselect();
57
+ }
58
+ }, [showItemList, onItemDeselect]);
59
+
52
60
  if (!children && !renderContent) {
53
61
  console.warn("A child node is required");
54
62
  return null;
@@ -63,7 +71,7 @@ export const SplitPage = ({
63
71
  return (
64
72
  <Box
65
73
  onClick={() => {
66
- setSelectedId(itemInfo.index);
74
+ onItemSelect(itemInfo);
67
75
  }}
68
76
  >
69
77
  {renderListViewItem(itemInfo)}
@@ -72,19 +80,14 @@ export const SplitPage = ({
72
80
  };
73
81
 
74
82
  const renderList = () => {
75
- if (!mediaQueryLargerThan("sm") && selectedId) {
76
- return null;
77
- }
78
83
  return (
79
84
  <View
80
85
  style={{
81
- width: mediaQueryLargerThan("sm") ? listViewWidth ?? 300 : "100%",
82
- maxWidth: mediaQueryLargerThan("sm") ? listViewWidth ?? 300 : "100%",
86
+ width: listViewWidth ?? 300,
87
+ maxWidth: listViewWidth ?? 300,
83
88
  flexGrow: 1,
84
89
  flexShrink: 0,
85
90
  display: "flex",
86
- paddingTop: "12px",
87
- paddingBottom: "12px",
88
91
  flexDirection: "column",
89
92
  }}
90
93
  >
@@ -102,16 +105,6 @@ export const SplitPage = ({
102
105
  const renderListContent = () => {
103
106
  return (
104
107
  <Box flex="grow" padding={2}>
105
- {!mediaQueryLargerThan("sm") && (
106
- <Box width="100%">
107
- <IconButton
108
- accessibilityLabel="close"
109
- icon="times"
110
- iconColor="darkGray"
111
- onClick={() => setSelectedId(undefined)}
112
- />
113
- </Box>
114
- )}
115
108
  {renderContent && renderContent(selectedId)}
116
109
  </Box>
117
110
  );
@@ -128,7 +121,7 @@ export const SplitPage = ({
128
121
  alignItems: "center",
129
122
  }}
130
123
  >
131
- <Box paddingX={4} paddingY={2} width="100%">
124
+ <Box marginBottom={4} paddingX={4} width="100%">
132
125
  <SegmentedControl
133
126
  items={tabs}
134
127
  multiselect
@@ -143,10 +136,10 @@ export const SplitPage = ({
143
136
  direction="row"
144
137
  flex="grow"
145
138
  height="100%"
146
- paddingX={2}
139
+ paddingX={4}
147
140
  width={activeTabs.length > 1 ? "100%" : "60%"}
148
141
  >
149
- {activeTabs.map((tabIndex) => {
142
+ {activeTabs.map((tabIndex, i) => {
150
143
  return (
151
144
  <ScrollView
152
145
  key={tabIndex}
@@ -156,8 +149,9 @@ export const SplitPage = ({
156
149
  style={{
157
150
  flex: 1,
158
151
  width: "60%",
159
- padding: 3 * SPACING,
160
152
  height: "100%",
153
+ paddingRight: i ? 0 : 16,
154
+ paddingLeft: i ? 16 : 0,
161
155
  }}
162
156
  >
163
157
  {elementArray[tabIndex]}
@@ -180,7 +174,6 @@ export const SplitPage = ({
180
174
  style={{
181
175
  flex: 1,
182
176
  width: "60%",
183
- padding: 3 * SPACING,
184
177
  height: "100%",
185
178
  }}
186
179
  >
@@ -193,21 +186,117 @@ export const SplitPage = ({
193
186
  }
194
187
  };
195
188
 
189
+ const renderMobileList = () => {
190
+ if (isMobileDevice && selectedId !== undefined) {
191
+ return null;
192
+ }
193
+
194
+ return (
195
+ <View
196
+ style={{
197
+ width: "100%",
198
+ maxWidth: "100%",
199
+ height: "100%",
200
+ flexGrow: 1,
201
+ flexShrink: 0,
202
+ display: "flex",
203
+ flexDirection: "column",
204
+ }}
205
+ >
206
+ {renderListViewHeader && renderListViewHeader()}
207
+ <FlatList
208
+ data={listViewData}
209
+ extraData={listViewExtraData}
210
+ keyExtractor={(item) => item.id}
211
+ nestedScrollEnabled
212
+ renderItem={renderItem}
213
+ />
214
+ </View>
215
+ );
216
+ };
217
+
218
+ const renderMobileListContent = () => {
219
+ if (isMobileDevice && selectedId === undefined) {
220
+ return null;
221
+ }
222
+
223
+ return (
224
+ <Box flex="grow" padding={2}>
225
+ {isMobileDevice && (
226
+ <Box width="100%">
227
+ <IconButton
228
+ accessibilityLabel="close"
229
+ icon="times"
230
+ iconColor="darkGray"
231
+ onClick={() => onItemDeselect()}
232
+ />
233
+ </Box>
234
+ )}
235
+ {renderContent && renderContent(selectedId)}
236
+ </Box>
237
+ );
238
+ };
239
+
240
+ const renderMobileChildrenContent = () => {
241
+ if (selectedId === undefined) {
242
+ return null;
243
+ }
244
+ return (
245
+ <SwiperFlatList
246
+ nestedScrollEnabled
247
+ paginationStyle={{justifyContent: "center", width: "95%"}}
248
+ renderAll
249
+ showPagination
250
+ style={{width: "100%"}}
251
+ >
252
+ {elementArray.map((element, i) => {
253
+ return (
254
+ <View
255
+ key={i}
256
+ style={{
257
+ width: width - 8,
258
+ padding: 4,
259
+ height: elementArray.length > 1 ? "90vh" : "100vh",
260
+ paddingBottom: bottomNavBarHeight,
261
+ }}
262
+ >
263
+ {element}
264
+ </View>
265
+ );
266
+ })}
267
+ </SwiperFlatList>
268
+ );
269
+ };
270
+
271
+ const renderSplitPage = () => {
272
+ return (
273
+ <>
274
+ {renderList()}
275
+ {renderContent ? renderListContent() : renderChildrenContent()}
276
+ </>
277
+ );
278
+ };
279
+
280
+ const renderMobileSplitPage = () => {
281
+ const renderMainContent = renderContent
282
+ ? renderMobileListContent()
283
+ : renderMobileChildrenContent();
284
+ return selectedId === undefined ? renderMobileList() : renderMainContent;
285
+ };
286
+
196
287
  return (
197
288
  <Box
198
289
  avoidKeyboard
199
290
  color={color || "lightGray"}
200
291
  direction="row"
201
292
  display="flex"
202
- flex="grow"
203
293
  height="100%"
204
294
  keyboardOffset={keyboardOffset}
205
295
  padding={2}
206
296
  width="100%"
207
297
  >
208
298
  {loading === true && <Spinner color={Unifier.theme.darkGray as any} size="md" />}
209
- {renderList()}
210
- {renderContent ? renderListContent() : renderChildrenContent()}
299
+ {Boolean(isMobileDevice) ? renderMobileSplitPage() : renderSplitPage()}
211
300
  </Box>
212
301
  );
213
302
  };
package/src/Table.tsx ADDED
@@ -0,0 +1,69 @@
1
+ import React, {Children, ReactElement, useRef} from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {ScrollView} from "./ScrollView";
5
+ import {ColumnSortInterface, TableContextProvider} from "./tableContext";
6
+
7
+ interface TableProps {
8
+ /**
9
+ * Must be instances of TableHeader, TableRow, and/or TableFooter components.
10
+ */
11
+ children: React.ReactNode | React.ReactNode[];
12
+ /**
13
+ * Width of columns in the table. This is used to calculate the width of each column. Can be numbers for pixels or strings for percentages.
14
+ */
15
+ columns: Array<number | string>;
16
+ /**
17
+ * Specify a border width for Table: "sm" is 1px.
18
+ */
19
+ borderStyle?: "sm" | "none";
20
+ /**
21
+ * Use numbers for pixels: `maxHeight={100}` and strings for percentages: `maxHeight="100%"`.
22
+ */
23
+ maxHeight?: number | string;
24
+ }
25
+
26
+ export function Table({children, columns, borderStyle, maxHeight}: TableProps): React.ReactElement {
27
+ const tableRef = useRef(null);
28
+ const arrayChildren = Children.toArray(children);
29
+ const [sortColumn, setSortColumn] = React.useState<ColumnSortInterface | undefined>(undefined);
30
+
31
+ // Check if any of the rows below have a drawerContents prop to see if we need to render space for the caret.
32
+ const hasDrawerContents = arrayChildren.some((child) => {
33
+ return (child as ReactElement).props?.drawerContents;
34
+ });
35
+
36
+ // Calculate the total width of the table. If the table has only number widths, calculate a width. Otherwise use 100%.
37
+ let width: string | number;
38
+ if (columns.every((column) => typeof column === "number")) {
39
+ width = columns.reduce((acc, curr) => {
40
+ return (acc as number) + (curr as number);
41
+ }, 0);
42
+ if (hasDrawerContents) {
43
+ width = (width as number) + 30;
44
+ }
45
+ } else {
46
+ width = "100%";
47
+ }
48
+
49
+ return (
50
+ <TableContextProvider
51
+ columns={columns}
52
+ hasDrawerContents={hasDrawerContents}
53
+ setSortColumn={setSortColumn}
54
+ sortColumn={sortColumn}
55
+ >
56
+ <ScrollView horizontal style={{width, maxWidth: "100%"}}>
57
+ <Box
58
+ width={width}
59
+ {...(borderStyle === "sm" ? {borderStyle: "sm", rounding: 1} : {})}
60
+ ref={tableRef}
61
+ flex="grow"
62
+ maxHeight={maxHeight}
63
+ >
64
+ {children}
65
+ </Box>
66
+ </ScrollView>
67
+ </TableContextProvider>
68
+ );
69
+ }
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {TableRow} from "./TableRow";
5
+
6
+ interface TableHeaderProps {
7
+ /**
8
+ * Must be an instance of TableRow.
9
+ */
10
+ children: React.ReactNode | React.ReactNode[];
11
+ /**
12
+ * Display `visuallyHidden` ensures the component is visually hidden but still is read by screen readers.
13
+ */
14
+ display?: "tableHeaderGroup" | "visuallyHidden";
15
+ /**
16
+ * If true, the table header will be sticky and the table body will be scrollable. Not yet implemented.
17
+ */
18
+ sticky?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Use TableHeader to group the header content in Table.
23
+ */
24
+ export function TableHeader({
25
+ children,
26
+ display = "tableHeaderGroup",
27
+ }: TableHeaderProps): React.ReactElement {
28
+ return (
29
+ <Box display={display === "visuallyHidden" ? "none" : "flex"}>
30
+ <TableRow headerRow>{children}</TableRow>
31
+ </Box>
32
+ );
33
+ }
@@ -0,0 +1,76 @@
1
+ import React, {ReactElement} from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {IconButton} from "./IconButton";
5
+ import {useTableContext} from "./tableContext";
6
+
7
+ interface Props {
8
+ /**
9
+ * The content of the table header cell.
10
+ */
11
+ children: ReactElement;
12
+ index: number;
13
+ sortable?: boolean;
14
+ onSortChange?: (direction: "asc" | "desc" | undefined) => void;
15
+ }
16
+
17
+ /**
18
+ * Use TableHeaderCell to define a header cell in Table.
19
+ */
20
+ export function TableHeaderCell({children, index, sortable, onSortChange}: Props): ReactElement {
21
+ const {columns, setSortColumn, sortColumn} = useTableContext();
22
+ const width = columns[index];
23
+ if (!width) {
24
+ console.warn(`No width defined for column ${index} in TableHeaderCell`);
25
+ }
26
+
27
+ const onClick = () => {
28
+ // desc => asc => undefined
29
+ const newSort = sort === "desc" ? "asc" : sort === "asc" ? undefined : "desc";
30
+ if (setSortColumn) {
31
+ setSortColumn(newSort ? {column: index, direction: newSort} : undefined);
32
+ }
33
+ onSortChange && onSortChange(newSort);
34
+ };
35
+ const sort = sortColumn?.column === index ? sortColumn.direction : undefined;
36
+
37
+ if (sortable) {
38
+ if (!onSortChange) {
39
+ console.error("onSortChange is required when sortable is true");
40
+ }
41
+ return (
42
+ <Box
43
+ alignItems="center"
44
+ direction="row"
45
+ flex="grow"
46
+ justifyContent="between"
47
+ marginBottom={2}
48
+ marginTop={2}
49
+ maxWidth={width}
50
+ minHeight={36}
51
+ width={width}
52
+ onClick={onClick}
53
+ >
54
+ {children}
55
+ {Boolean(sort) && (
56
+ <Box paddingX={2}>
57
+ <IconButton
58
+ accessibilityLabel="sort"
59
+ bgColor="white"
60
+ icon={sort === "asc" ? "arrow-down" : "arrow-up"}
61
+ iconColor="darkGray"
62
+ size="sm"
63
+ onClick={() => {}}
64
+ />
65
+ </Box>
66
+ )}
67
+ </Box>
68
+ );
69
+ } else {
70
+ return (
71
+ <Box marginBottom={2} marginTop={2} width={width}>
72
+ {children}
73
+ </Box>
74
+ );
75
+ }
76
+ }
@@ -0,0 +1,87 @@
1
+ import React, {Children, useRef} from "react";
2
+
3
+ import {Box} from "./Box";
4
+ import {IconButton} from "./IconButton";
5
+ import {useTableContext} from "./tableContext";
6
+
7
+ interface Props {
8
+ /**
9
+ * Must be instances of TableCell or TableHeaderCell.
10
+ */
11
+ children: React.ReactNode | React.ReactNode[];
12
+ /**
13
+ * Header rows have an extra thick bottom border.
14
+ */
15
+ headerRow?: boolean;
16
+ /**
17
+ * Whether the row should start expanded or not.
18
+ */
19
+ expanded?: boolean;
20
+ /**
21
+ * When the row is expanded, the drawerContents are shown. If not
22
+ */
23
+ drawerContents?: React.ReactNode | React.ReactNode[];
24
+ }
25
+
26
+ /**
27
+ * Use TableRow to define a row in Table.
28
+ */
29
+ export function TableRow({
30
+ children,
31
+ headerRow = false,
32
+ expanded,
33
+ drawerContents,
34
+ }: Props): React.ReactElement {
35
+ const [isExpanded, setIsExpanded] = React.useState(expanded || false);
36
+ const {columns, hasDrawerContents} = useTableContext();
37
+ const rowRef = useRef<Box>(null);
38
+
39
+ const renderCellWithColumnIndex = (child: React.ReactNode, index: number) => {
40
+ if (!columns[index]) {
41
+ console.warn(`No width defined for column ${index} in TableRow`);
42
+ return null;
43
+ }
44
+ return (
45
+ <Box paddingX={2} width={columns[index]}>
46
+ {child}
47
+ </Box>
48
+ );
49
+ };
50
+
51
+ const border = {__style: {borderBottom: `${headerRow ? 2 : 1}px solid #e0e0e0`}};
52
+
53
+ return (
54
+ <Box
55
+ ref={rowRef}
56
+ dangerouslySetInlineStyle={border}
57
+ marginBottom={1}
58
+ marginTop={1}
59
+ width="100%"
60
+ >
61
+ <Box direction="row" width="100%">
62
+ {Boolean(drawerContents) && (
63
+ <Box width={30}>
64
+ <IconButton
65
+ accessibilityLabel="expand"
66
+ bgColor="white"
67
+ icon={isExpanded ? "chevron-up" : "chevron-down"}
68
+ iconColor="darkGray"
69
+ size="sm"
70
+ onClick={() => {
71
+ setIsExpanded(!isExpanded);
72
+ }}
73
+ />
74
+ </Box>
75
+ )}
76
+ {/* Still render a blank space so the columns line up. */}
77
+ {Boolean(hasDrawerContents && !drawerContents) && <Box width={30} />}
78
+ {Children.map(children, renderCellWithColumnIndex)}
79
+ </Box>
80
+ {Boolean(isExpanded) && (
81
+ <Box paddingX={2} width="100%">
82
+ {drawerContents}
83
+ </Box>
84
+ )}
85
+ </Box>
86
+ );
87
+ }