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
5 changes: 5 additions & 0 deletions .changeset/tabpanel-classname.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@qorpe/ui": minor
---

`TabPanel` accepts `className` (defaults to the previous `pt-4`): a scrolling panel inside a flex column can now carry `min-h-0 flex-1 overflow-y-auto` and its own padding without losing the ARIA pairing — the seam mockifyr's five tabbed screens need to move off their local Radix tabs.
19 changes: 19 additions & 0 deletions src/components/TabStrip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,23 @@ describe("TabStrip", () => {
// meaning on its own.
expect(screen.getByRole("tab", { name: /Jobs/ })).toBeInTheDocument();
});

it("a panel carries its own layout classes when given, and the default padding when not", () => {
const { rerender } = render(
<TabPanel id="a" activeId="a" scope="s">
body
</TabPanel>,
);
expect(screen.getByRole("tabpanel")).toHaveClass("pt-4");
rerender(
<TabPanel id="a" activeId="a" scope="s" className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
body
</TabPanel>,
);
const panel = screen.getByRole("tabpanel");
expect(panel).toHaveClass("min-h-0", "flex-1", "overflow-y-auto");
expect(panel).not.toHaveClass("pt-4");
// The ARIA pairing survives the class swap.
expect(panel).toHaveAttribute("aria-labelledby", "tab-s-a");
});
});
11 changes: 9 additions & 2 deletions src/components/TabStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,23 @@ export interface TabPanelProps {
children: React.ReactNode;
/** Must match the strip's `scope` — the two halves pair through the prefixed ids (B7). */
scope?: string;
/**
* The panel's layout classes — a scrolling panel inside a flex column needs
* `min-h-0 flex-1 overflow-y-auto` and its own padding. Defaults to the strip's
* `pt-4`; passing a value REPLACES it (the ARIA pairing is untouched). Added for the
* family's second consumer, whose five tabbed screens are scroll layouts (2026-09-05).
*/
className?: string;
}

/** The panel half of the pattern — labelled by its tab, hidden when it is not the one. */
export function TabPanel({ id, activeId, children, scope }: TabPanelProps) {
export function TabPanel({ id, activeId, children, scope, className = "pt-4" }: TabPanelProps) {
if (id !== activeId) {
return null;
}

return (
<div role="tabpanel" id={`panel-${scope ? `${scope}-` : ""}${id}`} aria-labelledby={`tab-${scope ? `${scope}-` : ""}${id}`} tabIndex={0} className="pt-4">
<div role="tabpanel" id={`panel-${scope ? `${scope}-` : ""}${id}`} aria-labelledby={`tab-${scope ? `${scope}-` : ""}${id}`} tabIndex={0} className={className}>
{children}
</div>
);
Expand Down
Loading