diff --git a/src-docs/src/views/theme/breakpoints/_breakpoints_js.tsx b/src-docs/src/views/theme/breakpoints/_breakpoints_js.tsx index 0cbede119e7..870676528a9 100644 --- a/src-docs/src/views/theme/breakpoints/_breakpoints_js.tsx +++ b/src-docs/src/views/theme/breakpoints/_breakpoints_js.tsx @@ -5,6 +5,8 @@ import { EuiCode, EuiText, useEuiBreakpoint, + useEuiMaxBreakpoint, + useEuiMinBreakpoint, useCurrentEuiBreakpoint, useIsWithinBreakpoints, useIsWithinMaxBreakpoint, @@ -110,19 +112,10 @@ useIsWithinMinBreakpoint('s')`} title={useEuiBreakpoint(sizes[])} type="hook" description={ - <> -

- Given an array of breakpoint keys, this hook generates a CSS media - query string based on the minimum width and maximum width - provided. -

-

- You can also create media queries with a{' '} - (max-width) only or{' '} - (min-width) only by utilizing the{' '} - xs and xl arguments. -

- +

+ Given an array of screen sizes, this hook generates a CSS media + query string based on the minimum and maximum screen sizes provided. +

} example={

+ + + useEuiMaxBreakpoint(size) +
+ useEuiMinBreakpoint(size) + + } + type="hook" + description={ +

+ Given a single breakpoint key, these hooks generate a min or max CSS + media query string based on the single breakpoint dimension + returned. +

+ } + example={ +

+ This text is red on screens below the medium breakpoint, and green + on screens above. +

+ } + snippet={`\${useEuiMaxBreakpoint('m')} { + color: red; + } + \${useEuiMinBreakpoint('m')} { + color: green; }`} snippetLanguage="emotion" /> diff --git a/src/components/description_list/description_list.styles.ts b/src/components/description_list/description_list.styles.ts index d8c99a79efe..4e93841489d 100644 --- a/src/components/description_list/description_list.styles.ts +++ b/src/components/description_list/description_list.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { logicalTextAlignCSS, euiBreakpoint } from '../../global_styling'; +import { logicalTextAlignCSS, euiMinBreakpoint } from '../../global_styling'; import { UseEuiTheme } from '../../services'; export const euiDescriptionListStyles = (euiThemeContext: UseEuiTheme) => { @@ -29,7 +29,7 @@ export const euiDescriptionListStyles = (euiThemeContext: UseEuiTheme) => { `, // Responsive columns behave as a row on breakpoints xs-s responsiveColumn: css` - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { ${columnDisplay} } `, diff --git a/src/components/description_list/description_list_description.styles.ts b/src/components/description_list/description_list_description.styles.ts index c3c081557ca..2d4fd920a5f 100644 --- a/src/components/description_list/description_list_description.styles.ts +++ b/src/components/description_list/description_list_description.styles.ts @@ -9,7 +9,8 @@ import { css } from '@emotion/react'; import { euiFontSize, - euiBreakpoint, + euiMaxBreakpoint, + euiMinBreakpoint, logicalTextAlignCSS, logicalCSS, } from '../../global_styling'; @@ -35,11 +36,11 @@ export const euiDescriptionListDescriptionStyles = ( ${columnDisplay} `, responsiveColumn: css` - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { ${logicalCSS('width', '100%')} padding: 0; } - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { ${columnDisplay} } `, diff --git a/src/components/description_list/description_list_description.tsx b/src/components/description_list/description_list_description.tsx index 515d2fcef1e..2d34d394a8b 100644 --- a/src/components/description_list/description_list_description.tsx +++ b/src/components/description_list/description_list_description.tsx @@ -9,7 +9,7 @@ import React, { HTMLAttributes, FunctionComponent, useContext } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../common'; -import { useEuiTheme } from '../../services'; +import { useEuiTheme, useIsWithinMinBreakpoint } from '../../services'; import { euiDescriptionListDescriptionStyles } from './description_list_description.styles'; import { EuiDescriptionListContext } from './description_list_context'; @@ -26,6 +26,7 @@ export const EuiDescriptionListDescription: FunctionComponent { ${columnDisplay} `, responsiveColumn: css` - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { ${logicalCSS('width', '100%')} padding: 0; } - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { ${columnDisplay} } `, diff --git a/src/components/flex/flex_grid.styles.ts b/src/components/flex/flex_grid.styles.ts index 9465a743bfc..329921b8902 100644 --- a/src/components/flex/flex_grid.styles.ts +++ b/src/components/flex/flex_grid.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { euiBreakpoint } from '../../global_styling'; +import { euiMaxBreakpoint } from '../../global_styling'; import { UseEuiTheme } from '../../services'; // Note: the only way to get column direction working with `display: grid` @@ -23,7 +23,7 @@ export const euiFlexGridStyles = ( display: grid; `, responsive: css` - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { grid-template-columns: repeat(1, 1fr); grid-auto-flow: row; } diff --git a/src/components/flex/flex_group.styles.ts b/src/components/flex/flex_group.styles.ts index 419c8670c17..84640987330 100644 --- a/src/components/flex/flex_group.styles.ts +++ b/src/components/flex/flex_group.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { euiBreakpoint, logicalCSS } from '../../global_styling'; +import { euiMaxBreakpoint, logicalCSS } from '../../global_styling'; import { UseEuiTheme } from '../../services'; export const euiFlexGroupStyles = (euiThemeContext: UseEuiTheme) => { @@ -19,7 +19,7 @@ export const euiFlexGroupStyles = (euiThemeContext: UseEuiTheme) => { flex-grow: 1; // Grow nested flex-groups by default `, responsive: css` - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { flex-wrap: wrap; & > .euiFlexItem { diff --git a/src/components/flyout/flyout.styles.ts b/src/components/flyout/flyout.styles.ts index 6f002161f64..8b323c030e9 100644 --- a/src/components/flyout/flyout.styles.ts +++ b/src/components/flyout/flyout.styles.ts @@ -10,7 +10,8 @@ import { css, keyframes } from '@emotion/react'; import { _EuiFlyoutPaddingSize, EuiFlyoutSize } from './flyout'; import { euiCanAnimate, - euiBreakpoint, + euiMaxBreakpoint, + euiMinBreakpoint, logicalCSS, mathWithUnits, } from '../../global_styling'; @@ -66,22 +67,22 @@ export const euiFlyoutCloseButtonStyles = (euiThemeContext: UseEuiTheme) => { right: css` ${logicalCSS('left', 0)} - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { - transform: translateX(calc(-100% - ${euiTheme.size.l})) !important; - } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { transform: translateX(calc(-100% - ${euiTheme.size.xs})) !important; } + ${euiMinBreakpoint(euiThemeContext, 'm')} { + transform: translateX(calc(-100% - ${euiTheme.size.l})) !important; + } `, left: css` ${logicalCSS('right', 0)} - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { - transform: translateX(calc(100% + ${euiTheme.size.l})) !important; - } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { transform: translateX(calc(100% + ${euiTheme.size.xs})) !important; } + ${euiMinBreakpoint(euiThemeContext, 'm')} { + transform: translateX(calc(100% + ${euiTheme.size.l})) !important; + } `, }, }; @@ -107,7 +108,7 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => { outline: none; } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { // 1. Leave only a small sliver exposed on small screens so users understand that this is not a new page // 2. If a custom maxWidth is set, we need to override it. ${logicalCSS('max-width', '90vw !important')} @@ -219,14 +220,14 @@ const composeFlyoutSizing = ( return ` ${logicalCSS('max-width', flyoutSizes[size].max)} - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { - ${logicalCSS('min-width', flyoutSizes[size].min)} - ${logicalCSS('width', flyoutSizes[size].width)} - } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { ${logicalCSS('min-width', 0)} ${logicalCSS('width', flyoutSizes[size].min)} } + ${euiMinBreakpoint(euiThemeContext, 'm')} { + ${logicalCSS('min-width', flyoutSizes[size].min)} + ${logicalCSS('width', flyoutSizes[size].width)} + } `; }; diff --git a/src/components/image/image_wrapper.styles.ts b/src/components/image/image_wrapper.styles.ts index 9adfd83e864..a2c0bbb584b 100644 --- a/src/components/image/image_wrapper.styles.ts +++ b/src/components/image/image_wrapper.styles.ts @@ -8,7 +8,7 @@ import { css } from '@emotion/react'; import { - euiBreakpoint, + euiMinBreakpoint, logicalCSS, logicalTextAlignCSS, logicalSide, @@ -49,7 +49,7 @@ export const euiImageWrapperStyles = (euiThemeContext: UseEuiTheme) => { // 1: Logical properties/values in `float` is currently not yet supported by all browsers w/o flags // @see https://caniuse.com/mdn-css_properties_float_flow_relative_values for when we can remove left/right fallbacks left: css` - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { float: left; /* 1 */ float: ${logicalSide.left}; ${logicalCSS('margin-left', '0')}; @@ -57,7 +57,7 @@ export const euiImageWrapperStyles = (euiThemeContext: UseEuiTheme) => { } `, right: css` - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { float: right; /* 1 */ float: ${logicalSide.right}; ${logicalCSS('margin-right', '0')}; diff --git a/src/components/modal/modal.styles.ts b/src/components/modal/modal.styles.ts index d7ac17fda5c..a6fcda8195c 100644 --- a/src/components/modal/modal.styles.ts +++ b/src/components/modal/modal.styles.ts @@ -10,7 +10,7 @@ import { css } from '@emotion/react'; import { euiShadowXLarge } from '../../themes/amsterdam/global_styling/mixins'; import { euiCanAnimate, - euiBreakpoint, + euiMaxBreakpoint, euiAnimSlideInUp, } from '../../global_styling'; import { UseEuiTheme } from '../../services'; @@ -39,7 +39,7 @@ export const euiModalStyles = (euiThemeContext: UseEuiTheme) => { ${euiTheme.animation.slow} ${euiTheme.animation.bounce}; } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { position: fixed; inset: 0; border-radius: 0; @@ -66,7 +66,7 @@ export const euiModalStyles = (euiThemeContext: UseEuiTheme) => { confirmation: css` min-inline-size: ${euiFormVariables(euiThemeContext).maxWidth}; - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { ${euiShadowXLarge(euiThemeContext, { reverse: true })} inset-block-start: auto; } diff --git a/src/components/modal/modal_body.styles.ts b/src/components/modal/modal_body.styles.ts index 1330ca9a5c5..6889a5c9fac 100644 --- a/src/components/modal/modal_body.styles.ts +++ b/src/components/modal/modal_body.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { euiYScrollWithShadows, euiBreakpoint } from '../../global_styling'; +import { euiYScrollWithShadows, euiMaxBreakpoint } from '../../global_styling'; import { UseEuiTheme } from '../../services'; export const euiModalBodyStyles = (euiThemeContext: UseEuiTheme) => { @@ -31,7 +31,7 @@ export const euiModalBodyStyles = (euiThemeContext: UseEuiTheme) => { padding-inline: ${euiTheme.size.l}; padding-block: ${euiTheme.size.s}; - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { padding-block-end: ${euiTheme.size.l}; } `, diff --git a/src/components/modal/modal_footer.styles.ts b/src/components/modal/modal_footer.styles.ts index a0523bd5d7b..9031bd9b1ec 100644 --- a/src/components/modal/modal_footer.styles.ts +++ b/src/components/modal/modal_footer.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { euiBreakpoint } from '../../global_styling'; +import { euiMaxBreakpoint } from '../../global_styling'; import { UseEuiTheme } from '../../services'; export const euiModalFooterStyles = (euiThemeContext: UseEuiTheme) => { @@ -23,7 +23,7 @@ export const euiModalFooterStyles = (euiThemeContext: UseEuiTheme) => { flex-shrink: 0; // ensure the height of the footer is based off its contents and doesn't squish gap: ${euiTheme.size.base}; - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { background: ${euiTheme.colors.lightestShade}; padding-block: ${euiTheme.size.m}; padding-inline: ${euiTheme.size.l}; diff --git a/src/components/page/page.styles.ts b/src/components/page/page.styles.ts index 86e2a789d52..a0eb30e29d8 100644 --- a/src/components/page/page.styles.ts +++ b/src/components/page/page.styles.ts @@ -7,7 +7,7 @@ */ import { css } from '@emotion/react'; -import { euiBreakpoint, logicalCSS } from '../../global_styling'; +import { euiMinBreakpoint, logicalCSS } from '../../global_styling'; import { UseEuiTheme } from '../../services'; export const euiPageStyles = (euiThemeContext: UseEuiTheme) => { @@ -36,7 +36,7 @@ export const euiPageStyles = (euiThemeContext: UseEuiTheme) => { row: css` flex-direction: column; - ${euiBreakpoint(euiThemeContext, ['m', 'xl'])} { + ${euiMinBreakpoint(euiThemeContext, 'm')} { flex-direction: row; } `, diff --git a/src/components/toast/global_toast_list.styles.ts b/src/components/toast/global_toast_list.styles.ts index 8429a1d2173..ebcfdcec5ea 100644 --- a/src/components/toast/global_toast_list.styles.ts +++ b/src/components/toast/global_toast_list.styles.ts @@ -8,7 +8,8 @@ import { css, keyframes } from '@emotion/react'; import { - euiBreakpoint, + euiMaxBreakpoint, + euiMinBreakpoint, euiScrollBarStyles, logicalCSS, logicalCSSWithFallback, @@ -18,7 +19,7 @@ import { UseEuiTheme } from '../../services'; export const euiGlobalToastListStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; - const euiToastWidth = euiTheme.base * 20; + const euiToastWidth = euiTheme.base * 25; return { /** * 1. Allow list to expand as items are added, but cap it at the screen height. @@ -33,7 +34,7 @@ export const euiGlobalToastListStyles = (euiThemeContext: UseEuiTheme) => { position: fixed; z-index: ${euiTheme.levels.toast}; ${logicalCSS('bottom', 0)}; - ${logicalCSS('width', `${euiToastWidth + euiTheme.base * 5}px`)}; /* 2 */ + ${logicalCSS('width', `${euiToastWidth}px`)}; /* 2 */ ${logicalCSS('max-height', '100vh')}; /* 1 */ ${logicalCSSWithFallback('overflow-y', 'auto')}; @@ -53,7 +54,7 @@ export const euiGlobalToastListStyles = (euiThemeContext: UseEuiTheme) => { ${logicalCSS('padding-vertical', euiTheme.size.base)}; } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { + ${euiMaxBreakpoint(euiThemeContext, 'm')} { &:not(:empty) { ${logicalCSS('left', 0)}; ${logicalCSS('width', '100%')}; /* 1 */ @@ -64,22 +65,18 @@ export const euiGlobalToastListStyles = (euiThemeContext: UseEuiTheme) => { right: css` &:not(:empty) { ${logicalCSS('right', 0)}; - ${logicalCSS('padding-left', `${euiTheme.base * 4}px`)}; /* 2 */ - } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { - &:not(:empty) { - ${logicalCSS('padding-left', euiTheme.size.base)}; + + ${euiMinBreakpoint(euiThemeContext, 'm')} { + ${logicalCSS('padding-left', `${euiTheme.base * 4}px`)}; /* 2 */ } } `, left: css` &:not(:empty) { ${logicalCSS('left', 0)}; - ${logicalCSS('padding-right', `${euiTheme.base * 4}px`)}; /* 2 */ - } - ${euiBreakpoint(euiThemeContext, ['xs', 's'])} { - &:not(:empty) { - ${logicalCSS('padding-right', euiTheme.size.base)}; + + ${euiMinBreakpoint(euiThemeContext, 'm')} { + ${logicalCSS('padding-right', `${euiTheme.base * 4}px`)}; /* 2 */ } } `, diff --git a/src/global_styling/mixins/__snapshots__/_responsive.test.ts.snap b/src/global_styling/mixins/__snapshots__/_responsive.test.ts.snap index f8c2f08c825..c21c6c6e12c 100644 --- a/src/global_styling/mixins/__snapshots__/_responsive.test.ts.snap +++ b/src/global_styling/mixins/__snapshots__/_responsive.test.ts.snap @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`euiMaxBreakpoint generates a max-width only media query (l) 1`] = `"@media only screen and (max-width: 991px)"`; + +exports[`euiMaxBreakpoint generates a max-width only media query (m) 1`] = `"@media only screen and (max-width: 767px)"`; + +exports[`euiMaxBreakpoint generates a max-width only media query (s) 1`] = `"@media only screen and (max-width: 574px)"`; + +exports[`euiMaxBreakpoint generates a max-width only media query (xl) 1`] = `"@media only screen and (max-width: 1199px)"`; + +exports[`euiMinBreakpoint generates a min-width only media query (l) 1`] = `"@media only screen and (min-width: 992px)"`; + +exports[`euiMinBreakpoint generates a min-width only media query (m) 1`] = `"@media only screen and (min-width: 768px)"`; + +exports[`euiMinBreakpoint generates a min-width only media query (s) 1`] = `"@media only screen and (min-width: 575px)"`; + +exports[`euiMinBreakpoint generates a min-width only media query (xl) 1`] = `"@media only screen and (min-width: 1200px)"`; + exports[`useEuiBreakpoint common breakpoint size arrays returns a media query for two element breakpoint combinations (l and xl) 1`] = `"@media only screen and (min-width: 992px)"`; exports[`useEuiBreakpoint common breakpoint size arrays returns a media query for two element breakpoint combinations (m and l) 1`] = `"@media only screen and (min-width: 768px) and (max-width: 1199px)"`; diff --git a/src/global_styling/mixins/_responsive.test.ts b/src/global_styling/mixins/_responsive.test.ts index 83fe83c4619..6c5da7c013f 100644 --- a/src/global_styling/mixins/_responsive.test.ts +++ b/src/global_styling/mixins/_responsive.test.ts @@ -8,7 +8,14 @@ import { testCustomHook } from '../../test/internal'; import { EuiThemeBreakpoints, _EuiThemeBreakpoint } from '../variables'; -import { useEuiBreakpoint, euiBreakpoint } from './_responsive'; +import { + useEuiBreakpoint, + euiBreakpoint, + useEuiMinBreakpoint, + euiMinBreakpoint, + useEuiMaxBreakpoint, + euiMaxBreakpoint, +} from './_responsive'; describe('useEuiBreakpoint', () => { describe('common breakpoint size arrays', () => { @@ -97,7 +104,71 @@ describe('useEuiBreakpoint', () => { }); }); -describe('euiBreakpoint & custom theme breakpoints', () => { +describe('euiMinBreakpoint', () => { + describe('generates a min-width only media query', () => { + EuiThemeBreakpoints.slice(1).forEach((size) => { + it(`(${size})`, () => { + expect( + testCustomHook(() => useEuiMinBreakpoint(size)).return + ).toMatchSnapshot(); + }); + }); + }); + + describe('fallback behavior', () => { + const warnSpy = jest.spyOn(console, 'warn'); + beforeEach(() => warnSpy.mockReset()); + + it('warns if using min-width on a breakpoint that equals 0px', () => { + // This functionally does nothing, hence the warning + expect( + testCustomHook(() => useEuiMinBreakpoint('xs')).return + ).toMatchInlineSnapshot('"@media only screen"'); + expect(warnSpy).toHaveBeenCalledWith('Invalid min breakpoint size: xs'); + }); + + it('warns if an invalid size is passed', () => { + expect( + testCustomHook(() => useEuiMinBreakpoint('asdf')).return + ).toMatchInlineSnapshot('"@media only screen"'); + expect(warnSpy).toHaveBeenCalledWith('Invalid min breakpoint size: asdf'); + }); + }); +}); + +describe('euiMaxBreakpoint', () => { + describe('generates a max-width only media query', () => { + EuiThemeBreakpoints.slice(1).forEach((size) => { + it(`(${size})`, () => { + expect( + testCustomHook(() => useEuiMaxBreakpoint(size)).return + ).toMatchSnapshot(); + }); + }); + }); + + describe('fallback behavior', () => { + const warnSpy = jest.spyOn(console, 'warn'); + beforeEach(() => warnSpy.mockReset()); + + it('warns if using max-width on a breakpoint that equals 0px', () => { + // This functionally does nothing, hence the warning + expect( + testCustomHook(() => useEuiMaxBreakpoint('xs')).return + ).toMatchInlineSnapshot('"@media only screen"'); + expect(warnSpy).toHaveBeenCalledWith('Invalid max breakpoint size: xs'); + }); + + it('warns if an invalid size is passed', () => { + expect( + testCustomHook(() => useEuiMaxBreakpoint('asdf')).return + ).toMatchInlineSnapshot('"@media only screen"'); + expect(warnSpy).toHaveBeenCalledWith('Invalid max breakpoint size: asdf'); + }); + }); +}); + +describe('custom theme breakpoints', () => { const CUSTOM_BREAKPOINTS = { xxl: 700, xl: 600, @@ -109,21 +180,45 @@ describe('euiBreakpoint & custom theme breakpoints', () => { }; const mockEuiTheme: any = { euiTheme: { breakpoint: CUSTOM_BREAKPOINTS } }; - it('correctly inherits the breakpoint size override', () => { - expect(euiBreakpoint(mockEuiTheme, ['s', 'l'])).toMatchInlineSnapshot( - '"@media only screen and (min-width: 300px) and (max-width: 599px)"' - ); + describe('euiBreakpoint', () => { + it('correctly inherits the breakpoint size override', () => { + expect(euiBreakpoint(mockEuiTheme, ['s', 'l'])).toMatchInlineSnapshot( + '"@media only screen and (min-width: 300px) and (max-width: 599px)"' + ); + }); + + it('correctly infers the largest breakpoint and does not render a max-width if passed', () => { + expect(euiBreakpoint(mockEuiTheme, ['xl', 'xxl'])).toMatchInlineSnapshot( + '"@media only screen and (min-width: 600px)"' + ); + }); + + it('correctly uses the smallest breakpoint for a min-width if it is not set to 0', () => { + expect(euiBreakpoint(mockEuiTheme, ['xxs', 'xs'])).toMatchInlineSnapshot( + '"@media only screen and (min-width: 100px) and (max-width: 299px)"' + ); + }); }); - it('correctly infers the largest breakpoint and does not render a max-width if passed', () => { - expect(euiBreakpoint(mockEuiTheme, ['xl', 'xxl'])).toMatchInlineSnapshot( - '"@media only screen and (min-width: 600px)"' - ); + describe('euiMinBreakpoint', () => { + it('correctly inherits the custom breakpoint sizes', () => { + expect(euiMinBreakpoint(mockEuiTheme, 'xxs')).toMatchInlineSnapshot( + '"@media only screen and (min-width: 100px)"' + ); + expect(euiMinBreakpoint(mockEuiTheme, 'm')).toMatchInlineSnapshot( + '"@media only screen and (min-width: 400px)"' + ); + }); }); - it('correctly uses the smallest breakpoint for a min-width if it is not set to 0', () => { - expect(euiBreakpoint(mockEuiTheme, ['xxs', 'xs'])).toMatchInlineSnapshot( - '"@media only screen and (min-width: 100px) and (max-width: 299px)"' - ); + describe('euiMaxBreakpoint', () => { + it('correctly inherits the custom breakpoint sizes', () => { + expect(euiMaxBreakpoint(mockEuiTheme, 'l')).toMatchInlineSnapshot( + '"@media only screen and (max-width: 499px)"' + ); + expect(euiMaxBreakpoint(mockEuiTheme, 'xxl')).toMatchInlineSnapshot( + '"@media only screen and (max-width: 699px)"' + ); + }); }); }); diff --git a/src/global_styling/mixins/_responsive.ts b/src/global_styling/mixins/_responsive.ts index 54827c4b80f..b125196c11e 100644 --- a/src/global_styling/mixins/_responsive.ts +++ b/src/global_styling/mixins/_responsive.ts @@ -11,7 +11,7 @@ import { useEuiTheme, UseEuiTheme } from '../../services/theme/hooks'; import { _EuiThemeBreakpoint } from '../variables'; /** - * Generates a CSS media query rule string based on the input breakpoint ranges. + * Generates a CSS media query rule string based on the input breakpoint *ranges*. * Examples with default theme breakpoints: * * euiBreakpoint(['s']) becomes `@media only screen and (min-width: 575px) and (max-width: 767px)` @@ -65,3 +65,54 @@ export const useEuiBreakpoint = ( const euiTheme = useEuiTheme(); return euiBreakpoint(euiTheme, sizes); }; + +/** + * Min/Max width breakpoint utilities that generate only a single min/max query/bound + * + * *Unlike the above euiBreakpoint utility*, these utilities treat breakpoint + * sizes as a one-dimensional point, rather than a two-dimensional *screen range*. + * Examples with default theme breakpoints: + * + * euiMaxBreakpoint('m') becomes `@media only screen and (max-width: 767px)` + * euiMinBreakpoint('m') becomes `@media only screen and (min-width: 768px)` + * + * This is safer and more intentional to use than euiBreakpoint(['xs', 's']) / euiBreakpoint(['m', 'xl']) + * in the event that consumers add larger or smaller custom breakpoints (e.g 'xxs' or `xxl`) + * and if the intention of the media query is actually "m and below/above" vs. "only screens m/l/xl". + */ + +export const euiMinBreakpoint = ( + { euiTheme }: UseEuiTheme, + size: _EuiThemeBreakpoint +) => { + const minBreakpointSize = euiTheme.breakpoint[size]; + if (minBreakpointSize) { + return `@media only screen and (min-width: ${minBreakpointSize}px)`; + } else { + console.warn(`Invalid min breakpoint size: ${size}`); + return '@media only screen'; + } +}; + +export const useEuiMinBreakpoint = (size: _EuiThemeBreakpoint) => { + const euiTheme = useEuiTheme(); + return euiMinBreakpoint(euiTheme, size); +}; + +export const euiMaxBreakpoint = ( + { euiTheme }: UseEuiTheme, + size: _EuiThemeBreakpoint +) => { + const maxBreakpointSize = euiTheme.breakpoint[size]; + if (maxBreakpointSize) { + return `@media only screen and (max-width: ${maxBreakpointSize - 1}px)`; + } else { + console.warn(`Invalid max breakpoint size: ${size}`); + return '@media only screen'; + } +}; + +export const useEuiMaxBreakpoint = (size: _EuiThemeBreakpoint) => { + const euiTheme = useEuiTheme(); + return euiMaxBreakpoint(euiTheme, size); +}; diff --git a/upcoming_changelogs/6431.md b/upcoming_changelogs/6431.md new file mode 100644 index 00000000000..c9bd225670c --- /dev/null +++ b/upcoming_changelogs/6431.md @@ -0,0 +1,5 @@ +- Added the `euiMaxBreakpoint` and `euiMinBreakpoint` CSS-in-JS utilities for creating min/max-width media queries + +**Bug fixes** + +- Fixed multiple component media queries for consumers with custom theme breakpoints