Skip to content

Commit 199fc20

Browse files
✨ Feat: Custom actions context menu (#247)
* feature(web): Context menu UI implementation UI implementation of context menu, no business logic yet. WIP. * chore(web): Implement `theme.shape.borderRadius` to have border radius styling be consistent in UI Purpose is consistency. any inconsistent border radius, even by small pixel differences, will look "off" to the user. * chore(web): refactor component to use `styled-components` Purpose is to remain consistent across codebase * feature(web): Detect event right click from `GridContextMenuWrapper` POC Detecting whether an event was right clicked or not in a clean manner solves a big hurdle. * chore(web): Fix TS type Not sure why it was of type button in the first place, but this fixes it. * feature(web): Context menu edit event POC This commit demonstrates a POC for editing an event and inner functionality of how the context menu will interact with an event in its draft state. * feature(web): add invisible backdrop when the context menu is open the purpose is to prevent an additional action from occurring when user tries to close the context menu by clicking outside. Mimicking user expecations. * feature(web): Migrate vanilla context menu implementation to Floating UI Floating UI is a well tested library that handles a lot of nuances and edge cases with floating elements positioning * feature(web): Context menu enhancements - Replace `event` local state with draft event redux state as the source of truth for the selected event - Handle setting and clearing the right-clicked event as the draft event - rename `event` to `gridEvent` to avoid confusion with DOM events - TS typing and error fixing * feature(web): Prevent draft event window form from displaying when event is right clicked This is a crucial line. It allows us to handle right clicks separately (EG: Displaying a context menu) * feature(web): Context menu -> edit priority, refactor code - Implement context menu edit priority - Decouple `ContextMenu` and create `ContextMenuItems` to separate floating ui menu logic from menu items business logic * feature(web): Context menu -> Delete event Implement delete event * feature(web): support context menu for all-day events * feature(web): main grid area UX enhancement If user right clicks on an empty area in the main grid, we prevent drafting an event. This change should be a better match to user expectations * chore(web): extract `getSomedayEvents` into a reusable consistent function * chore(web): Rename `gridEvent` prop to `calEvent` this will ease the transition to supporting someday events later. A 'grid' event is an event that is usually associated with the main grid and not someday events. * chore(web): remove todo comments * chore(web): remove invisible backdrop component * bug(web): fix TS type import path * chore(web): Replace priority colors magic strings with constant ones * chore/feature(web): Implement `IconButton`, refactor code. - Implement `IconButton`, a reusable component for displaying an icon button - Remove `Delete` 'icon'. An `icon` component should not mix concerns with a `button` component. - Refactor instances where we use `Delete` icon * feature(web): Implement icons for menu context items * bug(web): Prevent context menu from opening for optimistic events Prevents optimistic events from being edited, which is to be expected. * feat(web): Add menu background color to theme Extend the theme with a new menu background color and update the context menu styling to use theme-based colors * refactor(web): Centralize event element ID constant Move the `DATA_EVENT_ELEMENT_ID` constant to the web constants file and update imports across components to use the centralized constant * refactor(web): Inline getSomedayEvents utility function Move the getSomedayEvents utility function directly into the SomedayEvents component, removing the separate utility file. (I'm in the process of reorganizing the utils dir, so this will save me from having a conflict) * refactor(web): Relocate IconButton component file Move IconButton component to a more specific file location and update import paths. We're moving away from index.ts files for the sake of more declarative imports. Why? Mostly because TK said so TBH. https://tkdodo.eu/blog/please-stop-using-barrel-files * refactor(web): Remove source property from draft event slice I think we'll need some way to start tracking these sources, but it's not needed for this use-case. So, I'd rather wait until it actually is required so that we can implement it with a real use-case * refactor(web): Rename calEvent prop to event in context menu components * refactor(web): Extract styled components for context menu items using named import for consistency, explicitness, and better tree-shaking * refactor(web): Improve error handling and variable naming in GridContextMenuWrapper * fix(web): Update draft slice discard action call in GridContextMenuWrapper Pass empty object to discard action to match expected signature --------- Co-authored-by: Tyler Dane <tyler@switchback.tech>
1 parent 39f95d9 commit 199fc20

File tree

28 files changed

+595
-122
lines changed

28 files changed

+595
-122
lines changed

packages/web/src/common/constants/web.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const ID_SOMEDAY_DRAFT = "somedayDraft";
1717
export const ID_SOMEDAY_EVENTS = "ID_SOMEDAY_EVENTS";
1818
export const ID_SOMEDAY_EVENT_FORM = "Someday Event Form";
1919
export const ID_OPTIMISTIC_PREFIX = "optimistic";
20+
export const DATA_EVENT_ELEMENT_ID = "data-event-id";
2021

2122
export const OPTIONS_RECURRENCE = {
2223
WEEK: { value: Recurrence_Selection.WEEK, label: "week" },

packages/web/src/common/styles/default-theme.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ declare module "styled-components" {
1919
gridLine: {
2020
primary: string;
2121
};
22+
menu: {
23+
bg: string;
24+
};
2225
panel: {
2326
bg: string;
2427
scrollbar: string;
@@ -72,5 +75,8 @@ declare module "styled-components" {
7275
transition: {
7376
default: string;
7477
};
78+
shape: {
79+
borderRadius: string;
80+
};
7581
}
7682
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export const theme: DefaultTheme = {
5050
gridLine: {
5151
primary: c.gray800,
5252
},
53+
menu: {
54+
bg: c.white200,
55+
},
5356
panel: {
5457
bg: c.gray600,
5558
scrollbar: c.gray500,
@@ -103,4 +106,7 @@ export const theme: DefaultTheme = {
103106
transition: {
104107
default: "0.3s",
105108
},
109+
shape: {
110+
borderRadius: "4px",
111+
},
106112
};

packages/web/src/common/utils/event.util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import {
2020
COLUMN_MONTH,
2121
COLUMN_WEEK,
22+
DATA_EVENT_ELEMENT_ID,
2223
ID_OPTIMISTIC_PREFIX,
2324
} from "../constants/web.constants";
2425

@@ -284,3 +285,8 @@ const _assembleBaseEvent = (
284285

285286
return baseEvent;
286287
};
288+
289+
export const getCalendarEventIdFromElement = (element: HTMLElement) => {
290+
const eventElement = element.closest(`[${DATA_EVENT_ELEMENT_ID}]`);
291+
return eventElement ? eventElement.getAttribute(DATA_EVENT_ELEMENT_ID) : null;
292+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from "react";
2+
import { IconButtonProps, StyledIconButton } from "./styled";
3+
4+
const IconButton: React.FC<IconButtonProps> = ({
5+
size = "medium",
6+
children: icon,
7+
...props
8+
}) => {
9+
return (
10+
<StyledIconButton size={size} {...props}>
11+
{icon}
12+
</StyledIconButton>
13+
);
14+
};
15+
16+
export default IconButton;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import styled from "styled-components";
2+
3+
export type IconButtonSize = "small" | "medium" | "large";
4+
5+
const sizeMap: Record<IconButtonSize, number> = {
6+
small: 20,
7+
medium: 27,
8+
large: 34,
9+
};
10+
11+
export interface IconButtonProps
12+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
13+
size?: IconButtonSize;
14+
}
15+
16+
const buttonStyleReset = `
17+
background: none;
18+
color: inherit;
19+
border: none;
20+
padding: 0;
21+
font: inherit;
22+
cursor: pointer;
23+
outline: inherit;
24+
`;
25+
26+
export const StyledIconButton = styled.button<IconButtonProps>`
27+
${buttonStyleReset}
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
transition: background-color 0.3s ease, transform 0.2s ease;
32+
font-size: ${({ size = "medium" }) => sizeMap[size]}px;
33+
34+
&:hover {
35+
transform: scale(1.05);
36+
}
37+
38+
&:active {
39+
transform: scale(0.95);
40+
}
41+
42+
&:disabled {
43+
opacity: 0.6;
44+
cursor: not-allowed;
45+
}
46+
`;

packages/web/src/components/Icons/Delete.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.

packages/web/src/components/Tooltip/styled.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { Flex } from "@web/components/Flex";
44
export const StyledShortcutTip = styled(Flex)`
55
background-color: ${({ theme }) => theme.color.fg.primary};
66
border: 1px solid ${({ theme }) => theme.color.bg.primary};
7-
border-radius: 3px;
7+
border-radius: ${({ theme }) => theme.shape.borderRadius};
88
padding: 5px 10px;
99
`;

packages/web/src/views/Calendar/Calendar.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ID_MAIN } from "@web/common/constants/web.constants";
44
import { useAppSelector } from "@web/store/store.hooks";
55
import { selectIsSidebarOpen } from "@web/ducks/events/selectors/view.selectors";
66

7+
import GridContextMenuWrapper from "./components/Grid/ContextMenu/GridContextMenuWrapper";
78
import { Grid } from "./components/Grid/";
89
import { useScroll } from "./hooks/grid/useScroll";
910
import { useToday } from "./hooks/useToday";
@@ -84,14 +85,16 @@ export const CalendarView = () => {
8485
weekProps={weekProps}
8586
/>
8687

87-
<Grid
88-
dateCalcs={dateCalcs}
89-
isSidebarOpen={isSidebarOpen}
90-
gridRefs={gridRefs}
91-
measurements={measurements}
92-
today={today}
93-
weekProps={weekProps}
94-
/>
88+
<GridContextMenuWrapper weekProps={weekProps}>
89+
<Grid
90+
dateCalcs={dateCalcs}
91+
isSidebarOpen={isSidebarOpen}
92+
gridRefs={gridRefs}
93+
measurements={measurements}
94+
today={today}
95+
weekProps={weekProps}
96+
/>
97+
</GridContextMenuWrapper>
9598
</StyledCalendar>
9699
</Styled>
97100
);

packages/web/src/views/Calendar/components/Event/Grid/GridEvent/GridEvent.tsx

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
} from "@web/components/Flex/styled";
2020
import { getLineClamp } from "@web/common/utils/grid.util";
2121
import { getTimesLabel } from "@web/common/utils/web.date.util";
22-
import { ZIndex } from "@web/common/constants/web.constants";
22+
import {
23+
ZIndex,
24+
DATA_EVENT_ELEMENT_ID,
25+
} from "@web/common/constants/web.constants";
2326
import { Text } from "@web/components/Text";
2427

2528
import { StyledEvent, StyledEventScaler, StyledEventTitle } from "../../styled";
@@ -53,7 +56,7 @@ const _GridEvent = (
5356
onScalerMouseDown,
5457
weekProps,
5558
}: Props,
56-
ref: ForwardedRef<HTMLButtonElement>
59+
ref: ForwardedRef<HTMLDivElement>
5760
) => {
5861
const { component } = weekProps;
5962

@@ -74,30 +77,39 @@ const _GridEvent = (
7477
[position.height]
7578
);
7679

77-
return (
78-
<StyledEvent
79-
allDay={event.isAllDay || false}
80-
className={isDraft ? "active" : undefined}
81-
height={position.height || 0}
82-
isDragging={isDragging}
83-
isInPast={isInPast}
84-
isPlaceholder={isPlaceholder}
85-
isOptimistic={isOptimistic}
86-
isResizing={isResizing}
87-
left={position.left}
88-
lineClamp={lineClamp}
89-
onMouseDown={(e) => {
90-
if (isOptimistic) return;
80+
const styledEventProps = {
81+
[DATA_EVENT_ELEMENT_ID]: event._id,
82+
allDay: event.isAllDay || false,
83+
className: isDraft ? "active" : undefined,
84+
height: position.height || 0,
85+
isDragging,
86+
isInPast,
87+
isPlaceholder,
88+
isOptimistic,
89+
isResizing,
90+
left: position.left,
91+
lineClamp,
92+
onMouseDown: (e: MouseEvent) => {
93+
const isRightBtnClick = e.button === 2;
94+
95+
if (
96+
isOptimistic || // Event is in the process of being created, don't allow any interactions until it's completely saved
97+
isRightBtnClick
98+
)
99+
return;
91100

92-
onEventMouseDown(event, e);
93-
}}
94-
priority={event.priority || Priorities.UNASSIGNED}
95-
ref={ref}
96-
role="button"
97-
tabindex="0"
98-
top={position.top}
99-
width={position.width || 0}
100-
>
101+
onEventMouseDown(event, e);
102+
},
103+
priority: event.priority || Priorities.UNASSIGNED,
104+
ref,
105+
role: "button",
106+
tabindex: "0",
107+
top: position.top,
108+
width: position.width || 0,
109+
};
110+
111+
return (
112+
<StyledEvent {...styledEventProps}>
101113
<Flex
102114
alignItems={AlignItems.FLEX_START}
103115
direction={FlexDirections.COLUMN}

0 commit comments

Comments
 (0)