diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx index b8cc8bfd3b8f..9bd891677495 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/__jest__/build_nav_tree.test.tsx @@ -260,6 +260,7 @@ describe('builds navigation tree', () => { href: undefined, href_prev: undefined, id: 'item1', + path: 'group1.item1', }); }); diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/analytics/event_tracker.ts b/src/platform/packages/shared/shared-ux/chrome/navigation/src/analytics/event_tracker.ts index f24ebd7a2f67..0fef1488924a 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/analytics/event_tracker.ts +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/analytics/event_tracker.ts @@ -9,12 +9,20 @@ import { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +interface ClickNavLinkEvent { + id: string; + path: string; + href?: string; + hrefPrev?: string; +} + export enum EventType { CLICK_NAVLINK = 'solutionNav_click_navlink', } export enum FieldType { ID = 'id', + PATH = 'path', HREF = 'href', HREF_PREV = 'href_prev', } @@ -34,9 +42,10 @@ export class EventTracker { /* * Track whenever a user clicks on a navigation link in the side nav */ - public clickNavLink({ id, href, hrefPrev }: { id: string; href?: string; hrefPrev?: string }) { + public clickNavLink({ id, path, href, hrefPrev }: ClickNavLinkEvent): void { this.track(EventType.CLICK_NAVLINK, { [FieldType.ID]: id, + [FieldType.PATH]: path, [FieldType.HREF]: href, [FieldType.HREF_PREV]: hrefPrev, }); diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/navigation_item_open_panel.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/navigation_item_open_panel.tsx index e01554b82aa5..96f290a679a3 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/navigation_item_open_panel.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/navigation_item_open_panel.tsx @@ -93,7 +93,7 @@ export const NavigationItemOpenPanel: FC = ({ item, activeNodes }: Props) [selectedNode?.id, item, closePanel, openPanel] ); - const onLinkClick = useCallback( + const onTogglePanelClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); togglePanel(e.target); @@ -103,7 +103,9 @@ export const NavigationItemOpenPanel: FC = ({ item, activeNodes }: Props) return ( ({ - paddingBlock: euiTheme.size.xs, - paddingInline: euiTheme.size.s, - }), - euiCollapsibleNavSection: ({ euiTheme }: Theme) => css` - & > .euiCollapsibleNavLink { - /* solution title in primary nav */ - font-weight: ${euiTheme.font.weight.bold}; - margin: ${euiTheme.size.s} 0; - margin-bottom: calc(${euiTheme.size.xs} * 1.5); - } - - .euiCollapsibleNavAccordion { - &.euiAccordion__triggerWrapper, - &.euiCollapsibleNavLink { - &:focus-within { - background: ${euiTheme.colors.backgroundBasePlain}; - } - - &:hover { - background: ${euiTheme.colors.backgroundBaseInteractiveHover}; - } - } - - &.isSelected { - .euiAccordion__triggerWrapper, - .euiCollapsibleNavLink { - background-color: ${euiTheme.colors.backgroundLightPrimary}; - - * { - color: ${euiTheme.colors.textPrimary}; - } - } - } - } - - .euiAccordion__children .euiCollapsibleNavItem__items { - padding-inline-start: ${euiTheme.size.m}; - margin-inline-start: ${euiTheme.size.m}; - } - - &:only-child .euiCollapsibleNavItem__icon { - transform: scale(1.33); - } - `, - euiCollapsibleNavSubItem: ({ euiTheme }: Theme) => css` - .euiAccordion__button:focus, - .euiAccordion__button:hover { - text-decoration: none; - } - - &.euiLink, - &.euiCollapsibleNavLink { - &:focus, - &:hover { - background-color: ${euiTheme.colors.backgroundBaseInteractiveHover}; - text-decoration: none; - } - - &.isSelected { - background-color: ${euiTheme.colors.backgroundLightPrimary}; - &:focus, - &:hover { - background-color: ${euiTheme.colors.backgroundLightPrimary}; - } - - * { - color: ${euiTheme.colors.textPrimary}; - } - } - } - `, - euiAccordionChildWrapper: ({ euiTheme }: Theme) => css` - .euiAccordion__childWrapper { - background-color: ${euiTheme.colors.backgroundBasePlain}; - transition: none; // Remove the transition as it does not play well with dynamic links added to the accordion - } - `, -}; +import { sectionStyles } from './styles'; const nodeHasLink = (navNode: ChromeProjectNavigationNode) => Boolean(navNode.deepLink) || Boolean(navNode.href); @@ -368,25 +287,17 @@ const getEuiProps = ( }) .flat(); - /** - * Check if the click event is a special click (e.g. right click, click with modifier key) - * We do not want to prevent the default behavior in these cases. - */ - const isSpecialClick = (e: React.MouseEvent) => { - const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); - const isLeftClickEvent = e.button === 0; - return isModifiedEvent || !isLeftClickEvent; - }; - const linkProps: EuiCollapsibleNavItemProps['linkProps'] | undefined = hasLink ? { href, external: isExternal, + 'aria-label': navNode.title, onClick: (e) => { if (href) { eventTracker.clickNavLink({ - href: basePath.remove(href), id: navNode.id, + path: navNode.path, + href: basePath.remove(href), hrefPrev: basePath.remove(window.location.pathname), }); } @@ -411,8 +322,9 @@ const getEuiProps = ( const onClick = (e: React.MouseEvent) => { if (href) { eventTracker.clickNavLink({ - href: basePath.remove(href), id: navNode.id, + path: navNode.path, + href: basePath.remove(href), hrefPrev: basePath.remove(window.location.pathname), }); } diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/styles.ts b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/styles.ts new file mode 100644 index 000000000000..818376334176 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/navigation_section/styles.ts @@ -0,0 +1,91 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { Theme, css } from '@emotion/react'; + +export const sectionStyles = { + blockTitle: ({ euiTheme }: Theme) => ({ + paddingBlock: euiTheme.size.xs, + paddingInline: euiTheme.size.s, + }), + euiCollapsibleNavSection: ({ euiTheme }: Theme) => css` + & > .euiCollapsibleNavLink { + /* solution title in primary nav */ + font-weight: ${euiTheme.font.weight.bold}; + margin: ${euiTheme.size.s} 0; + margin-bottom: calc(${euiTheme.size.xs} * 1.5); + } + + .euiCollapsibleNavAccordion { + &.euiAccordion__triggerWrapper, + &.euiCollapsibleNavLink { + &:focus-within { + background: ${euiTheme.colors.backgroundBasePlain}; + } + + &:hover { + background: ${euiTheme.colors.backgroundBaseInteractiveHover}; + } + } + + &.isSelected { + .euiAccordion__triggerWrapper, + .euiCollapsibleNavLink { + background-color: ${euiTheme.colors.backgroundLightPrimary}; + + * { + color: ${euiTheme.colors.textPrimary}; + } + } + } + } + + .euiAccordion__children .euiCollapsibleNavItem__items { + padding-inline-start: ${euiTheme.size.m}; + margin-inline-start: ${euiTheme.size.m}; + } + + &:only-child .euiCollapsibleNavItem__icon { + transform: scale(1.33); + } + `, + euiCollapsibleNavSubItem: ({ euiTheme }: Theme) => css` + .euiAccordion__button:focus, + .euiAccordion__button:hover { + text-decoration: none; + } + + &.euiLink, + &.euiCollapsibleNavLink { + &:focus, + &:hover { + background-color: ${euiTheme.colors.backgroundBaseInteractiveHover}; + text-decoration: none; + } + + &.isSelected { + background-color: ${euiTheme.colors.backgroundLightPrimary}; + &:focus, + &:hover { + background-color: ${euiTheme.colors.backgroundLightPrimary}; + } + + * { + color: ${euiTheme.colors.textPrimary}; + } + } + } + `, + euiAccordionChildWrapper: ({ euiTheme }: Theme) => css` + .euiAccordion__childWrapper { + background-color: ${euiTheme.colors.backgroundBasePlain}; + transition: none; // Remove the transition as it does not play well with dynamic links added to the accordion + } + `, +}; diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx index ccaf9ac78d30..fb4394930498 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx @@ -19,7 +19,7 @@ import React, { } from 'react'; import type { PanelSelectedNode } from '@kbn/core-chrome-browser'; -import { DefaultContent } from './default_content'; +import { Panel } from './panel'; export interface PanelContext { isOpen: boolean; @@ -79,7 +79,7 @@ export const PanelProvider: FC> = ({ return null; } - return ; + return ; }, [selectedNode]); const ctx: PanelContext = useMemo( diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel.tsx similarity index 95% rename from src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx rename to src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel.tsx index 2a8e8cda2c76..c2f7539e3897 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel.tsx @@ -84,7 +84,7 @@ interface Props { selectedNode: PanelSelectedNode; } -export const DefaultContent: FC = ({ selectedNode }) => { +export const Panel: FC = ({ selectedNode }) => { const filteredChildren = selectedNode.children?.filter( (child) => child.sideNavStatus !== 'hidden' ); @@ -121,7 +121,7 @@ export const DefaultContent: FC = ({ selectedNode }) => { {typeof selectedNode.title === 'string' ? ( -

{selectedNode.title}

+

{selectedNode.title}

) : ( selectedNode.title @@ -135,7 +135,7 @@ export const DefaultContent: FC = ({ selectedNode }) => { return isGroup ? ( - + {i < serializedChildren.length - 1 && } ) : ( diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_group.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_group.tsx index 23374ed1b141..c08caa570c8b 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_group.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_group.tsx @@ -42,7 +42,7 @@ const styles = { ${euiFontSize({ euiTheme } as UseEuiTheme<{}>, 'xs')} } `, - listGroup: ({ euiTheme }: Theme) => css` + listGroup: () => css` padding-left: 0; padding-right: 0; gap: 0; @@ -76,10 +76,11 @@ const someChildIsVisible = (children: ChromeProjectNavigationNode[]) => { interface Props { navNode: ChromeProjectNavigationNode; + parentId: string; nodeIndex: number; } -export const PanelGroup: FC = ({ navNode, nodeIndex }) => { +export const PanelGroup: FC = ({ navNode, parentId, nodeIndex }) => { const { id, title, spaceBefore: _spaceBefore, withBadge } = navNode; const filteredChildren = navNode.children?.filter((child) => child.sideNavStatus !== 'hidden'); const hasTitle = !!title && title !== ''; @@ -97,18 +98,21 @@ export const PanelGroup: FC = ({ navNode, nodeIndex }) => { } } - const renderChildren = useCallback(() => { - if (!filteredChildren) return null; + const renderChildren = useCallback( + (parentNode: ChromeProjectNavigationNode) => { + if (!filteredChildren) return null; - return filteredChildren.map((item, i) => { - const isItem = item.renderAs === 'item' || !item.children; - return isItem ? ( - - ) : ( - - ); - }); - }, [filteredChildren]); + return filteredChildren.map((item, i) => { + const isItem = item.renderAs === 'item' || !item.children; + return isItem ? ( + + ) : ( + + ); + }); + }, + [filteredChildren] + ); if (!filteredChildren?.length || !someChildIsVisible(filteredChildren)) { return null; @@ -130,7 +134,7 @@ export const PanelGroup: FC = ({ navNode, nodeIndex }) => { 'data-test-subj': `panelAccordionBtnId-${navNode.id}`, }} > - {renderChildren()} + {renderChildren(navNode)} ); @@ -140,13 +144,23 @@ export const PanelGroup: FC = ({ navNode, nodeIndex }) => {
{spaceBefore != null && } {hasTitle && ( - +

{title}

)}
- - {renderChildren()} + + {renderChildren(navNode)}
diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx index 97779b82f174..ab9d4fb33b42 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/panel/panel_nav_item.tsx @@ -7,13 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { FC, useCallback } from 'react'; -import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; -import { EuiListGroupItem } from '@elastic/eui'; import { Theme, css } from '@emotion/react'; +import React, { FC, useCallback } from 'react'; + +import { EuiListGroupItem } from '@elastic/eui'; +import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; -import { useNavigation } from '../../navigation'; import { useNavigation as useServices } from '../../../services'; +import { isSpecialClick } from '../../../utils'; +import { useNavigation } from '../../navigation'; import { SubItemTitle } from '../subitem_title'; import { usePanel } from './context'; @@ -55,22 +57,35 @@ const panelNavStyles = ({ euiTheme }: Theme) => css` `; export const PanelNavItem: FC = ({ item }) => { - const { navigateToUrl } = useServices(); + const { navigateToUrl, eventTracker, basePath } = useServices(); const { activeNodes } = useNavigation(); const { close: closePanel } = usePanel(); - const { id, icon, deepLink, openInNewTab, isExternalLink, renderItem } = item; + const { id, path, icon, deepLink, openInNewTab, isExternalLink, renderItem } = item; const href = deepLink?.url ?? item.href; - const onClick = useCallback( + const onClick = useCallback>( (e) => { - if (!!href) { + if (href) { + eventTracker.clickNavLink({ + id, + path, + href: basePath.remove(href), + hrefPrev: basePath.remove(window.location.pathname), + }); + } + + if (isSpecialClick(e)) { + return; + } + + if (href) { e.preventDefault(); navigateToUrl(href); closePanel(); } }, - [closePanel, href, navigateToUrl] + [closePanel, id, path, href, navigateToUrl, basePath, eventTracker] ); if (renderItem) { @@ -85,6 +100,7 @@ export const PanelNavItem: FC = ({ item }) => { } + aria-label={item.title} wrapText size="s" css={panelNavStyles} diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx index aef5b4f95d04..48f74291182e 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/ui/components/recently_accessed.tsx @@ -16,6 +16,7 @@ import type { Observable } from 'rxjs'; import { useNavigation as useServices } from '../../services'; import { getI18nStrings } from '../i18n_strings'; +import { isSpecialClick } from '../../utils'; const MAX_RECENTLY_ACCESS_ITEMS = 5; @@ -51,7 +52,11 @@ export const RecentlyAccessed: FC = ({ title: label, href, 'data-test-subj': `nav-recentlyAccessed-item nav-recentlyAccessed-item-${id}`, - onClick: (e: React.MouseEvent) => { + onClick: (e: React.MouseEvent) => { + if (isSpecialClick(e)) { + return; + } + e.preventDefault(); navigateToUrl(href); }, diff --git a/src/platform/packages/shared/shared-ux/chrome/navigation/src/utils.ts b/src/platform/packages/shared/shared-ux/chrome/navigation/src/utils.ts index 5dd125a267bf..04123f0e5fd6 100644 --- a/src/platform/packages/shared/shared-ux/chrome/navigation/src/utils.ts +++ b/src/platform/packages/shared/shared-ux/chrome/navigation/src/utils.ts @@ -46,3 +46,13 @@ export const isAccordionNode = ( ) => node.renderAs === 'accordion' || ['defaultIsCollapsed', 'isCollapsible'].some((prop) => Object.hasOwn(node, prop)); + +/** + * Can check if the click event is a special click (e.g. right click, click with modifier key) + * Allows us to not prevent the default behavior in these cases. + */ +export const isSpecialClick = (e: React.MouseEvent) => { + const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); + const isLeftClickEvent = e.button === 0; + return isModifiedEvent || !isLeftClickEvent; +}; diff --git a/src/platform/plugins/shared/navigation/public/analytics/register_event_types.ts b/src/platform/plugins/shared/navigation/public/analytics/register_event_types.ts index 85868b33c34b..7f579c91958b 100644 --- a/src/platform/plugins/shared/navigation/public/analytics/register_event_types.ts +++ b/src/platform/plugins/shared/navigation/public/analytics/register_event_types.ts @@ -22,6 +22,14 @@ const fields: Record>> = }, }, }, + [NavigationFieldType.PATH]: { + [NavigationFieldType.PATH]: { + type: 'keyword', + _meta: { + description: 'The path of the navigation node within the tree.', + }, + }, + }, [NavigationFieldType.HREF]: { [NavigationFieldType.HREF]: { type: 'keyword',