Commit 2a4adee7c6c for woocommerce
commit 2a4adee7c6ca787fd8e1f552708b049e4bb90137
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date: Mon Sep 7 12:33:03 2026 +0100
Restore WordPress defaults in the experimental Settings UI (#66867)
* fix(settings): Restore a visible focus indicator on the shell tabs
The shell tab styles set outline: none and box-shadow: none on :focus,
leaving only a colour change as the focus indicator. The active-tab
underline is bound to .is-active, not :focus, so keyboard users got no
visible focus indicator on the tabs, violating WPDS focus guidance and
WCAG 2.4.7.
Add the standard WPDS focus ring on :focus-visible, drawn inset so the
tab bar's overflow clipping keeps it visible. The ring uses the
wpds-border-width-focus and wpds-color-stroke-focus tokens, which the
settings embed already delivers through design-tokens.css.
Refs WOOPRD-3569
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: Add changelog for the shell tab focus ring fix
Refs WOOPRD-3569
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings): preserve native WordPress tab focus styles
* fix(settings): preserve independently registered unload handlers
* fix(settings): identify current navigation links
* refactor(settings): use native WordPress modal styling
* chore(settings): update changelogs for native behaviour fixes
* test(settings-ui): cover current navigation links
* fix(settings-ui): align active navigation underlines
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
diff --git a/packages/js/settings-ui/changelog/fix-preserve-wordpress-settings-behaviour b/packages/js/settings-ui/changelog/fix-preserve-wordpress-settings-behaviour
new file mode 100644
index 00000000000..d9d78444b57
--- /dev/null
+++ b/packages/js/settings-ui/changelog/fix-preserve-wordpress-settings-behaviour
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Identify current navigation links and preserve independently registered unload handlers in the Settings UI.
diff --git a/packages/js/settings-ui/src/settings-ui-page.tsx b/packages/js/settings-ui/src/settings-ui-page.tsx
index 86a13f8a1be..f8e45a3c41e 100644
--- a/packages/js/settings-ui/src/settings-ui-page.tsx
+++ b/packages/js/settings-ui/src/settings-ui-page.tsx
@@ -94,10 +94,6 @@ const getBadgeIntent = ( intent?: string ): SettingsUIShellBadgeIntent =>
const getSaveStrategy = ( schema: SettingsUISchema ): SettingsUISaveStrategy =>
schema.save || { adapter: 'form_post' };
-const clearLegacyFormPrompt = () => {
- window.onbeforeunload = null;
-};
-
const setFormPostRedirectInput = ( form: HTMLFormElement, href: string ) => {
let redirectInput = form.querySelector< HTMLInputElement >(
`input[name="${ FORM_POST_REDIRECT_INPUT_NAME }"]`
@@ -383,6 +379,9 @@ const ShellHeader = ( {
? 'wc-settings-ui-shell__tab is-active'
: 'wc-settings-ui-shell__tab'
}
+ aria-current={
+ item.active ? 'page' : undefined
+ }
href={ item.href }
key={ item.id }
>
@@ -407,6 +406,9 @@ const ShellHeader = ( {
? 'wc-settings-ui-shell__tab is-active'
: 'wc-settings-ui-shell__tab'
}
+ aria-current={
+ item.active ? 'page' : undefined
+ }
href={ item.href }
key={ item.id }
>
@@ -480,7 +482,6 @@ export const SettingsUIPage = ( {
const allowNavigation = useCallback( () => {
allowNavigationRef.current = true;
- clearLegacyFormPrompt();
}, [] );
const submitSettingsForm = useCallback(
diff --git a/packages/js/settings-ui/src/test/html-rendering.test.tsx b/packages/js/settings-ui/src/test/html-rendering.test.tsx
index 61fbd0e9296..f9fce857f44 100644
--- a/packages/js/settings-ui/src/test/html-rendering.test.tsx
+++ b/packages/js/settings-ui/src/test/html-rendering.test.tsx
@@ -578,7 +578,7 @@ describe( 'settings HTML rendering', () => {
}
} );
- it( 'submits form-post saves with the pending destination', () => {
+ it( 'submits form-post saves without clearing another unload handler', () => {
const requestSubmit = jest
.spyOn( HTMLFormElement.prototype, 'requestSubmit' )
.mockImplementation( () => undefined );
@@ -616,6 +616,10 @@ describe( 'settings HTML rendering', () => {
<SettingsUIPage schema={ schema } />
);
+ const previousBeforeUnload = window.onbeforeunload;
+ const otherBeforeUnload = jest.fn();
+ window.onbeforeunload = otherBeforeUnload;
+
try {
const input = container.querySelector(
'input:not([type="hidden"])'
@@ -665,7 +669,15 @@ describe( 'settings HTML rendering', () => {
expect( requestSubmit ).toHaveBeenCalledWith(
container.querySelector( '.woocommerce-save-button' )
);
+ expect( window.onbeforeunload ).toBe( otherBeforeUnload );
+ const beforeUnloadEvent = new Event( 'beforeunload', {
+ cancelable: true,
+ } );
+ window.dispatchEvent( beforeUnloadEvent );
+ expect( otherBeforeUnload ).toHaveBeenCalledTimes( 1 );
+ expect( beforeUnloadEvent.defaultPrevented ).toBe( false );
} finally {
+ window.onbeforeunload = previousBeforeUnload;
act( () => root.unmount() );
form.remove();
requestSubmit.mockRestore();
diff --git a/packages/js/settings-ui/src/test/shell-header-visibility.test.tsx b/packages/js/settings-ui/src/test/shell-header-visibility.test.tsx
index ae195ebfccc..4ef900759dd 100644
--- a/packages/js/settings-ui/src/test/shell-header-visibility.test.tsx
+++ b/packages/js/settings-ui/src/test/shell-header-visibility.test.tsx
@@ -75,6 +75,49 @@ describe( 'settings UI shell header visibility', () => {
document.body.innerHTML = '';
} );
+ it.each( [
+ [ 'navigation', 'Settings pages' ],
+ [ 'sectionNavigation', 'Settings sections' ],
+ ] )(
+ 'marks only the active link as current in %s',
+ ( navigation, label ) => {
+ const { container, root } = renderElement(
+ <SettingsUIPage
+ schema={ baseSchema( {
+ [ navigation ]: [
+ { id: 'first', label: 'First', href: '#first' },
+ {
+ id: 'current',
+ label: 'Current',
+ href: '#current',
+ active: true,
+ },
+ {
+ id: 'last',
+ label: 'Last',
+ href: '#last',
+ active: false,
+ },
+ ],
+ } ) }
+ page="test_page"
+ />
+ );
+
+ const links = container.querySelectorAll(
+ `nav[aria-label="${ label }"] a`
+ );
+ expect(
+ Array.from( links, ( link ) =>
+ link.getAttribute( 'aria-current' )
+ )
+ ).toEqual( [ null, 'page', null ] );
+
+ act( () => root.unmount() );
+ container.remove();
+ }
+ );
+
it( 'labels the shell region with a fallback when the schema has no title', () => {
const schema = baseSchema( {} );
delete schema.title;
diff --git a/plugins/woocommerce/changelog/fix-settings-ui-shell-tab-focus-ring b/plugins/woocommerce/changelog/fix-settings-ui-shell-tab-focus-ring
new file mode 100644
index 00000000000..7fc08951469
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-settings-ui-shell-tab-focus-ring
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore WordPress focus and modal styling, identify current navigation links, and preserve other plugins' navigation warnings in the experimental Settings UI.
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
index f7d6a9debe6..566cefc56cc 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/settings-embed/settings-ui.scss
@@ -160,7 +160,8 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
display: flex;
margin: 0 !important;
overflow-x: auto;
- padding: 0 48px;
+ // Keep WordPress focus rings inside the scrolling tab bar.
+ padding: var(--wpds-dimension-padding-xs) 48px;
width: 100%;
}
@@ -180,9 +181,7 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
&:focus,
&:hover,
&:active {
- box-shadow: none;
color: var(--wpds-color-foreground-interactive-brand);
- outline: none;
}
&.is-active {
@@ -191,7 +190,7 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
&::after {
background: var(--wpds-color-stroke-interactive-brand);
border-radius: 999px;
- bottom: 0;
+ inset-block-end: calc(-1 * var(--wpds-dimension-padding-xs));
content: "";
height: var(--wpds-border-width-sm);
left: 0;
@@ -214,29 +213,6 @@ body.woocommerce_page_wc-settings.woocommerce-settings-ui-page {
max-width: var(--wpds-dimension-surface-width-xl);
}
- .wc-settings-ui__unsaved-changes-modal {
- .components-modal__header {
- min-height: 72px;
- padding: 24px 32px 8px;
- }
-
- .components-modal__header-heading {
- font-size: 20px;
- font-weight: 500;
- line-height: 24px;
- }
-
- .components-modal__content {
- padding: 4px 32px 32px;
- }
-
- p {
- font-size: 13px;
- line-height: 20px;
- margin: 0;
- }
- }
-
.wc-settings-ui__unsaved-changes-actions {
display: flex;
gap: 8px;