From fd9fdb148e1f2085b40d3f9a7ef35d12adf853c7 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Tue, 18 Feb 2025 17:16:50 +0200 Subject: [PATCH] feat: add typings for --- src/Toast/README.md | 8 ++-- src/Toast/{Toast.test.jsx => Toast.test.tsx} | 48 ++++++-------------- src/Toast/ToastContainer.jsx | 40 ---------------- src/Toast/ToastContainer.scss | 3 +- src/Toast/ToastContainer.tsx | 32 +++++++++++++ src/Toast/index.scss | 9 ++-- src/Toast/{index.jsx => index.tsx} | 37 +++++++++++---- 7 files changed, 87 insertions(+), 90 deletions(-) rename src/Toast/{Toast.test.jsx => Toast.test.tsx} (59%) delete mode 100644 src/Toast/ToastContainer.jsx create mode 100644 src/Toast/ToastContainer.tsx rename src/Toast/{index.jsx => index.tsx} (88%) 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 59% rename from src/Toast/Toast.test.jsx rename to src/Toast/Toast.test.tsx index 9e591054fc..2dcfa6e729 100644 --- a/src/Toast/Toast.test.jsx +++ b/src/Toast/Toast.test.tsx @@ -1,27 +1,23 @@ -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 ( - - {children} - + {children} ); } describe('', () => { - const onCloseHandler = () => {}; + const onCloseHandler = jest.fn(); const props = { onClose: onCloseHandler, show: true, }; + it('renders optional action as link', () => { render( ', () => { Success message. , ); - const toastLink = screen.getByRole('button', { name: 'Optional action' }); expect(toastLink).toBeTruthy(); }); + it('renders optional action as button', () => { render( {}, + onClick: jest.fn(), }} > Success message. @@ -53,40 +49,26 @@ describe('', () => { const toastButton = screen.getByRole('button', { name: 'Optional action' }); expect(toastButton.className).toContain('btn'); }); + it('autohide is set to false on onMouseOver and true on onMouseLeave', async () => { - render( - - Success message. - , - ); - const toast = screen.getByTestId('toast'); + render(Success message.); + const toast = screen.getByRole('alert'); await userEvent.hover(toast); - setTimeout(() => { - expect(screen.getByText('Success message.')).toEqual(true); - expect(toast).toHaveLength(1); - }, 6000); + expect(screen.getByText('Success message.')).toBeTruthy(); await userEvent.unhover(toast); - setTimeout(() => { - expect(screen.getByText('Success message.')).toEqual(false); - expect(toast).toHaveLength(1); - }, 6000); + expect(screen.getByText('Success message.')).toBeTruthy(); }); + it('autohide is set to false onFocus and true onBlur', async () => { render( Success message. , ); - const toast = screen.getByTestId('toast'); + const toast = screen.getByRole('alert'); toast.focus(); - setTimeout(() => { - expect(screen.getByText('Success message.')).toEqual(true); - expect(toast).toHaveLength(1); - }, 6000); + expect(screen.getByText('Success message.')).toBeTruthy(); await userEvent.tab(); - setTimeout(() => { - expect(screen.getByText('Success message.')).toEqual(false); - expect(toast).toHaveLength(1); - }, 6000); + expect(screen.getByText('Success message.')).toBeTruthy(); }); }); 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..b79eb9d53e 100644 --- a/src/Toast/index.jsx +++ b/src/Toast/index.tsx @@ -1,9 +1,8 @@ import React, { useState } from 'react'; -import classNames from 'classnames'; import PropTypes from 'prop-types'; - -import BaseToast from 'react-bootstrap/Toast'; +import classNames from 'classnames'; import { useIntl } from 'react-intl'; +import BaseToast from 'react-bootstrap/Toast'; import { Close } from '../../icons'; import ToastContainer from './ToastContainer'; @@ -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 ( -
+

{children}