b
@@ -160,7 +160,7 @@ exports[`EuiGlobalToastList props toasts is rendered 1`] = `
/>
a
@@ -207,7 +207,7 @@ exports[`EuiGlobalToastList props toasts is rendered 1`] = `
/>
b
diff --git a/src/components/toast/__snapshots__/toast.test.tsx.snap b/src/components/toast/__snapshots__/toast.test.tsx.snap
index 7e0fec703f4..6c530cda117 100644
--- a/src/components/toast/__snapshots__/toast.test.tsx.snap
+++ b/src/components/toast/__snapshots__/toast.test.tsx.snap
@@ -173,7 +173,7 @@ exports[`EuiToast is rendered 1`] = `
diff --git a/src/components/toast/global_toast_list.tsx b/src/components/toast/global_toast_list.tsx
index d623badfd0a..6238a14c42a 100644
--- a/src/components/toast/global_toast_list.tsx
+++ b/src/components/toast/global_toast_list.tsx
@@ -12,13 +12,14 @@ import React, {
ReactNode,
useCallback,
useEffect,
+ useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { CommonProps, keysOf } from '../common';
-import { useEuiTheme } from '../../services';
+import { useEuiMemoizedStyles } from '../../services';
import { Timer } from '../../services/time';
import { EuiGlobalToastListItem } from './global_toast_list_item';
import { EuiToast, EuiToastProps } from './toast';
@@ -107,11 +108,10 @@ export const EuiGlobalToastList: FunctionComponent = ({
const listElement = useRef(null);
- const euiTheme = useEuiTheme();
- const styles = euiGlobalToastListStyles(euiTheme);
+ const styles = useEuiMemoizedStyles(euiGlobalToastListStyles);
const cssStyles = [styles.euiGlobalToastList, styles[side]];
- const startScrollingToBottom = () => {
+ const startScrollingToBottom = useCallback(() => {
isScrollingToBottom.current = true;
const scrollToBottom = () => {
@@ -143,9 +143,9 @@ export const EuiGlobalToastList: FunctionComponent = ({
startScrollingAnimationFrame.current =
window.requestAnimationFrame(scrollToBottom);
- };
+ }, []);
- const onMouseEnter = () => {
+ const onMouseEnter = useCallback(() => {
// Stop scrolling to bottom if we're in mid-scroll, because the user wants to interact with
// the list.
isScrollingToBottom.current = false;
@@ -158,9 +158,9 @@ export const EuiGlobalToastList: FunctionComponent = ({
timer.pause();
}
}
- };
+ }, []);
- const onMouseLeave = () => {
+ const onMouseLeave = useCallback(() => {
isUserInteracting.current = false;
for (const toastId in toastIdToTimerMap.current) {
if (toastIdToTimerMap.current.hasOwnProperty(toastId)) {
@@ -168,9 +168,9 @@ export const EuiGlobalToastList: FunctionComponent = ({
timer.resume();
}
}
- };
+ }, []);
- const onScroll = () => {
+ const onScroll = useCallback(() => {
// Given that this method also gets invoked by the synthetic scroll that happens when a new toast gets added,
// we want to evaluate if the scroll bottom has been reached only when the user is interacting with the toast,
// this way we always retain the scroll position the user has set despite adding in new toasts.
@@ -180,7 +180,7 @@ export const EuiGlobalToastList: FunctionComponent = ({
listElement.current.scrollHeight - listElement.current.scrollTop ===
listElement.current.clientHeight;
}
- };
+ }, []);
const dismissToast = useCallback((toast: Toast) => {
// Remove the toast after it's done fading out.
@@ -215,35 +215,28 @@ export const EuiGlobalToastList: FunctionComponent = ({
});
}, [scheduleToastForDismissal, toasts]);
- const addListeners = () => {
- if (listElement.current) {
- listElement.current.addEventListener('scroll', onScroll);
- listElement.current.addEventListener('mouseenter', onMouseEnter);
- listElement.current.addEventListener('mouseleave', onMouseLeave);
- }
- };
-
- const removeListeners = () => {
- if (listElement.current) {
- listElement.current.removeEventListener('scroll', onScroll);
- listElement.current.removeEventListener('mouseenter', onMouseEnter);
- listElement.current.removeEventListener('mouseleave', onMouseLeave);
- }
- };
-
// componentDidMount
useEffect(() => {
- addListeners();
+ const listenerEl = listElement.current;
+ if (listenerEl) {
+ listenerEl.addEventListener('scroll', onScroll);
+ listenerEl.addEventListener('mouseenter', onMouseEnter);
+ listenerEl.addEventListener('mouseleave', onMouseLeave);
+ }
// componentWillUnmount
return () => {
+ if (listenerEl) {
+ listenerEl.removeEventListener('scroll', onScroll);
+ listenerEl.removeEventListener('mouseenter', onMouseEnter);
+ listenerEl.removeEventListener('mouseleave', onMouseLeave);
+ }
if (isScrollingAnimationFrame.current !== 0) {
window.cancelAnimationFrame(isScrollingAnimationFrame.current);
}
if (startScrollingAnimationFrame.current !== 0) {
window.cancelAnimationFrame(startScrollingAnimationFrame.current);
}
- removeListeners();
dismissTimeoutIds.current.forEach(clearTimeout); // eslint-disable-line react-hooks/exhaustive-deps
for (const toastId in toastIdToTimerMap.current) {
if (toastIdToTimerMap.current.hasOwnProperty(toastId)) {
@@ -252,7 +245,7 @@ export const EuiGlobalToastList: FunctionComponent = ({
}
}
};
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [onMouseEnter, onMouseLeave, onScroll]);
// componentDidUpdate
useEffect(() => {
@@ -268,7 +261,7 @@ export const EuiGlobalToastList: FunctionComponent = ({
}
}
prevToasts.current = toasts;
- }, [toasts, scheduleAllToastsForDismissal]);
+ }, [toasts, scheduleAllToastsForDismissal, startScrollingToBottom]);
// Toast dismissal side effect
// Ensure the callback has correct state by not enclosing it in `setTimeout`
@@ -294,62 +287,76 @@ export const EuiGlobalToastList: FunctionComponent = ({
}
}, [toastToDismiss, dismissToastProp]);
- const renderedToasts = toasts.map((toast) => {
- const { text, toastLifeTimeMs, ...rest } = toast;
- const onClose = () => dismissToast(toast);
-
- return (
-
-
- {text}
-
-
- );
- });
-
- if (showClearAllButtonAt && toasts.length >= showClearAllButtonAt) {
- const dismissAllToasts = () => {
- toasts.forEach((toast) => dismissToastProp(toast));
- onClearAllToasts?.();
- };
-
- renderedToasts.push(
-
- {([
- clearAllToastsButtonAriaLabel,
- clearAllToastsButtonDisplayText,
- ]: string[]) => (
-
-
+ toasts.map((toast) => {
+ const { text, toastLifeTimeMs, ...rest } = toast;
+ const onClose = () => dismissToast(toast);
+
+ return (
+
+
- {clearAllToastsButtonDisplayText}
-
+ {text}
+
- )}
-
- );
- }
+ );
+ }),
+ [toasts, toastIdToDismissedMap, dismissToast, onMouseEnter, onMouseLeave]
+ );
+
+ const clearAllButton = useMemo(() => {
+ if (
+ toasts.length &&
+ showClearAllButtonAt &&
+ toasts.length >= showClearAllButtonAt
+ ) {
+ return (
+
+ {([
+ clearAllToastsButtonAriaLabel,
+ clearAllToastsButtonDisplayText,
+ ]: string[]) => (
+
+ {
+ toasts.forEach((toast) => dismissToastProp(toast));
+ onClearAllToasts?.();
+ }}
+ css={styles.euiGlobalToastListDismissButton}
+ aria-label={clearAllToastsButtonAriaLabel}
+ data-test-subj="euiClearAllToastsButton"
+ >
+ {clearAllToastsButtonDisplayText}
+
+
+ )}
+
+ );
+ }
+ }, [
+ showClearAllButtonAt,
+ onClearAllToasts,
+ toasts,
+ dismissToastProp,
+ styles,
+ ]);
const classes = classNames('euiGlobalToastList', className);
@@ -363,6 +370,7 @@ export const EuiGlobalToastList: FunctionComponent = ({
{...rest}
>
{renderedToasts}
+ {clearAllButton}
);
};
diff --git a/src/components/toast/toast.styles.ts b/src/components/toast/toast.styles.ts
index ccaf3413f67..7ad7279b6af 100644
--- a/src/components/toast/toast.styles.ts
+++ b/src/components/toast/toast.styles.ts
@@ -7,7 +7,7 @@
*/
import { css } from '@emotion/react';
-import { logicalCSS } from '../../global_styling';
+import { euiTextBreakWord, logicalCSS } from '../../global_styling';
import { UseEuiTheme } from '../../services';
import { euiShadowLarge } from '../../themes/amsterdam';
import { euiTitle } from '../title/title.styles';
@@ -27,6 +27,8 @@ export const euiToastStyles = (euiThemeContext: UseEuiTheme) => {
background-color: ${euiTheme.colors.emptyShade};
${logicalCSS('width', '100%')}
+ ${euiTextBreakWord()} /* Prevent long lines from overflowing */
+
&:hover,
&:focus {
[class*='euiToast__closeButton'] {
@@ -90,10 +92,3 @@ export const euiToastHeaderStyles = (euiThemeContext: UseEuiTheme) => {
`,
};
};
-
-export const euiToastBodyStyles = () => ({
- // Base
- euiToastBody: css`
- word-wrap: break-word; /* Prevent long lines from overflowing */
- `,
-});
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
index 09935b0f338..e8f555aa74a 100644
--- a/src/components/toast/toast.tsx
+++ b/src/components/toast/toast.tsx
@@ -6,15 +6,10 @@
* Side Public License, v 1.
*/
-import React, {
- FunctionComponent,
- HTMLAttributes,
- ReactElement,
- ReactNode,
-} from 'react';
+import React, { FunctionComponent, HTMLAttributes, ReactNode } from 'react';
import classNames from 'classnames';
-import { useEuiTheme } from '../../services';
+import { useEuiMemoizedStyles } from '../../services';
import { CommonProps } from '../common';
import { EuiScreenReaderOnly } from '../accessibility';
import { EuiButtonIcon } from '../button';
@@ -22,11 +17,7 @@ import { EuiI18n } from '../i18n';
import { IconType, EuiIcon } from '../icon';
import { EuiText } from '../text';
-import {
- euiToastStyles,
- euiToastBodyStyles,
- euiToastHeaderStyles,
-} from './toast.styles';
+import { euiToastStyles, euiToastHeaderStyles } from './toast.styles';
export const COLORS = ['primary', 'success', 'warning', 'danger'] as const;
@@ -50,11 +41,9 @@ export const EuiToast: FunctionComponent
= ({
className,
...rest
}) => {
- const euiTheme = useEuiTheme();
- const baseStyles = euiToastStyles(euiTheme);
+ const baseStyles = useEuiMemoizedStyles(euiToastStyles);
const baseCss = [baseStyles.euiToast, color && baseStyles[color]];
- const bodyStyles = euiToastBodyStyles();
- const headerStyles = euiToastHeaderStyles(euiTheme);
+ const headerStyles = useEuiMemoizedStyles(euiToastHeaderStyles);
const headerCss = [
headerStyles.euiToastHeader,
children && headerStyles.withBody,
@@ -62,55 +51,9 @@ export const EuiToast: FunctionComponent = ({
const classes = classNames('euiToast', className);
- let headerIcon: ReactElement;
-
- if (iconType) {
- headerIcon = (
-
- );
- }
-
- let closeButton;
-
- if (onClose) {
- closeButton = (
-
- {(dismissToast: string) => (
-
- )}
-
- );
- }
-
- let optionalBody;
-
- if (children) {
- optionalBody = (
-
- {children}
-
- );
- }
-
return (
+ {/* Screen reader announcement */}
= ({
+ {/* Header */}
{(notification: string) => (
= ({
aria-label={notification}
data-test-subj="euiToastHeader"
>
- {headerIcon}
+ {iconType && (
+
+ )}
= ({
)}
- {closeButton}
- {optionalBody}
+ {/* Close button */}
+ {onClose && (
+
+ {(dismissToast: string) => (
+
+ )}
+
+ )}
+
+ {/* Body */}
+ {children && (
+
+ {children}
+
+ )}
);
};