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 = ( -