Skip to content
Draft
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
139 changes: 139 additions & 0 deletions pages/top-navigation/custom-content.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
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';

export default function CustomContentPage() {
const [searchValue, setSearchValue] = useState('');

return (
<SimplePage title="TopNavigation customContent" screenshotArea={{}}>
<h2>customContent + identity + utilities</h2>
<TopNavigation
identity={{ href: '#', title: 'My Service', logo: { src: logo, alt: 'Logo' } }}
i18nStrings={I18N_STRINGS}
customContent={
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Link href="#">Dashboard</Link>
<Link href="#">Resources</Link>
<Link href="#">Docs</Link>
</div>
}
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' },
],
},
]}
/>

<h2>customContent + identity (no utilities)</h2>
<TopNavigation
identity={{ href: '#', title: 'Simple App', logo: { src: logo, alt: 'Logo' } }}
i18nStrings={I18N_STRINGS}
customContent={
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Link href="#">Overview</Link>
<Link href="#">Settings</Link>
</div>
}
/>

<h2>customContent + utilities (no identity)</h2>
<TopNavigation
i18nStrings={I18N_STRINGS}
customContent={
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<span style={{ fontWeight: 700 }}>Custom Brand</span>
<Link href="#">Home</Link>
<Link href="#">About</Link>
</div>
}
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' },
],
},
]}
/>

<h2>customContent only (no identity, no search, no utilities)</h2>
<TopNavigation
customContent={
<div style={{ display: 'flex', gap: 16, alignItems: 'center', width: '100%' }}>
<img src={logo} alt="Brand" style={{ height: 24 }} />
<span style={{ fontWeight: 700 }}>My App</span>
<div style={{ display: 'flex', gap: 12, marginInlineStart: 'auto' }}>
<Link href="#">Features</Link>
<Link href="#">Pricing</Link>
<Link href="#">Contact</Link>
</div>
</div>
}
/>

<h2>customContent + identity + utilities + visualContext=&quot;none&quot;</h2>
<TopNavigation
visualContext="none"
identity={{ href: '#', title: 'Light Mode', logo: { src: logo, alt: 'Logo' } }}
i18nStrings={I18N_STRINGS}
customContent={
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Link href="#">Pricing</Link>
<Link href="#">Blog</Link>
</div>
}
utilities={[{ type: 'button', text: 'Contact', iconName: 'envelope' }]}
/>

<h2>No customContent (structured mode, unchanged)</h2>
<TopNavigation
identity={{ href: '#', title: 'Structured Mode', logo: { src: logo, alt: 'Logo' } }}
i18nStrings={I18N_STRINGS}
search={
<Input
type="search"
placeholder="Search..."
value={searchValue}
onChange={({ detail }) => setSearchValue(detail.value)}
ariaLabel="Search"
/>
}
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' },
],
},
]}
/>
</SimplePage>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down Expand Up @@ -32476,8 +32476,30 @@ The following properties are supported across all utility types:
"optional": true,
"type": "ReadonlyArray<TopNavigationProps.Utility>",
},
{
"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": true,
"name": "children",
},
{
"description": "Use with an input or autosuggest control for a global search query.",
"isDefault": false,
Expand Down Expand Up @@ -44801,6 +44823,19 @@ 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": [],
Expand Down Expand Up @@ -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": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 6 additions & 2 deletions src/test-utils/dom/top-navigation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import styles from '../../../top-navigation/styles.selectors.js';
export default class TopNavigationWrapper extends ComponentWrapper {
static rootSelector = `${styles['top-navigation']}:not(.${styles.hidden})`;

findIdentityLink(): ElementWrapper {
return this.find(`.${styles.identity} a`)!;
findContent(): ElementWrapper | null {
return this.find(`.${styles['custom-content']}`);
}

findIdentityLink(): ElementWrapper | null {
return this.find(`.${styles.identity} a`);
}

findLogo(): ElementWrapper | null {
Expand Down
18 changes: 18 additions & 0 deletions src/top-navigation/__integ__/top-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,21 @@ describe('Top navigation', () => {
})
);
});

describe('Top navigation - customContent', () => {
const setupCustomContentTest = (testFn: (page: TopNavigationPage) => Promise<void>) => {
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 alongside identity',
setupCustomContentTest(async page => {
await expect(page.getText(wrapper.findContent().toSelector())).resolves.toContain('Dashboard');
})
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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(<TopNavigation i18nStrings={I18N_STRINGS} {...props} />);
return createWrapper(container).findTopNavigation()!;
};

describe('customContent', () => {
test('renders custom content alongside identity', () => {
const wrapper = renderTopNavigation({
identity: { href: '#', title: 'My Service' },
customContent: <div data-testid="custom">Custom Links</div>,
});
expect(wrapper.findContent()).not.toBeNull();
expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Links');
expect(wrapper.findTitle()!.getElement()).toHaveTextContent('My Service');
});

test('renders custom content alongside utilities', () => {
const wrapper = renderTopNavigation({
identity: { href: '#', title: 'Title' },
utilities: [{ type: 'button', text: 'Help' }],
customContent: <div>Custom</div>,
});
expect(wrapper.findContent()).not.toBeNull();
expect(wrapper.findUtilities()).toHaveLength(1);
});

test('hides search when customContent is provided', () => {
const wrapper = renderTopNavigation({
identity: { href: '#', title: 'Title' },
search: <input placeholder="Search" />,
customContent: <div>Custom</div>,
});
expect(wrapper.findContent()).not.toBeNull();
expect(wrapper.findSearch()).toBeNull();
});

test('does not render custom content wrapper when customContent is not provided', () => {
const wrapper = renderTopNavigation({
identity: { href: '#', title: 'Structured' },
});
expect(wrapper.findContent()).toBeNull();
});

test('renders custom content without identity', () => {
const wrapper = renderTopNavigation({
customContent: <div>Only custom</div>,
utilities: [{ type: 'button', text: 'Help' }],
});
expect(wrapper.findContent()!.getElement()).toHaveTextContent('Only custom');
expect(wrapper.findUtilities()).toHaveLength(1);
});
});

describe('visualContext', () => {
test('defaults to top-navigation (dark visual context)', () => {
const { container } = render(<TopNavigation identity={{ href: '#', title: 'Title' }} i18nStrings={I18N_STRINGS} />);
expect(container.querySelector('[class*="awsui-context-top-navigation"]')).not.toBeNull();
});

test('does not apply visual context when visualContext is "none"', () => {
const { container } = render(
<TopNavigation visualContext="none" identity={{ href: '#', title: 'Light Nav' }} i18nStrings={I18N_STRINGS} />
);
expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function testResponsiveness(
fullIdentityWidth: 0,
titleWidth: 0,
searchSlotWidth: 0,
customContentWidth: 0,
searchUtilityWidth: 0,
menuTriggerUtilityWidth: 0,
...sizeConfig,
Expand Down
8 changes: 4 additions & 4 deletions src/top-navigation/__tests__/top-navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -66,7 +66,7 @@ describe('TopNavigation Component', () => {
onFollow: event => onFollowSpy(event.detail),
},
});
const identityLink = topNavigation.findIdentityLink().getElement();
const identityLink = topNavigation.findIdentityLink()!.getElement();
identityLink.click();
expect(onFollowSpy).toHaveBeenCalledWith({});
});
Expand All @@ -81,7 +81,7 @@ describe('TopNavigation Component', () => {
onFollow: event => onFollowSpy(event.detail),
},
});
const identityLink = topNavigation.findIdentityLink();
const identityLink = topNavigation.findIdentityLink()!;
identityLink.click({ ctrlKey: true });
identityLink.click({ altKey: true });
identityLink.click({ shiftKey: true });
Expand Down Expand Up @@ -253,7 +253,7 @@ describe('URL sanitization', () => {
describe('for the identity', () => {
test('does not throw an error when a safe javascript: URL is passed', () => {
const element = renderTopNavigation({ identity: { href: 'javascript:void(0)' } });
expect((element.findIdentityLink().getElement() as HTMLAnchorElement).href).toBe('javascript:void(0)');
expect((element.findIdentityLink()!.getElement() as HTMLAnchorElement).href).toBe('javascript:void(0)');

expect(warnOnce).toHaveBeenCalledTimes(0);
});
Expand Down
Loading
Loading