From 0081a1c4658156641e717531c20f1ea5c3ab624d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 19 Oct 2026 15:53:41 +0000 Subject: [PATCH] fix(desktop-ui): event form MM.DD.YY date fields Co-authored-by: Tyler Dane --- .../EventForm/EventFormDateFormat.swift | 21 ++++ .../EventFormDateFormatTests.swift | 14 +++ .../EventForm/EventFormCompactDateField.swift | 75 +++++++++++++++ .../EventForm/EventFormScheduleSection.swift | 96 ++++++++++++------- 4 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift create mode 100644 apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift create mode 100644 apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift new file mode 100644 index 000000000..f33587384 --- /dev/null +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/EventForm/EventFormDateFormat.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Closed-label date format for the sidebar event form. Matches web `formatEventFormDateLabel`. +public enum EventFormDateFormat { + private static let labelFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = EffectiveTimeZone.timeZone + formatter.dateFormat = "MM.dd.yy" + return formatter + }() + + public static func formatLabel(for date: Date) -> String { + labelFormatter.string(from: date) + } + + /// Parses typed form dates using the same rules as go-to-date / web event form. + public static func parseTypedLabel(_ text: String, now: Date) -> Date? { + UserDateParsing.parseUserDate(text, now: now) + } +} diff --git a/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift b/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift new file mode 100644 index 000000000..b115f4fce --- /dev/null +++ b/apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift @@ -0,0 +1,14 @@ +import CompassKit +import XCTest + +final class EventFormDateFormatTests: XCTestCase { + override func setUp() { + super.setUp() + EffectiveTimeZone.identifier = "UTC" + } + + func testFormatLabelUsesMonthDayYearDots() throws { + let date = try XCTUnwrap(CompassDateParsing.parseInEffectiveTimeZone("2026-10-17T12:00:00")) + XCTAssertEqual(EventFormDateFormat.formatLabel(for: date), "10.17.26") + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift new file mode 100644 index 000000000..a9564b9a6 --- /dev/null +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift @@ -0,0 +1,75 @@ +import CompassKit +import SwiftUI + +struct EventFormCompactDateField: View { + @Environment(\.nativeWebTheme) private var theme + @Binding var date: Date + let accessibilityIdentifier: String + @State private var text: String + @State private var showsCalendar = false + @FocusState private var isFocused: Bool + + init(date: Binding, accessibilityIdentifier: String) { + _date = date + self.accessibilityIdentifier = accessibilityIdentifier + _text = State(initialValue: EventFormDateFormat.formatLabel(for: date.wrappedValue)) + } + + var body: some View { + TextField("Date", text: $text) + .textFieldStyle(.plain) + .font(.rubik(13)) + .foregroundStyle(theme.textColor) + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(theme.surfacePanelColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .overlay { + RoundedRectangle(cornerRadius: 4) + .stroke(isFocused ? theme.accentColor : theme.borderColor, lineWidth: 1) + } + .focused($isFocused) + .accessibilityIdentifier(accessibilityIdentifier) + .onSubmit { commitTypedDate() } + .onChange(of: isFocused) { _, focused in + if !focused { commitTypedDate() } + } + .onChange(of: date) { _, newValue in + guard !isFocused else { return } + text = EventFormDateFormat.formatLabel(for: newValue) + } + .onTapGesture(count: 2) { + showsCalendar = true + } + .popover(isPresented: $showsCalendar, arrowEdge: .bottom) { + DatePicker( + "Pick date", + selection: Binding( + get: { date }, + set: { newDate in + date = newDate + text = EventFormDateFormat.formatLabel(for: newDate) + } + ), + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + .labelsHidden() + .padding(12) + } + } + + private func commitTypedDate() { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + text = EventFormDateFormat.formatLabel(for: date) + return + } + if let parsed = EventFormDateFormat.parseTypedLabel(trimmed, now: date) { + date = parsed + text = EventFormDateFormat.formatLabel(for: parsed) + return + } + text = EventFormDateFormat.formatLabel(for: date) + } +} diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift index 4d30a7890..aa27c4ce5 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormScheduleSection.swift @@ -23,57 +23,75 @@ struct EventFormScheduleSection: View { .accessibilityIdentifier("compass-event-form-allday") if draft.schedule.kind == .timed { - DatePicker( - "Start", - selection: Binding( - get: { draft.schedule.start }, - set: { updateStart($0) } - ), - displayedComponents: [.date, .hourAndMinute] - ) - .labelsHidden() - .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-start") - .pageJumpChipAnchor(id: "form-start") + HStack(spacing: 8) { + EventFormCompactDateField( + date: Binding( + get: { draft.schedule.start }, + set: { updateStart(mergeCalendarDay(into: draft.schedule.start, day: $0)) } + ), + accessibilityIdentifier: "compass-event-form-start-date" + ) + .disabled(scheduleLocked) + .pageJumpChipAnchor(id: "form-start") - DatePicker( - "End", - selection: Binding( - get: { draft.schedule.end }, - set: { updateEnd($0) } - ), - displayedComponents: [.date, .hourAndMinute] - ) - .labelsHidden() - .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-end") - .pageJumpChipAnchor(id: "form-end") + DatePicker( + "Start time", + selection: Binding( + get: { draft.schedule.start }, + set: { updateStart($0) } + ), + displayedComponents: [.hourAndMinute] + ) + .labelsHidden() + .disabled(scheduleLocked) + .accessibilityIdentifier("compass-event-form-start") + } + + HStack(spacing: 8) { + EventFormCompactDateField( + date: Binding( + get: { draft.schedule.end }, + set: { updateEnd(mergeCalendarDay(into: draft.schedule.end, day: $0)) } + ), + accessibilityIdentifier: "compass-event-form-end-date" + ) + .disabled(scheduleLocked) + .pageJumpChipAnchor(id: "form-end") + + DatePicker( + "End time", + selection: Binding( + get: { draft.schedule.end }, + set: { updateEnd($0) } + ), + displayedComponents: [.hourAndMinute] + ) + .labelsHidden() + .disabled(scheduleLocked) + .accessibilityIdentifier("compass-event-form-end") + } } else { - DatePicker( - "Start day", - selection: Binding( + EventFormCompactDateField( + date: Binding( get: { draft.schedule.start }, set: { model.updateAllDayDraftStart($0) } ), - displayedComponents: [.date] + accessibilityIdentifier: "compass-event-form-start" ) .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-start") .pageJumpChipAnchor(id: "form-start") - DatePicker( - "End day", - selection: Binding( + EventFormCompactDateField( + date: Binding( get: { EffectiveTimeZone.calendar.date(byAdding: .day, value: -1, to: draft.schedule.end) ?? draft.schedule.start }, set: { model.updateAllDayDraftEnd($0) } ), - displayedComponents: [.date] + accessibilityIdentifier: "compass-event-form-end" ) .disabled(scheduleLocked) - .accessibilityIdentifier("compass-event-form-end") .pageJumpChipAnchor(id: "form-end") } @@ -90,6 +108,16 @@ struct EventFormScheduleSection: View { .eventFormSectionChrome(focused: focusField == .start || focusField == .end) } + private func mergeCalendarDay(into scheduleDate: Date, day: Date) -> Date { + var calendar = EffectiveTimeZone.calendar + let time = calendar.dateComponents([.hour, .minute, .second], from: scheduleDate) + var dayComponents = calendar.dateComponents([.year, .month, .day], from: day) + dayComponents.hour = time.hour + dayComponents.minute = time.minute + dayComponents.second = time.second ?? 0 + return calendar.date(from: dayComponents) ?? day + } + private func updateStart(_ date: Date) { var schedule = draft.schedule schedule.start = date