Repository navigation
OpenConceptLab/ocl_issues#2970 | Focused buttons show the two-tone ring, not the focus ripple - #118
Conversation
…ng, not the focus ripple While a button had keyboard focus, MUI pulsed a circle of the text color at 30% behind the label. Over it the label fell below 4.5:1: 3.64:1 on contained primary, 3.86:1 on contained error and 3.91:1 for primary text on outlined and text buttons. The theme now sets disableFocusRipple on MuiButton and draws the community site's ring on .Mui-focusVisible instead: 2px white, then 2px primary 30. Theme overrides come after MUI's own styles, so the ring replaces a contained button's focus elevation. A contained ButtonGroup sets box-shadow: none on its buttons at the same specificity, so the group gets the ring too, raised above its neighbours. The click ripple stays. The settings live in src/common/buttonFocus.js, because index.jsx renders the app and can't be imported by a test. The new integration test renders contained, outlined, text, contained-error and grouped buttons under a theme built from it, focuses each, and checks for the ring and for no pulsating ripple. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paynejd
left a comment
There was a problem hiding this comment.
Codex review, pass 1 (codex-cli 0.164.0, commit cad14f5)
Verdict: clean
Outcome: No findings, so nothing to fix. Next: the simplification review.
findings.json
{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "full",
"commit": "cad14f5bf8f9c3fbc477ad4f07197ee944052091",
"verdict": "clean",
"prior": [],
"findings": []
}The ring and disableFocusRipple don't depend on the button's color, and MUI's focus elevation on a contained button is the same for every color, so the error case repeated the contained one (simplification review S1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paynejd
left a comment
There was a problem hiding this comment.
Simplification review (claude subagent, commit cad14f5)
Verdict: findings (1 finding)
Outcome: S1 applied in 6d9b73a. Lint and tests rerun: ESLint clean, unit 603/603, integration 10/10.
- S1 (dup-test, medium)
src/common/__integration__/buttonFocus.test.js:60: The 'contained error button' test repeats the contained variant test. The ring rule and disableFocusRipple are set on MuiButton root regardless of color, and no acceptance criterion names the error color.
Responses:
- S1: agreed and cut. MUI puts a contained button's focus elevation in the
containedvariant for every color, so the error case only repeated the contained test.
findings.json
{
"reviewer": "claude subagent",
"perspective": "simplify",
"mode": "full",
"commit": "cad14f5bf8f9c3fbc477ad4f07197ee944052091",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "S1",
"severity": null,
"tag": "dup-test",
"file": "src/common/__integration__/buttonFocus.test.js",
"line": 60,
"claim": "The 'contained error button' test repeats the contained variant test. The ring rule and disableFocusRipple are set on MuiButton root regardless of color, and no acceptance criterion names the error color.",
"scenario": null,
"action": "Delete the test. The contained, outlined, text and group tests still cover both criteria. Keep it only if a color-specific MUI focus rule is a known concern.",
"lines_delta": -5,
"confidence": "medium"
}
]
}… design system's form The ButtonGroup rule moves from a root selector to styleOverrides.grouped['&.Mui-focusVisible'], as in the design system's theme. MUI emits it as the same selector, so the result doesn't change. The test now also checks the focused grouped button's z-index. In an outlined group whose selected button is filled, as on the Associations panel, that fill covered the right edge of a focused neighbour's ring until the button was raised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paynejd
left a comment
There was a problem hiding this comment.
Codex check of commits cad14f5..6d9b73a (codex-cli 0.164.0)
Verdict: clean
Outcome: Clean: the simplification cut broke nothing. A later commit (aeaad0e, from a cross-repo check of the ButtonGroup case) gets its own check below.
findings.json
{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "final",
"commit": "6d9b73a3bcdb58ada0b0578689b05e191e2426cb",
"verdict": "clean",
"prior": [],
"findings": []
}
paynejd
left a comment
There was a problem hiding this comment.
Codex check of commits 6d9b73a..aeaad0e (codex-cli 0.164.0)
Verdict: clean
Outcome: Clean. aeaad0e moves the grouped rule into the form the design system uses, which MUI emits as the same selector, and adds the z-index assertion.
findings.json
{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "final",
"commit": "aeaad0e2e87f918b695de4f3d8112a9b5206cf4c",
"verdict": "clean",
"prior": [],
"findings": []
}…xt color under Bootstrap's a:hover and a:focus
A contained Button with href renders as <a>. Bootstrap 3's
`a:hover, a:focus { color: #23527c; text-decoration: underline }` (an
element and a class) outranks MUI's contained `color:
var(--variant-containedColor)` (one class). So "New map project" and
other link-buttons showed dark-blue underlined text on the filled
button: 1.26:1 on primary while focused.
The theme now re-asserts MUI's own declaration on contained anchors on
:hover and :focus. Wherever MUI's rule applied before, it gives the same
value, including colors without a palette entry, where the variable is
unset and the text inherits as before. Buttons rendered as <button> are
untouched. This also covers the link-buttons' share of
OpenConceptLab/ocl_issues#2969's hover criterion; the hover fill itself
is #2969's palette change.
The integration test loads Bootstrap's rule from public/bootstrap.min.css
and checks the link-button's text is the contrast text, at 4.5:1 or more,
when focused and when hovered (happy-dom can't match :hover, so the test
copies :hover rules to a class).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es cleanly and covers a button's own color The test simulated hover by inserting copies of the :hover rules into Emotion's stylesheet and deleting them afterwards. happy-dom 15's deleteRule leaves holes in cssRules, and a later computed-style read in the same file threw (Codex final check FC.2). The copies now go in a separate <style> that the test removes. The test also renders a contained <button> with its own sx color. The link-button rule is scoped to <a>, so this button keeps its color on focus and hover. The test fails if the scope is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paynejd
left a comment
There was a problem hiding this comment.
Codex check of commits aeaad0e..57d6e11 (codex-cli 0.164.0)
Verdict: findings (2 findings)
Outcome: FC.2 fixed in 2a53a16. FC.1 depends on oclmap#117 and is open until that merges; see the response.
- FC.1 (P1, high)
src/common/buttonFocus.js:25: The new text-color override regresses hover contrast with the application's palette. - FC.2 (P2, high)
src/common/__integration__/buttonFocus.test.js:118: Hover-rule cleanup corrupts shared stylesheet state in the installed happy-dom version.
Responses:
- FC.1 (P1): confirmed in Chromium, and not fixable in this PR without changing the palette. With today's
primary.dark(#c3c0ff), every filled<button>already shows white on#c3c0ffon hover (1.71:1); link-buttons only scored 4.79:1 because Bootstrap's dark-blue text happened to land on that light fill. The fix is oclmap#117 (OpenConceptLab/ocl_issues#2969), which makesprimary.dark#2d00e5. With it, link-buttons on hover measure white on#2d00e5, 9.36:1, and on focus 6.47:1. Merge oclmap#117 before or together with this PR. The test uses MUI's default palette becausecolors.jsxcan't be loaded by the test runner; the palette's own hover contrast is #2969's test. - FC.2 (P2): reproduced (a later computed-style read threw) and fixed in 2a53a16. The hover copies now go in a separate
<style>that the test removes, and a follow-up read succeeds.
findings.json
{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "final",
"commit": "57d6e1117c2c222e8bb56adedb22b972eae85ac4",
"verdict": "findings",
"prior": [],
"findings": [
{
"id": "FC.1",
"severity": "P1",
"tag": null,
"file": "src/common/buttonFocus.js",
"line": 25,
"claim": "The new text-color override regresses hover contrast with the application's palette.",
"scenario": "On desktop, hovering the 'New map project' link-button uses primary.dark (#c3c0ff) as its background. This rule changes its text from Bootstrap's #23527c to white, reducing contrast from 4.80:1 to 1.71:1. The new test misses this because it uses MUI's default palette.",
"action": "Ensure the hover foreground/background pair meets 4.5:1 with the application palette, and exercise that palette in the regression test.",
"lines_delta": null,
"confidence": "high"
},
{
"id": "FC.2",
"severity": "P2",
"tag": null,
"file": "src/common/__integration__/buttonFocus.test.js",
"line": 118,
"claim": "Hover-rule cleanup corrupts shared stylesheet state in the installed happy-dom version.",
"scenario": "happy-dom 15.11.7 implements deleteRule with array deletion, leaving undefined entries in cssRules. After this test completes, a subsequent test in the same process that reads getComputedStyle(element).color throws 'Cannot read properties of undefined (reading type)'. This was reproduced by registering a follow-up test.",
"action": "Restore stylesheet snapshots using replaceSync or otherwise remove the temporary rules without leaving sparse cssRules arrays; verify a subsequent computed-style read succeeds.",
"lines_delta": null,
"confidence": "high"
}
]
}
paynejd
left a comment
There was a problem hiding this comment.
Codex check of commits 57d6e11..2a53a16 (codex-cli 0.164.0)
Verdict: clean
Outcome: Clean. FC.2 is resolved. FC.1 stays open until oclmap#117 merges, as planned.
Earlier findings: FC.1 unresolved, FC.2 resolved
findings.json
{
"reviewer": "codex-cli 0.164.0",
"perspective": "defects",
"mode": "final",
"commit": "2a53a16e606930dd18e20f2a2f12e6438acfe60a",
"verdict": "clean",
"prior": [
{
"id": "FC.1",
"status": "unresolved"
},
{
"id": "FC.2",
"status": "resolved"
}
],
"findings": []
}
Part of OpenConceptLab/ocl_issues#2970, and the link-button part of OpenConceptLab/ocl_issues#2969
What changed and why
While a button has keyboard focus, MUI pulses a focus ripple behind the label: a circle of the text color at 30% opacity. Over it the label falls below WCAG 1.4.3's 4.5:1:
primary.maintextIn the theme, buttons now drop the focus ripple and show the community site's two-tone ring on
.Mui-focusVisible:0 0 0 2px #FFF, 0 0 0 4px #2d00e5(WHITE, thenprimary.30). Text keeps its resting contrast (6.47:1 onprimary.main). The click ripple stays.MuiButton:defaultProps.disableFocusRipple: true, and the ring instyleOverrides.root. Theme overrides come after MUI's own styles, so the ring replaces a contained button's focus elevation.MuiButtonGroup:styleOverrides.grouped['&.Mui-focusVisible']sets the same ring withz-index: 1. This is the form the design system's theme uses.Contained link-buttons: a contained
Buttonwithhrefrenders as<a>. Bootstrap 3'sa:hover, a:focus { color: #23527c; text-decoration: underline }(an element and a class) outranks MUI's containedcolor: var(--variant-containedColor)(one class). So "New map project" inMapProjects.jsx(2 places) and the action button inPreviewLimitDialog.jsxshowed dark-blue underlined text on focus, 1.26:1 on primary, and on hover.MuiButton.styleOverrides.rootnow re-asserts MUI's own declaration ona&.MuiButton-contained:hoverand:focus, without the underline. Focused, they measure 6.47:1, or 6.46:1 on error.color="inherit", or a color with no palettemain(oclmap'scolor='default'), the variable is unset, and the text inherits as before.<a>, because Bootstrap's rule only matches anchors. A contained<button>with its ownsxcolor keeps that color on hover and focus. No contained button in the app sets its own text color today, except theQuotaDialogaction, which sets the same contrast text.The settings live in a new
src/common/buttonFocus.js, which returns both entries.src/index.jsxmerges them in as a thirdcreateThemeargument, so the theme's existingMuiButton.variantsstay as they are.Buttons in a ButtonGroup
ButtonGrouppasses its rawdisableFocusRippleprop through context, which is undefined unless set.Buttonresolves that context before the theme defaults, soMuiButton.defaultProps.disableFocusRippleapplies inside a group too, and noMuiButtonGroup.defaultPropsis needed. MUI 5 passed a defaultedfalsehere. The test proves it: with the default removed, the grouped button mounts a pulsating ripple.z-index: 1is needed today. On the concept Associations panel, the outlined scope group fills its selected button. Without thez-index, that fill covered the right edge of a focused neighbour's ring. With it, the ring shows in full. The same holds for a contained group's neighbour.box-shadowis a safeguard. A contained group setsbox-shadow: noneon its buttons, as specific as the button's ring rule, so the stylesheet loaded last wins. Once any contained button has rendered first, a focused grouped button loses the ring. No screen reaches a contained group today (MapButton's split-button branch is unused), but the rule covers it. Focused and hovered, a grouped button keeps the ring: the group's:hoverreset doesn't win.How it was tested
Integration tests,
src/common/__integration__/buttonFocus.test.js, run bynpm run test:integration:buttonFocusComponents, then focuses each.box-shadowis the ring, and that no.MuiTouchRipple-childPulsateis mounted. For the grouped button it also checksz-index: 1.disableFocusRippleis removed (1 pulsating ripple), and the button tests fail if the ring is removed.box-shadow: none), whether it runs alone or with the others: it renders a lone contained button first. It also fails without thez-index.public/bootstrap.min.css. It renders a contained link-button and a contained<button>with its ownsxcolor. On focus and on hover, the link's text must be the contrast text at 4.5:1 or more against its fill, and the<button>must keep its own color. happy-dom can't match:hover, so the test adds a removable stylesheet that repeats each:hoverrule for a class.#23527c. The test fails if the rule is dropped, if either the:hoveror the:focushalf is dropped, or if theascope is dropped.Why this approach:
index.jsxbuilds the theme and also renders the app, so a test can't import it.colors.jsx, and Node's test runner can't load.jsx..jsmodule that takes the two ring colors.index.jsxpassesWHITEandCOLORS.primary['30'].:focus-visible, so a plainfocus()takes MUI's real focus-visible path. No new test dependencies.Rendered check: I built a throwaway harness (not committed) with webpack, using the theme built the same way plus Bootstrap and
index.scss, and tabbed through it in headless Chromium.z-index: 1. I checked an Associations-style outlined group with and without it, as described above.#23527cunderlined, 1.26:1;<button>;primary.dark: 9.36:1.CI gate, as
.github/workflows/pr.ymlruns it, afternpm ci:./node_modules/eslint/bin/eslint.js src --ext .jsx,.js: exit 0.npm run test:all: unit tests 603 of 603 pass, integration tests 11 of 11 pass.Acceptance criteria this PR covers
The oclweb3, community site and design-system criteria are separate PRs.
Buttons with their own focus styles
No button's own styles set a
box-shadow, so everyMuiButtonin the Mapper now shows the ring on keyboard focus. None loses all visible focus.OwnerButton.jsx,RepoVersionButton.jsx: on focus these setoutline: none, a transparent background and inherited color. The focus ripple used to be their only sign of focus; now they show the ring. With a link they render as<a>and pick up Bootstrap's underline on focus. Text is 9.30:1.common/Link.jsx(a textButtonas<a>) and theno-anchor-stylessign-in button inUserMenu.jsx: these set color, decoration andoutline: noneon focus. They show the ring.QuotaDialog.jsxaction button (contained,<a>): it already kept white text on focus, and it hides Bootstrap'sa:focusoutline only for mouse focus. On keyboard focus it shows the ring with Bootstrap's 5px auto outline inside it. Text is 6.47:1..MuiButtonBase-rootrules inColumnMapTable.jsxandMapProject.jsx: these set color only, on Autocomplete and DataGrid header icon buttons, not onMuiButton. Unchanged.Candidates.jsx'sListItemButtons: these aren'tMuiButton, so they're out of scope and unchanged.Review
🤖 Generated with Claude Code