@openedx/paragon 23.4.5 → 23.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/DataTable/hooks.js +48 -2
  2. package/dist/DataTable/hooks.js.map +1 -1
  3. package/dist/DataTable/index.js +18 -9
  4. package/dist/DataTable/index.js.map +1 -1
  5. package/dist/DataTable/selection/ControlledSelectionStatus.js +7 -17
  6. package/dist/DataTable/selection/ControlledSelectionStatus.js.map +1 -1
  7. package/dist/DataTable/selection/data/actions.js +5 -0
  8. package/dist/DataTable/selection/data/reducer.js +12 -1
  9. package/dist/Form/_index.scss +4 -1
  10. package/dist/Modal/AlertModal.js.map +1 -1
  11. package/dist/Modal/ModalDialog.d.ts +1 -1
  12. package/dist/Modal/ModalDialog.js +1 -1
  13. package/dist/Modal/ModalDialog.js.map +1 -1
  14. package/dist/Modal/_ModalDialog.scss +1 -1
  15. package/dist/Toast/ToastContainer.d.ts +6 -0
  16. package/dist/Toast/ToastContainer.js +19 -29
  17. package/dist/Toast/ToastContainer.js.map +1 -1
  18. package/dist/Toast/index.d.ts +59 -0
  19. package/dist/Toast/index.js.map +1 -1
  20. package/dist/Toast/index.scss +2 -2
  21. package/dist/core.css +6 -4
  22. package/dist/core.css.map +1 -1
  23. package/dist/core.min.css +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +23 -23
  26. package/dist/theme-urls.json +6 -6
  27. package/package.json +1 -1
  28. package/src/DataTable/README.md +111 -78
  29. package/src/DataTable/hooks.jsx +55 -2
  30. package/src/DataTable/index.jsx +28 -16
  31. package/src/DataTable/selection/ControlledSelectionStatus.jsx +8 -23
  32. package/src/DataTable/selection/data/actions.js +5 -0
  33. package/src/DataTable/selection/data/reducer.js +12 -1
  34. package/src/DataTable/selection/tests/ControlledSelectionStatus.test.jsx +4 -23
  35. package/src/DataTable/selection/tests/reducer.test.js +4 -0
  36. package/src/DataTable/tests/DataTable.test.jsx +99 -3
  37. package/src/Form/_index.scss +4 -1
  38. package/src/Modal/AlertModal.jsx +5 -5
  39. package/src/Modal/ModalDialog.tsx +2 -2
  40. package/src/Modal/_ModalDialog.scss +1 -1
  41. package/src/Modal/alert-modal.mdx +4 -0
  42. package/src/Modal/fullscreen-modal.mdx +1 -0
  43. package/src/Modal/marketing-modal.mdx +1 -0
  44. package/src/Modal/modal-dialog.mdx +2 -2
  45. package/src/Modal/standard-modal.mdx +1 -0
  46. package/src/Modal/tests/AlertModal.test.jsx +4 -0
  47. package/src/Modal/tests/ModalDialog.test.tsx +3 -2
  48. package/src/Toast/README.md +4 -4
  49. package/src/Toast/{Toast.test.jsx → Toast.test.tsx} +23 -13
  50. package/src/Toast/ToastContainer.tsx +30 -0
  51. package/src/Toast/index.scss +2 -2
  52. package/src/Toast/{index.jsx → index.tsx} +27 -6
  53. package/src/index.d.ts +1 -1
  54. package/src/index.js +23 -23
  55. package/src/Toast/ToastContainer.jsx +0 -40
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalD
17
17
  export { default as ModalLayer } from './Modal/ModalLayer';
18
18
  export { default as Overlay, OverlayTrigger } from './Overlay';
19
19
  export { default as Portal } from './Modal/Portal';
20
+ export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
20
21
  export { default as Tooltip } from './Tooltip';
21
22
  export { default as useWindowSize, type WindowSizeData } from './hooks/useWindowSizeHook';
22
23
  export { default as useToggle, type Toggler, type ToggleHandlers } from './hooks/useToggleHook';
@@ -163,7 +164,6 @@ export const
163
164
  // from './Tabs';
164
165
  /** @deprecated Replaced by `Form.Control`. */
165
166
  export const TextArea: any; // from './TextArea';
166
- export const Toast: any, TOAST_CLOSE_LABEL_TEXT: string, TOAST_DELAY: number; // from './Toast';
167
167
  /** @deprecated Replaced by `Form.Group`. */
168
168
  export const ValidationFormGroup: any; // from './ValidationFormGroup';
169
169
  export const TransitionReplace: any; // from './TransitionReplace';
package/dist/index.js CHANGED
@@ -9,6 +9,28 @@ export { default as Button, ButtonGroup, ButtonToolbar } from './Button';
9
9
  export { default as Chip, CHIP_PGN_CLASS } from './Chip';
10
10
  export { default as ChipCarousel } from './ChipCarousel';
11
11
  export { default as Container } from './Container';
12
+ export {
13
+ default as Form,
14
+ RadioControl,
15
+ CheckboxControl,
16
+ SwitchControl,
17
+ FormSwitchSet,
18
+ FormControl,
19
+ FormControlDecoratorGroup,
20
+ FormControlFeedback,
21
+ FormCheck,
22
+ FormFile,
23
+ FormRadio,
24
+ FormRadioSet,
25
+ FormRadioSetContext,
26
+ FormGroup,
27
+ FormLabel,
28
+ useCheckboxSetValues,
29
+ FormText,
30
+ FormAutosuggest,
31
+ FormAutosuggestOption,
32
+ InputGroup,
33
+ } from './Form';
12
34
  export { default as Hyperlink } from './Hyperlink';
13
35
  export { default as Icon } from './Icon';
14
36
  export { default as IconButton, IconButtonWithTooltip } from './IconButton';
@@ -17,6 +39,7 @@ export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalD
17
39
  export { default as ModalLayer } from './Modal/ModalLayer';
18
40
  export { default as Overlay, OverlayTrigger } from './Overlay';
19
41
  export { default as Portal } from './Modal/Portal';
42
+ export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
20
43
  export { default as Tooltip } from './Tooltip';
21
44
  export { default as useWindowSize } from './hooks/useWindowSizeHook';
22
45
  export { default as useToggle } from './hooks/useToggleHook';
@@ -60,28 +83,6 @@ export {
60
83
  SplitButton,
61
84
  } from './Dropdown';
62
85
  export { default as Fade } from './Fade';
63
- export {
64
- default as Form,
65
- RadioControl,
66
- CheckboxControl,
67
- SwitchControl,
68
- FormSwitchSet,
69
- FormControl,
70
- FormControlDecoratorGroup,
71
- FormControlFeedback,
72
- FormCheck,
73
- FormFile,
74
- FormRadio,
75
- FormRadioSet,
76
- FormRadioSetContext,
77
- FormGroup,
78
- FormLabel,
79
- useCheckboxSetValues,
80
- FormText,
81
- FormAutosuggest,
82
- FormAutosuggestOption,
83
- InputGroup,
84
- } from './Form';
85
86
  export { default as IconButtonToggle } from './IconButtonToggle';
86
87
  export { default as Image, Figure } from './Image';
87
88
  export { default as MailtoLink, MAIL_TO_LINK_EXTERNAL_LINK_ALTERNATIVE_TEXT, MAIL_TO_LINK_EXTERNAL_LINK_TITLE } from './MailtoLink';
@@ -137,7 +138,6 @@ export {
137
138
  TabContent,
138
139
  TabPane,
139
140
  } from './Tabs';
140
- export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
141
141
  export { default as TransitionReplace } from './TransitionReplace';
142
142
  export { default as ValidationMessage } from './ValidationMessage';
143
143
  export { default as DataTable } from './DataTable';
@@ -1,5 +1,11 @@
1
1
  {
2
2
  "themeUrls": {
3
+ "core": {
4
+ "paths": {
5
+ "default": "./core.css",
6
+ "minified": "./core.min.css"
7
+ }
8
+ },
3
9
  "defaults": {
4
10
  "light": "light"
5
11
  },
@@ -10,12 +16,6 @@
10
16
  "minified": "./light.min.css"
11
17
  }
12
18
  }
13
- },
14
- "core": {
15
- "paths": {
16
- "default": "./core.css",
17
- "minified": "./core.min.css"
18
- }
19
19
  }
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openedx/paragon",
3
- "version": "23.4.5",
3
+ "version": "23.5.0",
4
4
  "description": "Accessible, responsive UI component library based on Bootstrap.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -167,76 +167,104 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
167
167
 
168
168
  ```jsx live
169
169
  () => {
170
- const PAGINATED_DATA = [
171
- [
172
- {
173
- id: '2baf70d1-42bb-4437-b551-e5fed5a87abe',
174
- title: 'Castle in the Sky',
175
- director: 'Hayao Miyazaki',
176
- producer: 'Isao Takahata',
177
- release_date: 1986,
178
- rt_score: 95,
179
- }, {
180
- id: '12cfb892-aac0-4c5b-94af-521852e46d6a',
181
- title: 'Grave of the Fireflies',
182
- director: 'Isao Takahata',
183
- producer: 'Toru Hara',
184
- release_date: 1988,
185
- rt_score: 97,
186
- },
187
- {
188
- id: '58611129-2dbc-4a81-a72f-77ddfc1b1b49',
189
- title: 'My Neighbor Totoro',
190
- director: 'Hayao Miyazaki',
191
- producer: 'Hayao Miyazaki',
192
- release_date: 1988,
193
- rt_score: 93,
194
- },
195
- ],
196
- [
197
- {
198
- id: 'ea660b10-85c4-4ae3-8a5f-41cea3648e3e',
199
- title: 'Kiki\'s Delivery Service',
200
- director: 'Hayao Miyazaki',
201
- producer: 'Hayao Miyazaki',
202
- release_date: 1989,
203
- rt_score: 96,
204
- },
205
- {
206
- id: '4e236f34-b981-41c3-8c65-f8c9000b94e7',
207
- title: 'Only Yesterday',
208
- director: 'Isao Takahata',
209
- producer: 'Toshio Suzuki',
210
- release_date: 1991,
211
- rt_score: 100,
212
- },
213
- {
214
- id: 'ebbb6b7c-945c-41ee-a792-de0e43191bd8',
215
- title: 'Porco Rosso',
216
- director: 'Hayao Miyazaki',
217
- producer: 'Toshio Suzuki',
218
- release_date: 1992,
219
- rt_score: 94,
220
- },
221
- ],
222
- [
223
- {
224
- id: '1b67aa9a-2e4a-45af-ac98-64d6ad15b16c',
225
- title: 'Pom Poko',
226
- director: 'Isao Takahata',
227
- producer: 'Toshio Suzuki',
228
- release_date: 1994,
229
- rt_score: 78,
230
- },
231
- ],
170
+ const DEFAULT_PAGE_SIZE = 3;
171
+ const DATA = [
172
+ {
173
+ id: '2baf70d1-42bb-4437-b551-e5fed5a87abe',
174
+ title: 'Castle in the Sky',
175
+ director: 'Hayao Miyazaki',
176
+ producer: 'Isao Takahata',
177
+ release_date: 1986,
178
+ rt_score: 95,
179
+ },
180
+ {
181
+ id: '12cfb892-aac0-4c5b-94af-521852e46d6a',
182
+ title: 'Grave of the Fireflies',
183
+ director: 'Isao Takahata',
184
+ producer: 'Toru Hara',
185
+ release_date: 1988,
186
+ rt_score: 97,
187
+ },
188
+ {
189
+ id: '58611129-2dbc-4a81-a72f-77ddfc1b1b49',
190
+ title: 'My Neighbor Totoro',
191
+ director: 'Hayao Miyazaki',
192
+ producer: 'Hayao Miyazaki',
193
+ release_date: 1988,
194
+ rt_score: 93,
195
+ },
196
+ {
197
+ id: 'ea660b10-85c4-4ae3-8a5f-41cea3648e3e',
198
+ title: 'Kiki\'s Delivery Service',
199
+ director: 'Hayao Miyazaki',
200
+ producer: 'Hayao Miyazaki',
201
+ release_date: 1989,
202
+ rt_score: 96,
203
+ },
204
+ {
205
+ id: '4e236f34-b981-41c3-8c65-f8c9000b94e7',
206
+ title: 'Only Yesterday',
207
+ director: 'Isao Takahata',
208
+ producer: 'Toshio Suzuki',
209
+ release_date: 1991,
210
+ rt_score: 100,
211
+ },
212
+ {
213
+ id: 'ebbb6b7c-945c-41ee-a792-de0e43191bd8',
214
+ title: 'Porco Rosso',
215
+ director: 'Hayao Miyazaki',
216
+ producer: 'Toshio Suzuki',
217
+ release_date: 1992,
218
+ rt_score: 94,
219
+ },
220
+ {
221
+ id: '1b67aa9a-2e4a-45af-ac98-64d6ad15b16c',
222
+ title: 'Pom Poko',
223
+ director: 'Isao Takahata',
224
+ producer: 'Toshio Suzuki',
225
+ release_date: 1994,
226
+ rt_score: 78,
227
+ },
232
228
  ];
233
- const [data, setData] = useState(PAGINATED_DATA[0]);
234
- const fetchData = useCallback(
235
- (args) => {
229
+
230
+ function paginateData(data, pageSize = DEFAULT_PAGE_SIZE) {
231
+ if (pageSize <= 0) {
232
+ throw new Error('Invalid page size');
233
+ }
234
+ const pages = [];
235
+ for (let i = 0; i < data.length; i += pageSize) {
236
+ pages.push(data.slice(i, i + pageSize));
237
+ }
238
+ return pages;
239
+ }
240
+
241
+ function filterData(data, filters) {
242
+ return data.filter((item) => filters.every(filter => (
243
+ String(item[filter.id]).toLowerCase().includes(String(filter.value).toLowerCase())
244
+ )));
245
+ }
246
+
247
+ const useDebouncedFetchData = (setData, setTotalItems, setTotalPages, setIsLoading) => useCallback(
248
+ debounce((args) => {
249
+ if (!args) { return; }
250
+ setIsLoading(true);
251
+
236
252
  setTimeout(() => {
237
- setData(PAGINATED_DATA[args.pageIndex]);
253
+ // Filter the data based on the current filters
254
+ const filteredData = filterData(DATA, args.filters);
255
+
256
+ // Paginate the filtered data
257
+ const paginatedData = paginateData(filteredData, args.pageSize);
258
+ const currentPageData = paginatedData[args.pageIndex] || [];
259
+
260
+ // Update the state with the paginated data, total items, and total pages
261
+ setData(currentPageData);
262
+ setTotalItems(filteredData.length);
263
+ setTotalPages(paginatedData.length);
264
+
265
+ setIsLoading(false);
238
266
  }, 1000);
239
- },
267
+ }, 300),
240
268
  [],
241
269
  );
242
270
 
@@ -247,12 +275,18 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
247
275
  disableSortBy: true,
248
276
  };
249
277
 
278
+ const [isLoading, setIsLoading] = useState(true);
279
+ const [data, setData] = useState([]);
280
+ const [totalItems, setTotalItems] = useState(0);
281
+ const [totalPages, setTotalPages] = useState(1);
282
+
283
+ const fetchData = useDebouncedFetchData(setData, setTotalItems, setTotalPages, setIsLoading);
284
+
250
285
  const DownloadCSVAction = ({ as: Component, selectedFlatRows, ...rest }) => (
251
286
  <Component onClick={() => console.log('Download CSV', selectedFlatRows, rest)}>
252
287
  Download CSV
253
288
  </Component>
254
289
  );
255
-
256
290
  const ClearAction = ({ as: Component, tableInstance }) => (
257
291
  <Component
258
292
  variant="danger"
@@ -264,9 +298,14 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
264
298
  Clear Selection
265
299
  </Component>
266
300
  );
267
-
301
+
268
302
  return (
269
303
  <DataTable
304
+ isLoading={isLoading}
305
+ bulkActions={[
306
+ <DownloadCSVAction />,
307
+ <ClearAction />,
308
+ ]}
270
309
  isSelectable
271
310
  manualSelectColumn={selectColumn}
272
311
  SelectionStatusComponent={DataTable.ControlledSelectionStatus}
@@ -275,17 +314,15 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
275
314
  defaultColumnValues={{ Filter: TextFilter }}
276
315
  isPaginated
277
316
  manualPagination
278
- isSortable
279
- manualSortBy
280
317
  initialState={{
281
318
  pageSize: 3,
282
- pageIndex: 0
319
+ pageIndex: 0,
283
320
  }}
284
321
  initialTableOptions={{
285
322
  getRowId: row => row.id,
286
323
  }}
287
- itemCount={7}
288
- pageCount={3}
324
+ itemCount={totalItems}
325
+ pageCount={totalPages}
289
326
  fetchData={fetchData}
290
327
  data={data}
291
328
  columns={[
@@ -302,10 +339,6 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
302
339
  accessor: 'release_date',
303
340
  },
304
341
  ]}
305
- bulkActions={[
306
- <DownloadCSVAction />,
307
- <ClearAction />,
308
- ]}
309
342
  />
310
343
  );
311
344
  }
@@ -1,6 +1,7 @@
1
- import { useContext } from 'react';
1
+ import { useContext, useEffect } from 'react';
2
2
  import DataTableContext from './DataTableContext';
3
- import { clearSelectionAction } from './selection/data/actions';
3
+ import { clearSelectionAction, setSelectedRowsAction, toggleIsEntireTableSelected } from './selection/data/actions';
4
+ import { getRowIds, getUnselectedPageRows } from './selection/data/helpers';
4
5
 
5
6
  export const useRows = () => {
6
7
  const {
@@ -39,3 +40,55 @@ export const useSelectionActions = (
39
40
  clearSelection,
40
41
  };
41
42
  };
43
+
44
+ /**
45
+ * Provides business logic around managing selection state, notably for controlled
46
+ * selections with API-driven data.
47
+ *
48
+ */
49
+ export const useDataTableSelections = ({
50
+ selections,
51
+ selectionsDispatch,
52
+ itemCount,
53
+ selectedRows,
54
+ page,
55
+ isAllPageRowsSelected,
56
+ }) => {
57
+ // If "Select all" is explicitly opted in by the user, ensure that all unselected rows are selected
58
+ // when the user navigates to a new page.
59
+ useEffect(
60
+ () => {
61
+ if (selections.isSelectAllEnabled && (itemCount > selectedRows.length || !isAllPageRowsSelected)) {
62
+ const selectedRowIds = getRowIds(selectedRows);
63
+ const unselectedPageRows = getUnselectedPageRows(selectedRowIds, page);
64
+ if (unselectedPageRows.length) {
65
+ selectionsDispatch(setSelectedRowsAction(unselectedPageRows, itemCount));
66
+ }
67
+ }
68
+ },
69
+ [selectedRows, itemCount, page, selectionsDispatch, selections.isSelectAllEnabled, isAllPageRowsSelected],
70
+ );
71
+
72
+ // When `selections.isSelectAllEnabled` is true, ensure `selections.isEntireTableSelected` is true.
73
+ useEffect(() => {
74
+ if (selections.isSelectAllEnabled && !selections.isEntireTableSelected) {
75
+ selectionsDispatch(toggleIsEntireTableSelected());
76
+ }
77
+ }, [selectionsDispatch, selections.isEntireTableSelected, selections.isSelectAllEnabled]);
78
+
79
+ // When `selections.isSelectAllEnabled` differs from `selections.isEntireTableSelected` and
80
+ // `isAllPageRowsSelected` matches `selections.isSelectAllEnabled`, toggle `selections.isEntireTableSelected`.
81
+ useEffect(() => {
82
+ if (!selections.isSelectAllEnabled && selections.isEntireTableSelected && !isAllPageRowsSelected) {
83
+ selectionsDispatch(toggleIsEntireTableSelected());
84
+ }
85
+ if (selections.isSelectAllEnabled && !selections.isEntireTableSelected && isAllPageRowsSelected) {
86
+ selectionsDispatch(toggleIsEntireTableSelected());
87
+ }
88
+ }, [
89
+ selectionsDispatch,
90
+ isAllPageRowsSelected,
91
+ selections.isEntireTableSelected,
92
+ selections.isSelectAllEnabled,
93
+ ]);
94
+ };
@@ -33,8 +33,10 @@ import DataTableLayout from './DataTableLayout';
33
33
  import ExpandAll from './ExpandAll';
34
34
  import ExpandRow from './ExpandRow';
35
35
 
36
- import { useSelectionActions } from './hooks';
37
- import selectionsReducer, { initialState as initialSelectionsState } from './selection/data/reducer';
36
+ import { useDataTableSelections, useSelectionActions } from './hooks';
37
+ import selectionsReducer, {
38
+ initialState as initialSelectionsState,
39
+ } from './selection/data/reducer';
38
40
 
39
41
  function DataTable({
40
42
  columns, data, defaultColumnValues, additionalColumns, isSelectable,
@@ -79,12 +81,13 @@ function DataTable({
79
81
  selectedRowIds: {},
80
82
  };
81
83
  }
82
- /* Note: We override the `toggleRowSelected` action from react-table
83
- because we need to preserve the order of the selected rows.
84
- While `selectedRowIds` is an object that contains the selected rows as key-value pairs,
85
- it does not maintain the order of selection. Therefore, we have added the `selectedRowsOrdered` property
86
- to keep track of the order in which the rows were selected.
87
- */
84
+ /**
85
+ * Note: We override the `toggleRowSelected` action from react-table
86
+ * because we need to preserve the order of the selected rows.
87
+ * while `selectedRowIds` is an object that contains the selected rows as key-value pairs,
88
+ * it does not maintain the order of selection. Therefore, we have added the `selectedRowsOrdered` property
89
+ * to keep track of the order in which the rows were selected.
90
+ */
88
91
  case 'toggleRowSelected': {
89
92
  const rowIndex = parseInt(action.id, 10);
90
93
  const { selectedRowsOrdered = [] } = previousState;
@@ -185,6 +188,15 @@ function DataTable({
185
188
 
186
189
  const selectionActions = useSelectionActions(instance, controlledTableSelections);
187
190
 
191
+ useDataTableSelections({
192
+ selections,
193
+ selectionsDispatch,
194
+ itemCount,
195
+ selectedRows,
196
+ page: instance.page,
197
+ isAllPageRowsSelected: instance.isAllPageRowsSelected,
198
+ });
199
+
188
200
  const enhancedInstance = {
189
201
  ...instance,
190
202
  manualFilters,
@@ -216,12 +228,12 @@ function DataTable({
216
228
  })}
217
229
  >
218
230
  {children || (
219
- <>
220
- <TableControlBar />
221
- <Table />
222
- <EmptyTableComponent content="No results found" />
223
- <TableFooter />
224
- </>
231
+ <>
232
+ <TableControlBar />
233
+ <Table />
234
+ <EmptyTableComponent content="No results found" />
235
+ <TableFooter />
236
+ </>
225
237
  )}
226
238
  </div>
227
239
  </DataTableLayout>
@@ -328,7 +340,7 @@ DataTable.propTypes = {
328
340
  Cell: PropTypes.oneOfType([PropTypes.elementType, PropTypes.node]),
329
341
  })),
330
342
  /** Function that will fetch table data. Called when page size, page index or filters change.
331
- * Meant to be used with manual filters and pagination */
343
+ * Meant to be used with manual filters and pagination */
332
344
  fetchData: PropTypes.func,
333
345
  /** Initial state passed to react-table's documentation https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/api/useTable.md */
334
346
  initialState: PropTypes.shape({
@@ -340,7 +352,7 @@ DataTable.propTypes = {
340
352
  selectedRowsOrdered: PropTypes.arrayOf(PropTypes.number),
341
353
  }),
342
354
  /** Table options passed to react-table's useTable hook. Will override some options passed in to DataTable, such
343
- as: data, columns, defaultColumn, manualFilters, manualPagination, manualSortBy, and initialState */
355
+ as: data, columns, defaultColumn, manualFilters, manualPagination, manualSortBy, and initialState */
344
356
  initialTableOptions: PropTypes.shape({}),
345
357
  /** Actions to be performed on the table. Called with the table instance. Not displayed if rows are selected. */
346
358
  itemCount: PropTypes.number.isRequired,
@@ -1,4 +1,4 @@
1
- import React, { useContext, useEffect } from 'react';
1
+ import React, { useContext } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
 
4
4
  import DataTableContext from '../DataTableContext';
@@ -7,34 +7,19 @@ import BaseSelectionStatus from './BaseSelectionStatus';
7
7
  import {
8
8
  clearSelectionAction,
9
9
  setSelectAllRowsAllPagesAction,
10
- setSelectedRowsAction,
11
10
  } from './data/actions';
12
- import {
13
- getUnselectedPageRows,
14
- getRowIds,
15
- } from './data/helpers';
16
11
 
17
12
  function ControlledSelectionStatus({ className, clearSelectionText }) {
18
13
  const {
19
14
  itemCount,
20
15
  page,
21
- controlledTableSelections: [{ selectedRows, isEntireTableSelected }, dispatch],
16
+ controlledTableSelections: [
17
+ { selectedRows, isSelectAllEnabled },
18
+ selectionsDispatch,
19
+ ],
22
20
  } = useContext(DataTableContext);
23
21
 
24
- useEffect(
25
- () => {
26
- if (isEntireTableSelected) {
27
- const selectedRowIds = getRowIds(selectedRows);
28
- const unselectedPageRows = getUnselectedPageRows(selectedRowIds, page);
29
- if (unselectedPageRows.length) {
30
- dispatch(setSelectedRowsAction(unselectedPageRows, itemCount));
31
- }
32
- }
33
- },
34
- [isEntireTableSelected, selectedRows, itemCount, page, dispatch],
35
- );
36
-
37
- const numSelectedRows = isEntireTableSelected ? itemCount : selectedRows.length;
22
+ const numSelectedRows = itemCount === selectedRows.length || isSelectAllEnabled ? itemCount : selectedRows.length;
38
23
  const numSelectedRowsOnPage = (page || []).filter(r => r.isSelected).length;
39
24
 
40
25
  const selectionStatusProps = {
@@ -42,8 +27,8 @@ function ControlledSelectionStatus({ className, clearSelectionText }) {
42
27
  numSelectedRows,
43
28
  numSelectedRowsOnPage,
44
29
  clearSelectionText,
45
- onSelectAll: () => dispatch(setSelectAllRowsAllPagesAction()),
46
- onClear: () => dispatch(clearSelectionAction()),
30
+ onSelectAll: () => selectionsDispatch(setSelectAllRowsAllPagesAction()),
31
+ onClear: () => selectionsDispatch(clearSelectionAction()),
47
32
  };
48
33
  return <BaseSelectionStatus {...selectionStatusProps} />;
49
34
  }
@@ -33,3 +33,8 @@ export const clearPageSelectionAction = (rowIds) => ({
33
33
  type: CLEAR_PAGE_SELECTION,
34
34
  rowIds,
35
35
  });
36
+
37
+ export const TOGGLE_IS_ENTIRE_TABLE_SELECTED = 'TOGGLE IS ENTIRE TABLE SELECTED';
38
+ export const toggleIsEntireTableSelected = () => ({
39
+ type: TOGGLE_IS_ENTIRE_TABLE_SELECTED,
40
+ });
@@ -6,11 +6,13 @@ import {
6
6
  CLEAR_SELECTION,
7
7
  CLEAR_PAGE_SELECTION,
8
8
  SET_SELECT_ALL_ROWS_ALL_PAGES,
9
+ TOGGLE_IS_ENTIRE_TABLE_SELECTED,
9
10
  } from './actions';
10
11
 
11
12
  export const initialState = {
12
13
  selectedRows: [],
13
14
  isEntireTableSelected: false,
15
+ isSelectAllEnabled: false,
14
16
  };
15
17
 
16
18
  const selectionsReducer = (state = initialState, action = {}) => {
@@ -21,7 +23,7 @@ const selectionsReducer = (state = initialState, action = {}) => {
21
23
  ...state,
22
24
  selectedRows,
23
25
  };
24
- if (selectedRows.length === action.itemCount) {
26
+ if (state.isSelectAllEnabled || selectedRows.length === action.itemCount) {
25
27
  updatedState.isEntireTableSelected = true;
26
28
  }
27
29
  return updatedState;
@@ -30,16 +32,19 @@ const selectionsReducer = (state = initialState, action = {}) => {
30
32
  return {
31
33
  ...state,
32
34
  isEntireTableSelected: true,
35
+ isSelectAllEnabled: true,
33
36
  };
34
37
  case DELETE_ROW:
35
38
  return {
36
39
  selectedRows: state.selectedRows.filter((row) => row.id !== action.rowId),
37
40
  isEntireTableSelected: false,
41
+ isSelectAllEnabled: false,
38
42
  };
39
43
  case ADD_ROW: {
40
44
  const selectedRows = uniqBy([...state.selectedRows, action.row], row => row.id);
41
45
  const isEntireTableSelected = selectedRows.length === action.itemCount;
42
46
  return {
47
+ ...state,
43
48
  selectedRows,
44
49
  isEntireTableSelected,
45
50
  };
@@ -50,6 +55,12 @@ const selectionsReducer = (state = initialState, action = {}) => {
50
55
  return {
51
56
  isEntireTableSelected: false,
52
57
  selectedRows: state.selectedRows.filter(row => !action.rowIds.includes(row.id)),
58
+ isSelectAllEnabled: false,
59
+ };
60
+ case TOGGLE_IS_ENTIRE_TABLE_SELECTED:
61
+ return {
62
+ ...state,
63
+ isEntireTableSelected: !state.isEntireTableSelected,
53
64
  };
54
65
  default:
55
66
  return state;
@@ -4,7 +4,10 @@ import { IntlProvider } from 'react-intl';
4
4
  import userEvent from '@testing-library/user-event';
5
5
 
6
6
  import ControlledSelectionStatus from '../ControlledSelectionStatus';
7
- import { clearSelectionAction, setSelectAllRowsAllPagesAction, setSelectedRowsAction } from '../data/actions';
7
+ import {
8
+ clearSelectionAction,
9
+ setSelectAllRowsAllPagesAction,
10
+ } from '../data/actions';
8
11
  import DataTableContext from '../../DataTableContext';
9
12
  import {
10
13
  SELECT_ALL_TEST_ID,
@@ -88,28 +91,6 @@ describe('<ControlledSelectionStatus />', () => {
88
91
  const selectAllButton = screen.queryByTestId(SELECT_ALL_TEST_ID);
89
92
  expect(selectAllButton).not.toBeInTheDocument();
90
93
  });
91
-
92
- it('selects any unselected page rows', () => {
93
- const selectedRows = Array(instance.itemCount).map((item, index) => ({ id: index + 1 }));
94
- const dispatchSpy = jest.fn();
95
- render(
96
- <ControlledSelectionStatusWrapper
97
- value={{
98
- ...instance,
99
- controlledTableSelections: [
100
- {
101
- selectedRows,
102
- isEntireTableSelected: true,
103
- },
104
- dispatchSpy,
105
- ],
106
- }}
107
- />,
108
- );
109
- expect(dispatchSpy).toHaveBeenCalledTimes(1);
110
- const action = setSelectedRowsAction(instance.page, instance.itemCount);
111
- expect(dispatchSpy).toHaveBeenCalledWith(action);
112
- });
113
94
  });
114
95
 
115
96
  describe('individual rows selected', () => {