Skip to content

Commit 87991e0

Browse files
committed
feat(web): polish someday grid drop animation
1 parent cc57de1 commit 87991e0

21 files changed

Lines changed: 798 additions & 90 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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/common/styles/theme.util.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ 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+
70+
export const gridDraftColorByPriority = {
71+
[Priorities.UNASSIGNED]: darken(GRID_UNASSIGNED, 18),
72+
[Priorities.WORK]: darken(GRID_WORK, 18),
73+
[Priorities.RELATIONS]: darken(GRID_RELATIONS, 18),
74+
[Priorities.SELF]: darken(GRID_SELF, 18),
75+
};
76+
5177
export const blueGradient = `linear-gradient(${c.blue100}, ${c.blue300})`;
5278
const grayGradient = `linear-gradient(90deg, ${c.gray100}, ${c.gray200})`;
5379

packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const SomedayEventRectangle = ({
5454
<DotsSixVertical
5555
aria-hidden="true"
5656
className="shrink-0 text-text-light"
57+
data-someday-drag-affordance="true"
5758
size={14}
5859
weight="bold"
5960
/>
@@ -63,7 +64,10 @@ export const SomedayEventRectangle = ({
6364
</div>
6465

6566
{canMigrate ? (
66-
<div className={ACTIONS_CLASS_NAME}>
67+
<div
68+
className={ACTIONS_CLASS_NAME}
69+
data-someday-drag-affordance="true"
70+
>
6771
<button
6872
aria-label={`Migrate to previous ${target}`}
6973
className={ACTION_BUTTON_CLASS_NAME}
@@ -90,7 +94,10 @@ export const SomedayEventRectangle = ({
9094
</button>
9195
</div>
9296
) : (
93-
<div className={ACTIONS_CLASS_NAME}>
97+
<div
98+
className={ACTIONS_CLASS_NAME}
99+
data-someday-drag-affordance="true"
100+
>
94101
<button
95102
aria-label="Recurring events cannot be migrated"
96103
className={ACTION_BUTTON_CLASS_NAME}

packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventsContainer/SomedayEventsContainer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const SomedayEventsContainer: FC<Props> = ({
9898
return (
9999
<TooltipWrapper
100100
description={addLabel}
101+
placement="right"
101102
shortcut={
102103
category === Categories_Event.SOMEDAY_MONTH ? "Shift+M" : "Shift+W"
103104
}

packages/web/src/components/PlannerSidebar/SomedayEventSections/interaction/SomedayInteractionCoordinator.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createSomedayInteractionAdapter,
1414
type SomedayInteractionRuntime,
1515
} from "./adapter/SomedayInteractionAdapter";
16+
import { markSomedayCommitAcknowledgement } from "./state/somedayCommitAcknowledgementState";
1617

1718
interface Props extends PropsWithChildren {
1819
getLayoutSources?: () => WeekLayoutCacheSources;
@@ -66,7 +67,15 @@ export const SomedayInteractionCoordinator: FC<Props> = ({
6667
isSidebarDropAllowed: actions.isSomedaySidebarDropAllowed,
6768
onCancelInteraction: actions.cancelSomedayInteraction,
6869
onClickSomedayEvent: actions.onDraft,
69-
onCommitSomedayInteraction: actions.commitSomedayInteraction,
70+
onCommitSomedayInteraction: (result) => {
71+
// Mark before dispatching so the freshly rendered GridEvent /
72+
// AllDayEvent picks up the acknowledgment on its first paint.
73+
if (result.type === "schedule") {
74+
markSomedayCommitAcknowledgement(result.eventId);
75+
}
76+
77+
actions.commitSomedayInteraction(result);
78+
},
7079
onMotionActivation: (target) => {
7180
actions.startSomedayInteraction(target.event._id);
7281
},

packages/web/src/components/PlannerSidebar/SomedayEventSections/interaction/adapter/SomedayInteractionAdapter.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { Priorities } from "@core/constants/core.constants";
12
import { Categories_Event, type Schema_Event } from "@core/types/event.types";
23
import dayjs from "@core/util/date/dayjs";
34
import {
45
ID_ALLDAY_COLUMNS,
56
ID_GRID_COLUMNS_TIMED,
67
ID_GRID_MAIN,
78
} from "@web/common/constants/web.constants";
9+
import { theme } from "@web/common/styles/theme";
10+
import { gridHoverColorByPriority } from "@web/common/styles/theme.util";
811
import { createSomedayInteractionAdapter } from "@web/components/PlannerSidebar/SomedayEventSections/interaction/adapter/SomedayInteractionAdapter";
912
import { somedayDropTargetRegistry } from "@web/components/PlannerSidebar/SomedayEventSections/interaction/registry/somedayDropTargetRegistry";
1013
import { somedayEventRegistry } from "@web/components/PlannerSidebar/SomedayEventSections/interaction/registry/somedayEventRegistry";
@@ -70,6 +73,34 @@ const makePointerEvent = (
7073
return event;
7174
};
7275

76+
const normalizeCssColor = (color: string) => {
77+
const element = document.createElement("span");
78+
79+
element.style.color = color;
80+
81+
return element.style.color;
82+
};
83+
84+
const setReducedMotionPreference = (matches: boolean) => {
85+
const originalMatchMedia = window.matchMedia;
86+
const reducedMotionMatchMedia = mock((query: string) => ({
87+
addEventListener: mock(),
88+
addListener: mock(),
89+
dispatchEvent: mock(),
90+
matches: query === "(prefers-reduced-motion: reduce)" ? matches : false,
91+
media: query,
92+
onchange: null,
93+
removeEventListener: mock(),
94+
removeListener: mock(),
95+
}));
96+
97+
window.matchMedia = reducedMotionMatchMedia as typeof window.matchMedia;
98+
99+
return () => {
100+
window.matchMedia = originalMatchMedia;
101+
};
102+
};
103+
73104
const createHarness = () => {
74105
document.body.innerHTML = "";
75106
somedayDropTargetRegistry.clear();
@@ -302,6 +333,87 @@ describe("SomedayInteractionAdapter", () => {
302333
});
303334
});
304335

336+
it("transitions dragged Someday overlay text to dark while over the calendar", () => {
337+
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness();
338+
339+
adapter.handlePointerDown(
340+
makePointerEvent("pointerdown", { target: sourceChild, x: 20, y: 12 }),
341+
);
342+
adapter.handlePointerMove(
343+
makePointerEvent("pointermove", {
344+
target: timedColumns,
345+
x: 250,
346+
y: 220,
347+
}),
348+
);
349+
flushFrame();
350+
351+
const overlay = document.body.querySelector<HTMLElement>(
352+
"[data-calendar-interaction-overlay]",
353+
);
354+
const expectedTextColor = normalizeCssColor(theme.color.text.dark);
355+
356+
expect(overlay).toBeTruthy();
357+
expect(overlay?.style.transition).toContain("color 240ms");
358+
expect(overlay?.style.color).toBe(expectedTextColor);
359+
expect(overlay?.querySelector("[data-someday-drag-affordance]")).toBeNull();
360+
});
361+
362+
it("uses the hovered grid color for the dragged Someday overlay over the calendar", () => {
363+
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness();
364+
365+
adapter.handlePointerDown(
366+
makePointerEvent("pointerdown", { target: sourceChild, x: 20, y: 12 }),
367+
);
368+
adapter.handlePointerMove(
369+
makePointerEvent("pointermove", {
370+
target: timedColumns,
371+
x: 250,
372+
y: 220,
373+
}),
374+
);
375+
flushFrame();
376+
377+
const overlay = document.body.querySelector<HTMLElement>(
378+
"[data-calendar-interaction-overlay]",
379+
);
380+
const expectedHoverColor = normalizeCssColor(
381+
gridHoverColorByPriority[Priorities.UNASSIGNED],
382+
);
383+
384+
expect(overlay).toBeTruthy();
385+
expect(overlay?.style.backgroundColor).toBe(expectedHoverColor);
386+
});
387+
388+
it("disables Someday overlay motion when reduced motion is preferred", () => {
389+
const restoreMatchMedia = setReducedMotionPreference(true);
390+
const { adapter, flushFrame, sourceChild, timedColumns } = createHarness();
391+
392+
try {
393+
adapter.handlePointerDown(
394+
makePointerEvent("pointerdown", { target: sourceChild, x: 20, y: 12 }),
395+
);
396+
adapter.handlePointerMove(
397+
makePointerEvent("pointermove", {
398+
target: timedColumns,
399+
x: 250,
400+
y: 220,
401+
}),
402+
);
403+
flushFrame();
404+
405+
const overlay = document.body.querySelector<HTMLElement>(
406+
"[data-calendar-interaction-overlay]",
407+
);
408+
409+
expect(overlay).toBeTruthy();
410+
expect(overlay?.style.transition).toBe("none");
411+
expect(overlay?.style.scale).toBe("1");
412+
} finally {
413+
restoreMatchMedia();
414+
}
415+
});
416+
305417
it("schedules a dragged Someday event as an all-day event", () => {
306418
const { adapter, flushFrame, onCommitSomedayInteraction, sourceChild } =
307419
createHarness();

0 commit comments

Comments
 (0)