Skip to content

Commit bfcd8cb

Browse files
committed
feat: add secondaryContent to Header
1 parent c7e4aef commit bfcd8cb

9 files changed

Lines changed: 175 additions & 11 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import { Button, Container, Link, Select, SpaceBetween } from '~components';
6+
import Header, { HeaderProps } from '~components/header';
7+
8+
import ScreenshotArea from '../utils/screenshot-area';
9+
10+
const variants: HeaderProps.Variant[] = ['h1', 'h2', 'h3', 'awsui-h1-sticky'];
11+
12+
const secondaryContent = (
13+
<Select
14+
ariaLabel="Time range"
15+
selectedOption={{ value: '1h', label: 'Last hour' }}
16+
options={[{ value: '1h', label: 'Last hour' }]}
17+
onChange={() => {}}
18+
/>
19+
);
20+
21+
const actions = (
22+
<SpaceBetween direction="horizontal" size="xs">
23+
<Button>Refresh</Button>
24+
<Button variant="primary">Create resource</Button>
25+
</SpaceBetween>
26+
);
27+
28+
function Row({ variant }: { variant: HeaderProps.Variant }) {
29+
return (
30+
<SpaceBetween size="m">
31+
<Header variant={variant} secondaryContent={secondaryContent}>
32+
Title only
33+
</Header>
34+
<Header variant={variant} secondaryContent={secondaryContent} actions={actions}>
35+
With actions
36+
</Header>
37+
<Header
38+
variant={variant}
39+
counter="(12)"
40+
info={<Link variant="info">Info</Link>}
41+
description="A description shown below the title of this header."
42+
secondaryContent={secondaryContent}
43+
actions={actions}
44+
>
45+
Counter, info and description
46+
</Header>
47+
<Header variant={variant} secondaryContent={secondaryContent} actions={actions}>
48+
A much longer title that is likely to wrap onto a second line in narrower layouts
49+
</Header>
50+
</SpaceBetween>
51+
);
52+
}
53+
54+
export default function HeaderSecondaryContentPage() {
55+
return (
56+
<article>
57+
<h1>Header - secondary content</h1>
58+
<ScreenshotArea disableAnimations={true}>
59+
<SpaceBetween size="xl">
60+
{variants.map(variant => (
61+
<Container key={variant} header={<Header variant="h3">{`variant="${variant}"`}</Header>}>
62+
<Row variant={variant} />
63+
</Container>
64+
))}
65+
<Container
66+
header={
67+
<Header variant="h2" secondaryContent={secondaryContent} actions={actions}>
68+
As a container header
69+
</Header>
70+
}
71+
>
72+
Container content
73+
</Container>
74+
</SpaceBetween>
75+
</ScreenshotArea>
76+
</article>
77+
);
78+
}

‎src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16319,6 +16319,12 @@ to display resource counters in table and cards components.",
1631916319
"isDefault": false,
1632016320
"name": "info",
1632116321
},
16322+
{
16323+
"description": "Content displayed next to the heading, before the actions. For example, a control that
16324+
changes what the section shows.",
16325+
"isDefault": false,
16326+
"name": "secondaryContent",
16327+
},
1632216328
],
1632316329
"releaseStatus": "stable",
1632416330
}
@@ -43632,6 +43638,19 @@ The dismiss button is only rendered when the \`dismissible\` property is set to
4363243638
],
4363343639
},
4363443640
},
43641+
{
43642+
"name": "findSecondaryContent",
43643+
"parameters": [],
43644+
"returnType": {
43645+
"isNullable": true,
43646+
"name": "ElementWrapper",
43647+
"typeArguments": [
43648+
{
43649+
"name": "HTMLElement",
43650+
},
43651+
],
43652+
},
43653+
},
4363543654
],
4363643655
"name": "HeaderWrapper",
4363743656
},
@@ -55065,6 +55084,14 @@ The dismiss button is only rendered when the \`dismissible\` property is set to
5506555084
"name": "ElementWrapper",
5506655085
},
5506755086
},
55087+
{
55088+
"name": "findSecondaryContent",
55089+
"parameters": [],
55090+
"returnType": {
55091+
"isNullable": false,
55092+
"name": "ElementWrapper",
55093+
},
55094+
},
5506855095
],
5506955096
"name": "HeaderWrapper",
5507055097
},

‎src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ exports[`test-utils selectors 1`] = `
378378
"awsui_heading-text_2qdw9",
379379
"awsui_info_2qdw9",
380380
"awsui_root_2qdw9",
381+
"awsui_secondary-content_2qdw9",
381382
],
382383
"help-panel": [
383384
"awsui_content_1d237",

‎src/expandable-section/expandable-section-header.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ const ExpandableHeaderTextWrapper = ({
410410
actions={actions}
411411
headingTagOverride={hideText ? 'div' : headingTagOverride}
412412
__inExpandableSection={true}
413-
__additionalTitleContent={secondaryContent ? <span id={describedById}>{secondaryContent}</span> : undefined}
413+
secondaryContent={secondaryContent ? <span id={describedById}>{secondaryContent}</span> : undefined}
414414
>
415415
{headerButton}
416416
</InternalHeader>

‎src/header/__tests__/header.test.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,37 @@ test('renders everything provided', () => {
4242
expect(wrapper.findActions()!.find('button')!.getElement()).toHaveTextContent('Click me');
4343
});
4444

45+
describe('secondary content', () => {
46+
test('is not rendered by default', () => {
47+
const { wrapper } = renderHeader(<Header>Test title</Header>);
48+
expect(wrapper.findSecondaryContent()).toBeNull();
49+
});
50+
51+
test.each(['h1', 'h2', 'h3', 'awsui-h1-sticky'] as const)('renders outside the heading element for %s', variant => {
52+
const { wrapper } = renderHeader(
53+
<Header variant={variant} secondaryContent={<button>Filter</button>}>
54+
Test title
55+
</Header>
56+
);
57+
const secondaryContent = wrapper.findSecondaryContent()!.getElement();
58+
expect(secondaryContent).toHaveTextContent('Filter');
59+
expect(secondaryContent.closest('h1,h2,h3,h4,h5,h6')).toBeNull();
60+
});
61+
62+
test('renders between the title and the actions', () => {
63+
const { wrapper } = renderHeader(
64+
<Header info="Info" secondaryContent={<button>Filter</button>} actions={<button>Create</button>}>
65+
Test title
66+
</Header>
67+
);
68+
const info = wrapper.findInfo()!.getElement();
69+
const secondaryContent = wrapper.findSecondaryContent()!.getElement();
70+
const actions = wrapper.findActions()!.getElement();
71+
expect(info.compareDocumentPosition(secondaryContent) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
72+
expect(secondaryContent.compareDocumentPosition(actions) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
73+
});
74+
});
75+
4576
test('renders arbitrary counter content', () => {
4677
const { wrapper } = renderHeader(<Header counter={<Spinner />} />);
4778
expect(wrapper.findCounter()).not.toBeNull();

‎src/header/interfaces.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export interface HeaderProps extends BaseComponentProps {
4343
* Area next to the heading to display an Info link.
4444
*/
4545
info?: React.ReactNode;
46+
/**
47+
* Content displayed next to the heading, before the actions. For example, a control that
48+
* changes what the section shows.
49+
*/
50+
secondaryContent?: React.ReactNode;
4651
}
4752

4853
export namespace HeaderProps {

‎src/header/internal.tsx‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ interface InternalHeaderProps extends SomeRequired<HeaderProps, 'variant'>, Inte
2727
__headingTagRef?: MutableRefObject<HTMLHeadingElement | null>;
2828
__headingTagTabIndex?: number;
2929
__inExpandableSection?: boolean;
30-
/**
31-
* Rendered in the title row but outside the heading tag, so content beside the title is not
32-
* nested inside the heading element.
33-
*/
34-
__additionalTitleContent?: React.ReactNode;
3530
}
3631

3732
export default function InternalHeader({
@@ -42,12 +37,12 @@ export default function InternalHeader({
4237
counter,
4338
description,
4439
info,
40+
secondaryContent,
4541
__internalRootRef,
4642
__disableActionsWrapping,
4743
__headingTagRef,
4844
__headingTagTabIndex,
4945
__inExpandableSection,
50-
__additionalTitleContent,
5146
...restProps
5247
}: InternalHeaderProps) {
5348
const isMobile = useMobile();
@@ -124,8 +119,16 @@ export default function InternalHeader({
124119
</InfoLinkLabelContext.Provider>
125120
)}
126121
</div>
127-
{__additionalTitleContent && (
128-
<div className={styles['additional-title-content']}>{__additionalTitleContent}</div>
122+
{secondaryContent && (
123+
<div
124+
className={clsx(
125+
styles['secondary-content'],
126+
styles[`secondary-content-variant-${variantOverride}`],
127+
isRefresh && styles.refresh
128+
)}
129+
>
130+
{secondaryContent}
131+
</div>
129132
)}
130133
{actions && (
131134
<div

‎src/header/styles.scss‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,28 @@
130130
}
131131
}
132132

133-
.additional-title-content {
133+
.secondary-content {
134134
display: inline-flex;
135135
align-items: center;
136136
flex-shrink: 0;
137137
min-inline-size: 0;
138138
max-inline-size: 100%;
139139
margin-inline-end: auto;
140+
141+
// In classic, mirror the .actions box so content lines up with the heading text the way buttons do.
142+
&-variant-h1:not(.refresh),
143+
&-variant-h2:not(.refresh),
144+
&-variant-h3:not(.refresh) {
145+
align-self: flex-start;
146+
min-block-size: awsui.$size-vertical-input;
147+
}
148+
&-variant-h1:not(.refresh) {
149+
padding-block: awsui.$space-scaled-xs;
150+
}
151+
&-variant-h2:not(.refresh),
152+
&-variant-h3:not(.refresh) {
153+
padding-block: awsui.$space-scaled-xxs;
154+
}
140155
}
141156

142157
.title {
@@ -189,7 +204,7 @@
189204

190205
// With secondary content beside the heading, center both against a constant-height line so their
191206
// alignment no longer shifts when actions wrap to a second row.
192-
.main:has(> .additional-title-content) > .title-variant-h2.title-in-expandable-section.refresh {
207+
.main:has(> .secondary-content) > .title-variant-h2.title-in-expandable-section.refresh {
193208
padding-block-start: 0;
194209
min-block-size: awsui.$size-vertical-input;
195210
display: flex;

‎src/test-utils/dom/header/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@ export default class HeaderWrapper extends ComponentWrapper {
2626
findActions(): ElementWrapper | null {
2727
return this.findByClassName(styles.actions);
2828
}
29+
30+
findSecondaryContent(): ElementWrapper | null {
31+
return this.findByClassName(styles['secondary-content']);
32+
}
2933
}

0 commit comments

Comments
 (0)