Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17542,8 +17564,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",
Expand All @@ -17552,6 +17574,8 @@ largest size allowed by the design guidelines. Other sizes
"max",
"medium",
"large",
"x-large",
"xx-large",
],
},
"name": "size",
Expand Down
11 changes: 10 additions & 1 deletion src/modal/__tests__/modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,22 @@ 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]);
});
});
});

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();
Expand Down
14 changes: 12 additions & 2 deletions src/modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ 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(
'Modal',
{
props: {
size,
position,
disableContentPaddings: props.disableContentPaddings,
flowType: analyticsMetadata.flowType,
},
Expand All @@ -95,12 +96,21 @@ export default function Modal({ size = 'medium', ...props }: ModalProps) {
analyticsMetadata={analyticsMetadata}
baseComponentProps={baseComponentProps}
size={size}
position={position}
{...props}
/>
);
}

return <InternalModal size={size} {...props} {...baseComponentProps} __injectAnalyticsComponentMetadata={true} />;
return (
<InternalModal
size={size}
position={position}
{...props}
{...baseComponentProps}
__injectAnalyticsComponentMetadata={true}
/>
);
}

applyDisplayName(Modal, 'Modal');
18 changes: 15 additions & 3 deletions src/modal/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,21 @@ 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;
/**
* 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.
Expand Down Expand Up @@ -81,7 +92,8 @@ 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 type Position = 'center' | 'top';

export interface DismissDetail {
reason: string;
Expand Down
10 changes: 8 additions & 2 deletions src/modal/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function PortaledModal({
children,
footer,
disableContentPaddings,
position = 'center',
onButtonClick = () => {},
onDismiss,
__internalRootRef,
Expand All @@ -108,7 +109,7 @@ function PortaledModal({
const instanceUniqueId = useUniqueId();
const headerId = `${rest.id || instanceUniqueId}-header`;
const lastMouseDownElementRef = useRef<HTMLElement | null>(null);
const [breakpoint, breakpointsRef] = useContainerBreakpoints(['xs']);
const [breakpoint, breakpointsRef] = useContainerBreakpoints(['l']);

const i18n = useInternalI18n('modal');
const closeAriaLabel = i18n('closeAriaLabel', rest.closeAriaLabel);
Expand Down Expand Up @@ -247,7 +248,12 @@ function PortaledModal({
style={footerHeight ? { scrollPaddingBottom: footerHeight } : undefined}
data-awsui-referrer-id={subStepRef.current?.id || referrerId}
>
<FocusLock disabled={!visible} autoFocus={true} restoreFocus={true} className={styles['focus-lock']}>
<FocusLock
disabled={!visible}
autoFocus={true}
restoreFocus={true}
className={clsx(styles['focus-lock'], styles[`position-${position}`])}
>
<div
className={clsx(
styles.dialog,
Expand Down
19 changes: 17 additions & 2 deletions src/modal/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,19 @@ $modal-z-index: 5000;

.focus-lock {
align-self: flex-start;
margin-block: auto;
margin-inline: auto;
padding-block: awsui.$space-s;
padding-inline: 0;
z-index: $modal-z-index;
pointer-events: none;

&.position-top {
margin-block-start: 0;
}

&.position-center {
margin-block: auto;
}
}

.dialog {
Expand Down Expand Up @@ -66,7 +73,15 @@ $modal-z-index: 5000;
max-inline-size: 820px;
}

&.max.breakpoint-xs {
&.x-large {
max-inline-size: 1024px;
}

&.xx-large {
max-inline-size: 1280px;
}

&.breakpoint-l.max {
// 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;
Expand Down
Loading