@openedx/paragon 22.15.3 → 22.16.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 (37) 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/Toast/ToastContainer.d.ts +6 -0
  10. package/dist/Toast/ToastContainer.js +19 -29
  11. package/dist/Toast/ToastContainer.js.map +1 -1
  12. package/dist/Toast/ToastContainer.scss +2 -1
  13. package/dist/Toast/index.d.ts +59 -0
  14. package/dist/Toast/index.js.map +1 -1
  15. package/dist/Toast/index.scss +5 -4
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/paragon.css +1 -1
  19. package/package.json +1 -1
  20. package/src/DataTable/README.md +111 -78
  21. package/src/DataTable/hooks.jsx +55 -2
  22. package/src/DataTable/index.jsx +28 -16
  23. package/src/DataTable/selection/ControlledSelectionStatus.jsx +8 -23
  24. package/src/DataTable/selection/data/actions.js +5 -0
  25. package/src/DataTable/selection/data/reducer.js +12 -1
  26. package/src/DataTable/selection/tests/ControlledSelectionStatus.test.jsx +4 -23
  27. package/src/DataTable/selection/tests/reducer.test.js +4 -0
  28. package/src/DataTable/tests/DataTable.test.jsx +99 -3
  29. package/src/Toast/README.md +4 -4
  30. package/src/Toast/{Toast.test.jsx → Toast.test.tsx} +23 -13
  31. package/src/Toast/ToastContainer.scss +2 -1
  32. package/src/Toast/ToastContainer.tsx +30 -0
  33. package/src/Toast/index.scss +5 -4
  34. package/src/Toast/{index.jsx → index.tsx} +27 -6
  35. package/src/index.d.ts +1 -1
  36. package/src/index.js +1 -1
  37. package/src/Toast/ToastContainer.jsx +0 -40
@@ -1,5 +1,5 @@
1
1
  import React, { useContext } from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import * as reactTable from 'react-table';
5
5
  import { IntlProvider } from 'react-intl';
@@ -212,7 +212,6 @@ describe('<DataTable />', () => {
212
212
  pageCount: 3,
213
213
  fetchData: jest.fn(),
214
214
  };
215
-
216
215
  render(<DataTableWrapper {...propsWithSelection} />);
217
216
  const filtersButton = screen.getByRole('button', { name: 'Filters' });
218
217
 
@@ -228,6 +227,10 @@ describe('<DataTable />', () => {
228
227
  // A filtered array is returned from the backend,
229
228
  // and the element counter displays its length.
230
229
  expect(selectAllButton).toHaveTextContent('Select all 7');
230
+
231
+ await userEvent.click(selectAllButton);
232
+
233
+ expect(screen.getByText('All 7 selected')).toBeInTheDocument();
231
234
  });
232
235
 
233
236
  describe('[legacy] controlled table selections', () => {
@@ -242,7 +245,7 @@ describe('<DataTable />', () => {
242
245
 
243
246
  const contextValue = JSON.parse(contextDiv.getAttribute('data-contextvalue'));
244
247
  expect(contextValue.controlledTableSelections).toEqual([
245
- { selectedRows: [], isEntireTableSelected: false },
248
+ { selectedRows: [], isEntireTableSelected: false, isSelectAllEnabled: false },
246
249
  null,
247
250
  ]);
248
251
  });
@@ -312,5 +315,98 @@ describe('<DataTable />', () => {
312
315
  expect(mockOnSelectedRowsChange).toHaveBeenCalledTimes(1);
313
316
  expect(mockOnSelectedRowsChange).toHaveBeenCalledWith({});
314
317
  });
318
+ it('Selects all rows across all pages with ControlledSelectionStatus component', async () => {
319
+ const selectColumn = {
320
+ id: 'selection',
321
+ Header: DataTable.ControlledSelectHeader,
322
+ Cell: DataTable.ControlledSelect,
323
+ disableSortBy: true,
324
+ };
325
+ const propsWithSelection = {
326
+ ...props,
327
+ isPaginated: true,
328
+ isSelectable: true,
329
+ manualSelectColumn: selectColumn,
330
+ SelectionStatusComponent: DataTable.ControlledSelectionStatus,
331
+ initialState: {
332
+ pageIndex: 0,
333
+ pageSize: 2,
334
+ selectedRowIds: {
335
+ 1: true,
336
+ },
337
+ },
338
+ };
339
+ render(<DataTableWrapper {...propsWithSelection} />);
340
+ const selectAllButton = screen.getByTestId('test_selection_state_select_all_button');
341
+
342
+ await userEvent.click(selectAllButton);
343
+
344
+ expect(screen.getByText('All 7 selected')).toBeInTheDocument();
345
+ });
346
+
347
+ it('Select all item rows with ControlledSelectionStatus Component', async () => {
348
+ const selectColumn = {
349
+ id: 'selection',
350
+ Header: DataTable.ControlledSelectHeader,
351
+ Cell: DataTable.ControlledSelect,
352
+ disableSortBy: true,
353
+ };
354
+ const propsWithSelection = {
355
+ ...props,
356
+ isPaginated: true,
357
+ isSelectable: true,
358
+ manualSelectColumn: selectColumn,
359
+ SelectionStatusComponent: DataTable.ControlledSelectionStatus,
360
+ initialState: {
361
+ pageIndex: 0,
362
+ pageSize: 10,
363
+ },
364
+ };
365
+ render(<DataTableWrapper {...propsWithSelection} />);
366
+ const selectAllRowsButton = screen.getByTitle('Toggle All Current Page Rows Selected');
367
+
368
+ await userEvent.click(selectAllRowsButton);
369
+
370
+ expect(screen.getByText('All 7 selected')).toBeInTheDocument();
371
+ });
372
+
373
+ it('Select all item rows individually with ControlledSelectionStatus Component', async () => {
374
+ const selectColumn = {
375
+ id: 'selection',
376
+ Header: DataTable.ControlledSelectHeader,
377
+ Cell: DataTable.ControlledSelect,
378
+ disableSortBy: true,
379
+ };
380
+ const propsWithSelection = {
381
+ ...props,
382
+ isPaginated: true,
383
+ isSelectable: true,
384
+ manualSelectColumn: selectColumn,
385
+ SelectionStatusComponent: DataTable.ControlledSelectionStatus,
386
+ initialState: {
387
+ pageIndex: 0,
388
+ pageSize: 4,
389
+ },
390
+ };
391
+ render(<DataTableWrapper {...propsWithSelection} />);
392
+
393
+ const selectAllRows = () => {
394
+ const selectRowsButtons = screen.getAllByTitle('Toggle Row Selected');
395
+ selectRowsButtons.forEach(async (button) => {
396
+ await userEvent.click(button);
397
+ });
398
+ };
399
+ // Select all page 1
400
+ selectAllRows();
401
+
402
+ // Paginate to page 2
403
+ const nextPageButton2 = screen.getByLabelText('Next, Page 2');
404
+ await userEvent.click(nextPageButton2);
405
+
406
+ // Select all page 2
407
+ selectAllRows();
408
+
409
+ await waitFor(() => expect(screen.getByText('All 7 selected')).toBeInTheDocument());
410
+ });
315
411
  });
316
412
  });
@@ -5,7 +5,7 @@ components:
5
5
  - Toast
6
6
  categories:
7
7
  - Overlays
8
- status: 'New'
8
+ status: 'Stable'
9
9
  designStatus: 'Done'
10
10
  devStatus: 'Done'
11
11
  notes: ''
@@ -39,7 +39,7 @@ notes: ''
39
39
  Example of a basic Toast.
40
40
  </Toast>
41
41
 
42
- <Button variant="primary" onClick={() => setShow(true)}>Show Toast</Button>
42
+ <Button onClick={() => setShow(true)}>Show Toast</Button>
43
43
  </>
44
44
  );
45
45
  }
@@ -64,7 +64,7 @@ notes: ''
64
64
  Success! Example of a Toast with a button.
65
65
  </Toast>
66
66
 
67
- <Button variant="primary" onClick={() => setShow(true)}>Show Toast</Button>
67
+ <Button onClick={() => setShow(true)}>Show Toast</Button>
68
68
  </>
69
69
  );
70
70
  }
@@ -89,7 +89,7 @@ notes: ''
89
89
  Success! Example of a Toast with a link.
90
90
  </Toast>
91
91
 
92
- <Button variant="primary" onClick={() => setShow(true)}>Show Toast</Button>
92
+ <Button onClick={() => setShow(true)}>Show Toast</Button>
93
93
  </>
94
94
  );
95
95
  }
@@ -1,12 +1,10 @@
1
- import React from 'react';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { IntlProvider } from 'react-intl';
4
3
  import userEvent from '@testing-library/user-event';
5
4
 
6
5
  import Toast from '.';
7
6
 
8
- /* eslint-disable-next-line react/prop-types */
9
- function ToastWrapper({ children, ...props }) {
7
+ function ToastWrapper({ children, ...props }: React.ComponentProps<typeof Toast>) {
10
8
  return (
11
9
  <IntlProvider locale="en">
12
10
  <Toast {...props}>
@@ -17,7 +15,7 @@ function ToastWrapper({ children, ...props }) {
17
15
  }
18
16
 
19
17
  describe('<Toast />', () => {
20
- const onCloseHandler = () => {};
18
+ const onCloseHandler = jest.fn();
21
19
  const props = {
22
20
  onClose: onCloseHandler,
23
21
  show: true,
@@ -44,7 +42,7 @@ describe('<Toast />', () => {
44
42
  {...props}
45
43
  action={{
46
44
  label: 'Optional action',
47
- onClick: () => {},
45
+ onClick: jest.fn(),
48
46
  }}
49
47
  >
50
48
  Success message.
@@ -55,38 +53,50 @@ describe('<Toast />', () => {
55
53
  });
56
54
  it('autohide is set to false on onMouseOver and true on onMouseLeave', async () => {
57
55
  render(
58
- <ToastWrapper data-testid="toast" {...props}>
56
+ <ToastWrapper {...props}>
59
57
  Success message.
60
58
  </ToastWrapper>,
61
59
  );
62
- const toast = screen.getByTestId('toast');
60
+ const toast = screen.getByRole('alert');
63
61
  await userEvent.hover(toast);
64
62
  setTimeout(() => {
65
- expect(screen.getByText('Success message.')).toEqual(true);
63
+ expect(screen.getByText('Success message.')).toBeTruthy();
66
64
  expect(toast).toHaveLength(1);
67
65
  }, 6000);
68
66
  await userEvent.unhover(toast);
69
67
  setTimeout(() => {
70
- expect(screen.getByText('Success message.')).toEqual(false);
68
+ expect(screen.getByText('Success message.')).toBeTruthy();
71
69
  expect(toast).toHaveLength(1);
72
70
  }, 6000);
73
71
  });
74
72
  it('autohide is set to false onFocus and true onBlur', async () => {
75
73
  render(
76
- <ToastWrapper data-testid="toast" {...props}>
74
+ <ToastWrapper {...props}>
77
75
  Success message.
78
76
  </ToastWrapper>,
79
77
  );
80
- const toast = screen.getByTestId('toast');
78
+ const toast = screen.getByRole('alert');
81
79
  toast.focus();
82
80
  setTimeout(() => {
83
- expect(screen.getByText('Success message.')).toEqual(true);
81
+ expect(screen.getByText('Success message.')).toBeTruthy();
84
82
  expect(toast).toHaveLength(1);
85
83
  }, 6000);
86
84
  await userEvent.tab();
87
85
  setTimeout(() => {
88
- expect(screen.getByText('Success message.')).toEqual(false);
86
+ expect(screen.getByText('Success message.')).toBeTruthy();
89
87
  expect(toast).toHaveLength(1);
90
88
  }, 6000);
91
89
  });
90
+ it('should contain aria-atomic and aria-live', async () => {
91
+ render(
92
+ <ToastWrapper {...props}>
93
+ Success message.
94
+ </ToastWrapper>,
95
+ );
96
+
97
+ const toast = screen.getByRole('alert');
98
+
99
+ expect(toast).toHaveAttribute('aria-atomic', 'true');
100
+ expect(toast).toHaveAttribute('aria-live', 'assertive');
101
+ });
92
102
  });
@@ -1,3 +1,4 @@
1
+ @use "sass:map";
1
2
  @import "variables";
2
3
 
3
4
  .toast-container {
@@ -11,7 +12,7 @@
11
12
  left: 0;
12
13
  }
13
14
 
14
- @media only screen and (width <= 768px) {
15
+ @media (max-width: map.get($grid-breakpoints, "md")) {
15
16
  bottom: $toast-container-gutter-sm;
16
17
  right: $toast-container-gutter-sm;
17
18
  left: $toast-container-gutter-sm;
@@ -0,0 +1,30 @@
1
+ import { ReactNode, useEffect, useState } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ interface ToastContainerProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ const TOAST_ROOT_ID = 'toast-root';
9
+
10
+ function ToastContainer({ children }: ToastContainerProps) {
11
+ const [rootElement, setRootElement] = useState<HTMLElement | null>(null);
12
+
13
+ useEffect(() => {
14
+ if (typeof document !== 'undefined') {
15
+ let existingElement = document.getElementById(TOAST_ROOT_ID);
16
+
17
+ if (!existingElement) {
18
+ existingElement = document.createElement('div');
19
+ existingElement.id = TOAST_ROOT_ID;
20
+ existingElement.className = 'toast-container';
21
+ document.body.appendChild(existingElement);
22
+ }
23
+ setRootElement(existingElement);
24
+ }
25
+ }, []);
26
+
27
+ return rootElement ? ReactDOM.createPortal(children, rootElement) : null;
28
+ }
29
+
30
+ export default ToastContainer;
@@ -1,3 +1,4 @@
1
+ @use "sass:map";
1
2
  @import "variables";
2
3
  @import "~bootstrap/scss/toasts";
3
4
 
@@ -5,7 +6,7 @@
5
6
  background-color: $toast-background-color;
6
7
  box-shadow: $toast-box-shadow;
7
8
  margin: 0;
8
- padding: 1rem;
9
+ padding: $spacer;
9
10
  position: relative;
10
11
  border-radius: $toast-border-radius;
11
12
  z-index: 2;
@@ -38,15 +39,15 @@
38
39
  }
39
40
 
40
41
  & + .btn {
41
- margin-top: 1rem;
42
+ margin-top: $spacer;
42
43
  }
43
44
  }
44
45
 
45
- @media only screen and (width <= 768px) {
46
+ @media (max-width: map.get($grid-breakpoints, "md")) {
46
47
  max-width: 100%;
47
48
  }
48
49
 
49
- @media only screen and (width >= 768px) {
50
+ @media (min-width: map.get($grid-breakpoints, "md")) {
50
51
  min-width: $toast-max-width;
51
52
  max-width: $toast-max-width;
52
53
  }
@@ -1,7 +1,6 @@
1
1
  import React, { useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import PropTypes from 'prop-types';
4
-
5
4
  import BaseToast from 'react-bootstrap/Toast';
6
5
  import { useIntl } from 'react-intl';
7
6
 
@@ -14,16 +13,40 @@ import IconButton from '../IconButton';
14
13
  export const TOAST_CLOSE_LABEL_TEXT = 'Close';
15
14
  export const TOAST_DELAY = 5000;
16
15
 
16
+ interface ToastAction {
17
+ label: string;
18
+ href?: string;
19
+ onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
20
+ }
21
+
22
+ interface ToastProps {
23
+ children: string;
24
+ onClose: () => void;
25
+ show: boolean;
26
+ action?: ToastAction;
27
+ closeLabel?: string;
28
+ delay?: number;
29
+ className?: string;
30
+ }
31
+
17
32
  function Toast({
18
- action, children, className, closeLabel, onClose, show, ...rest
19
- }) {
33
+ action,
34
+ children,
35
+ className,
36
+ closeLabel,
37
+ onClose,
38
+ show,
39
+ ...rest
40
+ }: ToastProps) {
20
41
  const intl = useIntl();
21
42
  const [autoHide, setAutoHide] = useState(true);
43
+
22
44
  const intlCloseLabel = closeLabel || intl.formatMessage({
23
45
  id: 'pgn.Toast.closeLabel',
24
46
  defaultMessage: 'Close',
25
47
  description: 'Close label for Toast component',
26
48
  });
49
+
27
50
  return (
28
51
  <ToastContainer>
29
52
  <BaseToast
@@ -37,9 +60,7 @@ function Toast({
37
60
  show={show}
38
61
  {...rest}
39
62
  >
40
- <div
41
- className="toast-header"
42
- >
63
+ <div className="toast-header">
43
64
  <p className="small">{children}</p>
44
65
  <div className="toast-header-btn-container">
45
66
  <IconButton
package/src/index.d.ts CHANGED
@@ -39,6 +39,7 @@ export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalD
39
39
  export { default as ModalLayer } from './Modal/ModalLayer';
40
40
  export { default as Overlay, OverlayTrigger } from './Overlay';
41
41
  export { default as Portal } from './Modal/Portal';
42
+ export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
42
43
  export { default as Tooltip } from './Tooltip';
43
44
  export { default as useWindowSize, type WindowSizeData } from './hooks/useWindowSizeHook';
44
45
  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/src/index.js CHANGED
@@ -39,6 +39,7 @@ export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalD
39
39
  export { default as ModalLayer } from './Modal/ModalLayer';
40
40
  export { default as Overlay, OverlayTrigger } from './Overlay';
41
41
  export { default as Portal } from './Modal/Portal';
42
+ export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
42
43
  export { default as Tooltip } from './Tooltip';
43
44
  export { default as useWindowSize } from './hooks/useWindowSizeHook';
44
45
  export { default as useToggle } from './hooks/useToggleHook';
@@ -163,7 +164,6 @@ export {
163
164
  } from './Tabs';
164
165
  /** @deprecated Replaced by `Form.Control`. */
165
166
  export { default as TextArea } from './TextArea';
166
- export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
167
167
  /** @deprecated Replaced by `Form.Group`. */
168
168
  export { default as ValidationFormGroup } from './ValidationFormGroup';
169
169
  export { default as TransitionReplace } from './TransitionReplace';
@@ -1,40 +0,0 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom';
3
- import PropTypes from 'prop-types';
4
-
5
- class ToastContainer extends React.Component {
6
- constructor(props) {
7
- super(props);
8
- this.toastRootName = 'toast-root';
9
- if (typeof document === 'undefined') {
10
- this.rootElement = null;
11
- } else if (document.getElementById(this.toastRootName)) {
12
- this.rootElement = document.getElementById(this.toastRootName);
13
- } else {
14
- const rootElement = document.createElement('div');
15
- rootElement.setAttribute('id', this.toastRootName);
16
- rootElement.setAttribute('class', 'toast-container');
17
- rootElement.setAttribute('role', 'alert');
18
- rootElement.setAttribute('aria-live', 'polite');
19
- rootElement.setAttribute('aria-atomic', 'true');
20
- this.rootElement = document.body.appendChild(rootElement);
21
- }
22
- }
23
-
24
- render() {
25
- if (this.rootElement) {
26
- return ReactDOM.createPortal(
27
- this.props.children,
28
- this.rootElement,
29
- );
30
- }
31
- return null;
32
- }
33
- }
34
-
35
- ToastContainer.propTypes = {
36
- /** Specifies contents of the component. */
37
- children: PropTypes.node.isRequired,
38
- };
39
-
40
- export default ToastContainer;