|
1 | | -import { |
2 | | - FloatingFocusManager, |
3 | | - FloatingOverlay, |
4 | | - FloatingPortal, |
5 | | - useClick, |
6 | | - useDismiss, |
7 | | - useFloating, |
8 | | - useInteractions, |
9 | | - useRole, |
10 | | -} from "@floating-ui/react"; |
11 | | -import { useCallback, useMemo, useState } from "react"; |
12 | | -import { Priorities } from "@core/constants/core.constants"; |
| 1 | +import { useCallback, useState } from "react"; |
13 | 2 | import { RecurringEventUpdateScope } from "@core/types/event.types"; |
14 | 3 | import { DirtyParser } from "@web/common/parsers/dirty.parser"; |
15 | | -import { theme } from "@web/common/styles/theme"; |
| 4 | +import { type Schema_GridEvent } from "@web/common/types/web.event.types"; |
| 5 | +import { |
| 6 | + OverlayPanel, |
| 7 | + OverlayPanelActionButton, |
| 8 | + OverlayPanelActions, |
| 9 | +} from "@web/components/OverlayPanel/OverlayPanel"; |
16 | 10 | import { selectDraft } from "@web/ducks/events/selectors/draft.selectors"; |
17 | 11 | import { useAppSelector } from "@web/store/store.hooks"; |
18 | | -import { SaveSection } from "@web/views/Forms/EventForm/SaveSection/SaveSection"; |
19 | | -import { StyledEventForm } from "@web/views/Forms/EventForm/styled"; |
20 | 12 | import { useDraftContext } from "@web/views/Week/components/Draft/context/useDraftContext"; |
21 | 13 |
|
22 | | -const UPDATE_SCOPE_OPTIONS: Array<[string, RecurringEventUpdateScope]> = [ |
23 | | - ["this", RecurringEventUpdateScope.THIS_EVENT], |
24 | | - ["this-and-following", RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS], |
25 | | - ["all", RecurringEventUpdateScope.ALL_EVENTS], |
| 14 | +const UPDATE_SCOPE_OPTIONS: RecurringEventUpdateScope[] = [ |
| 15 | + RecurringEventUpdateScope.THIS_EVENT, |
| 16 | + RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, |
| 17 | + RecurringEventUpdateScope.ALL_EVENTS, |
| 18 | +]; |
| 19 | + |
| 20 | +const RECURRENCE_CHANGED_UPDATE_SCOPE_OPTIONS: RecurringEventUpdateScope[] = [ |
| 21 | + RecurringEventUpdateScope.THIS_AND_FOLLOWING_EVENTS, |
| 22 | + RecurringEventUpdateScope.ALL_EVENTS, |
26 | 23 | ]; |
27 | 24 |
|
| 25 | +const updateScopeOptionClassName = |
| 26 | + "flex min-h-11 cursor-pointer items-center gap-3 rounded px-3 text-base text-text-lighter transition-colors hover:bg-panel-badge-bg"; |
| 27 | + |
| 28 | +const selectedUpdateScopeOptionClassName = "bg-panel-badge-bg"; |
| 29 | + |
| 30 | +const radioDotClassName = |
| 31 | + "relative flex size-[18px] flex-none rounded-full border-2 border-border-secondary transition-colors after:absolute after:inset-0 after:m-auto after:size-2 after:scale-0 after:rounded-full after:bg-accent-primary after:transition-transform peer-checked:border-accent-primary peer-checked:after:scale-100 peer-focus-visible:ring-2 peer-focus-visible:ring-accent-primary peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-panel-bg"; |
| 32 | + |
28 | 33 | export function RecurringEventUpdateScopeDialog() { |
29 | 34 | const { |
30 | 35 | confirmation, |
31 | 36 | state: { draft }, |
32 | 37 | } = useDraftContext(); |
33 | | - const { isRecurrenceUpdateScopeDialogOpen } = confirmation; |
34 | | - const { setRecurrenceUpdateScopeDialogOpen } = confirmation; |
35 | | - const { onUpdateScopeChange } = confirmation; |
36 | | - const reduxDraft = useAppSelector(selectDraft); |
37 | | - const { UNASSIGNED } = Priorities; |
38 | | - const priority = draft?.priority ?? reduxDraft?.priority ?? UNASSIGNED; |
| 38 | + const { |
| 39 | + isRecurrenceUpdateScopeDialogOpen, |
| 40 | + setRecurrenceUpdateScopeDialogOpen, |
| 41 | + onUpdateScopeChange, |
| 42 | + } = confirmation; |
| 43 | + if (!isRecurrenceUpdateScopeDialogOpen) return null; |
| 44 | + |
| 45 | + return ( |
| 46 | + <RecurringEventUpdateScopeDialogContent |
| 47 | + draft={draft} |
| 48 | + onUpdateScopeChange={onUpdateScopeChange} |
| 49 | + setRecurrenceUpdateScopeDialogOpen={setRecurrenceUpdateScopeDialogOpen} |
| 50 | + /> |
| 51 | + ); |
| 52 | +} |
| 53 | + |
| 54 | +interface RecurringEventUpdateScopeDialogContentProps { |
| 55 | + draft: Schema_GridEvent | null; |
| 56 | + onUpdateScopeChange: (applyTo: RecurringEventUpdateScope) => void; |
| 57 | + setRecurrenceUpdateScopeDialogOpen: (isOpen: boolean) => void; |
| 58 | +} |
39 | 59 |
|
| 60 | +function RecurringEventUpdateScopeDialogContent({ |
| 61 | + draft, |
| 62 | + onUpdateScopeChange, |
| 63 | + setRecurrenceUpdateScopeDialogOpen, |
| 64 | +}: RecurringEventUpdateScopeDialogContentProps) { |
| 65 | + const reduxDraft = useAppSelector(selectDraft); |
40 | 66 | const currentDraft = draft ?? reduxDraft; |
41 | 67 | const recurrenceChanged = |
42 | 68 | currentDraft && reduxDraft |
43 | 69 | ? DirtyParser.recurrenceChanged(currentDraft, reduxDraft) |
44 | 70 | : false; |
| 71 | + const options = recurrenceChanged |
| 72 | + ? RECURRENCE_CHANGED_UPDATE_SCOPE_OPTIONS |
| 73 | + : UPDATE_SCOPE_OPTIONS; |
| 74 | + const [fallbackScope] = options; |
45 | 75 |
|
46 | | - const { context, refs, floatingStyles } = useFloating({ |
47 | | - open: isRecurrenceUpdateScopeDialogOpen, |
48 | | - onOpenChange: setRecurrenceUpdateScopeDialogOpen, |
49 | | - strategy: "absolute", |
50 | | - placement: "bottom", |
51 | | - transform: true, |
52 | | - }); |
53 | | - |
54 | | - const [value, setValue] = useState<RecurringEventUpdateScope>( |
55 | | - RecurringEventUpdateScope.THIS_EVENT, |
56 | | - ); |
57 | | - |
58 | | - const click = useClick(context); |
59 | | - const role = useRole(context); |
60 | | - const dismiss = useDismiss(context, { outsidePressEvent: "mousedown" }); |
61 | | - const interactions = useInteractions([click, role, dismiss]); |
| 76 | + const [selectedScope, setSelectedScope] = |
| 77 | + useState<RecurringEventUpdateScope>(fallbackScope); |
| 78 | + const activeScope = options.includes(selectedScope) |
| 79 | + ? selectedScope |
| 80 | + : fallbackScope; |
62 | 81 |
|
63 | | - const options = useMemo<Array<[string, RecurringEventUpdateScope]>>(() => { |
64 | | - if (!recurrenceChanged) return UPDATE_SCOPE_OPTIONS; |
65 | | - |
66 | | - return UPDATE_SCOPE_OPTIONS.slice(1); |
67 | | - }, [recurrenceChanged]); |
| 82 | + const closeDialog = useCallback(() => { |
| 83 | + setRecurrenceUpdateScopeDialogOpen(false); |
| 84 | + }, [setRecurrenceUpdateScopeDialogOpen]); |
68 | 85 |
|
69 | 86 | const onSubmitHandler = useCallback(() => { |
70 | | - onUpdateScopeChange(value); |
71 | | - setValue(RecurringEventUpdateScope.THIS_EVENT); |
72 | | - }, [value, onUpdateScopeChange]); |
73 | | - |
74 | | - if (!isRecurrenceUpdateScopeDialogOpen) return null; |
| 87 | + onUpdateScopeChange(activeScope); |
| 88 | + setSelectedScope(RecurringEventUpdateScope.THIS_EVENT); |
| 89 | + }, [activeScope, onUpdateScopeChange]); |
75 | 90 |
|
76 | 91 | return ( |
77 | | - <FloatingPortal> |
78 | | - <FloatingOverlay |
79 | | - className="dialog-overlay" |
80 | | - style={{ background: "rgba(0, 0, 0, 0.8)", zIndex: 4 }} |
81 | | - lockScroll |
| 92 | + <OverlayPanel |
| 93 | + title="Apply changes to" |
| 94 | + onDismiss={closeDialog} |
| 95 | + variant="modal" |
| 96 | + > |
| 97 | + <div |
| 98 | + role="radiogroup" |
| 99 | + aria-label="Apply changes to" |
| 100 | + className="flex w-full flex-col gap-1" |
82 | 101 | > |
83 | | - <FloatingFocusManager context={context}> |
84 | | - <div |
85 | | - ref={refs.setFloating} |
86 | | - style={{ ...floatingStyles, top: "50%", left: "50%" }} |
87 | | - {...interactions.getFloatingProps()} |
88 | | - > |
89 | | - <StyledEventForm role="form" priority={priority}> |
90 | | - <fieldset style={{ borderRadius: theme.shape.borderRadius }}> |
91 | | - <legend>Apply Changes To</legend> |
| 102 | + {options.map((option) => { |
| 103 | + const isSelected = activeScope === option; |
92 | 104 |
|
93 | | - {options.map(([id, option]) => ( |
94 | | - <div key={id}> |
95 | | - <input |
96 | | - type="radio" |
97 | | - name="recurring-event-update-scope" |
98 | | - value={option} |
99 | | - onChange={() => setValue(option)} |
100 | | - checked={value === option} |
101 | | - id={`recurring-event-update-scope-select-${id}`} |
102 | | - /> |
103 | | - |
104 | | - <label |
105 | | - htmlFor={`recurring-event-update-scope-select--${id}`} |
106 | | - > |
107 | | - {option} |
108 | | - </label> |
109 | | - </div> |
110 | | - ))} |
111 | | - </fieldset> |
112 | | - |
113 | | - <SaveSection |
114 | | - saveText="Ok" |
115 | | - priority={priority} |
116 | | - onSubmit={onSubmitHandler} |
117 | | - onCancel={() => setRecurrenceUpdateScopeDialogOpen(false)} |
| 105 | + return ( |
| 106 | + <label |
| 107 | + key={option} |
| 108 | + className={`${updateScopeOptionClassName} ${ |
| 109 | + isSelected ? selectedUpdateScopeOptionClassName : "" |
| 110 | + }`} |
| 111 | + > |
| 112 | + <input |
| 113 | + type="radio" |
| 114 | + name="recurring-event-update-scope" |
| 115 | + value={option} |
| 116 | + checked={isSelected} |
| 117 | + onChange={() => setSelectedScope(option)} |
| 118 | + className="peer sr-only" |
118 | 119 | /> |
119 | | - </StyledEventForm> |
120 | | - </div> |
121 | | - </FloatingFocusManager> |
122 | | - </FloatingOverlay> |
123 | | - </FloatingPortal> |
| 120 | + <span aria-hidden="true" className={radioDotClassName} /> |
| 121 | + {option} |
| 122 | + </label> |
| 123 | + ); |
| 124 | + })} |
| 125 | + </div> |
| 126 | + |
| 127 | + <OverlayPanelActions> |
| 128 | + <OverlayPanelActionButton onClick={closeDialog}> |
| 129 | + Cancel |
| 130 | + </OverlayPanelActionButton> |
| 131 | + <OverlayPanelActionButton variant="primary" onClick={onSubmitHandler}> |
| 132 | + Ok |
| 133 | + </OverlayPanelActionButton> |
| 134 | + </OverlayPanelActions> |
| 135 | + </OverlayPanel> |
124 | 136 | ); |
125 | 137 | } |
0 commit comments