Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/components/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Banner } from './banner';
import { GlobalNavigation } from './global-navigation';
import { ContentHeader, PageHeader } from './header';
import { Layout as LayoutRoot } from './Layout';

import {
AsideLeft,
BannerContainer,
Expand All @@ -30,9 +31,11 @@ import {
PageFooter,
PageGrid,
} from './LayoutSlots';

import { SidebarNavigation } from './sidebar-navigation';

export { BannerVariety, type BannerProps } from './banner';

export type {
GlobalNavigationAccountProps,
GlobalNavigationActionProps,
Expand All @@ -43,20 +46,25 @@ export type {
GlobalNavigationPrimaryProps,
GlobalNavigationProps,
} from './global-navigation';

export type {
ContentHeaderProps,
ContentHeaderTitleProps,
PageHeaderMetadataProps,
PageHeaderProps,
PageHeaderTitleProps,
} from './header';

export type { LayoutProps } from './Layout';
export type { AsideProps, PageGridProps } from './LayoutSlots';
export { AsideSize, PageWidth } from './LayoutTypes';

export type {
SidebarNavigationAccordionChildItemProps,
SidebarNavigationAccordionItemProps,
SidebarNavigationGroupProps,
SidebarNavigationHeaderProps,
SidebarNavigationItemBaseProps,
SidebarNavigationItemProps,
SidebarNavigationProps,
} from './sidebar-navigation';
Expand Down Expand Up @@ -354,6 +362,7 @@ export const Layout = Object.assign(LayoutRoot, {
* - {@link SidebarNavigation.Group} - Grouped navigation sections
* - {@link SidebarNavigation.Item} - Individual navigation items
* - {@link SidebarNavigation.AccordionItem} - Expandable sections
* - {@link SidebarNavigation.AccordionItem.Item} - Navigation rows within accordion sections

@gregaubert gregaubert Jul 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly now we have a new AccordionItem specifc item that we didn't have before and we should use as the content of the AccordionItem right?

And the only difference between the AccordionChildItem and the SidebarNavigationItem we where using before is the optional Icon for AccordionChildItem? Or are there other differences?

It seems like a lot of changes for making a prop optional. Couldn't we just make it optional on SidebarNavigationItem and that's it?

* - {@link SidebarNavigation.Footer} - Fixed footer section
*
* ```tsx
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Echoes React
* Copyright (C) 2023-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { SidebarNavigationBaseItem } from './SidebarNavigationBaseItem';
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.

import {
SidebarNavigationIconComponent,
SidebarNavigationItemBaseProps,
} from './SidebarNavigationTypes';

export interface SidebarNavigationAccordionChildItemProps extends SidebarNavigationItemBaseProps {
/**
* The icon component to display at the start of the SidebarNavigationAccordionChildItem.
* Must be an Echoes Icon component. Omit it for icon-less accordion child rows.
*/
Icon?: SidebarNavigationIconComponent;
}

export function SidebarNavigationAccordionChildItem(
props: Readonly<SidebarNavigationAccordionChildItemProps>,
) {
return <SidebarNavigationBaseItem {...props} />;
}
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.

SidebarNavigationAccordionChildItem.displayName = 'SidebarNavigationAccordionChildItem';
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,53 @@
*/

import styled from '@emotion/styled';
import {
forwardRef,
ForwardRefExoticComponent,
ReactNode,
useCallback,
useEffect,
useId,
useRef,
useState,
} from 'react';

import { ReactNode, Ref, useCallback, useEffect, useId, useRef, useState } from 'react';

Comment on lines +22 to +24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some weird spaces got added between the imports, we might be missing some formatting rules there

import { TextNode } from '~types/utils';
import { cssVar } from '~utils/design-tokens';
import { IconChevronDown, IconChevronRight, IconFilledProps } from '../../icons';
import { IconChevronDown, IconChevronRight } from '../../icons';
import { Tooltip } from '../../tooltip';

import {
sidebarNavigationBaseItemStyles,
sidebarNavigationItemIconStyles,
SidebarNavigationItemLabel,
} from './SidebarNavigationItemStyles';

import { SidebarNavigationIconComponent } from './SidebarNavigationTypes';
import { TOOLTIP_DELAY_IN_MS } from './utils';

export interface SidebarNavigationAccordionItemProps {
/**
* ARIA label for the SidebarNavigationAccordionItem button.
*/
ariaLabel?: string;
/**
* List of SidebarNavigationItem that are displayed when the accordion is expanded.
* Should ideally be maximum 5 items.
* List of navigation child items displayed when the accordion is expanded.
* Prefer `SidebarNavigation.AccordionItem.Item` and keep the list to five items or fewer.
*/
children: ReactNode;
/**
* Optional CSS class name applied to the accordion button element.
*/
className?: string;
/**
* Whether to disable the tooltip on the accordion item or not.
* By default the tooltip is enabled, it should only be disabled if you don't expect the content to be ellipsed.
* @defaultValue false
*/
disableTooltip?: boolean;
/**
* The icon component to display at the start of the SidebarNavigationAccordionItem.
* Must be an Echoes Icon component.
*/
Icon: SidebarNavigationIconComponent;
/**
* Whether the accordion is open by default.
* @defaultValue false
*/
isDefaultOpen?: boolean;
/**
* The label for the SidebarNavigationAccordionItem.
*/
Expand All @@ -66,37 +79,34 @@ export interface SidebarNavigationAccordionItemProps {
*/
onOpen?: VoidFunction;
/**
* Whether the accordion is open by default. Defaults to false.
* React ref forwarded to the root button element.
*/
isDefaultOpen?: boolean;
ref?: Ref<HTMLButtonElement>;
/**
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.
* When true, scrolls the last child item into view when the accordion opens.
* Useful when the accordion is near the bottom of a scrollable container.
* @defaultValue false
*/
scrollLastChildIntoViewOnOpen?: boolean;
/**
* Optional content to display on the right, before the chevron. Typically badges, item count and similar metadata.
*/
suffix?: ReactNode;
/**
* The icon component to display at the start of the SidebarNavigationAccordionItem.
* Must be an Echoes Icon component.
*/
Icon: ForwardRefExoticComponent<IconFilledProps & React.RefAttributes<HTMLSpanElement>>;
}

export const SidebarNavigationAccordionItem = forwardRef<
HTMLButtonElement,
SidebarNavigationAccordionItemProps
>((props, ref) => {
export function SidebarNavigationAccordionItem(
props: Readonly<SidebarNavigationAccordionItemProps>,
) {
const {
ariaLabel,
children,
isDefaultOpen = false,
disableTooltip = false,
Icon,
isDefaultOpen = false,
label,
onClose,
onOpen,
ref,
scrollLastChildIntoViewOnOpen,
suffix,
...htmlProps
Expand Down Expand Up @@ -139,9 +149,11 @@ export const SidebarNavigationAccordionItem = forwardRef<
{...htmlProps}
aria-controls={accordionPanelId}
aria-expanded={open}
aria-label={ariaLabel}
id={accordionId}
onClick={handleClick}
ref={ref}>
ref={ref}
type="button">
<Icon css={sidebarNavigationItemIconStyles} isFilled={false} />

<SidebarNavigationItemLabel>{label}</SidebarNavigationItemLabel>
Expand All @@ -165,7 +177,7 @@ export const SidebarNavigationAccordionItem = forwardRef<
</AccordionItemPanel>
</AccordionWrapper>
);
});
}

SidebarNavigationAccordionItem.displayName = 'SidebarNavigationAccordionItem';

Expand Down
148 changes: 148 additions & 0 deletions src/components/layout/sidebar-navigation/SidebarNavigationBaseItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Echoes React
* Copyright (C) 2023-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { css } from '@emotion/react';
import styled from '@emotion/styled';

import { MouseEvent, useCallback } from 'react';
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.

import { NavLinkBase } from '~common/components/NavLinkBase';
import { isDefined } from '~common/helpers/types';
import { cssVar } from '~utils/design-tokens';
import { Tooltip } from '../../tooltip';

import {
sidebarNavigationBaseItemStyles,
sidebarNavigationItemIconStyles,
SidebarNavigationItemLabel,
UnstyledListItem,
} from './SidebarNavigationItemStyles';

import {
SidebarNavigationIconComponent,
SidebarNavigationItemBaseProps,
} from './SidebarNavigationTypes';

import { TOOLTIP_DELAY_IN_MS } from './utils';

interface SidebarNavigationBaseItemInternalProps extends SidebarNavigationItemBaseProps {
Icon?: SidebarNavigationIconComponent;
}

export function SidebarNavigationBaseItem(props: Readonly<SidebarNavigationBaseItemInternalProps>) {
const {
ariaLabel,
children,
disableIconWhenSidebarOpen = false,
disableTooltip = false,
Icon,
onClick,
ref,
suffix,
...htmlProps
} = props;
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.

const handleClick = useCallback(
(event: MouseEvent<HTMLAnchorElement>) => {
event.currentTarget.blur();
onClick?.(event);
},
[onClick],
);

return (
<UnstyledListItem>
<Tooltip
content={disableTooltip ? undefined : children}
delayDuration={TOOLTIP_DELAY_IN_MS}
side="right">
<SidebarNavigationBaseItemLink
{...htmlProps}
aria-label={ariaLabel}
onClick={handleClick}
ref={ref}>
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.
{isDefined(Icon) && (
<Icon
css={[
sidebarNavigationBaseItemIconStyles,
disableIconWhenSidebarOpen
? sidebarNavigationBaseItemHideWhenSidebarOpenStyles
: undefined,
]}
isFilled={false}
/>
)}

<SidebarNavigationItemLabel>{children}</SidebarNavigationItemLabel>

{suffix}
</SidebarNavigationBaseItemLink>
</Tooltip>
</UnstyledListItem>
);
}

SidebarNavigationBaseItem.displayName = 'SidebarNavigationBaseItem';

const SidebarNavigationBaseItemLink = styled(NavLinkBase)`
${sidebarNavigationBaseItemStyles}

// When the item is inside an accordion, the display and visibility values change based on the
// accordion state. Outside of accordions, they fall back to flex and visible.
display: var(--sidebar-navigation-accordion-children-display, flex);
visibility: var(--sidebar-navigation-accordion-children-visibility, visible);

// The accordion provides an outline in closed sidebar mode for the active child item. Keeping it
// outside the active selector avoids overriding the focus outline from the base item styles.
outline: var(--sidebar-navigation-accordion-children-outline);

&:active,
&.active {
// Active items stay visible even when the parent accordion is closed.
display: flex;
visibility: visible;

background-color: ${cssVar('sidebar-navigation-item-colors-background-active')};
color: ${cssVar('color-text-accent')};
font: ${cssVar('typography-text-default-semi-bold')};

&:hover {
background-color: ${cssVar('sidebar-navigation-item-colors-background-hover')};
}
}
`;

SidebarNavigationBaseItemLink.displayName = 'SidebarNavigationBaseItemLink';

const sidebarNavigationBaseItemIconStyles = css`
${sidebarNavigationItemIconStyles}

${SidebarNavigationBaseItemLink}.active > &,
${SidebarNavigationBaseItemLink}:active > & {
color: ${cssVar('color-icon-accent')};
}
`;

const sidebarNavigationBaseItemHideWhenSidebarOpenStyles = css`
[data-sidebar-docked='true'] &,
[data-sidebar-docked='false'] nav:is(:hover, :focus-within) & {
display: none;
}
`;
Loading
Loading