@openedx/paragon 23.13.0 → 23.14.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.
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { IntlProvider } from 'react-intl';
3
3
  import renderer, { act } from 'react-test-renderer';
4
- import { render, screen } from '@testing-library/react';
4
+ import { render, screen, within } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { Context as ResponsiveContext } from 'react-responsive';
7
7
  import { Info } from '../../icons';
@@ -112,4 +112,17 @@ describe('<Alert />', () => {
112
112
  });
113
113
  expect(tree).toMatchSnapshot();
114
114
  });
115
+ it('renders with headings and links', async () => {
116
+ render(
117
+ <AlertWrapper>
118
+ <Alert.Heading>This is the heading</Alert.Heading>
119
+ And <Alert.Link href="#">here is a link</Alert.Link>.
120
+ </AlertWrapper>,
121
+ );
122
+ const alertDiv = screen.getByRole('alert');
123
+ const heading = within(alertDiv).getByText(/This is the heading/);
124
+ expect(heading).toHaveClass('alert-heading', 'h4');
125
+ const link = within(alertDiv).getByRole('link', { name: 'here is a link' });
126
+ expect(link).toHaveClass('alert-link');
127
+ });
115
128
  });
@@ -11,12 +11,12 @@ import React, {
11
11
  RefAttributes,
12
12
  cloneElement,
13
13
  } from 'react';
14
- import PropTypes from 'prop-types';
15
14
  import classNames from 'classnames';
16
15
  import {
17
16
  Alert as BaseAlert,
18
17
  AlertProps as BaseAlertProps,
19
18
  } from 'react-bootstrap';
19
+ import { type TransitionComponent } from 'react-bootstrap/helpers';
20
20
  import divWithClassName from 'react-bootstrap/divWithClassName';
21
21
  import { FormattedMessage } from 'react-intl';
22
22
  import { useMediaQuery } from 'react-responsive';
@@ -33,29 +33,51 @@ export type AlertVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'war
33
33
  export type BaseProps = Omit<BaseAlertProps, 'children' | 'variant' | 'closeLabel'>;
34
34
 
35
35
  export interface AlertProps extends BaseProps {
36
+ /** Specifies class name to append to the base element */
36
37
  className?: string;
38
+ /** Overrides underlying component base CSS class name */
37
39
  bsPrefix?: string;
40
+ /** Specifies variant to use. */
38
41
  variant?: AlertVariant;
42
+ /**
43
+ * Animate the entering and exiting of the Alert. `true` will use the `<Fade>` transition,
44
+ * more detailed customization is also provided.
45
+ */
46
+ transition?: boolean | TransitionComponent;
39
47
  children?: ReactNode;
48
+ /** Icon that will be shown in the alert */
40
49
  icon?: React.ComponentType<IconProps>;
50
+ /** Whether the alert is shown. */
41
51
  show?: boolean;
52
+ /** Whether the alert is dismissible. Defaults to false. */
42
53
  dismissible?: boolean;
54
+ /** Optional callback function for when the alert it dismissed. */
43
55
  onClose?: () => void;
56
+ /** Optional list of action elements. May include, at most, 2 actions, or 1 if dismissible is true. */
44
57
  actions?: React.ReactElement[];
58
+ /** Position of the dismiss and call-to-action buttons. Defaults to `false`. */
45
59
  stacked?: boolean;
60
+ /** Sets the text for alert close button, defaults to 'Dismiss'. */
46
61
  closeLabel?: string | ReactNode;
47
62
  }
48
63
 
49
64
  export interface AlertHeadingProps {
65
+ /** Specifies the base element */
50
66
  as?: ElementType;
67
+ /** Overrides underlying component base CSS class name */
51
68
  bsPrefix?: string;
69
+ // eslint-disable-next-line react/no-unused-prop-types
52
70
  children?: ReactNode;
53
71
  }
54
72
 
55
73
  export interface AlertLinkProps {
74
+ /** Specifies the base element */
56
75
  as?: ElementType;
76
+ /** Overrides underlying component base CSS class name */
57
77
  bsPrefix?: string;
78
+ // eslint-disable-next-line react/no-unused-prop-types
58
79
  children?: ReactNode;
80
+ // eslint-disable-next-line react/no-unused-prop-types
59
81
  href?: string;
60
82
  }
61
83
 
@@ -64,16 +86,17 @@ export interface AlertComponent extends ForwardRefExoticComponent<AlertProps & R
64
86
  Link: FC<AlertLinkProps>;
65
87
  }
66
88
 
67
- const Alert = forwardRef<HTMLDivElement, AlertProps>(({
89
+ const Alert = forwardRef(({
68
90
  children,
69
91
  icon,
70
92
  actions,
71
- dismissible,
72
- onClose,
93
+ dismissible = false,
94
+ onClose = () => {},
73
95
  closeLabel,
74
- stacked,
96
+ stacked = false,
97
+ show = true,
75
98
  ...props
76
- }, ref) => {
99
+ }: AlertProps, ref: React.ForwardedRef<HTMLDivElement>) => {
77
100
  const [isStacked, setIsStacked] = useState(stacked);
78
101
  const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
79
102
  const actionButtonSize = 'sm';
@@ -98,6 +121,7 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(({
98
121
  <BaseAlert
99
122
  {...props}
100
123
  className={classNames('alert-content', props.className)}
124
+ show={show}
101
125
  ref={ref}
102
126
  >
103
127
  {icon && <Icon src={icon} className="alert-icon" />}
@@ -142,97 +166,22 @@ const Alert = forwardRef<HTMLDivElement, AlertProps>(({
142
166
  const DivStyledAsH4 = divWithClassName('h4');
143
167
  DivStyledAsH4.displayName = 'DivStyledAsH4';
144
168
 
145
- function AlertHeading(props: AlertHeadingProps): JSX.Element {
146
- return <BaseAlert.Heading {...props} />;
169
+ function AlertHeading({
170
+ as = DivStyledAsH4,
171
+ bsPrefix = 'alert-heading',
172
+ ...props
173
+ }: AlertHeadingProps): JSX.Element {
174
+ return <BaseAlert.Heading {...{ as, bsPrefix, ...props }} />;
147
175
  }
148
176
 
149
- function AlertLink(props: AlertLinkProps): JSX.Element {
150
- return <BaseAlert.Link {...props} />;
177
+ function AlertLink({
178
+ as = 'a',
179
+ bsPrefix = 'alert-link',
180
+ ...props
181
+ }: AlertLinkProps): JSX.Element {
182
+ return <BaseAlert.Link {...{ as, bsPrefix, ...props }} />;
151
183
  }
152
184
 
153
- AlertLink.propTypes = {
154
- /** Specifies the base element */
155
- as: PropTypes.elementType as PropTypes.Validator<ElementType>,
156
- /** Overrides underlying component base CSS class name */
157
- bsPrefix: PropTypes.string,
158
- };
159
-
160
- AlertHeading.propTypes = {
161
- /** Specifies the base element */
162
- as: PropTypes.elementType as PropTypes.Validator<ElementType>,
163
- /** Overrides underlying component base CSS class name */
164
- bsPrefix: PropTypes.string,
165
- };
166
-
167
- AlertLink.defaultProps = {
168
- as: 'a' as ElementType,
169
- bsPrefix: 'alert-link',
170
- };
171
-
172
- AlertHeading.defaultProps = {
173
- as: DivStyledAsH4,
174
- bsPrefix: 'alert-heading',
175
- };
176
-
177
- Alert.propTypes = {
178
- ...BaseAlert.propTypes,
179
- /** Specifies class name to append to the base element */
180
- className: PropTypes.string,
181
- /** Overrides underlying component base CSS class name */
182
- bsPrefix: PropTypes.string,
183
- /** Specifies variant to use. */
184
- variant: PropTypes.oneOf(['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light'] as AlertVariant[]),
185
- /**
186
- * Animate the entering and exiting of the Alert. `true` will use the `<Fade>` transition,
187
- * more detailed customization is also provided.
188
- */
189
- transition: PropTypes.oneOfType([
190
- PropTypes.bool,
191
- PropTypes.shape({
192
- in: PropTypes.bool,
193
- appear: PropTypes.bool,
194
- children: PropTypes.node,
195
- onEnter: PropTypes.func,
196
- onEntered: PropTypes.func,
197
- onEntering: PropTypes.func,
198
- onExit: PropTypes.func,
199
- onExited: PropTypes.func,
200
- onExiting: PropTypes.func,
201
- }),
202
- ]) as PropTypes.Validator<BaseAlertProps['transition']>,
203
- /** Docstring for the children prop */
204
- children: PropTypes.node as PropTypes.Validator<ReactNode>,
205
- /** Docstring for the icon prop... Icon that will be shown in the alert */
206
- icon: PropTypes.func,
207
- /** Whether the alert is shown. */
208
- show: PropTypes.bool,
209
- /** Whether the alert is dismissible. Defaults to true. */
210
- dismissible: PropTypes.bool,
211
- /** Optional callback function for when the alert it dismissed. */
212
- onClose: PropTypes.func,
213
- /** Optional list of action elements. May include, at most, 2 actions, or 1 if dismissible is true. */
214
- actions: PropTypes.arrayOf(PropTypes.element) as PropTypes.Validator<React.ReactElement[]>,
215
- /** Position of the dismiss and call-to-action buttons. Defaults to ``false``. */
216
- stacked: PropTypes.bool,
217
- /** Sets the text for alert close button, defaults to 'Dismiss'. */
218
- closeLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
219
- };
220
-
221
- Alert.defaultProps = {
222
- ...BaseAlert.defaultProps,
223
- children: undefined,
224
- icon: undefined,
225
- actions: undefined,
226
- dismissible: false,
227
- onClose: () => {},
228
- closeLabel: undefined,
229
- show: true,
230
- stacked: false,
231
- className: undefined,
232
- bsPrefix: undefined,
233
- variant: undefined,
234
- };
235
-
236
185
  Alert.Heading = AlertHeading;
237
186
  Alert.Link = AlertLink;
238
187
 
@@ -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 () => {
@@ -284,7 +324,6 @@ describe('<ProductTour />', () => {
284
324
  expect(screen.getByText('Checkpoint 4')).toBeInTheDocument();
285
325
  const endButton = screen.getByRole('button', { name: 'Override end' });
286
326
  await user.click(endButton);
287
- expect(handleEnd).toBeCalledTimes(1);
288
327
  expect(customOnEnd).toHaveBeenCalledTimes(1);
289
328
  expect(screen.queryByText('Checkpoint 4')).not.toBeInTheDocument();
290
329
  });
@@ -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);