Skip to content

Commit

Permalink
[EuiToast] Wrap overflowing text in titles + perf optimizations (#7568)
Browse files Browse the repository at this point in the history
  • Loading branch information
cee-chen authored Mar 12, 2024
1 parent 779bf2d commit 2c68be5
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 164 deletions.
3 changes: 3 additions & 0 deletions changelogs/upcoming/7568.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Bug fixes**

- Fixed `EuiToast` title text to wrap instead of overflowing out of the container
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ exports[`EuiGlobalToastList props side can be changed to left 1`] = `
/>
</button>
<div
class="euiText emotion-euiText-s-euiToastBody"
class="euiText emotion-euiText-s"
data-test-subj="euiToastBody"
>
a
Expand Down Expand Up @@ -104,7 +104,7 @@ exports[`EuiGlobalToastList props side can be changed to left 1`] = `
/>
</button>
<div
class="euiText emotion-euiText-s-euiToastBody"
class="euiText emotion-euiText-s"
data-test-subj="euiToastBody"
>
b
Expand Down Expand Up @@ -160,7 +160,7 @@ exports[`EuiGlobalToastList props toasts is rendered 1`] = `
/>
</button>
<div
class="euiText emotion-euiText-s-euiToastBody"
class="euiText emotion-euiText-s"
data-test-subj="euiToastBody"
>
a
Expand Down Expand Up @@ -207,7 +207,7 @@ exports[`EuiGlobalToastList props toasts is rendered 1`] = `
/>
</button>
<div
class="euiText emotion-euiText-s-euiToastBody"
class="euiText emotion-euiText-s"
data-test-subj="euiToastBody"
>
b
Expand Down
2 changes: 1 addition & 1 deletion src/components/toast/__snapshots__/toast.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ exports[`EuiToast is rendered 1`] = `
</span>
</div>
<div
class="euiText emotion-euiText-s-euiToastBody"
class="euiText emotion-euiText-s"
data-test-subj="euiToastBody"
>
<p>
Expand Down
178 changes: 93 additions & 85 deletions src/components/toast/global_toast_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -107,11 +108,10 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({

const listElement = useRef<HTMLDivElement | null>(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 = () => {
Expand Down Expand Up @@ -143,9 +143,9 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({

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;
Expand All @@ -158,19 +158,19 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
timer.pause();
}
}
};
}, []);

const onMouseLeave = () => {
const onMouseLeave = useCallback(() => {
isUserInteracting.current = false;
for (const toastId in toastIdToTimerMap.current) {
if (toastIdToTimerMap.current.hasOwnProperty(toastId)) {
const timer = toastIdToTimerMap.current[toastId];
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.
Expand All @@ -180,7 +180,7 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
listElement.current.scrollHeight - listElement.current.scrollTop ===
listElement.current.clientHeight;
}
};
}, []);

const dismissToast = useCallback((toast: Toast) => {
// Remove the toast after it's done fading out.
Expand Down Expand Up @@ -215,35 +215,28 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
});
}, [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)) {
Expand All @@ -252,7 +245,7 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
}
}
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}, [onMouseEnter, onMouseLeave, onScroll]);

// componentDidUpdate
useEffect(() => {
Expand All @@ -268,7 +261,7 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
}
}
prevToasts.current = toasts;
}, [toasts, scheduleAllToastsForDismissal]);
}, [toasts, scheduleAllToastsForDismissal, startScrollingToBottom]);

// Toast dismissal side effect
// Ensure the callback has correct state by not enclosing it in `setTimeout`
Expand All @@ -294,62 +287,76 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
}
}, [toastToDismiss, dismissToastProp]);

const renderedToasts = toasts.map((toast) => {
const { text, toastLifeTimeMs, ...rest } = toast;
const onClose = () => dismissToast(toast);

return (
<EuiGlobalToastListItem
key={toast.id}
isDismissed={toastIdToDismissedMap[toast.id]}
>
<EuiToast
onClose={onClose}
onFocus={onMouseEnter}
onBlur={onMouseLeave}
{...rest}
>
{text}
</EuiToast>
</EuiGlobalToastListItem>
);
});

if (showClearAllButtonAt && toasts.length >= showClearAllButtonAt) {
const dismissAllToasts = () => {
toasts.forEach((toast) => dismissToastProp(toast));
onClearAllToasts?.();
};

renderedToasts.push(
<EuiI18n
key="euiClearAllToasts"
tokens={[
'euiGlobalToastList.clearAllToastsButtonAriaLabel',
'euiGlobalToastList.clearAllToastsButtonDisplayText',
]}
defaults={['Clear all toast notifications', 'Clear all']}
>
{([
clearAllToastsButtonAriaLabel,
clearAllToastsButtonDisplayText,
]: string[]) => (
<EuiGlobalToastListItem isDismissed={false}>
<EuiButton
fill
color="text"
onClick={dismissAllToasts}
css={[styles.euiGlobalToastListDismissButton]}
aria-label={clearAllToastsButtonAriaLabel}
data-test-subj="euiClearAllToastsButton"
const renderedToasts = useMemo(
() =>
toasts.map((toast) => {
const { text, toastLifeTimeMs, ...rest } = toast;
const onClose = () => dismissToast(toast);

return (
<EuiGlobalToastListItem
key={toast.id}
isDismissed={toastIdToDismissedMap[toast.id]}
>
<EuiToast
onClose={onClose}
onFocus={onMouseEnter}
onBlur={onMouseLeave}
{...rest}
>
{clearAllToastsButtonDisplayText}
</EuiButton>
{text}
</EuiToast>
</EuiGlobalToastListItem>
)}
</EuiI18n>
);
}
);
}),
[toasts, toastIdToDismissedMap, dismissToast, onMouseEnter, onMouseLeave]
);

const clearAllButton = useMemo(() => {
if (
toasts.length &&
showClearAllButtonAt &&
toasts.length >= showClearAllButtonAt
) {
return (
<EuiI18n
key="euiClearAllToasts"
tokens={[
'euiGlobalToastList.clearAllToastsButtonAriaLabel',
'euiGlobalToastList.clearAllToastsButtonDisplayText',
]}
defaults={['Clear all toast notifications', 'Clear all']}
>
{([
clearAllToastsButtonAriaLabel,
clearAllToastsButtonDisplayText,
]: string[]) => (
<EuiGlobalToastListItem isDismissed={false}>
<EuiButton
fill
color="text"
onClick={() => {
toasts.forEach((toast) => dismissToastProp(toast));
onClearAllToasts?.();
}}
css={styles.euiGlobalToastListDismissButton}
aria-label={clearAllToastsButtonAriaLabel}
data-test-subj="euiClearAllToastsButton"
>
{clearAllToastsButtonDisplayText}
</EuiButton>
</EuiGlobalToastListItem>
)}
</EuiI18n>
);
}
}, [
showClearAllButtonAt,
onClearAllToasts,
toasts,
dismissToastProp,
styles,
]);

const classes = classNames('euiGlobalToastList', className);

Expand All @@ -363,6 +370,7 @@ export const EuiGlobalToastList: FunctionComponent<EuiGlobalToastListProps> = ({
{...rest}
>
{renderedToasts}
{clearAllButton}
</div>
);
};
11 changes: 3 additions & 8 deletions src/components/toast/toast.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'] {
Expand Down Expand Up @@ -90,10 +92,3 @@ export const euiToastHeaderStyles = (euiThemeContext: UseEuiTheme) => {
`,
};
};

export const euiToastBodyStyles = () => ({
// Base
euiToastBody: css`
word-wrap: break-word; /* Prevent long lines from overflowing */
`,
});
Loading

0 comments on commit 2c68be5

Please sign in to comment.