diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js
index 7cff19e5a28..377a0806a94 100644
--- a/src-docs/src/routes.js
+++ b/src-docs/src/routes.js
@@ -63,6 +63,8 @@ import { CodeEditorExample } from './views/code_editor/code_editor_example';
import { CodeExample } from './views/code/code_example';
+import { CollapsibleNavExample } from './views/collapsible_nav/collapsible_nav_example';
+
import { ColorPickerExample } from './views/color_picker/color_picker_example';
import { ComboBoxExample } from './views/combo_box/combo_box_example';
@@ -321,6 +323,7 @@ const navigation = [
items: [
BreadcrumbsExample,
ButtonExample,
+ CollapsibleNavExample,
ContextMenuExample,
ControlBarExample,
FacetExample,
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
new file mode 100644
index 00000000000..287eae6e58e
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+
+import { EuiCollapsibleNav } from '../../../../src/components/collapsible_nav';
+import { EuiButton, EuiButtonToggle } from '../../../../src/components/button';
+import { EuiTitle } from '../../../../src/components/title';
+import { EuiSpacer } from '../../../../src/components/spacer';
+
+export default () => {
+ const [navIsOpen, setNavIsOpen] = useState(false);
+ const [navIsDocked, setNavIsDocked] = useState(false);
+
+ return (
+ <>
+ setNavIsOpen(!navIsOpen)}>Toggle nav
+ {navIsOpen && (
+ setNavIsOpen(false)}>
+
+
+ I am some nav
+
+
+ {
+ setNavIsDocked(!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
new file mode 100644
index 00000000000..12a225742cd
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import { Link } from 'react-router';
+
+import { renderToHtml } from '../../services';
+
+import { GuideSectionTypes } from '../../components';
+
+import {
+ EuiCode,
+ EuiCollapsibleNav,
+ EuiText,
+ EuiSpacer,
+ EuiCallOut,
+} from '../../../../src/components';
+
+import CollapsibleNav from './collapsible_nav';
+const collapsibleNavSource = require('!!raw-loader!./collapsible_nav');
+const collapsibleNavHtml = renderToHtml(CollapsibleNav);
+
+export const CollapsibleNavExample = {
+ title: 'Collapsible nav',
+ intro: (
+
+
+ This is a high level component that creates a flyout-style navigational
+ pane. It is the next evolution of{' '}
+
+ EuiNavDrawer
+ {' '}
+ which will be deprecated in the coming months.
+
+
+
+ ),
+ sections: [
+ {
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: collapsibleNavSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: collapsibleNavHtml,
+ },
+ ],
+ text: (
+ <>
+
+ EuiCollapsibleNav is a similar implementation to{' '}
+
+ EuiFlyout
+
+ ; the visibility of which must 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.
+
+
+ >
+ ),
+ props: { EuiCollapsibleNav },
+ demo: ,
+ },
+ ],
+};
diff --git a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
new file mode 100644
index 00000000000..1af87fb7989
--- /dev/null
+++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCollapsibleNav can be docked 1`] = `
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`EuiCollapsibleNav is rendered 1`] = `
+Array [
+
,
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
,
+]
+`;
diff --git a/src/components/collapsible_nav/_collapsible_nav.scss b/src/components/collapsible_nav/_collapsible_nav.scss
new file mode 100644
index 00000000000..7d11ac4fff7
--- /dev/null
+++ b/src/components/collapsible_nav/_collapsible_nav.scss
@@ -0,0 +1,52 @@
+// Extends euiFlyout
+@use '../flyout/flyout';
+
+.euiCollapsibleNav {
+ @extend %eui-flyout;
+ right: auto;
+ left: 0;
+ width: $euiCollapsibleNavWidth;
+ max-width: 80vw;
+
+ &:not(.euiCollapsibleNav--isDocked) {
+ animation: euiCollapsibleNavIn $euiAnimSpeedNormal $euiAnimSlightResistance;
+ }
+}
+
+.euiCollapsibleNav__closeButton {
+ position: absolute;
+ right: 0;
+ top: $euiSize;
+ margin-right: -25%;
+}
+
+@include euiBreakpoint('l', 'xl') {
+ // The addition of this class is handled through JS as well
+ // but adding under the breakpoint mixin is an additional fail-safe
+ .euiCollapsibleNav.euiCollapsibleNav--isDocked {
+ @include euiBottomShadowMedium;
+
+ .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;
+ }
+}
+
+// Specific keyframes so in comes in from the left
+@keyframes euiCollapsibleNavIn {
+ 0% {
+ opacity: 0;
+ transform: translateX(-100%);
+ }
+
+ 75% {
+ opacity: 1;
+ transform: translateX(0%);
+ }
+}
diff --git a/src/components/collapsible_nav/_index.scss b/src/components/collapsible_nav/_index.scss
new file mode 100644
index 00000000000..53427be8960
--- /dev/null
+++ b/src/components/collapsible_nav/_index.scss
@@ -0,0 +1,2 @@
+@import 'variables';
+@import 'collapsible_nav';
diff --git a/src/components/collapsible_nav/_variables.scss b/src/components/collapsible_nav/_variables.scss
new file mode 100644
index 00000000000..2378031a40f
--- /dev/null
+++ b/src/components/collapsible_nav/_variables.scss
@@ -0,0 +1,2 @@
+// Sizing
+$euiCollapsibleNavWidth: $euiSize * 20; // ~ 320px
diff --git a/src/components/collapsible_nav/collapsible_nav.test.tsx b/src/components/collapsible_nav/collapsible_nav.test.tsx
new file mode 100644
index 00000000000..38dd806e1fe
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav.test.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test/required_props';
+
+import { EuiCollapsibleNav } from './collapsible_nav';
+
+jest.mock('../overlay_mask', () => ({
+ EuiOverlayMask: (props: any) =>
,
+}));
+
+describe('EuiCollapsibleNav', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}} {...requiredProps} />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('can be docked', () => {
+ const component = render(
+ {}} />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/collapsible_nav/collapsible_nav.tsx b/src/components/collapsible_nav/collapsible_nav.tsx
new file mode 100644
index 00000000000..3ea6117be18
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav.tsx
@@ -0,0 +1,104 @@
+import React, {
+ FunctionComponent,
+ ReactNode,
+ useEffect,
+ useState,
+ Fragment,
+ HTMLAttributes,
+} from 'react';
+import classNames from 'classnames';
+import { throttle } from '../color_picker/utils';
+import { EuiWindowEvent, keyCodes } from '../../services';
+import { EuiFocusTrap } from '../focus_trap';
+import { EuiOverlayMask } from '../overlay_mask';
+import { CommonProps } from '../common';
+import { EuiButtonEmpty } from '../button';
+
+export type EuiCollapsibleNavProps = CommonProps &
+ HTMLAttributes & {
+ children?: ReactNode;
+ /**
+ * Keep navigation flyout visible and push `` content via padding
+ */
+ docked?: boolean;
+ onClose: () => void;
+ };
+
+export const EuiCollapsibleNav: FunctionComponent = ({
+ children,
+ className,
+ docked = false,
+ onClose,
+ ...rest
+}) => {
+ const [windowIsLargeEnoughToDock, setWindowIsLargeEnoughToDock] = useState(
+ window.innerWidth >= 992
+ );
+ const isDocked = docked && windowIsLargeEnoughToDock;
+
+ const functionToCallOnWindowResize = throttle(() => {
+ if (window.innerWidth < 992) {
+ setWindowIsLargeEnoughToDock(false);
+ } else {
+ setWindowIsLargeEnoughToDock(true);
+ }
+ // 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(() => {
+ if (docked) {
+ document.body.classList.add('euiBody--collapsibleNavIsDocked');
+ window.addEventListener('resize', functionToCallOnWindowResize);
+ }
+ return () => {
+ document.body.classList.remove('euiBody--collapsibleNavIsDocked');
+ window.removeEventListener('resize', functionToCallOnWindowResize);
+ };
+ }, [docked, functionToCallOnWindowResize]);
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.keyCode === keyCodes.ESCAPE) {
+ event.preventDefault();
+ collapse();
+ }
+ };
+
+ const collapse = () => {
+ if (!isDocked) {
+ onClose();
+ }
+ };
+
+ const classes = classNames(
+ 'euiCollapsibleNav',
+ { 'euiCollapsibleNav--isDocked': isDocked },
+ className
+ );
+
+ let optionalOverlay;
+ if (!isDocked) {
+ optionalOverlay = ;
+ }
+
+ return (
+
+
+ {optionalOverlay}
+ {/* Trap focus only when isDocked={false} */}
+
+
+ {children}
+
+
+ close
+
+
+
+
+ );
+};
diff --git a/src/components/collapsible_nav/index.ts b/src/components/collapsible_nav/index.ts
new file mode 100644
index 00000000000..4954e5ade02
--- /dev/null
+++ b/src/components/collapsible_nav/index.ts
@@ -0,0 +1 @@
+export { EuiCollapsibleNav } from './collapsible_nav';
diff --git a/src/components/flyout/_flyout.scss b/src/components/flyout/_flyout.scss
index def8aec630b..2333723e298 100644
--- a/src/components/flyout/_flyout.scss
+++ b/src/components/flyout/_flyout.scss
@@ -1,4 +1,4 @@
-.euiFlyout {
+%eui-flyout {
border-left: $euiBorderThin;
// The mixin augments the above
// sass-lint:disable mixins-before-declarations
@@ -10,12 +10,16 @@
height: 100%;
z-index: $euiZModal;
background: $euiColorEmptyShade;
- animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance;
display: flex;
flex-direction: column;
align-items: stretch;
}
+.euiFlyout {
+ @extend %eui-flyout;
+ animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance;
+}
+
// The actual size of the X button in pixels is a bit fuzzy because of all the
// button padding so there is some pixel pushing here.
.euiFlyout__closeButton {
diff --git a/src/components/index.js b/src/components/index.js
index 9efdf29620a..c3a9351a285 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -37,6 +37,8 @@ export { EuiCode, EuiCodeBlock, EuiCodeBlockImpl } from './code';
export { EuiCodeEditor } from './code_editor';
+export { EuiCollapsibleNav } from './collapsible_nav';
+
export {
EuiColorPicker,
EuiColorPickerSwatch,
diff --git a/src/components/index.scss b/src/components/index.scss
index cc4f1615102..dce5687119d 100644
--- a/src/components/index.scss
+++ b/src/components/index.scss
@@ -14,6 +14,7 @@
@import 'card/index';
@import 'code/index';
@import 'code_editor/index';
+@import 'collapsible_nav/index';
@import 'color_picker/index';
@import 'combo_box/index';
@import 'context_menu/index';