diff --git a/CHANGELOG.md b/CHANGELOG.md index 215f03d3496..c90c82f0baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ - Added `textTransform` property to `schemaDetectors` prop of `EuiDataGrid`([#4752](https://github.com/elastic/eui/pull/4752)) - Added `color`, `continuityAbove`, `continuityAboveBelow`, `continuityBelow`, `continuityWithin`, `eraser`, `fullScreenExit`, `function`, `percent`, `wordWrap`, and `wordWrapDisabled` glyphs to `EuiIcon` ([#4779](https://github.com/elastic/eui/pull/4779)) +- Added `as`, `role`, `closeButtonProps`, `closeButtonPosition`, `outsideClickCloses`, `side`, `type`, and `pushMinBreakpoint` props to `EuiFlyout` ([#4713](https://github.com/elastic/eui/pull/4713)) +- Extended `EuiFlyout` `size` prop to accept any CSS `width` value ([#4713](https://github.com/elastic/eui/pull/4713)) +- Extended `EuiFlyout` and most of its props in `EuiCollapsibleNav` ([#4713](https://github.com/elastic/eui/pull/4713)) + +**Breaking changes** + +- Changed the default of `EuiFlyout` `ownFocus` to `true` ([#4713](https://github.com/elastic/eui/pull/4713)) +- Wrapped `EuiFlyout` within the `EuiOverlayMask` when `ownFocus=true` ([#4713](https://github.com/elastic/eui/pull/4713)) +- Changed `EuiCollapsibleNav` width sizing from a Sass variable to a `size` prop ([#4713](https://github.com/elastic/eui/pull/4713)) +- Changed `EuiOverlayMask` z-indexing when positioned `below` header to using `top` offset ([#4713](https://github.com/elastic/eui/pull/4713)) **Bug fixes** diff --git a/src-docs/src/components/guide_components.scss b/src-docs/src/components/guide_components.scss index 56689840182..992a3bd721a 100644 --- a/src-docs/src/components/guide_components.scss +++ b/src-docs/src/components/guide_components.scss @@ -211,7 +211,7 @@ $elasticLogoTextDark: #1C1E23; height: 100%; left: 0; top: 0; - z-index: $euiZHeader + 1; + z-index: $euiZHeader; overflow: auto; &--withHeader { diff --git a/src-docs/src/services/playground/knobs.js b/src-docs/src/services/playground/knobs.js index 92b4eac1d49..73f46159645 100644 --- a/src-docs/src/services/playground/knobs.js +++ b/src-docs/src/services/playground/knobs.js @@ -324,7 +324,7 @@ const Knob = ({ }} /> ); - } else return null; + } else return helpText || null; case PropTypes.Custom: if (custom && custom.use) { @@ -352,9 +352,9 @@ const Knob = ({ case PropTypes.Function: case PropTypes.Array: case PropTypes.Object: - return null; + return helpText || null; default: - return assertUnreachable(); + return helpText || assertUnreachable(); } }; diff --git a/src-docs/src/views/accordion/accordion_multiple.js b/src-docs/src/views/accordion/accordion_multiple.js index 5167bc76a34..0d801cfec29 100644 --- a/src-docs/src/views/accordion/accordion_multiple.js +++ b/src-docs/src/views/accordion/accordion_multiple.js @@ -45,7 +45,7 @@ export default () => ( paddingSize="m">

- This content area will grow to accomodate when the accordion below + This content area will grow to accommodate when the accordion below opens

diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx index bec9da37bea..fa5925e5469 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav.tsx +++ b/src-docs/src/views/collapsible_nav/collapsible_nav.tsx @@ -8,16 +8,25 @@ import { EuiText } from '../../../../src/components/text'; import { EuiCode } from '../../../../src/components/code'; export default () => { - const [navIsOpen, setNavIsOpen] = useState(false); - const [navIsDocked, setNavIsDocked] = useState(false); + const [navIsOpen, setNavIsOpen] = useState( + JSON.parse( + String(localStorage.getItem('euiCollapsibleNavExample--isDocked')) + ) || false + ); + const [navIsDocked, setNavIsDocked] = useState( + JSON.parse( + String(localStorage.getItem('euiCollapsibleNavExample--isDocked')) + ) || false + ); return ( <> setNavIsOpen(!navIsOpen)}> + setNavIsOpen((isOpen) => !isOpen)}> Toggle nav } @@ -27,9 +36,20 @@ export default () => {

I am some nav

+ +

+ The docked status is being stored in{' '} + localStorage. +

+
+ { setNavIsDocked(!navIsDocked); + localStorage.setItem( + 'euiCollapsibleNavExample--isDocked', + JSON.stringify(!navIsDocked) + ); }}> Docked: {navIsDocked ? 'on' : 'off'} @@ -39,8 +59,9 @@ export default () => { {navIsDocked && (

- The button gets hidden by default when the nav is docked unless you - set showButtonIfDocked = true. + The button gets hidden by default when the nav is + docked unless you set{' '} + showButtonIfDocked = true.

)} diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx index d498c1c6992..5de38125816 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_all.tsx @@ -56,11 +56,9 @@ const LearnLinks: EuiPinnableListGroupItemProps[] = [ ]; export default () => { - const [navIsOpen, setNavIsOpen] = useState( - JSON.parse(String(localStorage.getItem('navIsDocked'))) || false - ); + const [navIsOpen, setNavIsOpen] = useState(true); const [navIsDocked, setNavIsDocked] = useState( - JSON.parse(String(localStorage.getItem('navIsDocked'))) || false + JSON.parse(String(localStorage.getItem('nav2IsDocked'))) || false ); /** @@ -234,7 +232,7 @@ export default () => { onClick={() => { setNavIsDocked(!navIsDocked); localStorage.setItem( - 'navIsDocked', + 'nav2IsDocked', JSON.stringify(!navIsDocked) ); }} diff --git a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js index 76ece572a5b..e2d3dc7b849 100644 --- a/src-docs/src/views/collapsible_nav/collapsible_nav_example.js +++ b/src-docs/src/views/collapsible_nav/collapsible_nav_example.js @@ -1,8 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { renderToHtml } from '../../services'; - import { GuideSectionTypes } from '../../components'; import { @@ -11,23 +9,22 @@ import { EuiText, EuiCallOut, EuiCollapsibleNavGroup, + EuiHorizontalRule, } from '../../../../src/components'; +import { collapsibleNavConfig } from './playground'; + import CollapsibleNav from './collapsible_nav'; const collapsibleNavSource = require('!!raw-loader!./collapsible_nav'); -const collapsibleNavHtml = renderToHtml(CollapsibleNav); import CollapsibleNavGroup from './collapsible_nav_group'; const collapsibleNavGroupSource = require('!!raw-loader!./collapsible_nav_group'); -const collapsibleNavGroupHtml = renderToHtml(CollapsibleNavGroup); import CollapsibleNavList from './collapsible_nav_list'; const collapsibleNavListSource = require('!!raw-loader!./collapsible_nav_list'); -const collapsibleNavListHtml = renderToHtml(CollapsibleNavList); import CollapsibleNavAll from './collapsible_nav_all'; const collapsibleNavAllSource = require('!!raw-loader!./collapsible_nav_all'); -const collapsibleNavAllHtml = renderToHtml(CollapsibleNavAll); export const CollapsibleNavExample = { title: 'Collapsible nav', @@ -46,19 +43,15 @@ export const CollapsibleNavExample = { type: GuideSectionTypes.JS, code: collapsibleNavSource, }, - { - type: GuideSectionTypes.HTML, - code: collapsibleNavHtml, - }, ], text: ( <>

- EuiCollapsibleNav is a similar implementation to{' '} + EuiCollapsibleNav is a custom implementation of{' '} EuiFlyout - ; the visibility of which must be maintained by the consuming + ; the visibility of which must still be maintained by the consuming application. An extra feature that it provides is the ability to{' '} dock the flyout. This affixes the flyout to the window and pushes the body content by adding left side padding. @@ -72,11 +65,13 @@ export const CollapsibleNavExample = { props: { EuiCollapsibleNav }, demo: , snippet: ` setNavIsOpen(!navIsOpen)}>Toggle nav} isOpen={navIsOpen} isDocked={navIsDocked} onClose={() => setNavIsOpen(false)} />`, + playground: collapsibleNavConfig, }, { title: 'Collapsible nav group', @@ -85,10 +80,6 @@ export const CollapsibleNavExample = { type: GuideSectionTypes.JS, code: collapsibleNavGroupSource, }, - { - type: GuideSectionTypes.HTML, - code: collapsibleNavGroupHtml, - }, ], text: ( <> @@ -130,10 +121,6 @@ export const CollapsibleNavExample = { type: GuideSectionTypes.JS, code: collapsibleNavListSource, }, - { - type: GuideSectionTypes.HTML, - code: collapsibleNavListHtml, - }, ], text: ( <> @@ -152,7 +139,15 @@ export const CollapsibleNavExample = {

Below are a few established patterns to use.

), - demo: , + demo: ( +
+ + +
+ ), + demoPanelProps: { + paddingSize: 'none', + }, snippet: ` diff --git a/src-docs/src/views/collapsible_nav/playground.js b/src-docs/src/views/collapsible_nav/playground.js new file mode 100644 index 00000000000..de52a76f769 --- /dev/null +++ b/src-docs/src/views/collapsible_nav/playground.js @@ -0,0 +1,54 @@ +import { PropTypes } from 'react-view'; +import { EuiCollapsibleNav } from '../../../../src/components/'; +import { propUtilityForPlayground } from '../../services/playground'; + +export const collapsibleNavConfig = () => { + const docgenInfo = Array.isArray(EuiCollapsibleNav.__docgenInfo) + ? EuiCollapsibleNav.__docgenInfo[0] + : EuiCollapsibleNav.__docgenInfo; + const propsToUse = propUtilityForPlayground(docgenInfo.props); + + propsToUse.isOpen = { + ...propsToUse.isOpen, + value: true, + }; + + propsToUse.ownFocus = { + ...propsToUse.ownFocus, + value: false, + disabled: true, + }; + + propsToUse.size = { + ...propsToUse.size, + type: PropTypes.Number, + value: 240, + }; + + propsToUse.as = { + ...propsToUse.as, + type: PropTypes.string, + value: 'nav', + }; + + propsToUse.as = { + ...propsToUse.as, + type: PropTypes.string, + value: 'nav', + }; + + return { + config: { + componentName: 'EuiCollapsibleNav', + props: propsToUse, + scope: { + EuiCollapsibleNav, + }, + imports: { + '@elastic/eui': { + named: ['EuiCollapsibleNav'], + }, + }, + }, + }; +}; diff --git a/src-docs/src/views/flyout/flyout_example.js b/src-docs/src/views/flyout/flyout_example.js index 38e6ecd39e2..c57729a21ae 100644 --- a/src-docs/src/views/flyout/flyout_example.js +++ b/src-docs/src/views/flyout/flyout_example.js @@ -33,7 +33,10 @@ const flyoutMaxWidthSource = require('!!raw-loader!./flyout_max_width'); import FlyoutWithBanner from './flyout_banner'; const flyoutWithBannerSource = require('!!raw-loader!./flyout_banner'); -const flyOutSnippet = ` +import FlyoutPush from './flyout_push'; +const flyoutPushSource = require('!!raw-loader!./flyout_push'); + +const flyOutSnippet = `

@@ -45,7 +48,7 @@ const flyOutSnippet = ` `; -const flyoutComplicatedSnippet = ` +const flyoutComplicatedSnippet = `

@@ -63,7 +66,7 @@ const flyoutComplicatedSnippet = ` `; -const flyoutSmallSnippet = ` +const flyoutSmallSnippet = `

@@ -89,7 +92,7 @@ const flyoutMediumPaddingSnippet = ` `; -const flyoutMaxWidthSnippet = ` +const flyoutMaxWidthSnippet = `

@@ -101,7 +104,7 @@ const flyoutMaxWidthSnippet = ` `; -const flyoutLargeSnippet = ` +const flyoutLargeSnippet = `

@@ -113,7 +116,7 @@ const flyoutLargeSnippet = ` `; -const flyoutWithBannerSnippet = ` +const flyoutWithBannerSnippet = `

@@ -125,6 +128,21 @@ const flyoutWithBannerSnippet = ` `; +const flyoutPushedSnippet = ` + + +

+
+
+ + + + + Close + +
+`; + export const FlyoutExample = { title: 'Flyout', sections: [ @@ -139,7 +157,7 @@ export const FlyoutExample = { <>

EuiFlyout is a fixed position panel that pops in - from the right side of the screen. It should be used to reveal more + from the side of the window. It should be used to reveal more detailed contextual information or to provide complex forms without losing the user's current state. It is a good alternative to{' '} modals when the action is not @@ -156,9 +174,8 @@ export const FlyoutExample = { iconType="accessibility" title={ <> - Use {'aria-labelledby={headingId}'} and{' '} - ownFocus to announce the flyout to screen - readers. + Use {'aria-labelledby={headingId}'} to + announce the flyout to screen readers. } /> @@ -203,9 +220,11 @@ export const FlyoutExample = { ], text: (

- Flyouts come in three predefined sizes,{' '} - {"'x' | 'm' | 'l'"}, which define the width - relative to the window size with a minimum width defined in pixels. + Flyouts come in three predefined sizes of{' '} + {"'s' | 'm' | 'l'"}, which define the width{' '} + relative to the window size with a minimum width + defined in pixels. You can otherwise supply your own fixed width in + number or string format.

), snippet: flyoutLargeSnippet, @@ -226,7 +245,7 @@ export const FlyoutExample = { wrapping EuiFlyout component. This ensures that all the horizontal edges line up no matter the{' '} paddingSize. When using the{' '} - {'"none"'} size, you will need to accomodate your + {'"none"'} size, you will need to accommodate your content with some other way of creating distance to the edges of the flyout.

@@ -269,13 +288,17 @@ export const FlyoutExample = { text: ( <>

- Like modals, you can, and usually want to, obscure the page content - beneath with ownFocus which adds an{' '} + Like modals, you will usually want to obscure the page content + beneath with ownFocus which wraps the flyout with + an{' '} EuiOverlayMask - - . By not adding this prop, the the underlying page content will be - visible and clickable. + {' '} + . However, there are use-cases where flyouts present more + information or controls, but need to maintain the interactions of + the page content. By setting{' '} + {'ownFocus={false}'}, the + underlying page content will be visible and clickable.

), @@ -283,6 +306,35 @@ export const FlyoutExample = { demo: , props: { EuiFlyout }, }, + { + title: 'Push versus overlay', + source: [ + { + type: GuideSectionTypes.JS, + code: flyoutPushSource, + }, + ], + text: ( + +

+ Another way to allow for continued interactions of the page content + while a flyout is visible, is to change the type{' '} + from overlay to push. +

+

+ A pushed flyout still positions itself as fixed, + but adds padding to the document's body element to accommodate + for the flyout's width. Because this squishes the page content, + the flyout changes back to overlay at smaller + window widths. You can adjust this minimum breakpoint with{' '} + pushMinBreakpoint. +

+
+ ), + snippet: flyoutPushedSnippet, + demo: , + props: { EuiFlyout }, + }, { title: 'Understanding max-width', source: [ diff --git a/src-docs/src/views/flyout/flyout_large.js b/src-docs/src/views/flyout/flyout_large.js index 9a8808af71e..d7e5b591d38 100644 --- a/src-docs/src/views/flyout/flyout_large.js +++ b/src-docs/src/views/flyout/flyout_large.js @@ -28,6 +28,10 @@ export default () => { id: 'l', label: 'Large', }, + { + id: '400px', + label: 'Fixed (400)', + }, ]; const closeFlyout = () => setIsFlyoutVisible(false); diff --git a/src-docs/src/views/flyout/flyout_max_width.js b/src-docs/src/views/flyout/flyout_max_width.js index 4508f449921..8dae53b4738 100644 --- a/src-docs/src/views/flyout/flyout_max_width.js +++ b/src-docs/src/views/flyout/flyout_max_width.js @@ -167,6 +167,27 @@ export default () => { + showFlyout(240)}> + Show 240 flyout with no max-width + + + showFlyout(240, true)}> + Show 240 flyout with default max-width + + + showFlyout(240, 110)}> + Show 240 flyout with{' '} + smaller custom max-width -- max-width wins but width + wins on small screens + + + showFlyout(240, 1600)}> + Show 240 flyout with{' '} + larger custom max-width + + + + showFlyout('m', 0)}> Trick for forms: Medium flyout with{' '} 0 as max-width diff --git a/src-docs/src/views/flyout/flyout_push.js b/src-docs/src/views/flyout/flyout_push.js new file mode 100644 index 00000000000..5ba18cebc6b --- /dev/null +++ b/src-docs/src/views/flyout/flyout_push.js @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiButton, + EuiText, + EuiTitle, + EuiFlyoutFooter, +} from '../../../../src/components'; + +export default () => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + let flyout; + + if (isFlyoutVisible) { + flyout = ( + setIsFlyoutVisible(false)} + aria-labelledby="pushedFlyoutTitle"> + + +

A pushed flyout

+
+
+ + +

+ A pushed flyout typically contains more information about a + particular piece of data or complex form controls for editing. +

+

+ Also, it is good to include a close button in the footer for a + larger hit target than the small close button provides. +

+
+
+ + setIsFlyoutVisible(false)}>Close + +
+ ); + } + + return ( +
+ setIsFlyoutVisible((visible) => !visible)}> + Toggle pushed flyout + + {flyout} +
+ ); +}; diff --git a/src-docs/src/views/flyout/flyout_small.js b/src-docs/src/views/flyout/flyout_small.js index b11fa269fa2..852791c5bdf 100644 --- a/src-docs/src/views/flyout/flyout_small.js +++ b/src-docs/src/views/flyout/flyout_small.js @@ -22,7 +22,10 @@ export default () => { let flyout; if (isFlyoutVisible) { flyout = ( - +

A flyout without ownFocus

diff --git a/src-docs/src/views/header/header_alert.js b/src-docs/src/views/header/header_alert.js index 49c0a318cb3..c349637bd56 100644 --- a/src-docs/src/views/header/header_alert.js +++ b/src-docs/src/views/header/header_alert.js @@ -304,7 +304,7 @@ const HeaderUserMenu = () => { export default () => { const [position, setPosition] = useState('static'); - const [theme, setTheme] = useState('light'); + const [theme, setTheme] = useState('default'); return ( <> @@ -321,7 +321,7 @@ export default () => { setTheme(e.target.checked ? 'dark' : 'light')} + onChange={(e) => setTheme(e.target.checked ? 'dark' : 'default')} /> diff --git a/src-docs/src/views/page/page_bottom_bar.js b/src-docs/src/views/page/page_bottom_bar.js index fc40a28c61e..1bceed1b972 100644 --- a/src-docs/src/views/page/page_bottom_bar.js +++ b/src-docs/src/views/page/page_bottom_bar.js @@ -15,7 +15,7 @@ export default ({ button = <>, content, sideNav, bottomBar }) => { {sideNav} - {/* Double EuiPageBody to accomodate for the bottom bar */} + {/* Double EuiPageBody to accommodate for the bottom bar */} 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 + means not needing to accommodate for the height of the bar in the body element.

( ...wrappingExampleStyle, }}> @@ -61,7 +61,7 @@ export default () => ( style={{ height: 200, overflowY: 'hidden' }}> `} /> @@ -81,7 +81,7 @@ export default () => ( } example={
-
+

Orbiting this at a distance of roughly ninety-two million miles @@ -95,7 +95,7 @@ export default () => ( } snippet={` + tabIndex={0}> `} /> 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 ab021c7d1a5..6b8a3a1bfd9 100644 --- a/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap @@ -2,80 +2,47 @@ exports[`EuiCollapsibleNav close button can be hidden 1`] = ` Array [ -

, -
, -
, -
- +
+
, -
, ] `; @@ -83,64 +50,54 @@ exports[`EuiCollapsibleNav does not render if isOpen is false 1`] = `null`; exports[`EuiCollapsibleNav is rendered 1`] = ` Array [ -
, -
, -
, -
- + + +
+
, -
, ] `; -exports[`EuiCollapsibleNav props button 1`] = ` +exports[`EuiCollapsibleNav props accepts EuiFlyout props 1`] = ` Array [ -
, @@ -187,101 +140,100 @@ Array [ ] `; -exports[`EuiCollapsibleNav props can alter mask props with maskProps without throwing error 1`] = ` +exports[`EuiCollapsibleNav props button 1`] = ` Array [ -
, -
, -
, -
- + + +
+
, -
, ] `; exports[`EuiCollapsibleNav props dockedBreakpoint 1`] = ` Array [ -
, -
, -
, -
- + + +
+
, -
, ] `; @@ -301,29 +253,11 @@ Array [ data-focus-lock-disabled="disabled" > + style="width:320px" + tabindex="-1" + />
,
, -
, -
, -
- + + +
+
, -
, ] `; @@ -388,7 +319,6 @@ Array [ aria-controls="id" aria-expanded="true" aria-pressed="true" - class="euiCollapsibleNav__toggle" />,
, +
, +] +`; + +exports[`EuiCollapsibleNav props size 1`] = ` +Array [ +
+
+
+
- - + + +
+
, -
, ] `; diff --git a/src/components/collapsible_nav/_collapsible_nav.scss b/src/components/collapsible_nav/_collapsible_nav.scss index d63860af27b..7f3b7c291c8 100644 --- a/src/components/collapsible_nav/_collapsible_nav.scss +++ b/src/components/collapsible_nav/_collapsible_nav.scss @@ -1,73 +1,5 @@ -// Extends euiFlyout -@import '../flyout/variables'; -@import '../flyout/mixins'; +// Extends -.euiCollapsibleNav { - @include euiFlyout; - border-left: none; - right: auto; - left: 0; - 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 { - position: absolute; - right: 0; - top: $euiSize; - margin-right: -27%; - padding: $euiSizeM 0; - line-height: 1; - border-radius: $euiBorderRadius; - - &:focus { - @include euiFocusRing; - // Override default `EuiButtonEmpty` :focus background to ensure better contrast - background: $euiColorEmptyShade !important; // sass-lint:disable-line no-important - } -} - -// The addition of this class is handled through JS -// via the `dockingBreakpoint` and `isDocked` combination -.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; - } -} - -.euiBody--collapsibleNavIsDocked { - // Shrink the content from the left so it's no longer overlapped by the nav drawer (ALWAYS) - padding-left: $euiCollapsibleNavWidth !important; // sass-lint:disable-line no-important - transition: padding $euiAnimSpeedFast $euiAnimSlightResistance; -} - -@include euiBreakpoint('xs') { - // At tiny screens, reduce the close button to a simple `x` - .euiCollapsibleNav__closeButton { - margin-right: -15%; - - .euiCollapsibleNav__closeButtonText { - // But be sure the text can still be read by a screen reader - @include euiScreenReaderOnly; - } - } -} - -// Specific keyframes so in comes in from the left -@keyframes euiCollapsibleNavIn { - 0% { - opacity: 0; - transform: translateX(-100%); - } - - 75% { - opacity: 1; - transform: translateX(0%); - } +.euiCollapsibleNav:not([class*='push']) { + z-index: $euiZNavigation !important; // sass-lint:disable-line no-important } diff --git a/src/components/collapsible_nav/_variables.scss b/src/components/collapsible_nav/_variables.scss index fbb573d7d2b..20d9200b330 100644 --- a/src/components/collapsible_nav/_variables.scss +++ b/src/components/collapsible_nav/_variables.scss @@ -1,6 +1,4 @@ // Sizing -$euiCollapsibleNavWidth: $euiSize * 20 !default; // ~ 320px - $euiCollapsibleNavGroupLightBackgroundColor: $euiPageBackgroundColor !default; $euiCollapsibleNavGroupDarkBackgroundColor: lightOrDarkTheme( diff --git a/src/components/collapsible_nav/collapsible_nav.test.tsx b/src/components/collapsible_nav/collapsible_nav.test.tsx index 980559a5562..7f926a0d670 100644 --- a/src/components/collapsible_nav/collapsible_nav.test.tsx +++ b/src/components/collapsible_nav/collapsible_nav.test.tsx @@ -22,6 +22,7 @@ import { render, mount } from 'enzyme'; import { requiredProps, takeMountedSnapshot } from '../../test'; import { EuiCollapsibleNav } from './collapsible_nav'; +import { EuiOverlayMaskProps } from '../overlay_mask'; jest.mock('../overlay_mask', () => ({ EuiOverlayMask: ({ headerZindexLocation, ...props }: any) => ( @@ -29,7 +30,17 @@ jest.mock('../overlay_mask', () => ({ ), })); -const propsNeededToRender = { id: 'id', isOpen: true }; +jest.mock('../portal', () => ({ + EuiPortal: ({ children }: { children: any }) => children, +})); + +const propsNeededToRender = { id: 'id', isOpen: true, onClose: () => {} }; +const flyoutProps = { + size: 240, + ownFocus: false, + outsideClickCloses: false, + maskProps: { headerZindexLocation: 'above' } as EuiOverlayMaskProps, +}; describe('EuiCollapsibleNav', () => { test('is rendered', () => { @@ -57,6 +68,18 @@ describe('EuiCollapsibleNav', () => { ).toMatchSnapshot(); }); + test('size', () => { + const component = mount( + + ); + + expect( + takeMountedSnapshot(component, { + hasArrayOutput: true, + }) + ).toMatchSnapshot(); + }); + test('isDocked', () => { const component = render( @@ -106,12 +129,9 @@ describe('EuiCollapsibleNav', () => { ).toMatchSnapshot(); }); - test('can alter mask props with maskProps without throwing error', () => { + test('accepts EuiFlyout props', () => { const component = mount( - + ); expect( @@ -125,22 +145,7 @@ describe('EuiCollapsibleNav', () => { describe('close button', () => { test('can be hidden', () => { const component = mount( - - ); - - expect( - takeMountedSnapshot(component, { - hasArrayOutput: true, - }) - ).toMatchSnapshot(); - }); - - test('extends EuiButtonEmpty', () => { - const component = mount( - + ); expect( @@ -152,7 +157,7 @@ describe('EuiCollapsibleNav', () => { }); test('does not render if isOpen is false', () => { - const component = render(); + const component = render( {}} id="id" />); expect(component).toMatchSnapshot(); }); diff --git a/src/components/collapsible_nav/collapsible_nav.tsx b/src/components/collapsible_nav/collapsible_nav.tsx index 4d5ebf23931..35cf89aaecd 100644 --- a/src/components/collapsible_nav/collapsible_nav.tsx +++ b/src/components/collapsible_nav/collapsible_nav.tsx @@ -20,144 +20,109 @@ import React, { cloneElement, FunctionComponent, - HTMLAttributes, ReactElement, ReactNode, useEffect, useState, } from 'react'; import classNames from 'classnames'; +import { htmlIdGenerator, isWithinMinBreakpoint } from '../../services'; +import { EuiFlyout, EuiFlyoutProps } from '../flyout'; import { throttle } from '../color_picker/utils'; -import { EuiWindowEvent, htmlIdGenerator, keys } from '../../services'; -import { EuiFocusTrap } from '../focus_trap'; -import { EuiOverlayMask, EuiOverlayMaskProps } from '../overlay_mask'; -import { CommonProps } from '../common'; -import { EuiButtonEmpty, EuiButtonEmptyProps } from '../button'; -import { EuiI18n } from '../i18n'; -import { EuiScreenReaderOnly } from '../accessibility'; -export type EuiCollapsibleNavProps = CommonProps & - HTMLAttributes & { - /** - * ReactNode to render as this component's content - */ - children?: ReactNode; - /** - * Keeps navigation flyout visible and push `` content via padding - */ - isDocked?: boolean; - /** - * Pixel value for customizing the minimum window width for enabling docking - */ - dockedBreakpoint?: number; - /** - * Shows the navigation flyout - */ - isOpen?: boolean; - /** - * Button for controlling visible state of the nav - */ - button?: ReactElement; - /** - * Keeps the display of toggle button when in docked state - */ - showButtonIfDocked?: boolean; - /** - * Keeps the display of floating close button. - * If `false`, you must then keep the `button` displayed at all breakpoints. - */ - showCloseButton?: boolean; - /** - * Extend the props of the close button, an EuiButtonEmpty - */ - closeButtonProps?: EuiButtonEmptyProps; - onClose?: () => void; - /** - * Adjustments to the EuiOverlayMask - */ - maskProps?: EuiOverlayMaskProps; - }; +// Extend all the flyout props except `onClose` because we handle this internally +export type EuiCollapsibleNavProps = Omit< + EuiFlyoutProps, + 'closeButtonAriaLabel' | 'type' | 'pushBreakpoint' +> & { + /** + * ReactNode to render as this component's content + */ + children?: ReactNode; + /** + * Shows the navigation flyout + */ + isOpen?: boolean; + /** + * Keeps navigation flyout visible and push `` content via padding + */ + isDocked?: boolean; + /** + * Named breakpoint or pixel value for customizing the minimum window width to enable docking + */ + dockedBreakpoint?: EuiFlyoutProps['pushMinBreakpoint']; + /** + * Button for controlling visible state of the nav + */ + button?: ReactElement; + /** + * Keeps the display of toggle button when in docked state + */ + showButtonIfDocked?: boolean; +}; export const EuiCollapsibleNav: FunctionComponent = ({ + id, children, className, isDocked = false, isOpen = false, button, showButtonIfDocked = false, - dockedBreakpoint = 992, - showCloseButton = true, - closeButtonProps, - onClose, - id, - maskProps, + dockedBreakpoint = 'l', + // Setting different EuiFlyout defaults + as = 'nav' as EuiCollapsibleNavProps['as'], + size = 320, + side = 'left', + role = null, + ownFocus = true, + outsideClickCloses = true, + closeButtonPosition = 'outside', + paddingSize = 'none', ...rest }) => { const [flyoutID] = useState(id || htmlIdGenerator()('euiCollapsibleNav')); - const [windowIsLargeEnoughToDock, setWindowIsLargeEnoughToDock] = useState( - (typeof window === 'undefined' ? Infinity : window.innerWidth) >= + + /** + * Setting the initial state of pushed based on the `type` prop + * and if the current window size is large enough (larger than `pushBreakpoint`) + */ + const [windowIsLargeEnoughToPush, setWindowIsLargeEnoughToPush] = useState( + isWithinMinBreakpoint( + typeof window === 'undefined' ? 0 : window.innerWidth, dockedBreakpoint + ) ); - const navIsDocked = isDocked && windowIsLargeEnoughToDock; + const navIsDocked = isDocked && windowIsLargeEnoughToPush; + + /** + * Watcher added to the window to maintain `isPushed` state depending on + * the window size compared to the `pushBreakpoint` + */ const functionToCallOnWindowResize = throttle(() => { - if (window.innerWidth < dockedBreakpoint) { - setWindowIsLargeEnoughToDock(false); + if (isWithinMinBreakpoint(window.innerWidth, dockedBreakpoint)) { + setWindowIsLargeEnoughToPush(true); } else { - setWindowIsLargeEnoughToDock(true); + setWindowIsLargeEnoughToPush(false); } // reacts every 50ms to resize changes and always gets the final update }, 50); - // Watch for docked status and appropriately add/remove body classes and resize handlers useEffect(() => { - window.addEventListener('resize', functionToCallOnWindowResize); - - if (navIsDocked) { - document.body.classList.add('euiBody--collapsibleNavIsDocked'); - } else if (isOpen) { - document.body.classList.add('euiBody--collapsibleNavIsOpen'); + if (isDocked) { + // Only add the event listener if we'll need to accommodate with padding + window.addEventListener('resize', functionToCallOnWindowResize); } return () => { - document.body.classList.remove('euiBody--collapsibleNavIsDocked'); - document.body.classList.remove('euiBody--collapsibleNavIsOpen'); - window.removeEventListener('resize', functionToCallOnWindowResize); + if (isDocked) { + window.removeEventListener('resize', functionToCallOnWindowResize); + } }; - }, [navIsDocked, functionToCallOnWindowResize, isOpen]); + }, [isDocked, functionToCallOnWindowResize]); - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === keys.ESCAPE) { - event.preventDefault(); - collapse(); - } - }; - - const collapse = () => { - // Skip collapsing if it is docked - if (navIsDocked) { - return; - } else { - onClose && onClose(); - } - }; - - const classes = classNames( - 'euiCollapsibleNav', - { 'euiCollapsibleNav--isDocked': navIsDocked }, - className - ); - - let optionalOverlay; - if (!navIsDocked) { - optionalOverlay = ( - - ); - } + const classes = classNames('euiCollapsibleNav', className); // Show a trigger button if one was passed but // not if navIsDocked and showButtonIfDocked is false @@ -169,41 +134,35 @@ export const EuiCollapsibleNav: FunctionComponent = ({ 'aria-controls': flyoutID, 'aria-expanded': isOpen, 'aria-pressed': isOpen, - className: classNames( - button.props.className, - 'euiCollapsibleNav__toggle' - ), + // When EuiOutsideClickDetector is enabled, we don't want both the toggle button and document touches/clicks to happen, they'll cancel eachother out + onTouchEnd: (e: React.MouseEvent) => { + e.nativeEvent.stopImmediatePropagation(); + }, + onMouseUpCapture: (e: React.MouseEvent) => { + e.nativeEvent.stopImmediatePropagation(); + }, }); - const closeButton = showCloseButton && ( - - - - - - ); - const flyout = ( - <> - - {optionalOverlay} - {/* Trap focus only when docked={false} */} - - - - + + {children} + ); return ( diff --git a/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap index 58b69ee2787..8a6f9696580 100644 --- a/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap +++ b/src/components/collapsible_nav/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap @@ -6,44 +6,28 @@ exports[`EuiCollapsibleNavGroup is rendered 1`] = ` class="euiCollapsibleNavGroup testClass1 testClass2" data-test-subj="test subject string" id="id" -> -
-
+/> `; exports[`EuiCollapsibleNavGroup props background dark is rendered 1`] = `
-
-
+/> `; exports[`EuiCollapsibleNavGroup props background light is rendered 1`] = `
-
-
+/> `; exports[`EuiCollapsibleNavGroup props background none is rendered 1`] = `
-
-
+/> `; exports[`EuiCollapsibleNavGroup props iconProps renders data-test-subj 1`] = ` @@ -77,9 +61,6 @@ exports[`EuiCollapsibleNavGroup props iconProps renders data-test-subj 1`] = `
-
`; @@ -113,9 +94,6 @@ exports[`EuiCollapsibleNavGroup props iconSize is rendered 1`] = `
-
`; @@ -149,9 +127,6 @@ exports[`EuiCollapsibleNavGroup props iconType is rendered 1`] = `
-
`; @@ -178,9 +153,6 @@ exports[`EuiCollapsibleNavGroup props title is rendered 1`] = `
-
`; @@ -207,9 +179,6 @@ exports[`EuiCollapsibleNavGroup props titleElement can change the rendered eleme
-
`; @@ -217,22 +186,14 @@ exports[`EuiCollapsibleNavGroup props titleSize can be larger 1`] = `
-
-
+/> `; exports[`EuiCollapsibleNavGroup throws a warning if iconType is passed without a title 1`] = `
-
-
+/> `; exports[`EuiCollapsibleNavGroup when isCollapsible is true accepts accordion props 1`] = ` @@ -291,11 +252,7 @@ exports[`EuiCollapsibleNavGroup when isCollapsible is true accepts accordion pro
-
-
+ />
@@ -353,11 +310,7 @@ exports[`EuiCollapsibleNavGroup when isCollapsible is true will render an accord
-
-
+ />
diff --git a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx index d6aa1e531b6..cb73399e720 100644 --- a/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx +++ b/src/components/collapsible_nav/collapsible_nav_group/collapsible_nav_group.tsx @@ -146,7 +146,7 @@ export const EuiCollapsibleNavGroup: FunctionComponent{children}
); diff --git a/src/components/flyout/__snapshots__/flyout.test.tsx.snap b/src/components/flyout/__snapshots__/flyout.test.tsx.snap index 87e945ced75..eb4766a1a3b 100644 --- a/src/components/flyout/__snapshots__/flyout.test.tsx.snap +++ b/src/components/flyout/__snapshots__/flyout.test.tsx.snap @@ -2,471 +2,491 @@ exports[`EuiFlyout is rendered 1`] = ` Array [ -
, -
, -
+