diff --git a/packages/components/src/components/navigation-item/model.ts b/packages/components/src/components/navigation-item/model.ts index 807e78df61fd..bdbe8938a970 100644 --- a/packages/components/src/components/navigation-item/model.ts +++ b/packages/components/src/components/navigation-item/model.ts @@ -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 & diff --git a/packages/components/src/components/navigation-item/navigation-item.lite.tsx b/packages/components/src/components/navigation-item/navigation-item.lite.tsx index af2f46b956da..ba3823a90792 100644 --- a/packages/components/src/components/navigation-item/navigation-item.lite.tsx +++ b/packages/components/src/components/navigation-item/navigation-item.lite.tsx @@ -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'), navigationItemSafeTriangle: undefined, handleNavigationItemClick: (event: any) => { if (event?.target?.nodeName === 'A') { @@ -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'); diff --git a/packages/components/src/components/navigation-item/navigation-item.spec.tsx b/packages/components/src/components/navigation-item/navigation-item.spec.tsx index aa055f6718d4..4fe216a2e67c 100644 --- a/packages/components/src/components/navigation-item/navigation-item.spec.tsx +++ b/packages/components/src/components/navigation-item/navigation-item.spec.tsx @@ -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 = ( + + Test + + ); + 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 = ( + + Test + + ); + 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 = ( + + Test + + ); + 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) => {