Skip to content

Commit ecff9fa

Browse files
committedNov 29, 2022
fix: handle open prop in Dialog component
1 parent 64c18ec commit ecff9fa

File tree

4 files changed

+305
-6
lines changed

4 files changed

+305
-6
lines changed
 

‎react/Dialog/DialogEffects.spec.tsx

+244
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import '@testing-library/jest-dom'
2+
import React from 'react'
13
import { Theme } from '@material-ui/core'
4+
import { render } from '@testing-library/react'
25

6+
import { WebviewIntentProvider, WebviewService } from 'cozy-intent'
7+
8+
import Dialog from '.'
9+
import { BreakpointsProvider } from '../hooks/useBreakpoints'
310
import { DOMStrings, makeOnMount, makeOnUnmount } from './DialogEffects'
411
import { ThemeColor } from '../hooks/useSetFlagshipUi/useSetFlagshipUI'
512

@@ -219,3 +226,240 @@ it('should provide the inversed UI when Cozybar is not black on white, but white
219226
topTheme: ThemeColor.Light
220227
})
221228
})
229+
230+
jest.mock('cozy-device-helper', () => ({
231+
isFlagshipApp: (): boolean => true,
232+
getFlagshipMetadata: (): Record<string, never> => ({})
233+
}))
234+
235+
const onOpenMountExpected = {
236+
bottomBackground: '#fff',
237+
bottomOverlay: 'rgba(0, 0, 0, 0.5)',
238+
bottomTheme: 'light',
239+
topOverlay: 'rgba(0, 0, 0, 0.5)',
240+
topTheme: 'light'
241+
}
242+
243+
const onOpenUnmountExpected = {
244+
bottomBackground: '#fff',
245+
bottomOverlay: 'transparent',
246+
bottomTheme: 'dark',
247+
topOverlay: 'transparent',
248+
topTheme: 'dark'
249+
}
250+
251+
it('should emit onMount() immediately and onUnmount() when the whole tree is deleted', () => {
252+
const caller = jest.fn<void, unknown[]>()
253+
const service = {
254+
call: (...args: unknown[]): void => caller(...args)
255+
} as WebviewService
256+
257+
const { unmount } = render(
258+
<WebviewIntentProvider webviewService={service}>
259+
<BreakpointsProvider>
260+
<Dialog open />
261+
</BreakpointsProvider>
262+
</WebviewIntentProvider>
263+
)
264+
265+
expect(caller).toHaveBeenNthCalledWith(
266+
1,
267+
'setFlagshipUI',
268+
onOpenMountExpected,
269+
'cozy-ui/Dialog (onOpenMount)'
270+
)
271+
272+
unmount()
273+
274+
expect(caller).toHaveBeenNthCalledWith(
275+
2,
276+
'setFlagshipUI',
277+
onOpenUnmountExpected,
278+
'cozy-ui/Dialog (onOpenUnmount)'
279+
)
280+
})
281+
282+
it('should emit onMount() immediately and onUnmount() when Dialog is deleted from the tree', () => {
283+
const caller = jest.fn<void, unknown[]>()
284+
const service = {
285+
call: (...args: unknown[]): void => caller(...args)
286+
} as WebviewService
287+
288+
const { rerender } = render(
289+
<WebviewIntentProvider webviewService={service}>
290+
<BreakpointsProvider>
291+
<Dialog open />
292+
</BreakpointsProvider>
293+
</WebviewIntentProvider>
294+
)
295+
296+
expect(caller).toHaveBeenNthCalledWith(
297+
1,
298+
'setFlagshipUI',
299+
onOpenMountExpected,
300+
'cozy-ui/Dialog (onOpenMount)'
301+
)
302+
303+
rerender(
304+
<WebviewIntentProvider webviewService={service}></WebviewIntentProvider>
305+
)
306+
307+
expect(caller).toHaveBeenNthCalledWith(
308+
2,
309+
'setFlagshipUI',
310+
onOpenUnmountExpected,
311+
'cozy-ui/Dialog (onOpenUnmount)'
312+
)
313+
})
314+
315+
it('should not emit onMount() if mounted as open:false, then emit onMount() on open:true, then emit onUnmount() on switch back to open:false, then emit onMount() again on switch back to open:true, then emit onUnmount() again when the tree is deleted', () => {
316+
const caller = jest.fn<void, unknown[]>()
317+
const service = {
318+
call: (...args: unknown[]): void => caller(...args)
319+
} as WebviewService
320+
321+
const { rerender, unmount } = render(
322+
<WebviewIntentProvider webviewService={service}>
323+
<BreakpointsProvider>
324+
<Dialog open={false} />
325+
</BreakpointsProvider>
326+
</WebviewIntentProvider>
327+
)
328+
329+
expect(caller).not.toHaveBeenCalled()
330+
331+
rerender(
332+
<WebviewIntentProvider webviewService={service}>
333+
<BreakpointsProvider>
334+
<Dialog open={true} />
335+
</BreakpointsProvider>
336+
</WebviewIntentProvider>
337+
)
338+
339+
expect(caller).toHaveBeenNthCalledWith(
340+
1,
341+
'setFlagshipUI',
342+
onOpenMountExpected,
343+
'cozy-ui/Dialog (onOpenMount)'
344+
)
345+
346+
rerender(
347+
<WebviewIntentProvider webviewService={service}>
348+
<BreakpointsProvider>
349+
<Dialog open={false} />
350+
</BreakpointsProvider>
351+
</WebviewIntentProvider>
352+
)
353+
354+
expect(caller).toHaveBeenNthCalledWith(
355+
2,
356+
'setFlagshipUI',
357+
onOpenUnmountExpected,
358+
'cozy-ui/Dialog (onOpenUnmount)'
359+
)
360+
361+
rerender(
362+
<WebviewIntentProvider webviewService={service}>
363+
<BreakpointsProvider>
364+
<Dialog open={true} />
365+
</BreakpointsProvider>
366+
</WebviewIntentProvider>
367+
)
368+
369+
expect(caller).toHaveBeenNthCalledWith(
370+
3,
371+
'setFlagshipUI',
372+
onOpenMountExpected,
373+
'cozy-ui/Dialog (onOpenMount)'
374+
)
375+
376+
unmount()
377+
378+
expect(caller).toHaveBeenNthCalledWith(
379+
4,
380+
'setFlagshipUI',
381+
onOpenUnmountExpected,
382+
'cozy-ui/Dialog (onOpenUnmount)'
383+
)
384+
})
385+
386+
it('when provided with a faulty <Dialog /> that has no open prop, it should emit nothing at mount or unmount', () => {
387+
/** Using Dialog without open prop is forbidden, so we acknowledge that but since we need to test it we silence the console */
388+
jest.spyOn(console, 'error').mockImplementation(() => {
389+
// do nothing
390+
})
391+
392+
const caller = jest.fn<void, unknown[]>()
393+
const service = {
394+
call: (...args: unknown[]): void => caller(...args)
395+
} as WebviewService
396+
397+
const { unmount } = render(
398+
<WebviewIntentProvider webviewService={service}>
399+
<BreakpointsProvider>
400+
<Dialog />
401+
</BreakpointsProvider>
402+
</WebviewIntentProvider>
403+
)
404+
405+
expect(caller).not.toHaveBeenCalled()
406+
407+
unmount()
408+
409+
/** As it was mounted without the open prop, and we never changed it, then it never sent an onMount message.
410+
* In this scenario, it is only logical that it does not send an unMount message either since it never showed up.
411+
*/
412+
expect(caller).not.toHaveBeenCalled()
413+
})
414+
415+
it('when provided with a faulty <Dialog /> that has no open prop, and then fixed at runtime, it should emit mount and unmount messages as shown earlier', () => {
416+
const caller = jest.fn<void, unknown[]>()
417+
const service = {
418+
call: (...args: unknown[]): void => caller(...args)
419+
} as WebviewService
420+
421+
const { rerender, unmount } = render(
422+
<WebviewIntentProvider webviewService={service}>
423+
<BreakpointsProvider>
424+
<Dialog />
425+
</BreakpointsProvider>
426+
</WebviewIntentProvider>
427+
)
428+
429+
expect(caller).not.toHaveBeenCalled()
430+
431+
rerender(
432+
<WebviewIntentProvider webviewService={service}>
433+
<BreakpointsProvider>
434+
<Dialog open />
435+
</BreakpointsProvider>
436+
</WebviewIntentProvider>
437+
)
438+
439+
expect(caller).toHaveBeenNthCalledWith(
440+
1,
441+
'setFlagshipUI',
442+
onOpenMountExpected,
443+
'cozy-ui/Dialog (onOpenMount)'
444+
)
445+
446+
rerender(
447+
<WebviewIntentProvider webviewService={service}>
448+
<BreakpointsProvider>
449+
<Dialog open={false} />
450+
</BreakpointsProvider>
451+
</WebviewIntentProvider>
452+
)
453+
454+
expect(caller).toHaveBeenNthCalledWith(
455+
2,
456+
'setFlagshipUI',
457+
onOpenUnmountExpected,
458+
'cozy-ui/Dialog (onOpenUnmount)'
459+
)
460+
461+
unmount()
462+
463+
/** Checking it doesn't send two unmount messages in a row */
464+
expect(caller).toHaveBeenCalledTimes(2)
465+
})

‎react/Dialog/DialogEffects.ts

+59-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { getLuminance, Theme, useTheme } from '@material-ui/core'
2+
import { useEffect } from 'react'
23

34
import { getFlagshipMetadata, isFlagshipApp } from 'cozy-device-helper'
5+
import { useWebviewIntent } from 'cozy-intent'
46

57
import {
68
FlagshipUI,
79
ThemeColor,
8-
useSetFlagshipUI
10+
parseArg
911
} from '../hooks/useSetFlagshipUi/useSetFlagshipUI'
1012

1113
interface DialogEffectsOptions {
@@ -120,23 +122,76 @@ const makeCaller = (
120122
const getRootModal = (): HTMLElement | null => {
121123
const modals = document.querySelectorAll(DOMStrings.DialogClass)
122124

123-
return modals.length > 0 ? (modals[0] as HTMLElement) : null
125+
/**
126+
* If we have more than one modal, we are in a stacked dialog scenario.
127+
* In this case we want to have access to the DOM element of the root modal.
128+
* This will allow us to apply the correct background color if a root modal exists, for instance.
129+
*/
130+
return modals.length > 1 ? (modals[0] as HTMLElement) : null
124131
}
125132

126-
const useHook = (fullscreen?: boolean): void => {
133+
const useHook = (open: boolean, fullscreen?: boolean): void => {
127134
const theme = useTheme()
128135
const cozybar = document.querySelector(DOMStrings.CozyBarClass)
129136
const sidebar = document.getElementById(DOMStrings.SidebarID)
130137
const rootModal = getRootModal()
131138
const immersive = Boolean(getFlagshipMetadata().immersive)
132139

133-
useSetFlagshipUI(
140+
useDialogSetFlagshipUI(
141+
open,
134142
makeOnMount({ fullscreen, theme, sidebar, rootModal, cozybar }),
135143
makeOnUnmount({ rootModal, theme, immersive, sidebar, cozybar }),
136144
makeCaller(!!fullscreen, !!rootModal, immersive)
137145
)
138146
}
139147

148+
/**
149+
* Custom version of useSetFlagshipUi() that is aware of the Dialog component.
150+
*
151+
* The difference here is that we send messages to the Native app when a props change.
152+
* In the original version, we send the mount message as soon as the component is rendered.
153+
*
154+
* Dialog can be rendered but hidden, so we need to wait for the open prop to be true
155+
*/
156+
export const useDialogSetFlagshipUI = (
157+
open: boolean,
158+
onMount: FlagshipUI,
159+
onUnmount?: FlagshipUI,
160+
caller?: string
161+
): void => {
162+
const webviewIntent = useWebviewIntent()
163+
164+
useEffect(() => {
165+
if (open)
166+
parseArg(webviewIntent, onMount, `${caller || 'unknown'} (onOpenMount)`)
167+
168+
return () => {
169+
/**
170+
* As we are listening to the open prop, we still want to send an unmount message when the prop changes to false.
171+
* To avoid false positives, we need to ensure the component is currently showing.
172+
* We do that by checking if value of open during this cleanup cycle is false,
173+
* if it is, that means the component is currently showing and is in the process of hiding.
174+
*
175+
* Note that this will also handle abrupt unmounting, as in hiding the dialog without using the open prop.
176+
*/
177+
if (open === false || open === undefined) return
178+
179+
parseArg(
180+
webviewIntent,
181+
onUnmount,
182+
`${caller || 'unknown'} (onOpenUnmount)`
183+
)
184+
}
185+
186+
/**
187+
* We don't want to listen to onMount/onUnmount arguments
188+
* It will create far too many unwanted calls
189+
* We only care about webviewIntent or open props presence,
190+
* Open should always be present, webviewIntent is more uncertain
191+
*/
192+
}, [open, webviewIntent]) // eslint-disable-line react-hooks/exhaustive-deps
193+
}
194+
140195
export const useDialogEffects = isFlagshipApp()
141196
? useHook
142197
: // eslint-disable-next-line @typescript-eslint/no-empty-function

‎react/Dialog/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const Dialog = props => {
2525
: React.Fragment
2626
const cozyTheme = useCozyTheme()
2727

28-
useDialogEffects(props.fullScreen)
28+
useDialogEffects(props.open, props.fullScreen)
2929

3030
return (
3131
<Wrapper>

‎react/hooks/useSetFlagshipUi/useSetFlagshipUI.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface FlagshipUI
1818
bottomTheme: ThemeColor
1919
}
2020

21-
const parseArg = (
21+
export const parseArg = (
2222
webviewIntent?: WebviewService | void,
2323
arg?: FlagshipUI,
2424
caller?: string

0 commit comments

Comments
 (0)
Please sign in to comment.