diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6867e0de9..bd90a979523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ - Converted observer utility components to TypeScript ([#2009](https://github.com/elastic/eui/pull/2009)) - Converted tool tip components to TypeScript ([#2013](https://github.com/elastic/eui/pull/2013)) - Converted `EuiCopy` to TypeScript ([#2016](https://github.com/elastic/eui/pull/2016)) -- Convert badge and token components to TypeScript ([#2026](https://github.com/elastic/eui/pull/2026)) +- Converted badge and token components to TypeScript ([#2026](https://github.com/elastic/eui/pull/2026)) - Added `magnet` glyph to `EuiIcon` ([2010](https://github.com/elastic/eui/pull/2010)) - Changed `logoAWS` SVG in `EuiIcon` to work better in dark mode ([#2036](https://github.com/elastic/eui/pull/2036)) +- Converted toast components to TypeScript ([#2032](https://github.com/elastic/eui/pull/2032)) **Bug fixes** diff --git a/src/components/index.d.ts b/src/components/index.d.ts index 5a0136e4d88..7e4dde50581 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -26,7 +26,6 @@ /// /// /// -/// declare module '@elastic/eui' { // @ts-ignore diff --git a/src/components/toast/__snapshots__/global_toast_list.test.js.snap b/src/components/toast/__snapshots__/global_toast_list.test.tsx.snap similarity index 100% rename from src/components/toast/__snapshots__/global_toast_list.test.js.snap rename to src/components/toast/__snapshots__/global_toast_list.test.tsx.snap diff --git a/src/components/toast/__snapshots__/toast.test.js.snap b/src/components/toast/__snapshots__/toast.test.tsx.snap similarity index 94% rename from src/components/toast/__snapshots__/toast.test.js.snap rename to src/components/toast/__snapshots__/toast.test.tsx.snap index 7fa051fe776..c1e4992390d 100644 --- a/src/components/toast/__snapshots__/toast.test.js.snap +++ b/src/components/toast/__snapshots__/toast.test.tsx.snap @@ -3,6 +3,7 @@ exports[`EuiToast Props color danger is rendered 1`] = ` + > + test title + @@ -41,6 +44,7 @@ exports[`EuiToast Props color danger is rendered 1`] = ` exports[`EuiToast Props color primary is rendered 1`] = ` + > + test title + @@ -79,6 +85,7 @@ exports[`EuiToast Props color primary is rendered 1`] = ` exports[`EuiToast Props color success is rendered 1`] = ` + > + test title + @@ -117,6 +126,7 @@ exports[`EuiToast Props color success is rendered 1`] = ` exports[`EuiToast Props color warning is rendered 1`] = ` + > + test title + @@ -155,6 +167,7 @@ exports[`EuiToast Props color warning is rendered 1`] = ` exports[`EuiToast Props iconType is rendered 1`] = ` + > + test title + @@ -273,7 +288,9 @@ exports[`EuiToast is rendered 1`] = ` > + > + test title + { test('is rendered', () => { @@ -21,7 +24,7 @@ describe('EuiGlobalToastList', () => { describe('props', () => { describe('toasts', () => { test('is rendered', () => { - const toasts = [ + const toasts: Toast[] = [ { title: 'A', text: 'a', @@ -54,7 +57,7 @@ describe('EuiGlobalToastList', () => { describe('dismissToast', () => { test('is called when a toast is clicked', done => { - const dismissToastSpy = sinon.spy(); + const dismissToastSpy = jest.fn(); const component = mount( { // The callback is invoked once the toast fades from view. setTimeout(() => { - expect(dismissToastSpy.called).toBe(true); + expect(dismissToastSpy).toBeCalled(); done(); }, TOAST_FADE_OUT_MS + 1); }); test('is called when the toast lifetime elapses', done => { const TOAST_LIFE_TIME_MS = 5; - const dismissToastSpy = sinon.spy(); + const dismissToastSpy = jest.fn(); mount( { // The callback is invoked once the toast fades from view. setTimeout(() => { - expect(dismissToastSpy.called).toBe(true); + expect(dismissToastSpy).toBeCalled(); done(); }, TOAST_LIFE_TIME_MS + TOAST_FADE_OUT_MS + 10); }); @@ -105,7 +108,7 @@ describe('EuiGlobalToastList', () => { test('toastLifeTimeMs is overrideable by individidual toasts', done => { const TOAST_LIFE_TIME_MS = 10; const TOAST_LIFE_TIME_MS_OVERRIDE = 100; - const dismissToastSpy = sinon.spy(); + const dismissToastSpy = jest.fn(); mount( { // The callback is invoked once the toast fades from view. setTimeout(() => { - expect(dismissToastSpy.called).toBe(false); + expect(dismissToastSpy).not.toBeCalled(); }, TOAST_LIFE_TIME_MS + TOAST_FADE_OUT_MS + 10); setTimeout(() => { - expect(dismissToastSpy.called).toBe(true); + expect(dismissToastSpy).toBeCalled(); done(); }, TOAST_LIFE_TIME_MS_OVERRIDE + TOAST_FADE_OUT_MS + 10); }); diff --git a/src/components/toast/global_toast_list.js b/src/components/toast/global_toast_list.tsx similarity index 72% rename from src/components/toast/global_toast_list.js rename to src/components/toast/global_toast_list.tsx index 74916ba8f89..d10c8505164 100644 --- a/src/components/toast/global_toast_list.js +++ b/src/components/toast/global_toast_list.tsx @@ -1,50 +1,52 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, ReactChild } from 'react'; import classNames from 'classnames'; +import { CommonProps } from '../common'; import { Timer } from '../../services/time'; -import { IconPropType } from '../icon'; import { EuiGlobalToastListItem } from './global_toast_list_item'; -import { EuiToast } from './toast'; +import { EuiToast, EuiToastProps } from './toast'; export const TOAST_FADE_OUT_MS = 250; -export class EuiGlobalToastList extends Component { - constructor(props) { - super(props); +export interface Toast extends EuiToastProps { + id: string; + text?: ReactChild; + toastLifeTimeMs?: number; +} - this.state = { - toastIdToDismissedMap: {}, - }; +export interface EuiGlobalToastListProps extends CommonProps { + toasts: Toast[]; + dismissToast: (this: EuiGlobalToastList, toast: Toast) => void; + toastLifeTimeMs: number; +} - this.dismissTimeoutIds = []; - this.toastIdToTimerMap = {}; +interface State { + toastIdToDismissedMap: { + [toastId: string]: boolean; + }; +} - this.isScrollingToBottom = false; - this.isScrolledToBottom = true; +export class EuiGlobalToastList extends Component< + EuiGlobalToastListProps, + State +> { + state: State = { + toastIdToDismissedMap: {}, + }; - // See [Return Value](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame#Return_value) - // for information on initial value of 0 - this.isScrollingAnimationFrame = 0; - this.startScrollingAnimationFrame = 0; - } + dismissTimeoutIds: number[] = []; + toastIdToTimerMap: { [toastId: string]: Timer } = {}; - static propTypes = { - className: PropTypes.string, - toasts: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - .isRequired, - title: PropTypes.node, - text: PropTypes.node, - color: PropTypes.string, - iconType: IconPropType, - toastLifeTimeMs: PropTypes.number, - }).isRequired - ), - dismissToast: PropTypes.func.isRequired, - toastLifeTimeMs: PropTypes.number.isRequired, - }; + isScrollingToBottom = false; + isScrolledToBottom = true; + isUserInteracting = false; + + // See [Return Value](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame#Return_value) + // for information on initial value of 0 + isScrollingAnimationFrame = 0; + startScrollingAnimationFrame = 0; + + listElement: Element | null = null; static defaultProps = { toasts: [], @@ -56,7 +58,9 @@ export class EuiGlobalToastList extends Component { const scrollToBottom = () => { // Although we cancel the requestAnimationFrame in componentWillUnmount, // it's possible for this.listElement to become null in the meantime - if (!this.listElement) return; + if (!this.listElement) { + return; + } const position = this.listElement.scrollTop; const destination = @@ -110,9 +114,11 @@ export class EuiGlobalToastList extends Component { }; onScroll = () => { - this.isScrolledToBottom = - this.listElement.scrollHeight - this.listElement.scrollTop === - this.listElement.clientHeight; + if (this.listElement) { + this.isScrolledToBottom = + this.listElement.scrollHeight - this.listElement.scrollTop === + this.listElement.clientHeight; + } }; scheduleAllToastsForDismissal = () => { @@ -123,7 +129,7 @@ export class EuiGlobalToastList extends Component { }); }; - scheduleToastForDismissal = toast => { + scheduleToastForDismissal = (toast: Toast) => { // Start fading the toast out once its lifetime elapses. this.toastIdToTimerMap[toast.id] = new Timer( this.dismissToast.bind(this, toast), @@ -133,16 +139,16 @@ export class EuiGlobalToastList extends Component { ); }; - dismissToast = toast => { + dismissToast = (toast: Toast) => { // Remove the toast after it's done fading out. this.dismissTimeoutIds.push( - setTimeout(() => { + window.setTimeout(() => { // Because this is wrapped in a setTimeout, and because React does not guarantee when // state updates happen, it is possible to double-dismiss a toast // including by double-clicking the "x" button on the toast // so, first check to make sure we haven't already dismissed this toast if (this.toastIdToTimerMap.hasOwnProperty(toast.id)) { - this.props.dismissToast(toast); + this.props.dismissToast.apply(this, [toast]); this.toastIdToTimerMap[toast.id].clear(); delete this.toastIdToTimerMap[toast.id]; @@ -173,13 +179,15 @@ export class EuiGlobalToastList extends Component { }; componentDidMount() { - this.listElement.addEventListener('scroll', this.onScroll); - this.listElement.addEventListener('mouseenter', this.onMouseEnter); - this.listElement.addEventListener('mouseleave', this.onMouseLeave); + if (this.listElement) { + this.listElement.addEventListener('scroll', this.onScroll); + this.listElement.addEventListener('mouseenter', this.onMouseEnter); + this.listElement.addEventListener('mouseleave', this.onMouseLeave); + } this.scheduleAllToastsForDismissal(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: EuiGlobalToastListProps) { this.scheduleAllToastsForDismissal(); if (!this.isUserInteracting) { @@ -200,9 +208,11 @@ export class EuiGlobalToastList extends Component { if (this.startScrollingAnimationFrame !== 0) { window.cancelAnimationFrame(this.startScrollingAnimationFrame); } - this.listElement.removeEventListener('scroll', this.onScroll); - this.listElement.removeEventListener('mouseenter', this.onMouseEnter); - this.listElement.removeEventListener('mouseleave', this.onMouseLeave); + if (this.listElement) { + this.listElement.removeEventListener('scroll', this.onScroll); + this.listElement.removeEventListener('mouseenter', this.onMouseEnter); + this.listElement.removeEventListener('mouseleave', this.onMouseLeave); + } this.dismissTimeoutIds.forEach(clearTimeout); for (const toastId in this.toastIdToTimerMap) { if (this.toastIdToTimerMap.hasOwnProperty(toastId)) { diff --git a/src/components/toast/index.d.ts b/src/components/toast/index.d.ts deleted file mode 100644 index 6bf7e9030fc..00000000000 --- a/src/components/toast/index.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - EuiGlobalToastListItemProps as ToastListItemProps, - EuiGlobalToastListItem as ToastListItem, -} from './global_toast_list_item'; -import { CommonProps, Omit } from '../common'; -import { IconType } from '../icon'; - -import { - Component, - FunctionComponent, - HTMLAttributes, - ReactChild, - ReactNode, -} from 'react'; - -declare module '@elastic/eui' { - /** - * EuiToast type def - * - * @see './toast.js' - */ - export interface EuiToastProps - extends CommonProps, - Omit, 'title'> { - title?: ReactNode; - color?: 'primary' | 'success' | 'warning' | 'danger'; - iconType?: IconType; - onClose?: () => void; - } - - export const EuiToast: FunctionComponent; - export interface EuiGlobalToastListItemProps extends ToastListItemProps {} - export const EuiGlobalToastListItem: typeof ToastListItem; - - /** - * EuiGlobalToastList type def - * - * @see './global_toast_list.js' - */ - export interface Toast extends EuiToastProps { - id: string; - text?: ReactChild; - toastLifeTimeMs?: number; - } - - export interface EuiGlobalToastListProps { - toasts?: Toast[]; - dismissToast: (this: EuiGlobalToastList, toast: Toast) => void; - toastLifeTimeMs: number; - } - - export class EuiGlobalToastList extends Component { - scheduleAllToastsForDismissal(): void; - scheduleToastForDismissal(toast: Toast): void; - dismissToast(toast: Toast): void; - } -} diff --git a/src/components/toast/index.js b/src/components/toast/index.ts similarity index 100% rename from src/components/toast/index.js rename to src/components/toast/index.ts diff --git a/src/components/toast/toast.test.js b/src/components/toast/toast.test.tsx similarity index 73% rename from src/components/toast/toast.test.js rename to src/components/toast/toast.test.tsx index 23983a495ba..af2d1b5c585 100644 --- a/src/components/toast/toast.test.js +++ b/src/components/toast/toast.test.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { render, mount } from 'enzyme'; -import sinon from 'sinon'; import { findTestSubject, requiredProps } from '../../test'; import { COLORS, EuiToast } from './toast'; @@ -8,7 +7,7 @@ import { COLORS, EuiToast } from './toast'; describe('EuiToast', () => { test('is rendered', () => { const component = render( - + Hi ); @@ -27,7 +26,7 @@ describe('EuiToast', () => { describe('color', () => { COLORS.forEach(color => { test(`${color} is rendered`, () => { - const component = ; + const component = ; expect(mount(component)).toMatchSnapshot(); }); }); @@ -35,20 +34,22 @@ describe('EuiToast', () => { describe('iconType', () => { test('is rendered', () => { - const component = ; + const component = ; expect(mount(component)).toMatchSnapshot(); }); }); describe('onClose', () => { test('is called when the close button is clicked', () => { - const onCloseHandler = sinon.stub(); + const onCloseHandler = jest.fn(); - const component = mount(); + const component = mount( + + ); const closeButton = findTestSubject(component, 'toastCloseButton'); closeButton.simulate('click'); - sinon.assert.calledOnce(onCloseHandler); + expect(onCloseHandler).toBeCalledTimes(1); }); }); }); diff --git a/src/components/toast/toast.js b/src/components/toast/toast.tsx similarity index 69% rename from src/components/toast/toast.js rename to src/components/toast/toast.tsx index 87cac47e24f..be2d69aeef9 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.tsx @@ -1,24 +1,40 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { + FunctionComponent, + HTMLAttributes, + ReactElement, + ReactNode, +} from 'react'; import classNames from 'classnames'; +import { CommonProps, keysOf, Omit } from '../common'; import { EuiScreenReaderOnly } from '../accessibility'; import { EuiI18n } from '../i18n'; -import { IconPropType, EuiIcon } from '../icon'; +import { IconType, EuiIcon } from '../icon'; import { EuiText } from '../text'; -const colorToClassNameMap = { +type ToastColor = 'primary' | 'success' | 'warning' | 'danger'; + +const colorToClassNameMap: { [color in ToastColor]: string } = { primary: 'euiToast--primary', success: 'euiToast--success', warning: 'euiToast--warning', danger: 'euiToast--danger', }; -export const COLORS = Object.keys(colorToClassNameMap); +export const COLORS = keysOf(colorToClassNameMap); + +export interface EuiToastProps + extends CommonProps, + Omit, 'title'> { + title?: ReactNode; + color?: ToastColor; + iconType?: IconType; + onClose?: () => void; +} -export const EuiToast = ({ +export const EuiToast: FunctionComponent = ({ title, color, iconType, @@ -27,12 +43,16 @@ export const EuiToast = ({ className, ...rest }) => { - const classes = classNames('euiToast', colorToClassNameMap[color], className); + const classes = classNames( + 'euiToast', + color ? colorToClassNameMap[color] : null, + className + ); const headerClasses = classNames('euiToastHeader', { 'euiToastHeader--withBody': children, }); - let headerIcon; + let headerIcon: ReactElement; if (iconType) { headerIcon = ( @@ -50,7 +70,7 @@ export const EuiToast = ({ if (onClose) { closeButton = ( - {dismissToast => ( + {(dismissToast: string) => ( - {notification => ( + {(notification: string) => ( ); }; - -EuiToast.propTypes = { - title: PropTypes.node, - iconType: IconPropType, - color: PropTypes.oneOf(COLORS), - onClose: PropTypes.func, - children: PropTypes.node, -};
Hi