@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.
- package/README.md +1 -0
- package/bin/paragon-scripts.js +38 -1
- package/dist/Alert/index.d.ts +20 -0
- package/dist/Alert/index.js +21 -80
- package/dist/Alert/index.js.map +1 -1
- package/dist/ProductTour/index.js +8 -2
- package/dist/ProductTour/index.js.map +1 -1
- package/lib/__tests__/help.test.js +2 -2
- package/lib/__tests__/serve-theme-css.test.js +284 -0
- package/lib/queryParamEncoding.js +66 -0
- package/lib/serve-theme-css.js +317 -0
- package/package.json +4 -1
- package/src/Alert/Alert.test.tsx +14 -1
- package/src/Alert/index.tsx +42 -93
- package/src/ProductTour/ProductTour.test.jsx +40 -1
- package/src/ProductTour/index.jsx +8 -2
package/src/Alert/Alert.test.tsx
CHANGED
|
@@ -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
|
});
|
package/src/Alert/index.tsx
CHANGED
|
@@ -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
|
|
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(
|
|
146
|
-
|
|
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(
|
|
150
|
-
|
|
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
|
-
|
|
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);
|