diff --git a/packages/eui/src/components/call_out/__snapshots__/call_out.test.tsx.snap b/packages/eui/src/components/call_out/__snapshots__/call_out.test.tsx.snap index 1c2bda5cd1c0..590eaf6d9404 100644 --- a/packages/eui/src/components/call_out/__snapshots__/call_out.test.tsx.snap +++ b/packages/eui/src/components/call_out/__snapshots__/call_out.test.tsx.snap @@ -9,7 +9,7 @@ exports[`EuiCallOut is rendered 1`] = ` style="--euiCallOutTypeColor: #0B64DD;" >
= { title: 'Display/EuiCallOut', @@ -88,16 +101,7 @@ export const WithActions: Story = { args: { title, text, - actionProps: { - primary: { - children: 'Primary action', - onClick: action('primary onClick'), - }, - secondary: { - children: 'Secondary action', - onClick: action('secondary onClick'), - }, - }, + actionProps: defaultActionProps, }, }; @@ -365,3 +369,103 @@ export const KitchenSinkCustomChildren: Story = { ); }, }; + +export const WrapperKitchenSink: Story = { + tags: ['vrt-only'], + render: function Render() { + return ( +
+ +

within EuiPanel

+
+ + + + + + + + +

within EuiPopover

+
+ + Show popover} + isOpen + anchorPosition="downCenter" + closePopover={() => {}} + aria-label="Popover containing a CallOut" + > + +

{textLong}

+
+ +
+ + Show popover} + isOpen + anchorPosition="downCenter" + closePopover={() => {}} + aria-label="Popover containing a CallOut" + > + + + +
+ + +

within EuiFlexGroup

+
+ + + + + + + + + + + + + + + + + + + + +
+ ); + }, +}; diff --git a/packages/eui/src/components/call_out/call_out.styles.ts b/packages/eui/src/components/call_out/call_out.styles.ts index 85847bd6a5db..338cc586ede0 100644 --- a/packages/eui/src/components/call_out/call_out.styles.ts +++ b/packages/eui/src/components/call_out/call_out.styles.ts @@ -14,46 +14,9 @@ import { preventForcedColors, } from '../../global_styling'; import { UseEuiTheme } from '../../services'; -import { EuiCallOutSize } from './types'; /** Maximum reading width for `text` and `children` slots. */ const TEXT_MAX_WIDTH = 1200; -const CONTAINER_NAME = 'euiCallOut'; -const CQC_LAYOUTS = ['superNarrow', 'wide'] as const; -type EuiCallOutLayouts = (typeof CQC_LAYOUTS)[number]; -const CQC_BREAKPOINTS: Record< - EuiCallOutSize, - Record -> = { - s: { - superNarrow: '(max-width: 400px)', - wide: '(min-width: 800px)', - }, - m: { - superNarrow: '(max-width: 600px)', - wide: '(min-width: 1000px)', - }, -}; - -const withContainerQuery = ({ - layout, - styles, -}: { - layout: EuiCallOutLayouts; - styles: string; -}) => { - return Object.keys(CQC_BREAKPOINTS) - .map( - (sizeKey) => ` - @container ${CONTAINER_NAME}--${sizeKey} ${ - CQC_BREAKPOINTS[sizeKey as EuiCallOutSize][layout] - } { - ${styles} - } - ` - ) - .join('\n'); -}; export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; @@ -76,11 +39,9 @@ export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { return { euiCallOut: css` - container-type: inline-size; - container-name: ${CONTAINER_NAME}; position: relative; display: flex; - inline-size: 100%; + max-inline-size: 100%; align-items: center; border: ${euiTheme.border.width.thin} solid; border-radius: ${borderRadius}; @@ -104,14 +65,10 @@ export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { } &:where([data-size='s']) { - container-name: ${CONTAINER_NAME} ${CONTAINER_NAME}--s; - ${logicalShorthandCSS('padding', `${paddingSizes.s} ${paddingSizes.m}`)} } &:where([data-size='m']) { - container-name: ${CONTAINER_NAME} ${CONTAINER_NAME}--m; - padding: ${paddingSizes.m}; } `, @@ -129,13 +86,10 @@ export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { gap: ${euiTheme.size.m}; } - ${withContainerQuery({ - layout: 'wide', - styles: ` - flex-direction: row; - gap: ${euiTheme.size.xxl}; - `, - })} + &:where([data-layout='wide'] &) { + flex-direction: row; + gap: ${euiTheme.size.xxl}; + } `, // handles icon + text layout body: css` @@ -232,8 +186,7 @@ export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { ${logicalCSS('margin-left', euiTheme.size.xl)} } - /* uses container query directly as it should apply generically independent of size */ - @container ${CONTAINER_NAME} ${CQC_BREAKPOINTS.s.superNarrow} { + &:where([data-layout='superNarrow'] &) { flex-wrap: wrap; /* use full width actions */ @@ -243,15 +196,12 @@ export const euiCallOutStyles = (euiThemeContext: UseEuiTheme) => { } } - ${withContainerQuery({ - layout: 'wide', - styles: ` - ${logicalCSS('margin-left', '0')} - align-self: center; - flex-shrink: 0; - flex-direction: row-reverse; - `, - })} + &:where([data-layout='wide'] &) { + ${logicalCSS('margin-left', '0')} + align-self: center; + flex-shrink: 0; + flex-direction: row-reverse; + } `, }; }; diff --git a/packages/eui/src/components/call_out/call_out.tsx b/packages/eui/src/components/call_out/call_out.tsx index ee267d3d4d97..21f063bed1bb 100644 --- a/packages/eui/src/components/call_out/call_out.tsx +++ b/packages/eui/src/components/call_out/call_out.tsx @@ -26,6 +26,7 @@ import { type EuiNotificationIconType, } from '../notification_icon/notification_icon'; +import { useLayoutObserver } from './use_layout_observer'; import { EuiCallOutAction, EuiCallOutActionPrimaryProps, @@ -133,6 +134,11 @@ export const EuiCallOut = forwardRef( const { euiTheme } = useEuiTheme(); const color = getCallOutColor(_color); + /* Uses resize observer to determine the container width/layout instead of native container queries, + because callouts can be placed in containers without defined size (absolute positioned, no-grow flex layout etc.) + where container queries would collapse by design instead of adjusting to the content dimensions. */ + const panelRef = useLayoutObserver(size, ref); + const borderColors = useEuiBorderColorCSS(); const styles = useEuiMemoizedStyles(euiCallOutStyles); const cssStyles = [ @@ -307,7 +313,7 @@ export const EuiCallOut = forwardRef( // uses custom padding paddingSize="none" className={classes} - panelRef={ref} + panelRef={panelRef} grow={false} style={{ ...cssVariables, ...style }} data-size={size} diff --git a/packages/eui/src/components/call_out/use_layout_observer.ts b/packages/eui/src/components/call_out/use_layout_observer.ts new file mode 100644 index 000000000000..df2da8e32884 --- /dev/null +++ b/packages/eui/src/components/call_out/use_layout_observer.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useRef } from 'react'; + +import { hasResizeObserver } from '../observer/resize_observer/resize_observer'; +import { EuiCallOutSize } from './types'; + +const SUPER_NARROW_WIDTH = 400; +const WIDE_BREAKPOINTS: Record = { + s: 800, + m: 1000, +}; + +/** + * Observes the rendered width and sets `data-layout` on the root + * element so that CSS can respond to size changes. + * + * This is an alternative to native CSS container queries. Its purpose is to handle cases where + * container queries would collapse if the element is placed inside a container without a defined size. + */ +export const useLayoutObserver = ( + size: EuiCallOutSize, + ref: React.ForwardedRef +): ((node: HTMLDivElement | null) => void) => { + const elementRef = useRef(null); + const forwardedRefRef = useRef(ref); + forwardedRefRef.current = ref; + + const panelRef = useCallback((node: HTMLDivElement | null) => { + elementRef.current = node; + const fRef = forwardedRefRef.current; + if (typeof fRef === 'function') { + fRef(node); + } else if (fRef) { + (fRef as React.MutableRefObject).current = node; + } + }, []); + + useEffect(() => { + const element = elementRef.current; + if (!element || !hasResizeObserver) return; + + const wide = WIDE_BREAKPOINTS[size]; + + const observer = new ResizeObserver(([entry]) => { + const width = entry.borderBoxSize[0].inlineSize; + + if (width <= SUPER_NARROW_WIDTH) { + element.setAttribute('data-layout', 'superNarrow'); + } else if (width >= wide) { + element.setAttribute('data-layout', 'wide'); + } else { + element.removeAttribute('data-layout'); + } + }); + + observer.observe(element); + return () => observer.disconnect(); + }, [size]); + + return panelRef; +}; diff --git a/packages/eui/src/components/form/__snapshots__/form.test.tsx.snap b/packages/eui/src/components/form/__snapshots__/form.test.tsx.snap index 4339ad571243..8122c87a108b 100644 --- a/packages/eui/src/components/form/__snapshots__/form.test.tsx.snap +++ b/packages/eui/src/components/form/__snapshots__/form.test.tsx.snap @@ -31,7 +31,7 @@ exports[`EuiForm renders with error callout when isInvalid is "true" 1`] = ` tabindex="-1" >