Skip to content

Commit d794461

Browse files
authored
Move Someday events to interaction engine (#1788)
* feat(web): move someday events to interaction engine * feat(web): add someday sidebar interaction sorting * feat(web): polish someday grid drop animation * refactor(web): simplify someday interactions * refine somedays tests and remove weak month coverage
1 parent faa809d commit d794461

54 files changed

Lines changed: 3373 additions & 1339 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTEXT.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ An event shown in the all-day row.
7979
**Grid Event**:
8080
An event assigned to a concrete day/week calendar slot.
8181

82+
**Planner Sidebar**:
83+
The calendar side panel that holds navigation, account context, and Someday
84+
Events.
85+
_Avoid_: Planning sidebar
86+
8287
**Someday Event**:
8388
An unscheduled event stored in the sidebar instead of the calendar grid.
8489
_Avoid_: Someday task
@@ -193,6 +198,16 @@ during Import or Public watch notification handling.
193198
- A **Task** belongs to a **Date key** and stays local today, even when the user
194199
is authenticated.
195200
- A **Someday Event** is an **Event**, not a **Task**.
201+
- A **Someday Event** can move between **Planner Sidebar** sections without
202+
becoming a **Grid Event**.
203+
- During **Planner Sidebar** sorting, sibling **Someday Events** make room for
204+
the dragged event's preview position before the drop commits.
205+
- When a dragged **Someday Event** leaves the **Planner Sidebar** for a calendar
206+
surface, the sidebar stops previewing a sidebar sort.
207+
- A full **Planner Sidebar** section is not a valid drop target for another
208+
**Someday Event**.
209+
- A **Someday Event** is scheduled as a **Timed Event** or **All-Day Event**
210+
based on the calendar surface where it is dropped.
196211
- A **Recurring Series** has exactly one **Base Event** and zero or more
197212
**Instance Events**.
198213
- An **Instance Event** belongs to exactly one **Base Event** through

packages/web/src/common/calendar-interaction/CalendarInteractionEngine.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ describe("FloatingInteractionOverlay", () => {
412412

413413
expect(overlay.getNode()).toBe(clone);
414414
expect(clone.parentElement).toBe(document.body);
415+
expect(clone.style.position).toBe("fixed");
415416
expect(clone.style.transition).toBe("none");
416417
expect(clone.style.transform).toBe("translate3d(7px, 9px, 0)");
417418
expect(clone.style.height).toBe("24px");

packages/web/src/common/calendar-interaction/dom/overlay/FloatingInteractionOverlay.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class FloatingInteractionOverlay {
2626
clone.style.contain = "layout paint style";
2727
clone.style.height = `${rect.height}px`;
2828
clone.style.left = `${rect.left}px`;
29-
clone.style.position = "absolute";
29+
clone.style.position = "fixed";
3030
clone.style.pointerEvents = "none";
3131
clone.style.top = `${rect.top}px`;
3232
clone.style.cursor = cursor ?? "";
@@ -71,9 +71,8 @@ export class FloatingInteractionOverlay {
7171
this.#node.style.width = `${width}px`;
7272
}
7373

74-
this.#node.style.transition = "none";
75-
this.#node.style.transform = `translate3d(${transform.x}px, ${transform.y}px, 0)`;
7674
mutate?.(this.#node);
75+
this.#node.style.transform = `translate3d(${transform.x}px, ${transform.y}px, 0)`;
7776
}
7877

7978
getNode() {

packages/web/src/views/Week/interaction/WeekPointerCaptureBoundary.test.tsx renamed to packages/web/src/common/calendar-interaction/react/CalendarInteractionPointerCaptureBoundary.test.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import { fireEvent, render, screen } from "@testing-library/react";
22
import { type PointerEvent as ReactPointerEvent } from "react";
3-
import { type WeekInteractionAdapter } from "./adapter/WeekInteractionAdapter";
4-
import { WeekPointerCaptureBoundary } from "./WeekPointerCaptureBoundary";
3+
import {
4+
CalendarInteractionPointerCaptureBoundary,
5+
type CalendarPointerCaptureAdapter,
6+
} from "./CalendarInteractionPointerCaptureBoundary";
57
import { describe, expect, it, mock } from "bun:test";
68

7-
const createOwningWeekInteractionAdapter = (): WeekInteractionAdapter => {
9+
const createOwningAdapter = (): CalendarPointerCaptureAdapter => {
810
return {
9-
...createNonOwningWeekInteractionAdapter(),
11+
...createNonOwningAdapter(),
1012
handlePointerDown: () => ({
1113
reason: "test-owner",
1214
shouldOwn: true,
1315
}),
1416
};
1517
};
1618

17-
const createCancellationAwareWeekInteractionAdapter = () => {
19+
const createCancellationAwareAdapter = () => {
1820
const disconnectCancellationEvents = mock();
1921

2022
return {
21-
...createNonOwningWeekInteractionAdapter(),
23+
...createNonOwningAdapter(),
2224
cancel: mock(),
2325
connectCancellationEvents: mock(() => disconnectCancellationEvents),
2426
disconnectCancellationEvents,
2527
};
2628
};
2729

28-
const createNonOwningWeekInteractionAdapter = (): WeekInteractionAdapter => ({
30+
const createNonOwningAdapter = (): CalendarPointerCaptureAdapter => ({
2931
cancel: () => undefined,
3032
connectCancellationEvents: () => () => undefined,
3133
handlePointerCancel: () => false,
@@ -35,42 +37,40 @@ const createNonOwningWeekInteractionAdapter = (): WeekInteractionAdapter => ({
3537
}),
3638
handlePointerMove: () => false,
3739
handlePointerUp: () => false,
38-
ownsPointer: () => false,
39-
rebuildLayoutAfterNavigation: () => undefined,
4040
});
4141

42-
describe("WeekPointerCaptureBoundary", () => {
42+
describe("CalendarInteractionPointerCaptureBoundary", () => {
4343
it("does not block child pointer handlers when the adapter declines ownership", () => {
44-
const adapter = createNonOwningWeekInteractionAdapter();
44+
const adapter = createNonOwningAdapter();
4545
const onPointerDown = mock(
4646
(event: ReactPointerEvent<HTMLButtonElement>) => {
4747
expect(event.defaultPrevented).toBe(false);
4848
},
4949
);
5050

5151
render(
52-
<WeekPointerCaptureBoundary adapter={adapter}>
52+
<CalendarInteractionPointerCaptureBoundary adapter={adapter}>
5353
<button onPointerDown={onPointerDown} type="button">
5454
event
5555
</button>
56-
</WeekPointerCaptureBoundary>,
56+
</CalendarInteractionPointerCaptureBoundary>,
5757
);
5858

5959
fireEvent.pointerDown(screen.getByRole("button", { name: "event" }));
6060

6161
expect(onPointerDown).toHaveBeenCalledTimes(1);
6262
});
6363

64-
it("can stop propagation once a future adapter owns a pointerdown", () => {
65-
const adapter = createOwningWeekInteractionAdapter();
64+
it("can stop propagation once an adapter owns a pointerdown", () => {
65+
const adapter = createOwningAdapter();
6666
const onPointerDown = mock();
6767

6868
render(
69-
<WeekPointerCaptureBoundary adapter={adapter}>
69+
<CalendarInteractionPointerCaptureBoundary adapter={adapter}>
7070
<button onPointerDown={onPointerDown} type="button">
7171
event
7272
</button>
73-
</WeekPointerCaptureBoundary>,
73+
</CalendarInteractionPointerCaptureBoundary>,
7474
);
7575

7676
fireEvent.pointerDown(screen.getByRole("button", { name: "event" }));
@@ -79,8 +79,8 @@ describe("WeekPointerCaptureBoundary", () => {
7979
});
8080

8181
it("stops child pointer continuation handlers once the adapter consumes them", () => {
82-
const adapter: WeekInteractionAdapter = {
83-
...createNonOwningWeekInteractionAdapter(),
82+
const adapter: CalendarPointerCaptureAdapter = {
83+
...createNonOwningAdapter(),
8484
handlePointerCancel: mock(() => true),
8585
handlePointerMove: mock(() => true),
8686
handlePointerUp: mock(() => true),
@@ -90,7 +90,7 @@ describe("WeekPointerCaptureBoundary", () => {
9090
const onPointerUp = mock();
9191

9292
render(
93-
<WeekPointerCaptureBoundary adapter={adapter}>
93+
<CalendarInteractionPointerCaptureBoundary adapter={adapter}>
9494
<button
9595
onPointerCancel={onPointerCancel}
9696
onPointerMove={onPointerMove}
@@ -99,7 +99,7 @@ describe("WeekPointerCaptureBoundary", () => {
9999
>
100100
event
101101
</button>
102-
</WeekPointerCaptureBoundary>,
102+
</CalendarInteractionPointerCaptureBoundary>,
103103
);
104104

105105
const eventButton = screen.getByRole("button", { name: "event" });
@@ -117,12 +117,12 @@ describe("WeekPointerCaptureBoundary", () => {
117117
});
118118

119119
it("connects global cancellation events while mounted and disconnects them on unmount", () => {
120-
const adapter = createCancellationAwareWeekInteractionAdapter();
120+
const adapter = createCancellationAwareAdapter();
121121

122122
const { unmount } = render(
123-
<WeekPointerCaptureBoundary adapter={adapter}>
123+
<CalendarInteractionPointerCaptureBoundary adapter={adapter}>
124124
<button type="button">event</button>
125-
</WeekPointerCaptureBoundary>,
125+
</CalendarInteractionPointerCaptureBoundary>,
126126
);
127127

128128
expect(adapter.connectCancellationEvents).toHaveBeenCalledTimes(1);
@@ -134,12 +134,12 @@ describe("WeekPointerCaptureBoundary", () => {
134134
});
135135

136136
it("cancels any active interaction when unmounted", () => {
137-
const adapter = createCancellationAwareWeekInteractionAdapter();
137+
const adapter = createCancellationAwareAdapter();
138138

139139
const { unmount } = render(
140-
<WeekPointerCaptureBoundary adapter={adapter}>
140+
<CalendarInteractionPointerCaptureBoundary adapter={adapter}>
141141
<button type="button">event</button>
142-
</WeekPointerCaptureBoundary>,
142+
</CalendarInteractionPointerCaptureBoundary>,
143143
);
144144

145145
expect(adapter.cancel).not.toHaveBeenCalled();

packages/web/src/views/Week/interaction/WeekPointerCaptureBoundary.tsx renamed to packages/web/src/common/calendar-interaction/react/CalendarInteractionPointerCaptureBoundary.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import {
22
type FC,
3-
type PointerEvent as ReactPointerEvent,
43
type PropsWithChildren,
4+
type PointerEvent as ReactPointerEvent,
55
useEffect,
66
} from "react";
7-
import { type WeekInteractionAdapter } from "./adapter/WeekInteractionAdapter";
7+
import { type CalendarInteractionCancellationTargets } from "../CalendarInteractionEngine";
8+
9+
export interface CalendarPointerCaptureAdapter {
10+
cancel(): void;
11+
connectCancellationEvents(
12+
targets?: CalendarInteractionCancellationTargets,
13+
): () => void;
14+
handlePointerCancel(event: PointerEvent): boolean;
15+
handlePointerDown(event: PointerEvent): {
16+
reason: string;
17+
shouldOwn: boolean;
18+
};
19+
handlePointerMove(event: PointerEvent): boolean;
20+
handlePointerUp(event: PointerEvent): boolean;
21+
}
822

923
interface Props extends PropsWithChildren {
10-
adapter: WeekInteractionAdapter;
24+
adapter: CalendarPointerCaptureAdapter;
1125
}
1226

13-
export const WeekPointerCaptureBoundary: FC<Props> = ({
27+
export const CalendarInteractionPointerCaptureBoundary: FC<Props> = ({
1428
adapter,
1529
children,
1630
}) => {

packages/web/src/common/styles/theme.util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@ export const gradientByPriority = {
4848
)})`,
4949
};
5050

51+
const GRID_UNASSIGNED = c.blueGray400;
52+
const GRID_WORK = c.blueGray300;
53+
const GRID_RELATIONS = darken(c.teal, 5);
54+
const GRID_SELF = c.blueGray200;
55+
56+
export const gridColorByPriority = {
57+
[Priorities.UNASSIGNED]: GRID_UNASSIGNED,
58+
[Priorities.WORK]: GRID_WORK,
59+
[Priorities.RELATIONS]: GRID_RELATIONS,
60+
[Priorities.SELF]: GRID_SELF,
61+
};
62+
63+
export const gridHoverColorByPriority = {
64+
[Priorities.UNASSIGNED]: brighten(GRID_UNASSIGNED),
65+
[Priorities.WORK]: brighten(GRID_WORK),
66+
[Priorities.RELATIONS]: brighten(GRID_RELATIONS),
67+
[Priorities.SELF]: brighten(GRID_SELF),
68+
};
69+
5170
export const blueGradient = `linear-gradient(${c.blue100}, ${c.blue300})`;
5271
const grayGradient = `linear-gradient(90deg, ${c.gray100}, ${c.gray200})`;
5372

packages/web/src/components/DND/DropZone.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@ import { type HTMLAttributes, type Ref } from "react";
44
interface Props extends HTMLAttributes<HTMLDivElement> {
55
innerRef?: Ref<HTMLDivElement>;
66
isActive: boolean;
7+
isInvalid?: boolean;
78
}
89

910
export const DropZone = ({
1011
className,
1112
innerRef,
1213
isActive,
14+
isInvalid = false,
1315
...props
1416
}: Props) => {
1517
return (
1618
<div
1719
{...props}
20+
aria-invalid={isInvalid || undefined}
1821
className={classNames(
1922
"relative rounded-default border-2 border-dashed transition-[background-color,border-color] duration-200",
20-
isActive
21-
? "border-border-primary bg-bg-secondary"
22-
: "border-transparent bg-transparent",
23+
isInvalid
24+
? "border-status-error border-solid bg-status-error/10"
25+
: isActive
26+
? "border-border-primary bg-bg-secondary"
27+
: "border-transparent bg-transparent",
2328
className,
2429
)}
2530
ref={innerRef}

packages/web/src/components/PlannerSidebar/PlannerSidebar.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { type HTMLAttributes } from "react";
22
import { type Dayjs } from "@core/util/date/dayjs";
33
import { ID_SIDEBAR } from "@web/common/constants/web.constants";
44
import { type ShortcutOverlaySection } from "@web/components/Shortcuts/ShortcutOverlay/ShortcutsOverlay";
5-
import { type DateCalcs } from "@web/views/Week/hooks/grid/useDateCalcs";
6-
import {
7-
type Measurements_Grid,
8-
type Refs_Grid,
9-
} from "@web/views/Week/hooks/grid/useGridLayout";
105
import { PlannerAccountSummary } from "./PlannerAccountSummary/PlannerAccountSummary";
116
import { PlannerMonthPicker } from "./PlannerMonthPicker/PlannerMonthPicker";
127
import { PlannerSidebarActions } from "./PlannerSidebarActions/PlannerSidebarActions";
@@ -15,9 +10,6 @@ import { SomedayEventSections } from "./SomedayEventSections/SomedayEventSection
1510

1611
interface Props extends HTMLAttributes<HTMLDivElement> {
1712
calendarDate: Dayjs;
18-
dateCalcs?: DateCalcs;
19-
gridRefs?: Refs_Grid;
20-
measurements?: Measurements_Grid;
2113
monthsShown?: number;
2214
isShortcutsOpen: boolean;
2315
onCloseShortcuts: () => void;
@@ -32,9 +24,6 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
3224

3325
export function PlannerSidebar({
3426
calendarDate,
35-
dateCalcs,
36-
gridRefs,
37-
measurements,
3827
monthsShown = 1,
3928
isShortcutsOpen,
4029
onCloseShortcuts,
@@ -68,9 +57,6 @@ export function PlannerSidebar({
6857
<section aria-label="Someday events">
6958
<SomedayEventSections
7059
calendarDate={calendarDate}
71-
dateCalcs={dateCalcs}
72-
gridRefs={gridRefs}
73-
measurements={measurements}
7460
viewEnd={viewEnd}
7561
viewStart={viewStart}
7662
/>

0 commit comments

Comments
 (0)