From 0f3609ae4ae78fcbd3798a671d1994f0ac66e130 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:04:11 -0700 Subject: [PATCH] [EuiBreadcrumb] Fix styling on `application` types with popovers + perf improvements (#7580) --- changelogs/upcoming/7580.md | 3 + .../_breadcrumb_content.test.tsx.snap | 104 ++++++++ .../__snapshots__/breadcrumb.test.tsx.snap | 100 ++++---- .../__snapshots__/breadcrumbs.test.tsx.snap | 36 +-- .../breadcrumbs/_breadcrumb_content.styles.ts | 143 +++++++++++ .../breadcrumbs/_breadcrumb_content.test.tsx | 124 ++++++++++ .../breadcrumbs/_breadcrumb_content.tsx | 222 ++++++++++++++++++ .../breadcrumbs/breadcrumb.styles.ts | 126 +--------- .../breadcrumbs/breadcrumb.test.tsx | 128 ++-------- src/components/breadcrumbs/breadcrumb.tsx | 206 +--------------- .../breadcrumbs/breadcrumbs.stories.tsx | 3 +- src/components/breadcrumbs/breadcrumbs.tsx | 75 +----- src/components/breadcrumbs/index.ts | 4 +- src/components/breadcrumbs/types.ts | 126 ++++++++++ .../header/__snapshots__/header.test.tsx.snap | 2 +- .../header_breadcrumbs.test.tsx.snap | 6 +- .../header_breadcrumbs/header_breadcrumbs.tsx | 8 +- 17 files changed, 839 insertions(+), 577 deletions(-) create mode 100644 changelogs/upcoming/7580.md create mode 100644 src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap create mode 100644 src/components/breadcrumbs/_breadcrumb_content.styles.ts create mode 100644 src/components/breadcrumbs/_breadcrumb_content.test.tsx create mode 100644 src/components/breadcrumbs/_breadcrumb_content.tsx create mode 100644 src/components/breadcrumbs/types.ts diff --git a/changelogs/upcoming/7580.md b/changelogs/upcoming/7580.md new file mode 100644 index 00000000000..5423aedd343 --- /dev/null +++ b/changelogs/upcoming/7580.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed a visual bug with `EuiHeaderBreadcrumbs` with popovers diff --git a/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap new file mode 100644 index 00000000000..002da4ac433 --- /dev/null +++ b/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiBreadcrumbContent breadcrumbs with popovers renders with \`popoverContent\` 1`] = ` + +
+
+ +
+
+
+
+
+ +
+
+ +`; + +exports[`EuiBreadcrumbContent renders interactive breadcrumbs with href or onClick 1`] = ` +
+ + Link + + +
+`; + +exports[`EuiBreadcrumbContent renders plain uninteractive breadcrumb text 1`] = ` + + Text + +`; diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap index 116e054b27e..aae72d83cd5 100644 --- a/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +++ b/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap @@ -1,30 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiBreadcrumbContent breadcrumbs with popovers renders with \`popoverContent\` 1`] = ` +exports[`EuiBreadcrumb is a light
  • wrapper around the content 1`] = ` +
  • + Hello world +
  • +`; + +exports[`EuiBreadcrumbCollapsed renders a ... breadcrumb with collapsed content in a popover 1`] = `
    -
    - -
    + + + … + + + + - Clicking this button will toggle a popover dialog. + + +
    +
    `; - -exports[`EuiBreadcrumbContent renders interactive breadcrumbs with href or onClick 1`] = ` -
    - - Link - - -
    -`; - -exports[`EuiBreadcrumbContent renders plain uninteractive breadcrumb text 1`] = ` -
    - - Text - -
    -`; diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap index 27252750a54..8be50405a28 100644 --- a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap +++ b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap @@ -39,15 +39,15 @@ exports[`EuiBreadcrumbs is rendered 1`] = ` data-test-subj="euiBreadcrumb" >
    + )} + /> + ); + + fireEvent.click(getByTestSubject('popoverToggle')); + await waitForEuiPopoverOpen(); + + fireEvent.click(getByTestSubject('popoverClose')); + await waitForEuiPopoverClose(); + }); + }); + + describe('highlightLastBreadcrumb', () => { + it('adds an aria-current attr', () => { + const { getByText } = render( + + ); + expect(getByText('Home')).toHaveAttribute('aria-current', 'page'); + }); + + it('colors both interactive and non-interactive breadcrumbs text-colored', () => { + const { getByTestSubject } = render( + <> + + + + + + ); + expect(getByTestSubject('control')).toHaveStyleRule('color', '#646a77'); + expect(getByTestSubject('text')).toHaveStyleRule('color', '#343741'); + expect(getByTestSubject('link')).toHaveStyleRule('color', '#343741'); + expect(getByTestSubject('popover')).toHaveStyleRule('color', '#343741'); + }); + }); +}); diff --git a/src/components/breadcrumbs/_breadcrumb_content.tsx b/src/components/breadcrumbs/_breadcrumb_content.tsx new file mode 100644 index 00000000000..9547d6718d9 --- /dev/null +++ b/src/components/breadcrumbs/_breadcrumb_content.tsx @@ -0,0 +1,222 @@ +/* + * 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, + useState, + useCallback, + forwardRef, +} from 'react'; +import { ArrayCSSInterpolation } from '@emotion/css'; +import classNames from 'classnames'; + +import { useEuiMemoizedStyles } from '../../services'; +import { EuiInnerText } from '../inner_text'; +import { EuiTextColor } from '../text'; +import { EuiLink } from '../link'; +import { EuiPopover } from '../popover'; +import { EuiIcon } from '../icon'; +import { useEuiI18n } from '../i18n'; + +import type { EuiBreadcrumbProps, _EuiBreadcrumbProps } from './types'; +import { + euiBreadcrumbContentStyles, + euiBreadcrumbPopoverStyles, +} from './_breadcrumb_content.styles'; + +export const EuiBreadcrumbContent: FunctionComponent< + EuiBreadcrumbProps & _EuiBreadcrumbProps +> = ({ + text, + truncate, + type, + href, + rel, // required by our local href-with-rel eslint rule + onClick, + popoverContent, + popoverProps, + className, + color, + isFirstBreadcrumb, + isLastBreadcrumb, + isOnlyBreadcrumb, + highlightLastBreadcrumb, + truncateLastBreadcrumb, + ...rest +}) => { + const classes = classNames('euiBreadcrumb__content', className); + + const styles = useEuiMemoizedStyles(euiBreadcrumbContentStyles); + const cssStyles = [styles.euiBreadcrumb__content, styles[type]]; + if (type === 'application') { + if (isOnlyBreadcrumb) { + cssStyles.push(styles.applicationStyles.onlyChild); + } else if (isFirstBreadcrumb) { + cssStyles.push(styles.applicationStyles.firstChild); + } else if (isLastBreadcrumb) { + cssStyles.push(styles.applicationStyles.lastChild); + } + } + const truncationStyles = [ + truncate && !truncateLastBreadcrumb && styles.isTruncated, + truncateLastBreadcrumb && styles.isTruncatedLast, + ]; + + const isBreadcrumbWithPopover = !!popoverContent; + const isInteractiveBreadcrumb = href || onClick; + const linkColor = color || (highlightLastBreadcrumb ? 'text' : 'subdued'); + const plainTextColor = highlightLastBreadcrumb ? 'default' : 'subdued'; // Does not inherit `color` prop + const ariaCurrent = highlightLastBreadcrumb ? ('page' as const) : undefined; + + return ( + + {(ref, innerText) => { + const title = innerText === '' ? undefined : innerText; + const baseProps = { + ref, + title, + 'aria-current': ariaCurrent, + className: classes, + css: [...cssStyles, ...truncationStyles], + }; + + if (isBreadcrumbWithPopover) { + const { css: _, ...popoverButtonProps } = baseProps; + return ( + + {text} + + ); + } else if (isInteractiveBreadcrumb) { + return ( + + {text} + + ); + } else { + return ( + + + {text} + + + ); + } + }} + + ); +}; + +type EuiBreadcrumbPopoverProps = HTMLAttributes & + Pick & + Pick<_EuiBreadcrumbProps, 'type' | 'isLastBreadcrumb'> & { + breadcrumbCss: ArrayCSSInterpolation; + truncationCss: ArrayCSSInterpolation; + }; +const EuiBreadcrumbPopover = forwardRef< + HTMLButtonElement, + EuiBreadcrumbPopoverProps +>( + ( + { + popoverContent, + popoverProps, + color, + type, + title, + 'aria-current': ariaCurrent, + className, + isLastBreadcrumb, + breadcrumbCss, + truncationCss, + children, + ...rest + }, + ref + ) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const togglePopover = useCallback( + () => setIsPopoverOpen((isOpen) => !isOpen), + [] + ); + + const popoverAriaLabel = useEuiI18n( + // This component was moved into another file for organization/dev readability, + // but we're keeping the i18n token the same as before for consumer consistency + // eslint-disable-next-line local/i18n + 'euiBreadcrumb.popoverAriaLabel', + 'Clicking this button will toggle a popover dialog.' + ); + + const styles = useEuiMemoizedStyles(euiBreadcrumbPopoverStyles); + const wrapperStyles = [ + styles.popoverWrapper.euiBreadcrumb__popoverWrapper, + !isLastBreadcrumb && styles.popoverWrapper[type], + ]; + const buttonStyles = [ + styles.euiBreadcrumb__popoverButton, + ...breadcrumbCss, + ]; + const truncationStyles = [ + styles.euiBreadcrumb__popoverTruncation, + ...truncationCss, + ]; + + return ( + + {children} + + + } + > + {typeof popoverContent === 'function' + ? popoverContent(closePopover) + : popoverContent} + + ); + } +); +EuiBreadcrumbPopover.displayName = 'EuiBreadcrumbPopover'; diff --git a/src/components/breadcrumbs/breadcrumb.styles.ts b/src/components/breadcrumbs/breadcrumb.styles.ts index 2950c4951e9..126c787cffa 100644 --- a/src/components/breadcrumbs/breadcrumb.styles.ts +++ b/src/components/breadcrumbs/breadcrumb.styles.ts @@ -8,18 +8,12 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../services'; -import { transparentize } from '../../services/color'; -import { - euiFontSize, - euiTextTruncate, - euiFocusRing, - logicalCSS, - logicalBorderRadiusCSS, - mathWithUnits, -} from '../../global_styling'; +import { logicalCSS } from '../../global_styling'; +/** + * Styles cast to
  • element + */ export const euiBreadcrumbStyles = (euiThemeContext: UseEuiTheme) => { - // Styles cast to
  • element const { euiTheme } = euiThemeContext; return { euiBreadcrumb: css` @@ -61,115 +55,3 @@ export const euiBreadcrumbStyles = (euiThemeContext: UseEuiTheme) => { `, }; }; - -export const euiBreadcrumbContentStyles = (euiThemeContext: UseEuiTheme) => { - // Styles cast to , , or collapsed - )} - /> - ); - - fireEvent.click(getByTestSubject('popoverToggle')); - await waitForEuiPopoverOpen(); - - fireEvent.click(getByTestSubject('popoverClose')); - await waitForEuiPopoverClose(); - }); - }); - - describe('highlightLastBreadcrumb', () => { - it('adds an aria-current attr', () => { - const { getByText } = render( - - ); - expect(getByText('Home')).toHaveAttribute('aria-current', 'page'); - }); - - it('colors both interactive and non-interactive breadcrumbs text-colored', () => { - const { getByTestSubject } = render( - <> - - - - - - ); - expect(getByTestSubject('control')).toHaveStyleRule('color', '#646a77'); - expect(getByTestSubject('text')).toHaveStyleRule('color', '#343741'); - expect(getByTestSubject('link')).toHaveStyleRule('color', '#343741'); - expect(getByTestSubject('popover')).toHaveStyleRule('color', '#343741'); - }); + fireEvent.click(getByRole('button')); + waitForEuiPopoverOpen(); + expect(getByText('I render inside the popover')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); }); }); diff --git a/src/components/breadcrumbs/breadcrumb.tsx b/src/components/breadcrumbs/breadcrumb.tsx index 13daeef593e..80c5c813a21 100644 --- a/src/components/breadcrumbs/breadcrumb.tsx +++ b/src/components/breadcrumbs/breadcrumb.tsx @@ -9,86 +9,26 @@ import React, { FunctionComponent, HTMLAttributes, - AriaAttributes, - MouseEventHandler, - ReactNode, - useState, PropsWithChildren, } from 'react'; import classNames from 'classnames'; -import { useEuiTheme } from '../../services'; -import { CommonProps } from '../common'; -import { EuiInnerText } from '../inner_text'; -import { EuiTextColor } from '../text'; -import { EuiLink, EuiLinkColor } from '../link'; -import { EuiPopover, EuiPopoverProps } from '../popover'; -import { EuiIcon } from '../icon'; +import { useEuiMemoizedStyles } from '../../services'; import { useEuiI18n } from '../i18n'; -import { - euiBreadcrumbStyles, - euiBreadcrumbContentStyles, -} from './breadcrumb.styles'; +import type { EuiBreadcrumbProps, _EuiBreadcrumbProps } from './types'; +import { EuiBreadcrumbContent } from './_breadcrumb_content'; -export type EuiBreadcrumbProps = Omit< - HTMLAttributes, - 'color' | 'aria-current' -> & - CommonProps & { - href?: string; - rel?: string; - onClick?: MouseEventHandler; - /** - * Visible label of the breadcrumb - */ - text: ReactNode; - /** - * Force a max-width on the breadcrumb text - */ - truncate?: boolean; - /** - * Accepts any EuiLink `color` when rendered as one (has `href`, `onClick`, or `popoverContent`) - */ - color?: EuiLinkColor; - /** - * Override the existing `aria-current` which defaults to `page` for the last breadcrumb - */ - 'aria-current'?: AriaAttributes['aria-current']; - /** - * Creates a breadcrumb that toggles a popover dialog. Takes any rendered node(s), - * or a render function that will pass callback allowing you to close the - * breadcrumb popover from within your popover content. - * - * If passed, both `href` and `onClick` will be ignored - the breadcrumb's - * click behavior should only trigger a popover. - */ - popoverContent?: ReactNode | ((closePopover: () => void) => ReactNode); - /** - * Allows customizing the popover if necessary. Accepts any props that - * [EuiPopover](/#/layout/popover) accepts, except for props that control state. - */ - popoverProps?: Omit; - }; - -// Used internally only by the parent EuiBreadcrumbs -type _EuiBreadcrumbProps = PropsWithChildren & - Pick & { - type: 'page' | 'application'; - isFirstBreadcrumb?: boolean; - isLastBreadcrumb?: boolean; - isOnlyBreadcrumb?: boolean; - highlightLastBreadcrumb?: boolean; - truncateLastBreadcrumb?: boolean; - }; +import { euiBreadcrumbStyles } from './breadcrumb.styles'; export const EuiBreadcrumb: FunctionComponent< - HTMLAttributes & _EuiBreadcrumbProps + HTMLAttributes & + Pick<_EuiBreadcrumbProps, 'type'> & + Pick > = ({ children, className, type, truncate, ...rest }) => { const classes = classNames('euiBreadcrumb', className); - const euiTheme = useEuiTheme(); - const styles = euiBreadcrumbStyles(euiTheme); + const styles = useEuiMemoizedStyles(euiBreadcrumbStyles); const cssStyles = [ styles.euiBreadcrumb, styles[type], @@ -107,132 +47,10 @@ export const EuiBreadcrumb: FunctionComponent< ); }; -export const EuiBreadcrumbContent: FunctionComponent< - EuiBreadcrumbProps & _EuiBreadcrumbProps -> = ({ - text, - truncate, - type, - href, - rel, // required by our local href-with-rel eslint rule - onClick, - popoverContent, - popoverProps, - className, - color, - isFirstBreadcrumb, - isLastBreadcrumb, - isOnlyBreadcrumb, - highlightLastBreadcrumb, - truncateLastBreadcrumb, - ...rest -}) => { - const classes = classNames('euiBreadcrumb__content', className); - - const euiTheme = useEuiTheme(); - const styles = euiBreadcrumbContentStyles(euiTheme); - const cssStyles = [ - styles.euiBreadcrumb__content, - styles[type], - truncate && !truncateLastBreadcrumb && styles.isTruncated, - truncateLastBreadcrumb && styles.isTruncatedLast, - ]; - if (type === 'application') { - if (isOnlyBreadcrumb) { - cssStyles.push(styles.applicationStyles.onlyChild); - } else if (isFirstBreadcrumb) { - cssStyles.push(styles.applicationStyles.firstChild); - } else if (isLastBreadcrumb) { - cssStyles.push(styles.applicationStyles.lastChild); - } - } - - const isInteractiveBreadcrumb = href || onClick; - const linkColor = color || (highlightLastBreadcrumb ? 'text' : 'subdued'); - const plainTextColor = highlightLastBreadcrumb ? 'default' : 'subdued'; // Does not inherit `color` prop - const ariaCurrent = highlightLastBreadcrumb ? ('page' as const) : undefined; - - const isPopoverBreadcrumb = !!popoverContent; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const popoverAriaLabel = useEuiI18n( - 'euiBreadcrumb.popoverAriaLabel', - 'Clicking this button will toggle a popover dialog.' - ); - - return ( - - {(ref, innerText) => { - const title = innerText === '' ? undefined : innerText; - - const baseProps = { ref, title, 'aria-current': ariaCurrent }; - const styleProps = { className: classes, css: cssStyles }; - - if (isPopoverBreadcrumb) { - const closePopover = () => setIsPopoverOpen(false); - return ( - setIsPopoverOpen((isOpen) => !isOpen)} - {...rest} - > - {text} - - - } - > - {typeof popoverContent === 'function' - ? popoverContent(closePopover) - : popoverContent} - - ); - } else if (isInteractiveBreadcrumb) { - return ( - - {text} - - ); - } else { - return ( - - - {text} - - - ); - } - }} - - ); -}; - -export const EuiBreadcrumbCollapsed: FunctionComponent<_EuiBreadcrumbProps> = ({ - children, - isFirstBreadcrumb, - type, -}) => { - const euiTheme = useEuiTheme(); - const styles = euiBreadcrumbStyles(euiTheme); +export const EuiBreadcrumbCollapsed: FunctionComponent< + PropsWithChildren & Pick<_EuiBreadcrumbProps, 'type' | 'isFirstBreadcrumb'> +> = ({ children, isFirstBreadcrumb, type }) => { + const styles = useEuiMemoizedStyles(euiBreadcrumbStyles); const cssStyles = [styles.isCollapsed]; const ariaLabel = useEuiI18n( diff --git a/src/components/breadcrumbs/breadcrumbs.stories.tsx b/src/components/breadcrumbs/breadcrumbs.stories.tsx index 8bab5a82205..e2a7241f11f 100644 --- a/src/components/breadcrumbs/breadcrumbs.stories.tsx +++ b/src/components/breadcrumbs/breadcrumbs.stories.tsx @@ -8,7 +8,8 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { EuiBreadcrumbs, EuiBreadcrumbsProps } from './breadcrumbs'; +import type { EuiBreadcrumbsProps } from './types'; +import { EuiBreadcrumbs } from './breadcrumbs'; const meta: Meta = { title: 'Navigation/EuiBreadcrumbs', diff --git a/src/components/breadcrumbs/breadcrumbs.tsx b/src/components/breadcrumbs/breadcrumbs.tsx index 71d381982cd..5eb4f4937a3 100644 --- a/src/components/breadcrumbs/breadcrumbs.tsx +++ b/src/components/breadcrumbs/breadcrumbs.tsx @@ -9,74 +9,20 @@ import React, { FunctionComponent, useMemo } from 'react'; import classNames from 'classnames'; -import { CommonProps, ExclusiveUnion } from '../common'; +import { ExclusiveUnion } from '../common'; import { useEuiI18n } from '../i18n'; -import { - useEuiTheme, - EuiBreakpointSize, - useCurrentEuiBreakpoint, -} from '../../services'; - -import { - EuiBreadcrumb, - EuiBreadcrumbContent, - EuiBreadcrumbCollapsed, +import { useEuiMemoizedStyles, useCurrentEuiBreakpoint } from '../../services'; + +import type { + EuiBreadcrumbResponsiveMaxCount, + EuiBreadcrumbsProps, EuiBreadcrumbProps, -} from './breadcrumb'; +} from './types'; +import { EuiBreadcrumb, EuiBreadcrumbCollapsed } from './breadcrumb'; +import { EuiBreadcrumbContent } from './_breadcrumb_content'; import { euiBreadcrumbsListStyles } from './breadcrumbs.styles'; -export type EuiBreadcrumbResponsiveMaxCount = { - /** - * Any of the following keys are allowed: `'xs' | 's' | 'm' | 'l' | 'xl'` - * Omitting a key will display all breadcrumbs at that breakpoint - */ - [key in EuiBreakpointSize]?: number; -}; - -export type EuiBreadcrumbsProps = CommonProps & { - /** - * Hides extra (above the max) breadcrumbs under a collapsed item as the window gets smaller. - * Pass a custom #EuiBreadcrumbResponsiveMaxCount object to change the number of breadcrumbs to show at the particular breakpoints. - * - * Pass `false` to turn this behavior off. - * - * Default: `{ xs: 1, s: 2, m: 4 }` - */ - responsive?: boolean | EuiBreadcrumbResponsiveMaxCount; - - /** - * Forces all breadcrumbs to single line and - * truncates each breadcrumb to a particular width, - * except for the last item - */ - truncate?: boolean; - - /** - * Collapses the inner items past the maximum set here - * into a single ellipses item. - * Omitting or passing a `0` value will show all breadcrumbs. - */ - max?: number | null; - - /** - * The array of individual #EuiBreadcrumb items - */ - breadcrumbs: EuiBreadcrumbProps[]; - - /** - * Determines breadcrumbs appearance, with `page` being the default styling. - * Application breadcrumbs should only be once per page, in (e.g.) EuiHeader - */ - type?: 'page' | 'application'; - - /** - * Whether the last breadcrumb should visually (and accessibly, to screen readers) - * be highlighted as the current page. Defaults to true. - */ - lastBreadcrumbIsCurrentPage?: boolean; -}; - const responsiveDefault: EuiBreadcrumbResponsiveMaxCount = { xs: 1, s: 2, @@ -95,8 +41,7 @@ export const EuiBreadcrumbs: FunctionComponent = ({ }) => { const ariaLabel = useEuiI18n('euiBreadcrumbs.nav.ariaLabel', 'Breadcrumbs'); - const euiTheme = useEuiTheme(); - const breadcrumbsListStyles = euiBreadcrumbsListStyles(euiTheme); + const breadcrumbsListStyles = useEuiMemoizedStyles(euiBreadcrumbsListStyles); const cssBreadcrumbsListStyles = [ breadcrumbsListStyles.euiBreadcrumbs__list, truncate && breadcrumbsListStyles.isTruncated, diff --git a/src/components/breadcrumbs/index.ts b/src/components/breadcrumbs/index.ts index 34fd3ae44dd..a90c66c7d51 100644 --- a/src/components/breadcrumbs/index.ts +++ b/src/components/breadcrumbs/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -export type { EuiBreadcrumbProps as EuiBreadcrumb } from './breadcrumb'; export type { + EuiBreadcrumbProps as EuiBreadcrumb, EuiBreadcrumbsProps, EuiBreadcrumbResponsiveMaxCount, -} from './breadcrumbs'; +} from './types'; export { EuiBreadcrumbs } from './breadcrumbs'; diff --git a/src/components/breadcrumbs/types.ts b/src/components/breadcrumbs/types.ts new file mode 100644 index 00000000000..21fd76f9a7e --- /dev/null +++ b/src/components/breadcrumbs/types.ts @@ -0,0 +1,126 @@ +/* + * 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 type { + ReactNode, + HTMLAttributes, + AriaAttributes, + MouseEventHandler, +} from 'react'; +import type { EuiBreakpointSize } from '../../services'; +import type { CommonProps } from '../common'; +import type { EuiLinkColor } from '../link'; +import type { EuiPopoverProps } from '../popover'; + +/** + * Consumer facing type exports + */ + +export type EuiBreadcrumbResponsiveMaxCount = { + /** + * Any of the following keys are allowed: `'xs' | 's' | 'm' | 'l' | 'xl'` + * Omitting a key will display all breadcrumbs at that breakpoint + */ + [key in EuiBreakpointSize]?: number; +}; + +export type EuiBreadcrumbsProps = CommonProps & { + /** + * Hides extra (above the max) breadcrumbs under a collapsed item as the window gets smaller. + * Pass a custom #EuiBreadcrumbResponsiveMaxCount object to change the number of breadcrumbs to show at the particular breakpoints. + * + * Pass `false` to turn this behavior off. + * + * Default: `{ xs: 1, s: 2, m: 4 }` + */ + responsive?: boolean | EuiBreadcrumbResponsiveMaxCount; + + /** + * Forces all breadcrumbs to single line and + * truncates each breadcrumb to a particular width, + * except for the last item + */ + truncate?: boolean; + + /** + * Collapses the inner items past the maximum set here + * into a single ellipses item. + * Omitting or passing a `0` value will show all breadcrumbs. + */ + max?: number | null; + + /** + * The array of individual #EuiBreadcrumb items + */ + breadcrumbs: EuiBreadcrumbProps[]; + + /** + * Determines breadcrumbs appearance, with `page` being the default styling. + * Application breadcrumbs should only be once per page, in (e.g.) EuiHeader + */ + type?: 'page' | 'application'; + + /** + * Whether the last breadcrumb should visually (and accessibly, to screen readers) + * be highlighted as the current page. Defaults to true. + */ + lastBreadcrumbIsCurrentPage?: boolean; +}; + +export type EuiBreadcrumbProps = Omit< + HTMLAttributes, + 'color' | 'aria-current' +> & + CommonProps & { + href?: string; + rel?: string; + onClick?: MouseEventHandler; + /** + * Visible label of the breadcrumb + */ + text: ReactNode; + /** + * Force a max-width on the breadcrumb text + */ + truncate?: boolean; + /** + * Accepts any EuiLink `color` when rendered as one (has `href`, `onClick`, or `popoverContent`) + */ + color?: EuiLinkColor; + /** + * Override the existing `aria-current` which defaults to `page` for the last breadcrumb + */ + 'aria-current'?: AriaAttributes['aria-current']; + /** + * Creates a breadcrumb that toggles a popover dialog. Takes any rendered node(s), + * or a render function that will pass callback allowing you to close the + * breadcrumb popover from within your popover content. + * + * If passed, both `href` and `onClick` will be ignored - the breadcrumb's + * click behavior should only trigger a popover. + */ + popoverContent?: ReactNode | ((closePopover: () => void) => ReactNode); + /** + * Allows customizing the popover if necessary. Accepts any props that + * [EuiPopover](/#/layout/popover) accepts, except for props that control state. + */ + popoverProps?: Omit; + }; + +/** + * Internal props set by parent EuiBreadcrumbs only + */ + +export type _EuiBreadcrumbProps = { + type: NonNullable; + isFirstBreadcrumb?: boolean; + isLastBreadcrumb?: boolean; + isOnlyBreadcrumb?: boolean; + highlightLastBreadcrumb?: boolean; + truncateLastBreadcrumb?: boolean; +}; diff --git a/src/components/header/__snapshots__/header.test.tsx.snap b/src/components/header/__snapshots__/header.test.tsx.snap index 222aeff6d1c..8c4953f4658 100644 --- a/src/components/header/__snapshots__/header.test.tsx.snap +++ b/src/components/header/__snapshots__/header.test.tsx.snap @@ -53,7 +53,7 @@ exports[`EuiHeader sections render breadcrumbs and props 1`] = ` > Breadcrumb diff --git a/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap index 593e59847fe..d36a73a77d6 100644 --- a/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap +++ b/src/components/header/header_breadcrumbs/__snapshots__/header_breadcrumbs.test.tsx.snap @@ -14,7 +14,7 @@ exports[`EuiHeaderBreadcrumbs is rendered 1`] = ` data-test-subj="euiBreadcrumb" > Edit @@ -79,7 +79,7 @@ exports[`EuiHeaderBreadcrumbs renders only one breadcrumb with all rounded corne > Home diff --git a/src/components/header/header_breadcrumbs/header_breadcrumbs.tsx b/src/components/header/header_breadcrumbs/header_breadcrumbs.tsx index d11b6efcb38..8cf78fcf6e3 100644 --- a/src/components/header/header_breadcrumbs/header_breadcrumbs.tsx +++ b/src/components/header/header_breadcrumbs/header_breadcrumbs.tsx @@ -9,9 +9,9 @@ import React, { FunctionComponent } from 'react'; import classNames from 'classnames'; +import { useEuiMemoizedStyles } from '../../../services'; import { EuiBreadcrumbs, EuiBreadcrumbsProps } from '../../breadcrumbs'; import { euiHeaderBreadcrumbsStyles } from './header_breadcrumbs.styles'; -import { useEuiTheme } from '../../../services'; export const EuiHeaderBreadcrumbs: FunctionComponent = ({ className, @@ -20,9 +20,7 @@ export const EuiHeaderBreadcrumbs: FunctionComponent = ({ }) => { const classes = classNames('euiHeaderBreadcrumbs', className); - const euiTheme = useEuiTheme(); - const styles = euiHeaderBreadcrumbsStyles(euiTheme); - const cssHeaderBreadcrumbStyles = [styles.euiHeaderBreadcrumbs]; + const styles = useEuiMemoizedStyles(euiHeaderBreadcrumbsStyles); return ( = ({ truncate breadcrumbs={breadcrumbs} className={classes} - css={cssHeaderBreadcrumbStyles} + css={styles.euiHeaderBreadcrumbs} type="application" {...rest} />