Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiToast] Wrap overflowing text in titles + perf optimizations #7568

Merged
merged 5 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading