diff --git a/.changeset/revert-datagridcell-font-size.md b/.changeset/revert-datagridcell-font-size.md new file mode 100644 index 0000000000..3d1a5097d4 --- /dev/null +++ b/.changeset/revert-datagridcell-font-size.md @@ -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. diff --git a/.changeset/sidenavigation-label-overflow.md b/.changeset/sidenavigation-label-overflow.md new file mode 100644 index 0000000000..97510f7b13 --- /dev/null +++ b/.changeset/sidenavigation-label-overflow.md @@ -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. diff --git a/.gitignore b/.gitignore index 485b1bbafe..60696da95b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ act_*.json .out-of-code-insights .claude claude.md +.mcp.json diff --git a/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx b/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx index a2fef688d6..4d4c579ae0 100644 --- a/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx +++ b/packages/ui-components/src/components/DataGridCell/DataGridCell.component.tsx @@ -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 ` } diff --git a/packages/ui-components/src/components/SideNavigation/SideNavigation.stories.tsx b/packages/ui-components/src/components/SideNavigation/SideNavigation.stories.tsx index 1c0efe7084..8eb7749f22 100644 --- a/packages/ui-components/src/components/SideNavigation/SideNavigation.stories.tsx +++ b/packages/ui-components/src/components/SideNavigation/SideNavigation.stories.tsx @@ -42,7 +42,7 @@ export const Default: Story = { - + ), @@ -60,20 +60,20 @@ export const NavigationWithGroups: Story = { render: (args) => ( - - + + - + - - + + - + - - + + diff --git a/packages/ui-components/src/components/SideNavigation/levelContext.ts b/packages/ui-components/src/components/SideNavigation/levelContext.ts new file mode 100644 index 0000000000..034731bfed --- /dev/null +++ b/packages/ui-components/src/components/SideNavigation/levelContext.ts @@ -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(0) diff --git a/packages/ui-components/src/components/SideNavigation/sidenavigation.css b/packages/ui-components/src/components/SideNavigation/sidenavigation.css new file mode 100644 index 0000000000..f87bda78f1 --- /dev/null +++ b/packages/ui-components/src/components/SideNavigation/sidenavigation.css @@ -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; +} diff --git a/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.component.tsx b/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.component.tsx index f2079fda73..326d231046 100644 --- a/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.component.tsx +++ b/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.component.tsx @@ -3,15 +3,17 @@ * 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 @@ -19,6 +21,31 @@ const sideNavGroupStyles = ` 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 | ReactElement[] @@ -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(() => { @@ -52,26 +81,52 @@ export const SideNavigationGroup = ({ children, label = "", open = false }: Side setIsOpen(!isOpen) } - const renderExpandButton = () => - children && Children.count(children) > 0 ? ( - + ) : null - const renderGroup = () => ( -
- {label} - {renderExpandButton()} -
+ const renderLabel = () => ( + + {label} + ) + const renderGroup = () => { + const baseClassName = `juno-sidenavigation-group ${sideNavGroupStyles} ${isOpen ? "juno-sidenavigation-group-open" : ""}` + + if (hasChildren) { + return ( + + ) + } + + return ( +
+ {renderLabel()} +
+ ) + } + return ( <> {renderGroup()} - {isOpen && children} + {isOpen && {children}} ) } diff --git a/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.test.tsx b/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.test.tsx index d1b3323500..31345887e8 100644 --- a/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.test.tsx +++ b/packages/ui-components/src/components/SideNavigationGroup/SideNavigationGroup.test.tsx @@ -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( + + + + ) + + 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() + + 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( + + + + ) + + 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( + Node label}> + + + ) + + const group = screen.getByRole("button") + expect(group).not.toHaveAttribute("title") + }) + + test("indents the group label based on its nesting level", () => { + render( + + + + + + + + ) + + 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( + + + + + + ) + + expect(screen.getByText("Leaf")).toHaveClass("level-3") + }) }) diff --git a/packages/ui-components/src/components/SideNavigationItem/SideNavigationItem.component.tsx b/packages/ui-components/src/components/SideNavigationItem/SideNavigationItem.component.tsx index 9605c7d111..b80f50b3cb 100644 --- a/packages/ui-components/src/components/SideNavigationItem/SideNavigationItem.component.tsx +++ b/packages/ui-components/src/components/SideNavigationItem/SideNavigationItem.component.tsx @@ -7,7 +7,6 @@ import React, { useState, useEffect, useContext, - createContext, ReactNode, HTMLAttributes, MouseEventHandler, @@ -16,9 +15,8 @@ import React, { Children, } from "react" import { Icon, KnownIcons } from "../Icon/Icon.component" -import "./sidenavigationitem.css" - -const LevelContext = createContext(0) +import { LevelContext } from "../SideNavigation/levelContext" +import "../SideNavigation/sidenavigation.css" const sideNavItemStyles = ` jn:flex @@ -32,8 +30,9 @@ const sideNavItemStyles = ` const leftStyles = ` jn:flex - jn:items-center + jn:items-start jn:flex-grow + jn:min-w-0 ` const disabledStyles = ` @@ -112,6 +111,13 @@ export const SideNavigationItem = ({ } } + const titleText: string | undefined = + typeof label === "string" && label.length > 0 + ? label + : typeof children === "string" && children.length > 0 + ? children + : undefined + const renderExpandButton = () => children && typeof children !== "string" && Children.count(children) ? (