diff --git a/CHANGELOG.md b/CHANGELOG.md
index 215f03d3496..c90c82f0baa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
- Added `textTransform` property to `schemaDetectors` prop of `EuiDataGrid`([#4752](https://github.com/elastic/eui/pull/4752))
- Added `color`, `continuityAbove`, `continuityAboveBelow`, `continuityBelow`, `continuityWithin`, `eraser`, `fullScreenExit`, `function`, `percent`, `wordWrap`, and `wordWrapDisabled` glyphs to `EuiIcon` ([#4779](https://github.com/elastic/eui/pull/4779))
+- Added `as`, `role`, `closeButtonProps`, `closeButtonPosition`, `outsideClickCloses`, `side`, `type`, and `pushMinBreakpoint` props to `EuiFlyout` ([#4713](https://github.com/elastic/eui/pull/4713))
+- Extended `EuiFlyout` `size` prop to accept any CSS `width` value ([#4713](https://github.com/elastic/eui/pull/4713))
+- Extended `EuiFlyout` and most of its props in `EuiCollapsibleNav` ([#4713](https://github.com/elastic/eui/pull/4713))
+
+**Breaking changes**
+
+- Changed the default of `EuiFlyout` `ownFocus` to `true` ([#4713](https://github.com/elastic/eui/pull/4713))
+- Wrapped `EuiFlyout` within the `EuiOverlayMask` when `ownFocus=true` ([#4713](https://github.com/elastic/eui/pull/4713))
+- Changed `EuiCollapsibleNav` width sizing from a Sass variable to a `size` prop ([#4713](https://github.com/elastic/eui/pull/4713))
+- Changed `EuiOverlayMask` z-indexing when positioned `below` header to using `top` offset ([#4713](https://github.com/elastic/eui/pull/4713))
**Bug fixes**
diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss
index 56689840182..992a3bd721a 100644
--- a/src-docs/src/components/guide_components.scss
+++ b/src-docs/src/components/guide_components.scss
@@ -211,7 +211,7 @@ $elasticLogoTextDark: #1C1E23;
height: 100%;
left: 0;
top: 0;
- z-index: $euiZHeader + 1;
+ z-index: $euiZHeader;
overflow: auto;
&--withHeader {
diff --git a/src-docs/src/services/playground/knobs.js b/src-docs/src/services/playground/knobs.js
index 92b4eac1d49..73f46159645 100644
--- a/src-docs/src/services/playground/knobs.js
+++ b/src-docs/src/services/playground/knobs.js
@@ -324,7 +324,7 @@ const Knob = ({
}}
/>
);
- } else return null;
+ } else return helpText || null;
case PropTypes.Custom:
if (custom && custom.use) {
@@ -352,9 +352,9 @@ const Knob = ({
case PropTypes.Function:
case PropTypes.Array:
case PropTypes.Object:
- return null;
+ return helpText || null;
default:
- return assertUnreachable();
+ return helpText || assertUnreachable();
}
};
diff --git a/src-docs/src/views/accordion/accordion_multiple.js b/src-docs/src/views/accordion/accordion_multiple.js
index 5167bc76a34..0d801cfec29 100644
--- a/src-docs/src/views/accordion/accordion_multiple.js
+++ b/src-docs/src/views/accordion/accordion_multiple.js
@@ -45,7 +45,7 @@ export default () => (
paddingSize="m">
- This content area will grow to accomodate when the accordion below
+ This content area will grow to accommodate when the accordion below
opens
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
index bec9da37bea..fa5925e5469 100644
--- a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
@@ -8,16 +8,25 @@ import { EuiText } from '../../../../src/components/text';
import { EuiCode } from '../../../../src/components/code';
export default () => {
- const [navIsOpen, setNavIsOpen] = useState(false);
- const [navIsDocked, setNavIsDocked] = useState(false);
+ const [navIsOpen, setNavIsOpen] = useState(
+ JSON.parse(
+ String(localStorage.getItem('euiCollapsibleNavExample--isDocked'))
+ ) || false
+ );
+ const [navIsDocked, setNavIsDocked] = useState(
+ JSON.parse(
+ String(localStorage.getItem('euiCollapsibleNavExample--isDocked'))
+ ) || false
+ );
return (
<>
setNavIsOpen(!navIsOpen)}>
+ setNavIsOpen((isOpen) => !isOpen)}>
Toggle nav
}
@@ -27,9 +36,20 @@ export default () => {
I am some nav
+
+
+ The docked status is being stored in{' '}
+ localStorage .
+
+
+
{
setNavIsDocked(!navIsDocked);
+ localStorage.setItem(
+ 'euiCollapsibleNavExample--isDocked',
+ JSON.stringify(!navIsDocked)
+ );
}}>
Docked: {navIsDocked ? 'on' : 'off'}
@@ -39,8 +59,9 @@ export default () => {
{navIsDocked && (
- The button gets hidden by default when the nav is docked unless you
- set showButtonIfDocked = true .
+ The button gets hidden by default when the nav is
+ docked unless you set{' '}
+ showButtonIfDocked = true .
)}
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
index d498c1c6992..5de38125816 100644
--- a/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
@@ -56,11 +56,9 @@ const LearnLinks: EuiPinnableListGroupItemProps[] = [
];
export default () => {
- const [navIsOpen, setNavIsOpen] = useState(
- JSON.parse(String(localStorage.getItem('navIsDocked'))) || false
- );
+ const [navIsOpen, setNavIsOpen] = useState(true);
const [navIsDocked, setNavIsDocked] = useState(
- JSON.parse(String(localStorage.getItem('navIsDocked'))) || false
+ JSON.parse(String(localStorage.getItem('nav2IsDocked'))) || false
);
/**
@@ -234,7 +232,7 @@ export default () => {
onClick={() => {
setNavIsDocked(!navIsDocked);
localStorage.setItem(
- 'navIsDocked',
+ 'nav2IsDocked',
JSON.stringify(!navIsDocked)
);
}}
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js
index 76ece572a5b..e2d3dc7b849 100644
--- a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js
@@ -1,8 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
-import { renderToHtml } from '../../services';
-
import { GuideSectionTypes } from '../../components';
import {
@@ -11,23 +9,22 @@ import {
EuiText,
EuiCallOut,
EuiCollapsibleNavGroup,
+ EuiHorizontalRule,
} from '../../../../src/components';
+import { collapsibleNavConfig } from './playground';
+
import CollapsibleNav from './collapsible_nav';
const collapsibleNavSource = require('!!raw-loader!./collapsible_nav');
-const collapsibleNavHtml = renderToHtml(CollapsibleNav);
import CollapsibleNavGroup from './collapsible_nav_group';
const collapsibleNavGroupSource = require('!!raw-loader!./collapsible_nav_group');
-const collapsibleNavGroupHtml = renderToHtml(CollapsibleNavGroup);
import CollapsibleNavList from './collapsible_nav_list';
const collapsibleNavListSource = require('!!raw-loader!./collapsible_nav_list');
-const collapsibleNavListHtml = renderToHtml(CollapsibleNavList);
import CollapsibleNavAll from './collapsible_nav_all';
const collapsibleNavAllSource = require('!!raw-loader!./collapsible_nav_all');
-const collapsibleNavAllHtml = renderToHtml(CollapsibleNavAll);
export const CollapsibleNavExample = {
title: 'Collapsible nav',
@@ -46,19 +43,15 @@ export const CollapsibleNavExample = {
type: GuideSectionTypes.JS,
code: collapsibleNavSource,
},
- {
- type: GuideSectionTypes.HTML,
- code: collapsibleNavHtml,
- },
],
text: (
<>
- EuiCollapsibleNav is a similar implementation to{' '}
+ EuiCollapsibleNav is a custom implementation of{' '}
EuiFlyout
- ; the visibility of which must be maintained by the consuming
+ ; the visibility of which must still be maintained by the consuming
application. An extra feature that it provides is the ability to{' '}
dock the flyout. This affixes the flyout to the
window and pushes the body content by adding left side padding.
@@ -72,11 +65,13 @@ export const CollapsibleNavExample = {
props: { EuiCollapsibleNav },
demo: ,
snippet: ` setNavIsOpen(!navIsOpen)}>Toggle nav}
isOpen={navIsOpen}
isDocked={navIsDocked}
onClose={() => setNavIsOpen(false)}
/>`,
+ playground: collapsibleNavConfig,
},
{
title: 'Collapsible nav group',
@@ -85,10 +80,6 @@ export const CollapsibleNavExample = {
type: GuideSectionTypes.JS,
code: collapsibleNavGroupSource,
},
- {
- type: GuideSectionTypes.HTML,
- code: collapsibleNavGroupHtml,
- },
],
text: (
<>
@@ -130,10 +121,6 @@ export const CollapsibleNavExample = {
type: GuideSectionTypes.JS,
code: collapsibleNavListSource,
},
- {
- type: GuideSectionTypes.HTML,
- code: collapsibleNavListHtml,
- },
],
text: (
<>
@@ -152,7 +139,15 @@ export const CollapsibleNavExample = {
Below are a few established patterns to use.
>
),
- demo: ,
+ demo: (
+
+
+
+
+ ),
+ demoPanelProps: {
+ paddingSize: 'none',
+ },
snippet: `
diff --git a/src-docs/src/views/collapsible_nav/playground.js b/src-docs/src/views/collapsible_nav/playground.js
new file mode 100644
index 00000000000..de52a76f769
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/playground.js
@@ -0,0 +1,54 @@
+import { PropTypes } from 'react-view';
+import { EuiCollapsibleNav } from '../../../../src/components/';
+import { propUtilityForPlayground } from '../../services/playground';
+
+export const collapsibleNavConfig = () => {
+ const docgenInfo = Array.isArray(EuiCollapsibleNav.__docgenInfo)
+ ? EuiCollapsibleNav.__docgenInfo[0]
+ : EuiCollapsibleNav.__docgenInfo;
+ const propsToUse = propUtilityForPlayground(docgenInfo.props);
+
+ propsToUse.isOpen = {
+ ...propsToUse.isOpen,
+ value: true,
+ };
+
+ propsToUse.ownFocus = {
+ ...propsToUse.ownFocus,
+ value: false,
+ disabled: true,
+ };
+
+ propsToUse.size = {
+ ...propsToUse.size,
+ type: PropTypes.Number,
+ value: 240,
+ };
+
+ propsToUse.as = {
+ ...propsToUse.as,
+ type: PropTypes.string,
+ value: 'nav',
+ };
+
+ propsToUse.as = {
+ ...propsToUse.as,
+ type: PropTypes.string,
+ value: 'nav',
+ };
+
+ return {
+ config: {
+ componentName: 'EuiCollapsibleNav',
+ props: propsToUse,
+ scope: {
+ EuiCollapsibleNav,
+ },
+ imports: {
+ '@elastic/eui': {
+ named: ['EuiCollapsibleNav'],
+ },
+ },
+ },
+ };
+};
diff --git a/src-docs/src/views/flyout/flyout_example.js b/src-docs/src/views/flyout/flyout_example.js
index 38e6ecd39e2..c57729a21ae 100644
--- a/src-docs/src/views/flyout/flyout_example.js
+++ b/src-docs/src/views/flyout/flyout_example.js
@@ -33,7 +33,10 @@ const flyoutMaxWidthSource = require('!!raw-loader!./flyout_max_width');
import FlyoutWithBanner from './flyout_banner';
const flyoutWithBannerSource = require('!!raw-loader!./flyout_banner');
-const flyOutSnippet = `
+import FlyoutPush from './flyout_push';
+const flyoutPushSource = require('!!raw-loader!./flyout_push');
+
+const flyOutSnippet = `
@@ -45,7 +48,7 @@ const flyOutSnippet = `
`;
-const flyoutComplicatedSnippet = `
+const flyoutComplicatedSnippet = `
@@ -63,7 +66,7 @@ const flyoutComplicatedSnippet = `
`;
-const flyoutSmallSnippet = `
+const flyoutSmallSnippet = `
@@ -89,7 +92,7 @@ const flyoutMediumPaddingSnippet = `
`;
-const flyoutMaxWidthSnippet = `
+const flyoutMaxWidthSnippet = `
@@ -101,7 +104,7 @@ const flyoutMaxWidthSnippet = `
`;
-const flyoutLargeSnippet = `
+const flyoutLargeSnippet = `
@@ -113,7 +116,7 @@ const flyoutLargeSnippet = `
`;
-const flyoutWithBannerSnippet = `
+const flyoutWithBannerSnippet = `
@@ -125,6 +128,21 @@ const flyoutWithBannerSnippet = `
`;
+const flyoutPushedSnippet = `
+
+
+
+
+
+
+
+
+
+ Close
+
+
+`;
+
export const FlyoutExample = {
title: 'Flyout',
sections: [
@@ -139,7 +157,7 @@ export const FlyoutExample = {
<>
EuiFlyout is a fixed position panel that pops in
- from the right side of the screen. It should be used to reveal more
+ from the side of the window. It should be used to reveal more
detailed contextual information or to provide complex forms without
losing the user's current state. It is a good alternative to{' '}
modals when the action is not
@@ -156,9 +174,8 @@ export const FlyoutExample = {
iconType="accessibility"
title={
<>
- Use {'aria-labelledby={headingId}'} and{' '}
- ownFocus to announce the flyout to screen
- readers.
+ Use {'aria-labelledby={headingId}'} to
+ announce the flyout to screen readers.
>
}
/>
@@ -203,9 +220,11 @@ export const FlyoutExample = {
],
text: (
- Flyouts come in three predefined size s,{' '}
- {"'x' | 'm' | 'l'"} , which define the width
- relative to the window size with a minimum width defined in pixels.
+ Flyouts come in three predefined size s of{' '}
+ {"'s' | 'm' | 'l'"} , which define the width{' '}
+ relative to the window size with a minimum width
+ defined in pixels. You can otherwise supply your own fixed width in
+ number or string format.
),
snippet: flyoutLargeSnippet,
@@ -226,7 +245,7 @@ export const FlyoutExample = {
wrapping EuiFlyout component. This ensures that all
the horizontal edges line up no matter the{' '}
paddingSize . When using the{' '}
- {'"none"'} size, you will need to accomodate your
+ {'"none"'} size, you will need to accommodate your
content with some other way of creating distance to the edges of the
flyout.
@@ -269,13 +288,17 @@ export const FlyoutExample = {
text: (
<>
- Like modals, you can, and usually want to, obscure the page content
- beneath with ownFocus which adds an{' '}
+ Like modals, you will usually want to obscure the page content
+ beneath with ownFocus which wraps the flyout with
+ an{' '}
EuiOverlayMask
-
- . By not adding this prop, the the underlying page content will be
- visible and clickable.
+ {' '}
+ . However, there are use-cases where flyouts present more
+ information or controls, but need to maintain the interactions of
+ the page content. By setting{' '}
+ {'ownFocus={false}'} , the
+ underlying page content will be visible and clickable.
>
),
@@ -283,6 +306,35 @@ export const FlyoutExample = {
demo: ,
props: { EuiFlyout },
},
+ {
+ title: 'Push versus overlay',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: flyoutPushSource,
+ },
+ ],
+ text: (
+
+
+ Another way to allow for continued interactions of the page content
+ while a flyout is visible, is to change the type {' '}
+ from overlay to push .
+
+
+ A pushed flyout still positions itself as fixed ,
+ but adds padding to the document's body element to accommodate
+ for the flyout's width. Because this squishes the page content,
+ the flyout changes back to overlay at smaller
+ window widths. You can adjust this minimum breakpoint with{' '}
+ pushMinBreakpoint .
+
+
+ ),
+ snippet: flyoutPushedSnippet,
+ demo: ,
+ props: { EuiFlyout },
+ },
{
title: 'Understanding max-width',
source: [
diff --git a/src-docs/src/views/flyout/flyout_large.js b/src-docs/src/views/flyout/flyout_large.js
index 9a8808af71e..d7e5b591d38 100644
--- a/src-docs/src/views/flyout/flyout_large.js
+++ b/src-docs/src/views/flyout/flyout_large.js
@@ -28,6 +28,10 @@ export default () => {
id: 'l',
label: 'Large',
},
+ {
+ id: '400px',
+ label: 'Fixed (400)',
+ },
];
const closeFlyout = () => setIsFlyoutVisible(false);
diff --git a/src-docs/src/views/flyout/flyout_max_width.js b/src-docs/src/views/flyout/flyout_max_width.js
index 4508f449921..8dae53b4738 100644
--- a/src-docs/src/views/flyout/flyout_max_width.js
+++ b/src-docs/src/views/flyout/flyout_max_width.js
@@ -167,6 +167,27 @@ export default () => {
+ showFlyout(240)}>
+ Show 240 flyout with no max-width
+
+
+ showFlyout(240, true)}>
+ Show 240 flyout with default max-width
+
+
+ showFlyout(240, 110)}>
+ Show 240 flyout with{' '}
+ smaller custom max-width -- max-width wins but width
+ wins on small screens
+
+
+ showFlyout(240, 1600)}>
+ Show 240 flyout with{' '}
+ larger custom max-width
+
+
+
+
showFlyout('m', 0)}>
Trick for forms: Medium flyout with{' '}
0 as max-width
diff --git a/src-docs/src/views/flyout/flyout_push.js b/src-docs/src/views/flyout/flyout_push.js
new file mode 100644
index 00000000000..5ba18cebc6b
--- /dev/null
+++ b/src-docs/src/views/flyout/flyout_push.js
@@ -0,0 +1,57 @@
+import React, { useState } from 'react';
+
+import {
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiButton,
+ EuiText,
+ EuiTitle,
+ EuiFlyoutFooter,
+} from '../../../../src/components';
+
+export default () => {
+ const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
+
+ let flyout;
+
+ if (isFlyoutVisible) {
+ flyout = (
+ setIsFlyoutVisible(false)}
+ aria-labelledby="pushedFlyoutTitle">
+
+
+ A pushed flyout
+
+
+
+
+
+ A pushed flyout typically contains more information about a
+ particular piece of data or complex form controls for editing.
+
+
+ Also, it is good to include a close button in the footer for a
+ larger hit target than the small close button provides.
+
+
+
+
+ setIsFlyoutVisible(false)}>Close
+
+
+ );
+ }
+
+ return (
+
+ setIsFlyoutVisible((visible) => !visible)}>
+ Toggle pushed flyout
+
+ {flyout}
+
+ );
+};
diff --git a/src-docs/src/views/flyout/flyout_small.js b/src-docs/src/views/flyout/flyout_small.js
index b11fa269fa2..852791c5bdf 100644
--- a/src-docs/src/views/flyout/flyout_small.js
+++ b/src-docs/src/views/flyout/flyout_small.js
@@ -22,7 +22,10 @@ export default () => {
let flyout;
if (isFlyoutVisible) {
flyout = (
-
+
A flyout without ownFocus
diff --git a/src-docs/src/views/header/header_alert.js b/src-docs/src/views/header/header_alert.js
index 49c0a318cb3..c349637bd56 100644
--- a/src-docs/src/views/header/header_alert.js
+++ b/src-docs/src/views/header/header_alert.js
@@ -304,7 +304,7 @@ const HeaderUserMenu = () => {
export default () => {
const [position, setPosition] = useState('static');
- const [theme, setTheme] = useState('light');
+ const [theme, setTheme] = useState('default');
return (
<>
@@ -321,7 +321,7 @@ export default () => {
setTheme(e.target.checked ? 'dark' : 'light')}
+ onChange={(e) => setTheme(e.target.checked ? 'dark' : 'default')}
/>
diff --git a/src-docs/src/views/page/page_bottom_bar.js b/src-docs/src/views/page/page_bottom_bar.js
index fc40a28c61e..1bceed1b972 100644
--- a/src-docs/src/views/page/page_bottom_bar.js
+++ b/src-docs/src/views/page/page_bottom_bar.js
@@ -15,7 +15,7 @@ export default ({ button = <>>, content, sideNav, bottomBar }) => {
{sideNav}
- {/* Double EuiPageBody to accomodate for the bottom bar */}
+ {/* Double EuiPageBody to accommodate for the bottom bar */}
EuiPageBody. This way it will never overlap the{' '}
EuiPageSideBar , no matter the screen size. It also
- means not needing to accomodate for the height of the bar in the
+ means not needing to accommodate for the height of the bar in the
body element.
(
...wrappingExampleStyle,
}}>
@@ -61,7 +61,7 @@ export default () => (
style={{ height: 200, overflowY: 'hidden' }}>
`}
/>
@@ -81,7 +81,7 @@ export default () => (
}
example={
-
+
Orbiting this at a distance of roughly ninety-two million miles
@@ -95,7 +95,7 @@ export default () => (
}
snippet={`
+ tabIndex={0}>
`}
/>
diff --git a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
index ab021c7d1a5..6b8a3a1bfd9 100644
--- a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
@@ -2,80 +2,47 @@
exports[`EuiCollapsibleNav close button can be hidden 1`] = `
Array [
-
,
-
,
-
,
- ,
-
,
-]
-`;
-
-exports[`EuiCollapsibleNav close button extends EuiButtonEmpty 1`] = `
-Array [
-
,
-
,
-
,
-
-
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
@@ -83,64 +50,54 @@ exports[`EuiCollapsibleNav does not render if isOpen is false 1`] = `null`;
exports[`EuiCollapsibleNav is rendered 1`] = `
Array [
-
,
-
,
-
,
-
-
+
+
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
-exports[`EuiCollapsibleNav props button 1`] = `
+exports[`EuiCollapsibleNav props accepts EuiFlyout props 1`] = `
Array [
- ,
-
,
-
-
- close
-
-
+ aria-hidden="true"
+ class="euiButtonIcon__icon"
+ color="inherit"
+ data-euiicon-type="cross"
+ />
,
@@ -187,101 +140,100 @@ Array [
]
`;
-exports[`EuiCollapsibleNav props can alter mask props with maskProps without throwing error 1`] = `
+exports[`EuiCollapsibleNav props button 1`] = `
Array [
-
,
-
,
-
,
-
-
+
+
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
exports[`EuiCollapsibleNav props dockedBreakpoint 1`] = `
Array [
-
,
-
,
-
,
-
-
+
+
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
@@ -301,29 +253,11 @@ Array [
data-focus-lock-disabled="disabled"
>
-
-
-
-
- close
-
-
-
-
+ style="width:320px"
+ tabindex="-1"
+ />
,
,
-
,
-
,
-
-
+
+
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
@@ -388,7 +319,6 @@ Array [
aria-controls="id"
aria-expanded="true"
aria-pressed="true"
- class="euiCollapsibleNav__toggle"
/>,
+
,
+
,
+]
+`;
+
+exports[`EuiCollapsibleNav props size 1`] = `
+Array [
+
+
+
+
-
-
-
- close
-
-
-
-
+
+
+
+
,
-
,
]
`;
diff --git a/src/components/collapsible_nav/_collapsible_nav.scss b/src/components/collapsible_nav/_collapsible_nav.scss
index d63860af27b..7f3b7c291c8 100644
--- a/src/components/collapsible_nav/_collapsible_nav.scss
+++ b/src/components/collapsible_nav/_collapsible_nav.scss
@@ -1,73 +1,5 @@
-// Extends euiFlyout
-@import '../flyout/variables';
-@import '../flyout/mixins';
+// Extends
-.euiCollapsibleNav {
- @include euiFlyout;
- border-left: none;
- right: auto;
- left: 0;
- width: $euiCollapsibleNavWidth;
- max-width: 80vw;
- animation: euiCollapsibleNavIn $euiAnimSpeedNormal $euiAnimSlightResistance;
- clip-path: polygon(0 0, 150% 0, 150% 100%, 0 100%); // Must include the width of the close button too
-}
-
-.euiCollapsibleNav__closeButton {
- position: absolute;
- right: 0;
- top: $euiSize;
- margin-right: -27%;
- padding: $euiSizeM 0;
- line-height: 1;
- border-radius: $euiBorderRadius;
-
- &:focus {
- @include euiFocusRing;
- // Override default `EuiButtonEmpty` :focus background to ensure better contrast
- background: $euiColorEmptyShade !important; // sass-lint:disable-line no-important
- }
-}
-
-// The addition of this class is handled through JS
-// via the `dockingBreakpoint` and `isDocked` combination
-.euiCollapsibleNav.euiCollapsibleNav--isDocked {
- @include euiBottomShadowMedium;
- z-index: $euiZHeader; // When docked, make it the same level as the header
- clip-path: none;
-
- .euiCollapsibleNav__closeButton {
- display: none;
- }
-}
-
-.euiBody--collapsibleNavIsDocked {
- // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS)
- padding-left: $euiCollapsibleNavWidth !important; // sass-lint:disable-line no-important
- transition: padding $euiAnimSpeedFast $euiAnimSlightResistance;
-}
-
-@include euiBreakpoint('xs') {
- // At tiny screens, reduce the close button to a simple `x`
- .euiCollapsibleNav__closeButton {
- margin-right: -15%;
-
- .euiCollapsibleNav__closeButtonText {
- // But be sure the text can still be read by a screen reader
- @include euiScreenReaderOnly;
- }
- }
-}
-
-// Specific keyframes so in comes in from the left
-@keyframes euiCollapsibleNavIn {
- 0% {
- opacity: 0;
- transform: translateX(-100%);
- }
-
- 75% {
- opacity: 1;
- transform: translateX(0%);
- }
+.euiCollapsibleNav:not([class*='push']) {
+ z-index: $euiZNavigation !important; // sass-lint:disable-line no-important
}
diff --git a/src/components/collapsible_nav/_variables.scss b/src/components/collapsible_nav/_variables.scss
index fbb573d7d2b..20d9200b330 100644
--- a/src/components/collapsible_nav/_variables.scss
+++ b/src/components/collapsible_nav/_variables.scss
@@ -1,6 +1,4 @@
// Sizing
-$euiCollapsibleNavWidth: $euiSize * 20 !default; // ~ 320px
-
$euiCollapsibleNavGroupLightBackgroundColor: $euiPageBackgroundColor !default;
$euiCollapsibleNavGroupDarkBackgroundColor: lightOrDarkTheme(
diff --git a/src/components/collapsible_nav/collapsible_nav.test.tsx b/src/components/collapsible_nav/collapsible_nav.test.tsx
index 980559a5562..7f926a0d670 100644
--- a/src/components/collapsible_nav/collapsible_nav.test.tsx
+++ b/src/components/collapsible_nav/collapsible_nav.test.tsx
@@ -22,6 +22,7 @@ import { render, mount } from 'enzyme';
import { requiredProps, takeMountedSnapshot } from '../../test';
import { EuiCollapsibleNav } from './collapsible_nav';
+import { EuiOverlayMaskProps } from '../overlay_mask';
jest.mock('../overlay_mask', () => ({
EuiOverlayMask: ({ headerZindexLocation, ...props }: any) => (
@@ -29,7 +30,17 @@ jest.mock('../overlay_mask', () => ({
),
}));
-const propsNeededToRender = { id: 'id', isOpen: true };
+jest.mock('../portal', () => ({
+ EuiPortal: ({ children }: { children: any }) => children,
+}));
+
+const propsNeededToRender = { id: 'id', isOpen: true, onClose: () => {} };
+const flyoutProps = {
+ size: 240,
+ ownFocus: false,
+ outsideClickCloses: false,
+ maskProps: { headerZindexLocation: 'above' } as EuiOverlayMaskProps,
+};
describe('EuiCollapsibleNav', () => {
test('is rendered', () => {
@@ -57,6 +68,18 @@ describe('EuiCollapsibleNav', () => {
).toMatchSnapshot();
});
+ test('size', () => {
+ const component = mount(
+
+ );
+
+ expect(
+ takeMountedSnapshot(component, {
+ hasArrayOutput: true,
+ })
+ ).toMatchSnapshot();
+ });
+
test('isDocked', () => {
const component = render(
@@ -106,12 +129,9 @@ describe('EuiCollapsibleNav', () => {
).toMatchSnapshot();
});
- test('can alter mask props with maskProps without throwing error', () => {
+ test('accepts EuiFlyout props', () => {
const component = mount(
-
+
);
expect(
@@ -125,22 +145,7 @@ describe('EuiCollapsibleNav', () => {
describe('close button', () => {
test('can be hidden', () => {
const component = mount(
-
- );
-
- expect(
- takeMountedSnapshot(component, {
- hasArrayOutput: true,
- })
- ).toMatchSnapshot();
- });
-
- test('extends EuiButtonEmpty', () => {
- const component = mount(
-
+
);
expect(
@@ -152,7 +157,7 @@ describe('EuiCollapsibleNav', () => {
});
test('does not render if isOpen is false', () => {
- const component = render( );
+ const component = render( {}} id="id" />);
expect(component).toMatchSnapshot();
});
diff --git a/src/components/collapsible_nav/collapsible_nav.tsx b/src/components/collapsible_nav/collapsible_nav.tsx
index 4d5ebf23931..35cf89aaecd 100644
--- a/src/components/collapsible_nav/collapsible_nav.tsx
+++ b/src/components/collapsible_nav/collapsible_nav.tsx
@@ -20,144 +20,109 @@
import React, {
cloneElement,
FunctionComponent,
- HTMLAttributes,
ReactElement,
ReactNode,
useEffect,
useState,
} from 'react';
import classNames from 'classnames';
+import { htmlIdGenerator, isWithinMinBreakpoint } from '../../services';
+import { EuiFlyout, EuiFlyoutProps } from '../flyout';
import { throttle } from '../color_picker/utils';
-import { EuiWindowEvent, htmlIdGenerator, keys } from '../../services';
-import { EuiFocusTrap } from '../focus_trap';
-import { EuiOverlayMask, EuiOverlayMaskProps } from '../overlay_mask';
-import { CommonProps } from '../common';
-import { EuiButtonEmpty, EuiButtonEmptyProps } from '../button';
-import { EuiI18n } from '../i18n';
-import { EuiScreenReaderOnly } from '../accessibility';
-export type EuiCollapsibleNavProps = CommonProps &
- HTMLAttributes & {
- /**
- * ReactNode to render as this component's content
- */
- children?: ReactNode;
- /**
- * Keeps navigation flyout visible and push `` content via padding
- */
- isDocked?: boolean;
- /**
- * Pixel value for customizing the minimum window width for enabling docking
- */
- dockedBreakpoint?: number;
- /**
- * Shows the navigation flyout
- */
- isOpen?: boolean;
- /**
- * Button for controlling visible state of the nav
- */
- button?: ReactElement;
- /**
- * Keeps the display of toggle button when in docked state
- */
- showButtonIfDocked?: boolean;
- /**
- * Keeps the display of floating close button.
- * If `false`, you must then keep the `button` displayed at all breakpoints.
- */
- showCloseButton?: boolean;
- /**
- * Extend the props of the close button, an EuiButtonEmpty
- */
- closeButtonProps?: EuiButtonEmptyProps;
- onClose?: () => void;
- /**
- * Adjustments to the EuiOverlayMask
- */
- maskProps?: EuiOverlayMaskProps;
- };
+// Extend all the flyout props except `onClose` because we handle this internally
+export type EuiCollapsibleNavProps = Omit<
+ EuiFlyoutProps,
+ 'closeButtonAriaLabel' | 'type' | 'pushBreakpoint'
+> & {
+ /**
+ * ReactNode to render as this component's content
+ */
+ children?: ReactNode;
+ /**
+ * Shows the navigation flyout
+ */
+ isOpen?: boolean;
+ /**
+ * Keeps navigation flyout visible and push `` content via padding
+ */
+ isDocked?: boolean;
+ /**
+ * Named breakpoint or pixel value for customizing the minimum window width to enable docking
+ */
+ dockedBreakpoint?: EuiFlyoutProps['pushMinBreakpoint'];
+ /**
+ * Button for controlling visible state of the nav
+ */
+ button?: ReactElement;
+ /**
+ * Keeps the display of toggle button when in docked state
+ */
+ showButtonIfDocked?: boolean;
+};
export const EuiCollapsibleNav: FunctionComponent = ({
+ id,
children,
className,
isDocked = false,
isOpen = false,
button,
showButtonIfDocked = false,
- dockedBreakpoint = 992,
- showCloseButton = true,
- closeButtonProps,
- onClose,
- id,
- maskProps,
+ dockedBreakpoint = 'l',
+ // Setting different EuiFlyout defaults
+ as = 'nav' as EuiCollapsibleNavProps['as'],
+ size = 320,
+ side = 'left',
+ role = null,
+ ownFocus = true,
+ outsideClickCloses = true,
+ closeButtonPosition = 'outside',
+ paddingSize = 'none',
...rest
}) => {
const [flyoutID] = useState(id || htmlIdGenerator()('euiCollapsibleNav'));
- const [windowIsLargeEnoughToDock, setWindowIsLargeEnoughToDock] = useState(
- (typeof window === 'undefined' ? Infinity : window.innerWidth) >=
+
+ /**
+ * Setting the initial state of pushed based on the `type` prop
+ * and if the current window size is large enough (larger than `pushBreakpoint`)
+ */
+ const [windowIsLargeEnoughToPush, setWindowIsLargeEnoughToPush] = useState(
+ isWithinMinBreakpoint(
+ typeof window === 'undefined' ? 0 : window.innerWidth,
dockedBreakpoint
+ )
);
- const navIsDocked = isDocked && windowIsLargeEnoughToDock;
+ const navIsDocked = isDocked && windowIsLargeEnoughToPush;
+
+ /**
+ * Watcher added to the window to maintain `isPushed` state depending on
+ * the window size compared to the `pushBreakpoint`
+ */
const functionToCallOnWindowResize = throttle(() => {
- if (window.innerWidth < dockedBreakpoint) {
- setWindowIsLargeEnoughToDock(false);
+ if (isWithinMinBreakpoint(window.innerWidth, dockedBreakpoint)) {
+ setWindowIsLargeEnoughToPush(true);
} else {
- setWindowIsLargeEnoughToDock(true);
+ setWindowIsLargeEnoughToPush(false);
}
// reacts every 50ms to resize changes and always gets the final update
}, 50);
- // Watch for docked status and appropriately add/remove body classes and resize handlers
useEffect(() => {
- window.addEventListener('resize', functionToCallOnWindowResize);
-
- if (navIsDocked) {
- document.body.classList.add('euiBody--collapsibleNavIsDocked');
- } else if (isOpen) {
- document.body.classList.add('euiBody--collapsibleNavIsOpen');
+ if (isDocked) {
+ // Only add the event listener if we'll need to accommodate with padding
+ window.addEventListener('resize', functionToCallOnWindowResize);
}
return () => {
- document.body.classList.remove('euiBody--collapsibleNavIsDocked');
- document.body.classList.remove('euiBody--collapsibleNavIsOpen');
- window.removeEventListener('resize', functionToCallOnWindowResize);
+ if (isDocked) {
+ window.removeEventListener('resize', functionToCallOnWindowResize);
+ }
};
- }, [navIsDocked, functionToCallOnWindowResize, isOpen]);
+ }, [isDocked, functionToCallOnWindowResize]);
- const onKeyDown = (event: KeyboardEvent) => {
- if (event.key === keys.ESCAPE) {
- event.preventDefault();
- collapse();
- }
- };
-
- const collapse = () => {
- // Skip collapsing if it is docked
- if (navIsDocked) {
- return;
- } else {
- onClose && onClose();
- }
- };
-
- const classes = classNames(
- 'euiCollapsibleNav',
- { 'euiCollapsibleNav--isDocked': navIsDocked },
- className
- );
-
- let optionalOverlay;
- if (!navIsDocked) {
- optionalOverlay = (
-
- );
- }
+ const classes = classNames('euiCollapsibleNav', className);
// Show a trigger button if one was passed but
// not if navIsDocked and showButtonIfDocked is false
@@ -169,41 +134,35 @@ export const EuiCollapsibleNav: FunctionComponent = ({
'aria-controls': flyoutID,
'aria-expanded': isOpen,
'aria-pressed': isOpen,
- className: classNames(
- button.props.className,
- 'euiCollapsibleNav__toggle'
- ),
+ // When EuiOutsideClickDetector is enabled, we don't want both the toggle button and document touches/clicks to happen, they'll cancel eachother out
+ onTouchEnd: (e: React.MouseEvent) => {
+ e.nativeEvent.stopImmediatePropagation();
+ },
+ onMouseUpCapture: (e: React.MouseEvent) => {
+ e.nativeEvent.stopImmediatePropagation();
+ },
});
- const closeButton = showCloseButton && (
-
-
-
-
-
- );
-
const flyout = (
- <>
-
- {optionalOverlay}
- {/* Trap focus only when docked={false} */}
-
-
- {children}
- {closeButton}
-
-
- >
+
+ {children}
+
);
return (
diff --git a/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
index 58b69ee2787..8a6f9696580 100644
--- a/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
+++ b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
@@ -6,44 +6,28 @@ exports[`EuiCollapsibleNavGroup is rendered 1`] = `
class="euiCollapsibleNavGroup testClass1 testClass2"
data-test-subj="test subject string"
id="id"
->
-
-
+/>
`;
exports[`EuiCollapsibleNavGroup props background dark is rendered 1`] = `
+/>
`;
exports[`EuiCollapsibleNavGroup props background light is rendered 1`] = `
+/>
`;
exports[`EuiCollapsibleNavGroup props background none is rendered 1`] = `
+/>
`;
exports[`EuiCollapsibleNavGroup props iconProps renders data-test-subj 1`] = `
@@ -77,9 +61,6 @@ exports[`EuiCollapsibleNavGroup props iconProps renders data-test-subj 1`] = `
-
`;
@@ -113,9 +94,6 @@ exports[`EuiCollapsibleNavGroup props iconSize is rendered 1`] = `
-
`;
@@ -149,9 +127,6 @@ exports[`EuiCollapsibleNavGroup props iconType is rendered 1`] = `
-
`;
@@ -178,9 +153,6 @@ exports[`EuiCollapsibleNavGroup props title is rendered 1`] = `
-
`;
@@ -207,9 +179,6 @@ exports[`EuiCollapsibleNavGroup props titleElement can change the rendered eleme
-
`;
@@ -217,22 +186,14 @@ exports[`EuiCollapsibleNavGroup props titleSize can be larger 1`] = `
+/>
`;
exports[`EuiCollapsibleNavGroup throws a warning if iconType is passed without a title 1`] = `
+/>
`;
exports[`EuiCollapsibleNavGroup when isCollapsible is true accepts accordion props 1`] = `
@@ -291,11 +252,7 @@ exports[`EuiCollapsibleNavGroup when isCollapsible is true accepts accordion pro
@@ -353,11 +310,7 @@ exports[`EuiCollapsibleNavGroup when isCollapsible is true will render an accord
diff --git a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx
index d6aa1e531b6..cb73399e720 100644
--- a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx
+++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx
@@ -146,7 +146,7 @@ export const EuiCollapsibleNavGroup: FunctionComponent{children}
);
diff --git a/src/components/flyout/__snapshots__/flyout.test.tsx.snap b/src/components/flyout/__snapshots__/flyout.test.tsx.snap
index 87e945ced75..eb4766a1a3b 100644
--- a/src/components/flyout/__snapshots__/flyout.test.tsx.snap
+++ b/src/components/flyout/__snapshots__/flyout.test.tsx.snap
@@ -2,471 +2,491 @@
exports[`EuiFlyout is rendered 1`] = `
Array [
-
,
-
,
- ,
-
,
]
`;
exports[`EuiFlyout props accepts div props 1`] = `
Array [
-
,
-
,
- ,
-
,
]
`;
-exports[`EuiFlyout props close button is not rendered 1`] = `
+exports[`EuiFlyout props closeButtonPosition can be outside 1`] = `
Array [
-
,
-
,
-
+
,
-
,
]
`;
-exports[`EuiFlyout props max width can be set to a custom number 1`] = `
+exports[`EuiFlyout props closeButtonProps 1`] = `
Array [
-
,
-
,
-
,
-
,
]
`;
-exports[`EuiFlyout props max width can be set to a custom value and measurement 1`] = `
+exports[`EuiFlyout props hideCloseButton 1`] = `
Array [
-
,
-
,
-
+
,
-
,
]
`;
-exports[`EuiFlyout props max width can be set to a default 1`] = `
+exports[`EuiFlyout props is rendered as nav 1`] = `
Array [
-
,
-
,
-
+
,
-
,
]
`;
-exports[`EuiFlyout props ownFocus can alter mask props with maskProps without throwing error 1`] = `
+exports[`EuiFlyout props maxWidth can be set to a custom number 1`] = `
Array [
-
,
-
,
-
,
-
,
-
,
]
`;
-exports[`EuiFlyout props ownFocus is rendered 1`] = `
+exports[`EuiFlyout props maxWidth can be set to a custom value and measurement 1`] = `
Array [
-
,
-
,
-
,
-
,
-
,
]
`;
-exports[`EuiFlyout props paddingSize l is rendered 1`] = `
+exports[`EuiFlyout props maxWidth can be set to a default 1`] = `
Array [
-
,
-
,
-
,
-
,
]
`;
-exports[`EuiFlyout props paddingSize m is rendered 1`] = `
+exports[`EuiFlyout props outsideClickCloses 1`] = `
Array [
-
,
-
,
-
,
-
,
]
`;
-exports[`EuiFlyout props paddingSize none is rendered 1`] = `
+exports[`EuiFlyout props ownFocus can alter mask props with maskProps without throwing error 1`] = `
Array [
-
,
-
,
-
+
,
-
,
]
`;
-exports[`EuiFlyout props paddingSize s is rendered 1`] = `
+exports[`EuiFlyout props ownFocus can be false 1`] = `
Array [
@@ -509,115 +529,524 @@ Array [
]
`;
+exports[`EuiFlyout props paddingSize l is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props paddingSize m is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props paddingSize none is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props paddingSize s is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props role can be removed 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props sides left is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props sides right is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props size accepts custom number 1`] = `
+Array [
+ ,
+]
+`;
+
exports[`EuiFlyout props size l is rendered 1`] = `
Array [
-
,
-
,
- ,
-
,
]
`;
exports[`EuiFlyout props size m is rendered 1`] = `
Array [
-
,
-
,
- ,
-
,
]
`;
exports[`EuiFlyout props size s is rendered 1`] = `
+Array [
+ ,
+]
+`;
+
+exports[`EuiFlyout props type=push is rendered 1`] = `
Array [
,
,
@@ -633,7 +1062,7 @@ Array [
,
]
`;
diff --git a/src/components/flyout/__snapshots__/flyout_body.test.tsx.snap b/src/components/flyout/__snapshots__/flyout_body.test.tsx.snap
index b781a84ac6e..609df0be80c 100644
--- a/src/components/flyout/__snapshots__/flyout_body.test.tsx.snap
+++ b/src/components/flyout/__snapshots__/flyout_body.test.tsx.snap
@@ -8,6 +8,7 @@ exports[`EuiFlyoutBody is rendered 1`] = `
>
({
EuiOverlayMask: ({ headerZindexLocation, ...props }: any) => (
@@ -29,7 +29,9 @@ jest.mock('../overlay_mask', () => ({
),
}));
-const SIZES: EuiFlyoutSize[] = ['s', 'm', 'l'];
+jest.mock('../portal', () => ({
+ EuiPortal: ({ children }: { children: any }) => children,
+}));
describe('EuiFlyout', () => {
test('is rendered', () => {
@@ -43,7 +45,15 @@ describe('EuiFlyout', () => {
});
describe('props', () => {
- test('close button is not rendered', () => {
+ test('role can be removed', () => {
+ const component = mount(
{}} role={null} />);
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+
+ test('hideCloseButton', () => {
const component = mount( {}} hideCloseButton />);
expect(
@@ -51,7 +61,27 @@ describe('EuiFlyout', () => {
).toMatchSnapshot();
});
- describe('closeButtonLabel', () => {
+ test('closeButtonProps', () => {
+ const component = mount(
+ {}} closeButtonProps={requiredProps} />
+ );
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+
+ test('closeButtonPosition can be outside', () => {
+ const component = mount(
+ {}} closeButtonPosition="outside" />
+ );
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+
+ describe('closeButtonAriaLabel', () => {
test('has a default label for the close button', () => {
const component = render( {}} />);
const label = component
@@ -82,6 +112,38 @@ describe('EuiFlyout', () => {
).toMatchSnapshot();
});
+ describe('sides', () => {
+ SIDES.forEach((side) => {
+ it(`${side} is rendered`, () => {
+ const component = mount( {}} side={side} />);
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('type=push', () => {
+ test('is rendered', () => {
+ const component = mount(
+ {}} type="push" pushMinBreakpoint="xs" />
+ );
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+ });
+
+ test('is rendered as nav', () => {
+ const component = mount( {}} as="nav" />);
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+
describe('size', () => {
SIZES.forEach((size) => {
it(`${size} is rendered`, () => {
@@ -92,6 +154,14 @@ describe('EuiFlyout', () => {
).toMatchSnapshot();
});
});
+
+ it('accepts custom number', () => {
+ const component = mount( {}} size={500} />);
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
});
describe('paddingSize', () => {
@@ -108,7 +178,7 @@ describe('EuiFlyout', () => {
});
});
- describe('max width', () => {
+ describe('maxWidth', () => {
test('can be set to a default', () => {
const component = mount(
{}} maxWidth={true} />
@@ -140,10 +210,20 @@ describe('EuiFlyout', () => {
});
});
+ test('outsideClickCloses', () => {
+ const component = mount(
+ {}} outsideClickCloses />
+ );
+
+ expect(
+ takeMountedSnapshot(component, { hasArrayOutput: true })
+ ).toMatchSnapshot();
+ });
+
describe('ownFocus', () => {
- test('is rendered', () => {
+ test('can be false', () => {
const component = mount(
- {}} ownFocus={true} />
+ {}} ownFocus={false} />
);
expect(
@@ -155,7 +235,6 @@ describe('EuiFlyout', () => {
const component = mount(
{}}
- ownFocus={true}
maskProps={{ headerZindexLocation: 'above' }}
/>
);
diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx
index da7cb5ee157..967f1faeb83 100644
--- a/src/components/flyout/flyout.tsx
+++ b/src/components/flyout/flyout.tsx
@@ -18,30 +18,69 @@
*/
import React, {
- FunctionComponent,
+ useEffect,
+ useState,
+ forwardRef,
CSSProperties,
Fragment,
- HTMLAttributes,
- useEffect,
+ ComponentType,
+ ComponentPropsWithRef,
+ PropsWithChildren,
+ MutableRefObject,
} from 'react';
import classnames from 'classnames';
-import { keys, EuiWindowEvent } from '../../services';
+import {
+ keys,
+ EuiWindowEvent,
+ useCombinedRefs,
+ EuiBreakpointSize,
+ isWithinMinBreakpoint,
+} from '../../services';
import { CommonProps, keysOf } from '../common';
import { EuiFocusTrap } from '../focus_trap';
import { EuiOverlayMask, EuiOverlayMaskProps } from '../overlay_mask';
-import { EuiButtonIcon } from '../button';
+import { EuiButtonIcon, EuiButtonIconPropsForButton } from '../button';
import { EuiI18n } from '../i18n';
+import { useResizeObserver } from '../observer/resize_observer';
+import { EuiOutsideClickDetector } from '../outside_click_detector';
+import { throttle } from '../color_picker/utils';
+import { EuiPortal } from '../portal';
+
+const typeToClassNameMap = {
+ push: 'euiFlyout--push',
+ overlay: null,
+};
+
+export const TYPES = keysOf(typeToClassNameMap);
+type _EuiFlyoutType = typeof TYPES[number];
-export type EuiFlyoutSize = 's' | 'm' | 'l';
+const sideToClassNameMap = {
+ left: 'euiFlyout--left',
+ right: null,
+};
-const sizeToClassNameMap: { [size in EuiFlyoutSize]: string } = {
+export const SIDES = keysOf(sideToClassNameMap);
+type _EuiFlyoutSide = typeof SIDES[number];
+
+const sizeToClassNameMap = {
s: 'euiFlyout--small',
m: 'euiFlyout--medium',
l: 'euiFlyout--large',
};
+export const SIZES = keysOf(sizeToClassNameMap);
+export type EuiFlyoutSize = typeof SIZES[number];
+
+/**
+ * Custom type checker for named flyout sizes since the prop
+ * `size` can also be CSSProperties['width'] (string | number)
+ */
+function isEuiFlyoutSizeNamed(value: any): value is EuiFlyoutSize {
+ return SIZES.includes(value as any);
+}
+
const paddingSizeToClassNameMap = {
none: 'euiFlyout--paddingNone',
s: 'euiFlyout--paddingSmall',
@@ -50,157 +89,329 @@ const paddingSizeToClassNameMap = {
};
export const PADDING_SIZES = keysOf(paddingSizeToClassNameMap);
+type _EuiFlyoutPaddingSize = typeof PADDING_SIZES[number];
-export type EuiFlyoutPaddingSize = typeof PADDING_SIZES[number];
-
-export interface EuiFlyoutProps
- extends CommonProps,
- HTMLAttributes {
+type _EuiFlyoutProps = {
onClose: () => void;
/**
- * Defines the width of the panel
+ * Defines the width of the panel.
+ * Pass a predefined size of `s | m | l`, or pass any number/string compatible with the CSS `width` attribute
*/
- size?: EuiFlyoutSize;
+ size?: EuiFlyoutSize | CSSProperties['width'];
/**
- * Customize the padding around the content of the flyout header, body and footer
+ * Sets the max-width of the panel,
+ * set to `true` to use the default size,
+ * set to `false` to not restrict the width,
+ * set to a number for a custom width in px,
+ * set to a string for a custom width in custom measurement.
*/
- paddingSize?: EuiFlyoutPaddingSize;
+ maxWidth?: boolean | number | string;
/**
- * Hides the default close button. You must provide another close button somewhere within the flyout.
+ * Customize the padding around the content of the flyout header, body and footer
*/
- hideCloseButton?: boolean;
+ paddingSize?: _EuiFlyoutPaddingSize;
/**
- * Adds an EuiOverlayMask when set to `true`
+ * Adds an EuiOverlayMask and wraps in an EuiPortal
*/
ownFocus?: boolean;
+ /**
+ * Hides the default close button. You must provide another close button somewhere within the flyout.
+ */
+ hideCloseButton?: boolean;
/**
* Specify an aria-label for the close button of the flyout.
* Default is `'Close this dialog'`.
*/
closeButtonAriaLabel?: string;
/**
- * Sets the max-width of the panel,
- * set to `true` to use the default size,
- * set to `false` to not restrict the width,
- * set to a number for a custom width in px,
- * set to a string for a custom width in custom measurement.
+ * Extends EuiButtonIconProps onto the close button
*/
- maxWidth?: boolean | number | string;
-
- style?: CSSProperties;
-
+ closeButtonProps?: Partial;
+ /**
+ * Position of close button.
+ * `inside`: Floating to just inside the flyout, always top right;
+ * `outside`: Floating just outside the flyout near the top (side dependent on `side`). Helpful when the close button may cover other interactable content.
+ */
+ closeButtonPosition?: 'inside' | 'outside';
/**
* Adjustments to the EuiOverlayMask that is added when `ownFocus = true`
*/
maskProps?: EuiOverlayMaskProps;
-}
+ /**
+ * How to display the the flyout in relation to the body content;
+ * `push` keeps it visible, pushing the `` content via padding
+ */
+ type?: _EuiFlyoutType;
+ /**
+ * Forces this interaction on the mask overlay or body content.
+ * Defaults depend on `ownFocus` and `type` values
+ */
+ outsideClickCloses?: boolean;
+ /**
+ * Which side of the window to attach to.
+ * The `left` option should only be used for navigation.
+ */
+ side?: _EuiFlyoutSide;
+ /**
+ * Defaults to `dialog` which is best for most cases of the flyout.
+ * Otherwise pass in your own, aria-role, or `null` to remove it and use the semantic `as` element instead
+ */
+ role?: null | string;
+ /**
+ * Named breakpoint or pixel value for customizing the minimum window width to enable the `push` type
+ */
+ pushMinBreakpoint?: EuiBreakpointSize | number;
+ style?: React.CSSProperties;
+};
-export const EuiFlyout: FunctionComponent = ({
- className,
- children,
- hideCloseButton = false,
- onClose,
- ownFocus = false,
- size = 'm',
- paddingSize = 'l',
- closeButtonAriaLabel,
- maxWidth = false,
- style,
- maskProps,
- ...rest
-}) => {
- const onKeyDown = (event: KeyboardEvent) => {
- if (event.key === keys.ESCAPE) {
- event.preventDefault();
- onClose();
- }
- };
+// Using ReactHTML rather than JSX.IntrinsicElements here because it does not include
+// SVG element types which cause errors because they do not have all the attributes needed.
+type ComponentTypes =
+ | 'div'
+ | 'span'
+ | 'nav'
+ | 'aside'
+ | 'section'
+ | 'article'
+ | 'header'
+ | ComponentType;
+
+export type EuiFlyoutProps = CommonProps &
+ ComponentPropsWithRef & {
+ /**
+ * Sets the HTML element for `EuiFlyout`
+ */
+ as?: T;
+ } & _EuiFlyoutProps;
+
+const EuiFlyout = forwardRef(
+ (
+ {
+ className,
+ children,
+ as: Element = 'div' as T,
+ hideCloseButton = false,
+ closeButtonProps,
+ closeButtonAriaLabel,
+ closeButtonPosition = 'inside',
+ onClose,
+ ownFocus = true,
+ side = 'right',
+ size = 'm',
+ paddingSize = 'l',
+ maxWidth = false,
+ style,
+ maskProps,
+ type = 'overlay',
+ outsideClickCloses = false,
+ role = 'dialog',
+ pushMinBreakpoint = 'l',
+ ...rest
+ }: PropsWithChildren>,
+ ref:
+ | ((instance: ComponentPropsWithRef | null) => void)
+ | MutableRefObject | null>
+ | null
+ ) => {
+ /**
+ * Setting the initial state of pushed based on the `type` prop
+ * and if the current window size is large enough (larger than `pushMinBreakpoint`)
+ */
+ const [windowIsLargeEnoughToPush, setWindowIsLargeEnoughToPush] = useState(
+ isWithinMinBreakpoint(
+ typeof window === 'undefined' ? 0 : window.innerWidth,
+ pushMinBreakpoint
+ )
+ );
+
+ const isPushed = type === 'push' && windowIsLargeEnoughToPush;
+
+ /**
+ * Watcher added to the window to maintain `isPushed` state depending on
+ * the window size compared to the `pushBreakpoint`
+ */
+ const functionToCallOnWindowResize = throttle(() => {
+ if (isWithinMinBreakpoint(window.innerWidth, pushMinBreakpoint)) {
+ setWindowIsLargeEnoughToPush(true);
+ } else {
+ setWindowIsLargeEnoughToPush(false);
+ }
+ // reacts every 50ms to resize changes and always gets the final update
+ }, 50);
+
+ /**
+ * Setting up the refs on the actual flyout element in order to
+ * accommodate for the `isPushed` state by adding padding to the body equal to the width of the element
+ */
+ const [resizeRef, setResizeRef] = useState | null>(
+ null
+ );
+ const setRef = useCombinedRefs([setResizeRef, ref]);
+ // TODO: Allow this hooke to be conditional
+ const dimensions = useResizeObserver(resizeRef as Element);
+
+ useEffect(() => {
+ // This class doesn't actually do anything by EUI, but is nice to add for consumers (JIC)
+ document.body.classList.add('euiBody--hasFlyout');
+
+ /**
+ * Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element
+ */
+ if (type === 'push') {
+ // Only add the event listener if we'll need to accommodate with padding
+ window.addEventListener('resize', functionToCallOnWindowResize);
+
+ if (isPushed) {
+ if (side === 'right') {
+ document.body.style.paddingRight = `${dimensions.width}px`;
+ } else if (side === 'left') {
+ document.body.style.paddingLeft = `${dimensions.width}px`;
+ }
+ }
+ }
+
+ return () => {
+ document.body.classList.remove('euiBody--hasFlyout');
+
+ if (type === 'push') {
+ window.removeEventListener('resize', functionToCallOnWindowResize);
- useEffect(() => {
- document.body.classList.add('euiBody--hasFlyout');
+ if (side === 'right') {
+ document.body.style.paddingRight = '';
+ } else if (side === 'left') {
+ document.body.style.paddingLeft = '';
+ }
+ }
+ };
+ }, [type, side, dimensions, isPushed, functionToCallOnWindowResize]);
- return () => {
- document.body.classList.remove('euiBody--hasFlyout');
+ /**
+ * ESC key closes flyout (always?)
+ */
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (!isPushed && event.key === keys.ESCAPE) {
+ event.preventDefault();
+ onClose();
+ }
};
- });
-
- let newStyle;
- let widthClassName;
- if (maxWidth === true) {
- widthClassName = 'euiFlyout--maxWidth-default';
- } else if (maxWidth !== false) {
- const value = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
- newStyle = { ...style, maxWidth: value };
- }
- const classes = classnames(
- 'euiFlyout',
- sizeToClassNameMap[size!],
- paddingSizeToClassNameMap[paddingSize],
- widthClassName,
- className
- );
-
- let closeButton;
- if (onClose && !hideCloseButton) {
- closeButton = (
-
- {(closeAriaLabel: string) => (
- {
- onClose();
- }}
- data-test-subj="euiFlyoutCloseButton"
- />
- )}
-
+ let newStyle;
+ let widthClassName;
+ let sizeClassName;
+
+ // Setting max-width
+ if (maxWidth === true) {
+ widthClassName = 'euiFlyout--maxWidth-default';
+ } else if (maxWidth !== false) {
+ const value = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth;
+ newStyle = { ...style, maxWidth: value };
+ }
+
+ // Setting size
+ if (isEuiFlyoutSizeNamed(size)) {
+ sizeClassName = sizeToClassNameMap[size];
+ } else if (newStyle) {
+ newStyle.width = size;
+ } else {
+ newStyle = { ...style, width: size };
+ }
+
+ const classes = classnames(
+ 'euiFlyout',
+ typeToClassNameMap[type as _EuiFlyoutType],
+ sideToClassNameMap[side as _EuiFlyoutSide],
+ sizeClassName,
+ paddingSizeToClassNameMap[paddingSize as _EuiFlyoutPaddingSize],
+ widthClassName,
+ className
);
- }
- const flyoutContent = (
-
- {closeButton}
- {children}
-
- );
-
- // If ownFocus is set, show an overlay behind the flyout and allow the user
- // to click it to close it.
- let optionalOverlay;
- if (ownFocus) {
- optionalOverlay = (
-
+ let closeButton;
+ if (onClose && !hideCloseButton) {
+ const closeButtonClasses = classnames(
+ 'euiFlyout__closeButton',
+ `euiFlyout__closeButton--${closeButtonPosition}`,
+ closeButtonProps?.className
+ );
+
+ closeButton = (
+
+ {(closeAriaLabel: string) => (
+ ) => {
+ onClose();
+ closeButtonProps?.onClick && closeButtonProps.onClick(e);
+ }}
+ />
+ )}
+
+ );
+ }
+
+ const flyoutContent = (
+ // @ts-expect-error JSX element without construct
+ )}
+ role={role}
+ className={classes}
+ tabIndex={-1}
+ style={newStyle || style}
+ ref={setRef}>
+ {closeButton}
+ {children}
+
);
- }
- return (
-
-
- {optionalOverlay}
- {/*
- * Trap focus even when `ownFocus={false}`, otherwise closing
- * the flyout won't return focus to the originating button.
- *
- * Set `clickOutsideDisables={true}` when `ownFocus={false}`
- * to allow non-keyboard users the ability to interact with
- * elements outside the flyout.
- */}
-
- {flyoutContent}
+ /*
+ * Trap focus even when `ownFocus={false}`, otherwise closing
+ * the flyout won't return focus to the originating button.
+ *
+ * Set `clickOutsideDisables={true}` when `ownFocus={false}`
+ * to allow non-keyboard users the ability to interact with
+ * elements outside the flyout.
+ */
+ let flyout = (
+
+ {/* Outside click detector is needed if theres no overlay mask to auto-close when clicking on elements outside */}
+ onClose()}>
+ {flyoutContent}
+
-
- );
-};
+ );
+
+ // If ownFocus is set, wrap with an overlay and allow the user to click it to close it.
+ if (ownFocus && !isPushed) {
+ flyout = (
+
+ {flyout}
+
+ );
+ } else if (!isPushed) {
+ // Otherwise still wrap within an EuiPortal so it appends (unless it is the push style)
+ flyout = {flyout} ;
+ }
+
+ return (
+
+
+ {flyout}
+
+ );
+ }
+);
+
+EuiFlyout.displayName = 'EuiFlyout';
+
+export { EuiFlyout };
diff --git a/src/components/flyout/flyout_body.tsx b/src/components/flyout/flyout_body.tsx
index 0aa354d0099..12e626521c1 100644
--- a/src/components/flyout/flyout_body.tsx
+++ b/src/components/flyout/flyout_body.tsx
@@ -44,7 +44,7 @@ export const EuiFlyoutBody: EuiFlyoutBodyProps = ({
return (
-
+
{banner &&
{banner}
}
{children}
diff --git a/src/components/overlay_mask/_overlay_mask.scss b/src/components/overlay_mask/_overlay_mask.scss
index 4f2fddc112e..2a61db1836a 100644
--- a/src/components/overlay_mask/_overlay_mask.scss
+++ b/src/components/overlay_mask/_overlay_mask.scss
@@ -29,5 +29,5 @@
}
.euiOverlayMask--belowHeader {
- z-index: $euiZHeader - 1;
+ z-index: $euiZMaskBelowHeader;
}
diff --git a/src/components/page/page_template.tsx b/src/components/page/page_template.tsx
index 428ae8008c8..4e0be823032 100644
--- a/src/components/page/page_template.tsx
+++ b/src/components/page/page_template.tsx
@@ -329,7 +329,7 @@ export const EuiPageTemplate: FunctionComponent
= ({
{pageSideBar}
- {/* The extra PageBody is to accomodate the bottom bar stretching to both sides */}
+ {/* The extra PageBody is to accommodate the bottom bar stretching to both sides */}
{pageHeader && (
diff --git a/src/global_styling/mixins/_header.scss b/src/global_styling/mixins/_header.scss
index fc7d220f4f7..7e2a616f45b 100644
--- a/src/global_styling/mixins/_header.scss
+++ b/src/global_styling/mixins/_header.scss
@@ -19,5 +19,9 @@
top: #{$headerHeight};
}
}
+
+ .euiOverlayMask--belowHeader {
+ top: #{$headerHeight};
+ }
}
}
diff --git a/src/global_styling/mixins/_helpers.scss b/src/global_styling/mixins/_helpers.scss
index 5f3e21c35a5..81b6970e636 100644
--- a/src/global_styling/mixins/_helpers.scss
+++ b/src/global_styling/mixins/_helpers.scss
@@ -52,6 +52,7 @@
* 1. Focus rings shouldn't be visible on scrollable regions, but a11y requires them to be focusable.
* Browser's supporting `:focus-visible` will still show outline on keyboard focus only.
* Others like Safari, won't show anything at all.
+ * 2. Force the `:focus-visible` when the `tabindex=0` (is tabbable)
*/
// Just overflow and scrollbars
@@ -64,6 +65,10 @@
&:focus {
outline: none; /* 1 */
}
+
+ &[tabindex='0']:focus:focus-visible {
+ outline-style: auto; /* 2 */
+ }
}
@mixin euiXScroll {
@@ -73,6 +78,10 @@
&:focus {
outline: none; /* 1 */
}
+
+ &[tabindex='0']:focus:focus-visible {
+ outline-style: auto; /* 2 */
+ }
}
// The full overflow with shadow
diff --git a/src/global_styling/variables/_z_index.scss b/src/global_styling/variables/_z_index.scss
index bae45e1d638..2448a34c61a 100644
--- a/src/global_styling/variables/_z_index.scss
+++ b/src/global_styling/variables/_z_index.scss
@@ -12,23 +12,23 @@
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
+$euiZLevel0: 0;
+$euiZLevel1: 1000;
+$euiZLevel2: 2000;
+$euiZLevel3: 3000;
+$euiZLevel4: 4000;
+$euiZLevel5: 5000;
+$euiZLevel6: 6000;
+$euiZLevel7: 7000;
+$euiZLevel8: 8000;
+$euiZLevel9: 9000;
-$euiZLevel0: 0;
-$euiZLevel1: 1000;
-$euiZLevel2: 2000;
-$euiZLevel3: 3000;
-$euiZLevel4: 4000;
-$euiZLevel5: 5000;
-$euiZLevel6: 6000;
-$euiZLevel7: 7000;
-$euiZLevel8: 8000;
-$euiZLevel9: 9000;
-
-$euiZContent: $euiZLevel0;
-$euiZHeader: $euiZLevel1;
-$euiZContentMenu: $euiZLevel2;
-$euiZFlyout: $euiZLevel3;
-$euiZNavigation: $euiZLevel4;
-$euiZMask: $euiZLevel6;
-$euiZModal: $euiZLevel8;
-$euiZToastList: $euiZLevel9;
+$euiZToastList: $euiZLevel9;
+$euiZModal: $euiZLevel8;
+$euiZMask: $euiZLevel6;
+$euiZNavigation: $euiZLevel6;
+$euiZContentMenu: $euiZLevel2;
+$euiZHeader: $euiZLevel1;
+$euiZFlyout: $euiZHeader;
+$euiZMaskBelowHeader: $euiZHeader;
+$euiZContent: $euiZLevel0;
diff --git a/src/services/breakpoint.ts b/src/services/breakpoint.ts
index cbb73726f82..df3949cf43b 100644
--- a/src/services/breakpoint.ts
+++ b/src/services/breakpoint.ts
@@ -80,6 +80,31 @@ export function isWithinMaxBreakpoint(
}
}
+/**
+ * Given the current `width` and a max breakpoint key,
+ * this function returns true or false if the `width` falls within the max
+ * breakpoint or any breakpoints below
+ *
+ * @param {number} width Can either be the full window width or any width
+ * @param {EuiBreakpointSize | number} min The named breakpoint or custom number to check against
+ * @param {EuiBreakpoints} breakpoints An object with keys for sizing and values for minimum width
+ * @returns {boolean} Will return `false` if it can't find a value for the `min` breakpoint
+ */
+export function isWithinMinBreakpoint(
+ width: number,
+ min: EuiBreakpointSize | number,
+ breakpoints: EuiBreakpoints = BREAKPOINTS
+): boolean {
+ if (typeof min === 'number') {
+ return width >= min;
+ } else {
+ const currentBreakpoint = getBreakpoint(width, breakpoints);
+ return currentBreakpoint
+ ? breakpoints[currentBreakpoint] >= breakpoints[min]
+ : false;
+ }
+}
+
/**
* Given the current `width` and an array of breakpoint keys,
* this function returns true or false if the `width` falls within
diff --git a/src/services/index.ts b/src/services/index.ts
index 9b7e56f545d..66908fddd4e 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -41,6 +41,8 @@ export {
getBreakpoint,
isWithinBreakpoints,
isWithinMaxBreakpoint,
+ isWithinMinBreakpoint,
+ EuiBreakpointSize,
} from './breakpoint';
export {