From ce0be22945426492d751fbcb1536aa9870cd5257 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 16 Feb 2026 23:01:03 +0100 Subject: [PATCH 1/3] feat: add x-large and xx-large sizes to modal --- .../__snapshots__/documenter.test.ts.snap | 6 ++++-- src/modal/__tests__/modal.test.tsx | 2 +- src/modal/interfaces.ts | 6 +++--- src/modal/styles.scss | 19 ++++++++++++++----- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 01fbd65f7e..db61226c82 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -17542,8 +17542,8 @@ of the most recent getModalRoot call as an argument.", { "defaultValue": "'medium'", "description": "Sets the width of the modal. \`max\` uses variable width up to the -largest size allowed by the design guidelines. Other sizes -(\`small\`/\`medium\`/\`large\`) have fixed widths.", +largest size allowed by the design guidelines. Other sizes: +\`small\` (320px), \`medium\` (600px), \`large\` (820px), \`x-large\` (1024px), \`xx-large\` (1280px).", "inlineType": { "name": "ModalProps.Size", "type": "union", @@ -17552,6 +17552,8 @@ largest size allowed by the design guidelines. Other sizes "max", "medium", "large", + "x-large", + "xx-large", ], }, "name": "size", diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx index 94d43364e1..d36bb7ab3d 100644 --- a/src/modal/__tests__/modal.test.tsx +++ b/src/modal/__tests__/modal.test.tsx @@ -112,7 +112,7 @@ describe('Modal component', () => { describe('size property', () => { it('displays correct size', () => { - (['small', 'medium', 'large', 'max'] as ModalProps.Size[]).forEach(size => { + (['small', 'medium', 'large', 'x-large', 'xx-large', 'max'] as ModalProps.Size[]).forEach(size => { const wrapper = renderModal({ size }); expect(wrapper.findDialog().getElement()).toHaveClass(styles[size]); }); diff --git a/src/modal/interfaces.ts b/src/modal/interfaces.ts index f13a4b90c3..de33a935b9 100644 --- a/src/modal/interfaces.ts +++ b/src/modal/interfaces.ts @@ -26,8 +26,8 @@ export interface BaseModalProps { export interface ModalProps extends BaseComponentProps, BaseModalProps { /** * Sets the width of the modal. `max` uses variable width up to the - * largest size allowed by the design guidelines. Other sizes - * (`small`/`medium`/`large`) have fixed widths. + * largest size allowed by the design guidelines. Other sizes: + * `small` (320px), `medium` (600px), `large` (820px), `x-large` (1024px), `xx-large` (1280px). */ size?: ModalProps.Size; /** @@ -81,7 +81,7 @@ export interface ModalProps extends BaseComponentProps, BaseModalProps { } export namespace ModalProps { - export type Size = 'small' | 'medium' | 'large' | 'max'; + export type Size = 'small' | 'medium' | 'large' | 'x-large' | 'xx-large' | 'max'; export interface DismissDetail { reason: string; diff --git a/src/modal/styles.scss b/src/modal/styles.scss index dfaf049e6c..270bd03c5b 100644 --- a/src/modal/styles.scss +++ b/src/modal/styles.scss @@ -33,7 +33,6 @@ $modal-z-index: 5000; .focus-lock { align-self: flex-start; - margin-block: auto; margin-inline: auto; padding-block: awsui.$space-s; padding-inline: 0; @@ -66,11 +65,21 @@ $modal-z-index: 5000; max-inline-size: 820px; } - &.max.breakpoint-xs { + &.x-large { + max-inline-size: 1024px; + } + + &.xx-large { + max-inline-size: 1280px; + } + + &.breakpoint-xs { // viewport - (closed app layout panel widths + 20px on each side) - max-inline-size: calc(100vw - (2 * 4 * #{styles.$base-size} + #{awsui.$space-xxxl})); - margin-block: auto; - margin-inline: auto; + inline-size: calc(100vw - (2 * 4 * #{styles.$base-size} + #{awsui.$space-xxxl})); + &.max { + margin-block: auto; + margin-inline: auto; + } } } From 24d93c54cc493168fca1fdcee134fa2523182c5a Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 20 Feb 2026 16:15:23 +0100 Subject: [PATCH 2/3] chore: use breakpoint-l for max size modal width constraint --- src/modal/internal.tsx | 2 +- src/modal/styles.scss | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx index 79e9e88c31..071402c655 100644 --- a/src/modal/internal.tsx +++ b/src/modal/internal.tsx @@ -108,7 +108,7 @@ function PortaledModal({ const instanceUniqueId = useUniqueId(); const headerId = `${rest.id || instanceUniqueId}-header`; const lastMouseDownElementRef = useRef(null); - const [breakpoint, breakpointsRef] = useContainerBreakpoints(['xs']); + const [breakpoint, breakpointsRef] = useContainerBreakpoints(['l']); const i18n = useInternalI18n('modal'); const closeAriaLabel = i18n('closeAriaLabel', rest.closeAriaLabel); diff --git a/src/modal/styles.scss b/src/modal/styles.scss index 270bd03c5b..d7bf619ddb 100644 --- a/src/modal/styles.scss +++ b/src/modal/styles.scss @@ -73,13 +73,11 @@ $modal-z-index: 5000; max-inline-size: 1280px; } - &.breakpoint-xs { + &.breakpoint-l.max { // viewport - (closed app layout panel widths + 20px on each side) - inline-size: calc(100vw - (2 * 4 * #{styles.$base-size} + #{awsui.$space-xxxl})); - &.max { - margin-block: auto; - margin-inline: auto; - } + max-inline-size: calc(100vw - (2 * 4 * #{styles.$base-size} + #{awsui.$space-xxxl})); + margin-block: auto; + margin-inline: auto; } } From 747253279e5057c308f98f597f5822308251d851 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 16 Feb 2026 23:21:02 +0100 Subject: [PATCH 3/3] feat: Add position prop to modal --- .../__snapshots__/documenter.test.ts.snap | 22 +++++++++++++++++++ src/modal/__tests__/modal.test.tsx | 9 ++++++++ src/modal/index.tsx | 14 ++++++++++-- src/modal/interfaces.ts | 12 ++++++++++ src/modal/internal.tsx | 8 ++++++- src/modal/styles.scss | 8 +++++++ 6 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index db61226c82..b24be0c032 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -17520,6 +17520,28 @@ render to an element under \`document.body\`.", "optional": true, "type": "HTMLElement", }, + { + "defaultValue": "'center'", + "description": "Controls the vertical positioning of the modal. + +- \`center\` (default) - Modal is vertically centered in viewport and re-centers + when content height changes. Use for dialogs with static, predictable content. + +- \`top\` - Modal anchors at fixed distance and grows downward + as content expands. Use when content changes dynamically to prevent disruptive + vertical repositioning that causes users to lose focus.", + "inlineType": { + "name": "ModalProps.Position", + "type": "union", + "values": [ + "center", + "top", + ], + }, + "name": "position", + "optional": true, + "type": "string", + }, { "description": "Use this property when \`getModalRoot\` is used to clean up the modal root element after a user closes the dialog. The function receives the return value diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx index d36bb7ab3d..a185397d0c 100644 --- a/src/modal/__tests__/modal.test.tsx +++ b/src/modal/__tests__/modal.test.tsx @@ -119,6 +119,15 @@ describe('Modal component', () => { }); }); + describe('position property', () => { + it('displays correct position', () => { + (['center', 'top'] as ModalProps.Position[]).forEach(position => { + const wrapper = renderModal({ position }); + expect(wrapper.findFocusLock().getElement()).toHaveClass(styles[`position-${position}`]); + }); + }); + }); + describe('dismiss on click', () => { it('closes the dialog when clicked on the overlay section of the container', () => { const onDismissSpy = jest.fn(); diff --git a/src/modal/index.tsx b/src/modal/index.tsx index c2b6982958..4195ce68c8 100644 --- a/src/modal/index.tsx +++ b/src/modal/index.tsx @@ -70,7 +70,7 @@ function ModalWithAnalyticsFunnel({ ); } -export default function Modal({ size = 'medium', ...props }: ModalProps) { +export default function Modal({ size = 'medium', position = 'center', ...props }: ModalProps) { const { isInFunnel } = useFunnel(); const analyticsMetadata = getAnalyticsMetadataProps(props as BasePropsWithAnalyticsMetadata); const baseComponentProps = useBaseComponent( @@ -78,6 +78,7 @@ export default function Modal({ size = 'medium', ...props }: ModalProps) { { props: { size, + position, disableContentPaddings: props.disableContentPaddings, flowType: analyticsMetadata.flowType, }, @@ -95,12 +96,21 @@ export default function Modal({ size = 'medium', ...props }: ModalProps) { analyticsMetadata={analyticsMetadata} baseComponentProps={baseComponentProps} size={size} + position={position} {...props} /> ); } - return ; + return ( + + ); } applyDisplayName(Modal, 'Modal'); diff --git a/src/modal/interfaces.ts b/src/modal/interfaces.ts index de33a935b9..99ff019aaf 100644 --- a/src/modal/interfaces.ts +++ b/src/modal/interfaces.ts @@ -30,6 +30,17 @@ export interface ModalProps extends BaseComponentProps, BaseModalProps { * `small` (320px), `medium` (600px), `large` (820px), `x-large` (1024px), `xx-large` (1280px). */ size?: ModalProps.Size; + /** + * Controls the vertical positioning of the modal. + * + * - `center` (default) - Modal is vertically centered in viewport and re-centers + * when content height changes. Use for dialogs with static, predictable content. + * + * - `top` - Modal anchors at fixed distance and grows downward + * as content expands. Use when content changes dynamically to prevent disruptive + * vertical repositioning that causes users to lose focus. + */ + position?: ModalProps.Position; /** * Determines whether the modal is displayed on the screen. Modals are hidden by default. * Set this property to `true` to show them. @@ -82,6 +93,7 @@ export interface ModalProps extends BaseComponentProps, BaseModalProps { export namespace ModalProps { export type Size = 'small' | 'medium' | 'large' | 'x-large' | 'xx-large' | 'max'; + export type Position = 'center' | 'top'; export interface DismissDetail { reason: string; diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx index 071402c655..be6b41fa89 100644 --- a/src/modal/internal.tsx +++ b/src/modal/internal.tsx @@ -94,6 +94,7 @@ function PortaledModal({ children, footer, disableContentPaddings, + position = 'center', onButtonClick = () => {}, onDismiss, __internalRootRef, @@ -247,7 +248,12 @@ function PortaledModal({ style={footerHeight ? { scrollPaddingBottom: footerHeight } : undefined} data-awsui-referrer-id={subStepRef.current?.id || referrerId} > - +