diff --git a/locales/rn/de.json b/locales/rn/de.json index 9cce5e08..7ea5fb1f 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -76,6 +76,7 @@ "clear_confirm": "Alle entfernen", "clear_description": "Alle Termine in diesem Kalender werden gelöscht. Termine, die auch zu einem anderen Kalender gehören, werden nur aus diesem entfernt.", "clear_title": "Alle Termine entfernen?", + "color_invalid": "Geben Sie eine Farbe als sechs Hex-Ziffern ein, mit oder ohne #, etwa #3b82f6.", "delete_description": "Der Kalender und alle seine Termine werden gelöscht. Dies kann nicht rückgängig gemacht werden.", "delete_title": "Kalender löschen?", "description_field": "Beschreibung", diff --git a/locales/rn/en.json b/locales/rn/en.json index a819ca55..04385b69 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -95,7 +95,7 @@ "clear_confirm": "Remove all", "clear_description": "Every event in this calendar will be deleted. Events that also belong to another calendar are only unlinked.", "clear_title": "Remove all events?", - "color_invalid": "Enter a color as # and six hex digits, such as #3b82f6.", + "color_invalid": "Enter a color as six hex digits, with or without #, such as #3b82f6.", "custom_color": "Custom color (hex)", "delete_description": "The calendar and all of its events will be deleted. This cannot be undone.", "delete_title": "Delete calendar?", diff --git a/src/components/calendar/CalendarEditSheet.tsx b/src/components/calendar/CalendarEditSheet.tsx index 2c1fcf94..739aebc0 100644 --- a/src/components/calendar/CalendarEditSheet.tsx +++ b/src/components/calendar/CalendarEditSheet.tsx @@ -27,7 +27,7 @@ import { radius, spacing, typography, type ThemePalette } from '../../theme/toke import { useColors } from '../../theme/colors'; import { useLocaleStore, type TranslateFn } from '../../stores/locale-store'; import { CALENDAR_COLOR_PALETTE, calendarColorName, getCalendarColor } from '../../lib/calendar-utils'; -import { customColorGrid, normalizeHexColor } from '../../lib/calendar-custom-color'; +import { customColorGrid, HEX_FIELD_MAX_LENGTH, hexFieldText, normalizeHexColor } from '../../lib/calendar-custom-color'; const CUSTOM_COLOR_GRID = customColorGrid(); import Button from '../Button'; @@ -260,11 +260,14 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend const hex = normalizeHexColor(v); if (hex) setColor(hex); }} + // "3b82f6" works as well as "#3b82f6"; leaving the field + // shows it with its "#". + onBlur={() => setHexInput((v) => hexFieldText(v))} placeholder="#3b82f6" placeholderTextColor={c.textMuted} autoCapitalize="none" autoCorrect={false} - maxLength={7} + maxLength={HEX_FIELD_MAX_LENGTH} accessibilityLabel={t('calendar.management.custom_color', 'Custom color (hex)')} style={[styles.input, styles.hexInput, hexInvalid && styles.inputInvalid]} /> @@ -283,7 +286,7 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend {hexInvalid && ( - {t('calendar.management.color_invalid', 'Enter a color as # and six hex digits, such as #3b82f6.')} + {t('calendar.management.color_invalid', 'Enter a color as six hex digits, with or without #, such as #3b82f6.')} )} {moreColors && ( diff --git a/src/lib/__tests__/calendar-custom-color.test.ts b/src/lib/__tests__/calendar-custom-color.test.ts index e8f534bb..0a41bbba 100644 --- a/src/lib/__tests__/calendar-custom-color.test.ts +++ b/src/lib/__tests__/calendar-custom-color.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { customColorGrid, normalizeHexColor } from '../calendar-custom-color'; +import { customColorGrid, HEX_FIELD_MAX_LENGTH, hexFieldText, normalizeHexColor } from '../calendar-custom-color'; describe('normalizeHexColor', () => { it('takes a typed hex colour in its usual spellings', () => { @@ -23,3 +23,31 @@ describe('customColorGrid', () => { expect(grid[1][0]).toBe('#c32222'); }); }); + +describe('the hex field', () => { + it('shows a colour typed without "#" with it once the user leaves the field', () => { + expect(hexFieldText('3b82f6')).toBe('#3b82f6'); + expect(hexFieldText('3B82F6')).toBe('#3b82f6'); + expect(hexFieldText('#3b82f6')).toBe('#3b82f6'); + expect(hexFieldText('3b8')).toBe('#33bb88'); + // Not a colour: left as typed for the hint under the field. + expect(hexFieldText('3b82f')).toBe('3b82f'); + }); + + it('takes a pasted colour with spaces around it', () => { + const pasted = ' #3b82f6 '; + expect(pasted.length).toBeLessThanOrEqual(HEX_FIELD_MAX_LENGTH); + expect(normalizeHexColor(pasted.slice(0, HEX_FIELD_MAX_LENGTH))).toBe('#3b82f6'); + }); +}); + +describe('the calendar sheet’s hex field', () => { + it('spells the colour out on blur and takes the longer pasted text', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const sheet = fs.readFileSync(path.resolve(__dirname, '../../components/calendar/CalendarEditSheet.tsx'), 'utf8'); + const field = sheet.slice(sheet.indexOf('value={hexInput}'), sheet.indexOf('/>', sheet.indexOf('value={hexInput}'))); + expect(field).toMatch(/onBlur=\{\(\) => setHexInput\(\(v\) => hexFieldText\(v\)\)\}/); + expect(field).toMatch(/maxLength=\{HEX_FIELD_MAX_LENGTH\}/); + }); +}); diff --git a/src/lib/calendar-custom-color.ts b/src/lib/calendar-custom-color.ts index 2217b414..7ede5bb5 100644 --- a/src/lib/calendar-custom-color.ts +++ b/src/lib/calendar-custom-color.ts @@ -10,6 +10,21 @@ export function normalizeHexColor(input: string): string | null { return `#${hex.toLowerCase()}`; } +/** + * What the hex field shows once the user leaves it: a colour typed without + * its "#", in capitals or as three digits, spelled out as "#rrggbb"; anything + * else stays as typed, so the hint under the field still applies to it. + */ +export function hexFieldText(input: string): string { + return normalizeHexColor(input) ?? input; +} + +/** + * Longest text the hex field takes: "#rrggbb" plus a space either side, as a + * pasted value can carry. Seven cut a pasted " #3b82f6" to an invalid colour. + */ +export const HEX_FIELD_MAX_LENGTH = 9; + function hslToHex(h: number, s: number, l: number): string { const a = s * Math.min(l, 1 - l); const f = (n: number) => {