diff --git a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx index 6b35a2c739b..c5b74955d1d 100644 --- a/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx +++ b/packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx @@ -14,6 +14,7 @@ import { import type {Meta} from '@storybook/react-vite' import {UnderlineNav} from './index' import {INITIAL_VIEWPORTS} from 'storybook/viewport' +import Popover from '../Popover' const meta = { title: 'Components/UnderlineNav/Features', @@ -154,3 +155,35 @@ export const VariantFlush = () => { ) } + +export const WithPopover = () => { + return ( + + }> + Code + + }> + Issues + + } counter={12}> + Security + + Popover content + + + }> + Insights + + }> + Settings + + + ) +} diff --git a/packages/react/src/UnderlineNav/UnderlineNav.test.tsx b/packages/react/src/UnderlineNav/UnderlineNav.test.tsx index 7b4c2dd8497..6039ec2ad8a 100644 --- a/packages/react/src/UnderlineNav/UnderlineNav.test.tsx +++ b/packages/react/src/UnderlineNav/UnderlineNav.test.tsx @@ -210,6 +210,34 @@ describe('UnderlineNav', () => { expect(screen.getByTestId('jsx-element')).toBeInTheDocument() expect(screen.getByTestId('functional-component')).toBeInTheDocument() }) + + it('extracts only direct text content for data-content attribute, ignoring nested elements', () => { + render( + + + Tab Label + Hidden element + + , + ) + + const item = screen.getByRole('link', {name: /Tab Label/}) + const textSpan = item.querySelector('[data-component="text"]') + // data-content should only have the content of the Text and not the nested span + expect(textSpan).toHaveAttribute('data-content', 'Tab Label') + }) + + it('handles string children correctly for data-content attribute', () => { + render( + + Simple Text + , + ) + + const item = screen.getByRole('link', {name: 'Simple Text'}) + const textSpan = item.querySelector('[data-component="text"]') + expect(textSpan).toHaveAttribute('data-content', 'Simple Text') + }) }) describe('Keyboard Navigation', () => { diff --git a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx index a6fa4bee785..509459ea47f 100644 --- a/packages/react/src/internal/components/UnderlineTabbedInterface.tsx +++ b/packages/react/src/internal/components/UnderlineTabbedInterface.tsx @@ -13,6 +13,20 @@ import {clsx} from 'clsx' // The gap between the list items. It is a constant because the gap is used to calculate the possible number of items that can fit in the container. export const GAP = 8 +// Helper to extract direct text content from children for the data-content attribute. +// This is used by CSS to reserve space for bold text (preventing layout shift). +// Only extracts strings/numbers, not text from nested React elements (e.g., Popovers). +function getTextContent(children: React.ReactNode): string { + if (typeof children === 'string' || typeof children === 'number') { + return String(children) + } + if (Array.isArray(children)) { + return children.map(getTextContent).join('') + } + // Skip React elements - we only want direct text content, not text from nested components + return '' +} + type UnderlineWrapperProps = { slot?: string as?: As @@ -59,11 +73,12 @@ export type UnderlineItemProps = { export const UnderlineItem = React.forwardRef((props, ref) => { const {as: Component = 'a', children, counter, icon: Icon, iconsVisible, loadingCounters, className, ...rest} = props + const textContent = getTextContent(children) return ( {iconsVisible && Icon && {isElement(Icon) ? Icon : }} {children && ( - + {children} )}