{
const isContainer = variant === 'container';
- const HeadingTag = headingTagOverride || 'div';
const supportsInteractiveElements = variantSupportsActions(variant);
- const restrictClickableArea = (supportsInteractiveElements && (!!headerInfo || !!headerActions)) || !!headerEndInfo;
+ const supportsSecondaryContent = variantSupportsHeaderSecondaryContent(variant);
+ const hideText = supportsSecondaryContent && hideHeaderText;
+ const HeadingTag = hideText ? 'div' : headingTagOverride || 'div';
+ const secondaryContent = supportsSecondaryContent && headerSecondaryContent;
+ const describedById = secondaryContent ? secondaryContentId : undefined;
+ const restrictClickableArea =
+ (supportsInteractiveElements && (!!headerInfo || !!headerActions || !!secondaryContent)) || !!headerEndInfo;
const actions = supportsInteractiveElements && headerActions;
const endInfo = !isContainer && headerEndInfo;
const description = variantSupportsDescription(variant) && headerDescription && (
@@ -263,12 +281,13 @@ const ExpandableHeaderTextWrapper = ({
{headerDescription}
);
+ const ariaDescribedBy = clsx(description && descriptionId, describedById) || undefined;
const listeners = { onClick, onKeyDown, onKeyUp };
const iconAtEnd = expandIconPosition === 'end';
// For the variants that allow actions, when end placement is active, the icon is rendered outside the
// InternalHeader so it appears at the inline-end of the wrapper, past any header actions.
// Other variants render the icon at the end of the headerButton itself.
- const renderIconOutsideHeader = iconAtEnd && (isContainer || !!actions || !!endInfo);
+ const renderIconOutsideHeader = iconAtEnd && (isContainer || !!actions || !!endInfo || !!secondaryContent);
// If interactive elements are present, constrain the clickable area to only the icon and the header text
// to prevent nesting interactive elements.
@@ -294,6 +313,7 @@ const ExpandableHeaderTextWrapper = ({
icon={icon}
ariaLabel={ariaLabel}
ariaLabelledBy={!ariaLabel ? id : undefined}
+ ariaDescribedBy={ariaDescribedBy}
ariaControls={ariaControls}
expanded={expanded}
onClick={onClick}
@@ -319,44 +339,54 @@ const ExpandableHeaderTextWrapper = ({
const outsideIcon = renderIconOutsideHeader ? expandButton : null;
const textElement = (
-
- {children}
-
- );
- const headerButton = (
- {renderIconOutsideHeader ? (
- textElement
- ) : iconAtEnd ? (
- <>
- {textElement}
- {expandIcon}
- >
- ) : (
- <>
- {expandIcon}
- {textElement}
- >
- )}
+ {children}
);
+ const headerButton =
+ renderIconOutsideHeader && hideText ? (
+ textElement
+ ) : (
+
+ {renderIconOutsideHeader ? (
+ textElement
+ ) : iconAtEnd ? (
+ <>
+ {textElement}
+ {expandIcon}
+ >
+ ) : (
+ <>
+ {expandIcon}
+ {textElement}
+ >
+ )}
+
+ );
return (
{secondaryContent} : undefined}
>
{headerButton}
@@ -390,18 +421,25 @@ const ExpandableHeaderTextWrapper = ({
-
- {headerButton}
-
+
+
+ {headerButton}
+
+ {secondaryContent && (
+
+ {secondaryContent}
+
+ )}
+
{endInfo}
{actions}
@@ -424,6 +462,9 @@ export const ExpandableSectionHeader = ({
headerCounter,
headerInfo,
headerActions,
+ headerSecondaryContent,
+ secondaryContentId,
+ hideHeaderText,
headingTagOverride,
expanded,
ariaControls,
@@ -472,6 +513,24 @@ export const ExpandableSectionHeader = ({
warnOnce(componentName, `The \`headerActions\` prop is only supported for the "container" and "default" variants.`);
}
+ if (headerSecondaryContent && !variantSupportsHeaderSecondaryContent(variant) && isDevelopment) {
+ warnOnce(
+ componentName,
+ `The \`headerSecondaryContent\` prop is only supported for the "container" and "default" variants.`
+ );
+ }
+
+ if (hideHeaderText && !headerText && isDevelopment) {
+ warnOnce(componentName, 'The `hideHeaderText` prop requires `headerText` to be set.');
+ }
+
+ if (hideHeaderText && !variantSupportsHeaderSecondaryContent(variant) && isDevelopment) {
+ warnOnce(
+ componentName,
+ `The \`hideHeaderText\` prop is only supported for the "container" and "default" variants.`
+ );
+ }
+
if (headerDescription && !variantSupportsDescription(variant) && isDevelopment) {
warnOnce(componentName, `The \`headerDescription\` prop is not supported for the ${variant} variant.`);
}
@@ -504,7 +563,10 @@ export const ExpandableSectionHeader = ({
className,
wrapperClassName,
expanded && styles.expanded,
- !expanded && (!headerActions || headerDescription) && styles['wrapper-not-expanded-without-actions']
+ !expanded &&
+ (!headerActions || headerDescription) &&
+ !(headerSecondaryContent && variantSupportsHeaderSecondaryContent(variant)) &&
+ styles['wrapper-not-expanded-without-actions']
)}
descriptionId={descriptionId}
headerDescription={headerDescription}
@@ -512,6 +574,9 @@ export const ExpandableSectionHeader = ({
headerInfo={headerInfo}
headerActions={headerActions}
headerEndInfo={headerEndInfo}
+ headerSecondaryContent={headerSecondaryContent}
+ secondaryContentId={secondaryContentId}
+ hideHeaderText={hideHeaderText}
headingTagOverride={headingTagOverride}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
diff --git a/src/expandable-section/interfaces.ts b/src/expandable-section/interfaces.ts
index 89d6359e9fb..728dab5d217 100644
--- a/src/expandable-section/interfaces.ts
+++ b/src/expandable-section/interfaces.ts
@@ -110,4 +110,17 @@ export interface ExpandableSectionProps extends BaseComponentProps {
* Actions for the header. Use with the default or container, default, inline or stacked variants.
*/
headerActions?: React.ReactNode;
+
+ /**
+ * Secondary content displayed next to the header text, such as an interactive control or supporting text.
+ * Use with the default or container variant.
+ */
+ headerSecondaryContent?: React.ReactNode;
+
+ /**
+ * 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.
+ */
+ hideHeaderText?: boolean;
}
diff --git a/src/expandable-section/internal.tsx b/src/expandable-section/internal.tsx
index 33aa9c14d85..038b57536a2 100644
--- a/src/expandable-section/internal.tsx
+++ b/src/expandable-section/internal.tsx
@@ -17,7 +17,7 @@ import { ExpandableSectionContainer } from './expandable-section-container';
import { ExpandableSectionHeader } from './expandable-section-header';
import { ExpandableSectionProps } from './interfaces';
import { InternalVariant } from './internal-interfaces';
-import { variantSupportsDescription } from './utils';
+import { variantSupportsDescription, variantSupportsHeaderSecondaryContent } from './utils';
import analyticsSelectors from './analytics-metadata/styles.css.js';
import styles from './styles.css.js';
@@ -63,6 +63,8 @@ export default function InternalExpandableSection({
headerDescription,
headerInfo,
headerActions,
+ headerSecondaryContent,
+ hideHeaderText,
headingTagOverride,
disableContentPaddings,
headerAriaLabel,
@@ -79,6 +81,7 @@ export default function InternalExpandableSection({
const controlId = useUniqueId();
const triggerControlId = `${controlId}-trigger`;
const descriptionId = `${controlId}-description`;
+ const secondaryContentId = `${controlId}-secondary`;
const baseProps = getBaseProps(props);
const [expanded, setExpanded] = useControllable(controlledExpanded, onChange, defaultExpanded, {
@@ -138,12 +141,20 @@ export default function InternalExpandableSection({
// Map stacked variant to container to avoid code duplication
const baseVariant: InternalVariant = variant === 'stacked' ? 'container' : variant;
+ const headerTextHidden = hideHeaderText && variantSupportsHeaderSecondaryContent(baseVariant);
+
return (
.header-wrapper {
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ }
+ }
+
+ &-secondary-content {
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ min-inline-size: 0;
+ max-inline-size: 100%;
+ margin-inline-end: auto;
+ }
+
&-button,
&-container-button {
@include focus-visible.when-visible {
@@ -325,9 +349,17 @@ $nav-caret-pad-one-theme: 12px; // 24px minus the one-theme x-small (12px) icon
&-text {
/* used in test-utils */
+
+ &-hidden {
+ @include styles.awsui-util-hide;
+ }
}
}
+.header-container-button:has(> .header-text-hidden) > .icon-container {
+ margin-inline-end: 0;
+}
+
// Make sure that actions can wrap for default expandable section headers.
// The compact variant is used in chart popovers and should not wrap.
:not(.wrapper-compact) > .header-actions-wrapper {
diff --git a/src/expandable-section/utils.ts b/src/expandable-section/utils.ts
index 528a60ed6c5..8b3fae3a116 100644
--- a/src/expandable-section/utils.ts
+++ b/src/expandable-section/utils.ts
@@ -12,6 +12,10 @@ export function variantSupportsActions(variant: InternalVariant) {
return variantIsOneOf(variant, ['container', 'compact', 'default', 'inline']);
}
+export function variantSupportsHeaderSecondaryContent(variant: InternalVariant) {
+ return variantIsOneOf(variant, ['container', 'default']);
+}
+
export function variantSupportsInfoLink(variant: InternalVariant) {
return variantIsOneOf(variant, ['container', 'compact']);
}
diff --git a/src/header/__tests__/header.test.tsx b/src/header/__tests__/header.test.tsx
index 5634e2e6af3..72f433c85ac 100644
--- a/src/header/__tests__/header.test.tsx
+++ b/src/header/__tests__/header.test.tsx
@@ -62,6 +62,18 @@ test('supports title tag name override', () => {
expect(wrapper.getElement()).toHaveClass(styles['root-variant-h2']);
});
+test('supports overriding the title tag with h6', () => {
+ const { wrapper } = renderHeader();
+ expect(wrapper.find('h2')).toBeNull();
+ expect(wrapper.find('h6')!.getElement()).toHaveTextContent('title');
+});
+
+test('renders the title without heading semantics when overridden to div', () => {
+ const { wrapper } = renderHeader();
+ expect(wrapper.find('h1,h2,h3,h4,h5,h6')).toBeNull();
+ expect(wrapper.findHeadingText().getElement()).toHaveTextContent('title');
+});
+
test('renders h1 variant', () => {
const { wrapper } = renderHeader();
expect(wrapper.find('h1')!.getElement()).toHaveTextContent('title');
diff --git a/src/header/interfaces.ts b/src/header/interfaces.ts
index de2754fde51..08731b3a9e9 100644
--- a/src/header/interfaces.ts
+++ b/src/header/interfaces.ts
@@ -22,7 +22,8 @@ export interface HeaderProps extends BaseComponentProps {
variant?: HeaderProps.Variant;
/**
* 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.
*/
headingTagOverride?: HeaderProps.HeadingTag;
/**
@@ -46,5 +47,5 @@ export interface HeaderProps extends BaseComponentProps {
export namespace HeaderProps {
export type Variant = 'h1' | 'h2' | 'h3' | 'awsui-h1-sticky';
- export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
+ export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div';
}
diff --git a/src/header/internal.tsx b/src/header/internal.tsx
index 1a42ac1010c..24d37f7095b 100644
--- a/src/header/internal.tsx
+++ b/src/header/internal.tsx
@@ -27,6 +27,11 @@ interface InternalHeaderProps extends SomeRequired, Inte
__headingTagRef?: MutableRefObject;
__headingTagTabIndex?: number;
__inExpandableSection?: boolean;
+ /**
+ * Rendered in the title row but outside the heading tag, so content beside the title is not
+ * nested inside the heading element.
+ */
+ __additionalTitleContent?: React.ReactNode;
}
export default function InternalHeader({
@@ -42,6 +47,7 @@ export default function InternalHeader({
__headingTagRef,
__headingTagTabIndex,
__inExpandableSection,
+ __additionalTitleContent,
...restProps
}: InternalHeaderProps) {
const isMobile = useMobile();
@@ -118,6 +124,9 @@ export default function InternalHeader({
)}
+ {__additionalTitleContent && (
+
{__additionalTitleContent}
+ )}
{actions && (
.additional-title-content) > .title-variant-h2.title-in-expandable-section.refresh {
+ padding-block-start: 0;
+ min-block-size: awsui.$size-vertical-input;
+ display: flex;
+ align-items: center;
+
+ @include theming.one-theme-only {
+ min-block-size: calc(#{awsui.$size-vertical-input} - 2 * #{awsui.$border-width-button});
+ }
+}
+
.root-no-actions.root-has-description > .main > :not(.refresh) {
&.title-variant-h2,
&.title-variant-h3 {