-
Notifications
You must be signed in to change notification settings - Fork 232
feat: add children for custom content and visualContext props to top navigation #4594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b5e0213
695dbd4
21b1139
f5c5deb
2ab8f82
e66c422
e3bc328
e410884
ebbcadd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // 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 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 ( | ||
| <div style={{ display: 'flex', alignItems: 'center', paddingInline: 24, height: 48, gap: 16 }}> | ||
| <img src={logo} alt="Service" style={{ height: 24 }} /> | ||
| <div style={{ flex: 1, maxWidth: 320 }}> | ||
| <Input | ||
| type="search" | ||
| placeholder="Search..." | ||
| value={searchValue} | ||
| onChange={({ detail }) => onSearchChange(detail.value)} | ||
| ariaLabel="Search" | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default function CustomContentPage() { | ||
| const [searchValue, setSearchValue] = useState(''); | ||
|
|
||
| return ( | ||
| <SimplePage title="TopNavigation children" screenshotArea={{}}> | ||
| <h2>Custom content (default visual context)</h2> | ||
| <TopNavigation> | ||
| <CustomNav searchValue={searchValue} onSearchChange={setSearchValue} /> | ||
| </TopNavigation> | ||
|
|
||
| <br /> | ||
|
|
||
| <h2>Custom content (visualContext="none")</h2> | ||
| <TopNavigation visualContext="none"> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also add one or two examples with visualContext="none" to some existing top-nav pages that use structured content |
||
| <CustomNav searchValue={searchValue} onSearchChange={setSearchValue} /> | ||
| </TopNavigation> | ||
|
|
||
| <br /> | ||
|
|
||
| <h2>Structured mode (unchanged)</h2> | ||
| <TopNavigation | ||
|
amanabiy marked this conversation as resolved.
|
||
| identity={{ href: '#', title: 'Structured Mode', logo: { src: logo, alt: 'Logo' } }} | ||
| i18nStrings={I18N_STRINGS} | ||
| 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 |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // 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 = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these strings needed in the custom content tests? |
||
| searchIconAriaLabel: 'Search', | ||
| searchDismissIconAriaLabel: 'Close search', | ||
| overflowMenuTriggerText: 'More', | ||
| overflowMenuTitleText: 'All', | ||
| overflowMenuBackIconAriaLabel: 'Back', | ||
| overflowMenuDismissIconAriaLabel: 'Close', | ||
| }; | ||
|
|
||
| const renderTopNavigation = (props: TopNavigationProps, children?: React.ReactNode) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: children is already a part of TopNavigationProps - why do we define it separately? |
||
| const { container } = render( | ||
| <TopNavigation i18nStrings={I18N_STRINGS} {...props}> | ||
| {children} | ||
| </TopNavigation> | ||
| ); | ||
| return createWrapper(container).findTopNavigation()!; | ||
| }; | ||
|
|
||
| describe('children', () => { | ||
| test('renders custom content when children are provided', () => { | ||
| const wrapper = renderTopNavigation({}, <div data-testid="custom">Custom Nav</div>); | ||
| expect(wrapper.findContent()).not.toBeNull(); | ||
| expect(wrapper.findContent()!.getElement()).toHaveTextContent('Custom Nav'); | ||
| }); | ||
|
|
||
| test('does not render identity when children are provided', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: should we add these assertions to the 'renders custom content when children are provided' test instead: ? As custom content replaces everything else, I think it makes sense to check for everything in one place rather than creating separate tests for that. |
||
| const wrapper = renderTopNavigation({ identity: { href: '#', title: 'Should Not Render' } }, <div>Custom</div>); | ||
| expect(wrapper.findTitle()).toBeNull(); | ||
| 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' }] }, | ||
| <div>Custom</div> | ||
| ); | ||
| expect(wrapper.findUtilities()).toHaveLength(0); | ||
| }); | ||
|
|
||
| test('does not render search when children are provided', () => { | ||
| const wrapper = renderTopNavigation( | ||
| { identity: { href: '#', title: 'Title' }, search: <input placeholder="Search" /> }, | ||
| <div>Custom</div> | ||
| ); | ||
| expect(wrapper.findSearch()).toBeNull(); | ||
| }); | ||
|
|
||
| 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'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('visualContext', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do visual context related tests live inside top-navigation-custom-content.test? |
||
| 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('applies visual context when visualContext is "top-navigation"', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test also checks custom content, but that is not explicitly called out. I recommend making test names better uniform to ensure we capture all 4 states explicitly. Alternatively, we can group tests together: |
||
| const { container } = render( | ||
| <TopNavigation visualContext="top-navigation"> | ||
| <div>Custom</div> | ||
| </TopNavigation> | ||
| ); | ||
| 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"> | ||
| <div>Custom</div> | ||
| </TopNavigation> | ||
| ); | ||
| expect(container.querySelector('[class*="awsui-context-top-navigation"]')).toBeNull(); | ||
| }); | ||
|
|
||
| test('visualContext="none" works with structured mode', () => { | ||
| 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 |
|---|---|---|
|
|
@@ -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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if a top nav is rendered without custom content and also without identity? Will the component maintain its layout and responsiveness or appear broken? |
||
|
|
||
| /** | ||
| * Specifies custom navigation content. | ||
| * When provided, replaces all structured content (identity, search, utilities are ignored). | ||
| */ | ||
| children?: 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is "branded colors"? |
||
| * - "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'; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this custom nav is not that custom. I would extend this example or even create a few more examples to feature more Cloudscape components that can be used inside the top-nav, such as button group, button dropdown, links, and more.