diff --git a/CHANGELOG.md b/CHANGELOG.md index 48519e5aa7a..475f805fc0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added `footerLink` and `showToolTips` to `EuiNavDrawer` and added `EuiNavDrawerGroup` ([#1701](https://github.com/elastic/eui/pull/1701)) + **Bug fixes** - Fixed `EuiSuperDatePicker` time selection jumping on focus ([#1704](https://github.com/elastic/eui/pull/1704)) @@ -14,7 +16,6 @@ - Adjusted the dark theme palette a bit more and adjusted a few components ([#1700](https://github.com/elastic/eui/pull/1700)) - ## [`9.1.0`](https://github.com/elastic/eui/tree/v9.1.0) - Adjusted the dark theme palette to have a slight blue tint ([#1691](https://github.com/elastic/eui/pull/1691)) diff --git a/src-docs/src/views/list_group/list_group.js b/src-docs/src/views/list_group/list_group.js index 4112799a0d0..0f596fe16d7 100644 --- a/src-docs/src/views/list_group/list_group.js +++ b/src-docs/src/views/list_group/list_group.js @@ -55,7 +55,7 @@ export default class extends Component { - + diff --git a/src-docs/src/views/nav_drawer/nav_drawer.js b/src-docs/src/views/nav_drawer/nav_drawer.js index af13f8a9121..d043bf1f906 100644 --- a/src-docs/src/views/nav_drawer/nav_drawer.js +++ b/src-docs/src/views/nav_drawer/nav_drawer.js @@ -17,16 +17,16 @@ import { EuiHeaderLogo, EuiIcon, EuiTitle, + EuiNavDrawerGroup, EuiNavDrawer, - EuiNavDrawerMenu, - EuiNavDrawerFlyout, - EuiListGroup, EuiHorizontalRule, EuiShowFor, - EuiHideFor, - EuiOutsideClickDetector, + EuiFocusTrap, + EuiButton } from '../../../../src/components'; +import { keyCodes } from '../../../../src/services'; + import HeaderUserMenu from '../header/header_user_menu'; import HeaderSpacesMenu from '../header/header_spaces_menu'; @@ -35,48 +35,84 @@ export default class extends Component { super(props); this.state = { - isCollapsed: true, - flyoutIsCollapsed: true, - flyoutIsAnimating: false, - navFlyoutTitle: undefined, - navFlyoutContent: [], - mobileIsHidden: true, - showScrollbar: false, - outsideClickDisabled: true, - isManagingFocus: false, + isFullScreen: false, }; this.topLinks = [ { label: 'Recently viewed', iconType: 'clock', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Recently viewed items', - onClick: () => this.expandFlyout(this.recentLinks, 'Recent items'), - extraAction: { - color: 'subdued', - iconType: 'arrowRight', - iconSize: 's', - 'aria-label': 'Expand to view recent apps and objects', - onClick: () => this.expandFlyout(this.recentLinks, 'Recent items'), - alwaysShow: true, + flyoutMenu: { + title: 'Recent items', + listItems: [ + { + label: 'My dashboard', + href: '/#/layout/nav-drawer', + iconType: 'dashboardApp', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + { + label: 'Workpad with title that wraps', + href: '/#/layout/nav-drawer', + iconType: 'canvasApp', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + { + label: 'My logs', + href: '/#/layout/nav-drawer', + iconType: 'loggingApp', + 'aria-label': 'This is an alternate aria-label', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + ], }, }, { label: 'Favorites', iconType: 'starEmpty', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Favorited items', - onClick: () => this.expandFlyout(this.favoriteLinks, 'Favorite items'), - extraAction: { - color: 'subdued', - iconType: 'arrowRight', - iconSize: 's', - 'aria-label': 'Expand to view favorited apps and objects', - onClick: () => this.expandFlyout(this.favoriteLinks, 'Favorite items'), - alwaysShow: true, + flyoutMenu: { + title: 'Favorite items', + listItems: [ + { + label: 'My workpad', + href: '/#/layout/nav-drawer', + iconType: 'canvasApp', + extraAction: { + color: 'subdued', + iconType: 'starFilled', + iconSize: 's', + 'aria-label': 'Add to favorites', + alwaysShow: true, + }, + }, + { + label: 'My logs', + href: '/#/layout/nav-drawer', + iconType: 'loggingApp', + extraAction: { + color: 'subdued', + iconType: 'starFilled', + iconSize: 's', + 'aria-label': 'Add to favorites', + alwaysShow: true, + }, + }, + ], }, }, ]; @@ -86,9 +122,6 @@ export default class extends Component { label: 'Canvas', href: '/#/layout/nav-drawer', iconType: 'canvasApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Canvas', isActive: true, extraAction: { color: 'subdued', @@ -102,9 +135,6 @@ export default class extends Component { label: 'Discover', href: '/#/layout/nav-drawer', iconType: 'discoverApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Discover', extraAction: { color: 'subdued', iconType: 'pin', @@ -116,9 +146,6 @@ export default class extends Component { label: 'Visualize', href: '/#/layout/nav-drawer', iconType: 'visualizeApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Visualize', extraAction: { color: 'subdued', iconType: 'pin', @@ -130,9 +157,6 @@ export default class extends Component { label: 'Dashboard', href: '/#/layout/nav-drawer', iconType: 'dashboardApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Dashboard', extraAction: { color: 'subdued', iconType: 'pin', @@ -144,9 +168,6 @@ export default class extends Component { label: 'Machine learning', href: '/#/layout/nav-drawer', iconType: 'machineLearningApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Machine learning', extraAction: { color: 'subdued', iconType: 'pin', @@ -158,9 +179,6 @@ export default class extends Component { label: 'Graph', href: '/#/layout/nav-drawer', iconType: 'graphApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Graph', extraAction: { color: 'subdued', iconType: 'pin', @@ -175,9 +193,6 @@ export default class extends Component { label: 'APM', href: '/#/layout/nav-drawer', iconType: 'apmApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'APM', extraAction: { color: 'subdued', iconType: 'pin', @@ -189,9 +204,6 @@ export default class extends Component { label: 'Infrastructure', href: '/#/layout/nav-drawer', iconType: 'infraApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Infra', extraAction: { color: 'subdued', iconType: 'pin', @@ -203,9 +215,6 @@ export default class extends Component { label: 'Log viewer', href: '/#/layout/nav-drawer', iconType: 'loggingApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Logs', extraAction: { color: 'subdued', iconType: 'pin', @@ -217,9 +226,6 @@ export default class extends Component { label: 'Uptime', href: '/#/layout/nav-drawer', iconType: 'upgradeAssistantApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Graph', extraAction: { color: 'subdued', iconType: 'pin', @@ -231,9 +237,6 @@ export default class extends Component { label: 'Maps', href: '/#/layout/nav-drawer', iconType: 'gisApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Maps', extraAction: { color: 'subdued', iconType: 'pin', @@ -245,9 +248,6 @@ export default class extends Component { label: 'SIEM', href: '/#/layout/nav-drawer', iconType: 'securityAnalyticsApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'SIEM', extraAction: { color: 'subdued', iconType: 'pin', @@ -261,144 +261,67 @@ export default class extends Component { { label: 'Admin', iconType: 'managementApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Admin', - onClick: () => this.expandFlyout(this.adminSubLinks, 'Tools and settings'), - extraAction: { - color: 'subdued', - iconType: 'arrowRight', - iconSize: 's', - 'aria-label': 'Pin to top', - alwaysShow: true, - onClick: () => this.expandFlyout(this.adminSubLinks, 'Tools and settings'), + flyoutMenu: { + title: 'Tools and settings', + listItems: [ + { + label: 'Dev tools', + href: '/#/layout/nav-drawer', + iconType: 'devToolsApp', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + { + label: 'Stack Monitoring', + href: '/#/layout/nav-drawer', + iconType: 'monitoringApp', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + { + label: 'Stack Management', + href: '/#/layout/nav-drawer', + iconType: 'managementApp', + extraAction: { + color: 'subdued', + iconType: 'starEmpty', + iconSize: 's', + 'aria-label': 'Add to favorites', + }, + }, + ] }, }, ]; + } - this.adminSubLinks = [ - { - label: 'Dev tools', - href: '/#/layout/nav-drawer', - iconType: 'devToolsApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Dev tools', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - { - label: 'Stack Monitoring', - href: '/#/layout/nav-drawer', - iconType: 'monitoringApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Monitoring', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - { - label: 'Stack Management', - href: '/#/layout/nav-drawer', - iconType: 'managementApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Management', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - ]; + onKeyDown = event => { + if (event.keyCode === keyCodes.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + this.closeFullScreen(); + } + }; - this.recentLinks = [ - { - label: 'My dashboard', - href: '/#/layout/nav-drawer', - iconType: 'dashboardApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'My dashboard', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - { - label: 'Workpad with title that wraps', - href: '/#/layout/nav-drawer', - iconType: 'canvasApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'Workpad with title that wraps', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - { - label: 'My logs', - href: '/#/layout/nav-drawer', - iconType: 'loggingApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'My logs', - extraAction: { - color: 'subdued', - iconType: 'starEmpty', - iconSize: 's', - 'aria-label': 'Add to favorites', - }, - }, - ]; + toggleFullScreen = () => { + this.setState(prevState => ({ + isFullScreen: !prevState.isFullScreen, + })); + }; - this.favoriteLinks = [ - { - label: 'My workpad', - href: '/#/layout/nav-drawer', - iconType: 'canvasApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'My workpad', - extraAction: { - color: 'subdued', - iconType: 'starFilled', - iconSize: 's', - 'aria-label': 'Add to favorites', - alwaysShow: true, - }, - }, - { - label: 'My logs', - href: '/#/layout/nav-drawer', - iconType: 'loggingApp', - size: 's', - style: { color: 'inherit' }, - 'aria-label': 'My logs', - extraAction: { - color: 'subdued', - iconType: 'starFilled', - iconSize: 's', - 'aria-label': 'Add to favorites', - alwaysShow: true, - }, - }, - ]; - } + closeFullScreen = () => { + this.setState({ + isFullScreen: false, + }); + }; renderLogo() { return ( @@ -414,7 +337,7 @@ export default class extends Component { return ( this.navDrawerRef.toggleOpen()} > @@ -469,181 +392,49 @@ export default class extends Component { ); } - timeoutID; - - toggleOpen = () => { - this.setState({ - mobileIsHidden: !this.state.mobileIsHidden - }); - - setTimeout(() => { - this.setState({ - outsideClickDisabled: this.state.mobileIsHidden ? true : false, - }); - }, 350); - }; - - expandDrawer = () => { - this.setState({ isCollapsed: false }); - - setTimeout(() => { - this.setState({ - showScrollbar: true, - }); - }, 350); - - // This prevents the drawer from collapsing when tabbing through children - // by clearing the timeout thus cancelling the onBlur event (see focusOut). - // This means isManagingFocus remains true as long as a child element - // has focus. This is the case since React bubbles up onFocus and onBlur - // events from the child elements. - clearTimeout(this.timeoutID); - - if (!this.state.isManagingFocus) { - this.setState({ - isManagingFocus: true, - }); - } - }; - - collapseDrawer = () => { - this.setState({ - flyoutIsAnimating: false, - }); - - setTimeout(() => { - this.setState({ - isCollapsed: true, - flyoutIsCollapsed: true, - mobileIsHidden: true, - showScrollbar: false, - outsideClickDisabled: true, - }); - }, 350); - - // Scrolls the menu and flyout back to top when the nav drawer collapses - setTimeout(() => { - document.getElementById('navDrawerMenu').scrollTop = 0; - document.getElementById('navDrawerFlyout').scrollTop = 0; - }, 300); - }; - - focusOut = () => { - // This collapses the drawer when no children have focus (i.e. tabbed out). - // In other words, if focus does not bubble up from a child element, then - // the drawer will collapse. See the corresponding block in expandDrawer - // (called by onFocus) which cancels this operation via clearTimeout. - this.timeoutID = setTimeout(() => { - if (this.state.isManagingFocus) { - this.setState({ - isManagingFocus: false, - }); - - this.collapseDrawer(); - } - }, 0); - } - - expandFlyout = (links, title) => { - const content = links; - - this.setState(prevState => ({ - flyoutIsCollapsed: prevState.navFlyoutTitle === title ? !this.state.flyoutIsCollapsed : false, - })); - - this.setState({ - flyoutIsAnimating: true, - navFlyoutTitle: title, - navFlyoutContent: content - }); - }; - - collapseFlyout = () => { - this.setState({ flyoutIsAnimating: true }); - - setTimeout(() => { - this.setState({ - flyoutIsCollapsed: true, - navFlyoutTitle: null, - navFlyoutContent: null - }); - }, 250); - }; + setNavDrawerRef = ref => this.navDrawerRef = ref; render() { - const { - isCollapsed, - flyoutIsCollapsed, - flyoutIsAnimating, - navFlyoutTitle, - navFlyoutContent, - mobileIsHidden, - showScrollbar, - outsideClickDisabled, - } = this.state; - return ( - -
- - - + let fullScreenDisplay; + + if (this.state.isFullScreen) { + + fullScreenDisplay = ( + +
+ + + + + {this.renderMenuTrigger()} + + + {this.renderLogo()} - {this.renderMenuTrigger()} + - - {this.renderLogo()} - - - - + - {this.renderBreadcrumbs()} + {this.renderBreadcrumbs()} - - - - - - - this.collapseDrawer()} - isDisabled={outsideClickDisabled} - > - - - - - - - - - - - + + + + + + + + + + + + + + - - - - + + @@ -660,13 +451,38 @@ export default class extends Component { - Body content + + Exit fullscreen demo + - - - -
+ + +
+ + ); + } + return ( + + + Show fullscreen demo + + + {/* + If the below fullScreen code renders, it actually attaches to the body because of + EuiOverlayMask's React portal usage. + */} + + {fullScreenDisplay} ); } diff --git a/src-docs/src/views/nav_drawer/nav_drawer_example.js b/src-docs/src/views/nav_drawer/nav_drawer_example.js index 0cfd4a5f95f..ee6f93d4273 100644 --- a/src-docs/src/views/nav_drawer/nav_drawer_example.js +++ b/src-docs/src/views/nav_drawer/nav_drawer_example.js @@ -9,11 +9,15 @@ import { import { EuiNavDrawer, EuiCode, + EuiCallOut, } from '../../../../src/components'; import NavDrawer from './nav_drawer'; const navDrawerSource = require('!!raw-loader!./nav_drawer'); const navDrawerHtml = renderToHtml(NavDrawer); +const navDrawerSnippet = ` + +`; export const NavDrawerExample = { title: 'Nav Drawer', @@ -26,13 +30,28 @@ export const NavDrawerExample = { code: navDrawerHtml, }], text: ( -

- EuiNavDrawer provides a side navigation feature that - is complete with interactions and a mobile-friendly design. It can - contain one or more EuiListGroup components and is - designed to be used in conjunction with EuiHeader. -

+
+

+ EuiNavDrawer provides a side navigation feature that + is complete with interactions and a mobile-friendly design. It can + contain one or more EuiNavDrawerGroup components and is + designed to be used in conjunction with EuiHeader. +

+ +

+ Providing a flyoutMenu prop on + the listItems object of + an EuiNavDrawerGroup will result in that link + opening a secondary menu. Note that this will also override + the onClick event. See sample data in the Demo JS tab. +

+
+
), + snippet: navDrawerSnippet, props: { EuiNavDrawer, }, diff --git a/src/components/index.js b/src/components/index.js index beced0b814d..3f718c299bd 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -235,7 +235,7 @@ export { export { EuiNavDrawer, - EuiNavDrawerMenu, + EuiNavDrawerGroup, EuiNavDrawerFlyout, } from './nav_drawer'; diff --git a/src/components/list_group/__snapshots__/list_group_item.test.js.snap b/src/components/list_group/__snapshots__/list_group_item.test.js.snap index 1b34c68b527..edcd1a84d0e 100644 --- a/src/components/list_group/__snapshots__/list_group_item.test.js.snap +++ b/src/components/list_group/__snapshots__/list_group_item.test.js.snap @@ -9,6 +9,7 @@ exports[`EuiListGroupItem is rendered 1`] = ` > Label @@ -26,6 +27,7 @@ exports[`EuiListGroupItem renders href 1`] = ` > Label diff --git a/src/components/list_group/_index.scss b/src/components/list_group/_index.scss index ab5c9e1dd4c..9d6423c4f21 100644 --- a/src/components/list_group/_index.scss +++ b/src/components/list_group/_index.scss @@ -1,4 +1,5 @@ // List group provides a way to neatly present a set of text-based items +@import '../header/variables'; @import 'list_group'; @import 'list_group_item'; diff --git a/src/components/list_group/_list_group_item.scss b/src/components/list_group/_list_group_item.scss index 0c9f7a5b299..d69173a23b7 100644 --- a/src/components/list_group/_list_group_item.scss +++ b/src/components/list_group/_list_group_item.scss @@ -1,6 +1,6 @@ .euiListGroupItem { padding: 0; - margin-top: $euiSizeXS; + margin-top: $euiSizeS; border-radius: $euiBorderRadius; overflow: hidden; display: flex; @@ -38,7 +38,7 @@ .euiListGroupItem__text, .euiListGroupItem__button { - padding: $euiSizeM $euiSizeS; + padding: $euiSizeS; display: flex; align-items: center; flex: 1 0 auto; // The flex-shrink and flex-basis values are needed for IE11 @@ -81,17 +81,20 @@ .euiListGroupItem--xSmall { font-size: $euiFontSizeXS; + line-height: $euiSizeM; } .euiListGroupItem--small { font-size: $euiFontSizeS; + line-height: $euiSize; } .euiListGroupItem--large { font-size: $euiFontSizeL; + line-height: $euiSize; } -.euiListGroup-wrapText { +.euiListGroupItem--wrapText { .euiListGroupItem__button, .euiListGroupItem__text { width: 100%; @@ -99,7 +102,7 @@ } .euiListGroupItem__label { - white-space: initial; + white-space: inherit; } } @@ -120,3 +123,7 @@ border-bottom-right-radius: $euiBorderRadius; } } + +.euiListGroupItem__tooltip { + width: 100%; +} diff --git a/src/components/list_group/list_group.js b/src/components/list_group/list_group.js index 299550655c0..5dd88822178 100644 --- a/src/components/list_group/list_group.js +++ b/src/components/list_group/list_group.js @@ -15,6 +15,7 @@ export const EuiListGroup = ({ listItems, maxWidth, style, + showToolTips, ...rest, }) => { @@ -32,7 +33,6 @@ export const EuiListGroup = ({ { 'euiListGroup-flush': flush, 'euiListGroup-bordered': bordered, - 'euiListGroup-wrapText': wrapText, }, widthClassName, className @@ -45,13 +45,23 @@ export const EuiListGroup = ({ return [ ]; }) ); } else { - childrenOrListItems = children; + if (showToolTips) { + childrenOrListItems = React.Children.map(children, child => { + return React.cloneElement(child, { + showToolTip: true + }); + }); + } else { + childrenOrListItems = children; + } } return ( @@ -73,6 +83,7 @@ EuiListGroup.propTypes = { iconType: PropTypes.string, isActive: PropTypes.boolean, isDisabled: PropTypes.boolean, + showToolTip: PropTypes.boolean, })), children: PropTypes.node, className: PropTypes.string, @@ -92,6 +103,11 @@ EuiListGroup.propTypes = { */ wrapText: PropTypes.bool, + /** + * Display tooltips on all list items + */ + showToolTips: PropTypes.bool, + /** * Sets the max-width of the page, * set to `true` to use the default size, @@ -111,4 +127,5 @@ EuiListGroup.defaultProps = { bordered: false, wrapText: false, maxWidth: true, + showToolTips: false, }; diff --git a/src/components/list_group/list_group_item.js b/src/components/list_group/list_group_item.js index 22c871dfe08..6b48c367397 100644 --- a/src/components/list_group/list_group_item.js +++ b/src/components/list_group/list_group_item.js @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { EuiButtonIcon } from '../button'; import { ICON_TYPES, EuiIcon } from '../icon'; +import { EuiToolTip } from '../tool_tip'; const sizeToClassNameMap = { xs: 'euiListGroupItem--xSmall', @@ -24,6 +25,8 @@ export const EuiListGroupItem = ({ extraAction, onClick, size, + showToolTip, + wrapText, ...rest }) => { const classes = classNames( @@ -34,6 +37,7 @@ export const EuiListGroupItem = ({ 'euiListGroupItem-isDisabled': isDisabled, 'euiListGroupItem-isClickable': href || onClick, 'euiListGroupItem-hasExtraAction': extraAction, + 'euiListGroupItem--wrapText': wrapText, }, className ); @@ -65,6 +69,16 @@ export const EuiListGroupItem = ({ ); } + // Only add the label as the title attribute if it's possibly truncated + const labelContent = ( + + {label} + + ); + // Handle the variety of interaction behavior let itemContent; @@ -72,7 +86,7 @@ export const EuiListGroupItem = ({ itemContent = ( {iconNode} - {label} + {labelContent} ); } else if ((href && isDisabled) || onClick) { @@ -84,23 +98,43 @@ export const EuiListGroupItem = ({ {...rest} > {iconNode} - {label} + {labelContent} ); } else { itemContent = ( {iconNode} - {label} + {labelContent} ); } + if (showToolTip) { + itemContent = ( +
  • + + {itemContent} + +
  • + ); + } else { + itemContent = ( +
  • + {itemContent} + {extraActionNode} +
  • + ); + } + return ( -
  • - {itemContent} - {extraActionNode} -
  • + {itemContent} ); }; @@ -137,6 +171,11 @@ EuiListGroupItem.propTypes = { */ iconType: PropTypes.oneOf(ICON_TYPES), + /** + * Display tooltip on list item + */ + showToolTip: PropTypes.bool, + /** * Adds an `EuiButtonIcon` to the right side of the item; `iconType` is required; * pass `alwaysShow` if you don't want the default behavior of only showing on hover @@ -147,10 +186,16 @@ EuiListGroupItem.propTypes = { }), onClick: PropTypes.func, + + /** + * Allow link text to wrap + */ + wrapText: PropTypes.bool, }; EuiListGroupItem.defaultProps = { isActive: false, isDisabled: false, size: 'm', + showToolTip: false, }; diff --git a/src/components/nav_drawer/_index.scss b/src/components/nav_drawer/_index.scss index fc62c67f187..bb973574c19 100644 --- a/src/components/nav_drawer/_index.scss +++ b/src/components/nav_drawer/_index.scss @@ -3,5 +3,5 @@ // Components @import 'nav_drawer'; -@import 'nav_drawer_menu'; @import 'nav_drawer_flyout'; +@import 'nav_drawer_group'; diff --git a/src/components/nav_drawer/_nav_drawer.scss b/src/components/nav_drawer/_nav_drawer.scss index 78004ca67bd..39e9d20fa57 100644 --- a/src/components/nav_drawer/_nav_drawer.scss +++ b/src/components/nav_drawer/_nav_drawer.scss @@ -11,108 +11,109 @@ background: $euiHeaderBackgroundColor; box-shadow: $euiNavDrawerSideShadow; transition: width $euiAnimSpeedExtraFast $euiAnimSlightResistance; - transition-delay: $euiNavDrawerContractingDelay; + display: flex; - &.euiNavDrawer-isCollapsed { - .euiListGroupItem-hasExtraAction .euiListGroupItem__button { - max-width: 100%; + .euiNavDrawerMenu { + @include euiScrollBar; + overflow-y: auto; + width: $euiNavDrawerWidthCollapsed; + height: 100%; + + &-hasFooter { + padding-bottom: $euiSizeXXL; } } - &.euiNavDrawer-isExpanded { - width: $euiNavDrawerWidthExpanded; - transition-delay: $euiNavDrawerExpandingDelay; - - .euiNavDrawerMenu .euiListGroupItem__label { - opacity: 1; - transition-delay: ( - $euiNavDrawerExpandingDelay + $euiNavDrawerMenuAddedDelay - ); + .euiNavDrawer__expandButton { + @include euiBottomShadowFlat; + background-color: $euiColorEmptyShade; + position: fixed; + bottom: 0; + width: $euiNavDrawerWidthCollapsed; + transition: width $euiAnimSpeedExtraFast; + z-index: $euiZHeader + 1; + + .euiListGroupItem__button { + padding: $euiSizeM $euiSize; } + } + &.euiNavDrawer-isCollapsed { &.euiNavDrawer-flyoutIsExpanded { - width: $euiNavDrawerWidthExpanded * 2; + width: $euiNavDrawerWidthCollapsed + $euiNavDrawerWidthExpanded; } - &.euiNavDrawer-flyoutIsAnimating { - transition-delay: 0s; + .euiNavDrawerMenu { + // Prevents scrollbar from overlapping links in collapsed form + // sass-lint:disable-block no-vendor-prefixes + // sass-lint:disable-block no-misspelled-properties + -ms-overflow-style: -ms-autohiding-scrollbar; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0; + height: 0; + } + + .euiListGroup:not(.euiNavDrawer__expandButton) .euiListGroupItem__button { + max-width: $euiSizeXL; + } + + .euiListGroupItem__extraAction { + visibility: hidden; + } } } - .euiNavDrawerMenu .euiListGroupItem__label { - white-space: nowrap; - opacity: 0; - transition: opacity $euiAnimSpeedNormal; - transition-delay: ( - $euiNavDrawerContractingDelay + $euiNavDrawerMenuAddedDelay - ); - } + &.euiNavDrawer-isExpanded { + width: $euiNavDrawerWidthExpanded; - &.euiNavDrawer-showScrollbar .euiNavDrawerMenu, - &.euiNavDrawer-showScrollbar .euiNavDrawerFlyout { - height: 100%; - overflow-y: auto; - // This prevents the scrollbar from overlapping the nav links. Scrollbars still show on hover - // sass-lint:disable no-vendor-prefixes - -ms-overflow-style: -ms-autohiding-scrollbar; - } + .euiNavDrawerMenu, + .euiNavDrawer__expandButton { + width: $euiNavDrawerWidthExpanded; + } - .euiListGroupItem__label { - line-height: $euiSize; + &.euiNavDrawer-flyoutIsExpanded { + width: $euiNavDrawerWidthExpanded + $euiNavDrawerWidthCollapsed; + } } +} - .euiListGroupItem__extraAction { - visibility: hidden; - } +.euiNavDrawerPage { + height: 100%; - &.euiNavDrawer-showScrollbar .euiListGroupItem__extraAction { - visibility: visible; + .euiNavDrawerPage__pageBody { + margin-left: $euiNavDrawerWidthCollapsed; } } @include euiBreakpoint('xs', 's') { .euiNavDrawer { - width: $euiNavDrawerWidthExpanded; - - &.euiNavDrawer-mobileIsHidden { - width: 0; - } - - &.euiNavDrawer-isExpanded.euiNavDrawer-flyoutIsExpanded { - width: $euiNavDrawerWidthCollapsed + $euiNavDrawerWidthExpanded; + width: 0; - .euiNavDrawerFlyout { - left: $euiNavDrawerWidthCollapsed; - } - } - - &.euiNavDrawer-flyoutIsExpanded { - .euiNavDrawerMenu { - width: $euiNavDrawerWidthCollapsed; - overflow-y: hidden; - - .euiListGroupItem__extraAction { - visibility: hidden; - } - } - - .euiNavDrawerMenu .euiListGroupItem__label { - display: none; + &.euiNavDrawer-isExpanded .euiNavDrawerMenu { + .euiListGroupItem__icon { + margin-right: $euiSizeM; } } &.euiNavDrawer-flyoutIsCollapsed .euiNavDrawerFlyout { width: 0; - transition-delay: 0s; transition-duration: 0s; } - .euiNavDrawerMenu .euiListGroupItem__label { - opacity: 1; + // No expand toggle on mobile + + .euiNavDrawerMenu-hasFooter { + padding-bottom: 0; } - .euiListGroupItem__extraAction { - visibility: visible; + .euiNavDrawer__expandButton { + display: none; } } + + .euiNavDrawerPage .euiNavDrawerPage__pageBody { + margin-left: 0; + } } diff --git a/src/components/nav_drawer/_nav_drawer_flyout.scss b/src/components/nav_drawer/_nav_drawer_flyout.scss index 789ecbd3457..c0e64954df0 100644 --- a/src/components/nav_drawer/_nav_drawer_flyout.scss +++ b/src/components/nav_drawer/_nav_drawer_flyout.scss @@ -1,23 +1,28 @@ .euiNavDrawerFlyout { @include euiScrollBar; - position: absolute; - left: $euiNavDrawerWidthExpanded; - top: 0; - width: $euiNavDrawerWidthExpanded; + width: 0; height: 100%; - padding: $euiSize; + padding: $euiSizeM $euiSizeS; + overflow-y: auto; background-color: $euiNavDrawerBackgroundColor; border-left: $euiBorderThin; box-shadow: $euiNavDrawerSideShadow; + visibility: hidden; opacity: 0; &.euiNavDrawerFlyout-isExpanded { + visibility: visible; opacity: 1; - transition: opacity $euiAnimSpeedNormal; + width: $euiNavDrawerWidthExpanded; + transition: opacity $euiAnimSpeedFast $euiNavDrawerContractingDelay, width $euiAnimSpeedNormal; } &.euiNavDrawerFlyout-isCollapsed { - transition: opacity $euiAnimSpeedFast $euiNavDrawerContractingDelay; + transition: opacity $euiAnimSpeedFast, width $euiAnimSpeedFast; + } + + .euiNavDrawerFlyout__title { + margin: 0 $euiSizeS $euiSizeXS; } .euiNavDrawerFlyout__listGroup { diff --git a/src/components/nav_drawer/_nav_drawer_group.scss b/src/components/nav_drawer/_nav_drawer_group.scss new file mode 100644 index 00000000000..dc066e2aa9d --- /dev/null +++ b/src/components/nav_drawer/_nav_drawer_group.scss @@ -0,0 +1,14 @@ +.euiNavDrawerGroup__item { + .euiListGroupItem__label { + transition: all $euiAnimSpeedExtraFast; + } + + .euiListGroupItem__button { + color: inherit; // Force color to inherit from regular text color in case it's an anchor tag + + &:focus { + background-color: $euiFocusBackgroundColor; + border-radius: $euiBorderRadius; + } + } +} diff --git a/src/components/nav_drawer/_nav_drawer_menu.scss b/src/components/nav_drawer/_nav_drawer_menu.scss deleted file mode 100644 index 2ef3f729783..00000000000 --- a/src/components/nav_drawer/_nav_drawer_menu.scss +++ /dev/null @@ -1,4 +0,0 @@ -.euiNavDrawerMenu { - @include euiScrollBar; - max-width: $euiNavDrawerWidthExpanded; -} \ No newline at end of file diff --git a/src/components/nav_drawer/index.js b/src/components/nav_drawer/index.js index d67776d58f9..26722e4880f 100644 --- a/src/components/nav_drawer/index.js +++ b/src/components/nav_drawer/index.js @@ -3,8 +3,8 @@ export { } from './nav_drawer'; export { - EuiNavDrawerMenu, -} from './nav_drawer_menu'; + EuiNavDrawerGroup, +} from './nav_drawer_group'; export { EuiNavDrawerFlyout, diff --git a/src/components/nav_drawer/nav_drawer.js b/src/components/nav_drawer/nav_drawer.js index 03991ae15ef..1465bac1b4f 100644 --- a/src/components/nav_drawer/nav_drawer.js +++ b/src/components/nav_drawer/nav_drawer.js @@ -1,63 +1,250 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { EuiListGroup, EuiListGroupItem } from '../list_group'; +import { EuiNavDrawerFlyout } from './nav_drawer_flyout'; +import { EuiOutsideClickDetector } from '../outside_click_detector'; +import { EuiI18n } from '../i18n'; +import { EuiFlexItem } from '../flex'; -export const EuiNavDrawer = ({ - children, - className, - isCollapsed, - flyoutIsCollapsed, - flyoutIsAnimating, - mobileIsHidden, - showScrollbar, - ...rest -}) => { - const classes = classNames( - 'euiNavDrawer', - { - 'euiNavDrawer-isCollapsed': isCollapsed, - 'euiNavDrawer-isExpanded': !isCollapsed, - 'euiNavDrawer-flyoutIsCollapsed': flyoutIsCollapsed, - 'euiNavDrawer-flyoutIsExpanded': !flyoutIsCollapsed, - 'euiNavDrawer-flyoutIsAnimating': flyoutIsAnimating, - 'euiNavDrawer-mobileIsHidden': mobileIsHidden, - 'euiNavDrawer-showScrollbar': showScrollbar, - }, - className - ); - - return ( -
    - {children} -
    - ); -}; + +export class EuiNavDrawer extends Component { + constructor(props) { + super(props); + + this.state = { + isCollapsed: true, + flyoutIsCollapsed: true, + outsideClickDisabled: true, + isManagingFocus: false, + toolTipsEnabled: true, + }; + } + + timeoutID; + + toggleOpen = () => { + this.setState({ + isCollapsed: !this.state.isCollapsed + }); + + setTimeout(() => { + this.setState({ + outsideClickDisabled: this.state.isCollapsed ? true : false, + toolTipsEnabled: this.state.isCollapsed ? true : false, + }); + }, 150); + }; + + expandDrawer = () => { + this.setState({ + isCollapsed: false, + outsideClickDisabled: false, + }); + + setTimeout(() => { + this.setState({ + toolTipsEnabled: false, + }); + }, 150); + }; + + collapseDrawer = () => { + this.setState({ + isCollapsed: true, + outsideClickDisabled: this.state.flyoutIsCollapsed ? true : false, + toolTipsEnabled: true, + }); + + // Scrolls the menu and flyout back to top when the nav drawer collapses + setTimeout(() => { + document.getElementById('navDrawerMenu').scrollTop = 0; + }, 50); + }; + + manageFocus = () => { + // This prevents the drawer from collapsing when tabbing through children + // by clearing the timeout thus cancelling the onBlur event (see focusOut). + // This means isManagingFocus remains true as long as a child element + // has focus. This is the case since React bubbles up onFocus and onBlur + // events from the child elements. + clearTimeout(this.timeoutID); + + if (!this.state.isManagingFocus) { + this.setState({ + isManagingFocus: true, + }); + } + } + + focusOut = () => { + // This collapses the drawer when no children have focus (i.e. tabbed out). + // In other words, if focus does not bubble up from a child element, then + // the drawer will collapse. See the corresponding block in expandDrawer + // (called by onFocus) which cancels this operation via clearTimeout. + this.timeoutID = setTimeout(() => { + if (this.state.isManagingFocus) { + this.setState({ + isManagingFocus: false, + }); + + this.closeBoth(); + } + }, 0); + } + + expandFlyout = (links, title) => { + const content = links; + + if (this.state.navFlyoutTitle === title) { + this.collapseFlyout(); + } else { + this.setState({ + flyoutIsCollapsed: false, + navFlyoutTitle: title, + navFlyoutContent: content, + isCollapsed: true, + toolTipsEnabled: false, + outsideClickDisabled: false, + }); + } + }; + + collapseFlyout = () => { + this.setState({ + flyoutIsCollapsed: true, + navFlyoutTitle: null, + navFlyoutContent: null, + toolTipsEnabled: true, + }); + }; + + closeBoth = () => { + this.collapseDrawer(); + this.collapseFlyout(); + } + + render() { + const { + children, + className, + showExpandButton, + showToolTips, + ...rest + } = this.props; + + const classes = classNames( + 'euiNavDrawer', + { + 'euiNavDrawer-isCollapsed': this.state.isCollapsed, + 'euiNavDrawer-isExpanded': !this.state.isCollapsed, + 'euiNavDrawer-flyoutIsCollapsed': this.state.flyoutIsCollapsed, + 'euiNavDrawer-flyoutIsExpanded': !this.state.flyoutIsCollapsed, + }, + className + ); + + let footerContent; + if (showExpandButton) { + footerContent = ( + + + {([sideNavCollapse, sideNavExpand]) => ( + {this.expandDrawer(); this.collapseFlyout();} : () => this.collapseDrawer()} + data-test-subj={this.state.isCollapsed ? 'navDrawerExpandButton-isCollapsed' : 'navDrawerExpandButton-isExpanded'} + /> + )} + + + ); + } + + const flyoutContent = ( + + ); + + // Add an onClick that expands the flyout sub menu for any list items (links) + // that have a flyoutMenu prop (sub links) + let modifiedChildren = children; + + // 1. Loop through the EuiNavDrawer children (EuiListGroup, EuiHorizontalRules, etc) + modifiedChildren = React.Children.map(this.props.children, child => { + // 2. Check if child is an EuiNavDrawerGroup and if it does have a flyout, add the expand function + if (child.type.name === 'EuiNavDrawerGroup') { + const item = React.cloneElement(child, { + flyoutMenuButtonClick: this.expandFlyout, + showToolTips: this.state.toolTipsEnabled && showToolTips, + }); + return item; + } else { + return child; + } + }); + + const menuClasses = classNames( + 'euiNavDrawerMenu', { 'euiNavDrawerMenu-hasFooter': footerContent, }, + ); + + return ( + this.closeBoth()} + isDisabled={this.state.outsideClickDisabled} + > +
    + + + + + {flyoutContent} +
    +
    + ); + } +} EuiNavDrawer.propTypes = { + children: PropTypes.node, className: PropTypes.string, /** - * Toggle the nav drawer between collapsed (docked) and expanded + * Adds fixed toggle button to bottom of menu area */ - isCollapsed: PropTypes.bool, - mobileIsHidden: PropTypes.bool, + showExpandButton: PropTypes.bool, /** - * Toggle the flyout menu between collapsed and expanded + * Display tooltips on side nav items */ - flyoutIsCollapsed: PropTypes.bool, - flyoutIsAnimating: PropTypes.bool, - - showScrollbar: PropTypes.bool, + showToolTips: PropTypes.bool, }; EuiNavDrawer.defaultProps = { - isCollapsed: true, - mobileIsHidden: true, - flyoutIsCollapsed: true, - flyoutIsAnimating: false, - showScrollbar: false, -}; \ No newline at end of file + showExpandButton: true, + showToolTips: true, +}; diff --git a/src/components/nav_drawer/nav_drawer_flyout.js b/src/components/nav_drawer/nav_drawer_flyout.js index dcdc6327ab4..01b5477aaae 100644 --- a/src/components/nav_drawer/nav_drawer_flyout.js +++ b/src/components/nav_drawer/nav_drawer_flyout.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { EuiTitle } from '../title'; -import { EuiListGroup } from '../list_group'; +import { EuiNavDrawerGroup } from './nav_drawer_group'; +import { EuiListGroup } from '../list_group/list_group'; export const EuiNavDrawerFlyout = ({ className, title, isCollapsed, listItems, wrapText, ...rest }) => { const classes = classNames( @@ -21,8 +22,8 @@ export const EuiNavDrawerFlyout = ({ className, title, isCollapsed, listItems, w aria-labelledby="navDrawerFlyoutTitle" {...rest} > - - + + ); }; @@ -45,4 +46,4 @@ EuiNavDrawerFlyout.propTypes = { EuiNavDrawerFlyout.defaultProps = { isCollapsed: true, -}; \ No newline at end of file +}; diff --git a/src/components/nav_drawer/nav_drawer_group.js b/src/components/nav_drawer/nav_drawer_group.js new file mode 100644 index 00000000000..ae25750c593 --- /dev/null +++ b/src/components/nav_drawer/nav_drawer_group.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { EuiListGroup } from '../list_group/list_group'; + +export const EuiNavDrawerGroup = ({ className, listItems, flyoutMenuButtonClick, ...rest }) => { + const classes = classNames( + 'euiNavDrawerGroup', + className + ); + + const listItemsExists = listItems && !!listItems.length; + + // Alter listItems object with prop flyoutMenu and extra props + const newListItems = !listItemsExists ? undefined : listItems.map((item) => { + // If the flyout menu exists, pass back the list of times and the title with the onClick handler of the item + const { flyoutMenu, ...itemProps } = item; + if (flyoutMenu && flyoutMenuButtonClick) { + const items = [...flyoutMenu.listItems]; + const title = `${flyoutMenu.title}`; + itemProps.onClick = () => flyoutMenuButtonClick(items, title); + } + + // Make some declarations of props for the side nav implementation + itemProps.className = classNames('euiNavDrawerGroup__item', item.className); + itemProps.size = item.size || 's'; + itemProps['aria-label'] = item['aria-label'] || item.label; + + // And return the item with conditional `onClick` and without `flyoutMenu` + return { ...itemProps }; + }); + + + return ( + + ); +}; + +EuiNavDrawerGroup.propTypes = { + listItems: PropTypes.arrayOf(PropTypes.shape({ + ...EuiListGroup.propTypes.listItems[0], + flyoutMenu: PropTypes.shape({ + title: PropTypes.string.isRequired, + listItems: EuiListGroup.propTypes.listItems.isRequired, + }), + })), + /** + * While not normally required, it is required to pass a function for handling + * of the flyout menu button click + */ + flyoutMenuButtonClick: PropTypes.func, +}; diff --git a/src/components/nav_drawer/nav_drawer_menu.js b/src/components/nav_drawer/nav_drawer_menu.js deleted file mode 100644 index ac75d0388b5..00000000000 --- a/src/components/nav_drawer/nav_drawer_menu.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export const EuiNavDrawerMenu = ({ children, className, ...rest }) => { - const classes = classNames( - 'euiNavDrawerMenu', - className - ); - - return ( -
    - {children} -
    - ); -}; - -EuiNavDrawerMenu.propTypes = { - className: PropTypes.string, - children: PropTypes.node, -}; \ No newline at end of file