diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx
index 6caa0fce27..d5b4007414 100644
--- a/src/courseware/course/sequence/SequenceContent.jsx
+++ b/src/courseware/course/sequence/SequenceContent.jsx
@@ -24,6 +24,19 @@ const SequenceContent = ({
// Go back to the top of the page whenever the unit or sequence changes.
useEffect(() => {
global.scrollTo(0, 0);
+ // Move focus after every unit navigation so keyboard and screen-reader users
+ // are not left stranded on the next/previous button that triggered it.
+ // Targets div.app-container (the MFE root), which is always present regardless of
+ // plugin slot customization.
+ const mainContainer = global.document.querySelector('div.app-container');
+ if (mainContainer) {
+ if (!mainContainer.hasAttribute('tabindex')) {
+ mainContainer.setAttribute('tabindex', '-1');
+ }
+ mainContainer.focus();
+ } else {
+ global.document.body.focus();
+ }
}, [sequenceId, unitId]);
if (gated) {
diff --git a/src/courseware/course/sequence/SequenceContent.test.jsx b/src/courseware/course/sequence/SequenceContent.test.jsx
index e9c3a2d785..eec2edc401 100644
--- a/src/courseware/course/sequence/SequenceContent.test.jsx
+++ b/src/courseware/course/sequence/SequenceContent.test.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { act } from '@testing-library/react';
import { initializeTestStore, render, screen } from '../../../setupTest';
import SequenceContent from './SequenceContent';
@@ -42,4 +43,44 @@ describe('Sequence Content', () => {
render(, { wrapWithRouter: true });
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
});
+
+ it('moves focus to div.app-container after unit navigation', async () => {
+ // JSDOM does not include the app shell markup, so we create the element manually
+ // to match the real DOM structure the focus logic targets.
+ const appContainer = global.document.createElement('div');
+ appContainer.className = 'app-container';
+ global.document.body.appendChild(appContainer);
+
+ const secondUnitId = store.getState().models.sequences[mockData.sequenceId].unitIds[1];
+ render(, { store, wrapWithRouter: true });
+
+ // Simulate navigating to the next unit by re-rendering with a new unitId.
+ // A second render call is used instead of rerender because rerender bypasses
+ // the store and provider wrappers from setupTest's custom render helper.
+ // We use act to flush the useEffect triggered by the unitId change.
+ await act(async () => {
+ render(, { store, wrapWithRouter: true });
+ });
+
+ expect(appContainer).toHaveAttribute('tabindex', '-1');
+ expect(appContainer).toHaveFocus();
+
+ global.document.body.removeChild(appContainer);
+ });
+
+ it('falls back to focusing document.body when div.app-container is absent', async () => {
+ // Verify div.app-container is not in the DOM for this test — if a previous
+ // test left one behind, this assertion would give a false negative.
+ expect(global.document.querySelector('div.app-container')).toBeNull();
+
+ const secondUnitId = store.getState().models.sequences[mockData.sequenceId].unitIds[1];
+ render(, { store, wrapWithRouter: true });
+
+ // A second render call is used instead of rerender — see comment above.
+ await act(async () => {
+ render(, { store, wrapWithRouter: true });
+ });
+
+ expect(global.document.body).toHaveFocus();
+ });
});