diff --git a/src/Toast/README.md b/src/Toast/README.md
index 9b36e17520..cbf867b713 100644
--- a/src/Toast/README.md
+++ b/src/Toast/README.md
@@ -5,7 +5,7 @@ components:
- Toast
categories:
- Overlays
-status: 'New'
+status: 'Stable'
designStatus: 'Done'
devStatus: 'Done'
notes: ''
@@ -39,7 +39,7 @@ notes: ''
Example of a basic Toast.
-
+
>
);
}
@@ -64,7 +64,7 @@ notes: ''
Success! Example of a Toast with a button.
-
+
>
);
}
@@ -89,7 +89,7 @@ notes: ''
Success! Example of a Toast with a link.
-
+
>
);
}
diff --git a/src/Toast/Toast.test.jsx b/src/Toast/Toast.test.tsx
similarity index 76%
rename from src/Toast/Toast.test.jsx
rename to src/Toast/Toast.test.tsx
index 9e591054fc..dabf881f84 100644
--- a/src/Toast/Toast.test.jsx
+++ b/src/Toast/Toast.test.tsx
@@ -1,12 +1,10 @@
-import React from 'react';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import userEvent from '@testing-library/user-event';
import Toast from '.';
-/* eslint-disable-next-line react/prop-types */
-function ToastWrapper({ children, ...props }) {
+function ToastWrapper({ children, ...props }: React.ComponentProps) {
return (
@@ -17,7 +15,7 @@ function ToastWrapper({ children, ...props }) {
}
describe('', () => {
- const onCloseHandler = () => {};
+ const onCloseHandler = jest.fn();
const props = {
onClose: onCloseHandler,
show: true,
@@ -44,7 +42,7 @@ describe('', () => {
{...props}
action={{
label: 'Optional action',
- onClick: () => {},
+ onClick: jest.fn(),
}}
>
Success message.
@@ -55,19 +53,19 @@ describe('', () => {
});
it('autohide is set to false on onMouseOver and true on onMouseLeave', async () => {
render(
-
+
Success message.
,
);
- const toast = screen.getByTestId('toast');
+ const toast = screen.getByRole('alert');
await userEvent.hover(toast);
setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(true);
+ expect(screen.getByText('Success message.')).toBeTruthy();
expect(toast).toHaveLength(1);
}, 6000);
await userEvent.unhover(toast);
setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(false);
+ expect(screen.getByText('Success message.')).toBeTruthy();
expect(toast).toHaveLength(1);
}, 6000);
});
@@ -77,15 +75,15 @@ describe('', () => {
Success message.
,
);
- const toast = screen.getByTestId('toast');
+ const toast = screen.getByRole('alert');
toast.focus();
setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(true);
+ expect(screen.getByText('Success message.')).toBeTruthy();
expect(toast).toHaveLength(1);
}, 6000);
await userEvent.tab();
setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(false);
+ expect(screen.getByText('Success message.')).toBeTruthy();
expect(toast).toHaveLength(1);
}, 6000);
});
diff --git a/src/Toast/ToastContainer.jsx b/src/Toast/ToastContainer.jsx
deleted file mode 100644
index 05049ae0f4..0000000000
--- a/src/Toast/ToastContainer.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-
-class ToastContainer extends React.Component {
- constructor(props) {
- super(props);
- this.toastRootName = 'toast-root';
- if (typeof document === 'undefined') {
- this.rootElement = null;
- } else if (document.getElementById(this.toastRootName)) {
- this.rootElement = document.getElementById(this.toastRootName);
- } else {
- const rootElement = document.createElement('div');
- rootElement.setAttribute('id', this.toastRootName);
- rootElement.setAttribute('class', 'toast-container');
- rootElement.setAttribute('role', 'alert');
- rootElement.setAttribute('aria-live', 'polite');
- rootElement.setAttribute('aria-atomic', 'true');
- this.rootElement = document.body.appendChild(rootElement);
- }
- }
-
- render() {
- if (this.rootElement) {
- return ReactDOM.createPortal(
- this.props.children,
- this.rootElement,
- );
- }
- return null;
- }
-}
-
-ToastContainer.propTypes = {
- /** Specifies contents of the component. */
- children: PropTypes.node.isRequired,
-};
-
-export default ToastContainer;
diff --git a/src/Toast/ToastContainer.scss b/src/Toast/ToastContainer.scss
index 423b419044..e7d4f2b4fd 100644
--- a/src/Toast/ToastContainer.scss
+++ b/src/Toast/ToastContainer.scss
@@ -1,3 +1,4 @@
+@use "sass:map";
@import "variables";
.toast-container {
@@ -11,7 +12,7 @@
left: 0;
}
- @media only screen and (width <= 768px) {
+ @media (max-width: map.get($grid-breakpoints, "md")) {
bottom: $toast-container-gutter-sm;
right: $toast-container-gutter-sm;
left: $toast-container-gutter-sm;
diff --git a/src/Toast/ToastContainer.tsx b/src/Toast/ToastContainer.tsx
new file mode 100644
index 0000000000..522c3696fd
--- /dev/null
+++ b/src/Toast/ToastContainer.tsx
@@ -0,0 +1,32 @@
+import { ReactNode, useEffect, useState } from 'react';
+import ReactDOM from 'react-dom';
+
+interface ToastContainerProps {
+ children: ReactNode;
+}
+
+const TOAST_ROOT_ID = 'toast-root';
+
+function ToastContainer({ children }: ToastContainerProps) {
+ const [rootElement, setRootElement] = useState(null);
+
+ useEffect(() => {
+ if (typeof document !== 'undefined') {
+ let existingElement = document.getElementById(TOAST_ROOT_ID);
+
+ if (!existingElement) {
+ existingElement = document.createElement('div');
+ existingElement.id = TOAST_ROOT_ID;
+ existingElement.className = 'toast-container';
+ existingElement.setAttribute('aria-live', 'polite');
+ existingElement.setAttribute('aria-atomic', 'true');
+ document.body.appendChild(existingElement);
+ }
+ setRootElement(existingElement);
+ }
+ }, []);
+
+ return rootElement ? ReactDOM.createPortal(children, rootElement) : null;
+}
+
+export default ToastContainer;
diff --git a/src/Toast/index.scss b/src/Toast/index.scss
index ffce11dbf1..f178287c23 100644
--- a/src/Toast/index.scss
+++ b/src/Toast/index.scss
@@ -1,3 +1,4 @@
+@use "sass:map";
@import "variables";
@import "~bootstrap/scss/toasts";
@@ -5,7 +6,7 @@
background-color: $toast-background-color;
box-shadow: $toast-box-shadow;
margin: 0;
- padding: 1rem;
+ padding: $spacer;
position: relative;
border-radius: $toast-border-radius;
z-index: 2;
@@ -38,15 +39,15 @@
}
& + .btn {
- margin-top: 1rem;
+ margin-top: $spacer;
}
}
- @media only screen and (width <= 768px) {
+ @media (max-width: map.get($grid-breakpoints, "md")) {
max-width: 100%;
}
- @media only screen and (width >= 768px) {
+ @media (min-width: map.get($grid-breakpoints, "md")) {
min-width: $toast-max-width;
max-width: $toast-max-width;
}
diff --git a/src/Toast/index.jsx b/src/Toast/index.tsx
similarity index 88%
rename from src/Toast/index.jsx
rename to src/Toast/index.tsx
index 11461666c2..1ac20e6942 100644
--- a/src/Toast/index.jsx
+++ b/src/Toast/index.tsx
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
-
import BaseToast from 'react-bootstrap/Toast';
import { useIntl } from 'react-intl';
@@ -14,16 +13,40 @@ import IconButton from '../IconButton';
export const TOAST_CLOSE_LABEL_TEXT = 'Close';
export const TOAST_DELAY = 5000;
+interface ToastAction {
+ label: string;
+ href?: string;
+ onClick?: () => void;
+}
+
+interface ToastProps {
+ children: string;
+ onClose: () => void;
+ show: boolean;
+ action?: ToastAction;
+ closeLabel?: string;
+ delay?: number;
+ className?: string;
+}
+
function Toast({
- action, children, className, closeLabel, onClose, show, ...rest
-}) {
+ action,
+ children,
+ className,
+ closeLabel,
+ onClose,
+ show,
+ ...rest
+}: ToastProps) {
const intl = useIntl();
const [autoHide, setAutoHide] = useState(true);
+
const intlCloseLabel = closeLabel || intl.formatMessage({
id: 'pgn.Toast.closeLabel',
defaultMessage: 'Close',
description: 'Close label for Toast component',
});
+
return (
-