From 507bd17bf71cb520519fdeafe6627538008db68d Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Wed, 27 Sep 2023 11:05:14 -0700
Subject: [PATCH] [EuiCollapsibleNavBeta] Various API updates and cleanups
(#7228)
---
.../collapsible_nav_body_footer.test.tsx.snap | 26 +++
.../collapsible_nav_beta.stories.tsx | 207 ++++++++++--------
.../collapsible_nav_beta.styles.ts | 28 +--
.../collapsible_nav_beta.tsx | 27 ++-
.../collapsible_nav_body_footer.styles.ts | 44 ++++
.../collapsible_nav_body_footer.test.tsx | 50 +++++
.../collapsible_nav_body_footer.tsx | 69 ++++++
.../collapsible_nav_group.test.tsx.snap | 67 ++++++
.../collapsible_nav_group.stories.tsx | 94 ++++++++
.../collapsible_nav_group.styles.ts | 34 +++
.../collapsible_nav_group.test.tsx | 55 +++++
.../collapsible_nav_group.tsx | 83 +++++++
.../collapsible_nav_group/index.ts | 9 +
.../collapsible_nav_accordion.test.tsx.snap | 4 +-
.../collapsible_nav_item.test.tsx.snap | 2 +-
.../collapsible_nav_accordion.styles.ts | 25 +--
.../collapsible_nav_accordion.test.tsx | 21 --
.../collapsible_nav_accordion.tsx | 49 +----
.../collapsible_nav_item.stories.tsx | 12 +-
.../collapsible_nav_item.styles.ts | 33 ++-
.../collapsible_nav_item.test.tsx | 61 +++---
.../collapsible_nav_item.tsx | 102 ++++++---
.../collapsible_nav_item/index.ts | 1 -
src/components/collapsible_nav_beta/index.ts | 1 -
24 files changed, 804 insertions(+), 300 deletions(-)
create mode 100644 src/components/collapsible_nav_beta/__snapshots__/collapsible_nav_body_footer.test.tsx.snap
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_body_footer.styles.ts
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_body_footer.test.tsx
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_body_footer.tsx
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.stories.tsx
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.styles.ts
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.test.tsx
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.tsx
create mode 100644 src/components/collapsible_nav_beta/collapsible_nav_group/index.ts
diff --git a/src/components/collapsible_nav_beta/__snapshots__/collapsible_nav_body_footer.test.tsx.snap b/src/components/collapsible_nav_beta/__snapshots__/collapsible_nav_body_footer.test.tsx.snap
new file mode 100644
index 00000000000..4ef06f455f0
--- /dev/null
+++ b/src/components/collapsible_nav_beta/__snapshots__/collapsible_nav_body_footer.test.tsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCollapsibleNavBody renders 1`] = `
+
+`;
+
+exports[`EuiCollapsibleNavFooter renders 1`] = `
+
+`;
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx
index b7cc203b663..b8a21163188 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx
@@ -11,14 +11,15 @@ import type { Meta, StoryObj } from '@storybook/react';
import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '../header';
import { EuiPageTemplate } from '../page_template';
-import { EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter } from '../flyout';
+import { EuiFlyout } from '../flyout';
import { EuiButton } from '../button';
+import { EuiTitle } from '../title';
-import { EuiCollapsibleNavItem } from './collapsible_nav_item';
import {
EuiCollapsibleNavBeta,
EuiCollapsibleNavBetaProps,
-} from './collapsible_nav_beta';
+ EuiCollapsibleNavItemProps,
+} from './';
const meta: Meta = {
title: 'EuiCollapsibleNavBeta',
@@ -58,12 +59,41 @@ const OpenCollapsibleNav: FunctionComponent<
);
};
+const renderGroup = (
+ groupTitle: string,
+ groupItems: EuiCollapsibleNavItemProps[]
+) => {
+ return [
+ {
+ renderItem: () => (
+ ({
+ marginTop: euiTheme.size.base,
+ paddingBlock: euiTheme.size.xs,
+ paddingInline: euiTheme.size.s,
+ })}
+ >
+ {groupTitle}
+
+ ),
+ },
+ ...groupItems,
+ ];
+};
+
export const KibanaExample: Story = {
render: ({ ...args }) => (
-
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
-
-
-
+
),
};
@@ -288,28 +325,10 @@ export const KibanaExample: Story = {
export const SecurityExample: Story = {
render: ({ ...args }) => (
-
-
-
+
-
-
-
+
+
-
-
+
),
};
@@ -423,10 +442,10 @@ const MockConsumerFlyout: FunctionComponent = () => {
{flyoutIsOpen && (
setFlyoutOpen(false)}>
-
+
Some other mock consumer flyout that should overlap
EuiCollapsibleNav
-
+
)}
>
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.styles.ts b/src/components/collapsible_nav_beta/collapsible_nav_beta.styles.ts
index 5e93c634fa5..e52f8d8d155 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_beta.styles.ts
+++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.styles.ts
@@ -8,10 +8,12 @@
import { css } from '@emotion/react';
import { UseEuiTheme } from '../../services';
-import { logicalCSS, euiYScroll } from '../../global_styling';
+import { logicalCSS } from '../../global_styling';
import { euiShadowFlat } from '../../themes';
import { euiHeaderVariables } from '../header/header.styles';
+import { euiCollapsibleNavBodyStyles } from './collapsible_nav_body_footer.styles';
+
export const euiCollapsibleNavBetaStyles = (euiThemeContext: UseEuiTheme) => {
const { euiTheme } = euiThemeContext;
@@ -26,22 +28,9 @@ export const euiCollapsibleNavBetaStyles = (euiThemeContext: UseEuiTheme) => {
/* Fixed header affordance */
${logicalCSS('top', fixedHeaderOffset)}
- /* Allow the nav to scroll, in case consumers don't use EuiFlyoutBody/EuiFyoutFooter */
- ${euiYScroll(euiThemeContext, { height: 'inherit' })}
-
/* This extra padding is needed for EuiPopovers to have enough
space to render with the right anchorPosition */
${logicalCSS('padding-bottom', euiTheme.size.xs)}
-
- /* In case things get really dire responsively, ensure the footer doesn't overtake the body */
- .euiFlyoutBody {
- ${logicalCSS('min-height', '50%')}
- }
-
- .euiFlyoutFooter {
- background-color: ${euiTheme.colors.emptyShade};
- ${logicalCSS('border-top', euiTheme.border.thin)}
- }
`,
left: css`
${logicalCSS('border-right', euiTheme.border.thin)}
@@ -53,16 +42,7 @@ export const euiCollapsibleNavBetaStyles = (euiThemeContext: UseEuiTheme) => {
${euiShadowFlat(euiThemeContext)}
`,
isPushCollapsed: css`
- /* Hide the scrollbar for docked mode (while still keeping the nav scrollable)
- Otherwise if scrollbars are visible, button icon visibility suffers */
- &,
- .euiFlyoutBody__overflow {
- scrollbar-width: none; /* Firefox */
-
- &::-webkit-scrollbar {
- display: none; /* Chrome, Edge, & Safari */
- }
- }
+ ${euiCollapsibleNavBodyStyles._isPushCollapsed}
`,
isOverlayFullWidth: css`
/* Override EuiFlyout's max-width */
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx b/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx
index 94eed977152..5d3a62e91a7 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.tsx
@@ -36,8 +36,9 @@ export type EuiCollapsibleNavBetaProps = CommonProps &
'side' | 'focusTrapProps' | 'includeFixedHeadersInFocusTrap'
> & {
/**
- * ReactNode(s) to render as navigation flyout content, typically `EuiCollapsibleNavItem`s.
- * You may also want to use `EuiFlyoutBody` and `EuiFlyoutFooter` for organization.
+ * ReactNode(s) to render as navigation flyout content, typically `EuiCollapsibleNavBeta.Item`s.
+ * You will likely want to use `EuiCollapsibleNavBeta.Body` and `EuiCollapsibleNavBeta.Footer`
+ * for organization.
*/
children?: ReactNode;
/**
@@ -76,9 +77,7 @@ export type EuiCollapsibleNavBetaProps = CommonProps &
'aria-label'?: string;
};
-export const EuiCollapsibleNavBeta: FunctionComponent<
- EuiCollapsibleNavBetaProps
-> = ({
+const _EuiCollapsibleNavBeta: FunctionComponent = ({
id,
children,
className,
@@ -211,3 +210,21 @@ export const EuiCollapsibleNavBeta: FunctionComponent<
);
};
+
+/**
+ * Combined export with subcomponents
+ */
+
+import {
+ EuiCollapsibleNavBody,
+ EuiCollapsibleNavFooter,
+} from './collapsible_nav_body_footer';
+import { EuiCollapsibleNavGroup } from './collapsible_nav_group';
+import { EuiCollapsibleNavItem } from './collapsible_nav_item';
+
+export const EuiCollapsibleNavBeta = Object.assign(_EuiCollapsibleNavBeta, {
+ Body: EuiCollapsibleNavBody,
+ Footer: EuiCollapsibleNavFooter,
+ Group: EuiCollapsibleNavGroup,
+ Item: EuiCollapsibleNavItem,
+});
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_body_footer.styles.ts b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.styles.ts
new file mode 100644
index 00000000000..260697f2992
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.styles.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { logicalCSS } from '../../global_styling';
+
+export const euiCollapsibleNavBodyStyles = {
+ // In case things get really dire responsively, ensure the footer doesn't overtake the body
+ euiCollapsibleNav__body: css`
+ ${logicalCSS('min-height', '50%')}
+ `,
+ get isPushCollapsed() {
+ return css`
+ .euiFlyoutBody__overflow {
+ ${this._isPushCollapsed}
+ }
+ `;
+ },
+ // CSS is reused by main euiCollapsibleNav styles in case the body component isn't used
+ _isPushCollapsed: `
+ /* Hide the scrollbar for docked mode (while still keeping the nav scrollable)
+ Otherwise if scrollbars are visible, button icon visibility suffers. */
+ scrollbar-width: none; /* Firefox */
+
+ &::-webkit-scrollbar {
+ display: none; /* Chrome, Edge, & Safari */
+ }
+ `,
+};
+
+export const euiCollapsibleNavFooterStyles = ({ euiTheme }: UseEuiTheme) => {
+ return {
+ euiCollapsibleNav__footer: css`
+ background-color: ${euiTheme.colors.emptyShade};
+ ${logicalCSS('border-top', euiTheme.border.thin)}
+ `,
+ };
+};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_body_footer.test.tsx b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.test.tsx
new file mode 100644
index 00000000000..8493fa725fe
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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 React from 'react';
+import { render } from '../../test/rtl';
+import { shouldRenderCustomStyles } from '../../test/internal';
+import { requiredProps } from '../../test';
+
+import { EuiCollapsibleNavContext } from './context';
+import {
+ EuiCollapsibleNavBody,
+ EuiCollapsibleNavFooter,
+} from './collapsible_nav_body_footer';
+
+describe('EuiCollapsibleNavBody', () => {
+ shouldRenderCustomStyles();
+
+ it('renders', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('renders with docked styles', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.innerHTML).toContain('isPushCollapsed');
+ });
+});
+
+describe('EuiCollapsibleNavFooter', () => {
+ shouldRenderCustomStyles();
+
+ it('renders', () => {
+ const { container } = render(
+
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_body_footer.tsx b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.tsx
new file mode 100644
index 00000000000..7bd6ebb9a39
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_body_footer.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 React, { useContext } from 'react';
+import classNames from 'classnames';
+
+import { useEuiTheme } from '../../services';
+import {
+ EuiFlyoutBody,
+ EuiFlyoutBodyProps,
+ EuiFlyoutFooter,
+ EuiFlyoutFooterProps,
+} from '../flyout';
+
+import { EuiCollapsibleNavContext } from './context';
+import {
+ euiCollapsibleNavBodyStyles as bodyStyles,
+ euiCollapsibleNavFooterStyles,
+} from './collapsible_nav_body_footer.styles';
+
+/**
+ * These components are incredibly light wrappers around `EuiFlyoutBody`
+ * and `EuiFlyoutFooter` with collapsible nav-specific styling/considerations.
+ *
+ * Note: They are not intended to be used standalone outside of EuiCollapsibleNav
+ */
+
+export const EuiCollapsibleNavBody: EuiFlyoutBodyProps = ({
+ className,
+ ...props
+}) => {
+ const classes = classNames('euiCollapsibleNav__body', className);
+
+ const { isCollapsed, isPush } = useContext(EuiCollapsibleNavContext);
+ const cssStyles = [
+ bodyStyles.euiCollapsibleNav__body,
+ isCollapsed && isPush && bodyStyles.isPushCollapsed,
+ ];
+
+ return (
+
+ );
+};
+
+export const EuiCollapsibleNavFooter: EuiFlyoutFooterProps = ({
+ className,
+ ...props
+}) => {
+ const classes = classNames('euiCollapsibleNav__footer', className);
+
+ const euiTheme = useEuiTheme();
+ const styles = euiCollapsibleNavFooterStyles(euiTheme);
+ const cssStyles = [styles.euiCollapsibleNav__footer];
+
+ return ;
+};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap b/src/components/collapsible_nav_beta/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
new file mode 100644
index 00000000000..04a39c8552e
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/__snapshots__/collapsible_nav_group.test.tsx.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiCollapsibleNavGroup renders 1`] = `
+
+
+
+
+ Group
+
+
+
+
+
+ Item
+
+
+
+
+`;
+
+exports[`EuiCollapsibleNavGroup renders as a docked button icon 1`] = `
+
+`;
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.stories.tsx b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.stories.tsx
new file mode 100644
index 00000000000..e4fc402fc8b
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.stories.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 React, { FunctionComponent, PropsWithChildren } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { EuiHeader, EuiHeaderSection } from '../../header';
+import { EuiPageTemplate } from '../../page_template';
+
+import { EuiCollapsibleNavBeta, EuiCollapsibleNavBetaProps } from '../';
+import {
+ EuiCollapsibleNavGroup,
+ EuiCollapsibleNavGroupProps,
+} from './collapsible_nav_group';
+
+const meta: Meta = {
+ title: 'EuiCollapsibleNavBeta.Group',
+ component: EuiCollapsibleNavGroup,
+ parameters: {
+ layout: 'fullscreen',
+ },
+ args: {
+ title: 'Elastic',
+ icon: 'logoElastic',
+ items: [
+ { title: 'Get started', href: '#' },
+ { title: 'Dashboards', href: '#' },
+ {
+ title: 'Explore',
+ href: '#',
+ items: [
+ { title: 'Hello', href: '#' },
+ { title: 'World', href: '#' },
+ ],
+ },
+ ],
+ },
+ argTypes: {
+ wrapperProps: { control: 'object' },
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+const CollapsibleNavTemplate: FunctionComponent<
+ PropsWithChildren & Partial
+> = ({ children, ...props }) => {
+ return (
+ <>
+
+
+
+ {children}
+
+
+
+
+ Hello world
+
+ >
+ );
+};
+
+export const Playground: Story = {
+ render: ({ ...args }) => (
+
+
+
+ ),
+ args: {
+ wrapperProps: { 'data-test-subj': 'test' },
+ },
+};
+
+export const EdgeCaseTesting: Story = {
+ render: ({ ...args }) => (
+
+
+
+
+
+
+
+
+ ),
+ args: {
+ href: '#',
+ },
+};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.styles.ts b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.styles.ts
new file mode 100644
index 00000000000..82bc8d37935
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.styles.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { euiCollapsibleNavItemVariables } from '../collapsible_nav_item/collapsible_nav_item.styles';
+
+export const euiCollapsibleNavGroupStyles = (euiThemeContext: UseEuiTheme) => {
+ const { euiTheme } = euiThemeContext;
+ const sharedStyles = euiCollapsibleNavItemVariables(euiThemeContext);
+
+ return {
+ euiCollapsibleNavGroup: css``,
+ isWrapper: css`
+ margin: ${sharedStyles.padding};
+ `,
+ euiCollapsibleNavGroup__title: css`
+ margin-block: ${euiTheme.size.base};
+ margin-inline: 0;
+
+ /* Make title icons slightly larger */
+ .euiIcon {
+ transform: scale(1.25);
+ }
+ `,
+ };
+};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.test.tsx b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.test.tsx
new file mode 100644
index 00000000000..7ca82ad93b7
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.test.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 React from 'react';
+import { render } from '../../../test/rtl';
+import { shouldRenderCustomStyles } from '../../../test/internal';
+import { requiredProps } from '../../../test';
+
+import { EuiCollapsibleNavContext } from '../context';
+import { EuiCollapsibleNavGroup } from './collapsible_nav_group';
+
+describe('EuiCollapsibleNavGroup', () => {
+ const sharedProps = {
+ title: 'Group',
+ items: [{ title: 'Item' }],
+ icon: 'home',
+ };
+
+ shouldRenderCustomStyles(, {
+ skip: { style: true }, // Spread to a different location than className and CSS
+ });
+ shouldRenderCustomStyles(, {
+ targetSelector: '.euiCollapsibleNavItem',
+ skip: { className: true, css: true },
+ });
+ shouldRenderCustomStyles(, {
+ childProps: ['wrapperProps'],
+ skip: { parentTest: true },
+ });
+
+ it('renders', () => {
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('renders as a docked button icon', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.tsx b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.tsx
new file mode 100644
index 00000000000..2ddb694596a
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/collapsible_nav_group.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 React, { FunctionComponent, HTMLAttributes, useContext } from 'react';
+import classNames from 'classnames';
+
+import { useEuiTheme } from '../../../services';
+import { CommonProps } from '../../common';
+
+import { EuiCollapsibleNavContext } from '../context';
+import {
+ EuiCollapsibleNavItem,
+ EuiCollapsibleNavSubItems,
+ EuiCollapsibleNavSubItemProps,
+ EuiCollapsibleNavItemProps,
+} from '../collapsible_nav_item/collapsible_nav_item';
+import { EuiCollapsedNavPopover } from '../collapsible_nav_item/collapsed/collapsed_nav_popover';
+
+import { euiCollapsibleNavGroupStyles } from './collapsible_nav_group.styles';
+
+export type EuiCollapsibleNavGroupProps = Omit<
+ EuiCollapsibleNavItemProps,
+ 'items' | 'accordionProps'
+> & {
+ /**
+ * Will render an array of `EuiCollapsibleNavItems`.
+ *
+ * Accepts any #EuiCollapsibleNavItemProps. Or, to render completely custom
+ * subitem content, pass an object with a `renderItem` callback.
+ */
+ items: EuiCollapsibleNavSubItemProps[];
+ /**
+ * Optional props to pass to the wrapping div
+ */
+ wrapperProps?: HTMLAttributes & CommonProps;
+};
+
+/**
+ * This component should only ever be used as a **top-level component**, and not as a sub-item.
+ * It also should **not** be used in the nav footer.
+ */
+export const EuiCollapsibleNavGroup: FunctionComponent<
+ EuiCollapsibleNavGroupProps
+> = ({ items, className, wrapperProps, ...props }) => {
+ const { isCollapsed, isPush } = useContext(EuiCollapsibleNavContext);
+
+ const classes = classNames(
+ 'euiCollapsibleNavGroup',
+ className,
+ wrapperProps?.className
+ );
+
+ const euiTheme = useEuiTheme();
+ const styles = euiCollapsibleNavGroupStyles(euiTheme);
+ const cssStyles = [
+ styles.euiCollapsibleNavGroup,
+ isPush && isCollapsed
+ ? styles.euiCollapsibleNavGroup__title
+ : styles.isWrapper,
+ wrapperProps?.css,
+ ];
+
+ return (
+
+ {isCollapsed && isPush ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_group/index.ts b/src/components/collapsible_nav_beta/collapsible_nav_group/index.ts
new file mode 100644
index 00000000000..7403a1cffd1
--- /dev/null
+++ b/src/components/collapsible_nav_beta/collapsible_nav_group/index.ts
@@ -0,0 +1,9 @@
+/*
+ * 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.
+ */
+
+export { EuiCollapsibleNavGroup } from './collapsible_nav_group';
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_accordion.test.tsx.snap b/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_accordion.test.tsx.snap
index 95c4658d0e7..293ba2b0a01 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_accordion.test.tsx.snap
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/__snapshots__/collapsible_nav_accordion.test.tsx.snap
@@ -51,7 +51,7 @@ exports[`EuiCollapsibleNavAccordion renders as a sub item 1`] = `
class="euiAccordion__children emotion-euiAccordion__children"
>
x - y
- )
- )}
- `,
- },
};
};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.test.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.test.tsx
index 85d8a0d7051..86418cb60bb 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.test.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.test.tsx
@@ -53,27 +53,6 @@ describe('EuiCollapsibleNavAccordion', () => {
);
});
- describe('when any items have an icon', () => {
- it('renders all items without icon with an `empty` icon', () => {
- const { container } = render(
-
- );
-
- expect(
- container.querySelectorAll('[data-euiicon-type="empty"]')
- ).toHaveLength(3);
- });
- });
-
describe('when the accordion header is a link and the link is clicked', () => {
it('does not trigger the accordion opening', () => {
const { getByTestSubject, container } = render(
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.tsx
index 7bc84a55bc1..5929756b615 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_accordion.tsx
@@ -11,7 +11,6 @@ import React, {
ReactNode,
MouseEvent,
useCallback,
- useMemo,
} from 'react';
import classNames from 'classnames';
@@ -19,10 +18,10 @@ import { useEuiTheme, useGeneratedHtmlId } from '../../../services';
import { EuiAccordion } from '../../accordion';
import {
- EuiCollapsibleNavSubItem,
+ EuiCollapsibleNavSubItems,
+ EuiCollapsibleNavSubItemProps,
_SharedEuiCollapsibleNavItemProps,
_EuiCollapsibleNavItemDisplayProps,
- EuiCollapsibleNavItemProps,
} from './collapsible_nav_item';
import { EuiCollapsibleNavLink } from './collapsible_nav_link';
import { euiCollapsibleNavAccordionStyles } from './collapsible_nav_accordion.styles';
@@ -33,10 +32,7 @@ type EuiCollapsibleNavAccordionProps = Omit<
> &
_EuiCollapsibleNavItemDisplayProps & {
buttonContent: ReactNode;
- // On the main `EuiCollapsibleNavItem` component, this uses `EuiCollapsibleNavSubItemProps`
- // to allow for section headings, but by the time `items` reaches this component, we
- // know for sure it's an actual accordion item and not a section heading
- items: EuiCollapsibleNavItemProps[];
+ items: EuiCollapsibleNavSubItemProps[];
};
/**
@@ -73,9 +69,6 @@ export const EuiCollapsibleNavAccordion: FunctionComponent<
accordionProps?.css,
];
- /**
- * Title / accordion trigger
- */
const isTitleInteractive = !!(href || linkProps?.onClick);
// Stop propagation on the title so that the accordion toggle doesn't occur on click
@@ -88,36 +81,6 @@ export const EuiCollapsibleNavAccordion: FunctionComponent<
[linkProps?.onClick] // eslint-disable-line react-hooks/exhaustive-deps
);
- /**
- * Child items
- */
- // If any of the sub items have an icon, default to an
- // icon of `empty` so that all text lines up vertically
- const itemsHaveIcons = useMemo(
- () => items.some((item) => !!item.icon),
- [items]
- );
- const icon = itemsHaveIcons ? 'empty' : undefined;
-
- const childrenCssStyles = [
- styles.children.euiCollapsibleNavAccordion__children,
- isSubItem ? styles.children.isSubItem : styles.children.isTopItem,
- ];
-
- const children = (
-
- {items.map((item, index) => (
- // This is an intentional circular dependency between the accordion & parent item display.
- // EuiSideNavItem is purposely recursive to support any amount of nested sub items,
- // and split up into separate files/components for better dev readability
-
- ))}
-
- );
-
return (
- {children}
+
);
};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.stories.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.stories.tsx
index 5a5de350df3..aefb238c58d 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.stories.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.stories.tsx
@@ -9,6 +9,7 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
+import { EuiSpacer } from '../../spacer';
import { EuiCollapsibleNavBeta } from '../collapsible_nav_beta';
import {
@@ -100,10 +101,7 @@ export const EdgeCaseTesting: Story = {
{ ...args, title: 'Link', href: '#', isSelected: true },
{ ...args, title: 'Button', onClick: () => {} },
{ ...args, title: 'Span', href: '#' },
- {
- title: 'Section 2',
- isGroupTitle: true,
- },
+ { renderItem: () => },
{
...args,
title: 'Test 2',
@@ -125,11 +123,7 @@ export const EdgeCaseTesting: Story = {
{ title: 'grandchild 2', href: '#' },
],
},
- {
- title: 'Section 3',
- titleElement: 'h3',
- isGroupTitle: true,
- },
+ { renderItem: () => },
{
...args,
title: 'Nested accordion with grandchildren',
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.styles.ts b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.styles.ts
index 1250997a4f4..2873e006788 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.styles.ts
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.styles.ts
@@ -11,7 +11,7 @@ import { css } from '@emotion/react';
import { UseEuiTheme } from '../../../services';
import {
logicalCSS,
- logicalShorthandCSS,
+ mathWithUnits,
euiFontSize,
} from '../../../global_styling';
import { euiButtonColor } from '../../../themes/amsterdam/global_styling/mixins/button';
@@ -48,15 +48,30 @@ export const euiCollapsibleNavItemTitleStyles = {
`,
};
-export const euiCollapsibleNavSubItemGroupTitleStyles = ({
- euiTheme,
-}: UseEuiTheme) => {
+/**
+ * Sub item groups
+ */
+
+export const euiCollapsibleNavSubItemsStyles = ({ euiTheme }: UseEuiTheme) => {
return {
- euiCollapsibleNavItem__groupTitle: css`
- ${logicalCSS('margin-top', euiTheme.size.base)}
- ${logicalShorthandCSS(
- 'padding',
- `${euiTheme.size.xs} ${euiTheme.size.s}`
+ euiCollapsibleNavItem__items: css``,
+ isGroup: css`
+ ${logicalCSS('padding-top', euiTheme.size.xs)}
+ ${logicalCSS('padding-left', euiTheme.size.s)}
+ `,
+ isTopItem: css`
+ ${logicalCSS('padding-top', euiTheme.size.xs)}
+ ${logicalCSS('padding-left', euiTheme.size.xl)}
+ `,
+ isSubItem: css`
+ ${logicalCSS('border-left', euiTheme.border.thin)}
+ ${logicalCSS('margin-left', euiTheme.size.s)}
+ ${logicalCSS(
+ 'padding-left',
+ mathWithUnits(
+ [euiTheme.size.s, euiTheme.border.width.thin],
+ (x, y) => x - y
+ )
)}
`,
};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.test.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.test.tsx
index 708f301ee5c..41324c9d6fc 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.test.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.test.tsx
@@ -140,56 +140,47 @@ describe('EuiCollapsibleNavItem', () => {
/>
);
+ expect(
+ container.querySelectorAll('.euiCollapsibleNavItem__items')
+ ).toHaveLength(2);
expect(
container.querySelectorAll('.euiCollapsibleNavSubItem')
).toHaveLength(5);
});
- it('renders group titles', () => {
- const { container } = render(
-
- );
-
- expect(container.querySelector('.euiCollapsibleNavItem__groupTitle'))
- .toMatchInlineSnapshot(`
-
- Section
-
- `);
+ describe('when any items have an icon', () => {
+ it('renders all items without icon with an `empty` icon', () => {
+ const { container } = render(
+
+ );
+
+ expect(
+ container.querySelectorAll('[data-euiicon-type="empty"]')
+ ).toHaveLength(3);
+ });
});
- it('allows customizing the group title element', () => {
- const { container } = render(
+ it('allows rendering totally custom sub items', () => {
+ const { getByTestSubject } = render(
},
{ title: 'Link 1', titleElement: 'h3' },
]}
/>
);
- expect(container.querySelector('.euiCollapsibleNavItem__groupTitle'))
- .toMatchInlineSnapshot(`
-
- Group title
-
- `);
+ expect(getByTestSubject('custom')).toBeInTheDocument();
});
});
});
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx
index f3b4bd57728..0b786eeaded 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx
@@ -6,7 +6,13 @@
* Side Public License, v 1.
*/
-import React, { FunctionComponent, HTMLAttributes, useContext } from 'react';
+import React, {
+ FunctionComponent,
+ HTMLAttributes,
+ ReactNode,
+ useContext,
+ useMemo,
+} from 'react';
import classNames from 'classnames';
import { useEuiTheme } from '../../../services';
@@ -15,7 +21,6 @@ import { CommonProps, ExclusiveUnion } from '../../common';
import { EuiIcon, IconType, EuiIconProps } from '../../icon';
import { EuiLinkProps } from '../../link';
import { EuiAccordionProps } from '../../accordion';
-import { EuiTitle } from '../../title';
import { EuiCollapsibleNavContext } from '../context';
import { EuiCollapsedNavItem } from './collapsed';
@@ -23,7 +28,7 @@ import { EuiCollapsibleNavAccordion } from './collapsible_nav_accordion';
import { EuiCollapsibleNavLink } from './collapsible_nav_link';
import {
euiCollapsibleNavItemTitleStyles,
- euiCollapsibleNavSubItemGroupTitleStyles,
+ euiCollapsibleNavSubItemsStyles,
} from './collapsible_nav_item.styles';
export type _SharedEuiCollapsibleNavItemProps = HTMLAttributes &
@@ -37,8 +42,8 @@ export type _SharedEuiCollapsibleNavItemProps = HTMLAttributes &
/**
* When passed, an `EuiAccordion` with nested child item links will be rendered.
*
- * Accepts any #EuiCollapsibleNavItem prop, and also accepts an
- * #EuiCollapsibleNavSubItemGroupTitle
+ * Accepts any #EuiCollapsibleNavItemProps. Or, to render completely custom
+ * subitem content, pass an object with a `renderItem` callback.
*/
items?: EuiCollapsibleNavSubItemProps[];
/**
@@ -79,20 +84,13 @@ export type EuiCollapsibleNavItemProps = {
iconProps?: Partial;
} & _SharedEuiCollapsibleNavItemProps;
-export type EuiCollapsibleNavSubItemGroupTitle = Pick<
- EuiCollapsibleNavItemProps,
- 'title' | 'titleElement'
-> & {
- /**
- * Pass this flag to seperate links by group title headings.
- * Strongly consider using the `titleElement` prop for accessibility.
- */
- isGroupTitle?: boolean;
+export type EuiCollapsibleNavCustomSubItem = {
+ renderItem: () => ReactNode;
};
export type EuiCollapsibleNavSubItemProps = ExclusiveUnion<
EuiCollapsibleNavItemProps,
- EuiCollapsibleNavSubItemGroupTitle
+ EuiCollapsibleNavCustomSubItem
>;
export type _EuiCollapsibleNavItemDisplayProps = {
@@ -158,7 +156,7 @@ const EuiCollapsibleNavItemDisplay: FunctionComponent<
/**
* Internal subcomponent for title display
*/
-const EuiCollapsibleNavItemTitle: FunctionComponent<
+export const EuiCollapsibleNavItemTitle: FunctionComponent<
Pick<
EuiCollapsibleNavItemProps,
'title' | 'titleElement' | 'icon' | 'iconProps'
@@ -182,31 +180,69 @@ const EuiCollapsibleNavItemTitle: FunctionComponent<
};
/**
- * Sub-items can either be a group title, to visually separate sections
- * of nav links, or they can simply be more links or accordions
+ * Sub-items can either be a totally custom rendered item,
+ * or they can simply be more links or accordions
*/
export const EuiCollapsibleNavSubItem: FunctionComponent<
EuiCollapsibleNavSubItemProps
-> = ({ isGroupTitle, className, ...props }) => {
- const euiTheme = useEuiTheme();
- const styles = euiCollapsibleNavSubItemGroupTitleStyles(euiTheme);
+> = ({ renderItem, className, ...props }) => {
const classes = classNames('euiCollapsibleNavSubItem', className);
- if (isGroupTitle) {
- const TitleElement = props.titleElement || 'div';
- return (
-
- {props.title}
-
- );
+ if (renderItem) {
+ return <>{renderItem()}>;
}
return (
-
+
+ );
+};
+
+/**
+ * Reuseable component for rendering a group of sub items
+ * Used by both `EuiCollapsibleNavGroup` and `EuiCollapsibleNavAccordion`
+ */
+type EuiCollapsibleNavSubItemsProps = HTMLAttributes &
+ _EuiCollapsibleNavItemDisplayProps & {
+ items: EuiCollapsibleNavSubItemProps[];
+ isGroup?: boolean;
+ };
+export const EuiCollapsibleNavSubItems: FunctionComponent<
+ EuiCollapsibleNavSubItemsProps
+> = ({ items, isSubItem, isGroup, className, ...rest }) => {
+ const classes = classNames('euiCollapsibleNavItem__items', className);
+
+ const euiTheme = useEuiTheme();
+ const styles = euiCollapsibleNavSubItemsStyles(euiTheme);
+ const cssStyles = [
+ styles.euiCollapsibleNavItem__items,
+ isGroup ? styles.isGroup : isSubItem ? styles.isSubItem : styles.isTopItem,
+ ];
+
+ const itemsHaveIcons = useMemo(
+ () => items.some((item) => !!item.icon),
+ [items]
+ );
+
+ return (
+
+ {items.map((item, index) => {
+ // If any of the sub items have an icon, default to an
+ // icon of `empty` so that all text lines up vertically
+ if (!item.renderItem && itemsHaveIcons && !item.icon) {
+ item.icon = 'empty';
+ }
+ return (
+ // This is an intentional circular dependency between the accordion & parent item display.
+ // EuiSideNavItem is purposely recursive to support any amount of nested sub items,
+ // and split up into separate files/components for better dev readability
+
+ );
+ })}
+
);
};
diff --git a/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts b/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts
index 60bb1ef33be..3cfd0ef8d2a 100644
--- a/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts
+++ b/src/components/collapsible_nav_beta/collapsible_nav_item/index.ts
@@ -9,7 +9,6 @@
export type {
EuiCollapsibleNavItemProps,
EuiCollapsibleNavSubItemProps,
- EuiCollapsibleNavSubItemGroupTitle,
} from './collapsible_nav_item';
export { EuiCollapsibleNavItem } from './collapsible_nav_item';
diff --git a/src/components/collapsible_nav_beta/index.ts b/src/components/collapsible_nav_beta/index.ts
index 4e5135afa7a..c393404f2fb 100644
--- a/src/components/collapsible_nav_beta/index.ts
+++ b/src/components/collapsible_nav_beta/index.ts
@@ -17,6 +17,5 @@ export { EuiCollapsibleNavBeta } from './collapsible_nav_beta';
export type {
EuiCollapsibleNavItemProps,
EuiCollapsibleNavSubItemProps,
- EuiCollapsibleNavSubItemGroupTitle,
} from './collapsible_nav_item';
export { EuiCollapsibleNavItem } from './collapsible_nav_item';