diff --git a/src/components/calendar/EventBlock.tsx b/src/components/calendar/EventBlock.tsx index b1422085..0374a6d7 100644 --- a/src/components/calendar/EventBlock.tsx +++ b/src/components/calendar/EventBlock.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native'; +import { Pressable, StyleSheet, Text, View, useWindowDimensions, type StyleProp, type ViewStyle } from 'react-native'; import { TINTED_BAR_WIDTH, type EventBlockColors } from '../../lib/event-colors'; +import { EVENT_TIME_LINE_HEIGHT, EVENT_TITLE_LINE_HEIGHT, timedTitleLines } from '../../lib/calendar-time-grid'; // Events in the week and day grids, shared by the paged WeekView and the // scrolling TimeGridScrollView. Every event is a solid block of its calendar @@ -51,7 +52,9 @@ interface TimedEventBlockProps { continuesBefore: boolean; /** The event runs into the next day (square bottom corners). */ continuesAfter: boolean; - /** Position and size in the day column. */ + /** Height in the day column; the title wraps over as many lines as it leaves room for. */ + height: number; + /** Position and width in the day column. */ style: StyleProp; onPress?: () => void; /** What a screen reader says: title, date, time (see lib/calendar-a11y). */ @@ -66,11 +69,13 @@ export function TimedEventBlock({ ringColor, continuesBefore, continuesAfter, + height, style, onPress, accessibilityLabel, }: TimedEventBlockProps) { const inactive = colors.border !== null; + const { fontScale } = useWindowDimensions(); return ( {colors.bar ? : null} @@ -98,7 +104,7 @@ export function TimedEventBlock({ )} {title} @@ -173,8 +179,8 @@ const styles = StyleSheet.create({ barWithBar: { paddingStart: 4 + TINTED_BAR_WIDTH }, // "Light event colors": the calendar colour along the start edge. startBar: { position: 'absolute', top: 0, bottom: 0, start: 0, width: TINTED_BAR_WIDTH }, - blockTitle: { fontSize: 12, lineHeight: 15, fontWeight: '500' }, - blockTime: { fontSize: 10.5, lineHeight: 13, fontWeight: '400', opacity: 0.85 }, + blockTitle: { fontSize: 12, lineHeight: EVENT_TITLE_LINE_HEIGHT, fontWeight: '500' }, + blockTime: { fontSize: 10.5, lineHeight: EVENT_TIME_LINE_HEIGHT, fontWeight: '400', opacity: 0.85 }, struck: { textDecorationLine: 'line-through' }, bar: { position: 'absolute', diff --git a/src/components/calendar/TimeGridScrollView.tsx b/src/components/calendar/TimeGridScrollView.tsx index 7c3f2a05..a6708b96 100644 --- a/src/components/calendar/TimeGridScrollView.tsx +++ b/src/components/calendar/TimeGridScrollView.tsx @@ -662,7 +662,8 @@ const DayColumn = React.memo(function DayColumn({ continuesAfter={continuesAfter} accessibilityLabel={eventAccessibilityLabel(event, { t, locale, timeFormat, calendars, currentUserEmails })} onPress={() => onSelectEvent?.(event)} - style={{ top, height, left: `${column * widthPct}%`, width: `${widthPct}%` }} + height={height} + style={{ top, left: `${column * widthPct}%`, width: `${widthPct}%` }} /> ); })} diff --git a/src/components/calendar/WeekView.tsx b/src/components/calendar/WeekView.tsx index 2d47f51d..cbea2159 100644 --- a/src/components/calendar/WeekView.tsx +++ b/src/components/calendar/WeekView.tsx @@ -300,7 +300,8 @@ function WeekViewInner({ continuesAfter={continuesAfter} accessibilityLabel={eventAccessibilityLabel(event, { t, locale, timeFormat, calendars, currentUserEmails })} onPress={() => onSelectEvent?.(event)} - style={{ top, height, left: `${leftPct}%`, width: `${widthPct}%` }} + height={height} + style={{ top, left: `${leftPct}%`, width: `${widthPct}%` }} /> ); }, diff --git a/src/lib/__tests__/calendar-time-grid.test.ts b/src/lib/__tests__/calendar-time-grid.test.ts index 97bf8ca1..ce54f0f8 100644 --- a/src/lib/__tests__/calendar-time-grid.test.ts +++ b/src/lib/__tests__/calendar-time-grid.test.ts @@ -13,6 +13,7 @@ import { hourAtOffset, multiDayCount, timeGridFocusColumn, + timedTitleLines, windowDays, } from '../calendar-time-grid'; @@ -121,6 +122,32 @@ describe('hourAtOffset', () => { }); }); +describe('timedTitleLines', () => { + // The grids draw 48 px an hour and give a block its length less a pixel. + const block = (minutes: number) => Math.max(20, (minutes / 60) * 48 - 1); + + it('keeps short events to one line', () => { + expect(timedTitleLines(block(15), false)).toBe(1); + expect(timedTitleLines(block(30), false)).toBe(1); + expect(timedTitleLines(block(45), true)).toBe(1); + }); + + it('wraps the title of longer events over the room above the time line (#126)', () => { + expect(timedTitleLines(block(60), true)).toBe(2); + expect(timedTitleLines(block(90), true)).toBe(3); + expect(timedTitleLines(block(120), true)).toBe(5); + }); + + it('counts the room by the larger lines of a bigger system text size', () => { + expect(timedTitleLines(block(60), true, 1.3)).toBe(1); + expect(timedTitleLines(block(120), true, 1.3)).toBe(3); + }); + + it('never gives less than one line', () => { + expect(timedTitleLines(5, true, 2)).toBe(1); + }); +}); + describe('multiDayCount', () => { it('shows three days on a phone, five on a medium window and seven on a wide one', () => { expect(multiDayCount(411)).toBe(3); diff --git a/src/lib/calendar-time-grid.ts b/src/lib/calendar-time-grid.ts index aa900790..e0c2936d 100644 --- a/src/lib/calendar-time-grid.ts +++ b/src/lib/calendar-time-grid.ts @@ -164,6 +164,22 @@ export function shownAllDayRows(rows: number, expanded: boolean): number { return expanded ? rows : Math.min(rows, ALL_DAY_COLLAPSED_ROWS); } +/** Line heights of a timed event block's title and time, at the default text size. */ +export const EVENT_TITLE_LINE_HEIGHT = 15; +export const EVENT_TIME_LINE_HEIGHT = 13; +/** A timed block's border and padding, top and bottom together. */ +const EVENT_BLOCK_INSET = 4; + +/** + * Lines of title a timed event block `height` tall has room for: the title + * wraps over what is left once the time line under it is placed, and never + * gets fewer than one. The line heights grow with the system text size. + */ +export function timedTitleLines(height: number, withTimeLine: boolean, fontScale = 1): number { + const room = height - EVENT_BLOCK_INSET - (withTimeLine ? EVENT_TIME_LINE_HEIGHT * fontScale : 0); + return Math.max(1, Math.floor(room / (EVENT_TITLE_LINE_HEIGHT * fontScale))); +} + /** The hour a long press at `offsetY` in a day column lands on. */ export function hourAtOffset(offsetY: number, hourHeight: number): number { return Math.max(0, Math.min(23, Math.floor(offsetY / hourHeight)));