From 585b9dd3d81753836b76db0596759b1b68821cf9 Mon Sep 17 00:00:00 2001
From: Caroline Horn <549577+cchaos@users.noreply.github.com>
Date: Thu, 26 Mar 2020 20:44:30 -0400
Subject: [PATCH] [Feature Branch] EuiCollapsibleNav (#3019)
* [Feature] Added `EuiCollapsibleNav` component (#2977)
* [New Nav Feature] Added `ghost` colored EuiListGroupItem (#3018)
* [New Nav Feature] Created `EuiCollapsibleGroup` (#3031)
* [New Nav Feature] EuiPinnableListGroup (#3061)
* [K8 Nav Feature] Added `home` and `menu` glyphs to EuiIcon (#3109)
* [New Nav Feature] Final docs examples and patterns (#3117)
* [New Nav Feature] Move collapsible nav toggle button to be part of EuiCollapsibleNav (#3168)
---
CHANGELOG.md | 19 +-
scripts/a11y-testing.js | 1 +
src-docs/src/components/guide_components.scss | 12 +
src-docs/src/routes.js | 3 +
.../src/services/full_screen/full_screen.tsx | 43 +++
.../views/collapsible_nav/collapsible_nav.tsx | 50 +++
.../collapsible_nav/collapsible_nav_all.tsx | 281 +++++++++++++++
.../collapsible_nav_example.js | 214 ++++++++++++
.../collapsible_nav/collapsible_nav_group.tsx | 55 +++
.../collapsible_nav/collapsible_nav_list.tsx | 128 +++++++
src-docs/src/views/icon/icons.js | 2 +
.../views/list_group/list_group_example.js | 62 +++-
.../list_group/list_group_item_color.tsx | 31 +-
.../list_group/list_group_link_actions.js | 8 +-
.../views/list_group/pinnable_list_group.tsx | 57 ++++
src-docs/src/views/list_group/props.tsx | 7 +
.../__snapshots__/accordion.test.tsx.snap | 34 +-
src/components/accordion/_accordion.scss | 9 +-
src/components/accordion/accordion.tsx | 8 +-
.../collapsible_nav.test.tsx.snap | 122 +++++++
.../collapsible_nav/_collapsible_nav.scss | 57 ++++
src/components/collapsible_nav/_index.scss | 4 +
.../collapsible_nav/_variables.scss | 14 +
.../collapsible_nav/collapsible_nav.test.tsx | 59 ++++
.../collapsible_nav/collapsible_nav.tsx | 148 ++++++++
.../collapsible_nav_group.test.tsx.snap | 259 ++++++++++++++
.../_collapsible_nav_group.scss | 59 ++++
.../collapsible_nav_group/_index.scss | 1 +
.../collapsible_nav_group.test.tsx | 117 +++++++
.../collapsible_nav_group.tsx | 172 ++++++++++
.../collapsible_nav_group/index.ts | 4 +
src/components/collapsible_nav/index.ts | 6 +
src/components/flyout/_flyout.scss | 16 +-
src/components/header/_header.scss | 2 +-
src/components/header/_variables.scss | 6 +-
.../horizontal_rule/_horizontal_rule.scss | 6 +-
.../icon/__snapshots__/icon.test.tsx.snap | 35 ++
src/components/icon/assets/home.js | 16 +
src/components/icon/assets/home.svg | 3 +
src/components/icon/assets/menu.js | 17 +
src/components/icon/assets/menu.svg | 3 +
src/components/icon/icon.tsx | 2 +
src/components/index.js | 8 +-
src/components/index.scss | 1 +
.../__snapshots__/list_group.test.tsx.snap | 141 +++++++-
.../list_group_item.test.tsx.snap | 17 +
src/components/list_group/_index.scss | 1 +
.../list_group/_list_group_item.scss | 61 +++-
src/components/list_group/_variables.scss | 12 +-
src/components/list_group/index.ts | 5 +
src/components/list_group/list_group.test.tsx | 28 +-
src/components/list_group/list_group.tsx | 18 +-
src/components/list_group/list_group_item.tsx | 7 +-
.../pinnable_list_group.test.tsx.snap | 321 ++++++++++++++++++
.../pinnable_list_group/_index.scss | 1 +
.../_pinnable_list_group.scss | 9 +
.../list_group/pinnable_list_group/index.ts | 5 +
.../pinnable_list_group.test.tsx | 76 +++++
.../pinnable_list_group.tsx | 129 +++++++
.../__snapshots__/nav_drawer.test.js.snap | 16 +-
src/global_styling/mixins/_helpers.scss | 19 +-
src/global_styling/utility/_animations.scss | 4 +-
src/global_styling/utility/_utility.scss | 19 +-
src/global_styling/variables/_states.scss | 2 +
64 files changed, 2966 insertions(+), 86 deletions(-)
create mode 100644 src-docs/src/services/full_screen/full_screen.tsx
create mode 100644 src-docs/src/views/collapsible_nav/collapsible_nav.tsx
create mode 100644 src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
create mode 100644 src-docs/src/views/collapsible_nav/collapsible_nav_example.js
create mode 100644 src-docs/src/views/collapsible_nav/collapsible_nav_group.tsx
create mode 100644 src-docs/src/views/collapsible_nav/collapsible_nav_list.tsx
create mode 100644 src-docs/src/views/list_group/pinnable_list_group.tsx
create mode 100644 src-docs/src/views/list_group/props.tsx
create mode 100644 src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
create mode 100644 src/components/collapsible_nav/_collapsible_nav.scss
create mode 100644 src/components/collapsible_nav/_index.scss
create mode 100644 src/components/collapsible_nav/_variables.scss
create mode 100644 src/components/collapsible_nav/collapsible_nav.test.tsx
create mode 100644 src/components/collapsible_nav/collapsible_nav.tsx
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/_collapsible_nav_group.scss
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/_index.scss
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.test.tsx
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx
create mode 100644 src/components/collapsible_nav/collapsible_nav_group/index.ts
create mode 100644 src/components/collapsible_nav/index.ts
create mode 100644 src/components/icon/assets/home.js
create mode 100644 src/components/icon/assets/home.svg
create mode 100644 src/components/icon/assets/menu.js
create mode 100755 src/components/icon/assets/menu.svg
create mode 100644 src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap
create mode 100644 src/components/list_group/pinnable_list_group/_index.scss
create mode 100644 src/components/list_group/pinnable_list_group/_pinnable_list_group.scss
create mode 100644 src/components/list_group/pinnable_list_group/index.ts
create mode 100644 src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx
create mode 100644 src/components/list_group/pinnable_list_group/pinnable_list_group.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c38227a9a5e..0b7872a70d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+### Feature: EuiCollapsibleNav ([#3019](https://github.com/elastic/eui/pull/3019))
+
+- Added `EuiCollapsibleNav` and `EuiCollapsibleNavGroup` components
+- Added `EuiPinnableListGroup`, an extension of `EuiListGroup`
+- Added `ghost` colored `EuiListGroupItem`, increased overall large size, and fixed focus states
+- Added `color` and `size` props to `EuiListGroup`
+- Added `home` and `menu` glyphs to `EuiIcon`
+- Added simple `euiXScroll` and `euiYScroll` SASS mixins and CSS utility equivelants
+
+**Bug Fixes**
+
+- Fixed `EuiAccordion` icon margins, focus state, and flex issue in IE
+- Fixed `1.1px` height of `EuiHorizontalRule`
+
+### Extraneous to feature
+
- Improved `EuiModal` close button position to prevent from overlapping with the title ([#3176](https://github.com/elastic/eui/pull/3176))
**Bug Fixes**
@@ -7,12 +23,13 @@
- Fixed EuiBasicTable proptypes of itemId ([#3133](https://github.com/elastic/eui/pull/3133))
- Updated `EuiSuperDatePicker` to inherit the selected value in quick select ([#3105](https://github.com/elastic/eui/pull/3105))
+
## [`22.1.0`](https://github.com/elastic/eui/tree/v22.1.0)
- Added `delimiter` prop to `EuiComboBox` ([#3104](https://github.com/elastic/eui/pull/3104))
- Added `useColorPickerState` and `useColorStopsState` utilities ([#3067](https://github.com/elastic/eui/pull/3067))
- Fixed `EuiSearchBar` related types ([#3147](https://github.com/elastic/eui/pull/3147))
-- Added `prepend` and `append` ability to `EuiSuperSelect` ([#3167](https://github.com/elastic/eui/pull/3167))
+- Added `prepend` and `append` ability to `EuiSuperSelect` ([#3167](https://github.com/elastic/eui/pull/3167))
## [`22.0.0`](https://github.com/elastic/eui/tree/v22.0.0)
diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js
index 19532560db8..e9ed65cf31c 100644
--- a/scripts/a11y-testing.js
+++ b/scripts/a11y-testing.js
@@ -19,6 +19,7 @@ const docsPages = async (root, page) => {
`${root}#/layout/spacer`,
`${root}#/navigation/breadcrumbs`,
`${root}#/navigation/context-menu`,
+ `${root}#/navigation/collapsible-nav`,
`${root}#/navigation/control-bar`,
`${root}#/navigation/facet`,
`${root}#/navigation/link`,
diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss
index a37483cf9bc..249c88f7ff0 100644
--- a/src-docs/src/components/guide_components.scss
+++ b/src-docs/src/components/guide_components.scss
@@ -5,6 +5,10 @@ $guideZLevelHighest: $euiZLevel9 + 1000;
.guideBody {
background: linear-gradient(90deg, $euiPageBackgroundColor 50%, $euiColorEmptyShade 50%);
+
+ &--overflowHidden {
+ overflow: hidden;
+ }
}
.guidePage {
@@ -182,6 +186,14 @@ $guideZLevelHighest: $euiZLevel9 + 1000;
height: 1px;
}
+.guideFullScreenOverlay {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+}
+
@import '../views/guidelines/index';
@import 'guide_section/index';
@import 'guide_rule/index';
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/services/full_screen/full_screen.tsx b/src-docs/src/services/full_screen/full_screen.tsx
new file mode 100644
index 00000000000..716e0e93c1a
--- /dev/null
+++ b/src-docs/src/services/full_screen/full_screen.tsx
@@ -0,0 +1,43 @@
+import React, {
+ useState,
+ Fragment,
+ FunctionComponent,
+ ReactElement,
+ ReactNode,
+ useEffect,
+} from 'react';
+
+import { EuiFocusTrap } from '../../../../src/components/focus_trap';
+import { EuiButton } from '../../../../src/components/button';
+
+export const GuideFullScreen: FunctionComponent<{
+ children: (setFullScreen: (isFullScreen: boolean) => void) => ReactElement;
+ buttonText?: ReactNode;
+ isFullScreen?: boolean;
+}> = ({
+ children,
+ isFullScreen = false,
+ buttonText = 'Show fullscreen demo',
+}) => {
+ const [fullScreen, setFullScreen] = useState(isFullScreen);
+
+ // Watch for fullScreen status and appropriately add/remove body classes for hiding scroll
+ useEffect(() => {
+ if (fullScreen) {
+ document.body.classList.add('guideBody--overflowHidden');
+ }
+ return () => {
+ document.body.classList.remove('guideBody--overflowHidden');
+ };
+ }, [fullScreen]);
+
+ return (
+
+ setFullScreen(true)} iconType="fullScreen">
+ {buttonText}
+
+
+ {fullScreen && {children(setFullScreen)} }
+
+ );
+};
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..1846d214457
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx
@@ -0,0 +1,50 @@
+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';
+import { EuiText } from '../../../../src/components/text';
+import { EuiCode } from '../../../../src/components/code';
+
+export default () => {
+ const [navIsOpen, setNavIsOpen] = useState(false);
+ const [navIsDocked, setNavIsDocked] = useState(false);
+
+ return (
+ <>
+ setNavIsOpen(!navIsOpen)}>
+ Toggle nav
+
+ }
+ isDocked={navIsDocked}
+ onClose={() => setNavIsOpen(false)}>
+
+
+ I am some nav
+
+
+ {
+ setNavIsDocked(!navIsDocked);
+ }}
+ />
+
+
+
+ {navIsDocked && (
+
+
+ The button gets hidden by default when nav is docked unless you set{' '}
+ hideButtonIfDocked = false .
+
+
+ )}
+ >
+ );
+};
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
new file mode 100644
index 00000000000..4f663f4830f
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx
@@ -0,0 +1,281 @@
+import React, { useState } from 'react';
+import _ from 'lodash';
+
+import {
+ EuiCollapsibleNav,
+ EuiCollapsibleNavGroup,
+} from '../../../../src/components/collapsible_nav';
+import {
+ EuiHeaderSectionItemButton,
+ EuiHeaderLogo,
+ EuiHeader,
+} from '../../../../src/components/header';
+import { EuiIcon } from '../../../../src/components/icon';
+import { EuiButtonEmpty } from '../../../../src/components/button';
+import { EuiPage } from '../../../../src/components/page';
+import {
+ EuiPinnableListGroup,
+ EuiListGroupItem,
+ EuiPinnableListGroupItemProps,
+} from '../../../../src/components/list_group';
+import { EuiFlexItem } from '../../../../src/components/flex';
+import { EuiHorizontalRule } from '../../../../src/components/horizontal_rule';
+import { GuideFullScreen } from '../../services/full_screen/full_screen';
+
+import {
+ DeploymentsGroup,
+ KibanaNavLinks,
+ SecurityGroup,
+} from './collapsible_nav_list';
+import { EuiShowFor } from '../../../../src/components/responsive';
+
+const TopLinks: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Home',
+ iconType: 'home',
+ isActive: true,
+ 'aria-current': true,
+ href: '#/navigation/collapsible-nav',
+ pinnable: false,
+ },
+];
+const KibanaLinks: EuiPinnableListGroupItemProps[] = KibanaNavLinks.map(
+ link => {
+ return {
+ ...link,
+ href: '#/navigation/collapsible-nav',
+ };
+ }
+);
+const LearnLinks: EuiPinnableListGroupItemProps[] = [
+ { label: 'Docs', href: '#/navigation/collapsible-nav' },
+ { label: 'Blogs', href: '#/navigation/collapsible-nav' },
+ { label: 'Webinars', href: '#/navigation/collapsible-nav' },
+ { label: 'Elastic.co', href: 'https://elastic.co' },
+];
+
+export default () => {
+ const [navIsOpen, setNavIsOpen] = useState(
+ JSON.parse(String(localStorage.getItem('navIsDocked'))) || false
+ );
+ const [navIsDocked, setNavIsDocked] = useState(
+ JSON.parse(String(localStorage.getItem('navIsDocked'))) || false
+ );
+
+ /**
+ * Accordion toggling
+ */
+ const [openGroups, setOpenGroups] = useState(
+ JSON.parse(String(localStorage.getItem('openNavGroups'))) || [
+ 'Kibana',
+ 'Learn',
+ ]
+ );
+
+ // Save which groups are open and which are not with state and local store
+ const toggleAccordion = (isOpen: boolean, title?: string) => {
+ if (!title) return;
+ const itExists = openGroups.includes(title);
+ if (isOpen) {
+ if (itExists) return;
+ openGroups.push(title);
+ } else {
+ const index = openGroups.indexOf(title);
+ if (index > -1) {
+ openGroups.splice(index, 1);
+ }
+ }
+ setOpenGroups([...openGroups]);
+ localStorage.setItem('openNavGroups', JSON.stringify(openGroups));
+ };
+
+ /**
+ * Pinning
+ */
+ const [pinnedItems, setPinnedItems] = useState<
+ EuiPinnableListGroupItemProps[]
+ >(JSON.parse(String(localStorage.getItem('pinnedItems'))) || []);
+
+ const addPin = (item: any) => {
+ if (!item || _.find(pinnedItems, { label: item.label })) {
+ return;
+ }
+ item.pinned = true;
+ const newPinnedItems = pinnedItems ? pinnedItems.concat(item) : [item];
+ setPinnedItems(newPinnedItems);
+ localStorage.setItem('pinnedItems', JSON.stringify(newPinnedItems));
+ };
+
+ const removePin = (item: any) => {
+ const pinIndex = _.findIndex(pinnedItems, { label: item.label });
+ if (pinIndex > -1) {
+ item.pinned = false;
+ const newPinnedItems = pinnedItems;
+ newPinnedItems.splice(pinIndex, 1);
+ setPinnedItems([...newPinnedItems]);
+ localStorage.setItem('pinnedItems', JSON.stringify(newPinnedItems));
+ }
+ };
+
+ function alterLinksWithCurrentState(
+ links: EuiPinnableListGroupItemProps[],
+ showPinned = false
+ ): EuiPinnableListGroupItemProps[] {
+ return links.map(link => {
+ const { pinned, ...rest } = link;
+ return {
+ pinned: showPinned ? pinned : false,
+ ...rest,
+ };
+ });
+ }
+
+ function addLinkNameToPinTitle(listItem: EuiPinnableListGroupItemProps) {
+ return `Pin ${listItem.label} to top`;
+ }
+
+ function addLinkNameToUnpinTitle(listItem: EuiPinnableListGroupItemProps) {
+ return `Unpin ${listItem.label}`;
+ }
+
+ const collapsibleNav = (
+ setNavIsOpen(!navIsOpen)}>
+
+
+ }
+ onClose={() => setNavIsOpen(false)}>
+ {/* Dark deployments section */}
+
+ {DeploymentsGroup}
+
+
+ {/* Shaded pinned section always with a home item */}
+
+
+
+
+
+
+
+
+ {/* BOTTOM */}
+
+ {/* Kibana section */}
+ toggleAccordion(isOpen, 'Kibana')}>
+
+
+
+ {/* Security callout */}
+ {SecurityGroup}
+
+ {/* Learn section */}
+ toggleAccordion(isOpen, 'Learn')}>
+
+
+
+ {/* Docking button only for larger screens that can support it*/}
+
+
+ {
+ setNavIsDocked(!navIsDocked);
+ localStorage.setItem(
+ 'navIsDocked',
+ JSON.stringify(!navIsDocked)
+ );
+ }}
+ iconType={navIsDocked ? 'lock' : 'lockOpen'}
+ />
+
+
+
+
+ );
+
+ const leftSectionItems = [
+ collapsibleNav,
+ Elastic ,
+ ];
+
+ return (
+
+ {setIsFullScreen => (
+
+ setIsFullScreen(false)}>
+ Exit full screen
+ ,
+ ],
+ },
+ ]}
+ />
+
+
+
+ )}
+
+ );
+};
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..94b9a04327f
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js
@@ -0,0 +1,214 @@
+import React from 'react';
+import { Link } from 'react-router';
+
+import { renderToHtml } from '../../services';
+
+import { GuideSectionTypes } from '../../components';
+
+import {
+ EuiCode,
+ EuiCollapsibleNav,
+ EuiText,
+ EuiSpacer,
+ EuiCallOut,
+ EuiCollapsibleNavGroup,
+} from '../../../../src/components';
+
+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',
+ 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 soon.
+
+
+
+ ),
+ 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: ,
+ snippet: ` setNavIsOpen(!navIsOpen)}>Toggle nav}
+ isOpen={navIsOpen}
+ isDocked={navIsDocked}
+ onClose={() => setNavIsOpen(false)}
+/>`,
+ },
+ {
+ title: 'Collapsible nav group',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: collapsibleNavGroupSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: collapsibleNavGroupHtml,
+ },
+ ],
+ text: (
+ <>
+
+ An EuiCollapsibleNavGroup adds some basic borders
+ and background color of none ,{' '}
+ light , or dark . Give each
+ section a heading by providing an optional title {' '}
+ and iconType . Make the section collapsible (
+ accordion style) with{' '}
+ isCollapsible=true .
+
+
+ When in isCollapsible mode, a{' '}
+ title and{' '}
+ initialIsOpen:boolean is required.
+
+ >
+ ),
+ props: {
+ EuiCollapsibleNavGroup,
+ },
+ demo: ,
+ snippet: ` `,
+ },
+ {
+ title: 'Nav groups with lists and other content',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: collapsibleNavListSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: collapsibleNavListHtml,
+ },
+ ],
+ text: (
+ <>
+
+ EuiCollapsibleNavGroups can contain any children.
+ They work well with{' '}
+
+ EuiListGroup, EuiPinnableListGroup
+ {' '}
+ and simple{' '}
+
+ EuiText
+
+ .
+
+ Below are a few established patterns to use.
+ >
+ ),
+ demo: ,
+ snippet: `
+ {}}
+ maxWidth="none"
+ color="subdued"
+ gutterSize="none"
+ size="s"
+ />
+ `,
+ },
+ {
+ title: 'Full pattern with header and saved pins',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: collapsibleNavAllSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: collapsibleNavAllHtml,
+ },
+ ],
+ text: (
+ <>
+ Putting it all together
+
+ The button below will launch a full screen example that includes{' '}
+
+ EuiHeader
+ {' '}
+ with a toggle button to open an EuiCollapsibleNav .
+ The contents of which are multiple{' '}
+ EuiCollapsibleNavGroups and saves the
+ open/closed/pinned state for each section and item in local store.
+
+
+ This is just a pattern and should be treated as such. Consuming
+ applications will need to create the navigation groups according to
+ their context and save the states as is appropriate to their data
+ store.
+
+ >
+ ),
+ demo: ,
+ },
+ ],
+};
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_group.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav_group.tsx
new file mode 100644
index 00000000000..89463b9a46d
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_group.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { EuiCollapsibleNavGroup } from '../../../../src/components/collapsible_nav';
+import { EuiText } from '../../../../src/components/text';
+import { EuiCode } from '../../../../src/components/code';
+
+export default () => (
+ <>
+
+
+ This is a basic group without any modifications
+
+
+
+
+
+ This is a nice group with a heading supplied via{' '}
+ title and iconType .
+
+
+
+
+
+
+ This group is collapsible and set with{' '}
+ initialIsOpen . It has a heading that is the
+ collapsing button via title and{' '}
+ iconType .
+
+
+
+
+
+
+ This is a dark collapsible group
+ that is initally set to closed,{' '}
+ iconSize="xxl" and{' '}
+ titleSize="s" .
+
+
+
+ >
+);
diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_list.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav_list.tsx
new file mode 100644
index 00000000000..c64b3661cf3
--- /dev/null
+++ b/src-docs/src/views/collapsible_nav/collapsible_nav_list.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+
+import { EuiCollapsibleNavGroup } from '../../../../src/components/collapsible_nav';
+import { EuiText } from '../../../../src/components/text';
+import {
+ EuiListGroup,
+ EuiListGroupProps,
+ EuiPinnableListGroup,
+ EuiPinnableListGroupItemProps,
+} from '../../../../src/components/list_group';
+import { EuiSpacer } from '../../../../src/components/spacer';
+import { EuiButton, EuiButtonIcon } from '../../../../src/components/button';
+import { EuiLink } from '../../../../src/components/link';
+
+const deploymentsList: EuiListGroupProps['listItems'] = [
+ {
+ label: 'combining-binaries',
+ iconType: 'logoAzureMono',
+ size: 's',
+ },
+ {
+ label: 'stack-monitoring',
+ iconType: 'logoAWSMono',
+ size: 's',
+ },
+];
+
+export const TopNavLinks: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Home',
+ iconType: 'home',
+ isActive: true,
+ pinnable: false,
+ },
+ { label: 'Dashboards', pinned: true },
+ { label: 'Dev tools', pinned: true },
+ { label: 'Maps', pinned: true },
+];
+
+export const KibanaNavLinks: EuiPinnableListGroupItemProps[] = [
+ { label: 'Discover' },
+ { label: 'Visualize' },
+ { label: 'Dashboards' },
+ { label: 'Canvas' },
+ { label: 'Maps' },
+ { label: 'Machine Learning' },
+ { label: 'Graph' },
+];
+
+export const DeploymentsGroup = (
+
+ Deployment
+ personal-databoard
+
+ }
+ iconType="logoGCPMono"
+ iconSize="xl"
+ isCollapsible={true}
+ initialIsOpen={false}
+ background="dark">
+
+
+
+
+ Manage deployments
+
+
+
+);
+
+export const SecurityGroup = (
+
+ }>
+
+
+ Threat prevention, detection, and response with SIEM and endpoint
+ security.
+
+ Learn more
+
+
+
+);
+
+export default () => (
+ <>
+ {DeploymentsGroup}
+
+ {}}
+ maxWidth="none"
+ color="text"
+ gutterSize="none"
+ size="s"
+ />
+
+
+ {}}
+ maxWidth="none"
+ color="subdued"
+ gutterSize="none"
+ size="s"
+ />
+
+ {SecurityGroup}
+ >
+);
diff --git a/src-docs/src/views/icon/icons.js b/src-docs/src/views/icon/icons.js
index 9b3fb3853f8..d8f76852a32 100644
--- a/src-docs/src/views/icon/icons.js
+++ b/src-docs/src/views/icon/icons.js
@@ -94,6 +94,7 @@ export const iconTypes = [
'heart',
'heatmap',
'help',
+ 'home',
'iInCircle',
'image',
'importAction',
@@ -129,6 +130,7 @@ export const iconTypes = [
'mapMarker',
'memory',
'merge',
+ 'menu',
'menuLeft',
'menuRight',
'minimize',
diff --git a/src-docs/src/views/list_group/list_group_example.js b/src-docs/src/views/list_group/list_group_example.js
index 7e704b16cfc..bf2aa4cd593 100644
--- a/src-docs/src/views/list_group/list_group_example.js
+++ b/src-docs/src/views/list_group/list_group_example.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router';
import { renderToHtml } from '../../services';
@@ -7,8 +8,10 @@ import { GuideSectionTypes } from '../../components';
import {
EuiListGroup,
EuiListGroupItem,
+ EuiPinnableListGroup,
EuiCode,
} from '../../../../src/components';
+import { EuiPinnableListGroupItem } from './props';
import ListGroup from './list_group';
const listGroupSource = require('!!raw-loader!./list_group');
@@ -30,6 +33,10 @@ import ListGroupItemColor from './list_group_item_color';
const listGroupItemColorSource = require('!!raw-loader!./list_group_item_color');
const listGroupItemColorHtml = renderToHtml(ListGroupItemColor);
+import PinnableListGroup from './pinnable_list_group';
+const pinnableListGroupSource = require('!!raw-loader!./pinnable_list_group');
+const pinnableListGroupHtml = renderToHtml(PinnableListGroup);
+
export const ListGroupExample = {
title: 'List group',
sections: [
@@ -191,7 +198,8 @@ export const ListGroupExample = {
anchor , or span . You can
enforce a different color of primary ,{' '}
text , or subdued with the{' '}
- color prop.
+ color prop. Or provide the prop directly to{' '}
+ EuiListGroup .
They also accept options for text size;{' '}
@@ -204,6 +212,58 @@ export const ListGroupExample = {
label="Primary"
color="primary"
size="s"
+/>`,
+ },
+ {
+ title: 'Pinnable list group',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: pinnableListGroupSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: pinnableListGroupHtml,
+ },
+ ],
+ text: (
+ <>
+
+ EuiPinnableListGroup is simply an extra wrapper
+ around an{' '}
+
+ EuiListGroup
+ {' '}
+ that provides visual indicators for pinning .
+
+
+ Pinning is the concept that users can click a pin icon and add it to
+ a subset of links (most likely shown in different list group). By
+ providing an onPinClick handler, the component
+ will automatically add the pin action to the item. However, the
+ consuming application must manage the listItem s
+ and their pinned state.
+
+
+ In order to get the full benefit of using{' '}
+ EuiPinnableListGroup , the component only supports
+ providing list items via the listItem prop and
+ does not support children .
+
+ >
+ ),
+ props: { EuiPinnableListGroup, EuiPinnableListGroupItem },
+ demo: ,
+ snippet: ` {}}
+ listItems={[
+ {
+ label: 'A link',
+ href: '#',
+ pinned: true,
+ isActive: true,
+ },
+ ]}
/>`,
},
],
diff --git a/src-docs/src/views/list_group/list_group_item_color.tsx b/src-docs/src/views/list_group/list_group_item_color.tsx
index 883bd524338..46c58e3d680 100644
--- a/src-docs/src/views/list_group/list_group_item_color.tsx
+++ b/src-docs/src/views/list_group/list_group_item_color.tsx
@@ -4,20 +4,29 @@ import {
EuiListGroupItem,
EuiListGroup,
} from '../../../../src/components/list_group';
+import { EuiSpacer } from '../../../../src/components/spacer';
export default () => (
-
-
+ <>
+
+
- {}}
- label="Primary (s)"
- color="primary"
- size="s"
- />
+ {}}
+ label="Primary (s)"
+ color="primary"
+ size="s"
+ />
-
+
-
-
+
+
+
+
+
+
+
+
+ >
);
diff --git a/src-docs/src/views/list_group/list_group_link_actions.js b/src-docs/src/views/list_group/list_group_link_actions.js
index e3e31b1df89..ec6edf64304 100644
--- a/src-docs/src/views/list_group/list_group_link_actions.js
+++ b/src-docs/src/views/list_group/list_group_link_actions.js
@@ -68,7 +68,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link1Clicked,
- iconType: favorite1 === 'link1' ? 'pinFilled' : 'pin',
+ iconType: favorite1 === 'link1' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link1',
alwaysShow: favorite1 === 'link1',
@@ -83,7 +83,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link2Clicked,
- iconType: favorite2 === 'link2' ? 'pinFilled' : 'pin',
+ iconType: favorite2 === 'link2' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link2',
alwaysShow: favorite2 === 'link2',
@@ -98,7 +98,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link3Clicked,
- iconType: favorite3 === 'link3' ? 'pinFilled' : 'pin',
+ iconType: favorite3 === 'link3' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link3',
alwaysShow: favorite3 === 'link3',
@@ -114,7 +114,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: () => window.alert('Action clicked'),
- iconType: 'pin',
+ iconType: 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link4',
}}
diff --git a/src-docs/src/views/list_group/pinnable_list_group.tsx b/src-docs/src/views/list_group/pinnable_list_group.tsx
new file mode 100644
index 00000000000..4716de39ef8
--- /dev/null
+++ b/src-docs/src/views/list_group/pinnable_list_group.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+import {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupItemProps,
+} from '../../../../src/components/list_group';
+
+const someListItems: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Label with iconType',
+ iconType: 'stop',
+ },
+ {
+ label: 'Pinned button with onClick',
+ pinned: true,
+ onClick: e => {
+ console.log('Pinned button clicked', e);
+ },
+ },
+ {
+ label: 'Link with href and custom pin titles',
+ href: '/#',
+ },
+ {
+ label: 'Active link',
+ isActive: true,
+ href: '/#',
+ },
+ {
+ label: 'Custom extra actions will override pinning ability',
+ extraAction: {
+ iconType: 'bell',
+ alwaysShow: true,
+ 'aria-label': 'bell',
+ },
+ },
+ {
+ label: 'Item with pinnability turned off',
+ pinnable: false,
+ },
+];
+
+export default () => (
+ <>
+ {
+ console.warn('Clicked: ', item);
+ }}
+ maxWidth="none"
+ pinTitle={(item: EuiPinnableListGroupItemProps) => `Pin ${item.label}`}
+ unpinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Unpin ${item.label}`
+ }
+ />
+ >
+);
diff --git a/src-docs/src/views/list_group/props.tsx b/src-docs/src/views/list_group/props.tsx
new file mode 100644
index 00000000000..c131a02172c
--- /dev/null
+++ b/src-docs/src/views/list_group/props.tsx
@@ -0,0 +1,7 @@
+import React, { FunctionComponent } from 'react';
+
+import { EuiPinnableListGroupItemProps } from '../../../../src/components/list_group';
+
+export const EuiPinnableListGroupItem: FunctionComponent<
+ EuiPinnableListGroupItemProps
+> = () =>
;
diff --git a/src/components/accordion/__snapshots__/accordion.test.tsx.snap b/src/components/accordion/__snapshots__/accordion.test.tsx.snap
index 4f49249582f..d34f5210c31 100644
--- a/src/components/accordion/__snapshots__/accordion.test.tsx.snap
+++ b/src/components/accordion/__snapshots__/accordion.test.tsx.snap
@@ -35,7 +35,9 @@ exports[`EuiAccordion behavior closes when clicked twice 1`] = `
/>
-
+
-
+
-
+
-
+
-
+
-
+
Button content
@@ -306,7 +318,7 @@ exports[`EuiAccordion props buttonContentClassName is rendered 1`] = `
/>
@@ -344,7 +356,9 @@ exports[`EuiAccordion props extraAction is rendered 1`] = `
data-euiicon-type="arrowRight"
/>
-
+
-
+
{icon}
-
{buttonContent}
+
+ {buttonContent}
+
{optionalAction}
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..9c6136cdc25
--- /dev/null
+++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap
@@ -0,0 +1,122 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCollapsibleNav is rendered 1`] = `null`;
+
+exports[`EuiCollapsibleNav props button 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNav props hideButtonIfDocked 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNav props isDocked 1`] = `
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`EuiCollapsibleNav props isOpen 1`] = `
+Array [
+
,
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
,
+]
+`;
+
+exports[`EuiCollapsibleNav props onClose 1`] = `null`;
diff --git a/src/components/collapsible_nav/_collapsible_nav.scss b/src/components/collapsible_nav/_collapsible_nav.scss
new file mode 100644
index 00000000000..f15d58a6b49
--- /dev/null
+++ b/src/components/collapsible_nav/_collapsible_nav.scss
@@ -0,0 +1,57 @@
+// 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;
+ }
+ }
+
+ .euiCollapsibleNav__toggle--navIsDocked {
+ 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..a31c44be949
--- /dev/null
+++ b/src/components/collapsible_nav/_index.scss
@@ -0,0 +1,4 @@
+@import 'variables';
+
+@import 'collapsible_nav';
+@import 'collapsible_nav_group/index';
diff --git a/src/components/collapsible_nav/_variables.scss b/src/components/collapsible_nav/_variables.scss
new file mode 100644
index 00000000000..f883cd5f8a2
--- /dev/null
+++ b/src/components/collapsible_nav/_variables.scss
@@ -0,0 +1,14 @@
+// Sizing
+$euiCollapsibleNavWidth: $euiSize * 20; // ~ 320px
+
+$euiCollapsibleNavGroupLightBackgroundColor: $euiPageBackgroundColor;
+
+$euiCollapsibleNavGroupDarkBackgroundColor: lightOrDarkTheme(
+ shade($euiColorDarkestShade, 20%),
+ shade($euiColorLightestShade, 50%),
+);
+
+$euiCollapsibleNavGroupDarkHighContrastColor: makeGraphicContrastColor(
+ $euiColorPrimary,
+ $euiCollapsibleNavGroupDarkBackgroundColor
+);
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..e2cd94f2348
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav.test.tsx
@@ -0,0 +1,59 @@
+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(
);
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ test('onClose', () => {
+ const component = render(
+
{}} />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('isDocked', () => {
+ const component = render( );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('isOpen', () => {
+ const component = render( );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('button', () => {
+ const component = render(
+ } />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('hideButtonIfDocked', () => {
+ const component = render(
+ }
+ hideButtonIfDocked={false}
+ />
+ );
+
+ 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..9b917b5ed31
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav.tsx
@@ -0,0 +1,148 @@
+import React, {
+ FunctionComponent,
+ ReactNode,
+ useEffect,
+ useState,
+ HTMLAttributes,
+ ReactElement,
+ cloneElement,
+} from 'react';
+import classNames from 'classnames';
+import { throttle } from '../color_picker/utils';
+import { EuiWindowEvent, keyCodes, htmlIdGenerator } from '../../services';
+import { EuiFocusTrap } from '../focus_trap';
+import { EuiOverlayMask } from '../overlay_mask';
+import { CommonProps } from '../common';
+import { EuiButtonEmpty } from '../button';
+import { EuiI18n } from '../i18n';
+
+export type EuiCollapsibleNavProps = CommonProps &
+ HTMLAttributes & {
+ children?: ReactNode;
+ /**
+ * Keeps navigation flyout visible and push `` content via padding
+ */
+ isDocked?: boolean;
+ /**
+ * Shows the navigation flyout
+ */
+ isOpen?: boolean;
+ /**
+ * Button for controlling visible state of the nav
+ */
+ button?: ReactElement;
+ /**
+ * Removes display of toggle button when in docked state
+ */
+ hideButtonIfDocked?: boolean;
+ onClose?: () => void;
+ };
+
+export const EuiCollapsibleNav: FunctionComponent = ({
+ children,
+ className,
+ isDocked = false,
+ isOpen = false,
+ onClose,
+ button,
+ hideButtonIfDocked = true,
+ id,
+ ...rest
+}) => {
+ const [flyoutID] = useState(id || htmlIdGenerator()('euiCollapsibleNav'));
+ const [windowIsLargeEnoughToDock, setWindowIsLargeEnoughToDock] = useState(
+ window.innerWidth >= 992
+ );
+ const navIsDocked = isDocked && 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 (isDocked) {
+ document.body.classList.add('euiBody--collapsibleNavIsDocked');
+ window.addEventListener('resize', functionToCallOnWindowResize);
+ }
+ return () => {
+ document.body.classList.remove('euiBody--collapsibleNavIsDocked');
+ window.removeEventListener('resize', functionToCallOnWindowResize);
+ };
+ }, [isDocked, functionToCallOnWindowResize]);
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.keyCode === keyCodes.ESCAPE) {
+ event.preventDefault();
+ collapse();
+ }
+ };
+
+ const collapse = () => {
+ if (!navIsDocked) {
+ onClose && onClose();
+ }
+ };
+
+ const classes = classNames(
+ 'euiCollapsibleNav',
+ { 'euiCollapsibleNav--isDocked': navIsDocked },
+ className
+ );
+
+ let optionalOverlay;
+ if (!navIsDocked) {
+ optionalOverlay = ;
+ }
+
+ // Show a trigger button if one was passed but
+ // not if hideButtonIfDocked and navIsDocked
+ const trigger =
+ button &&
+ !(hideButtonIfDocked && navIsDocked) &&
+ cloneElement(button as ReactElement, {
+ 'aria-controls': flyoutID,
+ 'aria-expanded': isOpen,
+ 'aria-pressed': isOpen,
+ className: classNames(
+ button.props.className,
+ 'euiCollapsibleNav__toggle'
+ ),
+ });
+
+ const flyout = (
+ <>
+
+ {optionalOverlay}
+ {/* Trap focus only when docked={false} */}
+
+
+ {children}
+
+
+
+
+
+
+ >
+ );
+
+ return (
+ <>
+ {trigger}
+ {(isOpen || navIsDocked) && flyout}
+ >
+ );
+};
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
new file mode 100644
index 00000000000..ac3c4be8d54
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
@@ -0,0 +1,259 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCollapsibleNavGroup is rendered 1`] = `
+
+`;
+
+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 iconSize is rendered 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNavGroup props iconType is rendered 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNavGroup props title is rendered 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNavGroup props titleElement can change the rendered element to h2 1`] = `
+
+`;
+
+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 will render an accordion 1`] = `
+
+`;
diff --git a/src/components/collapsible_nav/collapsible_nav_group/_collapsible_nav_group.scss b/src/components/collapsible_nav/collapsible_nav_group/_collapsible_nav_group.scss
new file mode 100644
index 00000000000..257c5142be5
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/_collapsible_nav_group.scss
@@ -0,0 +1,59 @@
+.euiCollapsibleNavGroup {
+ &:not(:first-child) {
+ border-top: $euiBorderThin;
+ }
+
+ // This class does not accept a custom classname
+ .euiAccordion__triggerWrapper {
+ // Add padding to the trigger wrapper in case an `extraAction` is passed
+ // that doesn't get wrapped in the `__heading`
+ padding: $euiSize;
+ }
+}
+
+.euiCollapsibleNavGroup--light {
+ background-color: $euiCollapsibleNavGroupLightBackgroundColor;
+}
+
+.euiCollapsibleNavGroup--dark {
+ background-color: $euiCollapsibleNavGroupDarkBackgroundColor;
+ color: $euiColorGhost;
+
+ // Forcing better contrast of focus state on EuiAccordion toggle icon
+ .euiCollapsibleNavGroup__heading:focus .euiAccordion__iconWrapper {
+ color: $euiCollapsibleNavGroupDarkHighContrastColor;
+ animation-name: euiCollapsibleNavGroupDarkFocusRingAnimate !important; // sass-lint:disable-line no-important
+ }
+
+ .euiCollapsibleNavGroup__title {
+ color: inherit;
+ line-height: inherit;
+ }
+}
+
+.euiCollapsibleNavGroup__heading {
+ font-weight: $euiFontWeightSemiBold;
+
+ // If the heading is not in an accordion, it needs the padding
+ &:not(.euiAccordion__button) {
+ padding: $euiSize;
+ }
+}
+
+.euiCollapsibleNavGroup__children {
+ padding: $euiSizeS;
+}
+
+.euiCollapsibleNavGroup--withHeading .euiCollapsibleNavGroup__children {
+ padding-top: 0;
+}
+
+@keyframes euiCollapsibleNavGroupDarkFocusRingAnimate {
+ 0% {
+ box-shadow: 0 0 0 $euiFocusRingAnimStartSize $euiFocusRingAnimStartColor;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 $euiFocusRingSize $euiCollapsibleNavGroupDarkHighContrastColor;
+ }
+}
diff --git a/src/components/collapsible_nav/collapsible_nav_group/_index.scss b/src/components/collapsible_nav/collapsible_nav_group/_index.scss
new file mode 100644
index 00000000000..667ebab6881
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/_index.scss
@@ -0,0 +1 @@
+@import 'collapsible_nav_group';
diff --git a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.test.tsx b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.test.tsx
new file mode 100644
index 00000000000..3a37ffed510
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.test.tsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiCollapsibleNavGroup, BACKGROUNDS } from './collapsible_nav_group';
+
+describe('EuiCollapsibleNavGroup', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ test('title is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('iconType is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('iconSize is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('background', () => {
+ BACKGROUNDS.forEach(color => {
+ test(`${color} is rendered`, () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+
+ test('titleElement can change the rendered element to h2', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('titleSize can be larger', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe('when isCollapsible is true', () => {
+ test('will render an accordion', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ describe('throws a warning', () => {
+ const oldConsoleError = console.warn;
+ let consoleStub: jest.Mock;
+
+ beforeEach(() => {
+ // We don't use jest.spyOn() here, because EUI's tests apply a global
+ // console.error() override that throws an exception. For these
+ // tests, we just want to know if console.error() was called.
+ console.warn = consoleStub = jest.fn();
+ });
+
+ afterEach(() => {
+ console.warn = oldConsoleError;
+ });
+
+ test('if iconType is passed without a title', () => {
+ const component = render(
+
+ );
+
+ expect(consoleStub).toBeCalled();
+ expect(consoleStub.mock.calls[0][0]).toMatch(
+ 'not render an icon without `title`'
+ );
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
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
new file mode 100644
index 00000000000..f59694d1e73
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx
@@ -0,0 +1,172 @@
+import React, {
+ FunctionComponent,
+ ReactNode,
+ useState,
+ HTMLAttributes,
+} from 'react';
+import classNames from 'classnames';
+import { CommonProps, ExclusiveUnion } from '../../common';
+import { htmlIdGenerator } from '../../../services';
+
+import { EuiAccordion, EuiAccordionProps } from '../../accordion';
+import { EuiIcon, IconType, IconSize } from '../../icon';
+import { EuiFlexGroup, EuiFlexItem } from '../../flex';
+import { EuiTitle, EuiTitleProps, EuiTitleSize } from '../../title';
+
+type Background = 'none' | 'light' | 'dark';
+const backgroundToClassNameMap: { [color in Background]: string } = {
+ none: '',
+ light: 'euiCollapsibleNavGroup--light',
+ dark: 'euiCollapsibleNavGroup--dark',
+};
+export const BACKGROUNDS = Object.keys(
+ backgroundToClassNameMap
+) as Background[];
+
+export interface EuiCollapsibleNavGroupInterface extends CommonProps {
+ children?: ReactNode;
+ /**
+ * Sits left of the `title` and only when `title` is present
+ */
+ iconType?: IconType;
+ /**
+ * Change the size of the icon in the `title`
+ */
+ iconSize?: IconSize;
+ /**
+ * Optionally provide an id, otherwise one will be created
+ */
+ id?: string;
+ /**
+ * Adds a background color to the entire group,
+ * applying the correct text color to the `title` only
+ */
+ background?: Background;
+ /**
+ * Determines the title's heading element
+ */
+ titleElement?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span';
+ /**
+ * Title sizing equivelant to EuiTitle, but only `s` and smaller
+ */
+ titleSize?: Omit;
+}
+
+type GroupAsAccordion = EuiCollapsibleNavGroupInterface &
+ // The HTML `title` prop conflicts in type with our `title` prop
+ Omit & {
+ /**
+ * If `true`, wraps children in the body of an accordion,
+ * requiring the prop `title` to be used as the button.
+ * When `false`, simply renders a div without any accordion functionality.
+ */
+ isCollapsible: true;
+ /**
+ * The title gets wrapped in the appropriate heading level
+ * with the option to add an iconType
+ */
+ title: ReactNode;
+ };
+
+type GroupAsDiv = EuiCollapsibleNavGroupInterface & {
+ /**
+ * If `true`, wraps children in the body of an accordion,
+ * requiring the prop `title` to be used as the button.
+ * When `false`, simply renders a div without any accordion functionality.
+ */
+ isCollapsible?: false;
+ /**
+ * The title gets wrapped in the appropriate heading level
+ * with the option to add an iconType
+ */
+ title?: ReactNode;
+} & HTMLAttributes;
+
+export type EuiCollapsibleNavGroupProps = ExclusiveUnion<
+ GroupAsAccordion,
+ GroupAsDiv
+>;
+
+export const EuiCollapsibleNavGroup: FunctionComponent<
+ EuiCollapsibleNavGroupProps
+> = ({
+ className,
+ children,
+ id,
+ title,
+ iconType,
+ iconSize = 'l',
+ background = 'none',
+ isCollapsible = false,
+ titleElement = 'h3',
+ titleSize = 'xxs',
+ ...rest
+}) => {
+ const [groupID] = useState(id || htmlIdGenerator()());
+ const titleID = `${groupID}__title`;
+
+ const classes = classNames(
+ 'euiCollapsibleNavGroup',
+ backgroundToClassNameMap[background],
+ {
+ 'euiCollapsibleNavGroup--withHeading': title,
+ },
+ className
+ );
+
+ // Warn if consumer passes an iconType without a title
+ if (iconType && !title) {
+ console.warn(
+ 'EuiCollapsibleNavGroup will not render an icon without `title`.'
+ );
+ }
+
+ const content = (
+ {children}
+ );
+
+ const headingClasses = 'euiCollapsibleNavGroup__heading';
+
+ const TitleElement = titleElement;
+ const titleContent = title ? (
+
+ {iconType && (
+
+
+
+ )}
+
+
+
+
+ {title}
+
+
+
+
+ ) : (
+ undefined
+ );
+
+ if (isCollapsible && title) {
+ return (
+
+ {content}
+
+ );
+ } else {
+ return (
+
+ {titleContent &&
{titleContent}
}
+ {content}
+
+ );
+ }
+};
diff --git a/src/components/collapsible_nav/collapsible_nav_group/index.ts b/src/components/collapsible_nav/collapsible_nav_group/index.ts
new file mode 100644
index 00000000000..3ded0215793
--- /dev/null
+++ b/src/components/collapsible_nav/collapsible_nav_group/index.ts
@@ -0,0 +1,4 @@
+export {
+ EuiCollapsibleNavGroup,
+ EuiCollapsibleNavGroupProps,
+} from './collapsible_nav_group';
diff --git a/src/components/collapsible_nav/index.ts b/src/components/collapsible_nav/index.ts
new file mode 100644
index 00000000000..846e72f558f
--- /dev/null
+++ b/src/components/collapsible_nav/index.ts
@@ -0,0 +1,6 @@
+export {
+ EuiCollapsibleNavGroup,
+ EuiCollapsibleNavGroupProps,
+} from './collapsible_nav_group';
+
+export { EuiCollapsibleNav, EuiCollapsibleNavProps } from './collapsible_nav';
diff --git a/src/components/flyout/_flyout.scss b/src/components/flyout/_flyout.scss
index def8aec630b..690060c08c9 100644
--- a/src/components/flyout/_flyout.scss
+++ b/src/components/flyout/_flyout.scss
@@ -1,4 +1,6 @@
-.euiFlyout {
+@import '../header/variables';
+
+%eui-flyout {
border-left: $euiBorderThin;
// The mixin augments the above
// sass-lint:disable mixins-before-declarations
@@ -10,10 +12,20 @@
height: 100%;
z-index: $euiZModal;
background: $euiColorEmptyShade;
- animation: euiFlyout $euiAnimSpeedNormal $euiAnimSlightResistance;
display: flex;
flex-direction: column;
align-items: stretch;
+
+ // When the EuiHeader is fixed, we need to account for it in the position of the flyout
+ .euiBody--headerIsFixed & {
+ top: $euiHeaderHeightCompensation;
+ height: calc(100% - #{$euiHeaderHeightCompensation});
+ }
+}
+
+.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
diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss
index 389fb60f949..839a4858f2d 100644
--- a/src/components/header/_header.scss
+++ b/src/components/header/_header.scss
@@ -20,5 +20,5 @@
}
.euiBody--headerIsFixed {
- padding-top: $euiHeaderChildSize + $euiSizeS; // Extra padding to accound for the shadow
+ padding-top: $euiHeaderHeightCompensation + $euiSizeS; // Extra padding to account for the shadow
}
diff --git a/src/components/header/_variables.scss b/src/components/header/_variables.scss
index 61b4c2a86c7..9da0688ee28 100644
--- a/src/components/header/_variables.scss
+++ b/src/components/header/_variables.scss
@@ -4,4 +4,8 @@ $euiHeaderBackgroundColor: $euiColorEmptyShade;
$euiHeaderBreadcrumbColor: $euiColorDarkestShade;
// Layout vars
-$euiHeaderChildSize: $euiSizeXXL + $euiSizeS;
+$euiHeaderHeight: $euiSizeXXL + $euiSizeS;
+$euiHeaderChildSize: $euiHeaderHeight;
+
+// Use the following variable in other components to afford for the fixed header
+$euiHeaderHeightCompensation: $euiHeaderHeight + 1px !default;
diff --git a/src/components/horizontal_rule/_horizontal_rule.scss b/src/components/horizontal_rule/_horizontal_rule.scss
index 4e903a07051..b90b06bf18b 100644
--- a/src/components/horizontal_rule/_horizontal_rule.scss
+++ b/src/components/horizontal_rule/_horizontal_rule.scss
@@ -1,9 +1,9 @@
.euiHorizontalRule {
border: none;
- // Sometimes Chrome "calculates" an element height of e.g. 0.990px, which it
- // rounds down, thereby hiding the element.
- height: 1.1px;
+ height: 1px;
background-color: $euiBorderColor;
+ flex-shrink: 0; // Ensure when used in flex group, it retains its size
+ flex-grow: 0; // Ensure when used in flex group, it retains its size
&.euiHorizontalRule--full {
width: 100%;
diff --git a/src/components/icon/__snapshots__/icon.test.tsx.snap b/src/components/icon/__snapshots__/icon.test.tsx.snap
index 884cca1da10..c9d70b0f23b 100644
--- a/src/components/icon/__snapshots__/icon.test.tsx.snap
+++ b/src/components/icon/__snapshots__/icon.test.tsx.snap
@@ -2697,6 +2697,23 @@ exports[`EuiIcon props type help is rendered 1`] = `
`;
+exports[`EuiIcon props type home is rendered 1`] = `
+
+
+
+`;
+
exports[`EuiIcon props type iInCircle is rendered 1`] = `
`;
+exports[`EuiIcon props type menu is rendered 1`] = `
+
+
+
+`;
+
exports[`EuiIcon props type menuLeft is rendered 1`] = `
(
+
+ {title ? {title} : null}
+
+
+);
+
+export const icon = EuiIconHome;
diff --git a/src/components/icon/assets/home.svg b/src/components/icon/assets/home.svg
new file mode 100644
index 00000000000..ad1a980e9b3
--- /dev/null
+++ b/src/components/icon/assets/home.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/icon/assets/menu.js b/src/components/icon/assets/menu.js
new file mode 100644
index 00000000000..8f38cfd85d3
--- /dev/null
+++ b/src/components/icon/assets/menu.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+const EuiIconMenu = ({ title, titleId, ...props }) => (
+
+ {title ? {title} : null}
+
+
+);
+
+export const icon = EuiIconMenu;
diff --git a/src/components/icon/assets/menu.svg b/src/components/icon/assets/menu.svg
new file mode 100755
index 00000000000..3d1df578ed1
--- /dev/null
+++ b/src/components/icon/assets/menu.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 1b7815569cc..cbe7ecc5d24 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -142,6 +142,7 @@ const typeToPathMap = {
heartbeatApp: 'app_heartbeat',
heatmap: 'heatmap',
help: 'help',
+ home: 'home',
iInCircle: 'iInCircle',
image: 'image',
importAction: 'import',
@@ -241,6 +242,7 @@ const typeToPathMap = {
managementApp: 'app_management',
mapMarker: 'map_marker',
memory: 'memory',
+ menu: 'menu',
menuLeft: 'menuLeft',
menuRight: 'menuRight',
merge: 'merge',
diff --git a/src/components/index.js b/src/components/index.js
index 9efdf29620a..9dcc3991767 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, EuiCollapsibleNavGroup } from './collapsible_nav';
+
export {
EuiColorPicker,
EuiColorPickerSwatch,
@@ -181,7 +183,11 @@ export { EuiKeyPadMenu, EuiKeyPadMenuItem } from './key_pad_menu';
export { EuiLink } from './link';
-export { EuiListGroup, EuiListGroupItem } from './list_group';
+export {
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiPinnableListGroup,
+} from './list_group';
export { EuiMark } from './mark';
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';
diff --git a/src/components/list_group/__snapshots__/list_group.test.tsx.snap b/src/components/list_group/__snapshots__/list_group.test.tsx.snap
index 72252c29443..ac58002cf33 100644
--- a/src/components/list_group/__snapshots__/list_group.test.tsx.snap
+++ b/src/components/list_group/__snapshots__/list_group.test.tsx.snap
@@ -3,14 +3,14 @@
exports[`EuiListGroup is rendered 1`] = `
`;
-exports[`EuiListGroup is rendered with listItems 1`] = `
+exports[`EuiListGroup listItems is rendered 1`] = `
+
+
+
+ Active link
+
+
+
@@ -87,21 +103,122 @@ exports[`EuiListGroup is rendered with listItems 1`] = `
`;
+exports[`EuiListGroup listItems is rendered with color 1`] = `
+
+`;
+
+exports[`EuiListGroup listItems is rendered with size 1`] = `
+
+`;
+
exports[`EuiListGroup props bordered is rendered 1`] = `
`;
exports[`EuiListGroup props flush is rendered 1`] = `
`;
exports[`EuiListGroup props gutter size m is rendered 1`] = `
`;
@@ -113,38 +230,38 @@ exports[`EuiListGroup props gutter size none is rendered 1`] = `
exports[`EuiListGroup props gutter size s is rendered 1`] = `
`;
exports[`EuiListGroup props maxWidth as a number is rendered 1`] = `
`;
exports[`EuiListGroup props maxWidth as a string is rendered 1`] = `
`;
exports[`EuiListGroup props maxWidth as true is rendered 1`] = `
`;
exports[`EuiListGroup props showToolTips is rendered 1`] = `
`;
exports[`EuiListGroup props wrapText is rendered 1`] = `
`;
diff --git a/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
index e110e1cebbe..c43facbadcc 100644
--- a/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
+++ b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
@@ -17,6 +17,23 @@ exports[`EuiListGroupItem is rendered 1`] = `
`;
+exports[`EuiListGroupItem props color ghost is rendered 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
exports[`EuiListGroupItem props color inherit is rendered 1`] = `
{
expect(component).toMatchSnapshot();
});
- test('is rendered with listItems', () => {
- const component = render( );
+ describe('listItems', () => {
+ test('is rendered', () => {
+ const component = render( );
- expect(component).toMatchSnapshot();
+ expect(component).toMatchSnapshot();
+ });
+
+ test('is rendered with color', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('is rendered with size', () => {
+ const component = render( );
+
+ expect(component).toMatchSnapshot();
+ });
});
describe('props', () => {
diff --git a/src/components/list_group/list_group.tsx b/src/components/list_group/list_group.tsx
index b79a1d388de..22fe2003f4f 100644
--- a/src/components/list_group/list_group.tsx
+++ b/src/components/list_group/list_group.tsx
@@ -7,8 +7,8 @@ import { CommonProps } from '../common';
type GutterSize = 'none' | 's' | 'm';
const gutterSizeToClassNameMap: { [size in GutterSize]: string } = {
none: '',
- s: 'euiListGroup--gutterS',
- m: 'euiListGroup--gutterM',
+ s: 'euiListGroup--gutterSmall',
+ m: 'euiListGroup--gutterMedium',
};
export const GUTTER_SIZES = Object.keys(
gutterSizeToClassNameMap
@@ -36,6 +36,16 @@ export type EuiListGroupProps = CommonProps &
*/
listItems?: EuiListGroupItemProps[];
+ /**
+ * Change the colors of all `listItems` at once
+ */
+ color?: EuiListGroupItemProps['color'];
+
+ /**
+ * Change the size of all `listItems` at once
+ */
+ size?: EuiListGroupItemProps['size'];
+
/**
* Sets the max-width of the page,
* set to `true` to use the default size,
@@ -68,6 +78,8 @@ export const EuiListGroup: FunctionComponent = ({
wrapText = false,
maxWidth = true,
showToolTips = false,
+ color,
+ size,
ariaLabelledby,
...rest
}) => {
@@ -105,6 +117,8 @@ export const EuiListGroup: FunctionComponent = ({
key={`title-${index}`}
showToolTip={showToolTips}
wrapText={wrapText}
+ color={color}
+ size={size}
{...item}
/>,
];
diff --git a/src/components/list_group/list_group_item.tsx b/src/components/list_group/list_group_item.tsx
index 0801ae8043e..04457fe55e9 100644
--- a/src/components/list_group/list_group_item.tsx
+++ b/src/components/list_group/list_group_item.tsx
@@ -25,12 +25,13 @@ const sizeToClassNameMap: { [size in ItemSize]: string } = {
};
export const SIZES = Object.keys(sizeToClassNameMap) as ItemSize[];
-type Color = 'inherit' | 'primary' | 'text' | 'subdued';
+type Color = 'inherit' | 'primary' | 'text' | 'subdued' | 'ghost';
const colorToClassNameMap: { [color in Color]: string } = {
inherit: '',
primary: 'euiListGroupItem--primary',
text: 'euiListGroupItem--text',
subdued: 'euiListGroupItem--subdued',
+ ghost: 'euiListGroupItem--ghost',
};
export const COLORS = Object.keys(colorToClassNameMap) as Color[];
@@ -38,7 +39,7 @@ export type EuiListGroupItemProps = CommonProps &
ExclusiveUnion<
ExclusiveUnion<
ButtonHTMLAttributes,
- AnchorHTMLAttributes
+ Omit, 'href'>
>,
HTMLAttributes
> & {
@@ -48,7 +49,7 @@ export type EuiListGroupItemProps = CommonProps &
size?: ItemSize;
/**
* By default the item will inherit the color of its wrapper (button/link/span),
- * otherwise pass on of the acceptable options
+ * otherwise pass one of the acceptable options
*/
color?: Color;
diff --git a/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap b/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap
new file mode 100644
index 00000000000..75048f15230
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap
@@ -0,0 +1,321 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiPinnableListGroup can have custom pin icon titles 1`] = `
+
+`;
+
+exports[`EuiPinnableListGroup is rendered 1`] = `
+
+`;
diff --git a/src/components/list_group/pinnable_list_group/_index.scss b/src/components/list_group/pinnable_list_group/_index.scss
new file mode 100644
index 00000000000..0bd00f0595e
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/_index.scss
@@ -0,0 +1 @@
+@import 'pinnable_list_group';
diff --git a/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss b/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss
new file mode 100644
index 00000000000..05f7191ad2a
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss
@@ -0,0 +1,9 @@
+.euiPinnableListGroup__itemExtraAction {
+ svg {
+ transform: rotate(45deg);
+ }
+}
+
+.euiPinnableListGroup__itemExtraAction-pinned:not(:hover):not(:focus) {
+ color: makeGraphicContrastColor($euiColorLightShade);
+}
diff --git a/src/components/list_group/pinnable_list_group/index.ts b/src/components/list_group/pinnable_list_group/index.ts
new file mode 100644
index 00000000000..0b9cf97d764
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/index.ts
@@ -0,0 +1,5 @@
+export {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupProps,
+ EuiPinnableListGroupItemProps,
+} from './pinnable_list_group';
diff --git a/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx b/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx
new file mode 100644
index 00000000000..7957953c655
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupItemProps,
+} from './pinnable_list_group';
+
+const someListItems: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Label with iconType',
+ iconType: 'stop',
+ },
+ {
+ label: 'Custom extra action',
+ extraAction: {
+ iconType: 'bell',
+ alwaysShow: true,
+ 'aria-label': 'bell',
+ },
+ },
+ {
+ label: 'Active link',
+ isActive: true,
+ href: '#',
+ },
+ {
+ label: 'Button with onClick',
+ pinned: true,
+ onClick: e => {
+ console.log('Visualize clicked', e);
+ },
+ },
+ {
+ label: 'Link with href',
+ href: '#',
+ },
+ {
+ label: 'Not pinnable',
+ href: '#',
+ pinnable: false,
+ },
+];
+
+describe('EuiPinnableListGroup', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('can have custom pin icon titles', () => {
+ const component = render(
+ {}}
+ pinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Pin ${item.label} to the top`
+ }
+ unpinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Unpin ${item.label} to the top`
+ }
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx b/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx
new file mode 100644
index 00000000000..c1b1a551d07
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx
@@ -0,0 +1,129 @@
+import React, { FunctionComponent } from 'react';
+import classNames from 'classnames';
+import { CommonProps } from '../../common';
+
+import { EuiI18n } from '../../i18n';
+import { EuiListGroup, EuiListGroupProps } from '../list_group';
+import { EuiListGroupItemProps } from '../list_group_item';
+
+const pinExtraAction: EuiListGroupItemProps['extraAction'] = {
+ color: 'primary',
+ iconType: 'pinFilled',
+ iconSize: 's',
+ className: 'euiPinnableListGroup__itemExtraAction',
+};
+
+const pinnedExtraAction: EuiListGroupItemProps['extraAction'] = {
+ color: 'primary',
+ iconType: 'pinFilled',
+ iconSize: 's',
+ className:
+ 'euiPinnableListGroup__itemExtraAction euiPinnableListGroup__itemExtraAction-pinned',
+ alwaysShow: true,
+};
+
+export type EuiPinnableListGroupItemProps = EuiListGroupItemProps & {
+ /**
+ * Saves the pinned status and changes the visibility of the pin icon
+ */
+ pinned?: boolean;
+ /**
+ * Passing `onPinClick` to the full EuiPinnableListGroup, will make every item pinnable.
+ * Set this property to `false` to turn off individual item pinnability
+ */
+ pinnable?: boolean;
+};
+
+export interface EuiPinnableListGroupProps
+ extends CommonProps,
+ EuiListGroupProps {
+ /**
+ * Extends `EuiListGroupItemProps`, at the very least, expecting a `label`.
+ * See #EuiPinnableListGroupItem
+ */
+ listItems: EuiPinnableListGroupItemProps[];
+ /**
+ * Shows the pin icon and calls this function on click.
+ * Returns `item: EuiPinnableListGroupItemProps`
+ */
+ onPinClick: (item: EuiPinnableListGroupItemProps) => void;
+ /**
+ * The pin icon needs a title/aria-label for accessibility.
+ * It is a function that passes the item back and must return a string `(item) => string`.
+ * Default is `"Pin item"`
+ */
+ pinTitle?: (item: EuiPinnableListGroupItemProps) => string;
+ /**
+ * The unpin icon needs a title/aria-label for accessibility.
+ * It is a function that passes the item back and must return a string `(item) => string`.
+ * Default is `"Unpin item"`
+ */
+ unpinTitle?: (item: EuiPinnableListGroupItemProps) => string;
+}
+
+export const EuiPinnableListGroup: FunctionComponent<
+ EuiPinnableListGroupProps
+> = ({ className, listItems, pinTitle, unpinTitle, onPinClick, ...rest }) => {
+ const classes = classNames('euiPinnableListGroup', className);
+
+ // Alter listItems object with extra props
+ const getNewListItems = (
+ pinExtraActionLabel: string,
+ pinnedExtraActionLabel: string
+ ) =>
+ listItems.map(item => {
+ const { pinned, pinnable = true, ...itemProps } = item;
+ // Make some declarations of props for the nav implementation
+ itemProps.className = classNames(
+ 'euiPinnableListGroup__item',
+ item.className
+ );
+
+ // Add the pinning action unless the item has it's own extra action
+ if (onPinClick && !itemProps.extraAction && pinnable) {
+ // Different displays for pinned vs unpinned
+ if (pinned) {
+ itemProps.extraAction = {
+ ...pinnedExtraAction,
+ title: unpinTitle ? unpinTitle(item) : pinnedExtraActionLabel,
+ 'aria-label': unpinTitle
+ ? unpinTitle(item)
+ : pinnedExtraActionLabel,
+ };
+ } else {
+ itemProps.extraAction = {
+ ...pinExtraAction,
+ title: pinTitle ? pinTitle(item) : pinExtraActionLabel,
+ 'aria-label': pinTitle ? pinTitle(item) : pinExtraActionLabel,
+ };
+ }
+ // Return the item on click
+ itemProps.extraAction.onClick = () => onPinClick(item);
+ }
+
+ return itemProps;
+ });
+
+ return (
+
+ {([pinExtraActionLabel, pinnedExtraActionLabel]: string[]) => {
+ const newListItems = getNewListItems(
+ pinExtraActionLabel,
+ pinnedExtraActionLabel
+ );
+ return (
+
+ );
+ }}
+
+ );
+};
diff --git a/src/components/nav_drawer/__snapshots__/nav_drawer.test.js.snap b/src/components/nav_drawer/__snapshots__/nav_drawer.test.js.snap
index b8b6c6384b4..4c0ce67796d 100644
--- a/src/components/nav_drawer/__snapshots__/nav_drawer.test.js.snap
+++ b/src/components/nav_drawer/__snapshots__/nav_drawer.test.js.snap
@@ -15,7 +15,7 @@ exports[`EuiNavDrawer is rendered 1`] = `
id="navDrawerMenu"
>