Skip to content
Closed
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 packages/components/src/components/navigation-item/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export type DBNavigationItemDefaultProps = {
* This is for mobile navigation only, if it is set the sub-navigation is a static overlay
*/
subNavigationExpanded?: boolean | string;

/**
* ID for the sub-navigation element. If not provided, a deterministic ID will be generated to ensure SSR compatibility.
*/
subNavigationId?: string;
};

export type DBNavigationItemProps = DBNavigationItemDefaultProps &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export default function DBNavigationItem(props: DBNavigationItemProps) {
hasSubNavigation: true,
isSubNavigationExpanded: false,
autoClose: false,
subNavigationId: 'sub-navigation-' + uuid(),
// Use deterministic ID generation for SSR compatibility:
// 1. Prefer explicit subNavigationId prop
// 2. Fallback to component id + suffix
// 3. Default to fixed string (instead of random UUID)
subNavigationId: props.subNavigationId ?? (props.id ? `${props.id}-sub-navigation` : 'sub-navigation'),
Copy link
Contributor

Choose a reason for hiding this comment

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

The fallback id="sub-navigation" can collide when multiple items don’t provide id/subNavigationId.

navigationItemSafeTriangle: undefined,
handleNavigationItemClick: (event: any) => {
if (event?.target?.nodeName === 'A') {
Expand Down Expand Up @@ -68,6 +72,14 @@ export default function DBNavigationItem(props: DBNavigationItemProps) {
}
}, [props.subNavigationExpanded]);

onUpdate(() => {
// Update subNavigationId if props change
const newSubNavigationId = props.subNavigationId ?? (props.id ? `${props.id}-sub-navigation` : 'sub-navigation');
if (state.subNavigationId !== newSubNavigationId) {
state.subNavigationId = newSubNavigationId;
}
}, [props.subNavigationId, props.id]);

onUpdate(() => {
if (state.initialized && _ref) {
const subNavigationSlot = _ref.querySelector('menu');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ const testComponent = () => {
const component = await mount(comp);
await expect(component).toHaveScreenshot();
});

test('should use deterministic subNavigationId when id prop is provided', async ({ mount }) => {
const componentWithId = (
<DBNavigationItem id="test-nav-item">
<a href="#">Test</a>
</DBNavigationItem>
);
const component = await mount(componentWithId);
const subNav = component.locator('menu[id="test-nav-item-sub-navigation"]');
await expect(subNav).toBeAttached();
});

test('should use provided subNavigationId when specified', async ({ mount }) => {
const componentWithSubNavId = (
<DBNavigationItem subNavigationId="custom-sub-nav-id">
<a href="#">Test</a>
</DBNavigationItem>
);
const component = await mount(componentWithSubNavId);
const subNav = component.locator('menu[id="custom-sub-nav-id"]');
await expect(subNav).toBeAttached();
});

test('should use fallback subNavigationId when no id or subNavigationId provided', async ({ mount }) => {
const componentWithoutId = (
<DBNavigationItem>
<a href="#">Test</a>
</DBNavigationItem>
);
const component = await mount(componentWithoutId);
const subNav = component.locator('menu[id="sub-navigation"]');
await expect(subNav).toBeAttached();
});
};
const testA11y = () => {
test('should have same aria-snapshot', async ({ mount }, testInfo) => {
Expand Down
Loading