Skip to content
9 changes: 9 additions & 0 deletions packages/components/src/navigation/NavTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ interface NavTabProps {
index: number;
isDraggable: boolean;
contextActions?: ResolvableContextAction | ResolvableContextAction[];
/**
* Optional render function to render content after the tab title.
*
* @param tab The tab to render content for
* @returns The content to render after the tab title
*/
renderAfterTabContent?: (tab: NavTabItem) => React.ReactNode;
}

const NavTab = memo(
Expand All @@ -30,6 +37,7 @@ const NavTab = memo(
index,
isDraggable,
contextActions,
renderAfterTabContent,
}: NavTabProps) => {
const { key, isClosable = onClose != null, title, icon } = tab;

Expand Down Expand Up @@ -98,6 +106,7 @@ const NavTab = memo(
{title}
<Tooltip>{title}</Tooltip>
</span>
{renderAfterTabContent?.(tab)}
{isClosable && (
<Button
kind="ghost"
Expand Down
42 changes: 42 additions & 0 deletions packages/components/src/navigation/NavTabList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import NavTabList, { type NavTabItem } from './NavTabList';

// Helper to build tabs
function makeTabs(count = 3): NavTabItem[] {
return Array.from({ length: count }, (_, i) => ({
key: `TAB_${i + 1}`,
title: `Tab ${i + 1}`,
isClosable: false,
}));
}

// JSDOM doesn't implement scrollIntoView; stub to avoid errors triggered by effect
window.HTMLElement.prototype.scrollIntoView = jest.fn();

describe('NavTabList renderAfterTabContent', () => {
it('renders content after tab title when renderAfterTabContent provided', async () => {
const tabs = makeTabs(3);
const user = userEvent.setup();
const onSelect = jest.fn();

render(
<NavTabList
activeKey={tabs[0].key}
tabs={tabs}
onSelect={onSelect}
renderAfterTabContent={tab => <span>{`${tab.title}-slot`}</span>}
/>
);

// Assert each tab's content is rendered
tabs.forEach(tab => {
expect(screen.getByText(`${tab.title}-slot`)).toBeInTheDocument();
});

// Selecting a tab still works with content present
await user.click(screen.getByText('Tab 2'));
expect(onSelect).toHaveBeenCalledWith('TAB_2');
});
});
11 changes: 11 additions & 0 deletions packages/components/src/navigation/NavTabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ type NavTabListProps<T extends NavTabItem = NavTabItem> = {
* @returns Additional context items for the tab
*/
makeContextActions?: (tab: T) => ContextAction | ContextAction[];

/**
* Optional render function to render content after each tab's title.
* Should be wrapped in useCallback to avoid unnecessary re-renders.
*
* @param tab The tab to render content for
* @returns The content to render after the tab title
*/
renderAfterTabContent?: (tab: T) => React.ReactNode;
};

function isScrolledLeft(element: HTMLElement): boolean {
Expand Down Expand Up @@ -181,6 +190,7 @@ function NavTabList({
onReorder,
onClose,
makeContextActions,
renderAfterTabContent,
}: NavTabListProps): React.ReactElement {
const containerRef = useRef<HTMLDivElement>();
const [isOverflowing, setIsOverflowing] = useState(true);
Expand Down Expand Up @@ -431,6 +441,7 @@ function NavTabList({
onClose={onClose}
isDraggable={onReorder != null}
contextActions={tabContextActionMap.get(key)}
renderAfterTabContent={renderAfterTabContent}
/>
);
});
Expand Down
Loading