@openedx/paragon 22.15.3 → 22.16.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.
- package/dist/Toast/ToastContainer.d.ts +6 -0
- package/dist/Toast/ToastContainer.js +19 -29
- package/dist/Toast/ToastContainer.js.map +1 -1
- package/dist/Toast/ToastContainer.scss +2 -1
- package/dist/Toast/index.d.ts +59 -0
- package/dist/Toast/index.js.map +1 -1
- package/dist/Toast/index.scss +5 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/paragon.css +1 -1
- package/package.json +1 -1
- package/src/Toast/README.md +4 -4
- package/src/Toast/{Toast.test.jsx → Toast.test.tsx} +23 -13
- package/src/Toast/ToastContainer.scss +2 -1
- package/src/Toast/ToastContainer.tsx +30 -0
- package/src/Toast/index.scss +5 -4
- package/src/Toast/{index.jsx → index.tsx} +27 -6
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/Toast/ToastContainer.jsx +0 -40
package/package.json
CHANGED
package/src/Toast/README.md
CHANGED
|
@@ -5,7 +5,7 @@ components:
|
|
|
5
5
|
- Toast
|
|
6
6
|
categories:
|
|
7
7
|
- Overlays
|
|
8
|
-
status: '
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
56
|
+
<ToastWrapper {...props}>
|
|
59
57
|
Success message.
|
|
60
58
|
</ToastWrapper>,
|
|
61
59
|
);
|
|
62
|
-
const toast = screen.
|
|
60
|
+
const toast = screen.getByRole('alert');
|
|
63
61
|
await userEvent.hover(toast);
|
|
64
62
|
setTimeout(() => {
|
|
65
|
-
expect(screen.getByText('Success message.')).
|
|
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.')).
|
|
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
|
|
74
|
+
<ToastWrapper {...props}>
|
|
77
75
|
Success message.
|
|
78
76
|
</ToastWrapper>,
|
|
79
77
|
);
|
|
80
|
-
const toast = screen.
|
|
78
|
+
const toast = screen.getByRole('alert');
|
|
81
79
|
toast.focus();
|
|
82
80
|
setTimeout(() => {
|
|
83
|
-
expect(screen.getByText('Success message.')).
|
|
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.')).
|
|
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
|
|
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;
|
package/src/Toast/index.scss
CHANGED
|
@@ -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:
|
|
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:
|
|
42
|
+
margin-top: $spacer;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
@media
|
|
46
|
+
@media (max-width: map.get($grid-breakpoints, "md")) {
|
|
46
47
|
max-width: 100%;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
@media
|
|
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,
|
|
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;
|