From a39f9e35eb0bc1b757b7761b03ae0ad31664c0bd Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Tue, 9 Jun 2026 13:21:29 +0200 Subject: [PATCH 1/8] feat: add render content prop to top navigation --- src/test-utils/dom/top-navigation/index.ts | 4 + .../top-navigation-render-content.test.ts | 37 ++++++ .../top-navigation-render-content.test.tsx | 105 +++++++++++++++++ src/top-navigation/interfaces.ts | 18 ++- src/top-navigation/internal.tsx | 109 +++++++++++------- src/top-navigation/styles.scss | 4 + src/top-navigation/use-top-navigation.ts | 2 +- 7 files changed, 238 insertions(+), 41 deletions(-) create mode 100644 src/top-navigation/__integ__/top-navigation-render-content.test.ts create mode 100644 src/top-navigation/__tests__/top-navigation-render-content.test.tsx diff --git a/src/test-utils/dom/top-navigation/index.ts b/src/test-utils/dom/top-navigation/index.ts index 0b9e955047..89d4d0ac33 100644 --- a/src/test-utils/dom/top-navigation/index.ts +++ b/src/test-utils/dom/top-navigation/index.ts @@ -13,6 +13,10 @@ import styles from '../../../top-navigation/styles.selectors.js'; export default class TopNavigationWrapper extends ComponentWrapper { static rootSelector = `${styles['top-navigation']}:not(.${styles.hidden})`; + findContent(): ElementWrapper | null { + return this.find(`.${styles['custom-content']}`); + } + findIdentityLink(): ElementWrapper { return this.find(`.${styles.identity} a`)!; } diff --git a/src/top-navigation/__integ__/top-navigation-render-content.test.ts b/src/top-navigation/__integ__/top-navigation-render-content.test.ts new file mode 100644 index 0000000000..aaa0f030e6 --- /dev/null +++ b/src/top-navigation/__integ__/top-navigation-render-content.test.ts @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; +import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; +import { createWrapper } from '@cloudscape-design/test-utils-core/selectors'; + +import '../../../lib/components/test-utils/selectors'; + +const wrapper = createWrapper().findTopNavigation(); + +class RenderContentPage extends BasePageObject {} + +const setupTest = (testFn: (page: RenderContentPage) => Promise) => { + return useBrowser(async browser => { + await browser.url('#/light/top-navigation/render-content'); + const page = new RenderContentPage(browser); + await page.waitForVisible(wrapper.toSelector()); + await testFn(page); + }); +}; + +describe('TopNavigation renderContent', () => { + test( + 'renders custom content in the first top navigation', + setupTest(async page => { + await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('My Service'); + }) + ); + + test( + 'renders inside a header element', + setupTest(async page => { + const headerExists = await page.isExisting(`${wrapper.toSelector()} header`); + expect(headerExists).toBe(true); + }) + ); +}); diff --git a/src/top-navigation/__tests__/top-navigation-render-content.test.tsx b/src/top-navigation/__tests__/top-navigation-render-content.test.tsx new file mode 100644 index 0000000000..4b15c1d264 --- /dev/null +++ b/src/top-navigation/__tests__/top-navigation-render-content.test.tsx @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import createWrapper from '../../../lib/components/test-utils/dom'; +import TopNavigation, { TopNavigationProps } from '../../../lib/components/top-navigation'; + +const I18N_STRINGS: TopNavigationProps.I18nStrings = { + searchIconAriaLabel: 'Search', + searchDismissIconAriaLabel: 'Close search', + overflowMenuTriggerText: 'More', + overflowMenuTitleText: 'All', + overflowMenuBackIconAriaLabel: 'Back', + overflowMenuDismissIconAriaLabel: 'Close', +}; + +const renderTopNavigation = (props: TopNavigationProps) => { + const { container } = render(); + return createWrapper(container).findTopNavigation()!; +}; + +describe('renderContent', () => { + test('renders custom content when renderContent is provided', () => { + const wrapper = renderTopNavigation({ + renderContent: () =>
Custom Nav
, + }); + expect(wrapper.findContent()).not.toBeNull(); + expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Nav'); + }); + + test('does not render identity when renderContent is provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Should Not Render' }, + renderContent: () =>
Custom
, + }); + expect(wrapper.findTitle()).toBeNull(); + expect(wrapper.findIdentityLink()).toBeNull(); + }); + + test('does not render utilities when renderContent is provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Title' }, + utilities: [{ type: 'button', text: 'Help' }], + renderContent: () =>
Custom
, + }); + expect(wrapper.findUtilities()).toHaveLength(0); + }); + + test('does not render search when renderContent is provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Title' }, + search: , + renderContent: () =>
Custom
, + }); + expect(wrapper.findSearch()).toBeNull(); + }); + + test('renders structured mode when renderContent is not provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Structured' }, + }); + expect(wrapper.findContent()).toBeNull(); + expect(wrapper.findTitle()!.getElement()).toHaveTextContent('Structured'); + }); + + test('identity is optional when renderContent is provided', () => { + const wrapper = renderTopNavigation({ + renderContent: () =>
No identity needed
, + }); + expect(wrapper.findContent()!.getElement()).toHaveTextContent('No identity needed'); + }); + + test('renders inside a header element', () => { + const { container } = render(
Custom
} />); + expect(container.querySelector('header')).not.toBeNull(); + }); +}); + +describe('visualContext', () => { + test('defaults to top-navigation (dark visual context)', () => { + const { container } = render(); + // VisualContext applies awsui-context-top-navigation class + expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); + }); + + test('applies visual context when visualContext is "top-navigation"', () => { + const { container } = render( +
Custom
} /> + ); + expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); + }); + + test('does not apply visual context when visualContext is "none"', () => { + const { container } = render(
Custom
} />); + expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull(); + }); + + test('visualContext="none" works with structured mode', () => { + const { container } = render( + + ); + expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull(); + }); +}); diff --git a/src/top-navigation/interfaces.ts b/src/top-navigation/interfaces.ts index 3bad629d32..de783d75ac 100644 --- a/src/top-navigation/interfaces.ts +++ b/src/top-navigation/interfaces.ts @@ -15,7 +15,21 @@ export interface TopNavigationProps extends BaseComponentProps { * * `href` (string) - Specifies the `href` that the header links to. * * `onFollow` (() => void) - Specifies the event handler called when the identity is clicked without any modifier keys. */ - identity: TopNavigationProps.Identity; + identity?: TopNavigationProps.Identity; + + /** + * Specifies a render function for custom navigation content. + * When provided, replaces all structured content (identity, search, utilities are ignored). + */ + renderContent?: () => React.ReactNode; + + /** + * Controls the color scheme of the navigation bar and its contents. + * - "top-navigation": Applies the top-navigation visual context. The component and its contents use dark, branded colors in both light and dark mode. + * - "none": No visual context. The component and its contents use the same colors as the rest of the page. + * @default "top-navigation" + */ + visualContext?: TopNavigationProps.VisualContext; /** * Use with an input or autosuggest control for a global search query. @@ -126,4 +140,6 @@ export namespace TopNavigationProps { overflowMenuTriggerText?: string; overflowMenuTitleText?: string; } + + export type VisualContext = 'top-navigation' | 'none'; } diff --git a/src/top-navigation/internal.tsx b/src/top-navigation/internal.tsx index eea5af6761..759fc2e17e 100644 --- a/src/top-navigation/internal.tsx +++ b/src/top-navigation/internal.tsx @@ -27,12 +27,17 @@ export default function InternalTopNavigation({ i18nStrings, utilities, search, + renderContent, + visualContext = 'top-navigation', ...restProps }: InternalTopNavigationProps) { - checkSafeUrl('TopNavigation', identity.href); + if (identity) { + checkSafeUrl('TopNavigation', identity.href); + } const baseProps = getBaseProps(restProps); + const { mainRef, virtualRef, breakpoint, responsiveState, isSearchExpanded, onSearchUtilityClick } = useTopNavigation( - { identity, search, utilities } + { identity: identity ?? { href: '' }, search, utilities } ); const [overflowMenuOpen, setOverflowMenuOpen] = useState(false); const overflowMenuTriggerRef = useRef(null); @@ -41,16 +46,6 @@ export default function InternalTopNavigation({ const isLargeViewport = breakpoint === 's'; const i18n = useInternalI18n('top-navigation'); - const onIdentityClick = (event: React.MouseEvent) => { - if (isPlainLeftClick(event)) { - fireCancelableEvent(identity.onFollow, {}, event); - } - }; - - const toggleOverflowMenu = () => { - setOverflowMenuOpen(overflowMenuOpen => !overflowMenuOpen); - }; - const menuTriggerVisible = !isSearchExpanded && responsiveState.hideUtilities; useEffect(() => { @@ -63,12 +58,40 @@ export default function InternalTopNavigation({ } }, [overflowMenuOpen]); + // Custom content mode: render a simple container with no structured layout + if (renderContent !== undefined) { + const header = ( +
+
{renderContent()}
+
+ ); + return ( +
+ {visualContext === 'top-navigation' ? ( + {header} + ) : ( + header + )} +
+ ); + } + + const onIdentityClick = (event: React.MouseEvent) => { + if (isPlainLeftClick(event)) { + fireCancelableEvent(identity?.onFollow, {}, event); + } + }; + + const toggleOverflowMenu = () => { + setOverflowMenuOpen(overflowMenuOpen => !overflowMenuOpen); + }; + // Render the top nav twice; once as the top nav that users can see, and another // "virtual" top nav used just for calculations. The virtual top nav doesn't react to // layout changes and renders two sets of utilities: one with labels and one without. const content = (isVirtual: boolean) => { const Wrapper = isVirtual ? 'div' : 'header'; - const showIdentity = isVirtual || !isSearchExpanded; + const showIdentity = !!identity && (isVirtual || !isSearchExpanded); const showTitle = isVirtual || !responsiveState.hideTitle; const showSearchSlot = search && (isVirtual || !responsiveState.hideSearch || isSearchExpanded); const showSearchUtility = isVirtual || (search && responsiveState.hideSearch); @@ -88,9 +111,9 @@ export default function InternalTopNavigation({ >
{showIdentity && ( -
- - {identity.logo && ( + )} @@ -222,30 +245,38 @@ export default function InternalTopNavigation({ ); }; + const structuredContent = ( + <> + {/* Render virtual content first to ensure React refs for content will be assigned on the actual nodes. */} + {content(true)} + + {content(false)} + + {menuTriggerVisible && overflowMenuOpen && ( +
+ + (!responsiveState.hideUtilities || responsiveState.hideUtilities.indexOf(i) !== -1) && + !utility.disableUtilityCollapse + )} + onClose={toggleOverflowMenu} + /> +
+ )} + + ); + return (
- - {/* Render virtual content first to ensure React refs for content will be assigned on the actual nodes. */} - {content(true)} - - {content(false)} - - {menuTriggerVisible && overflowMenuOpen && ( -
- - (!responsiveState.hideUtilities || responsiveState.hideUtilities.indexOf(i) !== -1) && - !utility.disableUtilityCollapse - )} - onClose={toggleOverflowMenu} - /> -
- )} -
+ {visualContext === 'top-navigation' ? ( + {structuredContent} + ) : ( + structuredContent + )}
); } diff --git a/src/top-navigation/styles.scss b/src/top-navigation/styles.scss index 6167698a8a..ebd06cb915 100644 --- a/src/top-navigation/styles.scss +++ b/src/top-navigation/styles.scss @@ -42,6 +42,10 @@ inline-size: 9000px; } +.custom-content { + /* used in test-utils */ +} + .hidden { @include styles.awsui-util-hide; visibility: hidden; diff --git a/src/top-navigation/use-top-navigation.ts b/src/top-navigation/use-top-navigation.ts index 9c922fd200..eeaa5cf6a0 100644 --- a/src/top-navigation/use-top-navigation.ts +++ b/src/top-navigation/use-top-navigation.ts @@ -59,7 +59,7 @@ export function useTopNavigation({ identity, search, utilities }: UseTopNavigati // The component works by calculating the possible resize states that it can // be in, and having a state variable to track which state we're currently in. const hasSearch = !!search; - const hasTitleWithLogo = identity && !!identity.logo && !!identity.title; + const hasTitleWithLogo = !!identity && !!identity.logo && !!identity.title; const responsiveStates = useMemo>(() => { return generateResponsiveStateKeys(utilities, hasSearch, hasTitleWithLogo); }, [utilities, hasSearch, hasTitleWithLogo]); From 85bf5b57134fa5ac3099a87c91786412e48dabb1 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Tue, 9 Jun 2026 13:21:53 +0200 Subject: [PATCH 2/8] test: add render content test page --- pages/top-navigation/render-content.page.tsx | 82 ++++++++++++++++++++ pages/top-navigation/render-content.scss | 52 +++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 pages/top-navigation/render-content.page.tsx create mode 100644 pages/top-navigation/render-content.scss diff --git a/pages/top-navigation/render-content.page.tsx b/pages/top-navigation/render-content.page.tsx new file mode 100644 index 0000000000..495c447eca --- /dev/null +++ b/pages/top-navigation/render-content.page.tsx @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Button from '~components/button'; +import Input from '~components/input'; +import Link from '~components/link'; +import TopNavigation from '~components/top-navigation'; + +import ScreenshotArea from '../utils/screenshot-area'; +import { I18N_STRINGS } from './common'; +import logo from './logos/simple-logo.svg'; + +import styles from './render-content.scss'; + +function CustomNav({ searchValue, onSearchChange }: { searchValue: string; onSearchChange: (value: string) => void }) { + return ( +
+ Service + My Service +
+ onSearchChange(detail.value)} + ariaLabel="Search" + /> +
+ +
+
+
+ ); +} + +export default function RenderContentPage() { + const [searchValue, setSearchValue] = useState(''); + + return ( +
+

TopNavigation renderContent

+ +

Custom content (default visual context)

+ } /> + +
+ +

Custom content (visualContext="none")

+ } + /> + +
+ +

Structured mode (unchanged)

+ +
+
+ ); +} diff --git a/pages/top-navigation/render-content.scss b/pages/top-navigation/render-content.scss new file mode 100644 index 0000000000..7fef66697a --- /dev/null +++ b/pages/top-navigation/render-content.scss @@ -0,0 +1,52 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +.nav { + container-type: inline-size; + display: flex; + align-items: center; + padding-inline: 24px; + inline-size: 100%; + block-size: 48px; + box-sizing: border-box; + gap: 16px; +} + +.title { + font-weight: 700; +} + +.search { + flex: 1; + max-inline-size: 320px; +} + +.links { + display: flex; + gap: 16px; + margin-inline-start: auto; +} + +.menu-trigger { + display: none; + margin-inline-start: auto; +} + +@container (max-width: 700px) { + .links { + display: none; + } + + .menu-trigger { + display: flex; + } +} + +@container (max-width: 500px) { + .search, + .title { + display: none; + } +} From 4ccbc5ac74bda8f3a36782815e1091950d03199f Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Thu, 11 Jun 2026 09:50:30 +0200 Subject: [PATCH 3/8] fix: update the renderContent to slot --- ...ntent.page.tsx => custom-content.page.tsx} | 66 ++++++++++------- pages/top-navigation/render-content.scss | 52 ------------- .../__snapshots__/documenter.test.ts.snap | 47 +++++++++++- .../test-utils-selectors.test.tsx.snap | 1 + .../top-navigation-render-content.test.ts | 37 ---------- .../__integ__/top-navigation.test.ts | 18 +++++ ...=> top-navigation-custom-content.test.tsx} | 74 ++++++++----------- src/top-navigation/interfaces.ts | 4 +- src/top-navigation/internal.tsx | 6 +- 9 files changed, 139 insertions(+), 166 deletions(-) rename pages/top-navigation/{render-content.page.tsx => custom-content.page.tsx} (53%) delete mode 100644 pages/top-navigation/render-content.scss delete mode 100644 src/top-navigation/__integ__/top-navigation-render-content.test.ts rename src/top-navigation/__tests__/{top-navigation-render-content.test.tsx => top-navigation-custom-content.test.tsx} (51%) diff --git a/pages/top-navigation/render-content.page.tsx b/pages/top-navigation/custom-content.page.tsx similarity index 53% rename from pages/top-navigation/render-content.page.tsx rename to pages/top-navigation/custom-content.page.tsx index 495c447eca..00e91a4a30 100644 --- a/pages/top-navigation/render-content.page.tsx +++ b/pages/top-navigation/custom-content.page.tsx @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; +import { useContainerQuery } from '@cloudscape-design/component-toolkit'; + import Button from '~components/button'; import Input from '~components/input'; import Link from '~components/link'; @@ -11,50 +13,60 @@ import ScreenshotArea from '../utils/screenshot-area'; import { I18N_STRINGS } from './common'; import logo from './logos/simple-logo.svg'; -import styles from './render-content.scss'; - function CustomNav({ searchValue, onSearchChange }: { searchValue: string; onSearchChange: (value: string) => void }) { + const [width, ref] = useContainerQuery(entry => entry.contentBoxWidth, []); + const isNarrow = (width ?? 1000) < 700; + return ( -
+
Service - My Service -
- onSearchChange(detail.value)} - ariaLabel="Search" - /> -
- -
-
+ {!isNarrow && My Service} + {!isNarrow && ( +
+ onSearchChange(detail.value)} + ariaLabel="Search" + /> +
+ )} + {!isNarrow ? ( + + ) : ( +
+
+ )}
); } -export default function RenderContentPage() { +export default function CustomContentPage() { const [searchValue, setSearchValue] = useState(''); return (
-

TopNavigation renderContent

+

TopNavigation children

Custom content (default visual context)

- } /> + + +

Custom content (visualContext="none")

- } - /> + + +
diff --git a/pages/top-navigation/render-content.scss b/pages/top-navigation/render-content.scss deleted file mode 100644 index 7fef66697a..0000000000 --- a/pages/top-navigation/render-content.scss +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -.nav { - container-type: inline-size; - display: flex; - align-items: center; - padding-inline: 24px; - inline-size: 100%; - block-size: 48px; - box-sizing: border-box; - gap: 16px; -} - -.title { - font-weight: 700; -} - -.search { - flex: 1; - max-inline-size: 320px; -} - -.links { - display: flex; - gap: 16px; - margin-inline-start: auto; -} - -.menu-trigger { - display: none; - margin-inline-start: auto; -} - -@container (max-width: 700px) { - .links { - display: none; - } - - .menu-trigger { - display: flex; - } -} - -@container (max-width: 500px) { - .search, - .title { - display: none; - } -} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index a327e6590a..f00539c199 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -32432,7 +32432,7 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "type": "object", }, "name": "identity", - "optional": false, + "optional": true, "type": "TopNavigationProps.Identity", }, { @@ -32476,8 +32476,30 @@ The following properties are supported across all utility types: "optional": true, "type": "ReadonlyArray", }, + { + "description": "Controls the color scheme of the navigation bar and its contents. +- "top-navigation": Applies the top-navigation visual context. The component and its contents use dark, branded colors in both light and dark mode. +- "none": No visual context. The component and its contents use the same colors as the rest of the page.", + "inlineType": { + "name": "TopNavigationProps.VisualContext", + "type": "union", + "values": [ + "none", + "top-navigation", + ], + }, + "name": "visualContext", + "optional": true, + "type": "string", + }, ], "regions": [ + { + "description": "Specifies custom navigation content. +When provided, replaces all structured content (identity, search, utilities are ignored).", + "isDefault": false, + "name": "customContent", + }, { "description": "Use with an input or autosuggest control for a global search query.", "isDefault": false, @@ -44801,11 +44823,24 @@ Searches within this tooltip's scope to avoid conflicts with popovers.", }, { "methods": [ + { + "name": "findContent", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "name": "findIdentityLink", "parameters": [], "returnType": { - "isNullable": false, + "isNullable": true, "name": "ElementWrapper", "typeArguments": [ { @@ -53748,6 +53783,14 @@ Searches within this tooltip's scope to avoid conflicts with popovers.", }, { "methods": [ + { + "name": "findContent", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "name": "findIdentityLink", "parameters": [], diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 90d771415e..330dfbc9fd 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -722,6 +722,7 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1u26h", ], "top-navigation": [ + "awsui_custom-content_k5dlb", "awsui_hidden_k5dlb", "awsui_identity_k5dlb", "awsui_logo_k5dlb", diff --git a/src/top-navigation/__integ__/top-navigation-render-content.test.ts b/src/top-navigation/__integ__/top-navigation-render-content.test.ts deleted file mode 100644 index aaa0f030e6..0000000000 --- a/src/top-navigation/__integ__/top-navigation-render-content.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; -import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; -import { createWrapper } from '@cloudscape-design/test-utils-core/selectors'; - -import '../../../lib/components/test-utils/selectors'; - -const wrapper = createWrapper().findTopNavigation(); - -class RenderContentPage extends BasePageObject {} - -const setupTest = (testFn: (page: RenderContentPage) => Promise) => { - return useBrowser(async browser => { - await browser.url('#/light/top-navigation/render-content'); - const page = new RenderContentPage(browser); - await page.waitForVisible(wrapper.toSelector()); - await testFn(page); - }); -}; - -describe('TopNavigation renderContent', () => { - test( - 'renders custom content in the first top navigation', - setupTest(async page => { - await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('My Service'); - }) - ); - - test( - 'renders inside a header element', - setupTest(async page => { - const headerExists = await page.isExisting(`${wrapper.toSelector()} header`); - expect(headerExists).toBe(true); - }) - ); -}); diff --git a/src/top-navigation/__integ__/top-navigation.test.ts b/src/top-navigation/__integ__/top-navigation.test.ts index c59aeb5087..b9aba2a335 100644 --- a/src/top-navigation/__integ__/top-navigation.test.ts +++ b/src/top-navigation/__integ__/top-navigation.test.ts @@ -233,3 +233,21 @@ describe('Top navigation', () => { }) ); }); + +describe('Top navigation - children (custom content)', () => { + const setupCustomContentTest = (testFn: (page: TopNavigationPage) => Promise) => { + return useBrowser(async browser => { + await browser.url('#/light/top-navigation/custom-content'); + const page = new TopNavigationPage(browser); + await page.waitForVisible(wrapper.toSelector()); + await testFn(page); + }); + }; + + test( + 'renders custom content', + setupCustomContentTest(async page => { + await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('My Service'); + }) + ); +}); diff --git a/src/top-navigation/__tests__/top-navigation-render-content.test.tsx b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx similarity index 51% rename from src/top-navigation/__tests__/top-navigation-render-content.test.tsx rename to src/top-navigation/__tests__/top-navigation-custom-content.test.tsx index 4b15c1d264..9d151affd2 100644 --- a/src/top-navigation/__tests__/top-navigation-render-content.test.tsx +++ b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx @@ -15,84 +15,72 @@ const I18N_STRINGS: TopNavigationProps.I18nStrings = { overflowMenuDismissIconAriaLabel: 'Close', }; -const renderTopNavigation = (props: TopNavigationProps) => { - const { container } = render(); +const renderTopNavigation = (props: TopNavigationProps, children?: React.ReactNode) => { + const { container } = render( + + {children} + + ); return createWrapper(container).findTopNavigation()!; }; -describe('renderContent', () => { - test('renders custom content when renderContent is provided', () => { - const wrapper = renderTopNavigation({ - renderContent: () =>
Custom Nav
, - }); +describe('children', () => { + test('renders custom content when children are provided', () => { + const wrapper = renderTopNavigation({},
Custom Nav
); expect(wrapper.findContent()).not.toBeNull(); expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Nav'); }); - test('does not render identity when renderContent is provided', () => { - const wrapper = renderTopNavigation({ - identity: { href: '#', title: 'Should Not Render' }, - renderContent: () =>
Custom
, - }); + test('does not render identity when children are provided', () => { + const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Should Not Render' } },
Custom
); expect(wrapper.findTitle()).toBeNull(); expect(wrapper.findIdentityLink()).toBeNull(); }); - test('does not render utilities when renderContent is provided', () => { - const wrapper = renderTopNavigation({ - identity: { href: '#', title: 'Title' }, - utilities: [{ type: 'button', text: 'Help' }], - renderContent: () =>
Custom
, - }); + test('does not render utilities when children are provided', () => { + const wrapper = renderTopNavigation( + { identity: { href: '#', title: 'Title' }, utilities: [{ type: 'button', text: 'Help' }] }, +
Custom
+ ); expect(wrapper.findUtilities()).toHaveLength(0); }); - test('does not render search when renderContent is provided', () => { - const wrapper = renderTopNavigation({ - identity: { href: '#', title: 'Title' }, - search: , - renderContent: () =>
Custom
, - }); + test('does not render search when children are provided', () => { + const wrapper = renderTopNavigation( + { identity: { href: '#', title: 'Title' }, search: }, +
Custom
+ ); expect(wrapper.findSearch()).toBeNull(); }); - test('renders structured mode when renderContent is not provided', () => { - const wrapper = renderTopNavigation({ - identity: { href: '#', title: 'Structured' }, - }); + test('renders structured mode when children are not provided', () => { + const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Structured' } }); expect(wrapper.findContent()).toBeNull(); expect(wrapper.findTitle()!.getElement()).toHaveTextContent('Structured'); }); - - test('identity is optional when renderContent is provided', () => { - const wrapper = renderTopNavigation({ - renderContent: () =>
No identity needed
, - }); - expect(wrapper.findContent()!.getElement()).toHaveTextContent('No identity needed'); - }); - - test('renders inside a header element', () => { - const { container } = render(
Custom
} />); - expect(container.querySelector('header')).not.toBeNull(); - }); }); describe('visualContext', () => { test('defaults to top-navigation (dark visual context)', () => { const { container } = render(); - // VisualContext applies awsui-context-top-navigation class expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); }); test('applies visual context when visualContext is "top-navigation"', () => { const { container } = render( -
Custom
} /> + +
Custom
+
); expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); }); test('does not apply visual context when visualContext is "none"', () => { - const { container } = render(
Custom
} />); + const { container } = render( + +
Custom
+
+ ); expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull(); }); diff --git a/src/top-navigation/interfaces.ts b/src/top-navigation/interfaces.ts index de783d75ac..0e3b67954f 100644 --- a/src/top-navigation/interfaces.ts +++ b/src/top-navigation/interfaces.ts @@ -18,10 +18,10 @@ export interface TopNavigationProps extends BaseComponentProps { identity?: TopNavigationProps.Identity; /** - * Specifies a render function for custom navigation content. + * Specifies custom navigation content. * When provided, replaces all structured content (identity, search, utilities are ignored). */ - renderContent?: () => React.ReactNode; + children?: React.ReactNode; /** * Controls the color scheme of the navigation bar and its contents. diff --git a/src/top-navigation/internal.tsx b/src/top-navigation/internal.tsx index 759fc2e17e..b8431f5ed2 100644 --- a/src/top-navigation/internal.tsx +++ b/src/top-navigation/internal.tsx @@ -27,7 +27,7 @@ export default function InternalTopNavigation({ i18nStrings, utilities, search, - renderContent, + children, visualContext = 'top-navigation', ...restProps }: InternalTopNavigationProps) { @@ -59,10 +59,10 @@ export default function InternalTopNavigation({ }, [overflowMenuOpen]); // Custom content mode: render a simple container with no structured layout - if (renderContent !== undefined) { + if (children !== undefined) { const header = (
-
{renderContent()}
+
{children}
); return ( From 727266df964213ca52bbea1e2b1419e4500d74ff Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Thu, 11 Jun 2026 10:47:08 +0200 Subject: [PATCH 4/8] chore: add comment for findIdentityLink --- src/test-utils/dom/top-navigation/index.ts | 2 ++ .../__tests__/top-navigation-custom-content.test.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/src/test-utils/dom/top-navigation/index.ts b/src/test-utils/dom/top-navigation/index.ts index 89d4d0ac33..a88694345a 100644 --- a/src/test-utils/dom/top-navigation/index.ts +++ b/src/test-utils/dom/top-navigation/index.ts @@ -17,6 +17,8 @@ export default class TopNavigationWrapper extends ComponentWrapper { return this.find(`.${styles['custom-content']}`); } + // Returns non-null for backwards compatibility with existing consumers. + // Returns null at runtime when children (custom content) are provided. findIdentityLink(): ElementWrapper { return this.find(`.${styles.identity} a`)!; } diff --git a/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx index 9d151affd2..cda958bd43 100644 --- a/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx +++ b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx @@ -34,6 +34,7 @@ describe('children', () => { test('does not render identity when children are provided', () => { const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Should Not Render' } },
Custom
); expect(wrapper.findTitle()).toBeNull(); + // findIdentityLink() type is non-null for backwards compatibility, but returns null at runtime here. expect(wrapper.findIdentityLink()).toBeNull(); }); From 68d3e787d514dacac18b592f38a1aedc7bad3ba2 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Thu, 11 Jun 2026 10:51:06 +0200 Subject: [PATCH 5/8] fix: update snapshot and style for test page custom content --- pages/top-navigation/custom-content.page.tsx | 5 +---- .../snapshot-tests/__snapshots__/documenter.test.ts.snap | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pages/top-navigation/custom-content.page.tsx b/pages/top-navigation/custom-content.page.tsx index 00e91a4a30..b10b5898b1 100644 --- a/pages/top-navigation/custom-content.page.tsx +++ b/pages/top-navigation/custom-content.page.tsx @@ -18,10 +18,7 @@ function CustomNav({ searchValue, onSearchChange }: { searchValue: string; onSea const isNarrow = (width ?? 1000) < 700; return ( -
+
Service {!isNarrow && My Service} {!isNarrow && ( diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index f00539c199..bbc73a262d 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -32497,8 +32497,8 @@ The following properties are supported across all utility types: { "description": "Specifies custom navigation content. When provided, replaces all structured content (identity, search, utilities are ignored).", - "isDefault": false, - "name": "customContent", + "isDefault": true, + "name": "children", }, { "description": "Use with an input or autosuggest control for a global search query.", @@ -44840,7 +44840,7 @@ Searches within this tooltip's scope to avoid conflicts with popovers.", "name": "findIdentityLink", "parameters": [], "returnType": { - "isNullable": true, + "isNullable": false, "name": "ElementWrapper", "typeArguments": [ { From ffc964f4672a93d25f9d4190a4ec7efd026ea42b Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Thu, 11 Jun 2026 11:04:17 +0200 Subject: [PATCH 6/8] chore: simplify the custom-content page --- pages/top-navigation/custom-content.page.tsx | 105 ++++++++----------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/pages/top-navigation/custom-content.page.tsx b/pages/top-navigation/custom-content.page.tsx index b10b5898b1..cf53f15970 100644 --- a/pages/top-navigation/custom-content.page.tsx +++ b/pages/top-navigation/custom-content.page.tsx @@ -2,46 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; -import { useContainerQuery } from '@cloudscape-design/component-toolkit'; - -import Button from '~components/button'; import Input from '~components/input'; -import Link from '~components/link'; import TopNavigation from '~components/top-navigation'; -import ScreenshotArea from '../utils/screenshot-area'; +import { SimplePage } from '../app/templates'; import { I18N_STRINGS } from './common'; import logo from './logos/simple-logo.svg'; function CustomNav({ searchValue, onSearchChange }: { searchValue: string; onSearchChange: (value: string) => void }) { - const [width, ref] = useContainerQuery(entry => entry.contentBoxWidth, []); - const isNarrow = (width ?? 1000) < 700; - return ( -
+
Service - {!isNarrow && My Service} - {!isNarrow && ( -
- onSearchChange(detail.value)} - ariaLabel="Search" - /> -
- )} - {!isNarrow ? ( - - ) : ( -
-
- )} +
+ onSearchChange(detail.value)} + ariaLabel="Search" + /> +
); } @@ -50,42 +30,39 @@ export default function CustomContentPage() { const [searchValue, setSearchValue] = useState(''); return ( -
-

TopNavigation children

- -

Custom content (default visual context)

- - - + +

Custom content (default visual context)

+ + + -
+
-

Custom content (visualContext="none")

- - - +

Custom content (visualContext="none")

+ + + -
+
-

Structured mode (unchanged)

- -
-
+

Structured mode (unchanged)

+ + ); } From d8997e214f7da43253ba6e27e5111c035ddde902 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Thu, 11 Jun 2026 13:46:04 +0200 Subject: [PATCH 7/8] feat: make custom content a slot between identity and utility --- pages/top-navigation/custom-content.page.tsx | 129 ++++++++++++++---- .../__integ__/top-navigation.test.ts | 6 +- .../top-navigation-custom-content.test.tsx | 87 ++++++------ .../top-navigation-responsiveness.test.tsx | 1 + .../__tests__/top-navigation.test.tsx | 2 +- src/top-navigation/interfaces.ts | 5 +- src/top-navigation/internal.tsx | 31 ++--- src/top-navigation/use-top-navigation.ts | 18 ++- 8 files changed, 165 insertions(+), 114 deletions(-) diff --git a/pages/top-navigation/custom-content.page.tsx b/pages/top-navigation/custom-content.page.tsx index cf53f15970..c9786bf125 100644 --- a/pages/top-navigation/custom-content.page.tsx +++ b/pages/top-navigation/custom-content.page.tsx @@ -3,52 +3,123 @@ import React, { useState } from 'react'; import Input from '~components/input'; +import Link from '~components/link'; import TopNavigation from '~components/top-navigation'; import { SimplePage } from '../app/templates'; import { I18N_STRINGS } from './common'; import logo from './logos/simple-logo.svg'; -function CustomNav({ searchValue, onSearchChange }: { searchValue: string; onSearchChange: (value: string) => void }) { - return ( -
- Service -
- onSearchChange(detail.value)} - ariaLabel="Search" - /> -
-
- ); -} - export default function CustomContentPage() { const [searchValue, setSearchValue] = useState(''); return ( - -

Custom content (default visual context)

- - - + +

customContent + identity + utilities

+ + Dashboard + Resources + Docs +
+ } + utilities={[ + { type: 'button', iconName: 'notification', ariaLabel: 'Notifications', badge: true }, + { + type: 'menu-dropdown', + text: 'Jane Doe', + description: 'jane.doe@example.com', + iconName: 'user-profile', + items: [ + { id: 'profile', text: 'Profile' }, + { id: 'signout', text: 'Sign out' }, + ], + }, + ]} + /> -
+

customContent + identity (no utilities)

+ + Overview + Settings +
+ } + /> -

Custom content (visualContext="none")

- - - +

customContent + utilities (no identity)

+ + Custom Brand + Home + About +
+ } + utilities={[ + { type: 'button', iconName: 'notification', ariaLabel: 'Notifications', badge: true }, + { + type: 'menu-dropdown', + text: 'Jane Doe', + description: 'jane.doe@example.com', + iconName: 'user-profile', + items: [ + { id: 'profile', text: 'Profile' }, + { id: 'signout', text: 'Sign out' }, + ], + }, + ]} + /> -
+

customContent only (no identity, no search, no utilities)

+ + Brand + My App +
+ Features + Pricing + Contact +
+
+ } + /> + +

customContent + identity + utilities + visualContext="none"

+ + Pricing + Blog +
+ } + utilities={[{ type: 'button', text: 'Contact', iconName: 'envelope' }]} + /> -

Structured mode (unchanged)

+

No customContent (structured mode, unchanged)

setSearchValue(detail.value)} + ariaLabel="Search" + /> + } utilities={[ { type: 'button', iconName: 'notification', ariaLabel: 'Notifications', badge: true }, { diff --git a/src/top-navigation/__integ__/top-navigation.test.ts b/src/top-navigation/__integ__/top-navigation.test.ts index b9aba2a335..2709841623 100644 --- a/src/top-navigation/__integ__/top-navigation.test.ts +++ b/src/top-navigation/__integ__/top-navigation.test.ts @@ -234,7 +234,7 @@ describe('Top navigation', () => { ); }); -describe('Top navigation - children (custom content)', () => { +describe('Top navigation - customContent', () => { const setupCustomContentTest = (testFn: (page: TopNavigationPage) => Promise) => { return useBrowser(async browser => { await browser.url('#/light/top-navigation/custom-content'); @@ -245,9 +245,9 @@ describe('Top navigation - children (custom content)', () => { }; test( - 'renders custom content', + 'renders custom content alongside identity', setupCustomContentTest(async page => { - await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('My Service'); + await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('Dashboard'); }) ); }); diff --git a/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx index cda958bd43..f0d38e9047 100644 --- a/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx +++ b/src/top-navigation/__tests__/top-navigation-custom-content.test.tsx @@ -15,49 +15,56 @@ const I18N_STRINGS: TopNavigationProps.I18nStrings = { overflowMenuDismissIconAriaLabel: 'Close', }; -const renderTopNavigation = (props: TopNavigationProps, children?: React.ReactNode) => { - const { container } = render( - - {children} - - ); +const renderTopNavigation = (props: TopNavigationProps) => { + const { container } = render(); return createWrapper(container).findTopNavigation()!; }; -describe('children', () => { - test('renders custom content when children are provided', () => { - const wrapper = renderTopNavigation({},
Custom Nav
); +describe('customContent', () => { + test('renders custom content alongside identity', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'My Service' }, + customContent:
Custom Links
, + }); expect(wrapper.findContent()).not.toBeNull(); - expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Nav'); + expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Links'); + expect(wrapper.findTitle()!.getElement()).toHaveTextContent('My Service'); }); - test('does not render identity when children are provided', () => { - const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Should Not Render' } },
Custom
); - expect(wrapper.findTitle()).toBeNull(); - // findIdentityLink() type is non-null for backwards compatibility, but returns null at runtime here. - expect(wrapper.findIdentityLink()).toBeNull(); - }); - - test('does not render utilities when children are provided', () => { - const wrapper = renderTopNavigation( - { identity: { href: '#', title: 'Title' }, utilities: [{ type: 'button', text: 'Help' }] }, -
Custom
- ); - expect(wrapper.findUtilities()).toHaveLength(0); + test('renders custom content alongside utilities', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Title' }, + utilities: [{ type: 'button', text: 'Help' }], + customContent:
Custom
, + }); + expect(wrapper.findContent()).not.toBeNull(); + expect(wrapper.findUtilities()).toHaveLength(1); }); - test('does not render search when children are provided', () => { - const wrapper = renderTopNavigation( - { identity: { href: '#', title: 'Title' }, search: }, -
Custom
- ); + test('hides search when customContent is provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Title' }, + search: , + customContent:
Custom
, + }); + expect(wrapper.findContent()).not.toBeNull(); expect(wrapper.findSearch()).toBeNull(); }); - test('renders structured mode when children are not provided', () => { - const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Structured' } }); + test('does not render custom content wrapper when customContent is not provided', () => { + const wrapper = renderTopNavigation({ + identity: { href: '#', title: 'Structured' }, + }); expect(wrapper.findContent()).toBeNull(); - expect(wrapper.findTitle()!.getElement()).toHaveTextContent('Structured'); + }); + + test('renders custom content without identity', () => { + const wrapper = renderTopNavigation({ + customContent:
Only custom
, + utilities: [{ type: 'button', text: 'Help' }], + }); + expect(wrapper.findContent()!.getElement()).toHaveTextContent('Only custom'); + expect(wrapper.findUtilities()).toHaveLength(1); }); }); @@ -67,25 +74,7 @@ describe('visualContext', () => { expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); }); - test('applies visual context when visualContext is "top-navigation"', () => { - const { container } = render( - -
Custom
-
- ); - expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull(); - }); - test('does not apply visual context when visualContext is "none"', () => { - const { container } = render( - -
Custom
-
- ); - expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull(); - }); - - test('visualContext="none" works with structured mode', () => { const { container } = render( ); diff --git a/src/top-navigation/__tests__/top-navigation-responsiveness.test.tsx b/src/top-navigation/__tests__/top-navigation-responsiveness.test.tsx index ac441baec6..98d52152fe 100644 --- a/src/top-navigation/__tests__/top-navigation-responsiveness.test.tsx +++ b/src/top-navigation/__tests__/top-navigation-responsiveness.test.tsx @@ -30,6 +30,7 @@ function testResponsiveness( fullIdentityWidth: 0, titleWidth: 0, searchSlotWidth: 0, + customContentWidth: 0, searchUtilityWidth: 0, menuTriggerUtilityWidth: 0, ...sizeConfig, diff --git a/src/top-navigation/__tests__/top-navigation.test.tsx b/src/top-navigation/__tests__/top-navigation.test.tsx index dd417dc6ef..1f339b788b 100644 --- a/src/top-navigation/__tests__/top-navigation.test.tsx +++ b/src/top-navigation/__tests__/top-navigation.test.tsx @@ -53,7 +53,7 @@ describe('TopNavigation Component', () => { test('has a link', () => { const topNavigation = renderTopNavigation({ identity: { href: '#', title: 'Application Title' } }); - expect(topNavigation.findIdentityLink().getElement()).toHaveAttribute('href', '#'); + expect(topNavigation.findIdentityLink()?.getElement()).toHaveAttribute('href', '#'); }); test('fires follow event when the title is clicked', () => { diff --git a/src/top-navigation/interfaces.ts b/src/top-navigation/interfaces.ts index 0e3b67954f..cd4d03fe6a 100644 --- a/src/top-navigation/interfaces.ts +++ b/src/top-navigation/interfaces.ts @@ -19,9 +19,10 @@ export interface TopNavigationProps extends BaseComponentProps { /** * Specifies custom navigation content. - * When provided, replaces all structured content (identity, search, utilities are ignored). + * This is placed next to the search and should be used only when you want to provide a custom content + * Can be combined with the identity and utilities if needed. */ - children?: React.ReactNode; + customContent?: React.ReactNode; /** * Controls the color scheme of the navigation bar and its contents. diff --git a/src/top-navigation/internal.tsx b/src/top-navigation/internal.tsx index b8431f5ed2..39ef09b141 100644 --- a/src/top-navigation/internal.tsx +++ b/src/top-navigation/internal.tsx @@ -27,7 +27,7 @@ export default function InternalTopNavigation({ i18nStrings, utilities, search, - children, + customContent, visualContext = 'top-navigation', ...restProps }: InternalTopNavigationProps) { @@ -46,7 +46,7 @@ export default function InternalTopNavigation({ const isLargeViewport = breakpoint === 's'; const i18n = useInternalI18n('top-navigation'); - const menuTriggerVisible = !isSearchExpanded && responsiveState.hideUtilities; + const menuTriggerVisible = !isSearchExpanded && responsiveState.hideUtilities && utilities.length > 0; useEffect(() => { setOverflowMenuOpen(false); @@ -58,24 +58,6 @@ export default function InternalTopNavigation({ } }, [overflowMenuOpen]); - // Custom content mode: render a simple container with no structured layout - if (children !== undefined) { - const header = ( -
-
{children}
-
- ); - return ( -
- {visualContext === 'top-navigation' ? ( - {header} - ) : ( - header - )} -
- ); - } - const onIdentityClick = (event: React.MouseEvent) => { if (isPlainLeftClick(event)) { fireCancelableEvent(identity?.onFollow, {}, event); @@ -93,10 +75,11 @@ export default function InternalTopNavigation({ const Wrapper = isVirtual ? 'div' : 'header'; const showIdentity = !!identity && (isVirtual || !isSearchExpanded); const showTitle = isVirtual || !responsiveState.hideTitle; - const showSearchSlot = search && (isVirtual || !responsiveState.hideSearch || isSearchExpanded); - const showSearchUtility = isVirtual || (search && responsiveState.hideSearch); + const showSearchSlot = !customContent && search && (isVirtual || !responsiveState.hideSearch || isSearchExpanded); + const showSearchUtility = !customContent && (isVirtual || (search && responsiveState.hideSearch)); const showUtilities = isVirtual || !isSearchExpanded; - const showMenuTrigger = isVirtual || menuTriggerVisible; + const hasCollapsibleUtilities = utilities.some(u => !u.disableUtilityCollapse); + const showMenuTrigger = hasCollapsibleUtilities && (isVirtual || menuTriggerVisible); return ( )} + {customContent &&
{customContent}
} +
{showSearchUtility && (