diff --git a/src-docs/src/views/call_out/call_out_example.js b/src-docs/src/views/call_out/call_out_example.js
index feda7223691..97f563aceb3 100644
--- a/src-docs/src/views/call_out/call_out_example.js
+++ b/src-docs/src/views/call_out/call_out_example.js
@@ -46,6 +46,15 @@ const dangerSnippet = [
`,
];
+import OnDismiss from './on_dismiss';
+const onDismissSource = require('!!raw-loader!./on_dismiss');
+const onDismissSnippet = [
+ `
+
+
+`,
+];
+
export const CallOutExample = {
title: 'Callout',
intro: (
@@ -164,5 +173,23 @@ export const CallOutExample = {
snippet: dangerSnippet,
demo: ,
},
+ {
+ title: 'Dismissible callouts',
+ source: [
+ {
+ type: GuideSectionTypes.TSX,
+ code: onDismissSource,
+ },
+ ],
+ text: (
+
+ To render a cross icon in the top right hand corner, pass an{' '}
+ onDismiss callback that handles conditionally
+ rendering your callout.
+
+ ),
+ snippet: onDismissSnippet,
+ demo: ,
+ },
],
};
diff --git a/src-docs/src/views/call_out/on_dismiss.tsx b/src-docs/src/views/call_out/on_dismiss.tsx
new file mode 100644
index 00000000000..3047a1f6f3a
--- /dev/null
+++ b/src-docs/src/views/call_out/on_dismiss.tsx
@@ -0,0 +1,86 @@
+import React, { useState } from 'react';
+
+import {
+ EuiFlexGroup,
+ EuiSwitch,
+ EuiSpacer,
+ EuiButtonEmpty,
+ EuiCallOut,
+} from '../../../../src';
+
+export default () => {
+ const [showCallOut, setShowCallOut] = useState(
+ !localStorage.getItem('EuiCallOutOnDismissDemo')
+ );
+ const onDismiss = () => {
+ setShowCallOut(false);
+ localStorage.setItem('EuiCallOutOnDismissDemo', 'hidden');
+ };
+ const resetDemo = () => {
+ setShowCallOut(true);
+ localStorage.setItem('EuiCallOutOnDismissDemo', '');
+ };
+
+ // UI toggles
+ const [showTitle, setShowTitle] = useState(true);
+ const [showChildren, setShowChildren] = useState(true);
+ const [smallSize, setSmallSize] = useState(false);
+
+ return (
+
+
+ setShowTitle(e.target.checked)}
+ compressed
+ />
+ setShowChildren(e.target.checked)}
+ compressed
+ />
+ setSmallSize(e.target.checked)}
+ compressed
+ />
+
+
+ {showCallOut ? (
+
+ {showChildren && (
+ <>
+
+ Here’s more some stuff users need to know. But maybe users don't
+ need to know it on every page refresh, so you could remember
+ whether or not to display this callout in local storage.
+
+ {!showTitle && (
+
+ This second paragraph is here to demonstrate that only the
+ first one needs to account for the dismiss button in width.
+
+ )}
+ >
+ )}
+
+ ) : (
+
+ The callout has been dismissed. Click to reset the demo
+
+ )}
+
+ );
+};
diff --git a/src-docs/src/views/call_out/playground.js b/src-docs/src/views/call_out/playground.js
index d388ef32bd5..19847dd96de 100644
--- a/src-docs/src/views/call_out/playground.js
+++ b/src-docs/src/views/call_out/playground.js
@@ -3,6 +3,8 @@ import { EuiCallOut, EuiText } from '../../../../src/components/';
import {
propUtilityForPlayground,
iconValidator,
+ simulateFunction,
+ dummyFunction,
} from '../../services/playground';
export default () => {
@@ -29,6 +31,8 @@ export default () => {
hidden: false,
};
+ propsToUse.onDismiss = simulateFunction(propsToUse.onDismiss);
+
return {
config: {
componentName: 'EuiCallOut',
@@ -42,6 +46,9 @@ export default () => {
named: ['EuiCallOut', 'EuiText'],
},
},
+ customProps: {
+ onDismiss: dummyFunction,
+ },
},
};
};
diff --git a/src/components/call_out/__snapshots__/call_out.test.tsx.snap b/src/components/call_out/__snapshots__/call_out.test.tsx.snap
index 0813a304ab7..da1a8e37b32 100644
--- a/src/components/call_out/__snapshots__/call_out.test.tsx.snap
+++ b/src/components/call_out/__snapshots__/call_out.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`EuiCallOut is rendered 1`] = `
data-test-subj="test subject string"
>
Content
@@ -96,7 +96,10 @@ exports[`EuiCallOut props title is rendered 1`] = `
Title
+
Content
diff --git a/src/components/call_out/call_out.styles.ts b/src/components/call_out/call_out.styles.ts
index df020758a0f..a6164f8d951 100644
--- a/src/components/call_out/call_out.styles.ts
+++ b/src/components/call_out/call_out.styles.ts
@@ -12,17 +12,44 @@ import { UseEuiTheme } from '../../services';
export const euiCallOutStyles = ({ euiTheme }: UseEuiTheme) => {
return {
- euiCallOut: css``,
+ euiCallOut: css`
+ position: relative;
+ `,
+ hasDismissButton: {
+ // Ensure that only the top-most (first-child) title or child text
+ // has a padding-right on it (to account for the dismiss button)
+ hasDimissButton: css`
+ & > :first-child:is(.euiTitle),
+ & > :first-child:is(.euiText) > :first-child {
+ ${logicalCSS('padding-right', euiTheme.size.base)}
+ }
+ `,
+ // Ensure the callout always has enough height for the button
+ s: css`
+ ${logicalCSS('min-height', euiTheme.size.xl)}
+ `,
+ m: css`
+ ${logicalCSS('min-height', euiTheme.size.xxl)}
+ `,
+ },
+ dismissButton: {
+ euiCallOut__dismissButton: css`
+ position: absolute;
+ `,
+ s: css`
+ ${logicalCSS('top', euiTheme.size.xs)}
+ ${logicalCSS('right', euiTheme.size.xs)}
+ `,
+ m: css`
+ ${logicalCSS('top', euiTheme.size.s)}
+ ${logicalCSS('right', euiTheme.size.s)}
+ `,
+ },
euiCallOut__icon: css`
position: relative;
${logicalCSS('top', '-1px')}
${logicalCSS('margin-right', euiTheme.size.s)}
`,
- euiCallOut__description: css`
- :not(:only-child) {
- ${logicalCSS('margin-top', euiTheme.size.s)}
- }
- `,
};
};
@@ -36,7 +63,6 @@ export const euiCallOutHeadingStyles = ({ euiTheme }: UseEuiTheme) => {
// In case it's nested inside EuiText
)}
`,
-
primary: css`
color: ${euiTheme.colors.primaryText};
`,
diff --git a/src/components/call_out/call_out.test.tsx b/src/components/call_out/call_out.test.tsx
index 4e05b690aed..50c992e2319 100644
--- a/src/components/call_out/call_out.test.tsx
+++ b/src/components/call_out/call_out.test.tsx
@@ -7,6 +7,7 @@
*/
import React from 'react';
+import { fireEvent } from '@testing-library/react';
import { requiredProps } from '../../test/required_props';
import { render } from '../../test/rtl';
@@ -59,5 +60,15 @@ describe('EuiCallOut', () => {
});
});
});
+
+ test('onDismiss', () => {
+ const onDismiss = jest.fn();
+ const { getByTestSubject } = render(
+ Content
+ );
+
+ fireEvent.click(getByTestSubject('euiDismissCalloutButton'));
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/src/components/call_out/call_out.tsx b/src/components/call_out/call_out.tsx
index ea25ab23e43..728848b099f 100644
--- a/src/components/call_out/call_out.tsx
+++ b/src/components/call_out/call_out.tsx
@@ -13,10 +13,13 @@ import classNames from 'classnames';
import { CommonProps } from '../common';
import { IconType, EuiIcon } from '../icon';
+import { EuiButtonIcon } from '../button';
import { EuiText } from '../text';
import { useEuiTheme } from '../../services';
import { EuiPanel } from '../panel';
+import { EuiSpacer } from '../spacer';
import { EuiTitle } from '../title';
+import { EuiI18n } from '../i18n';
import { euiCallOutStyles, euiCallOutHeadingStyles } from './call_out.styles';
@@ -35,6 +38,14 @@ export type EuiCallOutProps = CommonProps &
color?: Color;
size?: Size;
heading?: Heading;
+ /**
+ * Passing an `onDismiss` callback will render a cross in the top right hand corner
+ * of the callout.
+ *
+ * This callback fires when users click this button, which allows conditionally
+ * removing the callout or other actions.
+ */
+ onDismiss?: () => void;
};
export const EuiCallOut = forwardRef(
@@ -47,16 +58,22 @@ export const EuiCallOut = forwardRef(
children,
className,
heading = 'p',
+ onDismiss,
...rest
},
ref
) => {
const theme = useEuiTheme();
const styles = euiCallOutStyles(theme);
- const cssStyles = [styles.euiCallOut];
- const cssIconStyle = [styles.euiCallOut__icon];
- const cssDescriptionStyle = [styles.euiCallOut__description];
-
+ const cssStyles = [
+ styles.euiCallOut,
+ onDismiss && styles.hasDismissButton.hasDimissButton,
+ onDismiss && styles.hasDismissButton[size],
+ ];
+ const cssDismissButtonStyles = [
+ styles.dismissButton.euiCallOut__dismissButton,
+ styles.dismissButton[size],
+ ];
const headerStyles = euiCallOutHeadingStyles(theme);
const cssHeaderStyles = [
headerStyles.euiCallOutHeader,
@@ -71,45 +88,66 @@ export const EuiCallOut = forwardRef(
className
);
- let headerIcon;
-
- if (iconType) {
- headerIcon = (
-
- );
- }
-
- let optionalChildren;
- if (children) {
- optionalChildren = (
-
- {children}
-
- );
- }
-
- let header;
- if (title) {
- const H: Heading = heading;
- header = (
-
-
- {headerIcon}
- {title}
-
-
+ const dismissButton = onDismiss && (
+
+ {(dismissAriaLabel: string) => (
+
+ )}
+
+ );
+
+ const headerIcon = iconType && (
+
+ );
+ const H: Heading = heading;
+ const header = title && (
+
+
+ {headerIcon}
+ {title}
+
+
+ );
+
+ const optionalChildren = children && (
+
+ {children}
+
+ );
+
+ // Note: the DOM position of the dismiss button matters to screen reader users.
+ // We generally want them to have some context of _what_ they're dismissing,
+ // instead of navigating to the dismiss button first before the callout content
+ const calloutContent =
+ header && optionalChildren ? (
+ <>
+ {header}
+ {dismissButton}
+
+ {optionalChildren}
+ >
+ ) : (
+ <>
+ {header || optionalChildren}
+ {dismissButton}
+ >
);
- }
return (
(
grow={false}
{...rest}
>
- {header}
-
- {optionalChildren}
+ {calloutContent}
);
}
diff --git a/src/components/form/__snapshots__/form.test.tsx.snap b/src/components/form/__snapshots__/form.test.tsx.snap
index f95020e3136..f0300518fb9 100644
--- a/src/components/form/__snapshots__/form.test.tsx.snap
+++ b/src/components/form/__snapshots__/form.test.tsx.snap
@@ -55,7 +55,10 @@ exports[`EuiForm renders with error callout when isInvalid is "true" and has mul
Please address the highlighted errors.
+