From 40c4239f44727417ea5861ccbb69d83acb2d895b Mon Sep 17 00:00:00 2001 From: Vahab Valiyev Date: Mon, 26 Oct 2026 16:53:03 +0100 Subject: [PATCH] chore: Adds internal style api v2 to container --- pages/container/style-api-v2.page.tsx | 78 +++++++++++++++ pages/container/style-api-v2.scss | 36 +++++++ .../__snapshots__/style-docs.test.ts.snap | 45 +++++++++ src/container/__tests__/style-api.test.tsx | 40 ++++++++ src/container/internal.tsx | 14 ++- src/container/styles.scss | 94 ++++++++++++++++--- src/internal/styles/forms/mixins.scss | 7 +- 7 files changed, 298 insertions(+), 16 deletions(-) create mode 100644 pages/container/style-api-v2.page.tsx create mode 100644 pages/container/style-api-v2.scss create mode 100644 src/container/__tests__/style-api.test.tsx diff --git a/pages/container/style-api-v2.page.tsx b/pages/container/style-api-v2.page.tsx new file mode 100644 index 00000000000..71615e653b8 --- /dev/null +++ b/pages/container/style-api-v2.page.tsx @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { Box, ColumnLayout, Container, ContainerProps, Header } from '~components'; + +import { SimplePage } from '../app/templates'; +import image169 from './images/16-9.png'; + +import styles from './style-api-v2.scss'; + +function StyledContainer(props: ContainerProps) { + return ( + + ); +} + +function Permutation({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ + {label} + + {children} +
+ ); +} + +export default function () { + return ( + + + + Header} footer="Footer"> + Content + + + + Header} + media={{ content: , position: 'top', height: 120 }} + > + Content + + + + First} footer="First footer"> + Content + + Second} footer="Second footer"> + Content + + + + Header}>Content + + + Content + + + + Header filled by its own element + + } + > + Content + + + + + ); +} diff --git a/pages/container/style-api-v2.scss b/pages/container/style-api-v2.scss new file mode 100644 index 00000000000..111168020ba --- /dev/null +++ b/pages/container/style-api-v2.scss @@ -0,0 +1,36 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~components/styling/container'; +@use '../style-api/palette' as palette; + +.container { + @include container.root( + $background: palette.$surface, + $border-color: palette.$border-accent, + $border-width: 2px, + $border-style: solid, + $box-shadow: 0 4px 12px palette.$subtle-active, + $border-radius: 24px + ); +} + +// Only the block-end side of the header border is visible: it is the divider between the header and the content. +.header { + @include container.header( + $background: palette.$genai-surface, + $border-color: palette.$genai, + $border-block-width: 0 2px + ); +} + +// Only the block-start side of the footer border is visible: it is the divider between the content and the footer. +.footer { + @include container.footer( + $background: palette.$info-surface, + $border-color: palette.$info, + $border-block-width: 3px 0 + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap index 53c0c16de39..4c060129cf1 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap @@ -109,6 +109,50 @@ exports[`Style API docs definition for checkbox matches the snapshot: checkbox 1 } `; +exports[`Style API docs definition for container matches the snapshot: container 1`] = ` +{ + "slots": [ + { + "name": "root", + "properties": [ + "background", + "border", + "box-shadow", + ], + "propertyDescriptions": {}, + "tokenDescriptions": { + "border-radius": "Also rounds the header, footer and media corners. Adjoining corners of stacked containers stay square.", + }, + "tokens": [ + "border-radius", + ], + }, + { + "description": "Rendered when \`header\` is set. The block-end border is the divider between the header and the content.", + "name": "header", + "properties": [ + "background", + "border", + ], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [], + }, + { + "description": "Rendered when \`footer\` is set. The block-start border is the divider between the content and the footer.", + "name": "footer", + "properties": [ + "background", + "border", + ], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [], + }, + ], +} +`; + exports[`Style API docs definition for dropdown matches the snapshot: dropdown 1`] = ` { "slots": [ @@ -274,6 +318,7 @@ exports[`Style API docs list of components with a Style API doc matches the snap "badge", "button", "checkbox", + "container", "dropdown", "input", "link", diff --git a/src/container/__tests__/style-api.test.tsx b/src/container/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..ff866ee716b --- /dev/null +++ b/src/container/__tests__/style-api.test.tsx @@ -0,0 +1,40 @@ +// 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 Container, { ContainerProps } from '../../../lib/components/container'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +function renderContainer(props: ContainerProps & { styleClassNames?: Record } = {}) { + const { container } = render(Content); + return { container, wrapper: createWrapper(container).findContainer()! }; +} + +const styleClassNames = { root: 'a', header: 'b', footer: 'c', unknown: 'd' }; + +describe('Container Style API v2', () => { + test('applies each slot class to its element and ignores unknown keys', () => { + const { container, wrapper } = renderContainer({ header: 'Header', footer: 'Footer', styleClassNames }); + expect(wrapper.getElement()).toHaveClass('a'); + expect(wrapper.findHeader()!.getElement()).toHaveClass('b'); + expect(wrapper.findFooter()!.getElement()).toHaveClass('c'); + expect(container.querySelectorAll('.a, .b, .c')).toHaveLength(3); + expect(container.querySelector('.d')).toBeNull(); + }); + + test('applies the root class in the stacked variant and with media', () => { + const { container, wrapper } = renderContainer({ + variant: 'stacked', + media: { content: }, + styleClassNames, + }); + expect(wrapper.getElement()).toHaveClass('a'); + expect(container.querySelectorAll('.a')).toHaveLength(1); + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderContainer({ header: 'Header', footer: 'Footer', styleClassNames }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/container/internal.tsx b/src/container/internal.tsx index e5e8c6a6aa4..c7ac8f4bf01 100644 --- a/src/container/internal.tsx +++ b/src/container/internal.tsx @@ -51,6 +51,15 @@ export interface InternalContainerProps extends Omit, * is necessary for components that use container as a root node internally so that their test-utils wrapper is not detached. */ __contentKey?: string; + + styleClassNames?: StyleClassNames; +} + +// Style API v2 +export interface StyleClassNames { + root?: string; + header?: string; + footer?: string; } export const InternalContainerAsSubstep = React.forwardRef( @@ -93,6 +102,7 @@ const InternalContainer = React.forwardRef(function InternalContainer( __funnelSubStepProps, __subStepRef, __contentKey, + styleClassNames, ...restProps }: InternalContainerProps, ref: React.Ref @@ -158,6 +168,7 @@ const InternalContainer = React.forwardRef(function InternalContainer( {...baseProps} {...__funnelSubStepProps} className={clsx( + styleClassNames?.root, baseProps.className, styles.root, styles[`variant-${variant}`], @@ -201,6 +212,7 @@ const InternalContainer = React.forwardRef(function InternalContainer(
{footer && (
& { + border-end-start-radius: 0; + border-end-end-radius: 0; + } + &.with-paddings { padding-block: awsui.$space-scaled-s; padding-inline: awsui.$space-container-horizontal; @@ -296,3 +345,26 @@ @include shared.divider; } } + +// Header and footer corners follow the root's: square where the media sits and where two stacked containers adjoin. +.root.with-top-media > .content-wrapper > .header { + border-start-start-radius: 0; + border-start-end-radius: 0; +} +.root.with-side-media > .content-wrapper > .header { + border-start-start-radius: 0; + border-end-start-radius: 0; +} +.root.with-side-media > .content-wrapper > .footer { + border-end-start-radius: 0; +} +.root.variant-stacked:not(:last-child) > .content-wrapper > .header, +.root.variant-stacked:not(:last-child) > .content-wrapper > .footer, +.root.variant-stacked:not(:last-child) > .content-wrapper-fit-height { + border-end-start-radius: 0; + border-end-end-radius: 0; +} +.root.variant-stacked + .root.variant-stacked > .content-wrapper > .header { + border-start-start-radius: 0; + border-start-end-radius: 0; +} diff --git a/src/internal/styles/forms/mixins.scss b/src/internal/styles/forms/mixins.scss index 7188bf455a1..537c54262c7 100644 --- a/src/internal/styles/forms/mixins.scss +++ b/src/internal/styles/forms/mixins.scss @@ -277,10 +277,9 @@ } } -@mixin container-style { - // Custom radius from the container style API if set, otherwise the token. Stacked - // containers still flatten adjacent corners because those rules set a literal 0. - $radius: var(#{custom-props.$containerStyleBorderRadius}, #{awsui.$border-radius-container}); +// Custom radius from the container style API if set, otherwise the token. Stacked +// containers still flatten adjacent corners because those rules set a literal 0. +@mixin container-style($radius: var(#{custom-props.$containerStyleBorderRadius}, #{awsui.$border-radius-container})) { border-start-start-radius: $radius; border-start-end-radius: $radius; border-end-start-radius: $radius;