From c12fb27a90973f3877cfcc26f1373de976a6efcd Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 26 Sep 2023 12:50:10 -0700 Subject: [PATCH 1/7] Remove `isGroupTitle` API in favor of `renderItem` + provide Storybook examples of custom Kibana usage --- .../collapsible_nav_beta.stories.tsx | 37 +++++++++--- .../collapsible_nav_accordion.tsx | 29 +++++----- .../collapsible_nav_item.stories.tsx | 12 +--- .../collapsible_nav_item.styles.ts | 20 +------ .../collapsible_nav_item.test.tsx | 41 ++----------- .../collapsible_nav_item.tsx | 58 +++++++------------ .../collapsible_nav_item/index.ts | 1 - src/components/collapsible_nav_beta/index.ts | 1 - 8 files changed, 73 insertions(+), 126 deletions(-) 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..ad68078dad6 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx @@ -13,6 +13,7 @@ import { EuiHeader, EuiHeaderSection, EuiHeaderSectionItem } from '../header'; import { EuiPageTemplate } from '../page_template'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter } from '../flyout'; import { EuiButton } from '../button'; +import { EuiTitle } from '../title'; import { EuiCollapsibleNavItem } from './collapsible_nav_item'; import { @@ -58,6 +59,22 @@ const OpenCollapsibleNav: FunctionComponent< ); }; +const KibanaNavTitle: FunctionComponent<{ + title: string; +}> = ({ title }) => ( + ({ + marginTop: euiTheme.size.base, + paddingBlock: euiTheme.size.xs, + paddingInline: euiTheme.size.s, + })} + > +
{title}
+
+); + export const KibanaExample: Story = { render: ({ ...args }) => ( @@ -78,15 +95,15 @@ export const KibanaExample: Story = { href="#" items={[ { title: 'Get started', href: '#' }, - { title: 'Explore', isGroupTitle: true }, + { renderItem: () => }, { title: 'Discover', href: '#' }, { title: 'Dashboards', href: '#' }, { title: 'Visualize library', href: '#' }, - { title: 'Content', isGroupTitle: true }, + { renderItem: () => }, { title: 'Indices', href: '#' }, { title: 'Transforms', href: '#' }, { title: 'Indexing API', href: '#' }, - { title: 'Security', isGroupTitle: true }, + { renderItem: () => }, { title: 'API keys', href: '#' }, ]} /> @@ -115,7 +132,7 @@ export const KibanaExample: Story = { { title: 'Alerts', href: '#' }, { title: 'Cases', href: '#' }, { title: 'SLOs', href: '#' }, - { title: 'Signals', isGroupTitle: true }, + { renderItem: () => }, { title: 'Logs', href: '#' }, { title: 'Tracing', @@ -126,7 +143,7 @@ export const KibanaExample: Story = { { title: 'Dependencies', href: '#' }, ], }, - { title: 'Toolbox', isGroupTitle: true }, + { renderItem: () => }, { title: 'Visualize library', href: '#' }, { title: 'Dashboards', href: '#' }, { @@ -217,18 +234,20 @@ export const KibanaExample: Story = { { title: 'Overview', href: '#' }, { title: 'Notifications', href: '#' }, { title: 'Memory usage', href: '#' }, - { title: 'Anomaly detection', isGroupTitle: true }, + { renderItem: () => }, { title: 'Jobs', href: '#' }, { title: 'Anomaly explorer', href: '#' }, { title: 'Single metric viewer', href: '#' }, { title: 'Settings', href: '#' }, - { title: 'Data frame analytics', isGroupTitle: true }, + { + renderItem: () => , + }, { title: 'Jobs', href: '#' }, { title: 'Results explorer', href: '#' }, { title: 'Analytics map', href: '#' }, - { title: 'Model management', isGroupTitle: true }, + { renderItem: () => }, { title: 'Trained models', href: '#' }, - { title: 'Data visualizer', isGroupTitle: true }, + { renderItem: () => }, { title: 'File', href: '#' }, { title: 'Data view', href: '#' }, ]} 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..dc98ffbe649 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 @@ -20,9 +20,9 @@ import { EuiAccordion } from '../../accordion'; import { EuiCollapsibleNavSubItem, + EuiCollapsibleNavSubItemProps, _SharedEuiCollapsibleNavItemProps, _EuiCollapsibleNavItemDisplayProps, - EuiCollapsibleNavItemProps, } from './collapsible_nav_item'; import { EuiCollapsibleNavLink } from './collapsible_nav_link'; import { euiCollapsibleNavAccordionStyles } from './collapsible_nav_accordion.styles'; @@ -33,10 +33,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[]; }; /** @@ -91,13 +88,10 @@ export const EuiCollapsibleNavAccordion: FunctionComponent< /** * 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, @@ -109,12 +103,19 @@ export const EuiCollapsibleNavAccordion: FunctionComponent< css={childrenCssStyles} className="euiCollapsibleNavAccordion__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 - - ))} + {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/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..bef726168fc 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 @@ -9,11 +9,7 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../../services'; -import { - logicalCSS, - logicalShorthandCSS, - euiFontSize, -} from '../../../global_styling'; +import { euiFontSize } from '../../../global_styling'; import { euiButtonColor } from '../../../themes/amsterdam/global_styling/mixins/button'; /** @@ -47,17 +43,3 @@ export const euiCollapsibleNavItemTitleStyles = { flex-grow: 1; `, }; - -export const euiCollapsibleNavSubItemGroupTitleStyles = ({ - euiTheme, -}: UseEuiTheme) => { - return { - euiCollapsibleNavItem__groupTitle: css` - ${logicalCSS('margin-top', euiTheme.size.base)} - ${logicalShorthandCSS( - 'padding', - `${euiTheme.size.xs} ${euiTheme.size.s}` - )} - `, - }; -}; 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..5727b739f27 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 @@ -145,51 +145,18 @@ describe('EuiCollapsibleNavItem', () => { ).toHaveLength(5); }); - it('renders group titles', () => { - const { container } = render( - - ); - - expect(container.querySelector('.euiCollapsibleNavItem__groupTitle')) - .toMatchInlineSnapshot(` -
- Section -
- `); - }); - - 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..709637b2d87 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,25 +6,25 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, HTMLAttributes, useContext } from 'react'; +import React, { + FunctionComponent, + HTMLAttributes, + ReactNode, + useContext, +} from 'react'; import classNames from 'classnames'; -import { useEuiTheme } from '../../../services'; 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'; import { EuiCollapsibleNavAccordion } from './collapsible_nav_accordion'; import { EuiCollapsibleNavLink } from './collapsible_nav_link'; -import { - euiCollapsibleNavItemTitleStyles, - euiCollapsibleNavSubItemGroupTitleStyles, -} from './collapsible_nav_item.styles'; +import { euiCollapsibleNavItemTitleStyles } from './collapsible_nav_item.styles'; export type _SharedEuiCollapsibleNavItemProps = HTMLAttributes & CommonProps & { @@ -37,8 +37,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 +79,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 = { @@ -182,31 +175,24 @@ 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 ( - + ); }; 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'; From bc1e8a29e219bfd056e757b7c439712bab3e2c19 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 26 Sep 2023 16:54:44 -0700 Subject: [PATCH 2/7] [kibana] Provide example of "grouped" API sitting on top of flatter EUI items API --- .../collapsible_nav_beta.stories.tsx | 150 ++++++++++-------- 1 file changed, 83 insertions(+), 67 deletions(-) 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 ad68078dad6..4dc29d2e083 100644 --- a/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx +++ b/src/components/collapsible_nav_beta/collapsible_nav_beta.stories.tsx @@ -15,7 +15,10 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter } from '../flyout'; import { EuiButton } from '../button'; import { EuiTitle } from '../title'; -import { EuiCollapsibleNavItem } from './collapsible_nav_item'; +import { + EuiCollapsibleNavItem, + EuiCollapsibleNavItemProps, +} from './collapsible_nav_item'; import { EuiCollapsibleNavBeta, EuiCollapsibleNavBetaProps, @@ -59,21 +62,29 @@ const OpenCollapsibleNav: FunctionComponent< ); }; -const KibanaNavTitle: FunctionComponent<{ - title: string; -}> = ({ title }) => ( - ({ - marginTop: euiTheme.size.base, - paddingBlock: euiTheme.size.xs, - paddingInline: euiTheme.size.s, - })} - > -
{title}
-
-); +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 }) => ( @@ -95,16 +106,17 @@ export const KibanaExample: Story = { href="#" items={[ { title: 'Get started', href: '#' }, - { renderItem: () => }, - { title: 'Discover', href: '#' }, - { title: 'Dashboards', href: '#' }, - { title: 'Visualize library', href: '#' }, - { renderItem: () => }, - { title: 'Indices', href: '#' }, - { title: 'Transforms', href: '#' }, - { title: 'Indexing API', href: '#' }, - { renderItem: () => }, - { title: 'API keys', href: '#' }, + ...renderGroup('Explore', [ + { title: 'Discover', href: '#' }, + { title: 'Dashboards', href: '#' }, + { title: 'Visualize library', href: '#' }, + ]), + ...renderGroup('Content', [ + { title: 'Indices', href: '#' }, + { title: 'Transforms', href: '#' }, + { title: 'Indexing API', href: '#' }, + ]), + ...renderGroup('Security', [{ title: 'API keys', href: '#' }]), ]} /> }, - { title: 'Logs', href: '#' }, - { - title: 'Tracing', - href: '#', - items: [ - { title: 'Services', href: '#' }, - { title: 'Traces', href: '#' }, - { title: 'Dependencies', href: '#' }, - ], - }, - { renderItem: () => }, - { title: 'Visualize library', href: '#' }, - { title: 'Dashboards', href: '#' }, - { - title: 'AIOps', - href: '#', - items: [ - { title: 'Anomaly detection', href: '#' }, - { title: 'Spike analysis', href: '#' }, - { title: 'Change point detection', href: '#' }, - { title: 'Notifications', href: '#' }, - ], - }, - { title: 'Add data', href: '#' }, + ...renderGroup('Signals', [ + { title: 'Logs', href: '#' }, + { + title: 'Tracing', + href: '#', + items: [ + { title: 'Services', href: '#' }, + { title: 'Traces', href: '#' }, + { title: 'Dependencies', href: '#' }, + ], + }, + ]), + ...renderGroup('Toolbox', [ + { title: 'Visualize library', href: '#' }, + { title: 'Dashboards', href: '#' }, + { + title: 'AIOps', + href: '#', + items: [ + { title: 'Anomaly detection', href: '#' }, + { title: 'Spike analysis', href: '#' }, + { title: 'Change point detection', href: '#' }, + { title: 'Notifications', href: '#' }, + ], + }, + { title: 'Add data', href: '#' }, + ]), ]} /> }, - { title: 'Jobs', href: '#' }, - { title: 'Anomaly explorer', href: '#' }, - { title: 'Single metric viewer', href: '#' }, - { title: 'Settings', href: '#' }, - { - renderItem: () => , - }, - { title: 'Jobs', href: '#' }, - { title: 'Results explorer', href: '#' }, - { title: 'Analytics map', href: '#' }, - { renderItem: () => }, - { title: 'Trained models', href: '#' }, - { renderItem: () => }, - { title: 'File', href: '#' }, - { title: 'Data view', href: '#' }, + ...renderGroup('Anomaly detection', [ + { title: 'Jobs', href: '#' }, + { title: 'Anomaly explorer', href: '#' }, + { title: 'Single metric viewer', href: '#' }, + { title: 'Settings', href: '#' }, + ]), + ...renderGroup('Data frame analytics', [ + { title: 'Jobs', href: '#' }, + { title: 'Results explorer', href: '#' }, + { title: 'Analytics map', href: '#' }, + ]), + ...renderGroup('Model management', [ + { title: 'Trained models', href: '#' }, + ]), + ...renderGroup('Data visualizer', [ + { title: 'File', href: '#' }, + { title: 'Data view', href: '#' }, + ]), ]} /> From 4a741a39c053bbec59df2b9baa7a8a8658394890 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 26 Sep 2023 13:59:46 -0700 Subject: [PATCH 3/7] [setup] Create DRY `EuiCollapsibleNavSubItems` component - split it out from `EuiCollapsibleNavAccordion` --- .../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 | 46 +++-------------- .../collapsible_nav_item.styles.ts | 35 ++++++++++++- .../collapsible_nav_item.test.tsx | 24 +++++++++ .../collapsible_nav_item.tsx | 51 ++++++++++++++++++- 8 files changed, 118 insertions(+), 90 deletions(-) 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 dc98ffbe649..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,7 +18,7 @@ import { useEuiTheme, useGeneratedHtmlId } from '../../../services'; import { EuiAccordion } from '../../accordion'; import { - EuiCollapsibleNavSubItem, + EuiCollapsibleNavSubItems, EuiCollapsibleNavSubItemProps, _SharedEuiCollapsibleNavItemProps, _EuiCollapsibleNavItemDisplayProps, @@ -70,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 @@ -85,40 +81,6 @@ export const EuiCollapsibleNavAccordion: FunctionComponent< [linkProps?.onClick] // eslint-disable-line react-hooks/exhaustive-deps ); - /** - * Child items - */ - const itemsHaveIcons = useMemo( - () => items.some((item) => !!item.icon), - [items] - ); - - const childrenCssStyles = [ - styles.children.euiCollapsibleNavAccordion__children, - isSubItem ? styles.children.isSubItem : styles.children.isTopItem, - ]; - - const children = ( -
- {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 - - ); - })} -
- ); - return ( - {children} + ); }; 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 bef726168fc..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 @@ -9,7 +9,11 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../../services'; -import { euiFontSize } from '../../../global_styling'; +import { + logicalCSS, + mathWithUnits, + euiFontSize, +} from '../../../global_styling'; import { euiButtonColor } from '../../../themes/amsterdam/global_styling/mixins/button'; /** @@ -43,3 +47,32 @@ export const euiCollapsibleNavItemTitleStyles = { flex-grow: 1; `, }; + +/** + * Sub item groups + */ + +export const euiCollapsibleNavSubItemsStyles = ({ euiTheme }: UseEuiTheme) => { + return { + 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 5727b739f27..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,11 +140,35 @@ describe('EuiCollapsibleNavItem', () => { /> ); + expect( + container.querySelectorAll('.euiCollapsibleNavItem__items') + ).toHaveLength(2); expect( container.querySelectorAll('.euiCollapsibleNavSubItem') ).toHaveLength(5); }); + 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 rendering totally custom sub items', () => { const { getByTestSubject } = render( & CommonProps & { @@ -196,6 +201,50 @@ export const EuiCollapsibleNavSubItem: FunctionComponent< ); }; +/** + * Reuseable component for rendering a group of sub items + * Used by both `EuiCollapsibleNavGroup` and `EuiCollapsibleNavAccordion` + */ +type EuiCollapsibleNavSubItemsProps = HTMLAttributes & + _EuiCollapsibleNavItemDisplayProps & { + items: EuiCollapsibleNavSubItemProps[]; + }; +export const EuiCollapsibleNavSubItems: FunctionComponent< + EuiCollapsibleNavSubItemsProps +> = ({ items, isSubItem, className, ...rest }) => { + const classes = classNames('euiCollapsibleNavItem__items', className); + + const euiTheme = useEuiTheme(); + const styles = euiCollapsibleNavSubItemsStyles(euiTheme); + const cssStyles = [ + styles.euiCollapsibleNavItem__items, + 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 + + ); + })} +
+ ); +}; + /** * The actual exported component */ From ee2c6e03e42805b6a4f3ec1c116ab55a15465049 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 26 Sep 2023 14:22:18 -0700 Subject: [PATCH 4/7] Create new beta EuiCollapsibleNavGroup subcomponent - usage is still fairly beta --- .../collapsible_nav_group.test.tsx.snap | 67 +++++++++++++++ .../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_item.tsx | 7 +- 6 files changed, 252 insertions(+), 3 deletions(-) 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.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/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.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/collapsible_nav_item.tsx b/src/components/collapsible_nav_beta/collapsible_nav_item/collapsible_nav_item.tsx index bc0e504b95f..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 @@ -156,7 +156,7 @@ const EuiCollapsibleNavItemDisplay: FunctionComponent< /** * Internal subcomponent for title display */ -const EuiCollapsibleNavItemTitle: FunctionComponent< +export const EuiCollapsibleNavItemTitle: FunctionComponent< Pick< EuiCollapsibleNavItemProps, 'title' | 'titleElement' | 'icon' | 'iconProps' @@ -208,17 +208,18 @@ export const EuiCollapsibleNavSubItem: FunctionComponent< type EuiCollapsibleNavSubItemsProps = HTMLAttributes & _EuiCollapsibleNavItemDisplayProps & { items: EuiCollapsibleNavSubItemProps[]; + isGroup?: boolean; }; export const EuiCollapsibleNavSubItems: FunctionComponent< EuiCollapsibleNavSubItemsProps -> = ({ items, isSubItem, className, ...rest }) => { +> = ({ items, isSubItem, isGroup, className, ...rest }) => { const classes = classNames('euiCollapsibleNavItem__items', className); const euiTheme = useEuiTheme(); const styles = euiCollapsibleNavSubItemsStyles(euiTheme); const cssStyles = [ styles.euiCollapsibleNavItem__items, - isSubItem ? styles.isSubItem : styles.isTopItem, + isGroup ? styles.isGroup : isSubItem ? styles.isSubItem : styles.isTopItem, ]; const itemsHaveIcons = useMemo( From 5152f08a059d48bf44925498b657b1151e38ffdd Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 26 Sep 2023 15:13:33 -0700 Subject: [PATCH 5/7] [refactor] Add new body & footer components - instead of relying on consumers to use `EuiFlyoutBody` and `EuiFlyoutFooter` directly --- .../collapsible_nav_body_footer.test.tsx.snap | 26 +++++++ .../collapsible_nav_beta.styles.ts | 25 ++----- .../collapsible_nav_body_footer.styles.ts | 44 ++++++++++++ .../collapsible_nav_body_footer.test.tsx | 50 ++++++++++++++ .../collapsible_nav_body_footer.tsx | 69 +++++++++++++++++++ 5 files changed, 193 insertions(+), 21 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 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`] = ` +