@openedx/paragon 22.16.2 → 22.18.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 (42) hide show
  1. package/dist/Modal/ModalDialog.d.ts +2 -2
  2. package/dist/Modal/ModalDialog.js +2 -2
  3. package/dist/Modal/ModalDialog.js.map +1 -1
  4. package/dist/ProductTour/Checkpoint.js +23 -16
  5. package/dist/ProductTour/Checkpoint.js.map +1 -1
  6. package/dist/ProductTour/Checkpoint.scss +8 -36
  7. package/dist/ProductTour/CheckpointActionRow.js +17 -21
  8. package/dist/ProductTour/CheckpointActionRow.js.map +1 -1
  9. package/dist/ProductTour/CheckpointHeader.js +57 -0
  10. package/dist/ProductTour/CheckpointHeader.js.map +1 -0
  11. package/dist/ProductTour/index.js +120 -20
  12. package/dist/ProductTour/index.js.map +1 -1
  13. package/dist/ProductTour/messages.js +10 -0
  14. package/dist/paragon.css +1 -1
  15. package/dist/withDeprecatedProps.js +11 -3
  16. package/dist/withDeprecatedProps.js.map +1 -1
  17. package/package.json +1 -1
  18. package/src/Modal/ModalDialog.tsx +3 -3
  19. package/src/Modal/README.md +1 -1
  20. package/src/Modal/alert-modal.mdx +4 -0
  21. package/src/Modal/fullscreen-modal.mdx +1 -0
  22. package/src/Modal/marketing-modal.mdx +1 -0
  23. package/src/Modal/modal-dialog.mdx +2 -0
  24. package/src/Modal/standard-modal.mdx +1 -0
  25. package/src/Modal/tests/AlertModal.test.jsx +4 -0
  26. package/src/Modal/tests/ModalDialog.test.tsx +3 -0
  27. package/src/ProductTour/Checkpoint.jsx +22 -16
  28. package/src/ProductTour/Checkpoint.scss +8 -36
  29. package/src/ProductTour/Checkpoint.test.jsx +20 -53
  30. package/src/ProductTour/CheckpointActionRow.jsx +32 -32
  31. package/src/ProductTour/CheckpointHeader.jsx +60 -0
  32. package/src/ProductTour/ProductTour.test.jsx +69 -60
  33. package/src/ProductTour/README.md +11 -3
  34. package/src/ProductTour/index.jsx +125 -17
  35. package/src/ProductTour/messages.js +10 -0
  36. package/src/withDeprecatedProps.tsx +10 -3
  37. package/dist/ProductTour/CheckpointBreadcrumbs.js +0 -37
  38. package/dist/ProductTour/CheckpointBreadcrumbs.js.map +0 -1
  39. package/dist/TransitionReplace/DemoTransitionReplace.js +0 -32
  40. package/dist/TransitionReplace/DemoTransitionReplace.js.map +0 -1
  41. package/src/ProductTour/CheckpointBreadcrumbs.jsx +0 -45
  42. package/src/TransitionReplace/DemoTransitionReplace.jsx +0 -57
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage, useIntl } from 'react-intl';
4
+
5
+ import Icon from '../Icon';
6
+ import IconButton from '../IconButton';
7
+ import { Close } from '../../icons';
8
+ import CheckpointTitle from './CheckpointTitle';
9
+ import messages from './messages';
10
+
11
+ const CheckpointHeader = React.forwardRef(({
12
+ dismissAltText, index, onDismiss, title, totalCheckpoints,
13
+ }) => {
14
+ const intl = useIntl();
15
+ const oneBasedIndex = index + 1;
16
+ const altText = (dismissAltText && typeof dismissAltText === 'string') ? dismissAltText : intl.formatMessage(messages.closeAltText);
17
+
18
+ return (
19
+ <>
20
+ <header className="pgn__checkpoint-header">
21
+ <span className="pgn__checkpoint-page-index">
22
+ <FormattedMessage
23
+ {...messages.pageIndexText}
24
+ values={{ step: oneBasedIndex, totalSteps: totalCheckpoints }}
25
+ />
26
+ </span>
27
+ <IconButton
28
+ size="sm"
29
+ iconAs={Icon}
30
+ src={Close}
31
+ alt={altText}
32
+ onClick={onDismiss}
33
+ data-testid="dismiss-tour"
34
+ />
35
+ </header>
36
+ {title && (<CheckpointTitle>{title}</CheckpointTitle>)}
37
+ </>
38
+ );
39
+ });
40
+
41
+ CheckpointHeader.defaultProps = {
42
+ dismissAltText: null,
43
+ title: '',
44
+ };
45
+
46
+ CheckpointHeader.propTypes = {
47
+ /** The text used in the alt for the icon used to dismiss the tour for the given Checkpoint */
48
+ dismissAltText: PropTypes.string,
49
+ /** The current index of the given Checkpoint */
50
+ index: PropTypes.number.isRequired,
51
+ /** A function that runs when triggering the `onClick` event of the dismiss
52
+ * button for the given Checkpoint. */
53
+ onDismiss: PropTypes.func.isRequired,
54
+ /** The text displayed in the title of the Checkpoint */
55
+ title: PropTypes.node,
56
+ /** The total number of Checkpoints in a tour */
57
+ totalCheckpoints: PropTypes.number.isRequired,
58
+ };
59
+
60
+ export default CheckpointHeader;
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
- import { render, screen, act } from '@testing-library/react';
2
+ import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { IntlProvider } from 'react-intl';
5
5
 
6
6
  import * as popper from '@popperjs/core';
7
7
 
8
8
  import ProductTour from '.';
9
+ import messages from './messages';
9
10
 
10
11
  const popperMock = jest.spyOn(popper, 'createPopper');
11
12
 
@@ -24,10 +25,10 @@ describe('<ProductTour />', () => {
24
25
  const customOnEnd = jest.fn();
25
26
  const customOnDismiss = jest.fn();
26
27
  const customOnAdvance = jest.fn();
28
+ const customOnBack = jest.fn();
27
29
 
28
30
  const disabledTourData = {
29
31
  advanceButtonText: 'Next',
30
- dismissButtonText: 'Dismiss',
31
32
  enabled: false,
32
33
  endButtonText: 'Okay',
33
34
  onDismiss: handleDismiss,
@@ -44,7 +45,7 @@ describe('<ProductTour />', () => {
44
45
 
45
46
  const tourData = {
46
47
  advanceButtonText: 'Next',
47
- dismissButtonText: 'Dismiss',
48
+ backButtonText: 'Back',
48
49
  enabled: true,
49
50
  endButtonText: 'Okay',
50
51
  onDismiss: handleDismiss,
@@ -57,22 +58,19 @@ describe('<ProductTour />', () => {
57
58
  title: 'Checkpoint 1',
58
59
  },
59
60
  {
60
- body: 'Lorem ipsum body',
61
+ body: 'Checkpoint 2',
61
62
  target: '#target-2',
62
- title: 'Checkpoint 2',
63
63
  },
64
64
  {
65
- body: 'Lorem ipsum body',
65
+ body: 'Checkpoint 3',
66
66
  target: '#target-3',
67
- title: 'Checkpoint 3',
68
- onDismiss: customOnDismiss,
67
+ onBack: customOnBack,
69
68
  advanceButtonText: 'Override advance',
70
- dismissButtonText: 'Override dismiss',
71
-
69
+ backButtonText: 'Override back',
72
70
  },
73
71
  {
74
72
  target: '#target-3',
75
- title: 'Checkpoint 4',
73
+ body: 'Checkpoint 4',
76
74
  endButtonText: 'End',
77
75
  },
78
76
  ],
@@ -98,31 +96,25 @@ describe('<ProductTour />', () => {
98
96
 
99
97
  describe('one enabled tour', () => {
100
98
  describe('with default settings', () => {
101
- it('renders checkpoint with correct title, body, and breadcrumbs', () => {
99
+ it('renders checkpoint with correct title, body, and page index', () => {
102
100
  render(<ProductTourWrapper tours={[tourData]} />);
103
101
 
104
102
  expect(screen.getByRole('dialog', { name: 'Checkpoint 1' })).toBeInTheDocument();
105
103
  expect(screen.getByText('Checkpoint 1')).toBeInTheDocument();
106
- expect(screen.getByTestId('pgn__checkpoint-breadcrumb_active')).toBeInTheDocument();
104
+ expect(screen.getByText('1 of 4')).toBeInTheDocument();
107
105
  });
108
106
 
109
107
  it('onClick of advance button advances to next checkpoint', async () => {
110
- const { rerender } = render(<ProductTourWrapper tours={[tourData]} />);
108
+ render(<ProductTourWrapper tours={[tourData]} />);
111
109
  // Verify the first Checkpoint has rendered
112
110
  expect(screen.getByRole('heading', { name: 'Checkpoint 1' })).toBeInTheDocument();
113
111
 
114
112
  // Click the advance button
115
113
  const advanceButton = screen.getByRole('button', { name: 'Next' });
116
- await act(async () => {
117
- await userEvent.click(advanceButton);
118
- });
119
-
120
- rerender(<ProductTourWrapper tours={[tourData]} />);
121
-
122
- const heading = screen.getByRole('heading', { name: 'Checkpoint 2' });
114
+ await userEvent.click(advanceButton);
123
115
 
124
116
  // Verify the second Checkpoint has rendered
125
- expect(heading).toBeInTheDocument();
117
+ expect(screen.getByText('Checkpoint 2')).toBeInTheDocument();
126
118
  });
127
119
 
128
120
  it('onClick of dismiss button disables tour', async () => {
@@ -132,20 +124,53 @@ describe('<ProductTour />', () => {
132
124
  expect(screen.getByRole('dialog', { name: 'Checkpoint 1' })).toBeInTheDocument();
133
125
 
134
126
  // Click the dismiss button
135
- const dismissButton = screen.getByRole('button', { name: 'Dismiss' });
136
- expect(dismissButton).toBeInTheDocument();
127
+ const closeButton = screen.getByRole('button', { name: 'Close tour' });
128
+ expect(closeButton).toBeInTheDocument();
137
129
 
138
- await act(async () => {
139
- await userEvent.click(dismissButton);
140
- });
130
+ await userEvent.click(closeButton);
141
131
 
142
132
  // Verify no Checkpoints have rendered
143
133
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
144
134
  });
145
135
 
136
+ it('onClick of close icon button disables tour', async () => {
137
+ const user = userEvent.setup();
138
+ render(<ProductTourWrapper tours={[tourData]} />);
139
+ // Advance the tour, close icon only appears after first step
140
+
141
+ await user.click(screen.getByRole('button', { name: 'Next' }));
142
+
143
+ const closeIcon = screen.getByRole('button', { name: messages.closeAltText.defaultMessage });
144
+ expect(closeIcon).toBeInTheDocument();
145
+
146
+ await user.click(closeIcon);
147
+
148
+ expect(handleDismiss).toHaveBeenCalled();
149
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
150
+ });
151
+
152
+ it('onClick of back button going to the previous checkpoint', async () => {
153
+ const user = userEvent.setup();
154
+ render(<ProductTourWrapper tours={[tourData]} />);
155
+ // Back button only appears when you are not on the first step of the tour
156
+ expect(screen.getByRole('heading', { name: 'Checkpoint 1' })).toBeInTheDocument();
157
+ expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument();
158
+ // Advance the tour
159
+ await user.click(screen.getByRole('button', { name: 'Next' }));
160
+
161
+ expect(screen.getByText('Checkpoint 2')).toBeInTheDocument();
162
+
163
+ // Go back in the tour
164
+ const backButton = screen.getByRole('button', { name: 'Back' });
165
+ expect(backButton).toBeInTheDocument();
166
+ await user.click(backButton);
167
+
168
+ expect(screen.getByText('Checkpoint 1')).toBeInTheDocument();
169
+ });
170
+
146
171
  it('onClick of end button disables tour', async () => {
147
172
  const user = userEvent.setup();
148
- const { rerender } = render(<ProductTourWrapper tours={[tourData]} />);
173
+ render(<ProductTourWrapper tours={[tourData]} />);
149
174
 
150
175
  // Verify a Checkpoint has rendered
151
176
  expect(screen.getByRole('dialog', { name: 'Checkpoint 1' })).toBeInTheDocument();
@@ -157,13 +182,9 @@ describe('<ProductTour />', () => {
157
182
  const advanceButton2 = screen.getByRole('button', { name: 'Next' });
158
183
  await user.click(advanceButton2);
159
184
 
160
- rerender(<ProductTourWrapper tours={[tourData]} />);
161
-
162
185
  const advanceButton3 = screen.getByRole('button', { name: 'Override advance' });
163
186
  await user.click(advanceButton3);
164
187
 
165
- rerender(<ProductTourWrapper tours={[tourData]} />);
166
-
167
188
  // Click the end button
168
189
  const endButton = screen.getByRole('button', { name: 'End' });
169
190
  await user.click(endButton);
@@ -191,7 +212,7 @@ describe('<ProductTour />', () => {
191
212
  describe('with Checkpoint override settings', () => {
192
213
  const overrideTourData = {
193
214
  advanceButtonText: 'Next',
194
- dismissButtonText: 'Dismiss',
215
+ backButtonText: 'Back',
195
216
  enabled: true,
196
217
  endButtonText: 'Okay',
197
218
  onDismiss: handleDismiss,
@@ -206,23 +227,21 @@ describe('<ProductTour />', () => {
206
227
  title: 'Checkpoint 1',
207
228
  },
208
229
  {
209
- body: 'Lorem ipsum body',
210
230
  target: '#target-2',
211
- title: 'Checkpoint 2',
231
+ body: 'Checkpoint 2',
212
232
  },
213
233
  {
214
- body: 'Lorem ipsum body',
215
234
  target: '#target-3',
216
- title: 'Checkpoint 3',
235
+ body: 'Checkpoint 3',
236
+ onBack: customOnBack,
217
237
  onDismiss: customOnDismiss,
218
238
  onAdvance: customOnAdvance,
219
239
  advanceButtonText: 'Override advance',
220
- dismissButtonText: 'Override dismiss',
221
-
240
+ backButtonText: 'Override back',
222
241
  },
223
242
  {
224
243
  target: '#target-4',
225
- title: 'Checkpoint 4',
244
+ body: 'Checkpoint 4',
226
245
  endButtonText: 'Override end',
227
246
  onEnd: customOnEnd,
228
247
  },
@@ -230,45 +249,38 @@ describe('<ProductTour />', () => {
230
249
  };
231
250
  it('renders correct checkpoint on index override', () => {
232
251
  render(<ProductTourWrapper tours={[overrideTourData]} />);
233
- expect(screen.getByRole('dialog', { name: 'Checkpoint 3' })).toBeInTheDocument();
234
- expect(screen.getByRole('heading', { name: 'Checkpoint 3' })).toBeInTheDocument();
252
+ expect(screen.getByText('Checkpoint 3')).toBeInTheDocument();
235
253
  });
236
254
 
237
255
  it('applies override for advanceButtonText', async () => {
238
- const { rerender } = render(<ProductTourWrapper tours={[overrideTourData]} />);
256
+ render(<ProductTourWrapper tours={[overrideTourData]} />);
239
257
  expect(screen.getByRole('button', { name: 'Override advance' })).toBeInTheDocument();
240
258
  const advanceButton = screen.getByRole('button', { name: 'Override advance' });
241
- await act(async () => {
242
- await userEvent.click(advanceButton);
243
- });
259
+ await userEvent.click(advanceButton);
244
260
  expect(screen.queryByRole('button', { name: 'Override advance' })).not.toBeInTheDocument();
245
261
  expect(customOnAdvance).toHaveBeenCalledTimes(1);
246
262
 
247
- rerender(<ProductTourWrapper tours={[overrideTourData]} />);
248
-
249
263
  expect(screen.getByText('Checkpoint 4')).toBeInTheDocument();
250
264
  });
251
- it('applies override for dismissButtonText', () => {
265
+ it('applies override for backButtonText', () => {
252
266
  render(<ProductTourWrapper tours={[overrideTourData]} />);
253
- expect(screen.getByRole('button', { name: 'Override dismiss' })).toBeInTheDocument();
267
+ expect(screen.getByRole('button', { name: 'Override back' })).toBeInTheDocument();
254
268
  });
269
+
255
270
  it('calls customHandleDismiss onClick of dismiss button', async () => {
256
271
  render(<ProductTourWrapper tours={[overrideTourData]} />);
257
- const dismissButton = screen.getByRole('button', { name: 'Override dismiss' });
258
- await act(async () => {
259
- await userEvent.click(dismissButton);
260
- });
272
+ const closeButton = screen.getByRole('button', { name: 'Close tour' });
273
+ await userEvent.click(closeButton);
274
+
261
275
  expect(customOnDismiss).toHaveBeenCalledTimes(1);
262
276
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
263
277
  });
264
278
  it('calls customHandleOnEnd onClick of end button', async () => {
265
279
  const user = userEvent.setup();
266
- const { rerender } = render(<ProductTourWrapper tours={[overrideTourData]} />);
280
+ render(<ProductTourWrapper tours={[overrideTourData]} />);
267
281
  const advanceButton = screen.getByRole('button', { name: 'Override advance' });
268
282
  await user.click(advanceButton);
269
283
 
270
- rerender(<ProductTourWrapper tours={[overrideTourData]} />);
271
-
272
284
  expect(screen.getByText('Checkpoint 4')).toBeInTheDocument();
273
285
  const endButton = screen.getByRole('button', { name: 'Override end' });
274
286
  await user.click(endButton);
@@ -292,7 +304,6 @@ describe('<ProductTour />', () => {
292
304
  it('does not render', () => {
293
305
  const badTourData = {
294
306
  advanceButtonText: 'Next',
295
- dismissButtonText: 'Dismiss',
296
307
  enabled: true,
297
308
  endButtonText: 'Okay',
298
309
  onDismiss: handleDismiss,
@@ -315,7 +326,6 @@ describe('<ProductTour />', () => {
315
326
  it('advances to next valid Checkpoint', () => {
316
327
  const badTourData = {
317
328
  advanceButtonText: 'Next',
318
- dismissButtonText: 'Dismiss',
319
329
  enabled: true,
320
330
  endButtonText: 'Okay',
321
331
  onDismiss: handleDismiss,
@@ -348,7 +358,6 @@ describe('<ProductTour />', () => {
348
358
  it('renders first enabled tour', () => {
349
359
  const secondEnabledTourData = {
350
360
  advanceButtonText: 'Next',
351
- dismissButtonText: 'Dismiss',
352
361
  enabled: true,
353
362
  endButtonText: 'Okay',
354
363
  onDismiss: handleDismiss,
@@ -16,6 +16,12 @@ tours are enabled, `ProductTour` will only render the first enabled in the `tour
16
16
  `Checkpoints` are rendered in the order they're listed in the checkpoint array.
17
17
  The checkpoint objects themselves have additional props that can override the props defined in `ProductTour`.
18
18
 
19
+ ## Usage guidelines
20
+ Best practices for ProductTour includes not overloading the user with a large amount of steps.
21
+ Paragon recommends keeping to no more than 5 steps, as well as any overriden button names to be
22
+ descriptive and readable. Also, we recommend using a title at every step or only at the first step for a
23
+ consistent and accessible experience.
24
+
19
25
  ## Basic Usage
20
26
 
21
27
  ```jsx live
@@ -24,11 +30,12 @@ The checkpoint objects themselves have additional props that can override the pr
24
30
  const myFirstTour = {
25
31
  tourId: 'myFirstTour',
26
32
  advanceButtonText: 'Next',
27
- dismissButtonText: 'Dismiss',
33
+ backButtonText: 'Back',
28
34
  endButtonText: 'Okay',
29
35
  enabled: isTourEnabled,
30
36
  onDismiss: () => setIsTourEnabled(false),
31
37
  onEnd: () => setIsTourEnabled(false),
38
+ dismissAltText: '',
32
39
  checkpoints: [
33
40
  {
34
41
  advanceButtonText: 'Onward', // Override the default advanceButtonText above
@@ -36,6 +43,7 @@ The checkpoint objects themselves have additional props that can override the pr
36
43
  placement: 'top',
37
44
  target: '#checkpoint-1',
38
45
  title: 'First checkpoint',
46
+ dismissAltText: '',
39
47
  },
40
48
  {
41
49
  body: "Here's the second checkpoint!",
@@ -46,12 +54,12 @@ The checkpoint objects themselves have additional props that can override the pr
46
54
  placement: 'right',
47
55
  target: '#checkpoint-2',
48
56
  title: 'Second checkpoint',
57
+ backButtonText: 'Rewind',
49
58
  },
50
59
  {
51
- body: "Here's the third checkpoint!",
60
+ body: "The third checkpoint without a title",
52
61
  placement: 'bottom',
53
62
  target: '#checkpoint-3',
54
- title: 'Third checkpoint',
55
63
  onEnd: () => {
56
64
  console.log('Ended the third checkpoint');
57
65
  setIsTourEnabled(false);
@@ -1,22 +1,39 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import withDeprecatedProps, { DeprTypes } from '../withDeprecatedProps';
3
4
 
4
5
  import Checkpoint from './Checkpoint';
5
6
 
6
7
  const ProductTour = React.forwardRef(({ tours }, ref) => {
7
8
  const tourValue = tours.find((tour) => tour.enabled);
8
9
  const {
9
- enabled, checkpoints = [], startingIndex, onEscape, onEnd, onDismiss: tourOnDismiss,
10
- advanceButtonText: tourAdvanceButtonText, dismissButtonText: tourDismissButtonText,
10
+ enabled,
11
+ checkpoints = [],
12
+ startingIndex,
13
+ onEscape,
14
+ onEnd,
15
+ onBack,
16
+ onDismiss: tourOnDismiss,
17
+ advanceButtonText: tourAdvanceButtonText,
18
+ dismissAltText: tourDismissAltText,
11
19
  endButtonText: tourEndButtonText,
20
+ backButtonText: tourBackButtonText,
12
21
  } = tourValue || {};
13
22
  const [currentCheckpointData, setCurrentCheckpointData] = useState(null);
14
23
  const [index, setIndex] = useState(0);
15
24
  const [isTourEnabled, setIsTourEnabled] = useState(false);
16
25
  const [prunedCheckpoints, setPrunedCheckpoints] = useState([]);
17
26
  const {
18
- title, body, onAdvance, onDismiss, advanceButtonText, dismissButtonText,
19
- endButtonText, placement, target, showDismissButton,
27
+ title,
28
+ body,
29
+ onAdvance,
30
+ onDismiss,
31
+ advanceButtonText,
32
+ dismissAltText,
33
+ endButtonText,
34
+ backButtonText,
35
+ placement,
36
+ target,
20
37
  } = currentCheckpointData || {};
21
38
 
22
39
  /**
@@ -71,6 +88,13 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
71
88
  }
72
89
  };
73
90
 
91
+ const handleBack = () => {
92
+ setIndex(index - 1);
93
+ if (onBack) {
94
+ onBack();
95
+ }
96
+ };
97
+
74
98
  const handleDismiss = () => {
75
99
  setIndex(0);
76
100
  setIsTourEnabled(false);
@@ -106,9 +130,11 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
106
130
  advanceButtonText={advanceButtonText || tourAdvanceButtonText}
107
131
  body={body}
108
132
  currentCheckpointData={currentCheckpointData}
109
- dismissButtonText={dismissButtonText || tourDismissButtonText}
133
+ dismissAltText={dismissAltText || tourDismissAltText}
110
134
  endButtonText={endButtonText || tourEndButtonText}
135
+ backButtonText={backButtonText || tourBackButtonText}
111
136
  index={index}
137
+ onBack={handleBack}
112
138
  onAdvance={handleAdvance}
113
139
  onDismiss={handleDismiss}
114
140
  onEnd={handleEnd}
@@ -116,7 +142,6 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
116
142
  target={target}
117
143
  title={title}
118
144
  totalCheckpoints={prunedCheckpoints.length}
119
- showDismissButton={showDismissButton}
120
145
  ref={ref}
121
146
  />
122
147
  );
@@ -125,19 +150,22 @@ const ProductTour = React.forwardRef(({ tours }, ref) => {
125
150
  ProductTour.defaultProps = {
126
151
  tours: {
127
152
  advanceButtonText: '',
153
+ backButtonText: '',
128
154
  checkpoints: {
129
155
  advanceButtonText: '',
156
+ backButtonText: '',
130
157
  body: '',
131
- dismissButtonText: '',
158
+ dismissAltText: '',
132
159
  endButtonText: '',
133
160
  onAdvance: () => {},
134
161
  onDismiss: () => {},
162
+ onBack: () => {},
135
163
  placement: 'top',
136
164
  title: '',
137
- showDismissButton: undefined,
138
165
  },
139
- dismissButtonText: '',
166
+ dismissAltText: '',
140
167
  endButtonText: '',
168
+ onBack: () => {},
141
169
  onDismiss: () => {},
142
170
  onEnd: () => {},
143
171
  onEscape: () => {},
@@ -149,16 +177,20 @@ ProductTour.propTypes = {
149
177
  tours: PropTypes.arrayOf(PropTypes.shape({
150
178
  /** The text displayed on all buttons used to advance the tour. */
151
179
  advanceButtonText: PropTypes.node,
180
+ /** The text displayed on all buttons used to go back in the tour */
181
+ backButtonText: PropTypes.string,
152
182
  /** An array comprised of checkpoint objects supporting the following values: */
153
183
  checkpoints: PropTypes.arrayOf(PropTypes.shape({
154
184
  /** The text displayed on the button used to advance the tour for the given Checkpoint
155
185
  * (overrides the* `advanceButtonText` defined in the parent tour object). */
156
186
  advanceButtonText: PropTypes.node,
187
+ /** The text displayed on the button used to go back in the tour for the given Checkpoint
188
+ * (overrides the* `backButtonText` defined in the parent tour object). */
189
+ backButtonText: PropTypes.string,
157
190
  /** The text displayed in the body of the Checkpoint */
158
191
  body: PropTypes.node,
159
- /** The text displayed on the button used to dismiss the tour for the given Checkpoint
160
- * (overrides the `dismissButtonText` defined in the parent tour object). */
161
- dismissButtonText: PropTypes.node,
192
+ /** The text used in the alt for the icon used to dismiss the tour for the given Checkpoint */
193
+ dismissAltText: PropTypes.string,
162
194
  /** The text displayed on the button used to end the tour for the given Checkpoint
163
195
  * (overrides the `endButtonText` defined in the parent tour object). */
164
196
  endButtonText: PropTypes.node,
@@ -180,15 +212,15 @@ ProductTour.propTypes = {
180
212
  target: PropTypes.string.isRequired,
181
213
  /** The text displayed in the title of the Checkpoint */
182
214
  title: PropTypes.node,
183
- /** Enforces visibility of the dismiss button under all circumstances */
184
- showDismissButton: PropTypes.bool,
185
215
  })),
186
- /** The text displayed on the button used to dismiss the tour. */
187
- dismissButtonText: PropTypes.node,
216
+ /** The text used in the alt for the icon used to dismiss the tour for the given Checkpoint */
217
+ dismissAltText: PropTypes.string,
188
218
  /** Whether the tour is enabled. If there are multiple tours defined, only one should be enabled at a time. */
189
219
  enabled: PropTypes.bool.isRequired,
190
220
  /** The text displayed on the button used to end the tour. */
191
221
  endButtonText: PropTypes.node,
222
+ /** A function that runs when triggering the `onBack` event of the back button. */
223
+ onBack: PropTypes.func,
192
224
  /** A function that runs when triggering the `onClick` event of the dismiss button. */
193
225
  onDismiss: PropTypes.func,
194
226
  /** A function that runs when triggering the `onClick` event of the end button. */
@@ -202,4 +234,80 @@ ProductTour.propTypes = {
202
234
  })),
203
235
  };
204
236
 
205
- export default ProductTour;
237
+ /**
238
+ * Checks if the given object has a deprecated/legacy `dismissButtonText` property.
239
+ * @param {Object} obj - The object to check
240
+ * @returns {boolean} - True if the object has a deprecated/legacy `dismissButtonText` property, false otherwise
241
+ */
242
+ const hasDismissButtonText = (obj) => {
243
+ if ('dismissButtonText' in obj && !!obj.dismissButtonText) {
244
+ return true;
245
+ }
246
+ return false;
247
+ };
248
+
249
+ export default withDeprecatedProps(ProductTour, 'ProductTour', {
250
+ tours: {
251
+ deprType: DeprTypes.FORMAT,
252
+ message: "The dismissButtonText options in the 'tours' prop have been moved to 'dismissAltText'.",
253
+ /**
254
+ * Determines whether the given prop value contains the deprecated/legacy `dismissButtonText` property.
255
+ * @param {Object[]} propValue - The tours prop value to check
256
+ * @returns {boolean} True if the prop value contains the deprecated/legacy `dismissButtonText`
257
+ * property, false otherwise
258
+ */
259
+ expect: (propValue) => {
260
+ if (!Array.isArray(propValue)) {
261
+ return true;
262
+ }
263
+ return !propValue.some((tour) => {
264
+ if (hasDismissButtonText(tour)) {
265
+ return true;
266
+ }
267
+ return Array.isArray(tour.checkpoints)
268
+ && tour.checkpoints.some(hasDismissButtonText);
269
+ });
270
+ },
271
+ /**
272
+ * Transforms the given prop value by updating the
273
+ * deprecated/legacy `dismissButtonText` property to
274
+ * `dismissAltText`, if the prop value is a string. Otherwise,
275
+ * the original `dismissButtonText` property is ignored.
276
+ * @param {Object[]} propValue - The tours prop value to transform
277
+ * @returns {Object[]} The transformed prop value
278
+ */
279
+ transform: (propValue) => {
280
+ const tours = propValue.map((tour) => {
281
+ const updatedTour = { ...tour };
282
+
283
+ // Replace tour level dismissButtonText with dismissAltText
284
+ if (hasDismissButtonText(tour)) {
285
+ if (typeof tour.dismissButtonText === 'string') {
286
+ updatedTour.dismissAltText = tour.dismissButtonText;
287
+ } else {
288
+ const warningMessage = "[Deprecated] ProductTour: The 'dismissButtonText' options within the 'tours' prop now expects a string";
289
+ // eslint-disable-next-line no-console
290
+ console.warn(warningMessage);
291
+ }
292
+ }
293
+
294
+ // Replace checkpoint level dismissButtonText with dismissAltText
295
+ if (Array.isArray(tour.checkpoints)) {
296
+ updatedTour.checkpoints = tour.checkpoints.map((checkpoint) => {
297
+ if (hasDismissButtonText(checkpoint)) {
298
+ const { dismissButtonText, ...rest } = checkpoint;
299
+ if (typeof dismissButtonText === 'string') {
300
+ return { ...rest, dismissAltText: dismissButtonText };
301
+ }
302
+ }
303
+ return checkpoint;
304
+ });
305
+ }
306
+ return updatedTour;
307
+ });
308
+
309
+ // Return the transformed tours
310
+ return tours;
311
+ },
312
+ },
313
+ });
@@ -11,6 +11,16 @@ const messages = defineMessages({
11
11
  defaultMessage: 'Bottom of step {step}',
12
12
  description: 'Screen-reader message to notify user that they are located at the bottom of the product tour step.',
13
13
  },
14
+ pageIndexText: {
15
+ id: 'pgn.ProductTour.Checkpoint.page-index-text',
16
+ defaultMessage: '{step} of {totalSteps}',
17
+ description: 'Page index showing your place in the ProductTour',
18
+ },
19
+ closeAltText: {
20
+ id: 'pgn.ProductTour.checkpointHeader.close',
21
+ defaultMessage: 'Close tour',
22
+ description: 'Close alternative text for ProductTour component',
23
+ },
14
24
  });
15
25
 
16
26
  export default messages;
@@ -66,10 +66,17 @@ function withDeprecatedProps<T extends Record<string, any>>(
66
66
  acc[propName] = this.props[propName];
67
67
  }
68
68
  break;
69
- case DeprTypes.MOVED_AND_FORMAT:
70
- this.warn(`${componentName}: The prop '${propName}' has been moved to '${newName}' and expects a new format. ${message}`);
71
- acc[newName!] = transform!(this.props[propName], this.props);
69
+ case DeprTypes.MOVED_AND_FORMAT: {
70
+ const propValue = this.props[propName];
71
+ let warningMessage = `${componentName}: The prop '${propName}' has been moved to '${newName}'`;
72
+ if (expect && !expect(propValue)) {
73
+ warningMessage += ' and expects a new format';
74
+ }
75
+ warningMessage += message ? `. ${message}` : '';
76
+ this.warn(warningMessage);
77
+ acc[newName!] = transform ? transform(propValue, this.props) : propValue;
72
78
  break;
79
+ }
73
80
  default:
74
81
  acc[propName] = this.props[propName];
75
82
  break;