From 2c68be5148edcdd016d3a95554d93cd4a9ed804f Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:35:58 -0700 Subject: [PATCH] [EuiToast] Wrap overflowing text in titles + perf optimizations (#7568) --- changelogs/upcoming/7568.md | 3 + .../global_toast_list.test.tsx.snap | 8 +- .../toast/__snapshots__/toast.test.tsx.snap | 2 +- src/components/toast/global_toast_list.tsx | 178 +++++++++--------- src/components/toast/toast.styles.ts | 11 +- src/components/toast/toast.tsx | 104 ++++------ 6 files changed, 142 insertions(+), 164 deletions(-) create mode 100644 changelogs/upcoming/7568.md diff --git a/changelogs/upcoming/7568.md b/changelogs/upcoming/7568.md new file mode 100644 index 00000000000..a2f8bbb4a26 --- /dev/null +++ b/changelogs/upcoming/7568.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed `EuiToast` title text to wrap instead of overflowing out of the container diff --git a/src/components/toast/__snapshots__/global_toast_list.test.tsx.snap b/src/components/toast/__snapshots__/global_toast_list.test.tsx.snap index b9b1f969a8e..d35e4ff73ed 100644 --- a/src/components/toast/__snapshots__/global_toast_list.test.tsx.snap +++ b/src/components/toast/__snapshots__/global_toast_list.test.tsx.snap @@ -57,7 +57,7 @@ exports[`EuiGlobalToastList props side can be changed to left 1`] = ` />
a @@ -104,7 +104,7 @@ exports[`EuiGlobalToastList props side can be changed to left 1`] = ` />
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 = ( -