Skip to content
Open
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
Expand Up @@ -122,7 +122,8 @@ extension NativeCalendarRootModel {
schedule: draft.schedule,
edge: edge.scheduleEdge,
key: key,
step: step
step: step,
weekViewClamp: allDayEdgeWeekViewClamp()
) else {
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ extension NativeCalendarRootModel {
schedule: current,
edge: edge.scheduleEdge,
key: key,
step: step
step: step,
weekViewClamp: allDayEdgeWeekViewClamp()
) else {
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,12 @@ public final class NativeCalendarRootModel {
) ?? startOfView
}

/// Week-view window for all-day edge nudge clamping (web `dayBoundary.weekDays`).
func allDayEdgeWeekViewClamp() -> (visibleStart: Date, visibleEndInclusive: Date)? {
guard viewStore.view == .week else { return nil }
return (startOfView, endOfView)
}

func visibleDateKeys() -> [String] {
let calendar = EffectiveTimeZone.calendar
return (0 ..< viewStore.visibleDayCount).compactMap { offset in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,50 @@ final class FocusedEventShiftNudgeTests: XCTestCase {
XCTAssertEqual(draft.schedule.end, endBefore)
}

func testWeekViewRejectsAllDaySavedEventStartEdgeNudgeOffWeek() async throws {
let (model, environment, _) = try await makeWritableAllDayEvent()
let weekStart = try XCTUnwrap(CompassDateParsing.parseInEffectiveTimeZone("2025-12-28"))
let event = try eventOnAllDay(weekStart)
try environment.localEventRepository.put(.init(id: event.id, event: event, isDemo: false))
model.loadedEvents = [event]
model.viewStore.view = .week
model.viewStore.setAnchorDate(weekStart)
model.rebuildPresentation()
model.focusEvent(eventId: event.id.rawValue)
model.focusStore.setEdge(
eventId: event.id,
edge: .startDate,
announcement: "Editing start time",
isAllDay: true
)
XCTAssertFalse(model.nudgeDraftOrPlace(key: "ArrowLeft", shiftKey: true, altKey: false))
await assertFinishedNudgePersistence(model)
let stored = try XCTUnwrap(persistedEvent(environment, id: event.id))
XCTAssertEqual(stored.schedule, event.schedule)
}

func testWeekViewRejectsAllDayDraftStartEdgeNudgeOffWeek() async throws {
let (model, environment, _) = try await makeWritableAllDayEvent()
let weekStart = try XCTUnwrap(CompassDateParsing.parseInEffectiveTimeZone("2025-12-28"))
let event = try eventOnAllDay(weekStart)
try environment.localEventRepository.put(.init(id: event.id, event: event, isDemo: false))
model.loadedEvents = [event]
model.viewStore.view = .week
model.viewStore.setAnchorDate(weekStart)
model.rebuildPresentation()
model.focusEvent(eventId: event.id.rawValue)
model.openKeyboardEditForFocusedEvent()
model.focusStore.setEdge(
eventId: event.id,
edge: .startDate,
announcement: "Editing start time",
isAllDay: true
)
let scheduleBefore = try XCTUnwrap(model.draftStore.gridDraft?.schedule)
XCTAssertFalse(model.nudgeDraftOrPlace(key: "ArrowLeft", shiftKey: true, altKey: false))
XCTAssertEqual(model.draftStore.gridDraft?.schedule, scheduleBefore)
}

func testDraftEndEdgeNudgeOnAllDayDraft() async throws {
let (model, _, event) = try await makeWritableAllDayEvent()
model.openKeyboardEditForFocusedEvent()
Expand Down Expand Up @@ -240,6 +284,25 @@ final class FocusedEventShiftNudgeTests: XCTestCase {
try environment.localEventRepository.fetchAll().first { $0.id == id }?.event
}

private func eventOnAllDay(_ day: Date) throws -> Event {
let startKey = CompassDateParsing.formatCalendarDay(day)
let calendar = EffectiveTimeZone.calendar
let exclusiveEnd = try XCTUnwrap(calendar.date(byAdding: .day, value: 1, to: day))
let endKey = CompassDateParsing.formatCalendarDay(exclusiveEnd)
let json = """
{
"id": "week-clamp-allday",
"calendarId": "cal-1",
"content": { "kind": "details", "title": "Week edge", "description": "" },
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z",
"recurrence": { "kind": "single" },
"schedule": { "kind": "allDay", "start": "\(startKey)", "end": "\(endKey)" }
}
"""
return try ContractTestDecoding.decodeJSON(json, as: Event.self)
}

private func makeWritableTimedEvent(canWrite: Bool = true) async throws
-> (NativeCalendarRootModel, NativeCalendarEnvironment, Event)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,60 @@ public enum DraftNudge {
return applyWholeEventMovement(to: schedule, movement: movement, isStartAllowed: isStartAllowed)
}

/// Week view: refuse all-day edge moves that would leave the visible week
/// (web `wouldAllDayEdgeLeaveVisibleWeek`).
public static func wouldAllDayEdgeLeaveVisibleWeek(
schedule: DraftSchedule,
edge: ScheduleEdge,
daysDelta: Int,
visibleStart: Date,
visibleEndInclusive: Date
) -> Bool {
guard schedule.kind == .allDay, daysDelta != 0 else { return false }
let calendar = EffectiveTimeZone.calendar
let edgeDay: Date
switch edge {
case .start:
edgeDay = calendar.startOfDay(for: schedule.start)
case .end:
guard let inclusiveEnd = calendar.date(byAdding: .day, value: -1, to: schedule.end) else {
return false
}
edgeDay = calendar.startOfDay(for: inclusiveEnd)
}
guard let next = calendar.date(byAdding: .day, value: daysDelta, to: edgeDay) else {
return false
}
let first = calendar.startOfDay(for: visibleStart)
let last = calendar.startOfDay(for: visibleEndInclusive)
return next < first || next > last
}

/// Single-edge keyboard nudge; mirrors web `nudgeEventEdgeDates`.
public static func nudgeScheduleEdgeByKeyboard(
schedule: DraftSchedule,
edge: ScheduleEdge,
key: String,
step: EventNudgeStep = .fine
step: EventNudgeStep = .fine,
weekViewClamp: (visibleStart: Date, visibleEndInclusive: Date)? = nil
) -> ScheduleEdgeNudgeResult? {
let isAllDay = schedule.kind == .allDay
guard let movement = getArrowKeyMovement(key: key, isAllDay: isAllDay, step: step) else {
return nil
}
if isAllDay {
guard movement.days != 0 else { return nil }
if let weekViewClamp,
wouldAllDayEdgeLeaveVisibleWeek(
schedule: schedule,
edge: edge,
daysDelta: movement.days,
visibleStart: weekViewClamp.visibleStart,
visibleEndInclusive: weekViewClamp.visibleEndInclusive
)
{
return nil
}
return nudgeAllDayScheduleEdge(schedule: schedule, edge: edge, daysDelta: movement.days)
}
guard movement.minutes != 0 else { return nil }
Expand Down
84 changes: 84 additions & 0 deletions apps/calendar-macos/CompassKitTests/AllDayEdgeWeekClampTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import XCTest
@testable import CompassKit

final class AllDayEdgeWeekClampTests: XCTestCase {
func testWouldAllDayStartEdgeLeavingWeekBeforeVisibleStart() throws {
let visibleStart = try day(2026, 1, 5)
let visibleEnd = try day(2026, 1, 11)
let schedule = allDay(start: visibleStart, inclusiveEnd: visibleStart)
XCTAssertTrue(
DraftNudge.wouldAllDayEdgeLeaveVisibleWeek(
schedule: schedule,
edge: .start,
daysDelta: -1,
visibleStart: visibleStart,
visibleEndInclusive: visibleEnd
)
)
}

func testWouldAllDayEndEdgeLeavingWeekAfterVisibleEnd() throws {
let visibleStart = try day(2026, 1, 5)
let visibleEnd = try day(2026, 1, 11)
let calendar = EffectiveTimeZone.calendar
let exclusiveEnd = try XCTUnwrap(calendar.date(byAdding: .day, value: 1, to: visibleEnd))
let schedule = allDay(start: visibleStart, exclusiveEnd: exclusiveEnd)
XCTAssertTrue(
DraftNudge.wouldAllDayEdgeLeaveVisibleWeek(
schedule: schedule,
edge: .end,
daysDelta: 1,
visibleStart: visibleStart,
visibleEndInclusive: visibleEnd
)
)
}

func testEdgeNudgeRejectsMoveOutsideWeekWhenClampProvided() throws {
let visibleStart = try day(2026, 1, 5)
let visibleEnd = try day(2026, 1, 11)
let schedule = allDay(start: visibleStart, inclusiveEnd: visibleStart)
let clamp = (visibleStart: visibleStart, visibleEndInclusive: visibleEnd)
XCTAssertNil(
DraftNudge.nudgeScheduleEdgeByKeyboard(
schedule: schedule,
edge: .start,
key: "ArrowLeft",
weekViewClamp: clamp
)
)
}

func testEdgeNudgeAllowsMoveInsideWeekWithClamp() throws {
let visibleStart = try day(2026, 1, 5)
let visibleEnd = try day(2026, 1, 11)
let start = try day(2026, 1, 7)
let schedule = allDay(start: start, inclusiveEnd: start)
let clamp = (visibleStart: visibleStart, visibleEndInclusive: visibleEnd)
XCTAssertNotNil(
DraftNudge.nudgeScheduleEdgeByKeyboard(
schedule: schedule,
edge: .start,
key: "ArrowRight",
weekViewClamp: clamp
)
)
}

private func day(_ year: Int, _ month: Int, _ day: Int) throws -> Date {
try XCTUnwrap(
EffectiveTimeZone.calendar.date(from: DateComponents(year: year, month: month, day: day))
)
}

private func allDay(start: Date, inclusiveEnd: Date) -> DraftSchedule {
let calendar = EffectiveTimeZone.calendar
let exclusiveEnd =
calendar.date(byAdding: .day, value: 1, to: inclusiveEnd) ?? inclusiveEnd
return DraftSchedule(start: start, end: exclusiveEnd, kind: .allDay)
}

private func allDay(start: Date, exclusiveEnd: Date) -> DraftSchedule {
DraftSchedule(start: start, end: exclusiveEnd, kind: .allDay)
}
}
Loading