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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@
{
"path": "lib/components/internal/widget-exports.js",
"brotli": false,
"limit": "1380 kB",
"limit": "1400 kB",
"ignore": "react-dom"
}
],
Expand Down
30 changes: 29 additions & 1 deletion pages/expandable-section/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import React from 'react';
import Button from '~components/button';
import Container from '~components/container';
import ExpandableSection, { ExpandableSectionProps } from '~components/expandable-section';
import InternalExpandableSection from '~components/expandable-section/internal';
import Header from '~components/header';
import SpaceBetween from '~components/space-between';
import StatusIndicator from '~components/status-indicator';
import Table from '~components/table';

import createPermutations from '../utils/permutations';
Expand Down Expand Up @@ -158,14 +161,39 @@ const permutations = createPermutations<ExpandableSectionProps>([
children: ['Sample content'],
},
]);
/* eslint-enable react/jsx-key */

const endIconPermutations = createPermutations<ExpandableSectionProps>([
{
variant: ['default', 'footer', 'navigation', 'container'],
headerText: [
'Short Header',
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
],
headerActions: [
undefined,
<SpaceBetween direction="horizontal" size="xs" alignItems="center">
<Button variant="inline-link">Action</Button>
<Button>Another action</Button>
<StatusIndicator>Information</StatusIndicator>
</SpaceBetween>,
],
},
]);

export default function ExpandableSectionPermutations() {
return (
<>
<h1>Expandable Section permutations</h1>
<ScreenshotArea disableAnimations={true}>
<PermutationsView permutations={permutations} render={permutation => <ExpandableSection {...permutation} />} />
<PermutationsView
permutations={endIconPermutations}
render={permutation => (
<InternalExpandableSection {...permutation} defaultExpanded={true} __expandIconPosition="end">
Variant {permutation.variant} section content
</InternalExpandableSection>
)}
/>
</ScreenshotArea>
</>
);
Expand Down
24 changes: 24 additions & 0 deletions pages/expandable-section/test.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Button from '~components/button';
import ExpandableSection from '~components/expandable-section';
import InternalExpandableSection from '~components/expandable-section/internal';

export default function ExpandableSectionTestPage() {
return (
Expand All @@ -15,6 +17,28 @@ export default function ExpandableSectionTestPage() {
After you enable your S3 bucket for static website hosting, web browsers can access your content through the
Amazon S3 website endpoint for the bucket.
</ExpandableSection>

<div data-testid="end-position-single-row">
<InternalExpandableSection
variant="default"
headerText="End position section"
__expandIconPosition="end"
headerActions={<Button>Action</Button>}
>
Content of the end-position expandable section used for keyboard toggle and focus tests.
</InternalExpandableSection>
</div>

<div data-testid="end-position-wrapping-header">
<InternalExpandableSection
variant="default"
headerText="This is a long wrapping header text that is intentionally verbose and extended to ensure it wraps to multiple lines when rendered in the test viewport used by integration tests"
__expandIconPosition="end"
headerActions={<Button>Action</Button>}
>
Content of the wrapping-header end-position expandable section used for the below-caret click test.
</InternalExpandableSection>
</div>
</article>
);
}
37 changes: 34 additions & 3 deletions pages/side-navigation/collapsed.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,45 @@ const items: SideNavigationProps.Item[] = [
{ type: 'link', text: 'Settings', href: '#/settings', icon: <Icon name="settings" /> },
{
type: 'section',
text: 'Resources',
text: 'Section',
items: [
{ type: 'link', text: 'Announcements', href: '#/announcements', icon: <Icon name="announcement" /> },
{ type: 'link', text: 'Team', href: '#/team', icon: <Icon name="group" /> },
{ type: 'link', text: 'Networking', href: '#/networking', icon: <Icon name="share" /> },
],
},
{ type: 'divider' },
{
type: 'link-group',
text: 'Link group',
href: '#/',
items: [
{ type: 'link', text: 'Announcements', href: '#/announcements3', icon: <Icon name="announcement" /> },
{ type: 'link', text: 'Team', href: '#/team3', icon: <Icon name="group" /> },
{ type: 'link', text: 'Networking', href: '#/networking3', icon: <Icon name="share" /> },
],
},
{
type: 'expandable-link-group',
text: 'Expandable link group',
href: '#/projects',
icon: <Icon name="folder" />,
items: [
{ type: 'link', text: 'Project 1', href: '#/projects/1' },
{ type: 'link', text: 'Project 2', href: '#/projects/2' },
{ type: 'link', text: 'Project 3', href: '#/projects/3' },
],
},
{ type: 'link', text: 'Documentation', href: '#/docs', external: true },
{
type: 'section-group',
title: 'Section group',
items: [
{ type: 'link', text: 'Announcements', href: '#/announcements2', icon: <Icon name="announcement" /> },
{ type: 'link', text: 'Team', href: '#/team2', icon: <Icon name="group" /> },
{ type: 'link', text: 'Networking', href: '#/networking2', icon: <Icon name="share" /> },
],
},
];

const COLLAPSED_WIDTH = 52;
Expand Down Expand Up @@ -49,7 +80,7 @@ export default function SideNavigationCollapsedPage() {
}

return (
<div style={{ display: 'flex', blockSize: '100vh', overflow: 'hidden' }}>
<div style={{ display: 'flex', blockSize: '100vh', overflow: 'auto' }}>
{/* Nav panel */}
<nav
id="side-navigation-panel"
Expand All @@ -64,7 +95,7 @@ export default function SideNavigationCollapsedPage() {
flexDirection: 'column',
}}
>
<div style={{ inlineSize: collapsed ? 'auto' : EXPANDED_WIDTH, boxSizing: 'border-box' }}>
<div style={{ inlineSize: collapsed ? 'auto' : EXPANDED_WIDTH, boxSizing: 'border-box', overflow: 'auto' }}>
{/* Toggle button at top */}
<div
style={{
Expand Down
8 changes: 8 additions & 0 deletions src/__integ__/__snapshots__/themes.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "compact" 1`] =
"color-background-segment-wrapper": "#ffffff",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#0073bb",
"color-background-side-navigation-item-icon-hover": "#f1faff",
"color-background-skeleton": "#eaeded",
"color-background-skeleton-wave": "#f6f6f9",
"color-background-slider-handle-active": "#0a4a74",
Expand Down Expand Up @@ -1164,6 +1165,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "dark" 1`] = `
"color-background-segment-wrapper": "#2a2e33",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#00a1c9",
"color-background-side-navigation-item-icon-hover": "#414750",
"color-background-skeleton": "#2a2e33",
"color-background-skeleton-wave": "#414750",
"color-background-slider-handle-active": "#44b9d6",
Expand Down Expand Up @@ -2146,6 +2148,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "light" 1`] = `
"color-background-segment-wrapper": "#ffffff",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#0073bb",
"color-background-side-navigation-item-icon-hover": "#f1faff",
"color-background-skeleton": "#eaeded",
"color-background-skeleton-wave": "#f6f6f9",
"color-background-slider-handle-active": "#0a4a74",
Expand Down Expand Up @@ -3128,6 +3131,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "reduced-motion"
"color-background-segment-wrapper": "#ffffff",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#0073bb",
"color-background-side-navigation-item-icon-hover": "#f1faff",
"color-background-skeleton": "#eaeded",
"color-background-skeleton-wave": "#f6f6f9",
"color-background-slider-handle-active": "#0a4a74",
Expand Down Expand Up @@ -4110,6 +4114,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh"
"color-background-segment-wrapper": "#ffffff",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#006ce0",
"color-background-side-navigation-item-icon-hover": "#f0fbff",
"color-background-skeleton": "#ebebf0",
"color-background-skeleton-wave": "#f6f6f9",
"color-background-slider-handle-active": "#004a9e",
Expand Down Expand Up @@ -5092,6 +5097,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh-
"color-background-segment-wrapper": "#ffffff",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#006ce0",
"color-background-side-navigation-item-icon-hover": "#f0fbff",
"color-background-skeleton": "#ebebf0",
"color-background-skeleton-wave": "#f6f6f9",
"color-background-slider-handle-active": "#004a9e",
Expand Down Expand Up @@ -6074,6 +6080,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh-
"color-background-segment-wrapper": "#0f141a",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#42b4ff",
"color-background-side-navigation-item-icon-hover": "#333843",
"color-background-skeleton": "#232b37",
"color-background-skeleton-wave": "#333843",
"color-background-slider-handle-active": "#75cfff",
Expand Down Expand Up @@ -7056,6 +7063,7 @@ exports[`CSS Custom Properties match previous snapshot for mode "visual-refresh-
"color-background-segment-wrapper": "#161d26",
"color-background-side-navigation-item-active": "transparent",
"color-background-side-navigation-item-active-collapsed": "#42b4ff",
"color-background-side-navigation-item-icon-hover": "#333843",
"color-background-skeleton": "#232b37",
"color-background-skeleton-wave": "#333843",
"color-background-slider-handle-active": "#75cfff",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13440,7 +13440,7 @@ manner even if you provide a value for this property.",
"type": "boolean",
},
{
"description": "Determines whether the content section's default padding is removed. This default padding is only present for the \`container\` variant.",
"description": "Determines whether the default padding around the content section is removed.",
"name": "disableContentPaddings",
"optional": true,
"type": "boolean",
Expand Down
140 changes: 140 additions & 0 deletions src/expandable-section/__integ__/expandable-section.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';

import createWrapper from '../../../lib/components/test-utils/selectors';

import styles from '../../../lib/components/expandable-section/styles.selectors.js';

const focusTargetSelector = '#focus-target';

const expandableSectionWrapper = createWrapper().findExpandableSection();
Expand Down Expand Up @@ -37,3 +39,141 @@ describe('Expandable Section', () => {
})
);
});

// --- End-position integration tests ---
// These sections use headerActions to trigger the OUTSIDE caret button path
// (renderIconOutsideHeader = iconAtEnd && (isContainer || !!actions)).
// The outside <button> with .icon-container-end is the element exercised by the #9 fixes:
// stopPropagation on keydown/keyup, focus ring, and align-self:stretch clickable area.

const endSingleRowWrapper = createWrapper().find('[data-testid="end-position-single-row"]').findExpandableSection();
const endWrappingWrapper = createWrapper().find('[data-testid="end-position-wrapping-header"]').findExpandableSection();

// The outside caret button: a native <button> with class .icon-container-end, rendered separately
// from the headerButton when actions are present. This is the element that receives the #9 fixes.
const endCaretButtonSelector = endSingleRowWrapper
.findHeader()
.find(`button.${styles['icon-container-end']}`)
.toSelector();
const wrappingCaretButtonSelector = endWrappingWrapper
.findHeader()
.find(`button.${styles['icon-container-end']}`)
.toSelector();

class EndPositionPage extends BasePageObject {
hasOutline(selector: string) {
return this.browser.execute(selector => {
const element = document.querySelector(selector);
return !!element && getComputedStyle(element).outline.includes('2px');
}, selector);
}

async clickAtPosition(x: number, y: number) {
await this.browser.performActions([
{
type: 'pointer',
id: 'mouse',
parameters: { pointerType: 'mouse' },
actions: [
{ type: 'pointerMove', duration: 0, x, y },
{ type: 'pointerDown', button: 0 },
{ type: 'pointerUp', button: 0 },
],
},
]);
}
}

const setupEndPositionTest = (testFn: (page: EndPositionPage) => Promise<void>) => {
return useBrowser(async browser => {
const page = new EndPositionPage(browser);
await browser.url('#/light/expandable-section/test');
await page.waitForVisible(endSingleRowWrapper.toSelector());
await testFn(page);
});
};

describe('Expandable Section - end icon position', () => {
test(
'Enter toggles an end-position section exactly once',
setupEndPositionTest(async page => {
await page.click(focusTargetSelector);

// Tab order from #focus-target:
// 1. First section headerButton ("Static website hosting")
// 2. End-position section headerButton (text-only, since caret is outside)
// 3. Action <Button> inside headerActions
// 4. Outside caret <button> (ExpandIconButton)
await page.keys(['Tab', 'Tab', 'Tab', 'Tab']);
await expect(page.isFocused(endCaretButtonSelector)).resolves.toBe(true);

// Press Enter — should expand (single toggle, not open+close due to stopPropagation fix)
await page.keys(['Enter']);
await expect(page.isExisting(endSingleRowWrapper.findExpandedContent().toSelector())).resolves.toBe(true);

// Press Enter again — should collapse
await page.keys(['Enter']);
await expect(page.isExisting(endSingleRowWrapper.findExpandedContent().toSelector())).resolves.toBe(false);
})
);

test(
'Space toggles an end-position section exactly once',
setupEndPositionTest(async page => {
await page.click(focusTargetSelector);

// Tab to the outside caret button (4 stops from #focus-target)
await page.keys(['Tab', 'Tab', 'Tab', 'Tab']);
await expect(page.isFocused(endCaretButtonSelector)).resolves.toBe(true);

// Press Space — should expand (single toggle, not open+close due to stopPropagation fix)
await page.keys(['Space']);
await expect(page.isExisting(endSingleRowWrapper.findExpandedContent().toSelector())).resolves.toBe(true);

// Press Space again — should collapse
await page.keys(['Space']);
await expect(page.isExisting(endSingleRowWrapper.findExpandedContent().toSelector())).resolves.toBe(false);
})
);

test(
'end-position caret button shows a focus ring when focused',
setupEndPositionTest(async page => {
await page.click(focusTargetSelector);

// Tab to the outside caret button
await page.keys(['Tab', 'Tab', 'Tab', 'Tab']);
await expect(page.isFocused(endCaretButtonSelector)).resolves.toBe(true);

// Verify focus ring (2px outline) is visible on the caret button
// This exercises the @include focus-highlight(2px) rule on .icon-container-end
await expect(page.hasOutline(endCaretButtonSelector)).resolves.toBe(true);
})
);

test(
'clicking below the caret on a wrapped end-position header toggles the section',
setupEndPositionTest(async page => {
// WHY HEADER-RELATIVE: Computing the click from the caret button's own bbox is vacuous —
// the point always lands inside the button regardless of align-self:stretch. Instead we
// derive y from the HEADER's full height (the tall wrapped row) and x from the caret's
// horizontal center. With align-self:stretch the caret button fills the header height,
// so (caret-x, header-bottom) is INSIDE the button → toggles. Without stretch, the
// caret button is only glyph-tall at the top and restrictClickableArea (actions present)
// makes the header wrapper dead space → no toggle. This genuinely exercises the fix.
const headerSelector = endWrappingWrapper.findHeader().toSelector();
const caretBox = await page.getBoundingBox(wrappingCaretButtonSelector);
const headerBox = await page.getBoundingBox(headerSelector);

// x = horizontal center of the caret button (at the header's inline-end)
const x = Math.round(caretBox.left + caretBox.width / 2);
// y = near the bottom of the tall wrapped header, well below the small caret glyph
const y = Math.round(headerBox.top + headerBox.height - 5);

await page.clickAtPosition(x, y);

// Should expand — the stretched caret button covers this point
await expect(page.isExisting(endWrappingWrapper.findExpandedContent().toSelector())).resolves.toBe(true);
})
);
});
Loading
Loading