diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 41a0c6088b6..f0452b69d56 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'; @@ -323,6 +325,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`] = ` +
+
+
+
+ +
+
+
+`; + +exports[`EuiCollapsibleNav is rendered 1`] = ` +Array [ +
, +
+
+
+
+ +
+
+
, +] +`; 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} */} + + + + + ); +}; 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 78214b35586..0bfe6522a04 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 57f885deb08..cf65eba046d 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';