import React, { useRef } from 'react'; import styled, { css } from 'styled-components'; import type { ItemState, MenuItemProps } from '@redocly/theme/core/types'; import type { JSX, KeyboardEvent } from 'react'; import { useNestedMenu, useThemeHooks } from '@redocly/theme/core/hooks'; import { LaunchIcon } from '@redocly/theme/icons/LaunchIcon/LaunchIcon'; import { Link } from '@redocly/theme/components/Link/Link'; import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon'; import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronRightIcon'; import { HttpTag } from '@redocly/theme/components/Tags/HttpTag'; import { MenuItemType } from '@redocly/theme/core/constants'; import { getMenuItemType, getOperationColor } from '@redocly/theme/core/utils'; import { ArrowRightIcon } from '@redocly/theme/icons/ArrowRightIcon/ArrowRightIcon'; import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon'; import { Tag, ContentWrapper } from '@redocly/theme/components/Tag/Tag'; export function MenuItem(props: React.PropsWithChildren): JSX.Element { const { item, depth, className, onClick } = props; const { useTranslate, useTelemetry } = useThemeHooks(); const { translate } = useTranslate(); const type = getMenuItemType(item); const nestedMenuRef = useRef(null); const labelRef = useRef(null); const { isExpanded, canUnmount, style, handleExpand } = useNestedMenu({ ...props, type, labelRef, nestedMenuRef, }); const telemetry = useTelemetry(); const isDrilldown = type === MenuItemType.DrillDown; const isSeparator = type === MenuItemType.Separator; const isNested = type === MenuItemType.Group; const hasChevron = isNested && !isDrilldown; const hasHttpTag = !!item.httpVerb || type === MenuItemType.Operation; const handleOnClick = () => { telemetry.sendSidebarItemClickedMessage([ { object: 'sidebar_item', label: item.label, type: item.type === 'link' || item.type === 'group' ? item.type : undefined, }, ]); onClick?.(); if (isNested) { handleExpand(); } }; const handleExpandOnEnter = (event: KeyboardEvent) => { if (event.key === 'Enter') { handleOnClick(); } }; const chevron = hasChevron ? ( isExpanded ? ( ) : ( ) ) : null; const httpColor = getOperationColor({ isAdditionalOperation: item.isAdditionalOperation, deprecated: item.deprecated, httpVerb: item.httpVerb || '', }); const label = item.label && ( {hasChevron ? {chevron} : null} {item.badges ?.filter(({ position }) => position === 'before') .map(({ name, color, icon }) => ( } > {name} ))} {translate(item.labelTranslationKey, item.label)} {item.badges ?.filter(({ position }) => position !== 'before') .map(({ name, color, icon }) => ( } > {name} ))} {item.external ? : null} {item.sublabel ? ( {translate(item.subLabelTranslationKey, item.sublabel)} ) : null} {isDrilldown ? : null} {hasHttpTag ? ( {item.httpVerb === 'hook' ? 'event' : item.httpVerb} ) : null} ); return ( {item.link ? ( {label} ) : ( label )} {isNested ? ( {isExpanded || !canUnmount ? props.children : null} ) : null} {item.separatorLine ? ( ) : null} ); } /* for backward compatibility */ function isDirectColorValue(color?: string) { if (!color) return false; return color.startsWith('#') || color.startsWith('rgb') || color.startsWith('hsl'); } function generateClassName({ type, item, className, }: { type: MenuItemType; item: ItemState; className?: string; }) { const classNames = [className, `menu-item-type-${type}`]; if (type === MenuItemType.Separator) { classNames.push(`menu-item-type-${type}-${item.variant || 'primary'}`); } if (item.menuStyle === 'drilldown-header') { classNames.push(`menu-item-type-drilldown-header`); if (item.link) { classNames.push(`menu-item-type-drilldown-header-link`); } } return classNames .filter((className) => className) .join(' ') .trim(); } const ChevronWrapper = styled.div` flex-shrink: 0; `; const MenuItemWrapper = styled.div` display: flex; flex-direction: column; background-color: var(--menu-item-bg-color); font-family: var(--menu-item-font-family); font-size: var(--menu-item-font-size); font-weight: var(--menu-item-font-weight); line-height: var(--menu-item-line-height); color: var(--menu-item-text-color); .tag-http { align-self: flex-start; margin-left: auto; } &.menu-item-type-separator { pointer-events: none; } > a { text-decoration: none; color: var(--menu-item-text-color); } `; const MenuItemNestedWrapper = styled.div<{ isExpanded?: boolean; depth?: number; }>` order: 1; position: relative; &:hover:has(&:hover)::before { display: none; } &:hover::before { content: ''; position: absolute; bottom: var(--spacing-unit); top: 0; z-index: var(--z-index-surface); left: ${({ depth }) => `calc( var(--menu-item-label-margin-horizontal) + var(--menu-item-padding-horizontal) + (var(--menu-item-label-chevron-size) / 2 - 1px) + var(--menu-item-nested-offset) * ${depth}) `}; border: 0.5px solid var(--menu-item-border-color-hover); } `; const MenuItemLabelWrapper = styled.li<{ active?: boolean; depth?: number; withChevron?: boolean; deprecated?: boolean; isSeparator?: boolean; }>` display: flex; position: relative; cursor: pointer; order: 1; align-items: var(--menu-item-label-align-items); transition: var(--menu-item-label-transition); word-break: var(--menu-item-label-word-break); border-radius: var(--menu-item-label-border-radius); margin: var(--menu-item-label-margin); padding: var(--menu-item-label-padding); gap: var(--menu-item-label-gap); padding-left: ${({ withChevron, depth, isSeparator }) => `calc( var(--menu-item-padding-horizontal) + ${!withChevron ? 'var(--menu-item-label-chevron-offset) + ' + (isSeparator ? 'var(--menu-item-separator-offest)' : '0px') : '0px'} + ${depth ? 'var(--menu-item-nested-offset) * ' + depth : '0px'} )`}; &:hover { color: var(--menu-item-color-hover); background: var(--menu-item-bg-color-hover); ${ChevronDownIcon} path { fill: var(--menu-item-color-hover); } ${ChevronRightIcon} path { fill: var(--menu-item-color-hover); } } ${({ active, deprecated }) => active && css` color: ${deprecated ? 'var(--menu-content-color-disabled)' : 'var(--menu-item-color-active)'}; background-color: var(--menu-item-bg-color-active); font-weight: var(--menu-item-font-weight-active); ${ChevronDownIcon} path { fill: var(--menu-item-color-active); } ${ChevronRightIcon} path { fill: var(--menu-item-color-active); } &:hover { color: var(--menu-item-color-active-hover); background: var(--menu-item-bg-color-active-hover); } `}; ${({ deprecated }) => deprecated && css` color: var(--menu-content-color-disabled); &:hover { color: var(--menu-content-color-disabled); } `}; &:empty { padding: 0; } `; const MenuItemLabelTextWrapper = styled.div` display: flex; flex-direction: column; flex-grow: 1; `; const MenuItemSubLabel = styled.div` margin: var(--menu-item-sublabel-margin); color: var(--menu-item-sublabel-text-color); font-weight: var(--menu-item-sublabel-font-weight); font-size: var(--menu-item-sublabel-font-size); font-family: var(--menu-item-sublabel-font-family); `; const MenuItemIcon = styled(GenericIcon)` --icon-width: var(--menu-item-icon-size); --icon-height: var(--menu-item-icon-size); margin: var(--menu-item-icon-margin); flex-shrink: 0; overflow: hidden; `; const MenuItemLink = styled(Link)` order: 1; `; const MenuItemSeparatorLine = styled.div<{ depth?: number; linePosition?: string; }>` height: var(--menu-item-separator-line-height); background-color: var(--menu-item-separator-line-bg-color); margin: ${({ depth }) => ` var(--menu-item-padding-vertical) var(--sidebar-margin-horizontal) var(--menu-item-padding-vertical) calc(var(--sidebar-margin-horizontal) + ${depth ? 'var(--menu-item-nested-offset) * ' + depth : '0px'}) `}; order: ${({ linePosition }) => (linePosition === 'top' ? 0 : 1)}; `; const MenuItemLabel = styled.span` & > * { margin-right: var(--spacing-xxs); } `; const SidebarTag = styled(Tag)<{ $bgColor?: string; icon?: React.ReactNode }>` ${({ $bgColor }) => $bgColor && `background-color: ${$bgColor};`} /* for backward compatibility */ margin-left: 0; font-size: var(--font-size-sm); line-height: var(--line-height-sm); padding: 0 var(--spacing-xxs); max-width: 90px; --tag-padding: 0 var(--spacing-xxs); --tag-content-padding: 0; --tag-font-size: var(--font-size-sm); --tag-line-height: var(--line-height-sm); vertical-align: middle; ${ContentWrapper} { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; display: block; } `; const BadgeIcon = styled(GenericIcon)` --icon-width: var(--font-size-sm); --icon-height: var(--font-size-sm); margin-right: var(--spacing-xxs); flex-shrink: 0; `;