diff --git a/src-docs/src/views/breadcrumbs/breadcrumbs_example.js b/src-docs/src/views/breadcrumbs/breadcrumbs_example.js index 1c037a0c176..1f0c72bfee3 100644 --- a/src-docs/src/views/breadcrumbs/breadcrumbs_example.js +++ b/src-docs/src/views/breadcrumbs/breadcrumbs_example.js @@ -3,7 +3,12 @@ import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; -import { EuiBreadcrumbs, EuiCode, EuiText } from '../../../../src/components'; +import { + EuiBreadcrumbs, + EuiCode, + EuiText, + EuiCallOut, +} from '../../../../src/components'; import { BreadcrumbProps, BreadcrumbResponsiveMaxCount } from './props'; import { breadcrumbsConfig } from './playground'; @@ -22,12 +27,14 @@ import TruncateSingle from './truncate_single'; const truncateSingleSource = require('!!raw-loader!./truncate_single'); import Max from './max'; -import { EuiCallOut } from '../../../../src/components/call_out'; const maxSource = require('!!raw-loader!./max'); import Color from './color'; const colorSource = require('!!raw-loader!./color'); +import PopoverContent from './popover_content'; +const popoverContentSource = require('!!raw-loader!./popover_content'); + const props = { EuiBreadcrumbs, EuiBreadcrumb: BreadcrumbProps, @@ -244,6 +251,75 @@ export const BreadcrumbsExample = { ], demo: , }, + { + title: 'Popover content', + text: ( + <> +

+ If you want a breadcrumb that toggles a popover, e.g. for an account + switcher, you can use the popoverContent prop for + this purpose. EuiBreadcrumbs will automatically + handle rendering a popover indicator and popover accessibility best + practies for you. We recommend using components such as{' '} + + EuiContextMenu + {' '} + or{' '} + + EuiListGroup + {' '} + for displaying popover options, or potentially{' '} + + EuiSelectable + {' '} + if you have many items that require filtering. +

+

+ You may also pass popoverProps with almost any + prop that{' '} + + EuiPopover + {' '} + accepts, such as customizing panelPaddingSize or{' '} + anchorPosition. However, props that affect + popover state such as closePopover,{' '} + isOpen, and button are not + accepted as they are controlled automatically by{' '} + EuiBreadcrumbs. +

+ + Please note that creating a breadcrumb with a popover will + nullify any passed href or{' '} + onClick behavior, as the only{' '} + interaction the breadcrumb should have at that point is the + popover toggle. + + } + > + + ), + props, + demo: , + snippet: `, + popoverProps: { panelPaddingSize: 's' }, + } + ]} +/>`, + source: [ + { + type: GuideSectionTypes.TSX, + code: popoverContentSource, + }, + ], + }, { title: 'Color for emphasis', text: ( diff --git a/src-docs/src/views/breadcrumbs/popover_content.tsx b/src-docs/src/views/breadcrumbs/popover_content.tsx new file mode 100644 index 00000000000..e148ec85066 --- /dev/null +++ b/src-docs/src/views/breadcrumbs/popover_content.tsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; + +import { + EuiBreadcrumbs, + EuiBreadcrumb, + EuiPopoverTitle, + EuiPopoverFooter, + EuiContextMenuPanel, + EuiSelectable, + EuiSelectableOption, + EuiAvatar, + EuiButton, + EuiContextMenuItem, +} from '../../../../src'; + +export default () => { + const [spaces, setSpaces] = useState([ + { + label: 'My space', + checked: 'on', + prepend: , + }, + { + label: "Jim's space", + prepend: , + }, + { + label: "Pam's space", + prepend: , + }, + { + label: "Michael's space", + prepend: , + }, + { + label: "Dwight's space", + prepend: , + }, + ]); + + const breadcrumbs: EuiBreadcrumb[] = [ + { + text: 'My deployment', + popoverContent: ( + <> + Select a deployment + e.preventDefault()} + > + Go to Deployment A + , + e.preventDefault()} + > + Go to Deployment B + , + e.preventDefault()} + > + Go to all deployments + , + ]} + /> + + ), + popoverProps: { panelPaddingSize: 'none' }, + }, + { + text: 'My space', + popoverContent: ( + setSpaces(newOptions)} + searchable + searchProps={{ placeholder: 'Filter spaces', compressed: true }} + aria-label="Space switcher" + emptyMessage="No spaces available" + noMatchesMessage="No spaces found" + > + {(list, search) => ( + <> + Select a space + {search} + {list} + + + Manage all spaces + + + + )} + + ), + popoverProps: { panelPaddingSize: 'none' }, + }, + { + text: 'Home', + }, + ]; + + return ; +}; diff --git a/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap new file mode 100644 index 00000000000..f8c63f18a81 --- /dev/null +++ b/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiBreadcrumbContent renders breadcrumbs with \`popoverContent\` with popovers 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__/breadcrumbs.test.tsx.snap b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap index 72b3380d75d..fce998e8fd9 100644 --- a/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap +++ b/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap @@ -45,15 +45,21 @@ exports[`EuiBreadcrumbs is rendered 1`] = ` class="euiPopover__anchor css-16vtueo-render" >
@@ -144,15 +150,21 @@ exports[`EuiBreadcrumbs is rendered with final item as link 1`] = ` class="euiPopover__anchor css-16vtueo-render" >
@@ -325,15 +337,21 @@ exports[`EuiBreadcrumbs props max renders 1 item 1`] = ` class="euiPopover__anchor css-16vtueo-render" > @@ -505,15 +523,21 @@ exports[`EuiBreadcrumbs props responsive is rendered 1`] = ` class="euiPopover__anchor css-16vtueo-render" > @@ -603,15 +627,21 @@ exports[`EuiBreadcrumbs props responsive is rendered as false 1`] = ` class="euiPopover__anchor css-16vtueo-render" > @@ -676,15 +706,21 @@ exports[`EuiBreadcrumbs props responsive is rendered with custom breakpoints 1`] class="euiPopover__anchor css-16vtueo-render" > diff --git a/src/components/breadcrumbs/breadcrumb.test.tsx b/src/components/breadcrumbs/breadcrumb.test.tsx new file mode 100644 index 00000000000..a5e2d4607fe --- /dev/null +++ b/src/components/breadcrumbs/breadcrumb.test.tsx @@ -0,0 +1,99 @@ +/* + * 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 { fireEvent } from '@testing-library/react'; +import { render, waitForEuiPopoverOpen } from '../../test/rtl'; + +import { EuiBreadcrumbContent } from './breadcrumb'; + +describe('EuiBreadcrumbContent', () => { + it('renders plain uninteractive breadcrumb text', () => { + const { container, getByText } = render( + <> + + + ); + expect(getByText('Text').nodeName).toEqual('SPAN'); + expect(container).toMatchSnapshot(); + }); + + it('renders interactive breadcrumbs with href or onClick', () => { + const { container, getByText } = render( + <> + + {}} /> + + ); + expect(getByText('Link').nodeName).toEqual('A'); + expect(getByText('Button').nodeName).toEqual('BUTTON'); + expect(container).toMatchSnapshot(); + }); + + it('renders breadcrumbs with `popoverContent` with popovers', async () => { + const { baseElement, getByTestSubject } = render( + + ); + fireEvent.click(getByTestSubject('popoverToggle')); + await waitForEuiPopoverOpen(); + + expect(getByTestSubject('popover')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + 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.tsx b/src/components/breadcrumbs/breadcrumb.tsx index c54172e336b..2d5d927c4e5 100644 --- a/src/components/breadcrumbs/breadcrumb.tsx +++ b/src/components/breadcrumbs/breadcrumb.tsx @@ -22,7 +22,7 @@ import { CommonProps } from '../common'; import { EuiInnerText } from '../inner_text'; import { EuiTextColor } from '../text'; import { EuiLink, EuiLinkColor } from '../link'; -import { EuiPopover } from '../popover'; +import { EuiPopover, EuiPopoverProps } from '../popover'; import { EuiIcon } from '../icon'; import { useEuiI18n } from '../i18n'; @@ -48,13 +48,25 @@ export type EuiBreadcrumbProps = Omit< */ truncate?: boolean; /** - * Accepts any EuiLink `color` when rendered as one (has `href` or `onClick`) + * 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 + * + * If passed, both `href` and `onClick` will be ignored - the breadcrumb's + * click behavior should only trigger a popover. + */ + popoverContent?: 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 @@ -102,6 +114,8 @@ export const EuiBreadcrumbContent: FunctionComponent< href, rel, // required by our local href-with-rel eslint rule onClick, + popoverContent, + popoverProps, className, color, isFirstBreadcrumb, @@ -131,45 +145,79 @@ export const EuiBreadcrumbContent: FunctionComponent< } } - const ariaCurrent = highlightLastBreadcrumb ? 'page' : undefined; + 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; - return !href && !onClick ? ( - - setIsPopoverOpen(false)} + button={ + setIsPopoverOpen((isOpen) => !isOpen)} + {...rest} + > + {text}{' '} + + + } + > + {popoverContent} + + ); + } else if (isInteractiveBreadcrumb) { + return ( + {text} - - - ) : ( - - {text} - - ); + + ); + } else { + return ( + + + {text} + + + ); + } }} ); @@ -180,8 +228,6 @@ export const EuiBreadcrumbCollapsed: FunctionComponent<_EuiBreadcrumbProps> = ({ isFirstBreadcrumb, type, }) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const euiTheme = useEuiTheme(); const styles = euiBreadcrumbStyles(euiTheme); const cssStyles = [styles.isCollapsed]; @@ -191,31 +237,16 @@ export const EuiBreadcrumbCollapsed: FunctionComponent<_EuiBreadcrumbProps> = ({ 'See collapsed breadcrumbs' ); - const ellipsisButton = ( - setIsPopoverOpen(!isPopoverOpen)} - truncate={false} - text={ - <> - … - - } - isFirstBreadcrumb={isFirstBreadcrumb} - type={type} - /> - ); - return ( - setIsPopoverOpen(false)} - > - {children} - + …} + title={ariaLabel} + truncate={false} + isFirstBreadcrumb={isFirstBreadcrumb} + type={type} + /> ); }; diff --git a/upcoming_changelogs/7031.md b/upcoming_changelogs/7031.md new file mode 100644 index 00000000000..b0ff6ee2dcf --- /dev/null +++ b/upcoming_changelogs/7031.md @@ -0,0 +1 @@ +- Updated `EuiBreadcrumbs` to support breadcrumbs that toggle popovers via `popoverContent` and `popoverProps`