Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 14 additions & 0 deletions apps/calendar-macos/CompassKitTests/EventFormDateFormatTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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<Date>, 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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
Expand Down
Loading