Skip to content

Commit c4eef75

Browse files
committed
chore(components): remove useToast / useConfirmationModal hooks, only provide global functions as interface
1 parent 05a707d commit c4eef75

File tree

10 files changed

+153
-316
lines changed

10 files changed

+153
-316
lines changed

packages/compass-components/src/hooks/use-confirmation.spec.tsx

Lines changed: 20 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
cleanup,
32
render,
43
screen,
54
waitFor,
@@ -10,85 +9,30 @@ import {
109
import { expect } from 'chai';
1110
import React from 'react';
1211

13-
import {
14-
ConfirmationModalArea,
15-
useConfirmationModal,
16-
showConfirmation,
17-
} from './use-confirmation';
18-
19-
const OpenConfirmationModalButton = () => {
20-
const { showConfirmation } = useConfirmationModal();
21-
return (
22-
<button
23-
type="button"
24-
onClick={() =>
25-
void showConfirmation({
26-
title: 'Are you sure?',
27-
description: 'This action can not be undone.',
28-
buttonText: 'Yes',
29-
})
30-
}
31-
>
32-
Open Modal
33-
</button>
34-
);
35-
};
12+
import { ConfirmationModalArea, showConfirmation } from './use-confirmation';
3613

3714
describe('use-confirmation', function () {
38-
afterEach(cleanup);
39-
40-
context('useConfirmationModal hook', function () {
41-
let modal: HTMLElement;
42-
beforeEach(function () {
43-
render(
44-
<ConfirmationModalArea>
45-
<OpenConfirmationModalButton />
46-
</ConfirmationModalArea>
47-
);
48-
49-
userEvent.click(screen.getByText('Open Modal'));
50-
modal = screen.getByTestId('confirmation-modal');
51-
expect(modal).to.exist;
52-
});
53-
54-
it('renders modal contents', function () {
55-
expect(within(modal).getByText('Are you sure?')).to.exist;
56-
expect(
57-
within(modal).getByText('This action can not be undone.')
58-
).to.exist;
59-
expect(within(modal).getByText('Yes')).to.exist;
60-
expect(within(modal).getByText('Cancel')).to.exist;
61-
});
62-
63-
it('handles cancel action', async function () {
64-
userEvent.click(within(modal).getByText('Cancel'));
65-
await waitForElementToBeRemoved(() =>
66-
screen.getByTestId('confirmation-modal')
67-
);
68-
});
69-
70-
it('handles confirm action', async function () {
71-
userEvent.click(within(modal).getByText('Yes'));
72-
await waitForElementToBeRemoved(() =>
73-
screen.getByTestId('confirmation-modal')
74-
);
75-
});
76-
});
77-
7815
context('showConfirmation global function', function () {
7916
let modal: HTMLElement;
8017
let response: Promise<boolean>;
8118
beforeEach(async function () {
8219
render(
8320
<ConfirmationModalArea>
84-
<div />
21+
<button
22+
type="button"
23+
onClick={() => {
24+
response = showConfirmation({
25+
title: 'Are you sure?',
26+
description: 'This action can not be undone.',
27+
buttonText: 'Yes',
28+
});
29+
}}
30+
>
31+
Open Modal
32+
</button>
8533
</ConfirmationModalArea>
8634
);
87-
response = showConfirmation({
88-
title: 'Are you sure?',
89-
description: 'This action can not be undone.',
90-
buttonText: 'Yes',
91-
});
35+
userEvent.click(screen.getByText('Open Modal'));
9236
await waitFor(() => {
9337
modal = screen.getByTestId('confirmation-modal');
9438
});
@@ -105,12 +49,18 @@ describe('use-confirmation', function () {
10549

10650
it('handles cancel action', async function () {
10751
userEvent.click(within(modal).getByText('Cancel'));
52+
await waitForElementToBeRemoved(() =>
53+
screen.getByTestId('confirmation-modal')
54+
);
10855
const confirmed = await response;
10956
expect(confirmed).to.be.false;
11057
});
11158

11259
it('handles confirm action', async function () {
11360
userEvent.click(within(modal).getByText('Yes'));
61+
await waitForElementToBeRemoved(() =>
62+
screen.getByTestId('confirmation-modal')
63+
);
11464
const confirmed = await response;
11565
expect(confirmed).to.be.true;
11666
});
Lines changed: 82 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useContext, useEffect, useRef, useState } from 'react';
1+
import React, {
2+
useCallback,
3+
useContext,
4+
useEffect,
5+
useRef,
6+
useState,
7+
} from 'react';
28
import { Variant as ConfirmationModalVariant } from '@leafygreen-ui/confirmation-modal';
39
import ConfirmationModal from '../components/modals/confirmation-modal';
410
import { css } from '@leafygreen-ui/emotion';
@@ -22,125 +28,66 @@ type ConfirmationProperties = Partial<
2228

2329
type ConfirmationCallback = (value: boolean) => void;
2430

25-
interface ConfirmationModalContextData {
26-
showConfirmation: (props: ConfirmationProperties) => Promise<boolean>;
27-
isMounted: boolean;
28-
}
29-
30-
type ShowConfirmationEventDetail = {
31-
props: ConfirmationProperties & { confirmationId: number };
31+
type OnShowConfirmationProperties = {
32+
props: ConfirmationProperties;
3233
resolve: ConfirmationCallback;
3334
reject: (err?: any) => void;
35+
confirmationId: number;
3436
};
3537

36-
interface ConfirmationEventMap {
37-
'show-confirmation': CustomEvent<ShowConfirmationEventDetail>;
38-
}
39-
40-
interface GlobalConfirmation extends EventTarget {
41-
addEventListener<K extends keyof ConfirmationEventMap>(
42-
type: K,
43-
listener: (this: GlobalConfirmation, ev: ConfirmationEventMap[K]) => void
44-
): void;
45-
addEventListener(
46-
type: string,
47-
listener: EventListenerOrEventListenerObject
48-
): void;
49-
removeEventListener<K extends keyof ConfirmationEventMap>(
50-
type: K,
51-
listener: (this: GlobalConfirmation, ev: ConfirmationEventMap[K]) => void
52-
): void;
53-
removeEventListener(
54-
type: string,
55-
listener: EventListenerOrEventListenerObject
56-
): void;
38+
interface ConfirmationModalActions {
39+
showConfirmation: (props: ConfirmationProperties) => Promise<boolean>;
5740
}
58-
59-
let confirmationId = 0;
60-
61-
class GlobalConfirmation extends EventTarget {
41+
class GlobalConfirmationModalState implements ConfirmationModalActions {
42+
private confirmationId = 0;
43+
onShowCallback: ((props: OnShowConfirmationProperties) => void) | null = null;
6244
showConfirmation(props: ConfirmationProperties) {
6345
return new Promise<boolean>((resolve, reject) => {
64-
this.dispatchEvent(
65-
new CustomEvent<ShowConfirmationEventDetail>('show-confirmation', {
66-
detail: {
67-
props: { ...props, confirmationId: ++confirmationId },
68-
resolve,
69-
reject,
70-
},
71-
})
72-
);
46+
this.onShowCallback?.({
47+
props,
48+
resolve,
49+
reject,
50+
confirmationId: ++this.confirmationId,
51+
});
7352
});
7453
}
7554
}
76-
const globalConfirmation = new GlobalConfirmation();
7755

78-
export const showConfirmation =
79-
globalConfirmation.showConfirmation.bind(globalConfirmation);
56+
const confirmationModalState = new GlobalConfirmationModalState();
8057

81-
const ConfirmationModalContext =
82-
React.createContext<ConfirmationModalContextData>({
83-
isMounted: false,
84-
showConfirmation,
85-
});
58+
export const showConfirmation = confirmationModalState.showConfirmation.bind(
59+
confirmationModalState
60+
);
8661

87-
type ConfirmationModalAreaProps = Partial<
88-
ShowConfirmationEventDetail['props']
89-
> & { open: boolean };
62+
export const showConfirmationModal = showConfirmation;
9063

9164
const hideButtonStyles = css({
9265
display: 'none !important',
9366
});
9467

95-
export const ConfirmationModalArea: React.FC = ({ children }) => {
96-
const hasParentContext = useContext(ConfirmationModalContext).isMounted;
97-
98-
const [confirmationProps, setConfirmationProps] =
99-
useState<ConfirmationModalAreaProps>({
100-
open: false,
101-
confirmationId: -1,
102-
});
68+
const _ConfirmationModalArea: React.FunctionComponent = ({ children }) => {
69+
const [confirmationProps, setConfirmationProps] = useState<
70+
Partial<ConfirmationProperties> & { open: boolean; confirmationId: number }
71+
>({
72+
open: false,
73+
confirmationId: -1,
74+
});
10375
const callbackRef = useRef<ConfirmationCallback>();
104-
105-
const listenerRef =
106-
useRef<(event: CustomEvent<ShowConfirmationEventDetail>) => void>();
107-
108-
const contextValue = React.useMemo(
109-
() => ({
110-
showConfirmation: (props: ConfirmationProperties) => {
111-
return new Promise<boolean>((resolve, reject) => {
112-
const event = new CustomEvent<ShowConfirmationEventDetail>(
113-
'show-confirmation',
114-
{
115-
detail: {
116-
props: { ...props, confirmationId: ++confirmationId },
117-
resolve,
118-
reject,
119-
},
120-
}
121-
);
122-
listenerRef.current?.(event);
123-
});
124-
},
125-
isMounted: true,
126-
}),
127-
[]
128-
);
129-
130-
useEffect(() => {
131-
return () => {
132-
callbackRef.current?.(false);
133-
};
134-
}, []);
135-
136-
// Event listener to use confirmation modal outside of react
137-
useEffect(() => {
138-
const listener = ({
139-
detail: { resolve, reject, props },
140-
}: CustomEvent<ShowConfirmationEventDetail>) => {
141-
setConfirmationProps({ open: true, ...props });
76+
const confirmationModalStateRef = useRef<GlobalConfirmationModalState>();
77+
78+
if (!confirmationModalStateRef.current) {
79+
confirmationModalStateRef.current = confirmationModalState;
80+
confirmationModalStateRef.current.onShowCallback = ({
81+
props,
82+
resolve,
83+
reject,
84+
confirmationId,
85+
}) => {
86+
setConfirmationProps({ open: true, confirmationId, ...props });
14287
const onAbort = () => {
143-
setConfirmationProps({ open: false, ...props });
88+
setConfirmationProps((state) => {
89+
return { ...state, open: false };
90+
});
14491
reject(props.signal?.reason);
14592
};
14693
callbackRef.current = (confirmed) => {
@@ -149,40 +96,42 @@ export const ConfirmationModalArea: React.FC = ({ children }) => {
14996
};
15097
props.signal?.addEventListener('abort', onAbort);
15198
};
152-
listenerRef.current = listener;
153-
globalConfirmation.addEventListener('show-confirmation', listener);
99+
}
100+
101+
useEffect(() => {
154102
return () => {
155-
globalConfirmation.removeEventListener('show-confirmation', listener);
103+
callbackRef.current?.(false);
104+
if (confirmationModalStateRef.current) {
105+
confirmationModalStateRef.current.onShowCallback = null;
106+
}
156107
};
157108
}, []);
158109

159-
const handleConfirm = () => {
160-
onUserAction(true);
161-
};
162-
163-
const handleCancel = () => {
164-
onUserAction(false);
165-
};
166-
167-
const onUserAction = (value: boolean) => {
168-
setConfirmationProps((state) => ({ ...state, open: false }));
110+
const onUserAction = useCallback((value: boolean) => {
111+
setConfirmationProps((state) => {
112+
return { ...state, open: false };
113+
});
169114
callbackRef.current?.(value);
170115
callbackRef.current = undefined;
171-
};
116+
}, []);
172117

173-
if (hasParentContext) {
174-
return <>{children}</>;
175-
}
118+
const handleConfirm = useCallback(() => {
119+
onUserAction(true);
120+
}, [onUserAction]);
121+
122+
const handleCancel = useCallback(() => {
123+
onUserAction(false);
124+
}, [onUserAction]);
176125

177126
return (
178-
<ConfirmationModalContext.Provider value={contextValue}>
127+
<>
179128
{children}
180129
<ConfirmationModal
181130
// To make sure that confirmation modal internal state is reset for
182131
// every confirmation request triggered with showConfirmation method we
183132
// pass `confirmationId` as a component key to force React to remount it
184133
// when request starts
185-
key={confirmationId}
134+
key={confirmationProps.confirmationId}
186135
data-testid={confirmationProps['data-testid'] ?? 'confirmation-modal'}
187136
open={confirmationProps.open}
188137
title={confirmationProps.title ?? 'Are you sure?'}
@@ -205,16 +154,22 @@ export const ConfirmationModalArea: React.FC = ({ children }) => {
205154
>
206155
{confirmationProps.description}
207156
</ConfirmationModal>
208-
</ConfirmationModalContext.Provider>
157+
</>
209158
);
210159
};
211160

212-
export const useConfirmationModal = () => {
213-
const { isMounted, showConfirmation } = useContext(ConfirmationModalContext);
214-
if (!isMounted) {
215-
throw new Error(
216-
'useConfirmationModal must be used within a ConfirmationModalArea'
217-
);
161+
const ConfirmationModalAreaMountedContext = React.createContext(false);
162+
163+
export const ConfirmationModalArea: React.FunctionComponent = ({
164+
children,
165+
}) => {
166+
if (useContext(ConfirmationModalAreaMountedContext)) {
167+
return <>{children}</>;
218168
}
219-
return { showConfirmation };
169+
170+
return (
171+
<ConfirmationModalAreaMountedContext.Provider value={true}>
172+
<_ConfirmationModalArea>{children}</_ConfirmationModalArea>
173+
</ConfirmationModalAreaMountedContext.Provider>
174+
);
220175
};

0 commit comments

Comments
 (0)