Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
615558c
fix(ui): revert DataGridCell font size to default (1rem)
edda Jun 12, 2026
7d0dbf5
feat(ui): add title attribute to SideNavigationItem string labels
edda Jun 12, 2026
2d91866
test(ui): cover string-children fallback for SideNavigationItem title
edda Jun 12, 2026
3b01f75
test(ui): assert SideNavigationItem omits title for ReactNode labels
edda Jun 12, 2026
8c792b2
fix(ui): clamp SideNavigationItem labels to two lines
edda Jun 12, 2026
ca6ee30
fix(ui): allow SideNavigationItem label container to shrink for clamp
edda Jun 12, 2026
dfd3400
chore(ui): add changeset for SideNavigation label overflow fix
edda Jun 12, 2026
08f869c
chore(config): add .mcp.json to gitignore
edda Jun 15, 2026
c0c91ee
fix(ui): left-align wrapped SideNavigationItem labels
edda Jun 15, 2026
5adc904
fix(ui): polish SideNavigation overflow, alignment, hover, and group …
edda Jun 17, 2026
a68dc19
fix(ui): address review feedback on SideNavigation polish PR
edda Jun 17, 2026
a17fa15
Merge branch 'main' into fix/datagrid-font-size-and-sidenav-overflow
edda Jun 17, 2026
2ab93c4
fix(ui): indent nested SideNavigationGroups correctly
edda Jun 18, 2026
e4b27c6
fix(ui): align SideNavigationItem icon with first line of wrapped label
edda Jun 18, 2026
4f09dd9
chore(ui): drop stale selected flag from SideNavigation story
edda Jun 18, 2026
2af0226
Merge branch 'main' into fix/datagrid-font-size-and-sidenav-overflow
edda Jun 18, 2026
1aa8220
test(ui): drop overly specific DataGridCell font-size test
edda Jun 18, 2026
eb0f357
fix(ui): suppress hover background on disabled SideNavigation expand …
edda Jun 18, 2026
4d65ee3
chore(ui): note disabled chevron hover fix in SideNavigation changeset
edda Jun 18, 2026
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
5 changes: 5 additions & 0 deletions .changeset/revert-datagridcell-font-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudoperators/juno-ui-components": patch
---

Revert DataGridCell font size back to the default (1rem). The reduced font size (0.875rem) introduced in #1710 was made the default without sufficient consideration; a smaller, opt-in size may be reintroduced as a configurable option in the future.
11 changes: 11 additions & 0 deletions .changeset/sidenavigation-label-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@cloudoperators/juno-ui-components": patch
---

SideNavigation polish:

- Long labels in `SideNavigationItem` and `SideNavigationGroup` now clamp to two lines and break mid-word, instead of overflowing the sidenav. String labels are exposed as a native `title` tooltip so users can read the full text on hover.
- Wrapped labels are left-aligned, and the expand/collapse chevron and optional icon stay aligned with the first line.
- `SideNavigationItem` and its expand chevron now show a hover background. The chevron's hover background is suppressed when the item is disabled.
- The whole `SideNavigationGroup` row is clickable to expand/collapse, and its children are indented to match nested `SideNavigationItem` children.
- Nested `SideNavigationGroup`s (a group inside another group, or inside a `SideNavigationItem`) now indent correctly.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ act_*.json
.out-of-code-insights
.claude
claude.md
.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const cellBaseStyles = (nowrap: boolean, cellVerticalAlignment: CellVerticalAlig
jn:border-b
jn:border-theme-background-lvl-2
jn:h-full
jn:text-sm
`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const Default: Story = {
<SideNavigationItem label="Inbox" />
<SideNavigationItem label="Sent Items" />
</SideNavigationItem>
<SideNavigationItem label="Searches" icon="search" />
<SideNavigationItem label="Searches" />
</SideNavigationList>
</SideNavigation>
),
Expand All @@ -60,20 +60,20 @@ export const NavigationWithGroups: Story = {
render: (args) => (
<SideNavigation {...args}>
<SideNavigationList>
<SideNavigationItem label="Item 1" icon="addCircle" selected={true} href="#" />
<SideNavigationItem label="Item 2" icon="addCircle">
<SideNavigationItem label="Item 1" selected={true} href="#" />
<SideNavigationGroup label="Item 2">
<SideNavigationItem label="Sub-Child 1" />
<SideNavigationItem label="Sub-Child 2">
<SideNavigationGroup label="Sub-Child 2">
<SideNavigationItem label="Sub-Child 3" />
</SideNavigationItem>
</SideNavigationItem>
</SideNavigationGroup>
</SideNavigationGroup>
<SideNavigationItem label="Item 3" href="#" />
<SideNavigationGroup label="Group Example">
<SideNavigationItem label="Grouped Item 1" />
<SideNavigationItem label="Grouped Item 2">
<SideNavigationGroup label="Grouped Item 2">
<SideNavigationItem label="Sub-Child 1" />
<SideNavigationItem label="Sub-Child 2" selected />
</SideNavigationItem>
<SideNavigationItem label="Sub-Child 2" />
</SideNavigationGroup>
</SideNavigationGroup>
</SideNavigationList>
</SideNavigation>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { createContext } from "react"

export const LevelContext = createContext<number>(0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

/* Shared nesting levels.
* Both SideNavigationItem and SideNavigationGroup tag the element that carries
* their label with one of these classes; the indentation lives here so an item
* and a group at the same nesting depth always indent identically.
* level-1 has no margin-left rule (top level, flush with the navigation edge).
*/
.level-2 {
margin-left: 1.75rem;
}

.level-3 {
margin-left: 2.5rem;
}

/* SideNavigationItem */

.juno-sidenavigation-item {
border-radius: 0.25rem;
border-left: 0.25rem solid transparent;
color: var(--color-sidenav-item-text-default);
}

.juno-sidenavigation-item:hover {
color: var(--color-sidenav-item-text-hover);
}

.juno-sidenavigation-item:not(.juno-sidenavigation-item-selected):hover {
background-color: var(--color-sidenav-item-background-hover);
}

.juno-sidenavigation-item:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-accent);
}

.juno-sidenavigation-item-selected {
background-color: var(--color-sidenav-item-background-selected);
border-left: 0.25rem solid var(--color-accent);
}

.expand-icon {
display: flex;
align-items: center;
min-height: 1.875rem;
border-radius: 0.25rem;
}

.expand-icon:not(:disabled):hover {
background-color: var(--color-sidenav-item-background-hover);
}

/* Item label: clamp + per-level font weight, derived from the shared level-N classes. */
.juno-sidenavigation-item .level-1,
.juno-sidenavigation-item .level-2,
.juno-sidenavigation-item .level-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
overflow-wrap: anywhere;
text-align: left;
}

.juno-sidenavigation-item .level-1 {
font-weight: bold;
}

.juno-sidenavigation-item .level-2,
.juno-sidenavigation-item .level-3 {
font-weight: 500;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,49 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { Children, ReactElement, ReactNode, useEffect, useState, MouseEvent } from "react"
import React, { Children, ReactElement, ReactNode, useContext, useEffect, useState, MouseEvent } from "react"
import { Icon } from "../Icon"
import { SideNavigationItemProps } from "../SideNavigationItem"
import { LevelContext } from "../SideNavigation/levelContext"
import "../SideNavigation/sidenavigation.css"

const sideNavGroupStyles = `
jn:flex
jn:items-start
jn:justify-between
jn:px-[0.5rem]
jn:py-[0.1875rem]
jn:pl-[0.5rem]
jn:text-theme-default
jn:w-full
jn:rounded
jn:border-l-[0.25rem]
jn:border-transparent
`

const interactiveGroupStyles = `
jn:cursor-pointer
jn:hover:bg-theme-sidenav-item-hover
`

const labelContainerStyles = `
jn:flex
jn:items-center
jn:flex-grow
jn:min-w-0
jn:min-h-[1.875rem]
`

const labelClampStyles = `
jn:text-left
jn:line-clamp-2
jn:[overflow-wrap:anywhere]
`

const chevronStyles = `
jn:flex
jn:items-center
jn:min-h-[1.875rem]
`

export interface SideNavigationGroupProps {
/** Represents the nested components within the navigation group. */
children?: ReactElement<SideNavigationItemProps> | ReactElement<SideNavigationItemProps>[]
Expand All @@ -41,6 +68,8 @@ export interface SideNavigationGroupProps {

export const SideNavigationGroup = ({ children, label = "", open = false }: SideNavigationGroupProps): ReactNode => {
const [isOpen, setIsOpen] = useState(open)
const level = useContext(LevelContext)
const levelClassName = `level-${level + 1}`

// Sync internal state with external prop changes
useEffect(() => {
Expand All @@ -52,26 +81,52 @@ export const SideNavigationGroup = ({ children, label = "", open = false }: Side
setIsOpen(!isOpen)
}

const renderExpandButton = () =>
children && Children.count(children) > 0 ? (
<button type="button" onClick={handleToggleOpen} aria-label={isOpen ? "Collapse section" : "Expand section"}>
const hasChildren = !!children && Children.count(children) > 0

const titleText: string | undefined = typeof label === "string" && label.length > 0 ? label : undefined

const renderChevron = () =>
hasChildren ? (
<span className={chevronStyles} aria-hidden="true">
<Icon size="24" icon={isOpen ? "expandMore" : "chevronRight"} />
</button>
</span>
) : null

const renderGroup = () => (
<div
className={`juno-sidenavigation-group ${sideNavGroupStyles} ${isOpen ? "juno-sidenavigation-group-open" : ""}`}
>
<span className="font-bold text-sm">{label}</span>
{renderExpandButton()}
</div>
const renderLabel = () => (
<span className={labelContainerStyles}>
<span className={`${labelClampStyles} ${levelClassName}`}>{label}</span>
</span>
)
Comment thread
edda marked this conversation as resolved.

const renderGroup = () => {
const baseClassName = `juno-sidenavigation-group ${sideNavGroupStyles} ${isOpen ? "juno-sidenavigation-group-open" : ""}`

if (hasChildren) {
return (
Comment thread
edda marked this conversation as resolved.
<button
type="button"
onClick={handleToggleOpen}
aria-expanded={isOpen}
className={`${baseClassName} ${interactiveGroupStyles}`}
title={titleText}
>
{renderLabel()}
{renderChevron()}
</button>
)
}

return (
<div className={baseClassName} title={titleText}>
{renderLabel()}
</div>
)
}

return (
<>
{renderGroup()}
{isOpen && children}
{isOpen && <LevelContext.Provider value={level + 1}>{children}</LevelContext.Provider>}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,73 @@ describe("SideNavigationGroup", () => {
expect(screen.queryByText("Child Item 1")).not.toBeInTheDocument()
expect(screen.queryByText("Child Item 2")).not.toBeInTheDocument()
})

test("renders the group as a button with aria-expanded when it has children", () => {
render(
<SideNavigationGroup label="Group with children">
<SideNavigationItem label="Child Item" />
</SideNavigationGroup>
)

const group = screen.getByRole("button", { name: /Group with children/ })
expect(group.tagName).toBe("BUTTON")
expect(group).toHaveAttribute("aria-expanded", "false")
})

test("renders the group as a non-button element when it has no children", () => {
render(<SideNavigationGroup label="Childless Group" />)

expect(screen.queryByRole("button")).not.toBeInTheDocument()
expect(screen.getByText("Childless Group")).toBeInTheDocument()
})

test("sets a title attribute on the group when label is a string", () => {
render(
<SideNavigationGroup label="A very long group label that may overflow">
<SideNavigationItem label="Child Item" />
</SideNavigationGroup>
)

const group = screen.getByRole("button")
expect(group).toHaveAttribute("title", "A very long group label that may overflow")
})

test("does not set a title attribute when label is a ReactNode", () => {
render(
<SideNavigationGroup label={<span>Node label</span>}>
<SideNavigationItem label="Child Item" />
</SideNavigationGroup>
)

const group = screen.getByRole("button")
expect(group).not.toHaveAttribute("title")
})

test("indents the group label based on its nesting level", () => {
render(
<SideNavigationGroup label="Top" open>
<SideNavigationGroup label="Middle" open>
<SideNavigationGroup label="Inner" open>
<SideNavigationItem label="Leaf" />
</SideNavigationGroup>
</SideNavigationGroup>
</SideNavigationGroup>
)

expect(screen.getByText("Top")).toHaveClass("level-1")
expect(screen.getByText("Middle")).toHaveClass("level-2")
expect(screen.getByText("Inner")).toHaveClass("level-3")
})

test("propagates its level so child SideNavigationItems indent correctly", () => {
render(
<SideNavigationGroup label="Outer" open>
<SideNavigationGroup label="Inner" open>
<SideNavigationItem label="Leaf" />
</SideNavigationGroup>
</SideNavigationGroup>
)

expect(screen.getByText("Leaf")).toHaveClass("level-3")
})
})
Loading
Loading