Skip to content
Merged
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 .changeset/clean-hoops-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudoperators/juno-ui-components": minor
---

feat(ui): Streamline `DataGridCheckboxCell` and add `verticalAlignment` prop to `DataGridCell`.

`DataGridCheckboxCell` is now removed completely. As it has been WIP for all the time and there isn't one documented use, we will release this as a minor instead of major, even though it is technically breaking.

`DataGridCell` gets a `verticalAlignment` prop (`"center" | "top"`) that overrides the parent `DataGrid`'s `cellVerticalAlignment` for individual cells.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { DataGrid } from "./DataGrid.component"
import { DataGridRow } from "../DataGridRow"
import { DataGridCell } from "../DataGridCell"
import { DataGridHeadCell } from "../DataGridHeadCell"
import { DataGridCheckboxCell } from "../DataGridCheckboxCell"
import { DataGridToolbar } from "../DataGridToolbar"
import { Stack } from "../Stack"
import { Button } from "../Button"
Expand Down Expand Up @@ -157,6 +156,7 @@ export const WithSearchOnly: Story = {
export const FullyFeatured: Story = {
render: () => {
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc")
const [selected, setSelected] = useState<Record<string, boolean>>({})

return (
<>
Expand Down Expand Up @@ -250,7 +250,13 @@ export const FullyFeatured: Story = {
</DataGridRow>
{servers.map((s) => (
<DataGridRow key={s.id}>
<DataGridCheckboxCell />
<DataGridCell verticalAlignment="center">
<Checkbox
aria-label={`Select ${s.name}`}
checked={!!selected[s.id]}
onChange={(e) => setSelected((prev) => ({ ...prev, [s.id]: e.target.checked }))}
/>
</DataGridCell>
<DataGridCell>{s.name}</DataGridCell>
<DataGridCell>{s.region}</DataGridCell>
<DataGridCell>{s.status}</DataGridCell>
Expand Down Expand Up @@ -367,7 +373,13 @@ export const FullyFeatured: Story = {
</DataGridRow>
{servers.map((s) => (
<DataGridRow key={s.id}>
<DataGridCheckboxCell />
<DataGridCell verticalAlignment="center">
<Checkbox
aria-label={\`Select \${s.name}\`}
checked={!!selected[s.id]}
onChange={(e) => setSelected((prev) => ({ ...prev, [s.id]: e.target.checked }))}
/>
</DataGridCell>
<DataGridCell>{s.name}</DataGridCell>
<DataGridCell>{s.region}</DataGridCell>
<DataGridCell>{s.status}</DataGridCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const cellBaseStyles = (nowrap: boolean, cellVerticalAlignment: CellVerticalAlig
? `
jn:justify-center
jn:flex
jn:flex-col
jn:flex-col
`
: ""
}
Expand All @@ -42,13 +42,13 @@ const cellCustomStyles = (colSpan: number | undefined) => {
* @see {@link DataGridCellProps}
*/
export const DataGridCell = forwardRef<HTMLDivElement, DataGridCellProps>(
({ colSpan, nowrap = false, className = "", children, ...props }, ref) => {
({ colSpan, nowrap = false, verticalAlignment, className = "", children, ...props }, ref) => {
const dataGridContext = useDataGridContext() || {}
const cellVerticalAlignment = dataGridContext.cellVerticalAlignment
const effectiveVerticalAlignment = verticalAlignment ?? dataGridContext.cellVerticalAlignment

return (
<div
className={`juno-datagrid-cell ${cellBaseStyles(nowrap, cellVerticalAlignment)} ${className}`}
className={`juno-datagrid-cell ${cellBaseStyles(nowrap, effectiveVerticalAlignment)} ${className}`}
style={cellCustomStyles(colSpan)}
role="gridcell"
ref={ref}
Expand All @@ -72,6 +72,12 @@ export interface DataGridCellProps extends HTMLAttributes<HTMLDivElement> {
*/
nowrap?: boolean

/**
* Overrides the parent `DataGrid`'s `cellVerticalAlignment` for this cell.
* When not set, the cell inherits the grid-level setting.
*/
verticalAlignment?: CellVerticalAlignmentType

/** Components or elements to render within the DataGridCell. */
children?: ReactNode

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import { DataGrid } from "../DataGrid/index"
import { DataGridRow } from "../DataGridRow/index"
import { DataGridHeadCell } from "../DataGridHeadCell/index"
import { DataGridCell } from "./index"

const meta: Meta<typeof DataGridCell> = {
Expand All @@ -19,65 +20,145 @@ const meta: Meta<typeof DataGridCell> = {
type: { summary: "ReactNode" },
},
},
},
decorators: [
(Story) => (
<DataGrid columns={3}>
<DataGridRow>
<Story />
</DataGridRow>
</DataGrid>
),
],
parameters: {
docs: {
source: {
excludeDecorators: false,
},
verticalAlignment: {
control: { type: "radio" },
options: ["center", "top"],
},
},
}

export default meta
type Story = StoryObj<typeof meta>

const items = [
{ id: "1", name: "node-prod-01", status: "Running" },
{ id: "2", name: "node-prod-02", status: "Stopped" },
{ id: "3", name: "node-staging-01", status: "Error" },
]

const longItems = [
{
id: "1",
name: "node-prod-01",
status: "Running — all systems operational, no issues detected, last checked 2 minutes ago",
},
{
id: "2",
name: "node-prod-02",
status: "Stopped — scheduled maintenance window currently active, expected to resume at 06:00 UTC",
},
{
id: "3",
name: "node-staging-01",
status:
"Error — health check failed on port 8080, automatic restart attempted 3 times, manual intervention required",
},
]

export const Default: Story = {
parameters: {
docs: {
description: {
story: "Juno DataGridCell for use in DataGrid",
story: "A standard `DataGridCell` inside a `DataGrid`.",
},
},
},
args: {
children: ["DataGridCell"],
},
render: () => (
<DataGrid columns={3}>
<DataGridRow>
<DataGridHeadCell>Name</DataGridHeadCell>
<DataGridHeadCell>Status</DataGridHeadCell>
<DataGridHeadCell>ID</DataGridHeadCell>
</DataGridRow>
{items.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell>{item.name}</DataGridCell>
<DataGridCell>{item.status}</DataGridCell>
<DataGridCell>{item.id}</DataGridCell>
</DataGridRow>
))}
</DataGrid>
),
}

export const NoWrap: Story = {
parameters: {
docs: {
description: {
story: "Juno DataGridCell with nowrap option (content has white-space: nowrap;)",
story:
"With `nowrap`, cell content will not wrap onto multiple lines. Non-wrapping cells push their column to the available maximum width, and overflowing content is visible by default — consumers are responsible for handling overflow. The last row demonstrates truncation with an ellipsis: wrap the cell content in a `<span>` with `block` and `truncate` — the span must be a block element because `text-overflow: ellipsis` does not apply directly to flex containers.",
},
},
},
args: {
nowrap: true,
children: ["DataGridCell does not wrap"],
},
render: () => (
<DataGrid columns={3}>
<DataGridRow>
<DataGridHeadCell>Name</DataGridHeadCell>
<DataGridHeadCell>Status</DataGridHeadCell>
<DataGridHeadCell>ID</DataGridHeadCell>
</DataGridRow>
{longItems.map((item, index) => (
<DataGridRow key={item.id}>
<DataGridCell nowrap>{item.name}</DataGridCell>
<DataGridCell nowrap className={index === longItems.length - 1 ? "jn:overflow-hidden" : ""}>
{index === longItems.length - 1 ? <span className="jn:block jn:truncate">{item.status}</span> : item.status}
</DataGridCell>
<DataGridCell nowrap>{item.id}</DataGridCell>
</DataGridRow>
))}
</DataGrid>
),
}

export const ColSpan: Story = {
parameters: {
docs: {
description: {
story: "Juno DataGridCell with colspan",
story: "A `DataGridCell` with `colSpan` spanning multiple columns.",
},
},
},
args: {
colSpan: 3,
children: ["DataGridCell with colspan"],
render: () => (
<DataGrid columns={3}>
<DataGridRow>
<DataGridHeadCell>Name</DataGridHeadCell>
<DataGridHeadCell>Status</DataGridHeadCell>
<DataGridHeadCell>ID</DataGridHeadCell>
</DataGridRow>
{items.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell colSpan={2}>{item.name}</DataGridCell>
<DataGridCell>{item.id}</DataGridCell>
</DataGridRow>
))}
</DataGrid>
),
}

export const VerticalAlignmentOverride: Story = {
parameters: {
docs: {
description: {
story:
'Use `verticalAlignment` to override the parent `DataGrid`\'s `cellVerticalAlignment` for individual cells. Here the grid is set to `cellVerticalAlignment="center"` (the default), but the description cell uses `verticalAlignment="top"` to align longer content to the top while the name cell remains centered.',
},
},
},
render: () => (
<DataGrid columns={2} cellVerticalAlignment="center">
<DataGridRow>
<DataGridHeadCell>Name</DataGridHeadCell>
<DataGridHeadCell>Description</DataGridHeadCell>
</DataGridRow>
{items.map((item) => (
<DataGridRow key={item.id}>
<DataGridCell verticalAlignment="top">{item.name}</DataGridCell>
<DataGridCell>
This is a longer description for {item.name} that spans multiple lines to demonstrate that top alignment
works independently of the grid-level setting.
</DataGridCell>
</DataGridRow>
))}
</DataGrid>
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,72 @@
import * as React from "react"
import { render, screen } from "@testing-library/react"
import { DataGridCell } from "./index"
import { DataGrid } from "../DataGrid/index"

describe("DataGridCell", () => {
test("renders a DataGridCell", () => {
render(<DataGridCell />)
expect(screen.getByRole("gridcell")).toBeInTheDocument()
expect(screen.getByRole("gridcell")).toHaveClass("juno-datagrid-cell")
})

test("renders a custom className", () => {
render(<DataGridCell className="my-custom-class" />)
expect(screen.getByRole("gridcell")).toBeInTheDocument()
expect(screen.getByRole("gridcell")).toHaveClass("my-custom-class")
})

test("renders arbitrary props", () => {
render(<DataGridCell data-testid="my-cell" data-foo="bar" />)
expect(screen.getByTestId("my-cell")).toHaveAttribute("data-foo", "bar")
})

test("inherits cellVerticalAlignment from parent DataGrid context", () => {
render(
<DataGrid columns={1} cellVerticalAlignment="center">
<DataGridCell />
</DataGrid>
)
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex")
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex-col")
expect(screen.getByRole("gridcell")).toHaveClass("jn:justify-center")
})

test("verticalAlignment prop overrides parent DataGrid context", () => {
render(
<DataGrid columns={1} cellVerticalAlignment="center">
<DataGridCell verticalAlignment="top" />
</DataGrid>
)
expect(screen.getByRole("gridcell")).not.toHaveClass("jn:flex")
expect(screen.getByRole("gridcell")).not.toHaveClass("jn:flex-col")
expect(screen.getByRole("gridcell")).not.toHaveClass("jn:justify-center")
})

test("verticalAlignment='center' overrides parent DataGrid cellVerticalAlignment='top'", () => {
render(
<DataGrid columns={1} cellVerticalAlignment="top">
<DataGridCell verticalAlignment="center" />
</DataGrid>
)
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex")
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex-col")
expect(screen.getByRole("gridcell")).toHaveClass("jn:justify-center")
})

test("renders nowrap class when nowrap is set", () => {
render(<DataGridCell nowrap />)
expect(screen.getByRole("gridcell")).toHaveClass("jn:whitespace-nowrap")
})

test("verticalAlignment prop works without a parent DataGrid context", () => {
render(<DataGridCell verticalAlignment="center" />)
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex")
expect(screen.getByRole("gridcell")).toHaveClass("jn:flex-col")
expect(screen.getByRole("gridcell")).toHaveClass("jn:justify-center")
})

test("renders colSpan via inline style", () => {
render(<DataGridCell colSpan={3} />)
expect(screen.getByRole("gridcell")).toHaveStyle({ gridColumn: "span 3 / span 3" })
})
})
Loading
Loading