mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
fix: make switches that are off visible against the page
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SettingItem, ToggleSwitch, RadioGroup, Select } from '../settings-section';
|
||||
@@ -59,3 +61,45 @@ describe('SettingItem controls', () => {
|
||||
expect(screen.getByRole('switch', { name: 'Standalone' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToggleSwitch off state (finding 56)', () => {
|
||||
// The token of the off track's outline and knob, per scheme, against the page.
|
||||
function token(block: string, name: string): string {
|
||||
const css = readFileSync(join(process.cwd(), 'app/globals.css'), 'utf8');
|
||||
const start = css.indexOf(block);
|
||||
const end = css.indexOf('}', start);
|
||||
const match = new RegExp(`${name}:[ \\t]*(#[0-9a-f]{6})`, 'i').exec(css.slice(start, end));
|
||||
if (!match) throw new Error(`${name} not found in ${block}`);
|
||||
return match[1];
|
||||
}
|
||||
function luminance(hex: string): number {
|
||||
const [r, g, b] = [1, 3, 5].map((i) => {
|
||||
const c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
function contrast(a: string, b: string): number {
|
||||
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
it('outlines the off track and greys its knob; the on state is unchanged', () => {
|
||||
const { rerender } = render(<ToggleSwitch checked={false} onChange={() => {}} ariaLabel="Off" />);
|
||||
const off = screen.getByRole('switch');
|
||||
expect(off).toHaveClass('ring-1', 'ring-inset', 'ring-muted-foreground');
|
||||
expect(off.firstElementChild).toHaveClass('bg-muted-foreground');
|
||||
rerender(<ToggleSwitch checked onChange={() => {}} ariaLabel="On" />);
|
||||
const on = screen.getByRole('switch');
|
||||
expect(on).toHaveClass('bg-primary');
|
||||
expect(on).not.toHaveClass('ring-1');
|
||||
expect(on.firstElementChild).toHaveClass('bg-background');
|
||||
});
|
||||
|
||||
it('reaches 3:1 against the page in light and dark', () => {
|
||||
for (const block of [':root {', '.dark {']) {
|
||||
const ratio = contrast(token(block, '--color-muted-foreground'), token(block, '--color-background'));
|
||||
expect(ratio, block).toBeGreaterThanOrEqual(3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,14 +256,17 @@ export function ToggleSwitch({ checked, onChange, disabled, ariaLabel, testId }:
|
||||
onClick={() => onChange(!checked)}
|
||||
className={cn(
|
||||
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors duration-150',
|
||||
checked ? 'bg-primary' : 'bg-muted',
|
||||
// Off: an outlined track and a grey knob, 3:1 against the page in
|
||||
// both schemes (a light track with a white knob was ~1.1:1). The
|
||||
// ring is inset, so the on state keeps its size and look.
|
||||
checked ? 'bg-primary' : 'bg-muted ring-1 ring-inset ring-muted-foreground',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block h-4 w-4 transform rounded-full bg-background transition-transform duration-150',
|
||||
checked ? 'ltr:translate-x-6 rtl:-translate-x-6' : 'ltr:translate-x-1 rtl:-translate-x-1'
|
||||
'inline-block h-4 w-4 transform rounded-full transition-transform duration-150',
|
||||
checked ? 'bg-background ltr:translate-x-6 rtl:-translate-x-6' : 'bg-muted-foreground ltr:translate-x-1 rtl:-translate-x-1'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user