Skip to content

Commit c51ee16

Browse files
gDINESH13heloiseluitay1orjones
authored
fix(combobox): modal focus issue (#21733)
* fix(modal): scrolling issue Signed-off-by: gDINESH13 <dinesh13g@gmail.com> * fix(modal): focus fix Signed-off-by: gDINESH13 <dinesh13g@gmail.com> * test(modal): cover stable modal scroll during keyboard nav --------- Signed-off-by: gDINESH13 <dinesh13g@gmail.com> Co-authored-by: Heloise Lui <71858203+heloiselui@users.noreply.github.com> Co-authored-by: Taylor Jones <taylor.jones826@gmail.com>
1 parent 48a45d1 commit c51ee16

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

e2e/components/Modal/Modal-test.avt.e2e.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,52 @@ test.describe('@avt Modal', () => {
115115
await page.keyboard.press('Tab');
116116
await expect(page.getByRole('button', { name: 'close' })).toBeFocused();
117117
});
118+
119+
test('@avt-keyboard-nav should only scrollIntoView interactive elements if necessary', async ({
120+
page,
121+
}) => {
122+
await visitStory(page, {
123+
component: 'Modal',
124+
id: 'components-modal--with-scrolling-content',
125+
globals: {
126+
theme: 'white',
127+
},
128+
});
129+
130+
const primaryInput = page.getByRole('textbox', { name: 'Domain name' });
131+
await expect(primaryInput).toBeFocused();
132+
133+
const modalContent = page.locator('.cds--modal-content');
134+
await expect(modalContent).toHaveClass(/cds--modal-scroll-content/);
135+
136+
const isScrollable = await modalContent.evaluate(
137+
(node) => node.scrollHeight > node.clientHeight
138+
);
139+
expect(isScrollable).toBe(true);
140+
141+
const initialScrollTop = await modalContent.evaluate(
142+
(node) => node.scrollTop
143+
);
144+
145+
await page.keyboard.press('Tab');
146+
await expect(page.getByRole('combobox', { name: 'Region' })).toBeFocused();
147+
148+
// scrollIntoView should only run when needed, not on every tab stop.
149+
const scrollTopAfterFirstTab = await modalContent.evaluate(
150+
(node) => node.scrollTop
151+
);
152+
expect(scrollTopAfterFirstTab).toBe(initialScrollTop);
153+
154+
await page.keyboard.press('Tab');
155+
await expect(
156+
page.getByRole('combobox', {
157+
name: 'Permissions (Example of Floating UI)',
158+
})
159+
).toBeFocused();
160+
161+
const scrollTopAfterSecondTab = await modalContent.evaluate(
162+
(node) => node.scrollTop
163+
);
164+
expect(scrollTopAfterSecondTab).toBe(initialScrollTop);
165+
});
118166
});

packages/react/src/components/Modal/Modal.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,18 @@ const ModalDialog = React.forwardRef(function ModalDialog(
463463
return;
464464
}
465465

466+
const modalRect = modalContent.getBoundingClientRect();
467+
const elementRect = currentActiveNode.getBoundingClientRect();
468+
469+
// Check if element is fully visible within the modal viewport
470+
const isFullyVisible =
471+
elementRect.top >= modalRect.top &&
472+
elementRect.bottom <= modalRect.bottom;
473+
474+
if (isFullyVisible) {
475+
return; // Don't scroll if already fully visible
476+
}
477+
466478
currentActiveNode.scrollIntoView({ block: 'center' });
467479
}
468480

0 commit comments

Comments
 (0)