Skip to content
Merged
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
234 changes: 234 additions & 0 deletions pages/expandable-section/header-secondary-content.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import Badge from '~components/badge';
import Box from '~components/box';
import Button from '~components/button';
import ButtonGroup from '~components/button-group';
import ExpandableSection from '~components/expandable-section';
import Input from '~components/input';
import Link from '~components/link';
import Popover from '~components/popover';
import Select, { SelectProps } from '~components/select';
import SpaceBetween from '~components/space-between';

import { SimplePage } from '../app/templates';

const languageOptions: SelectProps.Options = [
{ label: 'SQL', value: 'sql' },
{ label: 'PPL', value: 'ppl' },
{ label: 'Logstash', value: 'logstash' },
];

function StandardExample() {
return (
<ExpandableSection variant="container" defaultExpanded={true} headerText="Standard expandable section">
Content
</ExpandableSection>
);
}

function CreateQueryExample() {
const [expanded, setExpanded] = useState(true);
const [language, setLanguage] = useState<SelectProps.Option>(languageOptions[0]);
return (
<ExpandableSection
variant="container"
headerText="Create query"
expanded={expanded}
onChange={({ detail }) => setExpanded(detail.expanded)}
headerSecondaryContent={
<Select
selectedOption={language}
onChange={({ detail }) => setLanguage(detail.selectedOption)}
options={languageOptions}
Comment thread
gethinwebster marked this conversation as resolved.
Resolved
ariaLabel="Query language"
/>
}
headerActions={
expanded ? (
<SpaceBetween direction="horizontal" size="xs">
<ButtonGroup
ariaLabel="Query actions"
variant="icon"
items={[
{ type: 'icon-button', id: 'copy', iconName: 'copy', text: 'Copy' },
{ type: 'icon-button', id: 'remove', iconName: 'remove', text: 'Clear' },
]}
/>
<Button>Save</Button>
<Button variant="primary">Run</Button>
</SpaceBetween>
) : (
<Button variant="primary">Run</Button>
)
}
>
<Box color="text-body-secondary">SELECT * FROM logs WHERE status = 500 ORDER BY timestamp DESC LIMIT 100</Box>
</ExpandableSection>
);
}

function DescriptionAsSecondaryContentExample() {
return (
<ExpandableSection
variant="container"
defaultExpanded={true}
headerText="Production database"
headerSecondaryContent={<Box variant="small">Last synced 3 minutes ago</Box>}
headerActions={<Button>Refresh</Button>}
>
<Box color="text-body-secondary">
Supporting text can live in the secondary content slot next to the header text, instead of the description slot
below it.
</Box>
</ExpandableSection>
);
}

function HiddenHeaderTextExample() {
const sectionName = 'Data source A';
const [language, setLanguage] = useState<SelectProps.Option>(languageOptions[0]);
return (
<ExpandableSection
variant="container"
defaultExpanded={true}
headerText={sectionName}
hideHeaderText={true}
headerSecondaryContent={
<SpaceBetween direction="horizontal" size="xs" alignItems="center">
<Badge color="severity-low">
<Box padding={{ vertical: 'xxs' }} color="inherit">
<Popover content="Edit name...">
<Box fontSize="heading-m" color="inherit">
A
</Box>
</Popover>
</Box>
</Badge>
<Select
selectedOption={language}
onChange={({ detail }) => setLanguage(detail.selectedOption)}
options={languageOptions}
ariaLabel="Query language"
/>
</SpaceBetween>
}
>
<Box color="text-body-secondary">Configuration for {sectionName}.</Box>
</ExpandableSection>
);
}

function HiddenHeaderTextSearchExample() {
const [value, setValue] = useState('');
return (
<ExpandableSection
variant="container"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add one more example with variant equals to container, and it has headerCounter, headerInfo and headerDescription? It would be easy to test and see how they work together.

defaultExpanded={true}
headerText="Search results"
hideHeaderText={true}
headerSecondaryContent={
<Input
type="search"
value={value}
onChange={({ detail }) => setValue(detail.value)}
placeholder="Search results"
ariaLabel="Search results"
/>
}
>
<Box color="text-body-secondary">Results matching “{value || 'your query'}”.</Box>
</ExpandableSection>
);
}

function AllHeaderSlotsExample() {
const [language, setLanguage] = useState<SelectProps.Option>(languageOptions[0]);
return (
<ExpandableSection
variant="container"
defaultExpanded={true}
headerText="Queries"
headerCounter="(3)"
headerInfo={<Link variant="info">Info</Link>}
headerDescription="Saved queries for the production log group."
headerSecondaryContent={
<Select
selectedOption={language}
onChange={({ detail }) => setLanguage(detail.selectedOption)}
options={languageOptions}
ariaLabel="Query language"
/>
}
headerActions={<Button variant="primary">Run</Button>}
>
<Box color="text-body-secondary">Counter, info link, description, secondary content and actions together.</Box>
</ExpandableSection>
);
}

function LongContentExample() {
const [language, setLanguage] = useState<SelectProps.Option>(languageOptions[0]);
return (
<ExpandableSection
variant="container"
defaultExpanded={true}
headerText="Create query for the aggregated multi-region production log analytics dataset"
headerSecondaryContent={
<Select
selectedOption={language}
onChange={({ detail }) => setLanguage(detail.selectedOption)}
options={languageOptions}
ariaLabel="Query language"
/>
}
headerActions={
<SpaceBetween direction="horizontal" size="xs">
<Button>Save</Button>
<Button variant="primary">Run</Button>
</SpaceBetween>
}
>
<Box color="text-body-secondary">Stress case: long header text next to an interactive control and actions.</Box>
</ExpandableSection>
);
}

export default function ExpandableSectionHeaderSecondaryContentPage() {
return (
<SimplePage title="Expandable section — header secondary content" screenshotArea={{}}>
<SpaceBetween size="l">
<div>
<h2>Standard</h2>
<StandardExample />
</div>
<div>
<h2>Interactive control alongside visible header text</h2>
<CreateQueryExample />
</div>
<div>
<h2>Description-style text as secondary content</h2>
<DescriptionAsSecondaryContentExample />
</div>
<div>
<h2>Interactive control with hidden header text</h2>
<HiddenHeaderTextExample />
</div>
<div>
<h2>Search input with hidden header text</h2>
<HiddenHeaderTextSearchExample />
</div>
<div>
<h2>Counter, info link and description with secondary content</h2>
<AllHeaderSlotsExample />
</div>
<div>
<h2>Long header text with control and actions</h2>
<LongContentExample />
</div>
</SpaceBetween>
</SimplePage>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14042,6 +14042,14 @@ slot (not with the deprecated \`header\`), and not with the navigation variant."
"optional": true,
"type": "string",
},
{
"description": "Visually hides the header text while keeping it available to assistive technology.
Use with \`headerSecondaryContent\` to present a purely interactive header control.
Use with the default or container variant.",
"name": "hideHeaderText",
"optional": true,
"type": "boolean",
},
{
"deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases,
use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must
Expand Down Expand Up @@ -14100,6 +14108,12 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
"isDefault": false,
"name": "headerInfo",
},
{
"description": "Secondary content displayed next to the header text, such as an interactive control or supporting text.
Use with the default or container variant.",
"isDefault": false,
"name": "headerSecondaryContent",
},
{
"description": "The heading text. Use plain text. When using the container variant, you can use additional header props like \`headerDescription\` and \`headerCounter\` to display other elements in the header.",
"isDefault": false,
Expand Down Expand Up @@ -16005,16 +16019,19 @@ exports[`Components definition for header matches the snapshot: header 1`] = `
},
{
"description": "Overrides the default [HTML heading tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements)
provided by the variant. Using this property does not change the visual appearance of the component.",
provided by the variant. Set to \`div\` to render the title without heading semantics. Using this property does not
change the visual appearance of the component.",
"inlineType": {
"name": "HeaderProps.HeadingTag",
"type": "union",
"values": [
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
],
},
"name": "headingTagOverride",
Expand Down
18 changes: 18 additions & 0 deletions src/container/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ describe('Container renders correct analytics metadata', () => {
expect(getGeneratedAnalyticsMetadata(element)).toEqual(getMetadataContexts(label));
});
});

describe.each<HeaderProps.HeadingTag>(['h4', 'h6', 'div'])(
'With a Header component and headingTagOverride=%s',
tag => {
test('resolves the label from the header text', () => {
const label = 'Container header';
const element = renderContainer({
header: (
<Header counter="100" info="Info" headingTagOverride={tag}>
{label}
</Header>
),
});
validateComponentNameAndLabels(element, labels);
expect(getGeneratedAnalyticsMetadata(element)).toEqual(getMetadataContexts(label));
});
}
);
});

test('Internal Container does not render "component" metadata', () => {
Expand Down
12 changes: 11 additions & 1 deletion src/container/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ContainerProps } from './interfaces';
import { getContentStyles, getFooterStyles, getHeaderStyles, getMediaStyles, getRootStyles } from './style';
import { StickyHeaderContext, useStickyHeader } from './use-sticky-header';

import headerAnalyticsSelectors from '../header/analytics-metadata/styles.css.js';
import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
import testStyles from './test-classes/styles.css.js';
Expand Down Expand Up @@ -53,6 +54,12 @@ export interface InternalContainerProps extends Omit<ContainerProps, 'variant'>,
__contentKey?: string;

styleClassNames?: StyleClassNames;

/**
* Native attributes spread onto the container root, for ARIA that `getBaseProps` does not forward
* (it forwards only id/className/data-*).
*/
__nativeAttributes?: React.HTMLAttributes<HTMLDivElement>;
}

// Style API v2
Expand Down Expand Up @@ -103,6 +110,7 @@ const InternalContainer = React.forwardRef(function InternalContainer(
__subStepRef,
__contentKey,
styleClassNames,
__nativeAttributes,
...restProps
}: InternalContainerProps,
ref: React.Ref<ContainerProps.Ref>
Expand Down Expand Up @@ -167,6 +175,7 @@ const InternalContainer = React.forwardRef(function InternalContainer(
<div
{...baseProps}
{...__funnelSubStepProps}
{...__nativeAttributes}
className={clsx(
styleClassNames?.root,
baseProps.className,
Expand All @@ -182,7 +191,8 @@ const InternalContainer = React.forwardRef(function InternalContainer(
ref={mergedRef}
onBlur={onRootBlur}
{...getAnalyticsLabelAttribute(
`.${analyticsSelectors.header} h1, .${analyticsSelectors.header} h2, .${analyticsSelectors.header} h3`
// heading-text covers Header titles rendered outside h1-h3 via headingTagOverride.
`.${analyticsSelectors.header} :is(h1, h2, h3, .${headerAnalyticsSelectors['heading-text']})`
)}
style={getRootStyles(style)}
>
Expand Down
16 changes: 16 additions & 0 deletions src/expandable-section/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ describe('Expandable section renders correct analytics metadata', () => {
);
});
});
test.each(['container', 'stacked'])(
'%s variant resolves the label from header text when hideHeaderText hides the title',
variant => {
const wrapper = renderExpandableSection({
headerText: 'header',
children: 'content',
variant: variant as ExpandableSectionProps['variant'],
hideHeaderText: true,
});
// The title renders as a div (not h1/h2/h3) when hidden, so the label must resolve from the
// header-text marker class instead of the heading element.
expect(getGeneratedAnalyticsMetadata(wrapper.findExpandButton()!.getElement())).toEqual(
getExpandMetadata('header', variant, false)
);
}
);
test.each(['container', 'stacked'])('%s variant with additional properties', variant => {
const wrapper = renderExpandableSection({
headerText: 'header',
Expand Down
Loading
Loading