diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bbd1ad245..9ae92a3df24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,19 @@ - Updated props of `EuiCode` and `EuiCodeBlock` to reflect only functional props ([#3647](https://github.com/elastic/eui/pull/3647)) - Updated `EuiResizableContainer` `onPanelWidthChange` callback method to include all panel widths ([#3630](https://github.com/elastic/eui/pull/3630)) - Extended `Query` / `EuiSearchBar` to allow any character inside double-quoted phrases ([#3432](https://github.com/elastic/eui/pull/3432)) +- Added `headerZindexLocation` prop to `EuiOverlayMask` ([#3655](https://github.com/elastic/eui/pull/3655)) +- Added `maskProps` prop to `EuiFlyout` and `EuiCollapsibleNav` ([#3655](https://github.com/elastic/eui/pull/3655)) **Bug fixes** - Fixed `EuiContextMenu` panel `onAnimationEnd` transition bug in Chrome ([#3656](https://github.com/elastic/eui/pull/3656)) - Fixed `EuiSkipLink` interactive props and Safari click issue ([#3665](https://github.com/elastic/eui/pull/3665)) +- Fixed `z-index` issues with `EuiHeader`, `EuiFlyout`, and other portal content ([#3655](https://github.com/elastic/eui/pull/3655)) +- Fixed `color` prop error in `EuiBadge` to be more flexible with what format it accepts ([#3655](https://github.com/elastic/eui/pull/3655)) + +**Theme: Amsterdam** + +- Fixed `EuiHeaderBreadcrumb` height, `onClick`, border-radius, and single item display ([#3655](https://github.com/elastic/eui/pull/3655)) ## [`26.1.0`](https://github.com/elastic/eui/tree/v26.1.0) diff --git a/generator-eui/documentation/index.js b/generator-eui/documentation/index.js index e812de48857..4d3046a23af 100644 --- a/generator-eui/documentation/index.js +++ b/generator-eui/documentation/index.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 chalk = require('chalk'); const Generator = require('yeoman-generator'); const utils = require('../utils'); @@ -12,16 +31,20 @@ module.exports = class extends Generator { } prompting() { - let prompts = [{ - message: 'What\'s the name of the component you\'re documenting? Use snake_case, please.', - name: 'name', - type: 'input', - store: true, - }]; + const prompts = [ + { + message: + "What's the name of the component you're documenting? Use snake_case, please.", + name: 'name', + type: 'input', + store: true, + }, + ]; if (this.fileType === 'demo') { prompts.push({ - message: `What's the name of the directory this demo should go in? (Within src-docs/src/views). Use snake_case, please.`, + message: + "What's the name of the directory this demo should go in? (Within src-docs/src/views). Use snake_case, please.", name: 'folderName', type: 'input', store: true, @@ -29,7 +52,8 @@ module.exports = class extends Generator { }); prompts.push({ - message: 'What would you like to name this demo? Use snake_case, please.', + message: + 'What would you like to name this demo? Use snake_case, please.', name: 'demoName', type: 'input', store: true, @@ -46,22 +70,24 @@ module.exports = class extends Generator { const writeDocumentationPage = () => { const componentExampleName = utils.makeComponentName(config.name, false); - const componentExamplePrefix = utils.lowerCaseFirstLetter(componentExampleName); + const componentExamplePrefix = utils.lowerCaseFirstLetter( + componentExampleName + ); const componentName = utils.makeComponentName(config.name); const fileName = config.name; const path = DOCUMENTATION_PAGE_PATH; - const vars = config.documentationVars = { + const vars = (config.documentationVars = { componentExampleName, componentExamplePrefix, componentName, fileName, - }; + }); - const documentationPagePath - = config.documentationPagePath - = `${path}/${config.name}/${config.name}_example.js`; + const documentationPagePath = (config.documentationPagePath = `${path}/${ + config.name + }/${config.name}_example.js`); this.fs.copyTpl( this.templatePath('documentation_page.js'), @@ -72,21 +98,22 @@ module.exports = class extends Generator { const writeDocumentationPageDemo = (fileName, folderName) => { const componentExampleName = utils.makeComponentName(fileName, false); - const componentExamplePrefix = utils.lowerCaseFirstLetter(componentExampleName); + const componentExamplePrefix = utils.lowerCaseFirstLetter( + componentExampleName + ); const componentName = utils.makeComponentName(config.name); const path = DOCUMENTATION_PAGE_PATH; - const vars = config.documentationVars = { + const vars = (config.documentationVars = { componentExampleName, componentExamplePrefix, componentName, fileName, - }; + folderName, + }); - const documentationPageDemoPath - = config.documentationPageDemoPath - = `${path}/${folderName}/${fileName}.tsx`; + const documentationPageDemoPath = (config.documentationPageDemoPath = `${path}/${folderName}/${fileName}.tsx`); this.fs.copyTpl( this.templatePath('documentation_page_demo.tsx'), @@ -117,44 +144,55 @@ module.exports = class extends Generator { this.log(chalk.white('\n// Import demo into example.')); this.log( - `${chalk.magenta('import')} ${componentExampleName} from ${chalk.cyan(`'./${fileName}'`)};\n` + - `${chalk.magenta('const')} ${componentExamplePrefix}Source = require(${chalk.cyan(`'!!raw-loader!./${fileName}'`)});\n` + - `${chalk.magenta('const')} ${componentExamplePrefix}Html = renderToHtml(${componentExampleName});` + `${chalk.magenta('import')} ${componentExampleName} from ${chalk.cyan( + `'./${fileName}'` + )};\n` + + `${chalk.magenta( + 'const' + )} ${componentExamplePrefix}Source = require(${chalk.cyan( + `'!!raw-loader!./${fileName}'` + )});\n` + + `${chalk.magenta( + 'const' + )} ${componentExamplePrefix}Html = renderToHtml(${componentExampleName});` ); this.log(chalk.white('\n// Render demo.')); this.log( - `Description needed: how to use the ${componentExampleName} component.

\n` + - ` }\n` + - ` demo={\n` + - ` <${componentExampleName} />\n` + - ` }\n` + - `/>\n` + 'Description needed: how to use the ${componentExampleName} component.

\n` + + ' }\n' + + ' demo={\n' + + ` <${componentExampleName} />\n` + + ' }\n' + + '/>\n' ); }; const showImportRouteSnippet = (suffix, appendToRoute) => { - const { - componentExampleName, - fileName, - } = this.config.documentationVars; + const { componentExampleName, fileName } = this.config.documentationVars; - this.log(chalk.white('\n// Import example into routes.js and then add it to the "components" array.')); + this.log( + chalk.white( + '\n// Import example into routes.js and then add it to the "components" array.' + ) + ); this.log( `${chalk.magenta('import')} { ${componentExampleName}${suffix} }\n` + - ` ${chalk.magenta('from')} ${chalk.cyan(`'./views/${fileName}/${fileName}_${suffix.toLowerCase()}'`)};` + ` ${chalk.magenta('from')} ${chalk.cyan( + `'./views/${fileName}/${fileName}_${suffix.toLowerCase()}'` + )};` ); - } + }; this.log('------------------------------------------------'); this.log(chalk.bold('Import snippets:')); @@ -170,4 +208,4 @@ module.exports = class extends Generator { } this.log('------------------------------------------------'); } -} +}; diff --git a/generator-eui/documentation/templates/documentation_page_demo.tsx b/generator-eui/documentation/templates/documentation_page_demo.tsx index 47f4baedc9d..0d28a268e73 100644 --- a/generator-eui/documentation/templates/documentation_page_demo.tsx +++ b/generator-eui/documentation/templates/documentation_page_demo.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { <%= componentName %>, -} from '../../../../src/components/<%= fileName %>'; +} from '../../../../src/components/<%= folderName %>'; export default () => ( <<%= componentName %>> diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss index ddcd3add658..661a486db67 100644 --- a/src-docs/src/components/guide_components.scss +++ b/src-docs/src/components/guide_components.scss @@ -25,6 +25,12 @@ $guideZLevelHighest: $euiZLevel9 + 1000; .guideSideNav { top: $euiHeaderHeightCompensation * 2; } + + .euiHeader:not(.euiHeader--fixed) { + // Force headers below the full screen. + // This shouldn't be necessary in consuming applications because headers should always be at the top of the page + z-index: 0; + } } .guidePage { diff --git a/src-docs/src/views/header/header_alert.js b/src-docs/src/views/header/header_alert.js index 3beaac75e6d..8a8473d4dfe 100644 --- a/src-docs/src/views/header/header_alert.js +++ b/src-docs/src/views/header/header_alert.js @@ -1,24 +1,14 @@ -import React from 'react'; -import { GuideFullScreen } from '../../services/full_screen/full_screen'; +import React, { useState } from 'react'; import { - EuiButton, EuiHeader, EuiHeaderSection, EuiHeaderSectionItem, EuiHeaderLogo, EuiHeaderLink, EuiHeaderLinks, - EuiIcon, - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiPageContentBody, - EuiTitle, + EuiSpacer, + EuiSwitch, } from '../../../../src/components'; import HeaderUserMenu from './header_user_menu'; @@ -26,6 +16,8 @@ import HeaderSpacesMenu from './header_spaces_menu'; import HeaderUpdates from './header_updates'; export default () => { + const [position, setPosition] = useState('static'); + const renderLogo = () => { return ( { }; return ( - - {setIsFullScreen => ( -
- - - - {renderLogo()} - - - - - - Home - - - - - - - - - - - - + <> + setPosition(e.target.checked ? 'fixed' : 'static')} + /> + + + + + {renderLogo()} + + + + + + Home + + - - - - - -

Kibana news feed demo

-
-
-
- - - -

- Click the button to see - ‘What’s new?’ -

-
-
- - setIsFullScreen(false)} - iconType="exit" - aria-label="Exit fullscreen demo"> - Exit fullscreen demo - - -
-
-
-
- )} -
+ + + + + + + + + + ); }; diff --git a/src-docs/src/views/header/header_elastic_pattern.js b/src-docs/src/views/header/header_elastic_pattern.js new file mode 100644 index 00000000000..74353d46981 --- /dev/null +++ b/src-docs/src/views/header/header_elastic_pattern.js @@ -0,0 +1,323 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +// Uncomment to use in consuming apps or CodeSandbox +// import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { + EuiAvatar, + EuiBadge, + EuiButton, + EuiCollapsibleNav, + EuiCollapsibleNavGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFocusTrap, + EuiHeader, + EuiHeaderLink, + EuiHeaderLinks, + EuiHeaderLogo, + EuiHeaderSectionItemButton, + EuiIcon, + EuiListGroupItem, + EuiPage, + EuiPopover, + EuiPortal, + EuiShowFor, + EuiText, + EuiTitle, +} from '../../../../src/components'; + +export default ({ theme }) => { + /** + * FullScreen for docs only + */ + const [fullScreen, setFullScreen] = useState(false); + useEffect(() => { + if (fullScreen) { + document.body.classList.add('guideBody--overflowHidden'); + document.body.classList.add('euiBody--headerIsFixed--double'); + } + return () => { + document.body.classList.remove('guideBody--overflowHidden'); + document.body.classList.remove('euiBody--headerIsFixed--double'); + }; + }, [fullScreen]); + + /** + * Collapsible Nav + */ + const [navIsOpen, setNavIsOpen] = useState( + JSON.parse(String(localStorage.getItem('navIsDocked'))) || false + ); + const [navIsDocked, setNavIsDocked] = useState( + JSON.parse(String(localStorage.getItem('navIsDocked'))) || false + ); + const collapsibleNav = ( + setNavIsOpen(!navIsOpen)}> + + ); + + /** + * Header Alerts + */ + const [isAlertFlyoutVisible, setIsAlertFlyoutVisible] = useState(false); + const headerAlerts = ( + + setIsAlertFlyoutVisible(false)} + size="s" + id="guideHeaderAlertExample" + aria-labelledby="guideHeaderAlertExampleTitle"> + + +

EuiHeaderAlert

+
+
+ + +

+ Please see the component page for{' '} + + EuiHeaderAlert + {' '} + on how to configure your alerts. +

+
+
+
+
+ ); + + /** + * User Menu + */ + const [isUserMenuVisible, setIsUserMenuVisible] = useState(false); + const userMenu = ( + setIsUserMenuVisible(!isUserMenuVisible)}> + + + } + isOpen={isUserMenuVisible} + anchorPosition="downRight" + closePopover={() => setIsUserMenuVisible(false)}> +
+ +

+ Please see the component page for{' '} + + EuiHeader + {' '} + on how to configure your user menu. +

+
+
+
+ ); + + /** + * Spaces Menu + */ + const [isSpacesMenuVisible, setIsSpacesMenuVisible] = useState(false); + const spacesMenu = ( + setIsSpacesMenuVisible(!isSpacesMenuVisible)}> + + + } + isOpen={isSpacesMenuVisible} + anchorPosition="downRight" + closePopover={() => setIsSpacesMenuVisible(false)}> +
+ +

+ Please see the component page for{' '} + + EuiHeader + {' '} + on how to configure your spaces menu. +

+
+
+
+ ); + + /** + * Deployment Menu + */ + const [isDeploymentMenuVisible, setIsDeploymentMenuVisible] = useState(false); + const deploymentMenu = ( + setIsDeploymentMenuVisible(!isDeploymentMenuVisible)}> + Production logs + + } + isOpen={isDeploymentMenuVisible} + anchorPosition="downRight" + closePopover={() => setIsDeploymentMenuVisible(false)}> + +

Deployment menu pattern TBD

+
+
+ ); + + return ( + <> + setFullScreen(true)} iconType="fullScreen"> + Show fullscreen demo + + {/* FocusTrap for Docs only */} + {fullScreen && ( + + + Elastic +
, + ], + borders: 'none', + }, + { + items: [ + deploymentMenu, + + setIsAlertFlyoutVisible(!isAlertFlyoutVisible) + }> + + , + userMenu, + ], + borders: 'none', + }, + ]} + /> + {}, + }, + { + text: 'Users', + }, + ], + borders: 'right', + }, + { + items: [ + + Share + Clone + { + setFullScreen(false); + document.body.classList.remove( + 'euiBody--headerIsFixed--double' + ); + }}> + Exit full screen + + , + ], + }, + ]} + /> + + {isAlertFlyoutVisible ? headerAlerts : null} + + + + )} + + ); +}; diff --git a/src-docs/src/views/header/header_example.js b/src-docs/src/views/header/header_example.js index fb97e465e4b..c78bb0b32ab 100644 --- a/src-docs/src/views/header/header_example.js +++ b/src-docs/src/views/header/header_example.js @@ -49,6 +49,10 @@ import HeaderStacked from './header_stacked'; const headerStackedSource = require('!!raw-loader!./header_stacked'); const headerStackedHtml = renderToHtml(HeaderStacked); +import HeaderElasticPattern from './header_elastic_pattern'; +const headerElasticPatternSource = require('!!raw-loader!./header_elastic_pattern'); +const headerElasticPatternHtml = renderToHtml(HeaderElasticPattern); + const headerSnippet = ` @@ -264,7 +268,7 @@ export const HeaderExample = { demo: , }, { - title: 'Alerts in the header', + title: 'Portal content in the header', source: [ { type: GuideSectionTypes.JS, @@ -276,20 +280,27 @@ export const HeaderExample = { }, ], text: ( -

- Use an EuiHeaderSectionItemButton to display - additional information in an{' '} - - EuiPopover - {' '} - or{' '} - - EuiFlyout - - , such as a user profile or news feed. In the latter example, this - additional content can be presented in a list style format using{' '} - EuiHeaderAlert components, as shown below. -

+ <> +

+ Use an EuiHeaderSectionItemButton to display + additional information in popovers{' '} + or flyouts, such as a user profile + or news feed. When using{' '} + + EuiFlyout + + , be sure to wrap it in a{' '} + + EuiPortal + + . +

+

+ The example below shows how to incorporate{' '} + EuiHeaderAlert components to show a list of + updates. +

+ ), props: { EuiHeaderAlert, @@ -311,12 +322,12 @@ export const HeaderExample = { ], text: (

- Stacking multiple headers provide a great way to separate global + Stacking multiple headers provides a great way to separate global navigation concerns. However, the{' '} {'position="fixed"'} option will not - be aware of the number of headers. Therefore, if you do need fixed and - stacked headers, you will need to apply the helper mixin and pass in - the correct height to afford for. + be aware of the number of headers. If you do need fixed{' '} + and stacked headers, you will need to apply the SASS + helper mixin and pass in the correct height to afford for.

), snippet: [ @@ -326,5 +337,40 @@ export const HeaderExample = { ], demo: , }, + { + title: 'The Elastic navigation pattern', + source: [ + { + type: GuideSectionTypes.JS, + code: headerElasticPatternSource, + }, + { + type: GuideSectionTypes.HTML, + code: headerElasticPatternHtml, + }, + ], + text: ( + <> +

Putting it all together

+

+ The button below will launch a full screen example that includes two{' '} + EuiHeaders with all the appropriate navigation + pieces including{' '} + + EuiCollapsibleNav, + {' '} + EuiHeaderAlerts, user menu, deployment switcher, + space selector, EuiHeaderBreadcrumbs and{' '} + EuiHeaderLinks for app menu items. +

+

+ This is just a pattern and should be treated as such. Consuming + applications will need to recreate the pattern according to their + context and save the states as is appropriate to their data store. +

+ + ), + demo: , + }, ], }; diff --git a/src-docs/src/views/header/header_stacked.tsx b/src-docs/src/views/header/header_stacked.tsx index 6b5a5b96ff3..996701b9c2b 100644 --- a/src-docs/src/views/header/header_stacked.tsx +++ b/src-docs/src/views/header/header_stacked.tsx @@ -70,7 +70,11 @@ export default () => { borders: 'right', }, { - items: isFixed ? [] : undefined, + items: [ + , + ], borders: 'none', }, ]} diff --git a/src-docs/src/views/header/header_updates.js b/src-docs/src/views/header/header_updates.js index 474a1efa69d..fdc60f2ad6c 100644 --- a/src-docs/src/views/header/header_updates.js +++ b/src-docs/src/views/header/header_updates.js @@ -1,4 +1,4 @@ -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import { EuiIcon, @@ -15,9 +15,14 @@ import { EuiButtonEmpty, EuiText, EuiBadge, + EuiPopover, + EuiPopoverTitle, + EuiPopoverFooter, + EuiSpacer, } from '../../../../src/components'; +import { EuiPortal } from '../../../../src/components/portal'; -export default () => { +export default ({ flyoutOrPopover = 'flyout' }) => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [showBadge, setShowBadge] = useState(true); @@ -120,20 +125,69 @@ export default () => { ); - let flyout; - if (isFlyoutVisible) { - flyout = ( - closeFlyout()} - size="s" - id="headerNewsFeed" - aria-labelledby="flyoutSmallTitle"> - - -

What's new

-
-
- + let content; + if (flyoutOrPopover === 'flyout') { + content = ( + <> + {button} + {isFlyoutVisible && ( + + closeFlyout()} + size="s" + id="headerNewsFeed" + aria-labelledby="flyoutSmallTitle"> + + +

What's new

+
+
+ + {alerts.map((alert, i) => ( + + ))} + + + + + closeFlyout()} + flush="left"> + Close + + + + +

Version 7.0

+
+
+
+
+
+
+ )} + + ); + } + + if (flyoutOrPopover === 'popover') { + content = ( + closeFlyout()} + panelPaddingSize="none"> + What's new +
+ {alerts.map((alert, i) => ( { badge={alert.badge} /> ))} - - - - - closeFlyout()} - flush="left"> - Close - - - - -

Version 7.0

-
-
-
-
- +
+ + +

Version 7.0

+
+
+
); } - return ( - - {button} - {flyout} - - ); + return content; }; diff --git a/src-docs/src/views/overlay_mask/overlay_mask.js b/src-docs/src/views/overlay_mask/overlay_mask.js index b8794454e25..c318adb60b4 100644 --- a/src-docs/src/views/overlay_mask/overlay_mask.js +++ b/src-docs/src/views/overlay_mask/overlay_mask.js @@ -4,73 +4,51 @@ import { EuiOverlayMask, EuiButton, EuiSpacer, - EuiFlyout, EuiTitle, - EuiFlyoutHeader, } from '../../../../src/components'; export default () => { - const [modalOpen, changeModal] = useState(false); - const [selectedModal, selectModal] = useState(1); - const [flyOut, changeFlyOut] = useState(false); + const [maskOpen, changeMask] = useState(false); + const [maskWithClickOpen, changeMaskWithClick] = useState(false); - const openModal = modal => { - selectModal(modal); - changeModal(!modalOpen); - }; - - const closeModal = () => { - changeModal(!modalOpen); - }; - - const toggleFlyOut = () => { - changeFlyOut(!flyOut); - }; - - if (modalOpen) { - if (selectedModal === 1) { - return ( - - - -

Click anywhere to close overlay.

-
-
-
- ); - } - - return ( - - Click this button to close + const modal = ( + + { + changeMask(false); + }}> + +

Click anywhere to close overlay.

+
- ); - } +
+ ); - if (flyOut) { - return ( - - - - - -

Click outside this flyout to close overlay.

-
-
-
-
- ); - } + const maskWithClick = ( + + { + changeMaskWithClick(false); + }}> + Click this button to close + + + ); return ( - openModal(1)}>Overlay with onClick - - openModal(2)}>Overlay with button + { + changeMask(true); + }}> + Overlay with onClick + - toggleFlyOut()}> - Overlay as a sibling of a flyout + changeMaskWithClick(true)}> + Overlay with button + {maskOpen ? modal : undefined} + {maskWithClickOpen ? maskWithClick : undefined} ); }; diff --git a/src-docs/src/views/overlay_mask/overlay_mask_example.js b/src-docs/src/views/overlay_mask/overlay_mask_example.js index 4df9912a382..a5116302f76 100644 --- a/src-docs/src/views/overlay_mask/overlay_mask_example.js +++ b/src-docs/src/views/overlay_mask/overlay_mask_example.js @@ -11,9 +11,9 @@ import OverlayMask from './overlay_mask'; const overlayMaskSource = require('!!raw-loader!./overlay_mask'); const overlayMaskHtml = renderToHtml(OverlayMask); -const overlayMaskSnippet = ` - -`; +import OverlayMaskHeader from './overlay_mask_header'; +const overlayMaskHeaderSource = require('!!raw-loader!./overlay_mask_header'); +const overlayMaskHeaderHtml = renderToHtml(OverlayMaskHeader); export const OverlayMaskExample = { title: 'Overlay mask', @@ -46,16 +46,57 @@ export const OverlayMaskExample = { to make before choosing to use an overlay. At the very least, you must provide a visible button to close the overlay. You can also pass an onClick handler to handle closing the - overlay. However, be wary of using onClick and{' '} - children, as clicking the{' '} - children will also trigger the{' '} - onClick. + overlay.

), props: { EuiOverlayMask }, - snippet: overlayMaskSnippet, + snippet: ` {}}> + +`, demo: , }, + { + title: 'Masks with header', + source: [ + { + type: GuideSectionTypes.JS, + code: overlayMaskHeaderSource, + }, + { + type: GuideSectionTypes.HTML, + code: overlayMaskHeaderHtml, + }, + ], + text: ( +
+

+ Managing z-index levels of multiple portal-positioned components and + their different contexts is complicated from within the library.{' '} + EuiOverlayMask gives you control over whether it + should appear below or above an{' '} + + EuiHeader + {' '} + by providing the headerZindexLocation prop. By + default this is set to {'"above"'} for common + cases like with{' '} + + EuiModal + {' '} + where the header should be obscured. However, a component like{' '} + + EuiFlyout + {' '} + which utilizes the overlay mask but should keep the header visible + needs to change this prop to {'"below"'}. +

+
+ ), + props: { EuiOverlayMask }, + snippet: ` +`, + demo: , + }, ], }; diff --git a/src-docs/src/views/overlay_mask/overlay_mask_header.js b/src-docs/src/views/overlay_mask/overlay_mask_header.js new file mode 100644 index 00000000000..8cc6053ef44 --- /dev/null +++ b/src-docs/src/views/overlay_mask/overlay_mask_header.js @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; + +import { + EuiOverlayMask, + EuiButton, + EuiFlyout, + EuiTitle, + EuiFlyoutHeader, +} from '../../../../src/components'; + +export default () => { + const [flyOut, changeFlyOut] = useState(false); + + const toggleFlyOut = () => { + changeFlyOut(!flyOut); + }; + + let flyout; + if (flyOut) { + flyout = ( + + + + + +

Click outside this flyout to close overlay.

+
+
+
+
+ ); + } + + return ( + + toggleFlyOut()}> + Overlay as a sibling of a flyout + + {flyout} + + ); +}; diff --git a/src/components/badge/__snapshots__/badge.test.tsx.snap b/src/components/badge/__snapshots__/badge.test.tsx.snap index 3bf52801022..4c326db3643 100644 --- a/src/components/badge/__snapshots__/badge.test.tsx.snap +++ b/src/components/badge/__snapshots__/badge.test.tsx.snap @@ -175,6 +175,40 @@ exports[`EuiBadge props color accent is rendered 1`] = ` `; +exports[`EuiBadge props color accepts hex 1`] = ` + + + + Content + + + +`; + +exports[`EuiBadge props color accepts rgba 1`] = ` + + + + Content + + + +`; + exports[`EuiBadge props color danger is rendered 1`] = ` { expect(component).toMatchSnapshot(); }); }); + + it('accepts rgba', () => { + const component = render( + Content + ); + + expect(component).toMatchSnapshot(); + }); + + it('accepts hex', () => { + const component = render(Content); + + expect(component).toMatchSnapshot(); + }); }); describe('iconSide', () => { diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx index 6a6efa87e98..0bbc160d9db 100644 --- a/src/components/badge/badge.tsx +++ b/src/components/badge/badge.tsx @@ -26,15 +26,16 @@ import React, { Ref, } from 'react'; import classNames from 'classnames'; -import { CommonProps, ExclusiveUnion, keysOf, PropsOf } from '../common'; import chroma from 'chroma-js'; +import { CommonProps, ExclusiveUnion, keysOf, PropsOf } from '../common'; import { euiPaletteColorBlindBehindText, - isValidHex, getSecureRelForTarget, + isColorDark, } from '../../services'; import { EuiInnerText } from '../inner_text'; import { EuiIcon, IconColor, IconType } from '../icon'; +import { chromaValid, parseColor } from '../color_picker/utils'; type IconSide = 'left' | 'right'; @@ -354,24 +355,23 @@ function getColorContrast(textColor: string, color: string) { } function setTextColor(bgColor: string) { - const textColor = - getColorContrast(colorInk, bgColor) > getColorContrast(colorGhost, bgColor) - ? colorInk - : colorGhost; + const textColor = isColorDark(...chroma(bgColor).rgb()) + ? colorGhost + : colorInk; return textColor; } function checkValidColor(color: null | IconColor | string) { - if ( - color != null && - !isValidHex(color) && - !COLORS.includes(color) && - color !== 'hollow' - ) { + const colorExists = !!color; + const isNamedColor = (color && COLORS.includes(color)) || color === 'hollow'; + const isValidColorString = color && chromaValid(parseColor(color) || ''); + + if (!colorExists && !isNamedColor && !isValidColorString) { console.warn( 'EuiBadge expects a valid color. This can either be a three or six ' + - `character hex value, hollow, or one of the following: ${COLORS}` + `character hex value, rgb(a) value, hsv value, hollow, or one of the following: ${COLORS}. ` + + `Instead got ${color}.` ); } } diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap index 358d9be5b22..d4fa97dc334 100644 --- a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap +++ b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap @@ -48,12 +48,12 @@ exports[`EuiBreadcrumbs is rendered 1`] = `
- Reptiles - +
@@ -128,12 +128,12 @@ exports[`EuiBreadcrumbs props max doesn't break when max exceeds the number of b
- Reptiles - +
@@ -244,12 +244,12 @@ exports[`EuiBreadcrumbs props max renders all items with null 1`] = `
- Reptiles - +
@@ -319,12 +319,12 @@ exports[`EuiBreadcrumbs props responsive is rendered 1`] = `
- Reptiles - +
@@ -394,12 +394,12 @@ exports[`EuiBreadcrumbs props responsive is rendered as false 1`] = `
- Reptiles - +
@@ -505,12 +505,12 @@ exports[`EuiBreadcrumbs props truncate as false is rendered 1`] = `
- Reptiles - +
diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index dd22061a686..6da5869109c 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -223,7 +223,7 @@ export const EuiBreadcrumbs: FunctionComponent = ({ let link; - if (!href) { + if (!href && !onClick) { link = ( {(ref, innerText) => ( 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 db665c0f2d4..43fabc7dfea 100644 --- a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap @@ -195,6 +195,57 @@ Array [ ] `; +exports[`EuiCollapsibleNav props can alter mask props with maskProps without throwing error 1`] = ` +Array [ +
, +
+
+
+
+ +
+
+
, +] +`; + exports[`EuiCollapsibleNav props dockedBreakpoint 1`] = ` Array [
, diff --git a/src/components/collapsible_nav/_collapsible_nav.scss b/src/components/collapsible_nav/_collapsible_nav.scss index 6ac09a8de6e..e2d36a5d8ee 100644 --- a/src/components/collapsible_nav/_collapsible_nav.scss +++ b/src/components/collapsible_nav/_collapsible_nav.scss @@ -10,6 +10,7 @@ width: $euiCollapsibleNavWidth; max-width: 80vw; animation: euiCollapsibleNavIn $euiAnimSpeedNormal $euiAnimSlightResistance; + clip-path: polygon(0 0, 150% 0, 150% 100%, 0 100%); // Must include the width of the close button too } .euiCollapsibleNav__closeButton { @@ -37,6 +38,7 @@ .euiCollapsibleNav.euiCollapsibleNav--isDocked { @include euiBottomShadowMedium; z-index: $euiZHeader; // When docked, make it the same level as the header + clip-path: none; .euiCollapsibleNav__closeButton { display: none; diff --git a/src/components/collapsible_nav/collapsible_nav.test.tsx b/src/components/collapsible_nav/collapsible_nav.test.tsx index 92f7a8564e7..32f0c401bfa 100644 --- a/src/components/collapsible_nav/collapsible_nav.test.tsx +++ b/src/components/collapsible_nav/collapsible_nav.test.tsx @@ -24,7 +24,9 @@ import { requiredProps } from '../../test/required_props'; import { EuiCollapsibleNav } from './collapsible_nav'; jest.mock('../overlay_mask', () => ({ - EuiOverlayMask: (props: any) =>
, + EuiOverlayMask: ({ headerZindexLocation, ...props }: any) => ( +
+ ), })); const propsNeededToRender = { id: 'id', isOpen: true }; @@ -83,6 +85,17 @@ describe('EuiCollapsibleNav', () => { expect(component).toMatchSnapshot(); }); + + test('can alter mask props with maskProps without throwing error', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); }); describe('close button', () => { diff --git a/src/components/collapsible_nav/collapsible_nav.tsx b/src/components/collapsible_nav/collapsible_nav.tsx index 7b699db1cbf..c9b5f745d0e 100644 --- a/src/components/collapsible_nav/collapsible_nav.tsx +++ b/src/components/collapsible_nav/collapsible_nav.tsx @@ -30,7 +30,7 @@ import classNames from 'classnames'; import { throttle } from '../color_picker/utils'; import { EuiWindowEvent, keys, htmlIdGenerator } from '../../services'; import { EuiFocusTrap } from '../focus_trap'; -import { EuiOverlayMask } from '../overlay_mask'; +import { EuiOverlayMask, EuiOverlayMaskProps } from '../overlay_mask'; import { CommonProps } from '../common'; import { EuiButtonEmpty, EuiButtonEmptyProps } from '../button'; import { EuiI18n } from '../i18n'; @@ -69,6 +69,10 @@ export type EuiCollapsibleNavProps = CommonProps & */ closeButtonProps?: EuiButtonEmptyProps; onClose?: () => void; + /** + * Adjustments to the EuiOverlayMask + */ + maskProps?: EuiOverlayMaskProps; }; export const EuiCollapsibleNav: FunctionComponent = ({ @@ -83,6 +87,7 @@ export const EuiCollapsibleNav: FunctionComponent = ({ closeButtonProps, onClose, id, + maskProps, ...rest }) => { const [flyoutID] = useState(id || htmlIdGenerator()('euiCollapsibleNav')); @@ -141,7 +146,13 @@ export const EuiCollapsibleNav: FunctionComponent = ({ let optionalOverlay; if (!navIsDocked) { - optionalOverlay = ; + optionalOverlay = ( + + ); } // Show a trigger button if one was passed but diff --git a/src/components/flyout/__snapshots__/flyout.test.tsx.snap b/src/components/flyout/__snapshots__/flyout.test.tsx.snap index 1744e4a6802..f19ba8d840b 100644 --- a/src/components/flyout/__snapshots__/flyout.test.tsx.snap +++ b/src/components/flyout/__snapshots__/flyout.test.tsx.snap @@ -44,7 +44,7 @@ exports[`EuiFlyout is rendered 1`] = `
`; -exports[`EuiFlyout max width can be set to a custom number 1`] = ` +exports[`EuiFlyout props accepts div props 1`] = `
+ />
`; -exports[`EuiFlyout max width can be set to a default 1`] = ` +exports[`EuiFlyout props max width can be set to a custom number 1`] = `
`; -exports[`EuiFlyout size l is rendered 1`] = ` +exports[`EuiFlyout props ownFocus can alter mask props with maskProps without throwing error 1`] = ` +Array [ +
, +
+
+
+
+ +
+
+
, +] +`; + +exports[`EuiFlyout props ownFocus is rendered 1`] = ` +Array [ +
, +
+
+
+
+ +
+
+
, +] +`; + +exports[`EuiFlyout props size l is rendered 1`] = `
`; -exports[`EuiFlyout size m is rendered 1`] = ` +exports[`EuiFlyout props size m is rendered 1`] = `
`; -exports[`EuiFlyout size s is rendered 1`] = ` +exports[`EuiFlyout props size s is rendered 1`] = `
({ + EuiOverlayMask: ({ headerZindexLocation, ...props }: any) => ( +
+ ), +})); + const SIZES: EuiFlyoutSize[] = ['s', 'm', 'l']; describe('EuiFlyout', () => { @@ -71,41 +77,63 @@ describe('EuiFlyout', () => { expect(component).toMatchSnapshot(); }); - }); - describe('size', () => { - SIZES.forEach(size => { - it(`${size} is rendered`, () => { - const component = render( {}} size={size} />); + describe('size', () => { + SIZES.forEach(size => { + it(`${size} is rendered`, () => { + const component = render( + {}} size={size} /> + ); - expect(component).toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); }); }); - }); - describe('max width', () => { - test('can be set to a default', () => { - const component = render( - {}} maxWidth={true} /> - ); + describe('max width', () => { + test('can be set to a default', () => { + const component = render( + {}} maxWidth={true} /> + ); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); + }); - test('can be set to a custom number', () => { - const component = render( - {}} maxWidth={1024} /> - ); + test('can be set to a custom number', () => { + const component = render( + {}} maxWidth={1024} /> + ); - expect(component).toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); + + test('can be set to a custom value and measurement', () => { + const component = render( + {}} maxWidth="24rem" /> + ); + + expect(component).toMatchSnapshot(); + }); }); - test('can be set to a custom value and measurement', () => { - const component = render( - {}} maxWidth="24rem" /> - ); + describe('ownFocus', () => { + test('is rendered', () => { + const component = render( {}} ownFocus />); - expect(component).toMatchSnapshot(); + expect(component).toMatchSnapshot(); + }); + + test('can alter mask props with maskProps without throwing error', () => { + const component = render( + {}} + ownFocus + maskProps={{ headerZindexLocation: 'above' }} + /> + ); + + expect(component).toMatchSnapshot(); + }); }); }); }); diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx index 88918718b8d..1077bd16a17 100644 --- a/src/components/flyout/flyout.tsx +++ b/src/components/flyout/flyout.tsx @@ -30,7 +30,7 @@ import { keys, EuiWindowEvent } from '../../services'; import { CommonProps } from '../common'; import { EuiFocusTrap } from '../focus_trap'; -import { EuiOverlayMask } from '../overlay_mask'; +import { EuiOverlayMask, EuiOverlayMaskProps } from '../overlay_mask'; import { EuiButtonIcon } from '../button'; import { EuiI18n } from '../i18n'; @@ -55,7 +55,8 @@ export interface EuiFlyoutProps */ hideCloseButton?: boolean; /** - * Locks the mouse / keyboard focus to within the flyout + * Locks the mouse / keyboard focus to within the flyout, + * and shows an EuiOverlayMask */ ownFocus?: boolean; /** @@ -73,6 +74,11 @@ export interface EuiFlyoutProps maxWidth?: boolean | number | string; style?: CSSProperties; + + /** + * Adjustments to the EuiOverlayMask that is added when `ownFocus = true` + */ + maskProps?: EuiOverlayMaskProps; } export const EuiFlyout: FunctionComponent = ({ @@ -85,6 +91,7 @@ export const EuiFlyout: FunctionComponent = ({ closeButtonAriaLabel, maxWidth = false, style, + maskProps, ...rest }) => { const onKeyDown = (event: KeyboardEvent) => { @@ -152,7 +159,13 @@ export const EuiFlyout: FunctionComponent = ({ // to click it to close it. let optionalOverlay; if (ownFocus) { - optionalOverlay = ; + optionalOverlay = ( + + ); } return ( diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss index 948e76fe187..328d51f40c6 100644 --- a/src/components/header/_header.scss +++ b/src/components/header/_header.scss @@ -19,13 +19,6 @@ } } -.euiBody--collapsibleNavIsOpen, -.euiBody--hasFlyout { - .euiHeader--fixed { - z-index: $euiZModal + 1; - } -} - .euiHeader--fixed + .euiHeader--fixed { top: $euiHeaderHeightCompensation; } diff --git a/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap index 80a9ffae819..e94cccfdafc 100644 --- a/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -17,12 +17,12 @@ exports[`EuiHeaderBreadcrumbs is rendered 1`] = `
- Reptiles - +
diff --git a/src/components/header/header_links/_header_link.scss b/src/components/header/header_links/_header_link.scss index 42f656488ef..de088d4c576 100644 --- a/src/components/header/header_links/_header_link.scss +++ b/src/components/header/header_links/_header_link.scss @@ -1,13 +1,14 @@ +@import '../../link/mixins'; + .euiHeaderLink { @include euiLink; - margin: 0 $euiSizeS; } .euiHeaderLinks__mobileList { .euiHeaderLink { display: block; + width: 100%; padding: $euiSizeS; - margin: 0; // EuiButtons normally center, which makes sense. In mobile though we want // them to align left. This is a safe hack given the specificity. diff --git a/src/components/header/header_links/_header_links.scss b/src/components/header/header_links/_header_links.scss index f77ac959eba..862350756ea 100644 --- a/src/components/header/header_links/_header_links.scss +++ b/src/components/header/header_links/_header_links.scss @@ -1,5 +1,3 @@ -@import '../../link/mixins'; - .euiHeaderLinks { display: flex; justify-content: space-between; @@ -11,6 +9,11 @@ overflow: hidden; display: flex; align-items: center; + + > * { + // Apply margins to any children + margin: 0 $euiSizeS; + } } .euiHeaderLinks__mobile { diff --git a/src/components/overlay_mask/_overlay_mask.scss b/src/components/overlay_mask/_overlay_mask.scss index d17517fc44b..4f2fddc112e 100644 --- a/src/components/overlay_mask/_overlay_mask.scss +++ b/src/components/overlay_mask/_overlay_mask.scss @@ -1,6 +1,5 @@ .euiOverlayMask { position: fixed; - z-index: $euiZMask; top: 0; left: 0; right: 0; @@ -23,3 +22,12 @@ .euiBody-hasOverlayMask { overflow: hidden; } + +// Handling the z-index based on whether it should be displayed above or below the header +.euiOverlayMask--aboveHeader { + z-index: $euiZMask; +} + +.euiOverlayMask--belowHeader { + z-index: $euiZHeader - 1; +} diff --git a/src/components/overlay_mask/overlay_mask.tsx b/src/components/overlay_mask/overlay_mask.tsx index 57efb9f8307..981221cc719 100644 --- a/src/components/overlay_mask/overlay_mask.tsx +++ b/src/components/overlay_mask/overlay_mask.tsx @@ -35,8 +35,15 @@ import classNames from 'classnames'; import { CommonProps, keysOf } from '../common'; export interface EuiOverlayMaskInterface { + /** + * Function that applies to clicking the mask itself and not the children + */ onClick?: () => void; children?: ReactNode; + /** + * Should the mask visually sit above or below the EuiHeader (controlled by z-index) + */ + headerZindexLocation?: 'above' | 'below'; } export type EuiOverlayMaskProps = CommonProps & @@ -50,6 +57,7 @@ export const EuiOverlayMask: FunctionComponent = ({ className, children, onClick, + headerZindexLocation = 'above', ...rest }) => { const overlayMaskNode = useRef(document.createElement('div')); @@ -89,8 +97,12 @@ export const EuiOverlayMask: FunctionComponent = ({ useEffect(() => { if (!overlayMaskNode.current) return; - overlayMaskNode.current.className = classNames('euiOverlayMask', className); - }, [className]); + overlayMaskNode.current.className = classNames( + 'euiOverlayMask', + `euiOverlayMask--${headerZindexLocation}Header`, + className + ); + }, [className, headerZindexLocation]); useEffect(() => { if (!overlayMaskNode.current || !onClick) return; diff --git a/src/global_styling/variables/_z_index.scss b/src/global_styling/variables/_z_index.scss index a092f5bc1f0..2cf4bc9e27c 100644 --- a/src/global_styling/variables/_z_index.scss +++ b/src/global_styling/variables/_z_index.scss @@ -27,6 +27,7 @@ $euiZLevel9: 9000; $euiZContent: $euiZLevel0; $euiZHeader: $euiZLevel1; $euiZContentMenu: $euiZLevel2; +$euiZFlyout: $euiZLevel3; $euiZNavigation: $euiZLevel4; $euiZMask: $euiZLevel6; $euiZModal: $euiZLevel8; diff --git a/src/themes/eui-amsterdam/overrides/_flyout.scss b/src/themes/eui-amsterdam/overrides/_flyout.scss deleted file mode 100644 index 6e78d2fe0ce..00000000000 --- a/src/themes/eui-amsterdam/overrides/_flyout.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Amsterdam shadows extend upwards as well, but this means flyouts shadows can overlap fixed headers. -// The clip path ensures only the left side of the shadow is exposed. -.euiFlyout { - clip-path: polygon(-10% 0, 100% 0, 100% 100%, -10% 100%); -} diff --git a/src/themes/eui-amsterdam/overrides/_header.scss b/src/themes/eui-amsterdam/overrides/_header.scss index f32c374f462..1f9a52cbb36 100644 --- a/src/themes/eui-amsterdam/overrides/_header.scss +++ b/src/themes/eui-amsterdam/overrides/_header.scss @@ -32,8 +32,8 @@ // Breadcrumbs .euiHeaderBreadcrumbs { - @include euiFontSizeXS; - font-weight: $euiFontWeightMedium; + font-size: $euiFontSizeXS; + line-height: $euiSize; margin-left: $euiSizeS; margin-right: $euiSizeS; @@ -46,7 +46,9 @@ // still be default text only breadcrumbs for places like EuiControlBar .euiBreadcrumb { @include euiButtonDefaultStyle($euiTextColor); - padding: $euiSizeXS $euiSizeM $euiSizeXS $euiSize; + line-height: $euiSize; + font-weight: $euiFontWeightMedium; + padding: $euiSizeXS $euiSize; clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%, $euiSizeS 50%); // If it's a link the easiest way to detect is via our .euiLink class since it can accept either href or onClick @@ -75,15 +77,23 @@ &:first-child { padding-left: $euiSizeM; - border-radius: $euiSizeS 0 0 $euiSizeS; + border-radius: $euiBorderRadius 0 0 $euiBorderRadius; clip-path: polygon(0 0, calc(100% - #{$euiSizeS}) 0, 100% 50%, calc(100% - #{$euiSizeS}) 100%, 0 100%); } } .euiBreadcrumb--last { - border-radius: 0 #{$euiSizeS} #{$euiSizeS} 0; + border-radius: 0 $euiBorderRadius $euiBorderRadius 0; padding-right: $euiSizeM; clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, #{$euiSizeS} 50%); } + + // In case the item is first AND last, aka only, just make it a fully rounded item + .euiBreadcrumb:only-child { + clip-path: none; + padding-left: $euiSizeM; + padding-right: $euiSizeM; + border-radius: $euiBorderRadius; + } } diff --git a/src/themes/eui-amsterdam/overrides/_index.scss b/src/themes/eui-amsterdam/overrides/_index.scss index a8fcf7a02d1..d02573172ba 100644 --- a/src/themes/eui-amsterdam/overrides/_index.scss +++ b/src/themes/eui-amsterdam/overrides/_index.scss @@ -4,7 +4,6 @@ @import 'call_out'; @import 'code'; @import 'filter_group'; -@import 'flyout'; @import 'header'; @import 'image'; @import 'modal';