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) + } +}