diff --git a/.eslintrc.js b/.eslintrc.js index f85b821ecb0..ace1c2aae97 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + const APACHE_2_0_LICENSE_HEADER = ` /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -69,7 +88,7 @@ module.exports = { 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', 'jsx-a11y/aria-props': 'error', 'jsx-a11y/aria-proptypes': 'error', - 'jsx-a11y/aria-role': 'error', + 'jsx-a11y/aria-role': [2, { ignoreNonDOM: true }], 'jsx-a11y/aria-unsupported-elements': 'error', 'jsx-a11y/heading-has-content': 'error', 'jsx-a11y/html-has-lang': 'error', diff --git a/CHANGELOG.md b/CHANGELOG.md index 9babf04b73e..be6d6744cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - Added `element` prop to `EuiPanel` for forcing to `div` or `button` ([#4649](https://github.com/elastic/eui/pull/4649)) - Increased padding on `EuiCheckableCard` with refactor to use `EuiSplitPanel` ([#4649](https://github.com/elastic/eui/pull/4649)) - Added `valueInputProps` prop to `EuiColorStops` ([#4669](https://github.com/elastic/eui/pull/4669)) +- Added `position`, `usePortal`, `top`, `right`, `bottom`, and `left` props to `EuiBottomBar` ([#4662](https://github.com/elastic/eui/pull/4662)) +- Added `bottomBar` and `bottomBarProps` to `EuiPageTemplate` when `template = 'default'` ([#4662](https://github.com/elastic/eui/pull/4662)) +- Added `role="main"` to `EuiPageContent` by default ([#4662](https://github.com/elastic/eui/pull/4662)) +- Added `bottomBorder` prop to `EuiPageHeader` ([#4662](https://github.com/elastic/eui/pull/4662)) **Bug fixes** @@ -16,6 +20,7 @@ **Breaking changes** - Removed `betaBadgeLabel`, `betaBadgeTooltipContent`, and `betaBadgeTitle` props from `EuiPanel` ([#4649](https://github.com/elastic/eui/pull/4649)) +- Changed `EuiBottomBar` positioning styles from being applied at the CSS layer to the `style` property ([#4662](https://github.com/elastic/eui/pull/4662)) ## [`31.12.0`](https://github.com/elastic/eui/tree/v31.12.0) diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js index 0dbf913fc80..a607129a03e 100644 --- a/scripts/a11y-testing.js +++ b/scripts/a11y-testing.js @@ -25,6 +25,7 @@ const docsPages = async (root, page) => { const pagesToSkip = [ `${root}#/layout/resizable-container`, `${root}#/layout/page`, // Has duplicate `
` element + `${root}#/layout/page-header`, // Has duplicate `
` element `${root}#/tabular-content/tables`, `${root}#/tabular-content/in-memory-tables`, `${root}#/display/aspect-ratio`, diff --git a/src-docs/src/components/guide_page/guide_page.js b/src-docs/src/components/guide_page/guide_page.js index a3c11a4f417..e3e1cb912bf 100644 --- a/src-docs/src/components/guide_page/guide_page.js +++ b/src-docs/src/components/guide_page/guide_page.js @@ -92,23 +92,22 @@ const GuidePageComponent = ({ -
- - {playground && ( - {playground} - )} - {guidelines && ( - {guidelines} - )} - {children} - -
+ + {playground && ( + {playground} + )} + {guidelines && ( + {guidelines} + )} + {children} +
diff --git a/src-docs/src/components/guide_page/guide_page_chrome.js b/src-docs/src/components/guide_page/guide_page_chrome.js index 0fa5a5aaa80..eb2ec97b9fd 100644 --- a/src-docs/src/components/guide_page/guide_page_chrome.js +++ b/src-docs/src/components/guide_page/guide_page_chrome.js @@ -232,7 +232,10 @@ export class GuidePageChrome extends Component { direction="column" responsive={false} gutterSize="none"> - + = ({ {renderingPlayground && renderPlayground()} {!renderingPlayground && demo && ( -
{demo}
- - } + example={{demo}} tabs={renderTabs()} ghostBackground={ghostBackground} demoPanelProps={demoPanelProps} diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index b0a48e0f78a..18cbefe9806 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -30,7 +30,7 @@ export class AppView extends Component { } componentDidMount() { - document.title = `Elastic UI Framework - ${this.props.currentRoute.name}`; + document.title = `${this.props.currentRoute.name} - Elastic UI Framework`; document.addEventListener('keydown', this.onKeydown); } diff --git a/src-docs/src/views/bottom_bar/bottom_bar_example.js b/src-docs/src/views/bottom_bar/bottom_bar_example.js index 0e3c5d24ba6..8dad801f5c6 100644 --- a/src-docs/src/views/bottom_bar/bottom_bar_example.js +++ b/src-docs/src/views/bottom_bar/bottom_bar_example.js @@ -1,26 +1,35 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; import { EuiBottomBar, EuiCode } from '../../../../src/components'; -import Playground from './playground'; + +import { bottomBarConfig } from './playground'; import BottomBar from './bottom_bar'; const bottomBarSource = require('!!raw-loader!./bottom_bar'); -const bottomBarSnippet = ` - -`; - import BottomBarDisplacement from './bottom_bar_displacement'; const bottomBarDisplacementSource = require('!!raw-loader!./bottom_bar_displacement'); -const bottomBarDisplacementSnippet = ` - -`; +import BottomBarPosition from './bottom_bar_position'; +import { EuiCallOut } from '../../../../src/components/call_out'; +const bottomBarPositionSource = require('!!raw-loader!./bottom_bar_position'); export const BottomBarExample = { title: 'Bottom bar', + intro: ( + +

+ EuiPageTemplate offers a quick way to{' '} + + apply a bottom bar to your page layouts + + . +

+
+ ), sections: [ { source: [ @@ -33,8 +42,8 @@ export const BottomBarExample = { <>

EuiBottomBar is a simple wrapper component that - does nothing but fix a dark bar (usually filled with buttons) to the - bottom of the page. Use it when you have really long pages or + does nothing but affix a dark bar (usually filled with buttons) to + the bottom of the page. Use it when you have really long pages or complicated, multi-page forms. In the case of forms, only invoke it if a form is in a savable state.

@@ -46,10 +55,43 @@ export const BottomBarExample = {

), - playground: Playground, props: { EuiBottomBar }, - snippet: bottomBarSnippet, demo: , + playground: bottomBarConfig, + snippet: ` + +`, + }, + { + title: 'Positions', + source: [ + { + type: GuideSectionTypes.JS, + code: bottomBarPositionSource, + }, + ], + text: ( + <> +

+ Bottom bars default to a fixed position, in a portal at the bottom + of the browser window. Alternatively, you can change the{' '} + position to sticky where it + will render in place but stick to the window only as the window edge + nears. The static position reverts back to + default DOM behavior. +

+

+ You can also apply a different set of positioning locations just by + adjusting them in with the{' '} + top | right | bottom | left props. +

+ + ), + props: { EuiBottomBar }, + demo: , + snippet: ` + +`, }, { title: 'Displacement', @@ -65,16 +107,18 @@ export const BottomBarExample = { There is an affordForDisplacement prop (defaulting to true), which determines whether the component makes room for itself by adding bottom padding - equivalent to its own height on the document's body element. - Setting this to false can be useful to minimize - scrollbar visibility but will cause the bottom bar to overlap body - content. + equivalent to its own height on the document{' '} + {''} element. Setting this + to false can be useful to minimize scrollbar + visibility but will cause the bottom bar to overlap body content.

), props: { EuiBottomBar }, - snippet: bottomBarDisplacementSnippet, demo: , + snippet: ` + +`, }, ], }; diff --git a/src-docs/src/views/bottom_bar/bottom_bar_position.js b/src-docs/src/views/bottom_bar/bottom_bar_position.js new file mode 100644 index 00000000000..6a96c946440 --- /dev/null +++ b/src-docs/src/views/bottom_bar/bottom_bar_position.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { EuiBottomBar, EuiSpacer, EuiText } from '../../../../src/components'; + +export default () => { + return ( + <> + +

+ When scrolling past this example block, the{' '} + EuiBottomBar will stick to the bottom of the browser + window (with a 10px offset), but keeps it within the bounds of its + parent. +

+
+ + + + +

Scroll to see!

+
+
+ + ); +}; diff --git a/src-docs/src/views/bottom_bar/playground.js b/src-docs/src/views/bottom_bar/playground.js index 94ba2404b9a..20629c7d55c 100644 --- a/src-docs/src/views/bottom_bar/playground.js +++ b/src-docs/src/views/bottom_bar/playground.js @@ -1,20 +1,39 @@ import { PropTypes } from 'react-view'; -import { EuiBottomBar, EuiButton } from '../../../../src/components/'; -import { - propUtilityForPlayground, - dummyFunction, -} from '../../services/playground'; +import { EuiButton, EuiBottomBar } from '../../../../src/components/'; +import { propUtilityForPlayground } from '../../services/playground'; -export default () => { +export const bottomBarConfig = () => { const docgenInfo = Array.isArray(EuiBottomBar.__docgenInfo) ? EuiBottomBar.__docgenInfo[0] : EuiBottomBar.__docgenInfo; const propsToUse = propUtilityForPlayground(docgenInfo.props); propsToUse.children = { - value: 'Save', type: PropTypes.ReactNode, - hidden: false, + value: 'Save', + }; + + propsToUse.top = { + ...propsToUse.top, + type: PropTypes.Number, + }; + + propsToUse.right = { + ...propsToUse.right, + type: PropTypes.Number, + value: '0', + }; + + propsToUse.bottom = { + ...propsToUse.bottom, + type: PropTypes.Number, + value: '0', + }; + + propsToUse.left = { + ...propsToUse.left, + type: PropTypes.Number, + value: '0', }; return { @@ -30,9 +49,6 @@ export default () => { named: ['EuiBottomBar', 'EuiButton'], }, }, - customProps: { - onToggle: dummyFunction, - }, }, }; }; diff --git a/src-docs/src/views/breadcrumbs/breadcrumbs.js b/src-docs/src/views/breadcrumbs/breadcrumbs.js index 05bb6d9784b..7be7e72c7da 100644 --- a/src-docs/src/views/breadcrumbs/breadcrumbs.js +++ b/src-docs/src/views/breadcrumbs/breadcrumbs.js @@ -36,7 +36,7 @@ export default () => { ]; return ( - + ( color="transparent" borderRadius="none"> -
- - - - -

Elastic UI

-
- - - -

The framework powering the Elastic Stack

-
- - -

- The Elastic UI framework (EUI) is a design library in use at - Elastic to build internal products that need to share our - aesthetics. It distributes UI React components and static - assets for use in building web layouts. -

- - - - Getting started - - - - - What's new - - - - - Contributing - - - -
-
- - - -
-
- - - - } - layout="horizontal" - display="plain" - titleSize="xs" - title="Accessible to everyone" - description="Uses high contrast, color-blind safe palettes and tested with most - assistive technology." - /> + + + + +

Elastic UI

+
+ + + +

The framework powering the Elastic Stack

+
+ + +

+ The Elastic UI framework (EUI) is a design library in use at + Elastic to build internal products that need to share our + aesthetics. It distributes UI React components and static assets + for use in building web layouts. +

+ + + + Getting started + + + + + What's new + + + + + Contributing + + + +
- - } - layout="horizontal" - display="plain" - titleSize="xs" - title="Flexible and composable" - description="Configurable enough to meet the needs of a wide array of contexts while maintaining brand and low-level consistency." - /> - - - } - layout="horizontal" - display="plain" - titleSize="xs" - title="Well documented and tested" - description="Code is friendly to the novice and expert alike." - /> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - + + -
+ + + + + } + layout="horizontal" + display="plain" + titleSize="xs" + title="Accessible to everyone" + description="Uses high contrast, color-blind safe palettes and tested with most + assistive technology." + /> + + + } + layout="horizontal" + display="plain" + titleSize="xs" + title="Flexible and composable" + description="Configurable enough to meet the needs of a wide array of contexts while maintaining brand and low-level consistency." + /> + + + } + layout="horizontal" + display="plain" + titleSize="xs" + title="Well documented and tested" + description="Code is friendly to the novice and expert alike." + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
- -

- EUI is licensed under{' '} - - Apache License 2.0 - -

-
-
+ +

+ EUI is licensed under{' '} + + Apache License 2.0 + +

+
); diff --git a/src-docs/src/views/page/_page_demo.tsx b/src-docs/src/views/page/_page_demo.tsx index 8fb77f8d968..d1be5405d8f 100644 --- a/src-docs/src/views/page/_page_demo.tsx +++ b/src-docs/src/views/page/_page_demo.tsx @@ -22,7 +22,8 @@ export const PageDemo: FunctionComponent<{ button: typeof EuiButton, Content: ReactNode, SideNav: ReactNode, - showTemplate: boolean + showTemplate: boolean, + BottomBar: ReactNode ) => ReactNode; centered?: boolean; }> = ({ children, centered }) => { @@ -57,6 +58,7 @@ export const PageDemo: FunctionComponent<{ url={isMobileSize ? single : sideNav} /> ); + const Content = () => ( <> ); + const BottomBar = () => ( + + Save + + ); + return ( <> @@ -86,7 +94,8 @@ export const PageDemo: FunctionComponent<{ ? 'guideFullScreenOverlay guideFullScreenOverlay--withHeader' : 'guideDemo__highlightLayout' }> - {children && children(Button, Content, SideNav, showTemplate)} + {children && + children(Button, Content, SideNav, showTemplate, BottomBar)} diff --git a/src-docs/src/views/page/page_bottom_bar.js b/src-docs/src/views/page/page_bottom_bar.js new file mode 100644 index 00000000000..fc40a28c61e --- /dev/null +++ b/src-docs/src/views/page/page_bottom_bar.js @@ -0,0 +1,46 @@ +import React from 'react'; + +import { + EuiPage, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageSideBar, + EuiPageBody, + EuiBottomBar, +} from '../../../../src/components'; + +export default ({ button = <>, content, sideNav, bottomBar }) => { + return ( + + {sideNav} + + {/* Double EuiPageBody to accomodate for the bottom bar */} + + + + + {content} + + + + {/* Wrapping the contents with EuiPageContentBody allows us to match the restrictWidth to keep the contents aligned */} + + {bottomBar} + + + + + ); +}; diff --git a/src-docs/src/views/page/page_bottom_bar_template.js b/src-docs/src/views/page/page_bottom_bar_template.js new file mode 100644 index 00000000000..8e17e0cbf3c --- /dev/null +++ b/src-docs/src/views/page/page_bottom_bar_template.js @@ -0,0 +1,18 @@ +import React from 'react'; + +import { EuiPageTemplate } from '../../../../src/components'; + +export default ({ button = <>, content, sideNav, bottomBar }) => { + return ( + + {content} + + ); +}; diff --git a/src-docs/src/views/page/page_example.js b/src-docs/src/views/page/page_example.js index 01a981a59aa..664aef64ea6 100644 --- a/src-docs/src/views/page/page_example.js +++ b/src-docs/src/views/page/page_example.js @@ -18,6 +18,7 @@ import { EuiPageTemplate, EuiCallOut, EuiSpacer, + EuiBottomBar, } from '../../../../src/components'; import PageNew from './page_new'; @@ -25,6 +26,11 @@ const pageNewSource = require('!!raw-loader!./page_new'); import PageTemplate from './page_template'; const PageTemplateSource = require('!!raw-loader!./page_template'); +import PageBottomBar from './page_bottom_bar'; +const pageBottomBarSource = require('!!raw-loader!./page_bottom_bar'); +import PageBottomBarTemplate from './page_bottom_bar_template'; +const PageBottomBarTemplateSource = require('!!raw-loader!./page_bottom_bar_template'); + import PageRestricingWidth from './page_restricting_width'; const PageRestricingWidthSource = require('!!raw-loader!./page_restricting_width'); import PageRestricingWidthTemplate from './page_restricting_width_template'; @@ -82,7 +88,7 @@ export const PageExample = {

You'll find the code for each in their own tab and if you go to full screen, you can see how they would behave in a typical - applicaiton layout. + application layout.

@@ -161,22 +167,25 @@ export const PageExample = { EuiPageHeader, EuiPageContent, EuiPageContentBody, + EuiBottomBar, }, playground: pageTemplateConfig, demo: ( - {(Button, Content, SideNav, showTemplate) => + {(Button, Content, SideNav, showTemplate, BottomBar) => showTemplate ? ( } content={} sideNav={} + bottomBar={} /> ) : ( } content={} sideNav={} + bottomBar={} /> ) } @@ -240,6 +249,75 @@ export const PageExample = { ), }, + { + title: 'Showing a bottom bar', + source: [ + { + type: GuideSectionTypes.JS, + code: PageBottomBarTemplateSource, + displayName: 'Template JS', + }, + { + type: GuideSectionTypes.JS, + code: pageBottomBarSource, + displayName: 'Components JS', + }, + ], + text: ( + <> +

+ Adding an{' '} + + EuiBottomBar + {' '} + can be tricky to use and account for any side bars.{' '} + EuiPageTemplate handles this nicely by supplying a{' '} + bottomBar prop for passing the contents of your + bottom bar, and bottomBarProps that extends{' '} + EuiBottomBar. +

+

+ It uses the sticky position so that it sticks to + the bottom of and remains within the bounds of{' '} + EuiPageBody. This way it will never overlap the{' '} + EuiPageSideBar, no matter the screen size. It also + means not needing to accomodate for the height of the bar in the + body element. +

+ + EuiPageTemplate only supports bottom bars in + the default template. + + } + /> + + ), + demo: ( + + {(Button, Content, SideNav, showTemplate, BottomBar) => + showTemplate ? ( + } + content={} + sideNav={} + bottomBar={} + /> + ) : ( + } + content={} + sideNav={} + bottomBar={} + /> + ) + } + + ), + }, { title: 'Centered body', source: [ diff --git a/src-docs/src/views/page/page_template.js b/src-docs/src/views/page/page_template.js index 5c10e9704e9..59569a612bf 100644 --- a/src-docs/src/views/page/page_template.js +++ b/src-docs/src/views/page/page_template.js @@ -1,16 +1,27 @@ -import React from 'react'; +import React, { useState } from 'react'; import { EuiPageTemplate } from '../../../../src/components'; -export default ({ button = <>, content, sideNav }) => ( - - {content} - -); +export default ({ button = <>, content, sideNav }) => { + const [showBottomBar, setshowBottomBar] = useState(false); + + return ( + setshowBottomBar((showing) => !showing), + }, + ], + }}> + {content} + + ); +}; diff --git a/src-docs/src/views/page/playground.js b/src-docs/src/views/page/playground.js index 5328e069bd0..b2b958cba2e 100644 --- a/src-docs/src/views/page/playground.js +++ b/src-docs/src/views/page/playground.js @@ -30,6 +30,11 @@ export const pageTemplateConfig = () => { hidden: false, }; + propsToUse.bottomBar = { + ...propsToUse.bottomBar, + type: PropTypes.String, + }; + propsToUse.pageSideBar = { ...propsToUse.pageSideBar, value: 'Side bar', diff --git a/src-docs/src/views/text_scaling/text_scaling.js b/src-docs/src/views/text_scaling/text_scaling.js index c61e2e0275c..942e51d1b19 100644 --- a/src-docs/src/views/text_scaling/text_scaling.js +++ b/src-docs/src/views/text_scaling/text_scaling.js @@ -81,7 +81,10 @@ const text = [ export default () => ( - + {text} @@ -89,6 +92,7 @@ export default () => ( diff --git a/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap b/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap index 48fa6ec4de6..2f3fe4a42ac 100644 --- a/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap +++ b/src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap @@ -4,8 +4,9 @@ exports[`EuiBottomBar is rendered 1`] = ` Array [

`; +exports[`EuiBottomBar props landmarkHeading 1`] = ` +Array [ +
+

+ This should have been label +

+
, +

+ There is a new region landmark called This should have been label with page level controls at the end of the document. +

, +] +`; + exports[`EuiBottomBar props paddingSize l is rendered 1`] = ` Array [

+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props position fixed is rendered 1`] = ` +Array [ +
+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props position props are altered 1`] = ` +Array [ +
+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props position static is rendered 1`] = ` +Array [ +
+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props position sticky is rendered 1`] = ` +Array [ +
+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props style is customized 1`] = ` +Array [ +
+

+ Page level controls +

+
, +

+ There is a new region landmark with page level controls at the end of the document. +

, +] +`; + +exports[`EuiBottomBar props usePortal can be false 1`] = ` +Array [ +

{ }); }); + describe('position', () => { + POSITIONS.forEach((position) => { + test(`${position} is rendered`, () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + }); + }); + + test('landmarkHeading', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + test('affordForDisplacement can be false', () => { const component = render(); expect(component).toMatchSnapshot(); }); + test('usePortal can be false', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + test('bodyClassName is rendered', () => { const component = mount(); expect(takeMountedSnapshot(component)).toMatchSnapshot(); expect(document.body.classList.contains('customClass')).toBe(true); }); + + test('style is customized', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('position props are altered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/components/bottom_bar/bottom_bar.tsx b/src/components/bottom_bar/bottom_bar.tsx index a1ebe70453a..45a86c34769 100644 --- a/src/components/bottom_bar/bottom_bar.tsx +++ b/src/components/bottom_bar/bottom_bar.tsx @@ -18,10 +18,18 @@ */ import classNames from 'classnames'; -import React, { Component } from 'react'; +import React, { + CSSProperties, + forwardRef, + HTMLAttributes, + useEffect, + useState, +} from 'react'; +import { useCombinedRefs } from '../../services'; import { EuiScreenReaderOnly } from '../accessibility'; -import { CommonProps } from '../common'; +import { CommonProps, ExclusiveUnion } from '../common'; import { EuiI18n } from '../i18n'; +import { useResizeObserver } from '../observer/resize_observer'; import { EuiPortal } from '../portal'; type BottomBarPaddingSize = 'none' | 's' | 'm' | 'l'; @@ -36,99 +44,139 @@ export const paddingSizeToClassNameMap: { l: 'euiBottomBar--paddingLarge', }; -export interface EuiBottomBarProps extends CommonProps { - /** - * Padding applied to the bar. Default is 'm'. - */ - paddingSize: BottomBarPaddingSize; - - /** - * Whether the component should apply padding on the document body element to afford for its own displacement height. - * Default is true. - */ - affordForDisplacement: boolean; - - /** - * Optional class applied to the body element on mount - */ - bodyClassName?: string; - - /** - * Customize the screen reader heading that helps users find this control. Default is 'Page level controls'. - */ - landmarkHeading?: string; -} - -export class EuiBottomBar extends Component { - static defaultProps = { - paddingSize: 'm', - affordForDisplacement: true, - }; - - private bar: HTMLElement | null = null; - - componentDidMount() { - if (this.props.affordForDisplacement) { - const height = this.bar ? this.bar.clientHeight : -1; - document.body.style.paddingBottom = `${height}px`; - } - - if (this.props.bodyClassName) { - document.body.classList.add(this.props.bodyClassName); - } - } - - componentDidUpdate(prevProps: EuiBottomBarProps) { - if (prevProps.affordForDisplacement !== this.props.affordForDisplacement) { - if (this.props.affordForDisplacement) { - // start affording for displacement - const height = this.bar ? this.bar.clientHeight : -1; - document.body.style.paddingBottom = `${height}px`; - } else { - // stop affording for displacement - document.body.style.paddingBottom = ''; - } - } - - if (prevProps.bodyClassName !== this.props.bodyClassName) { - if (prevProps.bodyClassName) { - document.body.classList.remove(prevProps.bodyClassName); - } - if (this.props.bodyClassName) { - document.body.classList.add(this.props.bodyClassName); - } - } - } - - componentWillUnmount() { - if (this.props.affordForDisplacement) { - document.body.style.paddingBottom = ''; - } - - if (this.props.bodyClassName) { - document.body.classList.remove(this.props.bodyClassName); - } +export const POSITIONS = ['static', 'fixed', 'sticky'] as const; +export type _BottomBarPosition = typeof POSITIONS[number]; + +type _BottomBarExclusivePositions = ExclusiveUnion< + { + position?: 'fixed'; + /** + * Whether to wrap in an EuiPortal which appends the component to the body element. + * Only works if `position` is `fixed`. + */ + usePortal?: boolean; + /** + * Whether the component should apply padding on the document body element to afford for its own displacement height. + * Only works if `usePortal` is true and `position` is `fixed`. + */ + affordForDisplacement?: boolean; + }, + { + /** + * How to position the bottom bar against its parent. + */ + position: 'static' | 'sticky'; } +>; +export type EuiBottomBarProps = CommonProps & + HTMLAttributes & + _BottomBarExclusivePositions & { + /** + * Padding applied to the bar. Default is 'm'. + */ + paddingSize?: BottomBarPaddingSize; + /** + * Optional class applied to the body element on mount. + */ + bodyClassName?: string; + /** + * Customize the screen reader heading that helps users find this control. Default is 'Page level controls'. + */ + landmarkHeading?: string; + /** + * Starting vertical position when `fixed` position. + * Offset from the top of the window when `sticky` position. + * Has no affect on `static` positions. + */ + top?: CSSProperties['top']; + /** + * Ending horizontal position when `fixed` position. + * Has no affect on `static` or `sticky` positions. + */ + right?: CSSProperties['right']; + /** + * Starting vertical position when `fixed` position. + * Offset from the bottom of the window when `sticky` position. + * Has no affect on `static` positions. + */ + bottom?: CSSProperties['bottom']; + /** + * Starting horizontal position when `fixed` position. + * Has no affect on `static` or `sticky` positions. + */ + left?: CSSProperties['left']; + }; - render() { - const { +export const EuiBottomBar = forwardRef< + HTMLElement, // type of element or component the ref will be passed to + EuiBottomBarProps // what properties apart from `ref` the component accepts +>( + ( + { + position = 'fixed', + paddingSize = 'm', + affordForDisplacement = true, children, className, - paddingSize, bodyClassName, landmarkHeading, - affordForDisplacement, + usePortal = true, + left = 0, + right = 0, + bottom = 0, + top, + style, ...rest - } = this.props; + }, + ref + ) => { + // Force some props if `fixed` position, but not if the user has supplied these + affordForDisplacement = + position !== 'fixed' ? false : affordForDisplacement; + usePortal = position !== 'fixed' ? false : usePortal; + + const [resizeRef, setResizeRef] = useState(null); + const setRef = useCombinedRefs([setResizeRef, ref]); + // TODO: Allow this hooke to be conditional + const dimensions = useResizeObserver(resizeRef); + + useEffect(() => { + if (affordForDisplacement && usePortal) { + document.body.style.paddingBottom = `${dimensions.height}px`; + } + + if (bodyClassName) { + document.body.classList.add(bodyClassName); + } + + return () => { + if (affordForDisplacement && usePortal) { + document.body.style.paddingBottom = ''; + } + + if (bodyClassName) { + document.body.classList.remove(bodyClassName); + } + }; + }, [affordForDisplacement, usePortal, dimensions, bodyClassName]); const classes = classNames( 'euiBottomBar', + `euiBottomBar--${position}`, paddingSizeToClassNameMap[paddingSize], className ); - return ( - + const newStyle = { + left, + right, + bottom, + top, + ...style, + }; + + const bar = ( + <> @@ -140,9 +188,8 @@ export class EuiBottomBar extends Component { landmarkHeading ? landmarkHeading : screenReaderHeading } className={classes} - ref={(node) => { - this.bar = node; - }} + ref={setRef} + style={newStyle} {...rest}>

@@ -169,7 +216,11 @@ export class EuiBottomBar extends Component { )}

- + ); + + return usePortal ? {bar} : bar; } -} +); + +EuiBottomBar.displayName = 'EuiBottomBar'; diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss index 1d183512f12..8c04d7da17a 100644 --- a/src/components/header/_header.scss +++ b/src/components/header/_header.scss @@ -25,10 +25,5 @@ } .euiHeader--dark { - @if (lightness($euiTextColor) < 50) { - @include euiHeaderDarkTheme($backgroundColor: shade($euiColorDarkestShade, 28%)); - } @else { - // Makes forced "dark" theme darker than the typical dark them to separate them visually - @include euiHeaderDarkTheme($backgroundColor: shade($euiColorLightestShade, 50%)); - } + @include euiHeaderDarkTheme($euiHeaderDarkBackgroundColor); } diff --git a/src/components/page/__snapshots__/page_template.test.tsx.snap b/src/components/page/__snapshots__/page_template.test.tsx.snap index 1cb6400ba1b..59eef1154c5 100644 --- a/src/components/page/__snapshots__/page_template.test.tsx.snap +++ b/src/components/page/__snapshots__/page_template.test.tsx.snap @@ -11,6 +11,7 @@ exports[`EuiPageTemplate is rendered 1`] = ` >
+ class="euiPanel euiPanel--borderRadiusNone euiPanel--transparent euiPanel--noShadow euiPanel--noBorder euiPageContent euiPageContent--borderRadiusNone" + role="main" + > +
+
@@ -629,6 +667,7 @@ exports[`EuiPageTemplate template with pageSideBar is rendered 2`] = ` >
+ class="euiPanel euiPanel--borderRadiusNone euiPanel--transparent euiPanel--noShadow euiPanel--noBorder euiPageContent euiPageContent--borderRadiusNone" + role="main" + > +
+
@@ -725,6 +771,7 @@ exports[`EuiPageTemplate template with pageSideBar is rendered with pageSideBarP >
`; + +exports[`EuiPageTemplate with bottomBar is rendered 1`] = ` +
+
+
+
+
+
+

+ Page level controls +

+
+ Bottom Bar +
+
+

+ There is a new region landmark with page level controls at the end of the document. +

+
+
+`; diff --git a/src/components/page/page_body/_page_body.scss b/src/components/page/page_body/_page_body.scss index 32814540500..00f96e7c555 100644 --- a/src/components/page/page_body/_page_body.scss +++ b/src/components/page/page_body/_page_body.scss @@ -24,11 +24,8 @@ & > .euiPageHeader:not([class*='--padding']) { // Match the body's padding for spacing if it doesn't have it's own margin-bottom: $amount; - } - - // When the page header is actually inside of a panelled page body, - // We want to add some extra separation between it and the content body - &.euiPanel > .euiPageHeader { + // When the page header is actually inside of a panelled page body, + // We want to add some extra separation between it and the content body border-bottom: $euiBorderThin; &:not(.euiPageHeader--tabsAtBottom) { diff --git a/src/components/page/page_content/__snapshots__/page_content.test.tsx.snap b/src/components/page/page_content/__snapshots__/page_content.test.tsx.snap index a3eb77316cd..f5c6e8c7862 100644 --- a/src/components/page/page_content/__snapshots__/page_content.test.tsx.snap +++ b/src/components/page/page_content/__snapshots__/page_content.test.tsx.snap @@ -1,9 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`EuiPageContent accepts panel props 1`] = ` +
+`; + +exports[`EuiPageContent horizontalPosition is rendered 1`] = ` +
+`; + exports[`EuiPageContent is rendered 1`] = `
+`; + +exports[`EuiPageContent role can be removed 1`] = ` +
+`; + +exports[`EuiPageContent verticalPosition is rendered 1`] = ` +
`; diff --git a/src/components/page/page_content/page_content.test.tsx b/src/components/page/page_content/page_content.test.tsx index 2099ece58b0..595bf98ddd7 100644 --- a/src/components/page/page_content/page_content.test.tsx +++ b/src/components/page/page_content/page_content.test.tsx @@ -29,4 +29,34 @@ describe('EuiPageContent', () => { expect(component).toMatchSnapshot(); }); + + test('verticalPosition is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('horizontalPosition is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('role can be removed', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('accepts panel props', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/components/page/page_content/page_content.tsx b/src/components/page/page_content/page_content.tsx index 8650547b8fa..7af6cb74047 100644 --- a/src/components/page/page_content/page_content.tsx +++ b/src/components/page/page_content/page_content.tsx @@ -21,7 +21,13 @@ import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; -import { EuiPanel, PanelPaddingSize, EuiPanelProps } from '../../panel/panel'; +import { + EuiPanel, + PanelPaddingSize, + _EuiPanelProps, + _EuiPanelDivlike, +} from '../../panel/panel'; +import { HTMLAttributes } from 'enzyme'; export type EuiPageContentVerticalPositions = 'center'; export type EuiPageContentHorizontalPositions = 'center'; @@ -39,13 +45,20 @@ const horizontalPositionToClassNameMap: { }; export type EuiPageContentProps = CommonProps & - EuiPanelProps & { + // Use only the div properties of EuiPanel (not button) + _EuiPanelProps & + Omit<_EuiPanelDivlike, 'onClick' | 'role'> & { /** * **DEPRECATED: use `paddingSize` instead.** */ panelPaddingSize?: PanelPaddingSize; verticalPosition?: EuiPageContentVerticalPositions; horizontalPosition?: EuiPageContentHorizontalPositions; + /** + * There should only be one EuiPageContent per page and should contain the main contents. + * If this is untrue, set role = `null`, or change it to match your needed aria role + */ + role?: HTMLAttributes['role'] | null; }; export const EuiPageContent: FunctionComponent = ({ @@ -56,8 +69,11 @@ export const EuiPageContent: FunctionComponent = ({ borderRadius, children, className, + role: _role = 'main', ...rest }) => { + const role = _role === null ? undefined : _role; + const borderRadiusClass = borderRadius === 'none' ? 'euiPageContent--borderRadiusNone' : ''; @@ -76,6 +92,7 @@ export const EuiPageContent: FunctionComponent = ({ className={classes} paddingSize={panelPaddingSize ?? paddingSize} borderRadius={borderRadius} + role={role} {...rest}> {children} diff --git a/src/components/page/page_header/_page_header.scss b/src/components/page/page_header/_page_header.scss index 992ef5ec2f6..b8c4060c33b 100644 --- a/src/components/page/page_header/_page_header.scss +++ b/src/components/page/page_header/_page_header.scss @@ -13,19 +13,21 @@ flex-shrink: 0; // Ensures Safari doesn't shrink beyond contents } +.euiPageHeader--bottomBorder { + border-bottom: $euiBorderThin; +} + // Uses the same values as EuiPanel @each $modifier, $amount in $euiPanelPaddingModifiers { .euiPageHeader--#{$modifier} { - padding-top: $amount; - padding-left: $amount; - padding-right: $amount; - // Use margin for the bottom in case there's a border - margin-bottom: $amount; - } -} + padding: $amount; -.euiPageHeader--tabsAtBottom { - margin-bottom: 0; + &.euiPageHeader--tabsAtBottom { + // Use margin if there are tabs to keep border close to tabs + padding-bottom: 0; + margin-bottom: $amount; + } + } } .euiPageHeader--top { @@ -41,7 +43,6 @@ } @include euiBreakpoint('xs', 's') { - .euiPageHeader--responsive { flex-direction: column; } diff --git a/src/components/page/page_header/page_header.tsx b/src/components/page/page_header/page_header.tsx index 03ef15a479e..1bed5bfeb92 100644 --- a/src/components/page/page_header/page_header.tsx +++ b/src/components/page/page_header/page_header.tsx @@ -50,12 +50,14 @@ export type EuiPageHeaderProps = CommonProps & /** * Adds a bottom border to separate it from the content after */ + bottomBorder?: boolean; }; export const EuiPageHeader: FunctionComponent = ({ className, restrictWidth = false, paddingSize = 'none', + bottomBorder, style, // Page header content shared props: @@ -83,6 +85,7 @@ export const EuiPageHeader: FunctionComponent = ({ 'euiPageHeader', paddingSizeToClassNameMap[paddingSize], { + 'euiPageHeader--bottomBorder': bottomBorder, 'euiPageHeader--responsive': responsive === true, 'euiPageHeader--responsiveReverse': responsive === 'reverse', 'euiPageHeader--tabsAtBottom': pageTitle && tabs, diff --git a/src/components/page/page_template.test.tsx b/src/components/page/page_template.test.tsx index ab102774610..d80cd1edfd1 100644 --- a/src/components/page/page_template.test.tsx +++ b/src/components/page/page_template.test.tsx @@ -127,4 +127,17 @@ describe('EuiPageTemplate', () => { }); }); }); + + describe('with bottomBar', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/page/page_template.tsx b/src/components/page/page_template.tsx index 3df592fe4b3..428ae8008c8 100644 --- a/src/components/page/page_template.tsx +++ b/src/components/page/page_template.tsx @@ -29,6 +29,8 @@ import { EuiPageContentProps, EuiPageContentBodyProps, } from './page_content'; +import { EuiBottomBarProps, EuiBottomBar } from '../bottom_bar'; +import { ExclusiveUnion } from '../common'; export const TEMPLATES = [ 'default', @@ -37,47 +39,65 @@ export const TEMPLATES = [ 'empty', ] as const; -export type EuiPageTemplateProps = Omit & { - /** - * Choose between 3 types of templates. - * `default`: Typical layout with nothing centered - * `centeredBody`: The panelled content is centered - * `centeredContent`: The content inside the panel is centered - * `empty`: Removes the panneling of the page content - */ - template?: typeof TEMPLATES[number]; - /** - * - * Padding size will not get applie to the over-arching #EuiPage, - * but will propogate through all the components to keep them in sync - */ - paddingSize?: typeof SIZES[number]; - /** - * Optionally include #EuiPageSideBar content. - * The inclusion of this will affect the whole layout - */ - pageSideBar?: ReactNode; - /** - * Gets passed along to the #EuiPageSideBar component - */ - pageSideBarProps?: EuiPageSideBarProps; - /** - * Optionally include an #EuiPageHeader by passing an object of its props - */ - pageHeader?: EuiPageHeaderProps; - /** - * Gets passed along to the #EuiPageBody component - */ - pageBodyProps?: EuiPageBodyProps; - /** - * Gets passed along to the #EuiPageContent component - */ - pageContentProps?: EuiPageContentProps; - /** - * Gets passed along to the #EuiPageContentBody component - */ - pageContentBodyProps?: EuiPageContentBodyProps; -}; +type _EuiPageTemplateTypes = ExclusiveUnion< + { + template?: 'default'; + /** + * Adds contents inside of an EuiBottomBar. + * Only works when `template = 'default'` + */ + bottomBar?: EuiBottomBarProps['children']; + /** + * Gets passed along to the #EuiBottomBar component if `bottomBar` has contents + */ + bottomBarProps?: EuiBottomBarProps; + }, + { + /** + * Choose between 3 types of templates. + * `default`: Typical layout with nothing centered + * `centeredBody`: The panelled content is centered + * `centeredContent`: The content inside the panel is centered + * `empty`: Removes the panneling of the page content + */ + template: typeof TEMPLATES[number]; + } +>; + +export type EuiPageTemplateProps = Omit & + _EuiPageTemplateTypes & { + /** + * + * Padding size will not get applie to the over-arching #EuiPage, + * but will propogate through all the components to keep them in sync + */ + paddingSize?: typeof SIZES[number]; + /** + * Optionally include #EuiPageSideBar content. + * The inclusion of this will affect the whole layout + */ + pageSideBar?: ReactNode; + /** + * Gets passed along to the #EuiPageSideBar component + */ + pageSideBarProps?: EuiPageSideBarProps; + /** + * Optionally include an #EuiPageHeader by passing an object of its props + */ + pageHeader?: EuiPageHeaderProps; + /** + * Gets passed along to the #EuiPageBody component + */ + pageBodyProps?: EuiPageBodyProps; + /** + * Gets passed along to the #EuiPageContent component + */ + pageContentProps?: EuiPageContentProps; + /** + * Gets passed along to the #EuiPageContentBody component + */ + pageContentBodyProps?: EuiPageContentBodyProps; + }; export const EuiPageTemplate: FunctionComponent = ({ template = 'default', @@ -92,11 +112,15 @@ export const EuiPageTemplate: FunctionComponent = ({ pageBodyProps, pageContentProps, pageContentBodyProps, + bottomBar, + bottomBarProps, ...rest }) => { const classes = classNames('euiPageTemplate', className); - // This seems very repitious but it's the most readable, scalable, and maintainable + /** + * This seems very repetitious but it's the most readable, scalable, and maintainable + */ switch (template) { /** @@ -257,7 +281,7 @@ export const EuiPageTemplate: FunctionComponent = ({ restrictWidth={restrictWidth} paddingSize={paddingSize} {...pageHeader} - style={{ marginBottom: 0, ...pageHeader?.style }} + style={{ paddingBottom: 0, ...pageHeader?.style }} /> )} = ({ * Typical layout with nothing "centered" */ default: + // Only the default template can display a bottom bar + const bottomBarNode = bottomBar ? ( + + {/* Wrapping the contents with EuiPageContentBody allows us to match the restrictWidth to keep the contents aligned */} + + {bottomBar} + + + ) : undefined; + return pageSideBar ? ( {pageSideBar} - - {pageHeader && ( - - )} - - - {children} - - + {/* The extra PageBody is to accomodate the bottom bar stretching to both sides */} + + + {pageHeader && ( + + )} + + + {children} + + + + {bottomBarNode} ) : ( @@ -332,6 +380,7 @@ export const EuiPageTemplate: FunctionComponent = ({ {children} + {bottomBarNode} ); diff --git a/src/components/panel/panel.tsx b/src/components/panel/panel.tsx index cb2fc0a8a76..82141c57e8b 100644 --- a/src/components/panel/panel.tsx +++ b/src/components/panel/panel.tsx @@ -97,19 +97,22 @@ export interface _EuiPanelProps extends CommonProps { color?: PanelColor; } -interface Divlike +export interface _EuiPanelDivlike extends _EuiPanelProps, Omit, 'color'> { element?: 'div'; } -interface Buttonlike +export interface _EuiPanelButtonlike extends _EuiPanelProps, Omit, 'color'> { element?: 'button'; } -export type EuiPanelProps = ExclusiveUnion; +export type EuiPanelProps = ExclusiveUnion< + _EuiPanelButtonlike, + _EuiPanelDivlike +>; export const EuiPanel: FunctionComponent = ({ children, diff --git a/src/global_styling/variables/_header.scss b/src/global_styling/variables/_header.scss index 8fbfea8967a..ca97ee110ec 100644 --- a/src/global_styling/variables/_header.scss +++ b/src/global_styling/variables/_header.scss @@ -1,5 +1,6 @@ // Themeable colors $euiHeaderBackgroundColor: $euiColorEmptyShade !default; +$euiHeaderDarkBackgroundColor: lightOrDarkTheme(shade($euiColorDarkestShade, 28%), shade($euiColorLightestShade, 50%)) !default; $euiHeaderBorderColor: $euiBorderColor !default; $euiHeaderBreadcrumbColor: $euiColorDarkestShade !default;