From d7f04107434eb2e97834e9828860fcfa5d6cad56 Mon Sep 17 00:00:00 2001 From: leahpeker Date: Thu, 23 Jul 2026 10:11:16 -0400 Subject: [PATCH 1/3] fix(mobile): raise text input font-size to 16px to stop ios safari zoom (Issue 1131) iOS Safari auto-zooms the viewport when a focused input, textarea, or select computes font-size below 16px, and the viewport meta tag does not disable user-scalable, so the user is left zoomed in with no way to zoom back out without manually pinching. text-sm (14px) was used on the shared TextField, Textarea, and Select components plus a few call sites duplicating the same classes (CommentComposer, LocationField, DateTimePicker time input, EmailBlastDialog textarea). Bumped the base to text-base (16px) with an md:text-sm override to keep the compact desktop look. --- frontend/src/components/ui/DateTimePicker.tsx | 2 +- frontend/src/components/ui/LocationField.tsx | 2 +- frontend/src/components/ui/Select.tsx | 2 +- frontend/src/components/ui/TextField.tsx | 2 +- frontend/src/components/ui/Textarea.tsx | 2 +- frontend/src/screens/events/EmailBlastDialog.tsx | 2 +- frontend/src/screens/events/comments/CommentComposer.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/ui/DateTimePicker.tsx b/frontend/src/components/ui/DateTimePicker.tsx index 6484ab76..8c6363bf 100644 --- a/frontend/src/components/ui/DateTimePicker.tsx +++ b/frontend/src/components/ui/DateTimePicker.tsx @@ -139,7 +139,7 @@ export function DateTimePicker({ const base = selectedDate ?? new Date(); onChange(dateToIso(base, h, m)); }} - className="border-border bg-surface focus:border-brand-500 focus:ring-brand-200 h-8 rounded-md border px-2 text-sm outline-none focus:ring-1" + className="border-border bg-surface focus:border-brand-500 focus:ring-brand-200 h-8 rounded-md border px-2 text-base outline-none focus:ring-1 md:text-sm" /> diff --git a/frontend/src/components/ui/LocationField.tsx b/frontend/src/components/ui/LocationField.tsx index a985e137..a9d11be7 100644 --- a/frontend/src/components/ui/LocationField.tsx +++ b/frontend/src/components/ui/LocationField.tsx @@ -133,7 +133,7 @@ export function LocationField({ maxLength={maxLength} placeholder={placeholder} className={[ - 'bg-surface h-10 w-full rounded-md border px-3 text-sm transition-colors outline-none focus:ring-2', + 'bg-surface h-10 w-full rounded-md border px-3 text-base transition-colors outline-none focus:ring-2 md:text-sm', 'border-border-strong focus:border-brand-500 focus:ring-brand-200', error && 'border-destructive-border focus:border-red-500 focus:ring-red-100', disabled && 'bg-surface-dim text-muted-foreground', diff --git a/frontend/src/components/ui/Select.tsx b/frontend/src/components/ui/Select.tsx index 611fd8b4..7f8f9d0c 100644 --- a/frontend/src/components/ui/Select.tsx +++ b/frontend/src/components/ui/Select.tsx @@ -31,7 +31,7 @@ export const Select = forwardRef(function Select( aria-invalid={error ? true : undefined} aria-describedby={error ? `${inputId}-error` : undefined} className={cn( - 'border-border-strong bg-surface focus:ring-border h-10 w-full appearance-none rounded-md border pr-9 pl-3 text-sm transition-colors outline-none focus:border-neutral-500 focus:ring-2', + 'border-border-strong bg-surface focus:ring-border h-10 w-full appearance-none rounded-md border pr-9 pl-3 text-base transition-colors outline-none focus:border-neutral-500 focus:ring-2 md:text-sm', error && 'border-destructive-border focus:border-red-500 focus:ring-red-100', className, )} diff --git a/frontend/src/components/ui/TextField.tsx b/frontend/src/components/ui/TextField.tsx index e8589d99..408f9a6e 100644 --- a/frontend/src/components/ui/TextField.tsx +++ b/frontend/src/components/ui/TextField.tsx @@ -31,7 +31,7 @@ export const TextField = forwardRef(function TextField( aria-invalid={error ? true : undefined} aria-describedby={describedBy} className={cn( - 'focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface h-10 w-full rounded-md border px-3 text-sm transition-colors outline-none focus:ring-2', + 'focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface h-10 w-full rounded-md border px-3 text-base transition-colors outline-none focus:ring-2 md:text-sm', error && 'border-destructive-border focus:border-red-500 focus:ring-red-100', rightAdornment && 'pr-10', className, diff --git a/frontend/src/components/ui/Textarea.tsx b/frontend/src/components/ui/Textarea.tsx index 399d4209..b8fbf3b9 100644 --- a/frontend/src/components/ui/Textarea.tsx +++ b/frontend/src/components/ui/Textarea.tsx @@ -26,7 +26,7 @@ export const Textarea = forwardRef(function Textarea aria-invalid={error ? true : undefined} aria-describedby={describedBy} className={cn( - 'focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface min-h-[80px] w-full rounded-md border px-3 py-2 text-sm transition-colors outline-none focus:ring-2', + 'focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface min-h-[80px] w-full rounded-md border px-3 py-2 text-base transition-colors outline-none focus:ring-2 md:text-sm', error && 'border-destructive-border focus:border-red-500 focus:ring-red-100', className, )} diff --git a/frontend/src/screens/events/EmailBlastDialog.tsx b/frontend/src/screens/events/EmailBlastDialog.tsx index 3fbc29fc..cfabcd1a 100644 --- a/frontend/src/screens/events/EmailBlastDialog.tsx +++ b/frontend/src/screens/events/EmailBlastDialog.tsx @@ -170,7 +170,7 @@ export function EmailBlastDialog({ event, open, onClose }: Props) { onChange={(e) => { setMessage(e.target.value); }} - className="border-border-strong bg-surface focus:ring-brand-200 focus:border-brand-500 h-32 w-full rounded-md border px-3 py-2 text-sm transition-colors outline-none focus:ring-2" + className="border-border-strong bg-surface focus:ring-brand-200 focus:border-brand-500 h-32 w-full rounded-md border px-3 py-2 text-base transition-colors outline-none focus:ring-2 md:text-sm" />
diff --git a/frontend/src/screens/events/comments/CommentComposer.tsx b/frontend/src/screens/events/comments/CommentComposer.tsx index 1e3e3360..148d80c8 100644 --- a/frontend/src/screens/events/comments/CommentComposer.tsx +++ b/frontend/src/screens/events/comments/CommentComposer.tsx @@ -70,7 +70,7 @@ export function CommentComposer({ void submit(); } }} - className="focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface min-h-[80px] w-full rounded-md border px-3 py-2 text-sm transition-colors outline-none focus:ring-2" + className="focus:border-brand-500 focus:ring-brand-200 border-border-strong bg-surface min-h-[80px] w-full rounded-md border px-3 py-2 text-base transition-colors outline-none focus:ring-2 md:text-sm" />
Date: Thu, 23 Jul 2026 10:27:51 -0400 Subject: [PATCH 2/3] fix(events): wrap remaining user content on event detail so it can't overflow (Issue 1131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reported symptom — the event page always loads zoomed in on iOS Safari and re-zooms in whenever you submit a form — is horizontal overflow, the same bug class as Issue 611. When a user-entered field holds a long unbroken token (a pasted url, a spaceless location, a zelle handle), it has no break opportunity and pushes the layout wider than the phone viewport; iOS Safari then renders the whole page at a zoomed-in scale and resets to it on re-layout (e.g. after a submit). It is not the input-focus auto-zoom the earlier font-size change targeted. Issue 611 only covered the title + description. This adds break-words [overflow-wrap:anywhere] to the fields it missed: the location link, the "other" link label, the price/zelle cost lines, and comment + reply bodies (the field most likely to overflow right after a submit). Adds regression tests mirroring the 611 test. --- .../EventMemberSection.overflow.test.tsx | 45 +++++++++++++++++++ .../src/screens/events/EventMemberSection.tsx | 10 +++-- .../events/comments/CommentItem.test.tsx | 11 +++++ .../screens/events/comments/CommentItem.tsx | 4 +- .../src/screens/events/comments/ReplyItem.tsx | 4 +- 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 frontend/src/screens/events/EventMemberSection.overflow.test.tsx diff --git a/frontend/src/screens/events/EventMemberSection.overflow.test.tsx b/frontend/src/screens/events/EventMemberSection.overflow.test.tsx new file mode 100644 index 00000000..b551d030 --- /dev/null +++ b/frontend/src/screens/events/EventMemberSection.overflow.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { describe, expect, it } from 'vitest'; + +import type { Event } from '@/models/event'; + +import { CostSection, LinksSection, LocationSection } from './EventMemberSection'; + +const LONG_TOKEN = 'x'.repeat(200); + +function renderIn(node: React.ReactElement) { + return render({node}); +} + +// A long unbroken user-entered string (url/location/zelle) has no break +// opportunity; without overflow-wrap it pushes the layout wider than the phone +// viewport and iOS Safari renders the whole page zoomed in (issue 1131, same +// class as issue 611 which only covered the title + description). +describe('event detail — long user content wraps (issue 1131)', () => { + it('wraps a long location so it cannot overflow the viewport', () => { + const event = { location: `somewhere/${LONG_TOKEN}` } as Event; + renderIn(); + const link = screen.getByRole('link'); + expect(link).toHaveClass('break-words', '[overflow-wrap:anywhere]'); + }); + + it('wraps a long other-link label so it cannot overflow the viewport', () => { + const event = { + otherLink: `https://example.com/${LONG_TOKEN}`, + surveySlugs: [], + } as unknown as Event; + renderIn(); + const link = screen.getByRole('link'); + expect(link).toHaveClass('break-words', '[overflow-wrap:anywhere]'); + }); + + it('wraps long zelle info so it cannot overflow the viewport', () => { + const event = { zelleInfo: LONG_TOKEN } as Event; + renderIn(); + expect(screen.getByText(new RegExp(LONG_TOKEN.slice(0, 40)))).toHaveClass( + 'break-words', + '[overflow-wrap:anywhere]', + ); + }); +}); diff --git a/frontend/src/screens/events/EventMemberSection.tsx b/frontend/src/screens/events/EventMemberSection.tsx index ecbdf0be..2b68c2d5 100644 --- a/frontend/src/screens/events/EventMemberSection.tsx +++ b/frontend/src/screens/events/EventMemberSection.tsx @@ -113,7 +113,7 @@ export function LocationSection({ event }: { event: Event }) { target="_blank" rel="noopener noreferrer" aria-label={`open ${event.location} in maps`} - className="text-brand-700 hover:text-brand-900 text-sm" + className="text-brand-700 hover:text-brand-900 text-sm [overflow-wrap:anywhere] break-words" > {primary} @@ -135,7 +135,7 @@ export function LinksSection({ event }: { event: Event }) { href={l.url} target="_blank" rel="noopener noreferrer" - className="text-brand-700 hover:text-brand-900" + className="text-brand-700 hover:text-brand-900 [overflow-wrap:anywhere] break-words" > {l.label} @@ -170,12 +170,14 @@ export function CostSection({ event }: { event: Event }) { href={item.url} target="_blank" rel="noopener noreferrer" - className="text-brand-700 hover:text-brand-900" + className="text-brand-700 hover:text-brand-900 [overflow-wrap:anywhere] break-words" > {item.label} ) : ( - {item.label} + + {item.label} + )} ))} diff --git a/frontend/src/screens/events/comments/CommentItem.test.tsx b/frontend/src/screens/events/comments/CommentItem.test.tsx index ca4331e2..787db706 100644 --- a/frontend/src/screens/events/comments/CommentItem.test.tsx +++ b/frontend/src/screens/events/comments/CommentItem.test.tsx @@ -49,6 +49,17 @@ describe('CommentItem', () => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); + // A long unbroken token (e.g. a pasted url) in a just-submitted comment must + // wrap; without it the body pushes the page wider than the phone viewport and + // iOS Safari re-zooms in on submit (issue 1131). + it('wraps a long unbroken body so it cannot overflow the viewport', () => { + const longToken = `https://example.com/${'x'.repeat(200)}`; + wrap( + , + ); + expect(screen.getByText(longToken)).toHaveClass('break-words', '[overflow-wrap:anywhere]'); + }); + it('renders [deleted] placeholder when isDeleted', () => { wrap( [deleted]

) : ( -

{comment.body}

+

+ {comment.body} +

)} {!comment.isDeleted ? (
diff --git a/frontend/src/screens/events/comments/ReplyItem.tsx b/frontend/src/screens/events/comments/ReplyItem.tsx index 6e9e10ea..0af7a183 100644 --- a/frontend/src/screens/events/comments/ReplyItem.tsx +++ b/frontend/src/screens/events/comments/ReplyItem.tsx @@ -51,7 +51,9 @@ export function ReplyItem({ reply, eventId, token, canReact, reactDisabledReason {reply.isDeleted ? (

[deleted]

) : ( -

{reply.body}

+

+ {reply.body} +

)} {!reply.isDeleted ? (
From 30c18e6c70fcf1e9c5e2d584bdc3a1d8c7589839 Mon Sep 17 00:00:00 2001 From: leahpeker Date: Fri, 24 Jul 2026 18:06:45 -0400 Subject: [PATCH 3/3] chore(tests): remove verbose comments in overflow-wrap tests --- .../src/screens/events/EventMemberSection.overflow.test.tsx | 4 ---- frontend/src/screens/events/comments/CommentItem.test.tsx | 3 --- 2 files changed, 7 deletions(-) diff --git a/frontend/src/screens/events/EventMemberSection.overflow.test.tsx b/frontend/src/screens/events/EventMemberSection.overflow.test.tsx index b551d030..6cc95052 100644 --- a/frontend/src/screens/events/EventMemberSection.overflow.test.tsx +++ b/frontend/src/screens/events/EventMemberSection.overflow.test.tsx @@ -12,10 +12,6 @@ function renderIn(node: React.ReactElement) { return render({node}); } -// A long unbroken user-entered string (url/location/zelle) has no break -// opportunity; without overflow-wrap it pushes the layout wider than the phone -// viewport and iOS Safari renders the whole page zoomed in (issue 1131, same -// class as issue 611 which only covered the title + description). describe('event detail — long user content wraps (issue 1131)', () => { it('wraps a long location so it cannot overflow the viewport', () => { const event = { location: `somewhere/${LONG_TOKEN}` } as Event; diff --git a/frontend/src/screens/events/comments/CommentItem.test.tsx b/frontend/src/screens/events/comments/CommentItem.test.tsx index 787db706..04b73e68 100644 --- a/frontend/src/screens/events/comments/CommentItem.test.tsx +++ b/frontend/src/screens/events/comments/CommentItem.test.tsx @@ -49,9 +49,6 @@ describe('CommentItem', () => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); - // A long unbroken token (e.g. a pasted url) in a just-submitted comment must - // wrap; without it the body pushes the page wider than the phone viewport and - // iOS Safari re-zooms in on submit (issue 1131). it('wraps a long unbroken body so it cannot overflow the viewport', () => { const longToken = `https://example.com/${'x'.repeat(200)}`; wrap(