fix(ui): enforce accessible theme contrast
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
--color-sidebar: #f0f1f2;
|
||||
--color-panel: #ffffff;
|
||||
--color-panel-muted: #f7f8f9;
|
||||
--color-border: #dfe1e5;
|
||||
--color-border-strong: #c9cdd3;
|
||||
--color-border: #858c96;
|
||||
--color-border-strong: #747d89;
|
||||
--color-muted: #626a75;
|
||||
--color-accent: #3157e7;
|
||||
--color-success: #087a48;
|
||||
@@ -23,10 +23,10 @@
|
||||
--sidebar: #f0f1f2;
|
||||
--panel: #ffffff;
|
||||
--panel-muted: #f7f8f9;
|
||||
--border: #dfe1e5;
|
||||
--border-strong: #c9cdd3;
|
||||
--border: #858c96;
|
||||
--border-strong: #747d89;
|
||||
--muted: #626a75;
|
||||
--subtle: #818894;
|
||||
--subtle: #626a75;
|
||||
--accent: #3157e7;
|
||||
--focus: #3157e7;
|
||||
--success: #087a48;
|
||||
@@ -41,8 +41,8 @@
|
||||
--sidebar: #111317;
|
||||
--panel: #17191e;
|
||||
--panel-muted: #1d2026;
|
||||
--border: #30343d;
|
||||
--border-strong: #454b57;
|
||||
--border: #626b78;
|
||||
--border-strong: #707987;
|
||||
--muted: #b2b7c0;
|
||||
--subtle: #969da8;
|
||||
--accent: #8ca4ff;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
@@ -70,6 +72,28 @@ class UiContractWebTest {
|
||||
assertTrue(themeBootstrap.contains("matchMedia('(prefers-color-scheme: dark)')"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void themeTokensMeetTextFocusAndMeaningfulBoundaryContrast() throws Exception {
|
||||
String css = new ClassPathResource("static/assets/app.css")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String light = section(css, ":root\\{color-scheme:light;([^}]*)}");
|
||||
String dark = section(css, ":root\\[data-theme=dark]\\{color-scheme:dark;([^}]*)}");
|
||||
|
||||
assertContrast(light, "ink", "canvas", 4.5);
|
||||
assertContrast(light, "muted", "panel", 4.5);
|
||||
assertContrast(light, "subtle", "sidebar", 4.5);
|
||||
assertContrast(light, "border", "canvas", 3.0);
|
||||
assertContrast(light, "border-strong", "panel", 3.0);
|
||||
assertContrast(light, "focus", "canvas", 3.0);
|
||||
|
||||
assertContrast(dark, "ink", "canvas", 4.5);
|
||||
assertContrast(dark, "muted", "panel", 4.5);
|
||||
assertContrast(dark, "subtle", "sidebar", 4.5);
|
||||
assertContrast(dark, "border", "panel", 3.0);
|
||||
assertContrast(dark, "border-strong", "panel", 3.0);
|
||||
assertContrast(dark, "focus", "panel", 3.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
|
||||
void sharedComponentsExposeAccessibleFormsStatusAndEmptyState() throws Exception {
|
||||
@@ -100,4 +124,48 @@ class UiContractWebTest {
|
||||
return "test/components-consumer";
|
||||
}
|
||||
}
|
||||
|
||||
private static String section(String css, String expression) {
|
||||
Matcher matcher = Pattern.compile(expression).matcher(css);
|
||||
assertTrue(matcher.find(), () -> "Missing CSS token section: " + expression);
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
private static void assertContrast(String section, String foreground, String background, double minimum) {
|
||||
double ratio = contrast(color(section, foreground), color(section, background));
|
||||
assertTrue(ratio >= minimum,
|
||||
() -> foreground + " / " + background + " contrast " + ratio + " is below " + minimum);
|
||||
}
|
||||
|
||||
private static String color(String section, String name) {
|
||||
Matcher matcher = Pattern.compile("--" + Pattern.quote(name) + ":(#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?)")
|
||||
.matcher(section);
|
||||
assertTrue(matcher.find(), () -> "Missing CSS color token: " + name);
|
||||
String value = matcher.group(1);
|
||||
if (value.length() == 4) {
|
||||
return "#" + value.charAt(1) + value.charAt(1)
|
||||
+ value.charAt(2) + value.charAt(2)
|
||||
+ value.charAt(3) + value.charAt(3);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double contrast(String first, String second) {
|
||||
double lighter = Math.max(luminance(first), luminance(second));
|
||||
double darker = Math.min(luminance(first), luminance(second));
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
private static double luminance(String hex) {
|
||||
double red = linear(Integer.parseInt(hex.substring(1, 3), 16) / 255.0);
|
||||
double green = linear(Integer.parseInt(hex.substring(3, 5), 16) / 255.0);
|
||||
double blue = linear(Integer.parseInt(hex.substring(5, 7), 16) / 255.0);
|
||||
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||||
}
|
||||
|
||||
private static double linear(double component) {
|
||||
return component <= 0.04045
|
||||
? component / 12.92
|
||||
: Math.pow((component + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user