From e3c0bbcfcbb05df9c4c70b64145942a9ec6ea600 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Wed, 11 Feb 2026 18:01:21 +0100 Subject: [PATCH] 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 | 4 ++++ 6 files changed, 66 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 25480a4633..0fae9a3449 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -17484,6 +17484,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..c70fba989f 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.findDialog().getElement()).toHaveClass(styles[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 79e9e88c31..71877e9c4f 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} > - +