Skip to content
3 changes: 3 additions & 0 deletions packages/components/src/navigation/NavTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface NavTabProps {
index: number;
isDraggable: boolean;
contextActions?: ResolvableContextAction | ResolvableContextAction[];
renderTabSlot?: (tab: NavTabItem) => React.ReactNode;
Comment thread
dsmmcken marked this conversation as resolved.
Outdated
}

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

Expand Down Expand Up @@ -98,6 +100,7 @@ const NavTab = memo(
{title}
<Tooltip>{title}</Tooltip>
</span>
{renderTabSlot?.(tab)}
{isClosable && (
<Button
kind="ghost"
Expand Down
59 changes: 59 additions & 0 deletions packages/components/src/navigation/NavTabList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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
beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window.HTMLElement as any).prototype.scrollIntoView = jest.fn();
});
Comment thread
dsmmcken marked this conversation as resolved.
Outdated

describe('NavTabList renderTabSlot', () => {
it('renders slot content for each tab when renderTabSlot provided', async () => {
const tabs = makeTabs(3);
const user = userEvent.setup();
const slotTestId = (key: string) => `slot-${key}`;

render(
<NavTabList
activeKey={tabs[0].key}
tabs={tabs}
onSelect={jest.fn()}
renderTabSlot={tab => (
<span data-testid={slotTestId(tab.key)}>{`${tab.title}-slot`}</span>
)}
/>
);

// Assert each tab's slot is rendered
tabs.forEach(tab => {
expect(screen.getByTestId(slotTestId(tab.key))).toHaveTextContent(
`${tab.title}-slot`
);
});
Comment thread
dsmmcken marked this conversation as resolved.
Outdated

// Basic interaction sanity: selecting a tab still works with slot present
await user.click(screen.getByTestId('btn-nav-tab-Tab 2'));
Comment thread
dsmmcken marked this conversation as resolved.
Outdated
});

it('does not render slot content when renderTabSlot is omitted', () => {
const tabs = makeTabs(2);
render(
<NavTabList activeKey={tabs[0].key} tabs={tabs} onSelect={jest.fn()} />
);

// Querying any potential slot test id should fail
const query = screen.queryByTestId('slot-TAB_1');
expect(query).toBeNull();
});
Comment thread
dsmmcken marked this conversation as resolved.
Outdated
});
12 changes: 12 additions & 0 deletions packages/components/src/navigation/NavTabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ type NavTabListProps<T extends NavTabItem = NavTabItem> = {
* @returns Additional context items for the tab
*/
makeContextActions?: (tab: T) => ContextAction | ContextAction[];

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

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