diff --git a/changelogs/upcoming/7521.md b/changelogs/upcoming/7521.md new file mode 100644 index 00000000000..abf1dfb39f7 --- /dev/null +++ b/changelogs/upcoming/7521.md @@ -0,0 +1,19 @@ +- Updated `EuiPageSidebar` and `EuiPageTemplate.Sidebar` with a new `hasEmbellish` prop (defaults to false) + +**Bug fixes** + +- Fixed `EuiSideNav` not correctly typing the `items` prop as required + +**Accessibility** + +- Fixed `EuiSideNav` to render a fallback aria-label on mobile toggles if no heading or mobile title exists + +**CSS-in-JS conversions** + +- Converted `EuiSideNav` to Emotion; Removed the following Sass variables: + - `$euiSideNavEmphasizedBackgroundColor` + - `$euiSideNavRootTextcolor` + - `$euiSideNavBranchTextcolor` + - `$euiSideNavSelectedTextcolor` + - `$euiSideNavDisabledTextcolor` +- Removed the `euiSideNavEmbellish` Sass mixin. Use the new `EuiPageSidebar` `hasEmbellish` prop instead diff --git a/src-docs/src/components/guide_page/_guide_page.scss b/src-docs/src/components/guide_page/_guide_page.scss index af407b0b542..9e45f31f4b7 100644 --- a/src-docs/src/components/guide_page/_guide_page.scss +++ b/src-docs/src/components/guide_page/_guide_page.scss @@ -1,9 +1,5 @@ @import '../../../../src/global_styling/mixins/helpers'; -.guideSideNav { - @include euiSideNavEmbellish; -} - .guideSideNav__content { @include euiYScroll; padding: 0 $euiSizeL $euiSizeL; diff --git a/src-docs/src/views/app_view.js b/src-docs/src/views/app_view.js index 7425a452d36..2821844c740 100644 --- a/src-docs/src/views/app_view.js +++ b/src-docs/src/views/app_view.js @@ -50,7 +50,7 @@ export const AppView = ({ children, currentRoute = {} }) => { restrictWidth={false} mainProps={{ id: 'start-of-content' }} > - + > = () =>
; -import { EuiSideNavHeadingProps } from '../../../../src/components/side_nav/side_nav'; +import { EuiSideNavHeadingProps } from '../../../../src/components/side_nav/_side_nav_heading'; export const EuiSideNavHeading: FunctionComponent< EuiSideNavHeadingProps > = () =>
; diff --git a/src/components/index.scss b/src/components/index.scss index 5b934fcc505..dcfd6a3c324 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -6,6 +6,5 @@ @import 'datagrid/index'; @import 'form/index'; @import 'markdown_editor/index'; -@import 'side_nav/index'; @import 'selectable/index'; @import 'table/index'; diff --git a/src/components/page/page_sidebar/page_sidebar.stories.tsx b/src/components/page/page_sidebar/page_sidebar.stories.tsx index 5b0d1d7959c..2321cb71900 100644 --- a/src/components/page/page_sidebar/page_sidebar.stories.tsx +++ b/src/components/page/page_sidebar/page_sidebar.stories.tsx @@ -28,6 +28,7 @@ const meta: Meta = { args: { // Component defaults paddingSize: 'm', // The component default is actually 'none', but for nicer visuals in Storybook we'll set it to 'm' + hasEmbellish: false, sticky: false, minWidth: 248, responsive: ['xs', 's'], @@ -70,6 +71,7 @@ export const StickyOffset: Story = { ...hideStorybookControls([ 'minWidth', 'paddingSize', + 'hasEmbellish', 'responsive', ]), }, diff --git a/src/components/page/page_sidebar/page_sidebar.styles.ts b/src/components/page/page_sidebar/page_sidebar.styles.ts index 047ea201eea..51eeed04328 100644 --- a/src/components/page/page_sidebar/page_sidebar.styles.ts +++ b/src/components/page/page_sidebar/page_sidebar.styles.ts @@ -7,18 +7,38 @@ */ import { css } from '@emotion/react'; + +import { UseEuiTheme, transparentize } from '../../../services'; import { euiYScroll } from '../../../global_styling'; -import { UseEuiTheme } from '../../../services'; -export const euiPageSidebarStyles = (euiThemeContext: UseEuiTheme) => ({ - euiPageSidebar: css` - /* Prevent side bar width from changing when content width changes */ - flex: 0 1 0%; - `, +export const euiPageSidebarStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + const embellishColor = transparentize(euiTheme.colors.lightShade, 0.3); - sticky: css` - ${euiYScroll(euiThemeContext, { height: 'auto' })} - flex-grow: 1; - position: sticky; - `, -}); + return { + euiPageSidebar: css` + /* Prevent side bar width from changing when content width changes */ + flex: 0 1 0%; + `, + sticky: css` + ${euiYScroll(euiThemeContext, { height: 'auto' })} + flex-grow: 1; + position: sticky; + `, + embellish: css` + background: linear-gradient( + 160deg, + ${embellishColor} 0, + ${embellishColor} ${euiTheme.size.xl}, + transparent 0 + ), + linear-gradient( + 175deg, + ${embellishColor} 0, + ${embellishColor} ${euiTheme.size.base}, + transparent 0 + ); + `, + }; +}; diff --git a/src/components/page/page_sidebar/page_sidebar.tsx b/src/components/page/page_sidebar/page_sidebar.tsx index f4233e51884..4fcc8e9dd82 100644 --- a/src/components/page/page_sidebar/page_sidebar.tsx +++ b/src/components/page/page_sidebar/page_sidebar.tsx @@ -31,6 +31,10 @@ export interface EuiPageSidebarProps * When using this setting it's best to be consistent throughout all similar usages. */ paddingSize?: EuiPaddingSize; + /** + * Renders a fancy little visual in the top left corner of the side bar + */ + hasEmbellish?: boolean; /** * Adds `position: sticky` and affords for any fixed position headers. */ @@ -61,6 +65,7 @@ export const EuiPageSidebar: FunctionComponent = ({ paddingSize = 'none', minWidth = 248, responsive = ['xs', 's'], + hasEmbellish = false, style, ...rest }) => { @@ -71,6 +76,7 @@ export const EuiPageSidebar: FunctionComponent = ({ const cssStyles = [ styles.euiPageSidebar, !isResponding && sticky && styles.sticky, + hasEmbellish && styles.embellish, useEuiPaddingCSS()[paddingSize], ]; diff --git a/src/components/side_nav/__snapshots__/side_nav.test.tsx.snap b/src/components/side_nav/__snapshots__/side_nav.test.tsx.snap index 31b7ef46d28..58e307a5f25 100644 --- a/src/components/side_nav/__snapshots__/side_nav.test.tsx.snap +++ b/src/components/side_nav/__snapshots__/side_nav.test.tsx.snap @@ -7,7 +7,7 @@ exports[`EuiSideNav is rendered 1`] = ` data-test-subj="test subject string" >
@@ -20,14 +20,14 @@ exports[`EuiSideNav props heading accepts more headingProps 1`] = ` >

Side Nav Heading

@@ -39,13 +39,13 @@ exports[`EuiSideNav props heading is hidden with screenReaderOnly 1`] = ` class="euiSideNav" >

Side Nav Heading

@@ -57,13 +57,13 @@ exports[`EuiSideNav props heading is rendered 1`] = ` class="euiSideNav" >

Side Nav Heading

@@ -71,10 +71,34 @@ exports[`EuiSideNav props heading is rendered 1`] = ` exports[`EuiSideNav props isOpenOnMobile defaults to false 1`] = ` @@ -82,10 +106,35 @@ exports[`EuiSideNav props isOpenOnMobile defaults to false 1`] = ` exports[`EuiSideNav props isOpenOnMobile is rendered when specified as true 1`] = ` @@ -96,20 +145,20 @@ exports[`EuiSideNav props items is rendered 1`] = ` class="euiSideNav" >
A @@ -117,21 +166,21 @@ exports[`EuiSideNav props items is rendered 1`] = `
B @@ -140,17 +189,17 @@ exports[`EuiSideNav props items is rendered 1`] = `
E @@ -293,22 +342,22 @@ exports[`EuiSideNav props items renders items using a specified callback 1`] = ` class="euiSideNav" >
A @@ -316,20 +365,20 @@ exports[`EuiSideNav props items renders items using a specified callback 1`] = `
B @@ -348,22 +397,22 @@ exports[`EuiSideNav props items renders items which are links 1`] = ` class="euiSideNav" >
A @@ -371,19 +420,19 @@ exports[`EuiSideNav props items renders items which are links 1`] = `
B @@ -392,17 +441,17 @@ exports[`EuiSideNav props items renders items which are links 1`] = `
D @@ -508,16 +557,16 @@ exports[`EuiSideNav props items renders selected item and automatically opens pa
E @@ -539,18 +588,20 @@ exports[`EuiSideNav props mobileBreakpoints can be adjusted is rendered 1`] = ` class="euiSideNav" >

@@ -571,7 +622,7 @@ exports[`EuiSideNav props mobileBreakpoints can be adjusted null is rendered 1`] class="euiSideNav" >
diff --git a/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap b/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap index 8bb99340e41..aeef93a39a4 100644 --- a/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap +++ b/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap @@ -2,18 +2,18 @@ exports[`EuiSideNavItem can be disabled 1`] = `
+ + ), items: [ { name: 'Kibana', id: 'kibana', - }, - { - name: 'Observability', - id: 'observability', + icon: , + items: [ + { + name: 'Observability', + id: 'observability', + items: [ + { + name: 'Test', + id: 'test', + }, + ], + }, + ], }, { name: 'Security', id: 'security', + icon: , renderItem: ({ children }) => ( {children} ), diff --git a/src/components/side_nav/side_nav.styles.ts b/src/components/side_nav/side_nav.styles.ts new file mode 100644 index 00000000000..cb58d335505 --- /dev/null +++ b/src/components/side_nav/side_nav.styles.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../services'; +import { euiCanAnimate, logicalCSS } from '../../global_styling'; + +export const euiSideNavMobileStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + const fastTransition = `${euiTheme.animation.extraFast} ${euiTheme.animation.resistance}`; + const slowTransition = `${euiTheme.animation.extraSlow} ${euiTheme.animation.resistance}`; + + return { + // Mobile button + euiSideNav__mobileToggle: css` + ${logicalCSS('width', '100%')} + ${logicalCSS('height', 'auto')} + + /* Inherit from the wrapping heading element */ + padding: 1em; + font-size: inherit; + text-align: start; + + border-radius: 0; + ${logicalCSS('border-bottom', euiTheme.border.thin)} + `, + euiSideNav__mobileToggleContent: css` + justify-content: space-between; + `, + + // Mobile content + content: { + euiSideNav__mobileContent: css` + ${euiCanAnimate} { + transition: max-block-size ${fastTransition}, + padding-block ${fastTransition}, opacity ${slowTransition}, + visibility ${slowTransition}; + } + `, + hidden: css` + overflow: hidden; + visibility: hidden; + opacity: 0; + max-block-size: 0; + padding-inline: ${euiTheme.size.l}; + `, + open: css` + visibility: visible; + opacity: 1; + max-block-size: 5000px; + padding: ${euiTheme.size.l}; + `, + }, + }; +}; diff --git a/src/components/side_nav/side_nav.test.tsx b/src/components/side_nav/side_nav.test.tsx index 3be16f9097a..1df520b3d34 100644 --- a/src/components/side_nav/side_nav.test.tsx +++ b/src/components/side_nav/side_nav.test.tsx @@ -14,13 +14,15 @@ import { render } from '../../test/rtl'; import { EuiSideNav } from './side_nav'; import { RenderItem } from './side_nav_item'; +const JSDOM_BREAKPOINT = ['l']; + describe('EuiSideNav', () => { - shouldRenderCustomStyles(, { + shouldRenderCustomStyles(, { childProps: ['headingProps'], }); test('is rendered', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); @@ -28,13 +30,21 @@ describe('EuiSideNav', () => { describe('props', () => { describe('isOpenOnMobile', () => { test('defaults to false', () => { - const { container } = render(); + const { container } = render( + + ); expect(container.firstChild).toMatchSnapshot(); }); test('is rendered when specified as true', () => { - const { container } = render(); + const { container } = render( + + ); expect(container.firstChild).toMatchSnapshot(); }); @@ -43,7 +53,10 @@ describe('EuiSideNav', () => { describe('mobileBreakpoints can be adjusted', () => { test('is rendered', () => { const { container } = render( - + ); expect(container.firstChild).toMatchSnapshot(); @@ -51,7 +64,7 @@ describe('EuiSideNav', () => { test('null is rendered', () => { const { container } = render( - + ); expect(container.firstChild).toMatchSnapshot(); @@ -60,7 +73,9 @@ describe('EuiSideNav', () => { describe('heading', () => { test('is rendered', () => { - const { container } = render(); + const { container } = render( + + ); expect(container.firstChild).toMatchSnapshot(); }); @@ -68,6 +83,7 @@ describe('EuiSideNav', () => { test('is hidden with screenReaderOnly', () => { const { container } = render( @@ -79,6 +95,7 @@ describe('EuiSideNav', () => { test('accepts more headingProps', () => { const { container } = render( @@ -86,6 +103,63 @@ describe('EuiSideNav', () => { expect(container.firstChild).toMatchSnapshot(); }); + + describe('mobile behavior', () => { + it('is overridden by `mobileTitle`', () => { + const { getByTestSubject } = render( + + ); + const heading = getByTestSubject('heading'); + + expect(heading).toHaveTextContent('Mobile heading'); + expect(heading.querySelector('button')).toHaveAccessibleName( + 'Mobile heading' + ); + }); + + it('renders a fallback aria-label if neither `heading` nor `mobileTitle` is passed', () => { + const { getByTestSubject } = render( + + ); + const heading = getByTestSubject('heading'); + + expect(heading).toHaveTextContent(''); + expect(heading.querySelector('button')).toHaveAccessibleName( + 'Toggle navigation' + ); + }); + + it('does not render any visible text if `headingProps.screenReaderOnly` is true, but does render an aria-label', () => { + const { getByTestSubject } = render( + + ); + const heading = getByTestSubject('heading'); + + expect(heading).toHaveTextContent(''); + expect(heading.querySelector('button')).toHaveAccessibleName( + 'Toggle navigation' + ); + }); + }); }); describe('items', () => { diff --git a/src/components/side_nav/side_nav.tsx b/src/components/side_nav/side_nav.tsx index 663042523f5..d54d2bbac0a 100644 --- a/src/components/side_nav/side_nav.tsx +++ b/src/components/side_nav/side_nav.tsx @@ -10,26 +10,20 @@ import React, { Component, ReactNode, MouseEventHandler } from 'react'; import classNames from 'classnames'; import { CommonProps, PropsOf } from '../common'; - -import { EuiSideNavItem, RenderItem } from './side_nav_item'; -import { EuiSideNavItemType } from './side_nav_types'; import { EuiButtonEmpty } from '../button'; -import { EuiTitle, EuiTitleProps } from '../title'; -import { EuiScreenReaderOnly } from '../accessibility'; -import { EuiBreakpointSize, htmlIdGenerator } from '../../services'; +import { EuiI18n } from '../i18n'; +import { + EuiBreakpointSize, + htmlIdGenerator, + withEuiTheme, + WithEuiThemeProps, +} from '../../services'; import { EuiHideFor, EuiShowFor } from '../responsive'; -export type EuiSideNavHeadingProps = Partial & { - /** - * The actual HTML heading element to wrap the `heading`. - * Default is `h2` - */ - element?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span'; - /** - * For best accessibility, `