mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Solution Nav] Functionality fixes (#220580)
## Summary Epic: https://github.com/elastic/kibana-team/issues/1439 Closes https://github.com/elastic/kibana/issues/210221 Closes https://github.com/elastic/kibana/issues/219679 Changes 1. a11y fixes - `aria-label` to nav item links - `aria-expanded` for panel opener items - `aria-labelledby` for list elements 1. Add telemetry event tracking links in secondary panel 1. Fix cmd+click for recents and links in secondary panel 1. (maintenance) Rename `DefaultContent` => `Panel` 1. (maintenance) Move `navigation_section_ui` Emotion styles to separate file
This commit is contained in:
parent
904d419c9c
commit
2fbdf74164
12 changed files with 198 additions and 130 deletions
|
@ -260,6 +260,7 @@ describe('builds navigation tree', () => {
|
|||
href: undefined,
|
||||
href_prev: undefined,
|
||||
id: 'item1',
|
||||
path: 'group1.item1',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ export const NavigationItemOpenPanel: FC<Props> = ({ 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<Props> = ({ item, activeNodes }: Props)
|
|||
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={onLinkClick}
|
||||
aria-label={title}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={onTogglePanelClick}
|
||||
iconSide="right"
|
||||
iconSize="s"
|
||||
iconType="arrowRight"
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { type FC, useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { Theme, css } from '@emotion/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiCollapsibleNavItem,
|
||||
|
@ -23,7 +22,7 @@ import type { EuiThemeSize, RenderAs } from '@kbn/core-chrome-browser/src/projec
|
|||
|
||||
import { SubItemTitle } from '../subitem_title';
|
||||
import { useNavigation as useServices } from '../../../services';
|
||||
import { isAbsoluteLink, isActiveFromUrl, isAccordionNode } from '../../../utils';
|
||||
import { isAbsoluteLink, isActiveFromUrl, isAccordionNode, isSpecialClick } from '../../../utils';
|
||||
import type { BasePathService, NavigateToUrlFn } from '../../../types';
|
||||
import { useNavigation } from '../../navigation';
|
||||
import { EventTracker } from '../../../analytics';
|
||||
|
@ -36,87 +35,7 @@ import {
|
|||
import type { EuiCollapsibleNavSubItemPropsEnhanced } from '../../types';
|
||||
import { PanelContext, usePanel } from '../panel';
|
||||
import { NavigationItemOpenPanel } from './navigation_item_open_panel';
|
||||
|
||||
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
|
||||
}
|
||||
`,
|
||||
};
|
||||
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<HTMLElement | HTMLButtonElement>) => {
|
||||
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<HTMLElement | HTMLButtonElement>) => {
|
||||
if (href) {
|
||||
eventTracker.clickNavLink({
|
||||
href: basePath.remove(href),
|
||||
id: navNode.id,
|
||||
path: navNode.path,
|
||||
href: basePath.remove(href),
|
||||
hrefPrev: basePath.remove(window.location.pathname),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
`,
|
||||
};
|
|
@ -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<PropsWithChildren<Props>> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
return <DefaultContent selectedNode={selectedNode} />;
|
||||
return <Panel selectedNode={selectedNode} />;
|
||||
}, [selectedNode]);
|
||||
|
||||
const ctx: PanelContext = useMemo(
|
||||
|
|
|
@ -84,7 +84,7 @@ interface Props {
|
|||
selectedNode: PanelSelectedNode;
|
||||
}
|
||||
|
||||
export const DefaultContent: FC<Props> = ({ selectedNode }) => {
|
||||
export const Panel: FC<Props> = ({ selectedNode }) => {
|
||||
const filteredChildren = selectedNode.children?.filter(
|
||||
(child) => child.sideNavStatus !== 'hidden'
|
||||
);
|
||||
|
@ -121,7 +121,7 @@ export const DefaultContent: FC<Props> = ({ selectedNode }) => {
|
|||
<EuiFlexItem>
|
||||
{typeof selectedNode.title === 'string' ? (
|
||||
<EuiTitle size="xxs" css={panelContentStyles.title}>
|
||||
<h2>{selectedNode.title}</h2>
|
||||
<h2 id={`panelTitleId-${selectedNode.id}`}>{selectedNode.title}</h2>
|
||||
</EuiTitle>
|
||||
) : (
|
||||
selectedNode.title
|
||||
|
@ -135,7 +135,7 @@ export const DefaultContent: FC<Props> = ({ selectedNode }) => {
|
|||
|
||||
return isGroup ? (
|
||||
<Fragment key={child.id}>
|
||||
<PanelGroup navNode={child} nodeIndex={i} />
|
||||
<PanelGroup navNode={child} parentId={selectedNode.id} nodeIndex={i} />
|
||||
{i < serializedChildren.length - 1 && <EuiHorizontalRule margin="xs" />}
|
||||
</Fragment>
|
||||
) : (
|
|
@ -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<Props> = ({ navNode, nodeIndex }) => {
|
||||
export const PanelGroup: FC<Props> = ({ 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<Props> = ({ 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 ? (
|
||||
<PanelNavItem key={item.id} item={item} />
|
||||
) : (
|
||||
<PanelGroup navNode={item} key={item.id} nodeIndex={i} />
|
||||
);
|
||||
});
|
||||
}, [filteredChildren]);
|
||||
return filteredChildren.map((item, i) => {
|
||||
const isItem = item.renderAs === 'item' || !item.children;
|
||||
return isItem ? (
|
||||
<PanelNavItem key={item.id} item={item} />
|
||||
) : (
|
||||
<PanelGroup navNode={item} parentId={parentNode.id} key={item.id} nodeIndex={i} />
|
||||
);
|
||||
});
|
||||
},
|
||||
[filteredChildren]
|
||||
);
|
||||
|
||||
if (!filteredChildren?.length || !someChildIsVisible(filteredChildren)) {
|
||||
return null;
|
||||
|
@ -130,7 +134,7 @@ export const PanelGroup: FC<Props> = ({ navNode, nodeIndex }) => {
|
|||
'data-test-subj': `panelAccordionBtnId-${navNode.id}`,
|
||||
}}
|
||||
>
|
||||
{renderChildren()}
|
||||
{renderChildren(navNode)}
|
||||
</EuiAccordion>
|
||||
</>
|
||||
);
|
||||
|
@ -140,13 +144,23 @@ export const PanelGroup: FC<Props> = ({ navNode, nodeIndex }) => {
|
|||
<div data-test-subj={groupTestSubj} css={styles.panelGroup}>
|
||||
{spaceBefore != null && <EuiSpacer size={spaceBefore} />}
|
||||
{hasTitle && (
|
||||
<EuiTitle size="xs" css={styles.title} data-test-subj={`panelGroupTitleId-${navNode.id}`}>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
css={styles.title}
|
||||
id={`panelGroupTitleId-${navNode.id}`}
|
||||
data-test-subj={`panelGroupTitleId-${navNode.id}`}
|
||||
>
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
)}
|
||||
<div style={{ paddingTop: removePaddingTop ? 0 : undefined }}>
|
||||
<EuiListGroup css={[styles.listGroup, styles.listGroupItemButton]}>
|
||||
{renderChildren()}
|
||||
<EuiListGroup
|
||||
css={[styles.listGroup, styles.listGroupItemButton]}
|
||||
aria-labelledby={
|
||||
hasTitle ? `panelGroupTitleId-${navNode.id}` : `panelTitleId-${parentId}`
|
||||
}
|
||||
>
|
||||
{renderChildren(navNode)}
|
||||
</EuiListGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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<Props> = ({ 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<React.MouseEventHandler>(
|
||||
const onClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(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<Props> = ({ item }) => {
|
|||
<EuiListGroupItem
|
||||
key={id}
|
||||
label={<SubItemTitle item={item} />}
|
||||
aria-label={item.title}
|
||||
wrapText
|
||||
size="s"
|
||||
css={panelNavStyles}
|
||||
|
|
|
@ -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<Props> = ({
|
|||
title: label,
|
||||
href,
|
||||
'data-test-subj': `nav-recentlyAccessed-item nav-recentlyAccessed-item-${id}`,
|
||||
onClick: (e: React.MouseEvent) => {
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (isSpecialClick(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
navigateToUrl(href);
|
||||
},
|
||||
|
|
|
@ -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<HTMLElement | HTMLButtonElement>) => {
|
||||
const isModifiedEvent = !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
|
||||
const isLeftClickEvent = e.button === 0;
|
||||
return isModifiedEvent || !isLeftClickEvent;
|
||||
};
|
||||
|
|
|
@ -22,6 +22,14 @@ const fields: Record<NavigationFieldType, RootSchema<Record<string, unknown>>> =
|
|||
},
|
||||
},
|
||||
},
|
||||
[NavigationFieldType.PATH]: {
|
||||
[NavigationFieldType.PATH]: {
|
||||
type: 'keyword',
|
||||
_meta: {
|
||||
description: 'The path of the navigation node within the tree.',
|
||||
},
|
||||
},
|
||||
},
|
||||
[NavigationFieldType.HREF]: {
|
||||
[NavigationFieldType.HREF]: {
|
||||
type: 'keyword',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue