Skip to content

Commit 5d34658

Browse files
Merge pull request #12 from refactorfirst/fix-popup-close-button
Improve Contrast and focus ring on popup close button
2 parents 740ca69 + 557f480 commit 5d34658

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎app/globals.css‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
--btn-disabled-bg: #f3f4f6;
3737
--btn-disabled-text: #6b7280;
3838
--popup-border: #cccccc;
39+
/* Close-button red: white × on it passes AA (>= 4.5:1) and the square
40+
clears 3:1 against the white popup surface (SC 1.4.11). */
41+
--popup-close-bg: #c0392b;
42+
--popup-close-fg: #ffffff;
3943
--toast-bg: #1f2933;
4044
--toast-text: #ffffff;
4145
--menu-max-height: 140px;
@@ -95,6 +99,10 @@ html:root {
9599
--btn-disabled-bg: #232a35;
96100
--btn-disabled-text: #8a97a8;
97101
--popup-border: #3a4b61;
102+
/* Brighter red for the dark surface: keeps the white × at AA (4.6:1)
103+
and the square at 3.4:1 against --card-bg (SC 1.4.11). */
104+
--popup-close-bg: #cf4436;
105+
--popup-close-fg: #ffffff;
98106
--toast-bg: #1f2933;
99107
--toast-text: #ffffff;
100108
--color-bg: var(--bg-color);
@@ -132,6 +140,10 @@ html:root {
132140
--btn-disabled-bg: #232a35;
133141
--btn-disabled-text: #8a97a8;
134142
--popup-border: #3a4b61;
143+
/* Brighter red for the dark surface: keeps the white × at AA (4.6:1)
144+
and the square at 3.4:1 against --card-bg (SC 1.4.11). */
145+
--popup-close-bg: #cf4436;
146+
--popup-close-fg: #ffffff;
135147
--toast-bg: #1f2933;
136148
--toast-text: #ffffff;
137149
--color-bg: var(--bg-color);
@@ -774,6 +786,29 @@ main#app .popup {
774786
border-color: var(--popup-border);
775787
}
776788

789+
main#app .close-btn {
790+
/* Red square around the template's bare × button; 2rem clears the 24px
791+
pointer-target minimum (SC 2.5.8). */
792+
width: 2rem;
793+
height: 2rem;
794+
display: inline-flex;
795+
align-items: center;
796+
justify-content: center;
797+
padding: 0;
798+
border: none;
799+
border-radius: 4px;
800+
background: var(--popup-close-bg);
801+
color: var(--popup-close-fg);
802+
line-height: 1;
803+
}
804+
805+
main#app .close-btn:focus-visible {
806+
/* The ring floats off the red fill (blue on red is ~1:1) so it
807+
contrasts with the popup surface behind it instead. */
808+
outline-color: var(--brand-accent);
809+
outline-offset: 2px;
810+
}
811+
777812
main#app .rf-page-btn {
778813
background: var(--card-bg);
779814
border: 1px solid var(--brand-color);

‎tests/unit/theme-css.test.js‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,43 @@ describe('dark mode cascade structure', () => {
205205
});
206206
});
207207

208+
describe('popup close button', () => {
209+
it('is a red square re-driven from palette variables', () => {
210+
// The template ships a bare × button; the red-square chrome must come
211+
// from main#app overrides (1,1,1) so both palettes stay compatible.
212+
const close = rules.get('main#app .close-btn');
213+
expect(close).toBeTruthy();
214+
expect(close).toMatch(/background:\s*var\(--popup-close-bg\)/);
215+
expect(close).toMatch(/color:\s*var\(--popup-close-fg\)/);
216+
217+
// SC 2.5.8: the pointer target is at least 24x24 CSS px.
218+
const width = Number(close.match(/width:\s*([\d.]+)rem/)[1]);
219+
const height = Number(close.match(/height:\s*([\d.]+)rem/)[1]);
220+
expect(width).toBeGreaterThanOrEqual(1.5);
221+
expect(height).toBeGreaterThanOrEqual(1.5);
222+
});
223+
224+
it('the focus ring floats off the red square onto the popup surface', () => {
225+
// A ring hugging the red fill cannot use --brand-accent (blue on red
226+
// is ~1:1); offset it so it contrasts with --card-bg instead.
227+
const focus = rules.get('main#app .close-btn:focus-visible');
228+
expect(focus).toMatch(/outline-color:\s*var\(--brand-accent\)/);
229+
expect(focus).toMatch(/outline-offset:\s*2px/);
230+
});
231+
232+
for (const [name, palette] of [['light', lightPalette], ['dark', darkPalette]]) {
233+
it(`the white × keeps AA text contrast on the red square (${name})`, () => {
234+
expect(contrastOf(palette, '--popup-close-bg', '--popup-close-fg'))
235+
.toBeGreaterThanOrEqual(4.5);
236+
});
237+
238+
it(`the red square is perceivable against the popup surface (${name})`, () => {
239+
expect(contrastOf(palette, '--card-bg', '--popup-close-bg'))
240+
.toBeGreaterThanOrEqual(3);
241+
});
242+
}
243+
});
244+
208245
describe('toggle styles', () => {
209246
it('the toggle renders icon pills over appearance-none radios', () => {
210247
expect(rules.get('.theme-toggle')).toMatch(/display:\s*flex/);

0 commit comments

Comments
 (0)