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 12a225742cd..37ee299c83f 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js @@ -11,12 +11,17 @@ import { 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); + export const CollapsibleNavExample = { title: 'Collapsible nav', intro: ( @@ -64,6 +69,55 @@ export const CollapsibleNavExample = { ), props: { EuiCollapsibleNav }, demo: , + snippet: ` setNavIsOpen(!navIsOpen)}>Toggle nav +{navIsOpen && ( + 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 + seaction 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: ``, }, ], }; 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/components/collapsible_nav/_index.scss b/src/components/collapsible_nav/_index.scss index 53427be8960..fa7acfa01df 100644 --- a/src/components/collapsible_nav/_index.scss +++ b/src/components/collapsible_nav/_index.scss @@ -1,2 +1,4 @@ @import 'variables'; + +@import 'collapsible_nav_group/index'; @import 'collapsible_nav'; diff --git a/src/components/collapsible_nav/_variables.scss b/src/components/collapsible_nav/_variables.scss index 2378031a40f..f883cd5f8a2 100644 --- a/src/components/collapsible_nav/_variables.scss +++ b/src/components/collapsible_nav/_variables.scss @@ -1,2 +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_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..082ad42be5d --- /dev/null +++ b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap @@ -0,0 +1,252 @@ +// 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`] = ` +
+
+
+
+ +
+

+ Title +

+
+
+
+
+
+`; + +exports[`EuiCollapsibleNavGroup props iconType is rendered 1`] = ` +
+
+
+
+ +
+

+ Title +

+
+
+
+
+
+`; + +exports[`EuiCollapsibleNavGroup props title is rendered 1`] = ` +
+
+
+
+

+ Title +

+
+
+
+
+
+`; + +exports[`EuiCollapsibleNavGroup props titleElement can change the rendered element to h2 1`] = ` +
+
+
+
+

+ Title +

+
+
+
+
+
+`; + +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..b301243a8e3 --- /dev/null +++ b/src/components/collapsible_nav/collapsible_nav_group/_collapsible_nav_group.scss @@ -0,0 +1,47 @@ +.euiCollapsibleNavGroup { + &:not(:first-child) { + border-top: $euiBorderThin; + } +} + +.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; + } +} + +.euiCollapsibleNavGroup__heading { + padding: $euiSize; + font-weight: $euiFontWeightSemiBold; +} + +.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..836244a83bc --- /dev/null +++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx @@ -0,0 +1,161 @@ +import React, { FunctionComponent, ReactNode, useState } 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 & + Omit & { + /** + * If `true`, wraps children in the body of an accordion, + * requiring the prop `title` to be used as the button + */ + isCollapsible: true; + /** + * The title gets wrapped in the appropriate heading level + * with the option to add an iconType + */ + title: ReactNode; + }; + +type GroupAsDiv = EuiCollapsibleNavGroupInterface & { + /** + * 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; +}; + +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 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 index 4954e5ade02..846e72f558f 100644 --- a/src/components/collapsible_nav/index.ts +++ b/src/components/collapsible_nav/index.ts @@ -1 +1,6 @@ -export { EuiCollapsibleNav } from './collapsible_nav'; +export { + EuiCollapsibleNavGroup, + EuiCollapsibleNavGroupProps, +} from './collapsible_nav_group'; + +export { EuiCollapsibleNav, EuiCollapsibleNavProps } from './collapsible_nav'; diff --git a/src/components/index.js b/src/components/index.js index 62f71dcd23e..00b11be0cdc 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -37,7 +37,7 @@ export { EuiCode, EuiCodeBlock, EuiCodeBlockImpl } from './code'; export { EuiCodeEditor } from './code_editor'; -export { EuiCollapsibleNav } from './collapsible_nav'; +export { EuiCollapsibleNavGroup, EuiCollapsibleNav } from './collapsible_nav'; export { EuiColorPicker, diff --git a/src/global_styling/utility/_animations.scss b/src/global_styling/utility/_animations.scss index f5bedf92568..c0cb6e7365a 100644 --- a/src/global_styling/utility/_animations.scss +++ b/src/global_styling/utility/_animations.scss @@ -28,7 +28,7 @@ @keyframes focusRingAnimate { 0% { - box-shadow: 0 0 0 6px $euiFocusRingAnimStartColor; + box-shadow: 0 0 0 $euiFocusRingAnimStartSize $euiFocusRingAnimStartColor; } 100% { @@ -38,7 +38,7 @@ @keyframes focusRingAnimateLarge { 0% { - box-shadow: 0 0 0 10px $euiFocusRingAnimStartColor; + box-shadow: 0 0 0 $euiFocusRingAnimStartSizeLarge $euiFocusRingAnimStartColor; } 100% { diff --git a/src/global_styling/variables/_states.scss b/src/global_styling/variables/_states.scss index 42f750130a7..5152c3eef2e 100644 --- a/src/global_styling/variables/_states.scss +++ b/src/global_styling/variables/_states.scss @@ -1,6 +1,8 @@ // Colors $euiFocusRingColor: rgba($euiColorPrimary, .3) !default; $euiFocusRingAnimStartColor: rgba($euiColorPrimary, 0) !default; +$euiFocusRingAnimStartSize: 6px !default; +$euiFocusRingAnimStartSizeLarge: 10px !default; // Sizing $euiFocusRingSizeLarge: $euiSizeXS !default;