diff --git a/packages/eui/changelogs/upcoming/9264.md b/packages/eui/changelogs/upcoming/9264.md new file mode 100644 index 00000000000..ab4ef7db260 --- /dev/null +++ b/packages/eui/changelogs/upcoming/9264.md @@ -0,0 +1 @@ +- Added beta `euiContainer()`, `euiContainerCSS()`, and `euiContainerQuery()` Emotion utilities to help work with CSS Container Queries diff --git a/packages/eui/src/components/flyout/flyout.styles.ts b/packages/eui/src/components/flyout/flyout.styles.ts index b9aa7549247..00221d91587 100644 --- a/packages/eui/src/components/flyout/flyout.styles.ts +++ b/packages/eui/src/components/flyout/flyout.styles.ts @@ -25,6 +25,8 @@ import { import { UseEuiTheme } from '../../services'; import { euiFormMaxWidth } from '../form/form.styles'; +export const EUI_FLYOUT_CONTAINER_NAME = 'euiFlyout' as const; + export const FLYOUT_BREAKPOINT = 'm' as const; export const euiFlyoutSlideInRight = keyframes` diff --git a/packages/eui/src/global_styling/mixins/_container_query.test.tsx b/packages/eui/src/global_styling/mixins/_container_query.test.tsx new file mode 100644 index 00000000000..c36334a07b9 --- /dev/null +++ b/packages/eui/src/global_styling/mixins/_container_query.test.tsx @@ -0,0 +1,153 @@ +/* + * 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 from 'react'; +import { render } from '../../test/rtl'; +import { + euiContainer, + euiContainerCSS, + euiContainerQuery, +} from './_container_query'; + +describe('euiContainer', () => { + it('supports naming containers', () => { + expect(euiContainer('normal', 'my-unique-name')).toEqual( + 'container-name: my-unique-name' + ); + }); + + it('supports non-scroll state container types', () => { + expect(euiContainer('size', 'my-unique-name')).toEqual( + 'container-name: my-unique-name;container-type: size' + ); + expect(euiContainer('inline-size', 'my-unique-name')).toEqual( + 'container-name: my-unique-name;container-type: inline-size' + ); + }); + + it('supports scroll states via the scrollState argument', () => { + expect(euiContainer('size', 'my-unique-name', true)).toEqual( + 'container-name: my-unique-name;container-type: size scroll-state' + ); + + expect(euiContainer('size', 'my-unique-name', false)).toEqual( + 'container-name: my-unique-name;container-type: size' + ); + }); + + it('ignores the default "normal" container type', () => { + expect(euiContainer('normal', 'my-unique-name')).toEqual( + 'container-name: my-unique-name' + ); + + expect(euiContainer('normal', 'my-unique-name', true)).toEqual( + 'container-name: my-unique-name;container-type: scroll-state' + ); + }); +}); + +describe('euiContainerCSS', () => { + it('supports naming containers', () => { + const { container } = render( +
+ ); + + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + }); + + it('supports non-scroll state container types', () => { + const { container, rerender } = render( + + ); + + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).toHaveStyleRule('container-type', 'size'); + + rerender(); + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).toHaveStyleRule( + 'container-type', + 'inline-size' + ); + }); + + it('supports scroll states via the scrollState argument', () => { + const { container, rerender } = render( + + ); + + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).toHaveStyleRule( + 'container-type', + 'size scroll-state' + ); + + rerender(); + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).toHaveStyleRule('container-type', 'size'); + }); + + it('ignores the default "normal" container type', () => { + const { container, rerender } = render( + + ); + + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).not.toHaveStyleRule('container-type'); + + rerender(); + expect(container.firstChild).toHaveStyleRule( + 'container-name', + 'my-unique-name' + ); + expect(container.firstChild).toHaveStyleRule( + 'container-type', + 'scroll-state' + ); + }); +}); + +describe('euiContainerQuery', () => { + it('supports any container conditions', () => { + expect(euiContainerQuery('(width > 150px)')).toEqual( + `@container (width > 150px)` + ); + + expect(euiContainerQuery('(width > 150px) and (width < 300px)')).toEqual( + `@container (width > 150px) and (width < 300px)` + ); + }); + + it('supports container names', () => { + expect(euiContainerQuery('(width > 150px)', 'my-container')).toEqual( + `@container my-container (width > 150px)` + ); + + expect( + euiContainerQuery('(width > 150px) and (width < 300px)', 'my-container') + ).toEqual(`@container my-container (width > 150px) and (width < 300px)`); + }); +}); diff --git a/packages/eui/src/global_styling/mixins/_container_query.ts b/packages/eui/src/global_styling/mixins/_container_query.ts new file mode 100644 index 00000000000..0d888890548 --- /dev/null +++ b/packages/eui/src/global_styling/mixins/_container_query.ts @@ -0,0 +1,128 @@ +/* + * 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 { css } from '@emotion/react'; + +const CONTAINER_TYPES = ['normal', 'size', 'inline-size'] as const; + +/** + * Type of container context used in container queries. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/container-type} + */ +export type EuiContainerType = (typeof CONTAINER_TYPES)[number]; + +/** + * Establish element as a query container. + * The scroll state is applied through the `scrollState` argument + * and not the `type` argument. + * + * @example + * // Export container name to use across the application + * export const PAGE_CONTENT_CONTAINER_NAME = 'my-app-page-content'; + * const pageContentStyles = css` + * ${euiContainer('inline-size', PAGE_CONTENT_CONTAINER_NAME)} + * margin: 0 auto; + * `; + * + * @returns A style string to be used inside Emotion's `css` template literal + * @beta + */ +export const euiContainer = ( + type: EuiContainerType, + name?: string, + scrollState?: boolean +): string => { + let finalType = ''; + if (type !== 'normal') { + finalType += type; + } + if (scrollState) { + if (finalType.length) { + finalType += ' '; + } + + finalType += 'scroll-state'; + } + + return [ + !!name && `container-name: ${name}`, + !!finalType && `container-type: ${finalType}`, + ] + .filter(Boolean) + .join(';'); +}; + +/** + * Establish element as a query container. + * The scroll state is applied through the `scrollState` argument + * and not the `type` argument. + * + * @example + * // Export container name to use across the application + * export const PAGE_CONTENT_CONTAINER_NAME = 'my-app-page-content'; + * const PageContent = ({ children }: PropsWithChildren) => ( + *