Repository: elastic/eui (TypeScript)

Issue:
[EuiCallOut] Add optional close button
**Is your feature request related to a problem? Please describe.**
In practice, callouts can be dismissed or closed. The current docs demonstrate the use of a 'Dismiss' button below the copy and, in Kibana, we see the use of a close button in the upper-right corner (see below). 

![CleanShot 2023-08-03 at 07 10 41@2x](https://github.com/elastic/eui/assets/446285/a1bfb66b-8fa0-49eb-8f43-5dd19461acbd)

**Describe the solution you'd like**
It is possible for implementors to attain this layout/functionality, but it is not provided out of the box. A recent question was raised with the EUI team on how to do this, and the team felt it was worthwhile to 'pave the cowpath' and add an optional close button as opposed to leaving it up to a documented pattern.

**Describe alternatives you've considered**
Leaving it up to a documented pattern.

**Additional context**
With a fresh deployment (or by clearing localStorage) you can see the above example in Discover, as an example.

```[tasklist]
### Tasks
- [ ] https://github.com/elastic/eui/pull/7156
```



Discussion: @ryankeairns, my thoughts below:

**Dismissible prop**
We could add a `dismissible: bool` prop, which adds the `Dismiss` button as well as a close icon (top right).

<img width="1391" alt="Screenshot 2023-08-14 at 10 40 13" src="https://github.com/elastic/eui/assets/92029479/3e3fa0e9-7a8a-4f4b-aac7-dd38460768ea">

**:: CTA**

When `Dismiss` is the only option, should the button be a `primary`, `secondary` or `tertiary` action?
<img width="1190" alt="Screenshot 2023-08-14 at 16 26 02" src="https://github.com/elastic/eui/assets/92029479/45a96b8f-8c39-4083-8a4c-f1441d385599">


**:: Text**
Should we fix the text to `Dismiss`? 

**:: Other, similar requests**
Another request that has come in other areas of product (namely Userconsole) is a `Don't show again` action. 
Thanks for noodling on this @kyrspl 

In a constructive way, this walks the line of being either just-enough or too opinionated. Conservatively, I start with the latter, but I can't think of a situation where you wouldn't want to have both. Going the route of showing both, I am leaning toward the last option (the default button style) for the Dismiss button when it is the only CTA; else use the empty style.



Fix (patch):
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 = [
+  `<EuiCallOut onDismiss={onDismiss}>
+  <p><!-- Content --></p>
+</EuiCallOut>
+`,
+];
+
 export const CallOutExample = {
   title: 'Callout',
   intro: (
@@ -164,5 +173,23 @@ export const CallOutExample = {
       snippet: dangerSnippet,
       demo: <Danger />,
     },
+    {
+      title: 'Dismissible callouts',
+      source: [
+        {
+          type: GuideSectionTypes.TSX,
+          code: onDismissSource,
+        },
+      ],
+      text: (
+        <p>
+          To render a cross icon in the top right hand corner, pass an{' '}
+          <EuiCode>onDismiss</EuiCode> callback that handles conditionally
+          rendering your callout.
+        </p>
+      ),
+      snippet: onDismissSnippet,
+      demo: <OnDismiss />,
+    },
   ],
 };
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 (
+    <div css={{ maxInlineSize: 500 }}>
+      <EuiFlexGroup>
+        <EuiSwitch
+          label="Show title"
+          checked={showTitle}
+          onChange={(e) => setShowTitle(e.target.checked)}
+          compressed
+        />
+        <EuiSwitch
+          label="Show children"
+          checked={showChildren}
+          onChange={(e) => setShowChildren(e.target.checked)}
+          compressed
+        />
+        <EuiSwitch
+          label="Small size"
+          checked={smallSize}
+          onChange={(e) => setSmallSize(e.target.checked)}
+          compressed
+        />
+      </EuiFlexGroup>
+      <EuiSpacer />
+      {showCallOut ? (
+        <EuiCallOut
+          onDismiss={onDismiss}
+          iconType="iInCircle"
+          title={
+            showTitle
+              ? 'You can dismiss this callout by clicking the Close button in the top right corner'
+              : ''
+          }
+          size={smallSize ? 's' : 'm'}
+        >
+          {showChildren && (
+            <>
+              <p>
+                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.
+              </p>
+              {!showTitle && (
+                <p>
+                  This second paragraph is here to demonstrate that only the
+                  first one needs to account for the dismiss button in width.
+                </p>
+              )}
+            </>
+          )}
+        </EuiCallOut>
+      ) : (
+        <EuiButtonEmpty onClick={resetDemo}>
+          The callout has been dismissed. Click to reset the demo
+        </EuiButtonEmpty>
+      )}
+    </div>
+  );
+};
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/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.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<HTMLDivElement, EuiCallOutProps>(
@@ -47,16 +58,22 @@ export const EuiCallOut = forwardRef<HTMLDivElement, EuiCallOutProps>(
       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<HTMLDivElement, EuiCallOutProps>(
       className
     );
 
-    let headerIcon;
-
-    if (iconType) {
-      headerIcon = (
-        <EuiIcon
-          css={cssIconStyle}
-          type={iconType}
-          size="m"
-          aria-hidden="true"
-          color="inherit" // forces the icon to inherit its parent color
-        />
-      );
-    }
-
-    let optionalChildren;
-    if (children) {
-      optionalChildren = (
-        <EuiText
-          css={cssDescriptionStyle}
-          size={size === 's' ? 'xs' : 's'}
-          color="default"
-        >
-          {children}
-        </EuiText>
-      );
-    }
-
-    let header;
-    if (title) {
-      const H: Heading = heading;
-      header = (
-        <EuiTitle size={size === 's' ? 'xxs' : 'xs'} css={cssHeaderStyles}>
-          <H className="euiCallOutHeader__title">
-            {headerIcon}
-            {title}
-          </H>
-        </EuiTitle>
+    const dismissButton = onDismiss && (
+      <EuiI18n
+        token="euiCallOut.dismissAriaLabel"
+        default="Dismiss this callout"
+      >
+        {(dismissAriaLabel: string) => (
+          <EuiButtonIcon
+            iconType="cross"
+            onClick={onDismiss}
+            aria-label={dismissAriaLabel}
+            css={cssDismissButtonStyles}
+            color={color}
+            data-test-subj="euiDismissCalloutButton"
+          />
+        )}
+      </EuiI18n>
+    );
+
+    const headerIcon = iconType && (
+      <EuiIcon
+        css={styles.euiCallOut__icon}
+        type={iconType}
+        size="m"
+        aria-hidden="true"
+        color="inherit"
+      />
+    );
+    const H: Heading = heading;
+    const header = title && (
+      <EuiTitle size={size === 's' ? 'xxs' : 'xs'} css={cssHeaderStyles}>
+        <H className="euiCallOutHeader__title">
+          {headerIcon}
+          {title}
+        </H>
+      </EuiTitle>
+    );
+
+    const optionalChildren = children && (
+      <EuiText size={size === 's' ? 'xs' : 's'} color="default">
+        {children}
+      </EuiText>
+    );
+
+    // 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}
+          <EuiSpacer size="s" />
+          {optionalChildren}
+        </>
+      ) : (
+        <>
+          {header || optionalChildren}
+          {dismissButton}
+        </>
       );
-    }
 
     return (
       <EuiPanel
@@ -122,9 +160,7 @@ export const EuiCallOut = forwardRef<HTMLDivElement, EuiCallOutProps>(
         grow={false}
         {...rest}
       >
-        {header}
-
-        {optionalChildren}
+        {calloutContent}
       </EuiPanel>
     );
   }
diff --git a/upcoming_changelogs/7156.md b/upcoming_changelogs/7156.md
new file mode 100644
index 00000000000..8b2bda4ac17
--- /dev/null
+++ b/upcoming_changelogs/7156.md
@@ -0,0 +1,1 @@
+- Updated `EuiCallOut` with a new `onDismiss` prop