@openedx/paragon 23.14.0 → 23.14.2

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 (44) hide show
  1. package/README.md +80 -14
  2. package/dist/Alert/index.d.ts +20 -0
  3. package/dist/Alert/index.js +21 -80
  4. package/dist/Alert/index.js.map +1 -1
  5. package/dist/Bubble/index.d.ts +6 -2
  6. package/dist/Bubble/index.js +4 -26
  7. package/dist/Bubble/index.js.map +1 -1
  8. package/dist/DataTable/DropdownFilters.js +7 -2
  9. package/dist/DataTable/DropdownFilters.js.map +1 -1
  10. package/dist/DataTable/index.js +8 -1
  11. package/dist/DataTable/index.js.map +1 -1
  12. package/dist/DataTable/messages.js +5 -0
  13. package/dist/Modal/ModalDialog.d.ts +6 -65
  14. package/dist/Modal/ModalDialog.js +0 -64
  15. package/dist/Modal/ModalDialog.js.map +1 -1
  16. package/dist/Modal/ModalLayer.d.ts +0 -25
  17. package/dist/Modal/ModalLayer.js +0 -19
  18. package/dist/Modal/ModalLayer.js.map +1 -1
  19. package/dist/ProductTour/index.js +8 -2
  20. package/dist/ProductTour/index.js.map +1 -1
  21. package/dist/Spinner/index.d.ts +11 -0
  22. package/dist/Spinner/index.js +1 -11
  23. package/dist/Spinner/index.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -2
  26. package/dist/index.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/Alert/Alert.test.tsx +14 -1
  29. package/src/Alert/index.tsx +42 -93
  30. package/src/Bubble/index.tsx +12 -32
  31. package/src/DataTable/DropdownFilters.jsx +9 -2
  32. package/src/DataTable/README.md +19 -18
  33. package/src/DataTable/index.jsx +6 -1
  34. package/src/DataTable/messages.js +5 -0
  35. package/src/DataTable/tests/DataTable.test.jsx +51 -1
  36. package/src/DataTable/tests/DropdownFilters.test.jsx +11 -7
  37. package/src/Modal/ModalDialog.tsx +6 -66
  38. package/src/Modal/ModalLayer.tsx +0 -22
  39. package/src/ProductTour/ProductTour.test.jsx +40 -2
  40. package/src/ProductTour/index.jsx +8 -2
  41. package/src/Spinner/{index.jsx → index.tsx} +10 -15
  42. package/src/index.ts +1 -2
  43. /package/src/Spinner/{Spinner.test.jsx → Spinner.test.tsx} +0 -0
  44. /package/src/Spinner/__snapshots__/{Spinner.test.jsx.snap → Spinner.test.tsx.snap} +0 -0
@@ -1,13 +1,18 @@
1
1
  import React, { useContext, useMemo } from 'react';
2
+ import { useIntl } from 'react-intl';
2
3
  import DataTableContext from './DataTableContext';
3
4
  import { DropdownButton } from '../Dropdown';
4
5
  import useWindowSize from '../hooks/useWindowSizeHook';
5
6
  import breakpoints from '../utils/breakpoints';
7
+ import messages from './messages';
6
8
 
7
9
  /** The first filter will be as an input, additional filters will be available in a dropdown. */
8
10
  function DropdownFilters() {
11
+ const intl = useIntl();
9
12
  const { width } = useWindowSize();
10
- const { columns, numBreakoutFilters } = useContext(DataTableContext);
13
+ const {
14
+ columns, numBreakoutFilters, filtersTitle,
15
+ } = useContext(DataTableContext);
11
16
 
12
17
  const [breakoutFilters, otherFilters] = useMemo(() => {
13
18
  if (!columns) {
@@ -25,6 +30,8 @@ function DropdownFilters() {
25
30
  return [boFilters, dropdownFilters];
26
31
  }, [columns, width, numBreakoutFilters]);
27
32
 
33
+ const dropdownTitle = filtersTitle || intl.formatMessage(messages.filtersDropdownTitle);
34
+
28
35
  return (
29
36
  <div className="pgn__data-table-filters">
30
37
  {breakoutFilters.length > 0 && breakoutFilters.map((column) => (
@@ -33,7 +40,7 @@ function DropdownFilters() {
33
40
  </div>
34
41
  ))}
35
42
  {otherFilters.length > 0 && (
36
- <DropdownButton variant="outline-primary" id="table-filters-dropdown" title="Filters">
43
+ <DropdownButton variant="outline-primary" id="table-filters-dropdown" title={dropdownTitle}>
37
44
  {otherFilters.map((column) => (
38
45
  <div
39
46
  key={column.Header}
@@ -176,7 +176,7 @@ To enable proper selection behavior with backend pagination (i.e., when ``isSele
176
176
  producer: 'Isao Takahata',
177
177
  release_date: 1986,
178
178
  rt_score: 95,
179
- },
179
+ },
180
180
  {
181
181
  id: '12cfb892-aac0-4c5b-94af-521852e46d6a',
182
182
  title: 'Grave of the Fireflies',
@@ -567,7 +567,7 @@ Can be used to show the loading state when ``DataTable`` is asynchronously fetch
567
567
  },
568
568
  ];
569
569
  {/* end example state */}
570
-
570
+
571
571
  return (
572
572
  <>
573
573
  {/* start example form block */}
@@ -634,7 +634,7 @@ You can pass a function to render custom components for bulk actions and table a
634
634
  Enroll
635
635
  </Button>
636
636
  );
637
-
637
+
638
638
  const EnrollAction = ({ selectedFlatRows, ...rest }) => (
639
639
  // Here is access to the selectedFlatRows, isEntireTableSelected, tableInstance
640
640
  <Button variant="danger" onClick={() => console.log('Enroll', selectedFlatRows, rest)}>
@@ -647,13 +647,13 @@ You can pass a function to render custom components for bulk actions and table a
647
647
  Assign
648
648
  </Button>
649
649
  );
650
-
650
+
651
651
  const ExtraAction = ({ text, selectedFlatRows, ...rest }) => (
652
652
  <Button onClick={() => console.log(`Extra Action ${text}`, selectedFlatRows, rest)}>
653
653
  {`Extra Action ${text}`}
654
654
  </Button>
655
655
  );
656
-
656
+
657
657
  return (
658
658
  <DataTable
659
659
  isSelectable
@@ -734,7 +734,7 @@ You can pass a function to render custom components for bulk actions and table a
734
734
  </DataTable>
735
735
  )
736
736
  }
737
-
737
+
738
738
  ```
739
739
 
740
740
  #### Actions with Data view toggle enabled
@@ -889,7 +889,7 @@ a responsive grid of cards.
889
889
  Clear Selection
890
890
  </Component>
891
891
  );
892
-
892
+
893
893
  return (
894
894
  <DataTable
895
895
  isFilterable
@@ -1103,7 +1103,7 @@ Use `columnSizes` prop of `CardView` component to define how many `Cards` are sh
1103
1103
 
1104
1104
  const ExampleCard = ({ className, original }) => {
1105
1105
  const { name, color, famous_for: famousFor } = original;
1106
-
1106
+
1107
1107
  return (
1108
1108
  <Card className={className}>
1109
1109
  <Card.ImageCap src="https://picsum.photos/360/200/" srcAlt="Card image" />
@@ -1195,7 +1195,7 @@ You can also display `Cards` with horizontal view. If the table is selectable co
1195
1195
  <Card.Header title={name} />
1196
1196
  <Card.Section>
1197
1197
  <dl>
1198
- <dt>Color</dt>
1198
+ <dt>Color</dt>
1199
1199
  <dd>{color}</dd>
1200
1200
  <dt>Famous For</dt>
1201
1201
  <dd>{famousFor}</dd>
@@ -1217,7 +1217,7 @@ You can also display `Cards` with horizontal view. If the table is selectable co
1217
1217
  name: 'Lil Bub',
1218
1218
  color: 'brown tabby',
1219
1219
  famous_for: 'weird tongue',
1220
- },
1220
+ },
1221
1221
  {
1222
1222
  name: 'Grumpy Cat',
1223
1223
  color: 'siamese',
@@ -1258,6 +1258,7 @@ For a more desktop friendly view, you can move filters into a sidebar by providi
1258
1258
  ```jsx live
1259
1259
  <DataTable
1260
1260
  showFiltersInSidebar
1261
+ filtersTitle="Color filters"
1261
1262
  isFilterable
1262
1263
  isSortable
1263
1264
  defaultColumnValues={{ Filter: TextFilter }}
@@ -1330,17 +1331,17 @@ For a more desktop friendly view, you can move filters into a sidebar by providi
1330
1331
  ```
1331
1332
 
1332
1333
  ## Expandable rows
1333
- `DataTable` supports expandable rows which once expanded render additional content under the row. Displayed content
1334
+ `DataTable` supports expandable rows which once expanded render additional content under the row. Displayed content
1334
1335
  is controlled by the `renderRowSubComponent` prop, which is a function that receives `row` as its single prop and renders expanded view, you also
1335
1336
  need to pass `isEpandable` prop to `DataTable` to indicate that it should support expand behavior for rows.
1336
- Finally, an additional column is required to be included into `columns` prop which will contain handlers for expand / collapse behavior, see examples below.
1337
+ Finally, an additional column is required to be included into `columns` prop which will contain handlers for expand / collapse behavior, see examples below.
1337
1338
 
1338
1339
  ### Default view
1339
1340
 
1340
1341
  Here we use default expander column offered by Paragon and for each row render value of the `name` attribute as its subcomponent.
1341
1342
 
1342
1343
  ```jsx live
1343
- <DataTable
1344
+ <DataTable
1344
1345
  isExpandable
1345
1346
  itemCount={7}
1346
1347
  renderRowSubComponent={({ row }) => <div className='text-center'>{row.values.name}</div>}
@@ -1457,10 +1458,10 @@ You can create your own custom expander column and use it, see code example belo
1457
1458
  </DataTable>
1458
1459
  </div>
1459
1460
  )
1460
-
1461
+
1461
1462
  return (
1462
1463
  <DataTable
1463
- isExpandable
1464
+ isExpandable
1464
1465
  renderRowSubComponent={renderSubComponent}
1465
1466
  itemCount={3}
1466
1467
  data={[
@@ -1481,7 +1482,7 @@ You can create your own custom expander column and use it, see code example belo
1481
1482
  reason: 'Felt like it',
1482
1483
  },
1483
1484
  {
1484
- name: 'Smoothie',
1485
+ name: 'Smoothie',
1485
1486
  color: 'orange tabby',
1486
1487
  famous_for: 'modeling',
1487
1488
  date_modified: currentDate,
@@ -1527,7 +1528,7 @@ You can create your own cell content by passing the `Cell` property to a specifi
1527
1528
  newColors[index] = cellColors[index] < 3 ? cellColors[index] + 1 : 0;
1528
1529
  setCellColors(newColors);
1529
1530
  };
1530
-
1531
+
1531
1532
  return (
1532
1533
  <DataTable
1533
1534
  isExpandable
@@ -1544,7 +1545,7 @@ You can create your own cell content by passing the `Cell` property to a specifi
1544
1545
  famous_for: 'serving moods',
1545
1546
  },
1546
1547
  {
1547
- name: 'Smoothie',
1548
+ name: 'Smoothie',
1548
1549
  color: 'orange tabby',
1549
1550
  famous_for: 'modeling',
1550
1551
  },
@@ -49,6 +49,7 @@ function DataTable({
49
49
  EmptyTableComponent,
50
50
  manualSelectColumn,
51
51
  showFiltersInSidebar,
52
+ filtersTitle,
52
53
  dataViewToggleOptions,
53
54
  disableElevation,
54
55
  isLoading,
@@ -215,6 +216,7 @@ function DataTable({
215
216
  manualSelectColumn,
216
217
  maxSelectedRows,
217
218
  onMaxSelectedRows,
219
+ filtersTitle,
218
220
  ...selectionProps,
219
221
  ...selectionActions,
220
222
  ...props,
@@ -222,7 +224,7 @@ function DataTable({
222
224
 
223
225
  return (
224
226
  <DataTableContext.Provider value={enhancedInstance}>
225
- <DataTableLayout>
227
+ <DataTableLayout filtersTitle={filtersTitle}>
226
228
  <div className={classNames('pgn__data-table-wrapper', {
227
229
  'hide-shadow': !!disableElevation,
228
230
  })}
@@ -264,6 +266,7 @@ DataTable.defaultProps = {
264
266
  FilterStatusComponent: FilterStatus,
265
267
  RowStatusComponent: RowStatus,
266
268
  showFiltersInSidebar: false,
269
+ filtersTitle: undefined,
267
270
  dataViewToggleOptions: {
268
271
  isDataViewToggleEnabled: false,
269
272
  onDataViewToggle: () => {},
@@ -425,6 +428,8 @@ DataTable.propTypes = {
425
428
  children: PropTypes.node,
426
429
  /** If true filters will be shown on sidebar instead */
427
430
  showFiltersInSidebar: PropTypes.bool,
431
+ /** Title of the filters section */
432
+ filtersTitle: PropTypes.string,
428
433
  /** options for data view toggle */
429
434
  dataViewToggleOptions: PropTypes.shape({
430
435
  /** Whether to show a toggle button group which allows view switching between card and table views */
@@ -6,6 +6,11 @@ const messages = defineMessages({
6
6
  defaultMessage: 'table pagination',
7
7
  description: 'Accessibile name for the navigation element of a pagination component',
8
8
  },
9
+ filtersDropdownTitle: {
10
+ id: 'pgn.DataTable.filtersDropdownTitle',
11
+ defaultMessage: 'Filters',
12
+ description: 'Title of the filters dropdown',
13
+ },
9
14
  });
10
15
 
11
16
  export default messages;
@@ -8,6 +8,7 @@ import DataTable from '..';
8
8
  import DataTableContext from '../DataTableContext';
9
9
  import { TextFilter } from '../..';
10
10
  import { SELECT_ALL_TEST_ID } from '../selection/data/constants';
11
+ import messages from '../messages';
11
12
 
12
13
  const additionalColumns = [
13
14
  {
@@ -162,6 +163,55 @@ describe('<DataTable />', () => {
162
163
  expect(screen.getByText('Action')).toBeInTheDocument();
163
164
  expect(screen.getByText('More')).toBeInTheDocument();
164
165
  });
166
+
167
+ it('displays the custom filters title in the sidebar', () => {
168
+ render(
169
+ <DataTableWrapper
170
+ showFiltersInSidebar
171
+ filtersTitle="Refine Your Search"
172
+ isFilterable
173
+ defaultColumnValues={{ Filter: TextFilter }}
174
+ {...props}
175
+ />,
176
+ );
177
+ expect(screen.getByRole('heading', { name: 'Refine Your Search' })).toBeInTheDocument();
178
+ });
179
+
180
+ it('displays the default filters title in the sidebar', () => {
181
+ render(
182
+ <DataTableWrapper
183
+ showFiltersInSidebar
184
+ isFilterable
185
+ defaultColumnValues={{ Filter: TextFilter }}
186
+ {...props}
187
+ />,
188
+ );
189
+ expect(screen.getByRole('heading', { name: messages.filtersDropdownTitle.defaultMessage })).toBeInTheDocument();
190
+ });
191
+
192
+ it('displays the custom filters title in the filters dropdown', () => {
193
+ render(
194
+ <DataTableWrapper
195
+ filtersTitle="Refine Your Search"
196
+ isFilterable
197
+ defaultColumnValues={{ Filter: TextFilter }}
198
+ {...props}
199
+ />,
200
+ );
201
+ expect(screen.getByRole('button', { name: 'Refine Your Search' })).toBeInTheDocument();
202
+ });
203
+
204
+ it('displays the default filters title in the filters dropdown', () => {
205
+ render(
206
+ <DataTableWrapper
207
+ isFilterable
208
+ defaultColumnValues={{ Filter: TextFilter }}
209
+ {...props}
210
+ />,
211
+ );
212
+ expect(screen.getByRole('button', { name: messages.filtersDropdownTitle.defaultMessage })).toBeInTheDocument();
213
+ });
214
+
165
215
  it('calls useTable with the data and columns', () => {
166
216
  const spy = jest.spyOn(reactTable, 'useTable');
167
217
  render(<DataTableWrapper {...props} />);
@@ -213,7 +263,7 @@ describe('<DataTable />', () => {
213
263
  fetchData: jest.fn(),
214
264
  };
215
265
  render(<DataTableWrapper {...propsWithSelection} />);
216
- const filtersButton = screen.getByRole('button', { name: 'Filters' });
266
+ const filtersButton = screen.getByRole('button', { name: messages.filtersDropdownTitle.defaultMessage });
217
267
 
218
268
  await userEvent.click(filtersButton);
219
269
 
@@ -1,10 +1,12 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
+ import { IntlProvider } from 'react-intl';
4
5
 
5
6
  import DropdownFilters from '../DropdownFilters';
6
7
  import { useWindowSize } from '../..';
7
8
  import DataTableContext from '../DataTableContext';
9
+ import messages from '../messages';
8
10
 
9
11
  jest.mock('../../hooks/useWindowSizeHook');
10
12
 
@@ -31,9 +33,11 @@ const instance = {
31
33
  // eslint-disable-next-line react/prop-types
32
34
  function DropdownFiltersWrapper({ value = instance, props }) {
33
35
  return (
34
- <DataTableContext.Provider value={value}>
35
- <DropdownFilters {...props} />
36
- </DataTableContext.Provider>
36
+ <IntlProvider locale="en">
37
+ <DataTableContext.Provider value={value}>
38
+ <DropdownFilters {...props} />
39
+ </DataTableContext.Provider>
40
+ </IntlProvider>
37
41
  );
38
42
  }
39
43
 
@@ -56,7 +60,7 @@ describe('<DropdownFilters />', () => {
56
60
  // filter should be rendered in the dropdown, so should not be present before
57
61
  // clicking the button.
58
62
  expect(screen.queryByText('Occupation filter')).toBeNull();
59
- const filtersButton = screen.getByRole('button', { name: /Filters/i });
63
+ const filtersButton = screen.getByRole('button', { name: messages.filtersDropdownTitle.defaultMessage });
60
64
  await userEvent.click(filtersButton);
61
65
  expect(screen.getByText('Occupation filter')).toBeInTheDocument();
62
66
  });
@@ -65,7 +69,7 @@ describe('<DropdownFilters />', () => {
65
69
  useWindowSize.mockReturnValue({ width: 800 });
66
70
  render(<DropdownFiltersWrapper />);
67
71
  expect(screen.queryByText('DOB filter')).toBeNull();
68
- const filtersButton = screen.getByRole('button', { name: /Filters/i });
72
+ const filtersButton = screen.getByRole('button', { name: messages.filtersDropdownTitle.defaultMessage });
69
73
  await userEvent.click(filtersButton);
70
74
  expect(screen.queryByText('DOB filter')).toBeNull();
71
75
  });
@@ -74,7 +78,7 @@ describe('<DropdownFilters />', () => {
74
78
  useWindowSize.mockReturnValue({ width: 800 });
75
79
  render(<DropdownFiltersWrapper value={{ columns: [instance.columns[1]] }} />);
76
80
  expect(screen.getByText('Occupation filter')).toBeInTheDocument();
77
- expect(screen.queryByRole('button', { name: /Filters/i })).toBeNull();
81
+ expect(screen.queryByRole('button', { name: messages.filtersDropdownTitle.defaultMessage })).toBeNull();
78
82
  });
79
83
  });
80
84
 
@@ -88,7 +92,7 @@ describe('<DropdownFilters />', () => {
88
92
  it('renders all filters in the dropdown', async () => {
89
93
  useWindowSize.mockReturnValue({ width: 500 });
90
94
  render(<DropdownFiltersWrapper />);
91
- const filtersButton = screen.getByRole('button', { name: /Filters/i });
95
+ const filtersButton = screen.getByRole('button', { name: messages.filtersDropdownTitle.defaultMessage });
92
96
  await userEvent.click(filtersButton);
93
97
  expect(screen.getByText('Bears filter')).toBeInTheDocument();
94
98
  expect(screen.getByText('Occupation filter')).toBeInTheDocument();
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import PropTypes from 'prop-types';
3
2
  import classNames from 'classnames';
4
3
  import { useMediaQuery } from 'react-responsive';
5
4
  import { useIntl } from 'react-intl';
@@ -52,7 +51,12 @@ interface Props {
52
51
  isBlocking?: boolean;
53
52
  /** Specifies the z-index of the modal */
54
53
  zIndex?: number;
55
- /** Specifies whether overflow is visible in the modal */
54
+ /**
55
+ * Specifies whether overflow content inside the modal should be visible.
56
+ * - `true` - content that exceeds the modal boundaries will remain visible outside the modal's main viewport,
57
+ * rather than being clipped or hidden.
58
+ * - `false` - any overflow content will be clipped to fit within the modal's dimensions.
59
+ */
56
60
  isOverflowVisible: boolean;
57
61
  }
58
62
 
@@ -109,70 +113,6 @@ function ModalDialog({
109
113
  );
110
114
  }
111
115
 
112
- ModalDialog.propTypes = {
113
- /**
114
- * Specifies the content of the dialog
115
- */
116
- children: PropTypes.node.isRequired,
117
- /**
118
- * The aria-label of the dialog
119
- */
120
- title: PropTypes.string.isRequired,
121
- /**
122
- * A callback to close the modal dialog
123
- */
124
- onClose: PropTypes.func.isRequired,
125
- /**
126
- * Is the modal dialog open or closed
127
- */
128
- isOpen: PropTypes.bool,
129
- /**
130
- * The close 'x' icon button in the top right of the dialog box
131
- */
132
- hasCloseButton: PropTypes.bool,
133
- /**
134
- * Sizes determine the maximum width of the dialog box
135
- */
136
- size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl', 'fullscreen']),
137
- /**
138
- * The visual style of the dialog box
139
- */
140
- variant: PropTypes.oneOf(['default', 'warning', 'danger', 'success', 'dark']),
141
- /**
142
- * The label supplied to the close icon button if one is rendered
143
- */
144
- closeLabel: PropTypes.string,
145
- /**
146
- * Specifies class name to append to the base element
147
- */
148
- className: PropTypes.string,
149
- /**
150
- * Determines where a scrollbar should appear if a modal is too large for the
151
- * viewport. When false, the ``ModalDialog``. Body receives a scrollbar, when true
152
- * the browser window itself receives the scrollbar.
153
- */
154
- isFullscreenScroll: PropTypes.bool,
155
- /**
156
- * To show full screen view on mobile screens
157
- */
158
- isFullscreenOnMobile: PropTypes.bool,
159
- /**
160
- * Prevent clicking on the backdrop or pressing Esc to close the modal
161
- */
162
- isBlocking: PropTypes.bool,
163
- /**
164
- * Specifies the z-index of the modal
165
- */
166
- zIndex: PropTypes.number,
167
- /**
168
- * Specifies whether overflow content inside the modal should be visible.
169
- * - `true` - content that exceeds the modal boundaries will remain visible outside the modal's main viewport,
170
- * rather than being clipped or hidden.
171
- * - `false` - any overflow content will be clipped to fit within the modal's dimensions.
172
- */
173
- isOverflowVisible: PropTypes.bool.isRequired,
174
- };
175
-
176
116
  ModalDialog.Header = ModalDialogHeader;
177
117
  ModalDialog.Title = ModalDialogTitle;
178
118
  ModalDialog.Footer = ModalDialogFooter;
@@ -1,6 +1,5 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import classNames from 'classnames';
3
- import PropTypes from 'prop-types';
4
3
  import { FocusOn } from 'react-focus-on';
5
4
  import Portal from './Portal';
6
5
  import { ModalContextProvider } from './ModalContext';
@@ -18,19 +17,11 @@ function ModalBackdrop({ onClick }: { onClick?: () => void }) {
18
17
  );
19
18
  }
20
19
 
21
- ModalBackdrop.propTypes = {
22
- onClick: PropTypes.func,
23
- };
24
-
25
20
  // istanbul ignore next
26
21
  function ModalContentContainer({ children = null }: { children?: React.ReactNode }) {
27
22
  return <div className="pgn__modal-content-container">{children}</div>;
28
23
  }
29
24
 
30
- ModalContentContainer.propTypes = {
31
- children: PropTypes.node,
32
- };
33
-
34
25
  interface Props {
35
26
  /** Specifies the contents of the modal */
36
27
  children: React.ReactNode;
@@ -94,18 +85,5 @@ function ModalLayer({
94
85
  );
95
86
  }
96
87
 
97
- ModalLayer.propTypes = {
98
- /** Specifies the contents of the modal */
99
- children: PropTypes.node.isRequired,
100
- /** A callback function for when the modal is dismissed */
101
- onClose: PropTypes.func.isRequired,
102
- /** Is the modal dialog open or closed */
103
- isOpen: PropTypes.bool.isRequired,
104
- /** Prevent clicking on the backdrop or pressing Esc to close the modal */
105
- isBlocking: PropTypes.bool,
106
- /** Specifies the z-index of the modal */
107
- zIndex: PropTypes.number,
108
- };
109
-
110
88
  export { ModalBackdrop, ModalContentContainer };
111
89
  export default ModalLayer;
@@ -19,6 +19,8 @@ describe('<ProductTour />', () => {
19
19
  <div id="target-4">...</div>
20
20
  </>
21
21
  );
22
+ const handleAdvance = jest.fn();
23
+ const handleBack = jest.fn();
22
24
  const handleDismiss = jest.fn();
23
25
  const handleEnd = jest.fn();
24
26
  const handleEscape = jest.fn();
@@ -31,6 +33,8 @@ describe('<ProductTour />', () => {
31
33
  advanceButtonText: 'Next',
32
34
  enabled: false,
33
35
  endButtonText: 'Okay',
36
+ onAdvance: handleAdvance,
37
+ onBack: handleBack,
34
38
  onDismiss: handleDismiss,
35
39
  onEnd: handleEnd,
36
40
  tourId: 'disabledTour',
@@ -48,6 +52,8 @@ describe('<ProductTour />', () => {
48
52
  backButtonText: 'Back',
49
53
  enabled: true,
50
54
  endButtonText: 'Okay',
55
+ onAdvance: handleAdvance,
56
+ onBack: handleBack,
51
57
  onDismiss: handleDismiss,
52
58
  onEnd: handleEnd,
53
59
  tourId: 'enabledTour',
@@ -60,6 +66,7 @@ describe('<ProductTour />', () => {
60
66
  {
61
67
  body: 'Checkpoint 2',
62
68
  target: '#target-2',
69
+ onAdvance: customOnAdvance,
63
70
  },
64
71
  {
65
72
  body: 'Checkpoint 3',
@@ -82,6 +89,7 @@ describe('<ProductTour />', () => {
82
89
 
83
90
  afterEach(() => {
84
91
  popperMock.mockReset();
92
+ jest.resetAllMocks();
85
93
  });
86
94
 
87
95
  // eslint-disable-next-line react/prop-types
@@ -115,6 +123,38 @@ describe('<ProductTour />', () => {
115
123
 
116
124
  // Verify the second Checkpoint has rendered
117
125
  expect(screen.getByText('Checkpoint 2')).toBeInTheDocument();
126
+ expect(handleAdvance).toHaveBeenCalled();
127
+
128
+ await userEvent.click(advanceButton);
129
+ expect(screen.getByText('Checkpoint 3')).toBeInTheDocument();
130
+ expect(customOnAdvance).toHaveBeenCalled();
131
+ });
132
+
133
+ it('onClick of back button rewinds to last checkpoint', async () => {
134
+ render(<ProductTourWrapper tours={[tourData]} />);
135
+ // Verify the first Checkpoint has rendered
136
+ expect(screen.getByRole('heading', { name: 'Checkpoint 1' })).toBeInTheDocument();
137
+
138
+ // Click the advance button
139
+ const advanceButton = screen.getByRole('button', { name: 'Next' });
140
+ await userEvent.click(advanceButton);
141
+
142
+ // go forward to the 3rd checkpoint
143
+ expect(screen.getByText('Checkpoint 2')).toBeInTheDocument();
144
+ await userEvent.click(advanceButton);
145
+ expect(screen.getByText('Checkpoint 3')).toBeInTheDocument();
146
+
147
+ // First back button should use custom on back function
148
+ let backButton = screen.getByRole('button', { name: 'Override back' });
149
+ await userEvent.click(backButton);
150
+ expect(screen.getByText('Checkpoint 2')).toBeInTheDocument();
151
+ expect(customOnBack).toHaveBeenCalled();
152
+
153
+ // Second back button should use the tour's default back function
154
+ backButton = screen.getByRole('button', { name: 'Back' });
155
+ await userEvent.click(backButton);
156
+ expect(screen.getByText('Checkpoint 1')).toBeInTheDocument();
157
+ expect(handleBack).toHaveBeenCalled();
118
158
  });
119
159
 
120
160
  it('onClick of dismiss button disables tour', async () => {
@@ -191,7 +231,6 @@ describe('<ProductTour />', () => {
191
231
 
192
232
  // Verify no Checkpoints have rendered
193
233
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
194
- expect(handleEnd).toHaveBeenCalledTimes(1);
195
234
  expect(customOnEnd).not.toHaveBeenCalled();
196
235
  });
197
236
 
@@ -284,7 +323,6 @@ describe('<ProductTour />', () => {
284
323
  expect(screen.getByText('Checkpoint 4')).toBeInTheDocument();
285
324
  const endButton = screen.getByRole('button', { name: 'Override end' });
286
325
  await user.click(endButton);
287
- expect(handleEnd).toBeCalledTimes(1);
288
326
  expect(customOnEnd).toHaveBeenCalledTimes(1);
289
327
  expect(screen.queryByText('Checkpoint 4')).not.toBeInTheDocument();
290
328
  });
@@ -12,7 +12,8 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
12
12
  startingIndex,
13
13
  onEscape,
14
14
  onEnd,
15
- onBack,
15
+ onAdvance: tourOnAdvance,
16
+ onBack: tourOnBack,
16
17
  onDismiss: tourOnDismiss,
17
18
  advanceButtonText: tourAdvanceButtonText,
18
19
  dismissAltText: tourDismissAltText,
@@ -27,6 +28,7 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
27
28
  title,
28
29
  body,
29
30
  onAdvance,
31
+ onBack,
30
32
  onDismiss,
31
33
  advanceButtonText,
32
34
  dismissAltText,
@@ -85,6 +87,8 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
85
87
  setIndex(index + 1);
86
88
  if (onAdvance) {
87
89
  onAdvance();
90
+ } else if (tourOnAdvance) {
91
+ tourOnAdvance();
88
92
  }
89
93
  };
90
94
 
@@ -92,6 +96,8 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
92
96
  setIndex(index - 1);
93
97
  if (onBack) {
94
98
  onBack();
99
+ } else if (tourOnBack) {
100
+ tourOnBack();
95
101
  }
96
102
  };
97
103
 
@@ -100,7 +106,7 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
100
106
  setIsTourEnabled(false);
101
107
  if (onDismiss) {
102
108
  onDismiss();
103
- } else {
109
+ } else if (tourOnDismiss) {
104
110
  tourOnDismiss();
105
111
  }
106
112
  setCurrentCheckpointData(null);