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
28 changes: 28 additions & 0 deletions packages/react/src/UnderlineNav/UnderlineNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<UnderlineNav aria-label="Test">
<UnderlineNav.Item>
Tab Label
<span style={{position: 'absolute'}}>Hidden element</span>
</UnderlineNav.Item>
</UnderlineNav>,
)

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(
<UnderlineNav aria-label="Test">
<UnderlineNav.Item>Simple Text</UnderlineNav.Item>
</UnderlineNav>,
)

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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''
}
Comment on lines +19 to +28
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getTextContent function lacks test coverage for important edge cases. While tests verify basic functionality with strings and nested elements, additional test cases should be added for: arrays of mixed content (strings and elements), numeric children, and empty arrays. Consider adding a test that verifies mixed arrays like ['Text', <span>element</span>, 123] correctly extracts 'Text123'.

Copilot uses AI. Check for mistakes.

type UnderlineWrapperProps<As extends React.ElementType> = {
slot?: string
as?: As
Expand Down Expand Up @@ -59,11 +73,12 @@ export type UnderlineItemProps<As extends React.ElementType> = {

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 (
<Component {...rest} ref={ref} className={clsx(classes.UnderlineItem, className)}>
{iconsVisible && Icon && <span data-component="icon">{isElement(Icon) ? Icon : <Icon />}</span>}
{children && (
<span data-component="text" data-content={children}>
<span data-component="text" data-content={textContent || undefined}>
{children}
</span>
)}
Expand Down
Loading