From 06d5ec1a77dfe97d3ddb61c50bed22928511c3c9 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Tue, 20 Oct 2026 18:54:19 -0600 Subject: [PATCH] fix(macos): use web's month grid for the form date picker The event form date popover was the system graphical DatePicker. It now reuses the sidebar month picker (Rubik, ink circle on the selection, Go to this month, Sunday first), with clickable days, and picking a day keeps the time of day. The time field drops its up/down caret, which web's field does not have. Co-Authored-By: Claude Sonnet 5.5 --- .../EventForm/EventFormCompactDateField.swift | 39 +++++++++++------- .../EventForm/EventFormTimeField.swift | 1 - .../Sidebar/SidebarMonthPicker.swift | 25 +++++++++++- .../EventFormDatePickTests.swift | 40 +++++++++++++++++++ 4 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/EventFormDatePickTests.swift diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift index 5baef1dd78..c3da02e761 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormCompactDateField.swift @@ -8,6 +8,7 @@ struct EventFormCompactDateField: View { let accessibilityIdentifier: String @State private var text: String @State private var showsCalendar = false + @State private var pickerMonth = Date() @FocusState private var isFocused: Bool init(date: Binding, accessibilityIdentifier: String) { @@ -47,28 +48,38 @@ struct EventFormCompactDateField: View { 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] + // Web `react-datepicker`: the same compact month grid as the sidebar. + SidebarMonthPicker( + displayedMonth: $pickerMonth, + selectedDate: date, + onSelectDate: { picked in + date = Self.keepingTime(of: date, onDayOf: picked) + text = EventFormDateFormat.formatLabel(for: date) + showsCalendar = false + }, + today: Date(), + daysAreClickable: true ) - .datePickerStyle(.graphical) - .labelsHidden() + .frame(width: 252) .padding(12) .background(theme.surfacePanelColor) - .clipShape(RoundedRectangle(cornerRadius: NativeWebLayoutTokens.radiusDefault)) - .shadow(color: .black.opacity(0.08), radius: 4, y: 2) .accessibilityElement(children: .contain) .accessibilityIdentifier("compass-event-form-date-picker") + .onAppear { pickerMonth = date } } } + /// Picking a day changes the calendar day only; the time of day stays. + static func keepingTime(of original: Date, onDayOf picked: Date) -> Date { + let calendar = EffectiveTimeZone.calendar + var parts = calendar.dateComponents([.year, .month, .day], from: picked) + let time = calendar.dateComponents([.hour, .minute, .second], from: original) + parts.hour = time.hour + parts.minute = time.minute + parts.second = time.second + return calendar.date(from: parts) ?? picked + } + private func commitTypedDate() { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormTimeField.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormTimeField.swift index 8694ae4b3f..2fb19cd1ae 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormTimeField.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/EventForm/EventFormTimeField.swift @@ -25,7 +25,6 @@ struct EventFormTimeField: View { .font(.rubik(NativeWebLayoutTokens.fontSizeBody)) .foregroundStyle(theme.textColor) .lineLimit(1) - PhosphorIconView(icon: .caretUpDown, size: 12) } .padding(.horizontal, 8) .frame(minWidth: 72, idealWidth: 76, maxWidth: 88) diff --git a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift index 9d6e49f3c0..8675dced7a 100644 --- a/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift +++ b/apps/calendar-macos/CompassUI/Sources/CompassUI/Sidebar/SidebarMonthPicker.swift @@ -11,6 +11,9 @@ public struct SidebarMonthPicker: View { public let focusedDate: Date? /// The calendar's clock (`referenceNow`), so fixtures and pinned zones agree on today. public let today: Date + /// Web sidebar days are keyboard targets only; the form's date popover + /// reuses this picker with clickable days. + public let daysAreClickable: Bool private let trailingHeader: AnyView? public init( @@ -20,6 +23,7 @@ public struct SidebarMonthPicker: View { focusedDate: Date? = nil, visibleDayCount: Int = 1, today: Date = Date(), + daysAreClickable: Bool = false, trailingHeader: (() -> some View)? = nil ) { _displayedMonth = displayedMonth @@ -28,6 +32,7 @@ public struct SidebarMonthPicker: View { self.visibleDayCount = visibleDayCount self.focusedDate = focusedDate self.today = today + self.daysAreClickable = daysAreClickable if let trailingHeader { self.trailingHeader = AnyView(trailingHeader()) } else { @@ -35,6 +40,24 @@ public struct SidebarMonthPicker: View { } } + /// A picker with no trailing header content. + public init( + displayedMonth: Binding, + selectedDate: Date, + onSelectDate: @escaping (Date) -> Void, + today: Date = Date(), + daysAreClickable: Bool = false + ) { + _displayedMonth = displayedMonth + self.selectedDate = selectedDate + self.onSelectDate = onSelectDate + self.visibleDayCount = 1 + self.focusedDate = nil + self.today = today + self.daysAreClickable = daysAreClickable + self.trailingHeader = nil + } + public var body: some View { VStack(alignment: .leading, spacing: 8) { ViewThatFits(in: .horizontal) { @@ -153,7 +176,7 @@ public struct SidebarMonthPicker: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .allowsHitTesting(false) + .allowsHitTesting(daysAreClickable) .accessibilityLabel(dayAccessibilityLabel(day)) .accessibilityAddTraits(isInRange(day, from: selectedDate) ? .isSelected : []) } diff --git a/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/EventFormDatePickTests.swift b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/EventFormDatePickTests.swift new file mode 100644 index 0000000000..c5b3cc539d --- /dev/null +++ b/apps/calendar-macos/CompassUI/Tests/CompassUIUnitTests/EventFormDatePickTests.swift @@ -0,0 +1,40 @@ +import CompassKit +import XCTest +@testable import CompassUI + +final class EventFormDatePickTests: XCTestCase { + private var savedZone = "UTC" + + override func setUp() { + savedZone = EffectiveTimeZone.identifier + EffectiveTimeZone.identifier = "America/Denver" + } + + override func tearDown() { + EffectiveTimeZone.identifier = savedZone + } + + private func date(_ day: Int, hour: Int, minute: Int) -> Date { + var parts = DateComponents() + parts.year = 2026 + parts.month = 6 + parts.day = day + parts.hour = hour + parts.minute = minute + return EffectiveTimeZone.calendar.date(from: parts)! + } + + func testPickingADayKeepsTheTimeOfDay() { + let original = date(10, hour: 15, minute: 45) + let picked = date(22, hour: 0, minute: 0) + let result = EventFormCompactDateField.keepingTime(of: original, onDayOf: picked) + XCTAssertEqual(result, date(22, hour: 15, minute: 45)) + } + + func testPickingTheSameDayChangesNothing() { + let original = date(10, hour: 9, minute: 15) + XCTAssertEqual( + EventFormCompactDateField.keepingTime(of: original, onDayOf: date(10, hour: 0, minute: 0)), + original) + } +}