diff --git a/src/components/UndoSnackbar.tsx b/src/components/UndoSnackbar.tsx index 5f84e9cf..2b11fb81 100644 --- a/src/components/UndoSnackbar.tsx +++ b/src/components/UndoSnackbar.tsx @@ -6,6 +6,7 @@ import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; +import { undoSnackbarColors } from './undo-snackbar-colors'; import { useEmailStore } from '../stores/email-store'; import { useSendUndoStore, restoreUndoneSend, type PendingUndoSend } from '../stores/send-undo-store'; import { useLocaleStore } from '../stores/locale-store'; @@ -158,6 +159,7 @@ export function UndoSnackbar() { } function makeStyles(c: ThemePalette) { + const colors = undoSnackbarColors(c); return StyleSheet.create({ wrap: { position: 'absolute', @@ -170,7 +172,7 @@ function makeStyles(c: ThemePalette) { paddingLeft: spacing.lg, paddingRight: spacing.sm, borderRadius: radius.md, - backgroundColor: c.text, + backgroundColor: colors.bar, shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 12, @@ -179,7 +181,7 @@ function makeStyles(c: ThemePalette) { }, label: { ...typography.body, - color: c.background, + color: colors.label, flex: 1, }, undoBtn: { @@ -192,7 +194,7 @@ function makeStyles(c: ThemePalette) { }, undoText: { ...typography.bodySemibold, - color: c.primary, + color: colors.action, letterSpacing: 0.5, }, sendNowText: { diff --git a/src/components/__tests__/undo-snackbar-contrast.test.ts b/src/components/__tests__/undo-snackbar-contrast.test.ts new file mode 100644 index 00000000..3a357e64 --- /dev/null +++ b/src/components/__tests__/undo-snackbar-contrast.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { DARK_COLORS, LIGHT_COLORS, type ThemePalette } from '../../theme/tokens'; +import { BUILTIN_THEMES } from '../../theme/builtin-themes'; +import { undoSnackbarColors } from '../undo-snackbar-colors'; + +// The undo bar (UndoSnackbar) is drawn in the text colour. Its UNDO button +// used the primary colour, which is white in the default dark theme: white +// on white, so in dark mode the bar showed "Email archived" and no visible +// way to undo. The bound is 3:1, not 4.5:1, because Solarized's own text +// and background are a little under 4.5:1. + +function luminance(hex: string): number { + const v = hex.replace('#', ''); + const full = v.length === 3 ? v.split('').map((ch) => ch + ch).join('') : v.slice(0, 6); + const [r, g, b] = [0, 2, 4].map((i) => { + const c = parseInt(full.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); +} + +const palettes: Array<[string, ThemePalette]> = [ + ['default light', LIGHT_COLORS], + ['default dark', DARK_COLORS], + ...BUILTIN_THEMES.flatMap((theme): Array<[string, ThemePalette]> => [ + [`${theme.name} light`, { ...LIGHT_COLORS, ...theme.light } as ThemePalette], + [`${theme.name} dark`, { ...DARK_COLORS, ...theme.dark } as ThemePalette], + ]), +]; + +describe('undo bar', () => { + it.each(palettes)('UNDO is readable on the bar (%s)', (_name, palette) => { + const { bar, action } = undoSnackbarColors(palette); + expect(contrast(action, bar)).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/src/components/undo-snackbar-colors.ts b/src/components/undo-snackbar-colors.ts new file mode 100644 index 00000000..ed2c1221 --- /dev/null +++ b/src/components/undo-snackbar-colors.ts @@ -0,0 +1,10 @@ +import type { ThemePalette } from '../theme/tokens'; + +/** + * Colours of the undo bar. The bar is drawn in the text colour, so what is + * on it takes the background colour. The primary colour can't be used for + * UNDO: the default dark theme's primary is white, like the bar. + */ +export function undoSnackbarColors(c: ThemePalette): { bar: string; label: string; action: string } { + return { bar: c.text, label: c.background, action: c.background }; +}