@@ -78,7 +106,11 @@ export const HeaderExample = {
code: headerHtml,
},
],
- text: The header is made up of several individual components.
,
+ text: (
+
+ The header is made up of many individual components.
+
+ ),
props: {
EuiHeader,
EuiHeaderBreadcrumbs,
@@ -86,12 +118,80 @@ export const HeaderExample = {
EuiHeaderSectionItem,
EuiHeaderSectionItemButton,
EuiHeaderLogo,
+ EuiHeaderSectionsProp,
},
snippet: headerSnippet,
demo: ,
},
{
- title: 'Links',
+ title: 'Sections',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: headerSectionsSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: headerSectionsHtml,
+ },
+ ],
+ text: (
+ <>
+
+ Alternatively, you can pass an array objects to the{' '}
+ sections props that takes a key of{' '}
+ items (array of children to wrap in an{' '}
+ EuiHeaderSectionItem ) and/or{' '}
+ breadcrumbs (array of{' '}
+ breadcrumb objects). Each
+ item in the array will be wrapped in an{' '}
+ EuiHeaderSection .
+
+
+ Note: Passing sections and{' '}
+ children will disregard the{' '}
+ children as it is not easily interpreted at what
+ location the children should be placed.
+
+ >
+ ),
+ props: {
+ EuiHeader,
+ EuiHeaderSectionsProp,
+ EuiHeaderSection,
+ EuiHeaderSectionItem,
+ },
+ snippet: headerSectionsSnippet,
+ demo: ,
+ },
+ {
+ title: 'Fixed header',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: headerPositionSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: headerPositionHtml,
+ },
+ ],
+ text: (
+ <>
+
+ Most consumer need a header that does not scroll way with the page
+ contents. You can apply this display by changing{' '}
+ position to fixed . It will
+ also add the appropriate padding to the window body by applying a
+ class.
+
+ >
+ ),
+ snippet: ' ',
+ demo: ,
+ },
+ {
+ title: 'Header links',
source: [
{
type: GuideSectionTypes.JS,
@@ -117,7 +217,7 @@ export const HeaderExample = {
demo: ,
},
{
- title: 'Display header alerts',
+ title: 'Alerts in the header',
source: [
{
type: GuideSectionTypes.JS,
diff --git a/src-docs/src/views/header/header_position.js b/src-docs/src/views/header/header_position.js
new file mode 100644
index 00000000000..9e9d1de9fe9
--- /dev/null
+++ b/src-docs/src/views/header/header_position.js
@@ -0,0 +1,38 @@
+import React, { useState } from 'react';
+
+import {
+ EuiHeader,
+ EuiHeaderLogo,
+ EuiSwitch,
+} from '../../../../src/components';
+
+export default () => {
+ const [position, setPosition] = useState('static');
+
+ const sections = [
+ {
+ items: [
+ ,
+ ],
+ borders: 'none',
+ },
+ {
+ items: [
+
+ setPosition(e.target.checked ? 'fixed' : 'static')}
+ />
+
,
+ ],
+ borders: 'none',
+ },
+ ];
+
+ return ;
+};
diff --git a/src-docs/src/views/header/header_sections.js b/src-docs/src/views/header/header_sections.js
new file mode 100644
index 00000000000..dbe68843a00
--- /dev/null
+++ b/src-docs/src/views/header/header_sections.js
@@ -0,0 +1,82 @@
+import React from 'react';
+
+import {
+ EuiHeader,
+ EuiFieldSearch,
+ EuiHeaderLogo,
+} from '../../../../src/components';
+
+import HeaderAppMenu from './header_app_menu';
+import HeaderUserMenu from './header_user_menu';
+import HeaderSpacesMenu from './header_spaces_menu';
+
+export default () => {
+ const renderLogo = (
+
+ );
+
+ const breadcrumbs = [
+ {
+ text: 'Management',
+ href: '#',
+ onClick: e => {
+ e.preventDefault();
+ console.log('You clicked management');
+ },
+ 'data-test-subj': 'breadcrumbsAnimals',
+ className: 'customClass',
+ },
+ {
+ text: 'Truncation test is here for a really long item',
+ href: '#',
+ onClick: e => {
+ e.preventDefault();
+ console.log('You clicked truncation test');
+ },
+ },
+ {
+ text: 'hidden',
+ href: '#',
+ onClick: e => {
+ e.preventDefault();
+ console.log('You clicked hidden');
+ },
+ },
+ {
+ text: 'Users',
+ href: '#',
+ onClick: e => {
+ e.preventDefault();
+ console.log('You clicked users');
+ },
+ },
+ {
+ text: 'Create',
+ },
+ ];
+
+ const renderSearch = (
+
+ );
+
+ const sections = [
+ {
+ items: [renderLogo, ],
+ borders: 'right',
+ breadcrumbs: breadcrumbs,
+ },
+ {
+ items: [renderSearch,
],
+ borders: 'none',
+ },
+ {
+ items: [ , ],
+ },
+ ];
+
+ return ;
+};
diff --git a/src-docs/src/views/header/props.tsx b/src-docs/src/views/header/props.tsx
new file mode 100644
index 00000000000..4259e7d9842
--- /dev/null
+++ b/src-docs/src/views/header/props.tsx
@@ -0,0 +1,7 @@
+import React, { FunctionComponent } from 'react';
+
+import { EuiHeaderSections } from '../../../../src/components/header';
+
+export const EuiHeaderSectionsProp: FunctionComponent<
+ EuiHeaderSections
+> = () =>
;
diff --git a/src/components/header/__snapshots__/header.test.tsx.snap b/src/components/header/__snapshots__/header.test.tsx.snap
index 2ac2a381dee..3654b8e49a9 100644
--- a/src/components/header/__snapshots__/header.test.tsx.snap
+++ b/src/components/header/__snapshots__/header.test.tsx.snap
@@ -3,17 +3,101 @@
exports[`EuiHeader is rendered 1`] = `
`;
exports[`EuiHeader renders children 1`] = `
`;
+
+exports[`EuiHeader renders in fixed position 1`] = `
+
+`;
+
+exports[`EuiHeader sections render breadcrumbs and props 1`] = `
+
+`;
+
+exports[`EuiHeader sections render simple items and borders 1`] = `
+
+`;
+
+exports[`EuiHeader throws a warning if both children and sections were passed 1`] = `
+
+`;
diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss
index ca9c05b1145..389fb60f949 100644
--- a/src/components/header/_header.scss
+++ b/src/components/header/_header.scss
@@ -6,6 +6,19 @@
position: relative;
z-index: $euiZHeader; // ensure the shadow shows above content
display: flex;
+ justify-content: space-between;
background: $euiHeaderBackgroundColor;
border-bottom: $euiBorderThin;
+
+ &--fixed {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: $euiZLevel7;
+ }
+}
+
+.euiBody--headerIsFixed {
+ padding-top: $euiHeaderChildSize + $euiSizeS; // Extra padding to accound for the shadow
}
diff --git a/src/components/header/_header_logo.scss b/src/components/header/_header_logo.scss
index 1ac01250f81..b8687ef6639 100644
--- a/src/components/header/_header_logo.scss
+++ b/src/components/header/_header_logo.scss
@@ -12,6 +12,10 @@
vertical-align: middle;
white-space: nowrap;
+ &:hover {
+ background: $euiColorLightestShade;
+ }
+
&:focus,
&:hover {
text-decoration: none;
diff --git a/src/components/header/header.test.tsx b/src/components/header/header.test.tsx
index 5a36ac03da4..8be9d11b706 100644
--- a/src/components/header/header.test.tsx
+++ b/src/components/header/header.test.tsx
@@ -20,4 +20,84 @@ describe('EuiHeader', () => {
expect(component).toMatchSnapshot();
});
+
+ test('renders in fixed position', () => {
+ const component = render(
+
+ Hello!
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('sections', () => {
+ test('render simple items and borders', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('render breadcrumbs and props', () => {
+ 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 both children and sections were passed', () => {
+ const component = render(
+
+ Child
+
+ );
+
+ expect(consoleStub).toBeCalled();
+ expect(consoleStub.mock.calls[0][0]).toMatch(
+ 'cannot accept both `children` and `sections`'
+ );
+ expect(component).toMatchSnapshot();
+ });
+ });
});
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
index 1c59a263e1d..2da6d3eeb3c 100644
--- a/src/components/header/header.tsx
+++ b/src/components/header/header.tsx
@@ -1,20 +1,124 @@
-import React, { FunctionComponent, HTMLAttributes } from 'react';
+import React, { FunctionComponent, HTMLAttributes, useEffect } from 'react';
import classNames from 'classnames';
-
import { CommonProps } from '../common';
-export type EuiHeaderProps = CommonProps & HTMLAttributes;
+import {
+ EuiHeaderSectionItem,
+ EuiHeaderSectionItemProps,
+ EuiHeaderSection,
+} from './header_section';
+import { EuiHeaderBreadcrumbs } from './header_breadcrumbs';
+import { Breadcrumb, EuiBreadcrumbsProps } from '../breadcrumbs';
+
+type EuiHeaderSectionItemType = EuiHeaderSectionItemProps['children'];
+type EuiHeaderSectionBorderType = EuiHeaderSectionItemProps['border'];
+
+export interface EuiHeaderSections {
+ /**
+ * An arry of items that will be wrapped in a #EuiHeaderSectionItem
+ */
+ items?: EuiHeaderSectionItemType[];
+ /**
+ * Apply the passed border side to each #EuiHeaderSectionItem
+ */
+ borders?: EuiHeaderSectionBorderType;
+ /**
+ * Breadcrumbs in the header cannot be wrapped in a #EuiHeaderSection in order for truncation to work.
+ * Simply pass the array of Breadcrumb objects
+ */
+ breadcrumbs?: Breadcrumb[];
+ /**
+ * Other props to pass to #EuiHeaderBreadcrumbs
+ */
+ breadcrumbProps?: Omit;
+}
+
+function createHeaderSection(
+ sections: EuiHeaderSectionItemType[],
+ border?: EuiHeaderSectionBorderType
+) {
+ return sections.map((section, index) => {
+ return (
+
+ {section}
+
+ );
+ });
+}
+
+export type EuiHeaderProps = CommonProps &
+ HTMLAttributes & {
+ /**
+ * An array of objects to wrap in a #EuiHeaderSection.
+ * Each section is spaced using `space-between`.
+ * See #EuiHeaderSectionsProp for object details.
+ * This prop disregards the prop `children` if both are passed.
+ */
+ sections?: EuiHeaderSections[];
+ /**
+ * Helper that positions the header against the window body and
+ * adds the correct amount of top padding to the window when in `fixed` mode
+ */
+ position?: 'static' | 'fixed';
+ };
export const EuiHeader: FunctionComponent = ({
children,
className,
+ sections,
+ position = 'static',
...rest
}) => {
- const classes = classNames('euiHeader', className);
+ const classes = classNames('euiHeader', `euiHeader--${position}`, className);
+
+ useEffect(() => {
+ if (position === 'fixed') {
+ document.body.classList.add('euiBody--headerIsFixed');
+ }
+ return () => {
+ document.body.classList.remove('euiBody--headerIsFixed');
+ };
+ }, [position]);
+
+ let contents;
+ if (sections) {
+ if (children) {
+ // In case both children and sections are passed, warn in the console that the children will be disregarded
+ console.warn(
+ 'EuiHeader cannot accept both `children` and `sections`. It will disregard the `children`.'
+ );
+ }
+
+ contents = sections.map((section, index) => {
+ const content = [];
+ if (section.items) {
+ // Items get wrapped in EuiHeaderSection and each item in a EuiHeaderSectionItem
+ content.push(
+
+ {createHeaderSection(section.items, section.borders)}
+
+ );
+ }
+ if (section.breadcrumbs) {
+ content.push(
+ // Breadcrumbs are separate and cannot be contained in a EuiHeaderSection
+ // in order for truncation to work
+
+ );
+ }
+ return content;
+ });
+ } else {
+ contents = children;
+ }
return (
- {children}
+ {contents}
);
};
diff --git a/src/components/header/header_section/_header_section_item.scss b/src/components/header/header_section/_header_section_item.scss
index 098b6a98667..ac58b1774c1 100644
--- a/src/components/header/header_section/_header_section_item.scss
+++ b/src/components/header/header_section/_header_section_item.scss
@@ -2,10 +2,8 @@
.euiHeaderSectionItem {
position: relative;
-
- &:hover {
- background: $euiColorLightestShade;
- }
+ display: flex;
+ align-items: center;
&:after {
position: absolute;
@@ -23,6 +21,10 @@
text-align: center;
font-size: 0; // aligns icons better vertically
+ &:hover {
+ background: $euiColorLightestShade;
+ }
+
&:focus {
background: $euiFocusBackgroundColor;
}
diff --git a/src/components/header/header_section/header_section_item.tsx b/src/components/header/header_section/header_section_item.tsx
index 4bcdff3937d..2989c03c321 100644
--- a/src/components/header/header_section/header_section_item.tsx
+++ b/src/components/header/header_section/header_section_item.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent } from 'react';
+import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { CommonProps } from '../../common';
@@ -11,16 +11,14 @@ const borderToClassNameMap: { [border in Border]: string | undefined } = {
none: undefined,
};
-type Props = CommonProps & {
+export type EuiHeaderSectionItemProps = CommonProps & {
border?: Border;
+ children?: ReactNode;
};
-export const EuiHeaderSectionItem: FunctionComponent = ({
- border = 'left',
- children,
- className,
- ...rest
-}) => {
+export const EuiHeaderSectionItem: FunctionComponent<
+ EuiHeaderSectionItemProps
+> = ({ border = 'left', children, className, ...rest }) => {
const classes = classNames(
'euiHeaderSectionItem',
borderToClassNameMap[border],
diff --git a/src/components/header/header_section/index.ts b/src/components/header/header_section/index.ts
index 472a50057a3..2d032f7b088 100644
--- a/src/components/header/header_section/index.ts
+++ b/src/components/header/header_section/index.ts
@@ -1,5 +1,8 @@
export { EuiHeaderSection } from './header_section';
-export { EuiHeaderSectionItem } from './header_section_item';
+export {
+ EuiHeaderSectionItem,
+ EuiHeaderSectionItemProps,
+} from './header_section_item';
export { EuiHeaderSectionItemButton } from './header_section_item_button';
diff --git a/src/components/header/index.ts b/src/components/header/index.ts
index 731612722e6..029216ab1b4 100644
--- a/src/components/header/index.ts
+++ b/src/components/header/index.ts
@@ -1,4 +1,4 @@
-export { EuiHeader, EuiHeaderProps } from './header';
+export { EuiHeader, EuiHeaderProps, EuiHeaderSections } from './header';
export { EuiHeaderAlert, EuiHeaderAlertProps } from './header_alert';
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 cc4c0775932..623e6ad1ce6 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
@@ -294,7 +294,7 @@ exports[`EuiListGroupItem renders a disabled button even if provided an href 2`]
`;
-exports[`EuiListGroupItem throws an warning if both iconType and icon are provided but still renders 1`] = `
+exports[`EuiListGroupItem throws a warning if both iconType and icon are provided but still renders 1`] = `
diff --git a/src/components/list_group/list_group_item.test.tsx b/src/components/list_group/list_group_item.test.tsx
index ef4e9264c32..6582f7303b2 100644
--- a/src/components/list_group/list_group_item.test.tsx
+++ b/src/components/list_group/list_group_item.test.tsx
@@ -140,7 +140,7 @@ describe('EuiListGroupItem', () => {
expect(component).toMatchSnapshot();
});
- describe('throws an warning', () => {
+ describe('throws a warning', () => {
const oldConsoleError = console.warn;
let consoleStub: jest.Mock;