fix(styles): introduce modal max-height#2249
Conversation
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
Pull request overview
This PR addresses #2125 by constraining dialog/modal height so long modal content can be scrolled instead of pushing footer controls below the fold.
Changes:
- Add a max-height constraint to
.Dialog__innerandoverflow-y: autoto.Dialog__content. - Add Modal visual-regression coverage via new Playwright screenshot tests and baseline images.
- Add an accessibility (axe) regression test case for long modal content.
Reviewed changes
Copilot reviewed 3 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/styles/dialog.css | Adds max-height + scroll behavior for dialog content. |
| packages/react/src/components/Modal/screenshots.e2e.tsx | New Modal screenshot coverage for small/large content across viewports and themes. |
| packages/react/src/components/Modal/index.test.tsx | Adds axe tests intended to cover long-content scenarios. |
| e2e/screenshots/modal-small-content.png | New baseline screenshot. |
| e2e/screenshots/dark--modal-small-content.png | New baseline screenshot (dark theme). |
| e2e/screenshots/modal-large-content-large-viewport.png | New baseline screenshot (large viewport). |
| e2e/screenshots/modal-large-content-small-viewport.png | New baseline screenshot (small viewport). |
| e2e/screenshots/dark--modal-large-content-small-viewport.png | New baseline screenshot (dark + small viewport). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| describe('should return no axe violations when the modal content is long', () => { | ||
| afterAll(() => { | ||
| Object.defineProperty(window, 'innerWidth', { | ||
| writable: true, | ||
| configurable: true, | ||
| value: 1024 | ||
| }); | ||
| window.dispatchEvent(new Event('resize')); | ||
| }); | ||
|
|
||
| test('on small viewport', async () => { | ||
| Object.defineProperty(window, 'innerWidth', { | ||
| writable: true, | ||
| configurable: true, | ||
| value: 310 | ||
| }); | ||
|
|
||
| window.dispatchEvent(new Event('resize')); | ||
|
|
||
| const { container } = render( | ||
| <Modal {...defaults} show={true}> | ||
| <ModalContent>{createLongContent()}</ModalContent> | ||
| <ModalFooter> | ||
| <button>Ok</button> | ||
| </ModalFooter> | ||
| </Modal> | ||
| ); | ||
|
|
||
| expect(await axe(container)).toHaveNoViolations(); | ||
| }); | ||
|
|
||
| test('on large viewport', async () => { | ||
| Object.defineProperty(window, 'innerWidth', { | ||
| writable: true, | ||
| configurable: true, | ||
| value: 720 | ||
| }); | ||
|
|
||
| window.dispatchEvent(new Event('resize')); | ||
|
|
There was a problem hiding this comment.
These tests modify window.innerWidth and dispatch resize, but the Dialog/Modal implementation does not read innerWidth, matchMedia, or listen to resize events (it’s purely CSS-driven). That means this setup doesn’t affect rendering and adds unnecessary complexity/flakiness. Consider removing the innerWidth/resize manipulation (or replacing it with assertions that validate actual DOM/CSS behavior if needed).
packages/styles/dialog.css
Outdated
| @media (min-width: 320px) { | ||
| .Dialog__inner { | ||
| max-height: calc(100vh - 130px); | ||
| } | ||
|
|
||
| .Dialog__content { | ||
| overflow-y: auto; | ||
| } | ||
| } |
There was a problem hiding this comment.
The new rules are wrapped in @media (min-width: 320px), which effectively applies to almost all devices and makes the “small viewport” behavior only trigger below 320px. This doesn’t match the PR description/issue note about switching behavior at a meaningful breakpoint (so the whole dialog can scroll on small viewports). Consider using a real breakpoint (e.g. align with the existing 460px modal breakpoint in layout.css) and explicitly setting small-viewport behavior (e.g. make the .Dialog/overlay scrollable and avoid constraining .Dialog__inner).
There was a problem hiding this comment.
As per the design specs - we want the entire modal to be scrollable below 320px
| .Dialog__inner { | ||
| max-height: calc(100vh - 130px); | ||
| } | ||
|
|
There was a problem hiding this comment.
max-height: calc(100vh - 130px) introduces a hard-coded 130px that isn’t derived from existing dialog variables (e.g., the top: 100px offset, header/footer sizes, padding). This makes the layout fragile if any of those values change. Consider computing this from CSS custom properties (or introducing a --dialog-vertical-offset/--dialog-max-height-offset) so the max-height stays consistent with the dialog’s actual spacing.
| mount, | ||
| page | ||
| }) => { | ||
| await page.setViewportSize({ width: 319, height: 667 }); |
There was a problem hiding this comment.
This test sets the viewport width to 319px, which appears chosen specifically to fall below the new CSS min-width: 320px rule, rather than to represent a real breakpoint. This makes the screenshot coverage brittle and can miss the intended “small viewport” behavior for common device widths (e.g. 360–430px). Consider aligning the viewport size(s) here with the actual breakpoint you choose for the dialog scrolling behavior (and update screenshots accordingly).
| await page.setViewportSize({ width: 319, height: 667 }); | |
| await page.setViewportSize({ width: 360, height: 667 }); |
There was a problem hiding this comment.
As per the design we want the entire modal to be scrollable below 320 px.
| await expect(page).toHaveScreenshot('modal-small-content.png'); | ||
| await setTheme(page, 'dark'); | ||
| await expect(page).toHaveScreenshot('dark--modal-small-content.png'); | ||
| }); |
There was a problem hiding this comment.
In this repo’s screenshot tests, toHaveScreenshot is generally called without a file extension (Playwright appends .png), and often targets a specific locator rather than the full page. Using explicit .png names and full-page screenshots here is inconsistent and can increase snapshot churn. Consider switching to a dialog-scoped locator (e.g. page.getByRole('dialog')) and using extension-less screenshot names for consistency with other component screenshot suites.
Zidious
left a comment
There was a problem hiding this comment.
This is looking great! Left some minor comments, also the 🤖 has reasonable comments.
Small nit: we'd want to scope your title to something like fix(react): ... so we can have the changelog generation scope the fix to the correct CHANGELOG file (see conventional commits docs).
maksym-shynkarenko
left a comment
There was a problem hiding this comment.
LGTM, but I'm not really familiar with cauldron. Leave the final review on @anastasialanz
| expect(await axe(document.body)).toHaveNoViolations(); | ||
| }); | ||
|
|
||
| function createLongContent() { |
There was a problem hiding this comment.
Nit - For these types utility functions, try to have them at the top of the file in the event we need to add future test, like you have for the E2E tests.
| function createLongContent() { | ||
| return ( | ||
| <> | ||
| {Array.from({ length: 50 }, (_, i) => ( | ||
| <p key={i}>Modal content here, get your modal content here!</p> | ||
| ))} | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
nit -
Seems like we use this helper function in several places:
packages/react/src/components/Modal/index.test.tsx
packages/react/src/components/Modal/screenshots.e2e.tsx
it might have sense to move it to some test-util file or similar
be1bd51
be1bd51
2249.mov
This PR fixes situations when the length of the modal content exceeds the viewport. In case of modal content overflowing on large viewports, we want the modal content to be scrollable while the footer buttons stay fixed at the bottom of the screen, above the fold. On small viewports, the footer buttons can go below the fold of the screen and we should be able to scroll through the entire modal and not just the modal content.
The following are the breakpoints we follow -
@Viewport:320px (20rem)
@Viewport:768px (48rem)
@Viewport:1280px (64rem)
We want anything above 320px to have scrollable modal content.
Closes #2125