From b432d11e34eafeb08e76f2bb4e5fcacbf7e1ff36 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Wed, 25 Mar 2020 14:40:05 -0400 Subject: [PATCH] [New Nav Feature] Final docs examples and patterns (#3117) * Fixed the passing of `size` from EuiListGroup to items * Fix padding of `EuiCollapsibleNavGroup` if extra action is passed * Reset line-height of heading in button * Fix `title` type for EuiCollapsibleNavGroup * Starting full pattern example * Adjusted EuiFlyout position based on fixed EuiHeader * Utility CSS helper for simple overflow scroll without shadows * Adding GuideFullScreen component * Adding content and storing states * Fixing incompatible type with `href` * Fix EuiHorizontalSizing when in flex groups * Cleanup * Quick fix to nav heading * Using subdued text color * Ghost button in dark section for now * Some browser fixes * Fixes for mobile - Including the addition of the EuiCollapsibleNavToggle component * render prop pattern * clean up * Adding accessibility (?) * One more a11y piece * Addressing some a11y concerns - Focus state for accordions without arrow toggles (underline) - Added link name in pin/unpin titles * More a11y fixes --- src-docs/src/components/guide_components.scss | 8 + .../src/services/full_screen/full_screen.tsx | 32 ++ .../views/collapsible_nav/collapsible_nav.tsx | 11 +- .../collapsible_nav/collapsible_nav_all.tsx | 280 ++++++++++++++++++ .../collapsible_nav_example.js | 98 ++++++ .../collapsible_nav/collapsible_nav_list.tsx | 127 ++++++++ .../__snapshots__/accordion.test.tsx.snap | 34 ++- src/components/accordion/_accordion.scss | 3 +- src/components/accordion/accordion.tsx | 8 +- .../collapsible_nav.test.tsx.snap | 2 + .../collapsible_nav_toggle.test.tsx.snap | 13 + .../collapsible_nav/_collapsible_nav.scss | 5 + .../collapsible_nav/collapsible_nav.tsx | 2 +- .../collapsible_nav_group.test.tsx.snap | 9 +- .../_collapsible_nav_group.scss | 14 +- .../collapsible_nav_group.tsx | 16 +- .../collapsible_nav_toggle.test.tsx | 25 ++ .../collapsible_nav_toggle.tsx | 29 ++ src/components/collapsible_nav/index.ts | 5 + src/components/flyout/_flyout.scss | 8 + src/components/header/_variables.scss | 3 +- .../horizontal_rule/_horizontal_rule.scss | 6 +- src/components/index.js | 6 +- .../__snapshots__/list_group.test.tsx.snap | 10 +- src/components/list_group/_variables.scss | 2 +- src/components/list_group/list_group.test.tsx | 26 +- src/components/list_group/list_group.tsx | 7 + src/components/list_group/list_group_item.tsx | 2 +- .../pinnable_list_group.test.tsx.snap | 20 +- .../pinnable_list_group.tsx | 1 - src/global_styling/mixins/_helpers.scss | 19 +- src/global_styling/utility/_utility.scss | 19 +- 32 files changed, 794 insertions(+), 56 deletions(-) create mode 100644 src-docs/src/services/full_screen/full_screen.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_list.tsx create mode 100644 src/components/collapsible_nav/__snapshots__/collapsible_nav_toggle.test.tsx.snap create mode 100644 src/components/collapsible_nav/collapsible_nav_toggle.test.tsx create mode 100644 src/components/collapsible_nav/collapsible_nav_toggle.tsx diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss index a37483cf9bc..3950197f7de 100644 --- a/src-docs/src/components/guide_components.scss +++ b/src-docs/src/components/guide_components.scss @@ -182,6 +182,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/services/full_screen/full_screen.tsx b/src-docs/src/services/full_screen/full_screen.tsx new file mode 100644 index 00000000000..b28db7f134c --- /dev/null +++ b/src-docs/src/services/full_screen/full_screen.tsx @@ -0,0 +1,32 @@ +import React, { + useState, + Fragment, + FunctionComponent, + ReactElement, + ReactNode, +} 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); + + 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 index 287eae6e58e..10ad5cec714 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx +++ b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx @@ -11,9 +11,18 @@ export default () => { return ( <> - setNavIsOpen(!navIsOpen)}>Toggle nav + setNavIsOpen(!navIsOpen)} + aria-label="Toggle main navigation" + aria-controls="guideCollapsibleNavExampleNav" + aria-expanded={navIsOpen} + aria-pressed={navIsOpen}> + Toggle nav + {navIsOpen && ( setNavIsOpen(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..1f378e0be34 --- /dev/null +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx @@ -0,0 +1,280 @@ +import React, { useState } from 'react'; +import _ from 'lodash'; + +import { + EuiCollapsibleNav, + EuiCollapsibleNavToggle, + 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 = [ + { label: 'Home', iconType: 'home', isActive: true, 'aria-current': true }, +]; +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 leftSectionItems = [ + + setNavIsOpen(!navIsOpen)}> + + , + Elastic, + ]; + + return ( + + {setIsFullScreen => ( + + setIsFullScreen(false)}> + Exit full screen + , + ], + }, + ]} + /> + + {navIsOpen && ( + 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'} + /> + + + + + )} + + + + )} + + ); +}; diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js index 37ee299c83f..e94d31b86c0 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js @@ -22,6 +22,14 @@ 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: ( @@ -119,5 +127,95 @@ export const CollapsibleNavExample = { background="none" />`, }, + { + 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. +

+

EuiCollapsibleNavToggle

+

+ This example also introduces the{' '} + EuiCollapsibleNavToggle component, which is used to + simply wrap around your external nav trigger to show/hide in certain + docked and mobile states. +

+ + ), + demo: , + }, ], }; 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..dda87842154 --- /dev/null +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_list.tsx @@ -0,0 +1,127 @@ +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, + }, + { 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="subdued" + gutterSize="none" + size="s" + /> + + + {}} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + /> + + {SecurityGroup} + +); 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 index 1af87fb7989..81f4736c38b 100644 --- a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap @@ -17,6 +17,7 @@ exports[`EuiCollapsibleNav can be docked 1`] = ` >