Commit a5f6528633f for woocommerce
commit a5f6528633ffbc50bed35df9b5ed0ae2e8d60cf1
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Fri Aug 7 18:48:00 2026 +0300
[Payments NOX] Fix inconsistent focus indicators (#67448)
* Fix inconsistent focus indicators on Settings > Payments
The "More payment options" header painted its focus ring on mouse click,
not only on keyboard focus. That header was converted from a
div[role="button"] to a semantic <button> in PR #62841, which added a
bare :focus rule without the :focus:not(:focus-visible) guard that every
other indicator in the same PR received. The ring has been there since,
but WordPress 6.9 made the "modern" admin colour scheme the default,
moving --wp-admin-theme-color from #2271b1 to #3858e9 and making it far
more noticeable.
Auditing the whole screen turned up more drift. The drag handle and the
section header rendered square rings while everything else was rounded,
the official badge had no focus style at all and fell through to Chrome's
default #005fcc ring, and the two business location segments drew rings
that hugged the divider between them, with that divider's border also
pushing the indicator icon 1px off centre.
A ring's corner radius is the element's border-radius plus its outline
offset or shadow spread, so clearance and radius cannot be tuned
independently. Controls that need clearance now grow their box with
padding, cancelled by a matching negative margin so nothing moves, rather
than reaching for a larger offset. The header's ring is square because
nested corners follow inner = outer - gap and it sits well inside the
card's 4px radius. The divider now lives in a 1px gap between the two
location segments, so neither box contains it and both rings inset evenly
at 4px.
Two layout problems surfaced while checking this responsively. The card's
padding below $break-medium was $gap-smaller $gap, which left the title
crowded against the top border and put the ring 1px outside the card; it
is now a uniform $gap, one step down from the desktop $grid-unit-30. The
country rows in the location dropdown also ran the full popover width
while the search field and Apply button were inset, so they now match.
Verified against built assets at 1400px, 782px, 760px and 390px, and with
the location indicator both shown and hidden.
* Add changelog entry for Settings > Payments focus indicators
* Keep the Official badge focus ring at a 2px radius
The badge previously fell through to the browser's default focus ring, a
fixed blue that ignores the admin colour scheme. Giving it a themed ring
at a 2px radius and 2px offset produced a 4px ring, which reads far
rounder on a 20px-tall inline badge than the same 4px does on the 36px
controls it was meant to match.
An outline's corners follow the element's border-radius, and stay square
when that radius is zero rather than picking up the offset. So the radius
has to carry the rounding on its own: 2px on the element with no offset
keeps the ring at the badge's own 2px and hugging it, as the browser
default did, while the colour now follows --wp-admin-theme-color like
every other control on the screen.
* Theme the remaining info icon focus rings on Settings > Payments
Two more info triggers had no focus styling of their own and fell through
to the browser's default ring, a fixed blue that ignores the admin colour
scheme: the icon beside each category heading in the "More payment
options" section, and the one inside a status badge, which is what the
incentive badge ("Save 10% on processing fees") renders.
Both are spans with role="button" that open a popover, so they take the
same treatment as the Official badge: a 2px radius carrying the ring,
no offset, and the keyboard-only guard.
Confirmed against the Sunrise admin colour scheme, where the theme colour
is #dd823b and both previously drew #005fcc.
* Follow the admin colour scheme in two more Payments settings spots
The payment methods "+N" badge drew a 1.5px focus ring where every other
control on the screen uses 2px, and its hover state hardcoded the default
scheme's blue for text, background and border, so it stayed blue whatever
colour scheme the user had chosen.
The business location selector's highlighted row had the same problem for
a subtler reason: it reads --wp-admin-theme-color-rgb, which does not
exist. WordPress defines --wp-admin-theme-color--rgb, with two hyphens,
so the declaration always fell back to its hardcoded blue. That is the
only use of the misspelled variable in the tree.
Verified against the Sunrise scheme, where all three now resolve to
#dd823b instead of blue.
* Add changelog entry for the payment methods badge colour fixes
* Hold the payment methods badge label at gray-900 on hover
Making the "+N" badge follow the admin colour scheme meant deriving its
hover text, border and background tint from --wp-admin-theme-color. Text
and background then move together: the label is the theme colour and the
background is that same colour at 15% over white, so the pair only stays
legible while the theme colour is dark.
On the lighter schemes it is not. Measured against WordPress' per-scheme
values, the label drops to 2.48:1 on Sunrise, 3.24:1 on Midnight, 3.42:1
on Light and 3.71:1 on Ocean. The badge is 11px/600, so it is not WCAG
large text and 4.5:1 applies. The hardcoded pair this replaced sat at
4.52:1 in every scheme, making this a regression for four of the eight.
Holding the label at $gray-900 clears 4.5:1 everywhere (12.93:1 to
14.43:1) while the tint and border still follow the theme, so the badge
keeps reading as themed. --wp-admin-theme-color-darker-20 was the other
candidate and is not enough: WordPress darkens by roughly 10%, not 20%,
leaving Sunrise at 3.40:1 and Midnight at 4.26:1.
* Match the payment methods badge size to the logos beside it
The "+N" badge rendered 38x24 against the 40x26 of every logo in the row,
so it sat visibly short of its neighbours.
The row sizes its children under content-box. Logos are 38x24 plus a 1px
border, giving 40x26, and the ones that carry their own background hardcode
40x26 to match — the stylesheet says as much in a comment. The badge alone
declares 38x24 with no border, so it never picks up the extra 2px.
Give it a 1px transparent border. That lands it on the same 40x26 footprint
through the same mechanism the logos use, rather than hardcoding a third set
of numbers, and it gives the hover border-color something to paint on: with
no border-style the themed border declared for hover never rendered.
Predates this branch; the sizing rules are unchanged from trunk.
diff --git a/packages/js/onboarding/changelog/fix-payment-methods-count-theme-colors b/packages/js/onboarding/changelog/fix-payment-methods-count-theme-colors
new file mode 100644
index 00000000000..d8061d78009
--- /dev/null
+++ b/packages/js/onboarding/changelog/fix-payment-methods-count-theme-colors
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make the payment methods "+N" badge follow the admin colour scheme on hover, match its focus ring width to the surrounding controls, and align its size with the payment method logos beside it.
diff --git a/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss
index 16d34eaa575..b50cbbc0a6a 100644
--- a/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss
+++ b/packages/js/onboarding/src/components/WooPaymentsMethodsLogos/PaymentMethodsLogos.scss
@@ -22,6 +22,9 @@
&-count {
width: 38px;
height: 24px;
+ // Carries the same 2px as the bordered logos so the badge matches their
+ // 40x26 footprint, and gives the hover border something to paint on.
+ border: 1px solid transparent;
background-color: rgba($gray-700, 0.10);
color: $gray-900;
text-align: center;
@@ -33,13 +36,16 @@
&-count:hover {
cursor: pointer;
- color: rgba(56, 88, 233, 1);
- background-color: rgba(56, 88, 233, 0.15);
- border-color: rgba(56, 88, 233, 1);
+ // Hold the label at $gray-900 rather than the theme colour: the tint below is
+ // derived from that same colour, so theming both drops the pair under 4.5:1 on
+ // the lighter schemes (2.48:1 on Sunrise). Only the tint and border follow the theme.
+ color: $gray-900;
+ background-color: rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.15);
+ border-color: var(--wp-admin-theme-color, #3858e9);
}
&-count:focus {
- outline: 1.5px solid var(--wp-admin-theme-color, #007cba);
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
outline-offset: 1px;
}
@@ -48,7 +54,7 @@
}
&-count:focus-visible {
- outline: 1.5px solid var(--wp-admin-theme-color, #007cba);
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
outline-offset: 1px;
}
}
diff --git a/plugins/woocommerce/changelog/fix-nox-focus-ring-consistency b/plugins/woocommerce/changelog/fix-nox-focus-ring-consistency
new file mode 100644
index 00000000000..7504f6926b4
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-nox-focus-ring-consistency
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make focus indicators consistent on the Payments settings screen: stop the "More payment options" ring from appearing on mouse click, give it room to breathe, round the square rings on the drag handle and section header, and give the Official badge, the category info icons and the status badge info icon themed focus rings instead of the browser default. The highlighted row in the business location selector now follows the admin colour scheme too.
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/components/country-selector/country-selector.scss b/plugins/woocommerce/client/admin/client/settings-payments/components/country-selector/country-selector.scss
index 37efa35f753..8f860cff8f9 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/components/country-selector/country-selector.scss
+++ b/plugins/woocommerce/client/admin/client/settings-payments/components/country-selector/country-selector.scss
@@ -12,21 +12,26 @@
&__item {
padding: $gap-small;
- margin: 0;
+ // Inset to the same width as the search field and the Apply button
+ // instead of running the full width of the popover. The left padding
+ // and the check mark below drop by the same 12px so the row's contents
+ // stay where they were.
+ margin: 0 12px;
line-height: initial;
cursor: pointer;
position: relative;
- padding-left: 48px;
+ padding-left: 36px;
+ border-radius: 2px;
&.is-highlighted {
- background: rgba(var(--wp-admin-theme-color-rgb, 0, 124, 186), 0.08);
+ background: rgba(var(--wp-admin-theme-color--rgb, 0, 124, 186), 0.08);
outline: 2px solid var(--wp-admin-theme-color, #007cba);
outline-offset: -2px;
}
svg {
position: absolute;
- left: 20px;
+ left: 8px;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
@@ -40,7 +45,11 @@
&__button {
width: 100%;
- margin: 0 1px;
+ // No margin: any here makes the button overhang its track, sliding it
+ // under the indicator's divider and leaving the focus ring 1px wider on
+ // the sides than on the top and bottom. The gap the divider needs is
+ // owned by the indicator, so it disappears when the indicator does.
+ margin: 0;
display: flex;
justify-content: space-between;
align-items: center;
@@ -149,10 +158,12 @@
}
.components-button.components-country-select-control__button {
+ // Inset far enough to sit clear of both the container border and the
+ // indicator's divider, rather than hugging them.
&:focus {
box-shadow: none !important;
outline: 2px solid var(--wp-admin-theme-color, #007cba) !important;
- outline-offset: -2px;
+ outline-offset: -4px;
}
&:focus:not(:focus-visible) {
@@ -162,7 +173,7 @@
&:focus-visible {
box-shadow: none !important;
outline: 2px solid var(--wp-admin-theme-color, #007cba) !important;
- outline-offset: -2px;
+ outline-offset: -4px;
}
}
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/components/sortable/sortable.scss b/plugins/woocommerce/client/admin/client/settings-payments/components/sortable/sortable.scss
index 8e4108c9138..addd6f29015 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/components/sortable/sortable.scss
+++ b/plugins/woocommerce/client/admin/client/settings-payments/components/sortable/sortable.scss
@@ -9,6 +9,7 @@
margin-right: $gap;
align-self: stretch;
align-content: center;
+ border-radius: 2px;
&:hover {
cursor: grab;
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/components/status-badge/status-badge.scss b/plugins/woocommerce/client/admin/client/settings-payments/components/status-badge/status-badge.scss
index 924a585c1b0..0bc0d308b90 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/components/status-badge/status-badge.scss
+++ b/plugins/woocommerce/client/admin/client/settings-payments/components/status-badge/status-badge.scss
@@ -25,5 +25,20 @@
cursor: pointer;
display: inline-flex;
align-items: center;
+ border-radius: 2px;
+
+ &:focus {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
+
+ &:focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
}
}
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/settings-payments-main.scss b/plugins/woocommerce/client/admin/client/settings-payments/settings-payments-main.scss
index 9edacad996f..427e1eeedae 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/settings-payments-main.scss
+++ b/plugins/woocommerce/client/admin/client/settings-payments/settings-payments-main.scss
@@ -101,12 +101,33 @@
height: 28px;
cursor: pointer;
padding-top: 8px;
- margin-left: auto;
+ // This 1px is the gap the divider is drawn into. It belongs to
+ // the indicator so that it disappears along with it when the
+ // business location matches the store's country.
+ margin-left: 1px;
border-radius: 2px;
+ position: relative;
+
+ // Drawn into that gap rather than as a border inside either box.
+ // A border would sit inside this element, pushing the icon
+ // off-centre and leaving the focus ring 1px closer to the
+ // divider than to the other three sides.
+ &::before {
+ content: "";
+ position: absolute;
+ left: -1px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 20px;
+ background-color: $gray-200;
+ }
+ // Matches the select segment's inset so both rings sit clear of
+ // the container border and the divider between them.
&:focus {
- outline: 1.5px solid var(--wp-admin-theme-color, #007cba);
- outline-offset: -1.5px;
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: -4px;
}
&:focus:not(:focus-visible) {
@@ -114,14 +135,14 @@
}
&:focus-visible {
- outline: 1.5px solid var(--wp-admin-theme-color, #007cba);
- outline-offset: -1.5px;
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: -4px;
}
&-icon {
- border-left: 1px solid $gray-200;
height: 20px;
- padding: 0 5px;
+ // Wide enough that the inset ring clears the icon.
+ padding: 0 8px;
svg {
fill: $alert-yellow;
@@ -158,18 +179,42 @@
display: flex;
justify-content: space-between;
align-items: center;
- width: 100%;
background: none;
border: none;
- padding: 0;
+ // Nested corners follow inner = outer - gap. The card is 4px and the
+ // ring clears it by more than that, so the ring wants to be square;
+ // 0 here is as close as it gets, since the ring's own radius is this
+ // value plus the 2px offset. Anything larger reads rounder than the
+ // card that contains it.
+ border-radius: 0;
cursor: pointer;
text-align: left;
+ // A ring's corner radius is border-radius + outline-offset, so the
+ // offset cannot be used to buy clearance without also rounding the
+ // ring past the 4px every other control on this screen uses.
+ // Grow the box with padding instead and cancel it with a matching
+ // negative margin, which leaves the title and chevron in place.
+ // $grid-unit-10 of clearance fits inside the card's padding at every
+ // breakpoint, so it needs no responsive override.
+ width: calc(100% + 2 * #{$grid-unit-10});
+ margin: -$grid-unit-10;
+ padding: $grid-unit-10;
+
&:focus {
outline: 2px solid var(--wp-admin-theme-color, #007cba);
outline-offset: 2px;
}
+ &:focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 2px;
+ }
+
&__title {
display: flex;
gap: 10px;
@@ -220,7 +265,9 @@
&.is-expanded {
.other-payment-gateways__header {
- margin-bottom: $grid-unit-40;
+ // The header's $grid-unit-10 of padding contributes the rest, so
+ // the content still sits $grid-unit-40 below the title.
+ margin-bottom: $grid-unit-30;
}
}
@@ -259,6 +306,21 @@
&__icon-container {
height: 16px;
cursor: pointer;
+ border-radius: 2px;
+
+ &:focus {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
+
+ &:focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
}
&__icon {
@@ -384,7 +446,10 @@
@media screen and (max-width: $break-medium) {
margin: 32px $gap 0 $gap;
- padding: $gap-smaller $gap;
+ // One step down from the desktop $grid-unit-30, and uniform: the
+ // former $gap-smaller left the title crowded against the top border
+ // while the sides kept a full $gap.
+ padding: $gap;
}
}
@@ -468,6 +533,21 @@
align-items: center;
gap: 2px;
padding: 2px 4px 2px 2px;
+ border-radius: 2px;
+
+ &:focus {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
+
+ &:focus:not(:focus-visible) {
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--wp-admin-theme-color, #007cba);
+ outline-offset: 0;
+ }
> img {
height: 16px;