Skip to content

Commit ff25550

Browse files
authored
Fix popover closing on click inside popup content (#4739)
1 parent 55bd3c2 commit ff25550

15 files changed

Lines changed: 141 additions & 98 deletions

File tree

catalog/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ where verb is one of
1818

1919
## Changes
2020

21+
- [Fixed] Toolbar popover closing on click inside popup content, preventing text selection in code samples ([#4739](https://github.com/quiltdata/quilt/pull/4739))
2122
- [Changed] Package creation from S3 files now includes current bucket as default destination when no workflow config exists; cross-bucket push strictly respects workflow successors configuration ([#4734](https://github.com/quiltdata/quilt/pull/4734))
2223
- [Fixed] Add missing `deleteObject` property to GUI config editor to enable/disable delete buttons for files and directories ([#4692](https://github.com/quiltdata/quilt/pull/4692))
2324
- [Changed] Hide file and directory delete buttons in Bucket tab by default; enable with `ui.actions.deleteObject` ([#4689](https://github.com/quiltdata/quilt/pull/4689))

catalog/app/components/Buttons/WithPopover.spec.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { render, fireEvent, screen, cleanup } from '@testing-library/react'
33
import * as Icons from '@material-ui/icons'
44
import { describe, it, expect, afterEach } from 'vitest'
55

6-
import WithPopover from './WithPopover'
6+
import WithPopover, { CloseOnClick } from './WithPopover'
77

88
describe('components/Buttons/WithPopover', () => {
99
afterEach(cleanup)
@@ -87,24 +87,36 @@ describe('components/Buttons/WithPopover', () => {
8787
expect(screen.queryByTestId('popup-content')).toBeNull()
8888
})
8989

90-
it('should close popup when paper is clicked', () => {
91-
const { container } = render(
90+
it('should keep popup open when content inside it is clicked', () => {
91+
render(
9292
<WithPopover label="Test Button">
9393
<div data-testid="popup-content">Popup Content</div>
9494
</WithPopover>,
9595
)
9696

9797
const button = screen.getByRole('button', { name: /test button/i })
9898
fireEvent.click(button)
99+
expect(screen.getByTestId('popup-content')).toBeTruthy()
99100

101+
fireEvent.click(screen.getByTestId('popup-content'))
100102
expect(screen.getByTestId('popup-content')).toBeTruthy()
103+
})
101104

102-
const paper = container.querySelector('.MuiPaper-root')
103-
if (paper) {
104-
fireEvent.click(paper)
105-
}
105+
it('should close popup when CloseOnClick child is clicked', () => {
106+
render(
107+
<WithPopover label="Test Button">
108+
<CloseOnClick>
109+
<button data-testid="action-button">Do something</button>
110+
</CloseOnClick>
111+
</WithPopover>,
112+
)
106113

107-
expect(screen.queryByTestId('popup-content')).toBeNull()
114+
const trigger = screen.getByRole('button', { name: /test button/i })
115+
fireEvent.click(trigger)
116+
expect(screen.getByTestId('action-button')).toBeTruthy()
117+
118+
fireEvent.click(screen.getByTestId('action-button'))
119+
expect(screen.queryByTestId('action-button')).toBeNull()
108120
})
109121

110122
it('should toggle popup state on button click', () => {

catalog/app/components/Buttons/WithPopover.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as React from 'react'
22
import * as M from '@material-ui/core'
33
import * as Icons from '@material-ui/icons'
44

5+
import log from 'utils/Logging'
6+
57
import Iconized from './Iconized'
68
import type { StrIcon, SvgIcon } from './Iconized'
79

@@ -49,6 +51,21 @@ interface WithPopoverPropsOwn {
4951
export type WithPopoverProps = WithPopoverPropsOwn &
5052
Omit<Parameters<typeof Iconized>[0], 'icon'>
5153

54+
const CloseContext = React.createContext<() => void>(() =>
55+
log.warn('usePopoverClose() called outside of a WithPopover'),
56+
)
57+
58+
export const useClose = () => React.useContext(CloseContext)
59+
60+
interface CloseOnClickProps {
61+
children: React.ReactNode
62+
}
63+
64+
export function CloseOnClick({ children }: CloseOnClickProps) {
65+
const close = useClose()
66+
return <div onClick={close}>{children}</div>
67+
}
68+
5269
export default function WithPopover({
5370
children,
5471
icon,
@@ -86,9 +103,11 @@ export default function WithPopover({
86103

87104
<M.Backdrop open={opened} className={classes.backdrop} onClick={handleClose} />
88105
{opened && (
89-
<M.Paper className={classes.popup} elevation={4} onClick={handleClose}>
90-
{children}
91-
</M.Paper>
106+
<CloseContext.Provider value={handleClose}>
107+
<M.Paper className={classes.popup} elevation={4}>
108+
{children}
109+
</M.Paper>
110+
</CloseContext.Provider>
92111
)}
93112
</div>
94113
)
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export { default as Iconized } from './Iconized'
22
export { default as Skeleton } from './Skeleton'
3-
export { default as WithPopover } from './WithPopover'
3+
export {
4+
default as WithPopover,
5+
useClose as usePopoverClose,
6+
CloseOnClick,
7+
} from './WithPopover'
48

59
export type { SvgIcon, StrIcon } from './Iconized'
610
export type { WithPopoverProps } from './WithPopover'

catalog/app/components/Dialog/PopoverOptions.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,10 @@ interface TabProps {
3838

3939
function Tab({ active, onClick, className, children }: TabProps) {
4040
const classes = useTabStyles()
41-
const handleClick = React.useCallback(
42-
(event: React.MouseEvent<HTMLButtonElement>) => {
43-
event.stopPropagation()
44-
onClick()
45-
},
46-
[onClick],
47-
)
4841
return (
4942
<M.Button
5043
className={cx(classes.root, active && classes.active, className)}
51-
onClick={handleClick}
44+
onClick={onClick}
5245
>
5346
{children}
5447
</M.Button>

catalog/app/containers/Bucket/Dir/Toolbar/Add/Options.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react'
22
import * as M from '@material-ui/core'
33
import * as Icons from '@material-ui/icons'
44

5+
import { CloseOnClick } from 'components/Buttons'
56
import * as Context from './Context'
67

78
const LIST_ITEM_TYPOGRAPHY_PROPS = { noWrap: true } as const
@@ -27,17 +28,19 @@ function MenuItem({ icon, primary, onClick }: MenuItemProps) {
2728
export default function AddOptions() {
2829
const { createFile, openUploadDialog } = Context.use()
2930
return (
30-
<M.List dense>
31-
<MenuItem
32-
icon={<Icons.CreateOutlined />}
33-
primary="Create text file"
34-
onClick={createFile}
35-
/>
36-
<MenuItem
37-
icon={<Icons.PublishOutlined />}
38-
primary="Upload files"
39-
onClick={openUploadDialog}
40-
/>
41-
</M.List>
31+
<CloseOnClick>
32+
<M.List dense>
33+
<MenuItem
34+
icon={<Icons.CreateOutlined />}
35+
primary="Create text file"
36+
onClick={createFile}
37+
/>
38+
<MenuItem
39+
icon={<Icons.PublishOutlined />}
40+
primary="Upload files"
41+
onClick={openUploadDialog}
42+
/>
43+
</M.List>
44+
</CloseOnClick>
4245
)
4346
}

catalog/app/containers/Bucket/Dir/Toolbar/CreatePackage/Options.tsx

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as React from 'react'
22
import * as M from '@material-ui/core'
33
import * as Lab from '@material-ui/lab'
44

5+
import { CloseOnClick } from 'components/Buttons'
6+
57
import { EmptySlot, ErrorSlot } from 'containers/Bucket/Successors'
68
import * as Request from 'utils/useRequest'
79
import * as workflows from 'utils/workflows'
@@ -59,24 +61,19 @@ export default function CreatePackageOptions({
5961
}
6062

6163
return (
62-
<M.List dense className={classes.root}>
63-
{successors.map((successor) => (
64-
<M.ListItem
65-
key={successor.slug}
66-
onClick={(event) => {
67-
event.stopPropagation()
68-
onChange(successor)
69-
}}
70-
button
71-
>
72-
<M.ListItemText
73-
primary={successor.name}
74-
primaryTypographyProps={LIST_ITEM_TYPOGRAPHY_PROPS}
75-
secondary={successor.url}
76-
secondaryTypographyProps={LIST_ITEM_TYPOGRAPHY_PROPS}
77-
/>
78-
</M.ListItem>
79-
))}
80-
</M.List>
64+
<CloseOnClick>
65+
<M.List dense className={classes.root}>
66+
{successors.map((successor) => (
67+
<M.ListItem key={successor.slug} onClick={() => onChange(successor)} button>
68+
<M.ListItemText
69+
primary={successor.name}
70+
primaryTypographyProps={LIST_ITEM_TYPOGRAPHY_PROPS}
71+
secondary={successor.url}
72+
secondaryTypographyProps={LIST_ITEM_TYPOGRAPHY_PROPS}
73+
/>
74+
</M.ListItem>
75+
))}
76+
</M.List>
77+
</CloseOnClick>
8178
)
8279
}

catalog/app/containers/Bucket/Dir/Toolbar/Get/Options.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,11 @@ interface DownloadDirProps {
4141

4242
function DownloadDir({ dirHandle }: DownloadDirProps) {
4343
// TODO: pass selection to Buttons.DownloadDir
44-
const [downloading, setDownloading] = React.useState(false)
45-
React.useEffect(() => {
46-
if (!downloading) return
47-
setTimeout(() => setDownloading(false), 1000)
48-
}, [downloading])
44+
const feedback = Buttons.useDownloadFeedback()
4945
return (
5046
<Buttons.DownloadDir
5147
suffix={`dir/${dirHandle.bucket}/${dirHandle.path}`}
52-
onClick={(event) => {
53-
event.stopPropagation()
54-
setDownloading(true)
55-
}}
56-
{...(downloading ? { startIcon: <M.CircularProgress size={20} /> } : null)}
48+
{...feedback}
5749
>
5850
Download ZIP (directory)
5951
</Buttons.DownloadDir>

catalog/app/containers/Bucket/Dir/Toolbar/Organize/Options.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react'
22
import * as M from '@material-ui/core'
33
import * as Icons from '@material-ui/icons'
44

5+
import { CloseOnClick } from 'components/Buttons'
56
import * as Format from 'utils/format'
67
import assertNever from 'utils/assertNever'
78

@@ -77,7 +78,7 @@ export default function OrganizeOptions({ features }: OrganizeOptionsProps) {
7778
}, [bookmarkStatus])
7879

7980
return (
80-
<>
81+
<CloseOnClick>
8182
<M.ListSubheader inset component="div" disableSticky>
8283
<Format.Plural
8384
value={selectionCount}
@@ -121,6 +122,6 @@ export default function OrganizeOptions({ features }: OrganizeOptionsProps) {
121122
</M.List>
122123
</>
123124
)}
124-
</>
125+
</CloseOnClick>
125126
)
126127
}

catalog/app/containers/Bucket/Download/Buttons.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,35 @@ import * as React from 'react'
22
import * as M from '@material-ui/core'
33
import * as Icons from '@material-ui/icons'
44

5+
import { usePopoverClose } from 'components/Buttons'
56
import type * as Model from 'model'
67
import * as AWS from 'utils/AWS'
78

89
import { ZipDownloadForm } from '../FileView'
910

11+
export function useDownloadFeedback(): {
12+
onClick: () => void
13+
startIcon?: React.ReactNode
14+
} {
15+
const closePopover = usePopoverClose()
16+
const [downloading, setDownloading] = React.useState(false)
17+
React.useEffect(() => {
18+
if (!downloading) return
19+
const timer = setTimeout(() => {
20+
setDownloading(false)
21+
closePopover()
22+
}, 1000)
23+
return () => clearTimeout(timer)
24+
}, [downloading, closePopover])
25+
return React.useMemo(
26+
() => ({
27+
onClick: () => setDownloading(true),
28+
...(downloading ? { startIcon: <M.CircularProgress size={20} /> } : null),
29+
}),
30+
[downloading],
31+
)
32+
}
33+
1034
const useDownloadButtonStyles = M.makeStyles({
1135
root: {
1236
justifyContent: 'flex-start',
@@ -19,7 +43,10 @@ interface DownloadFileProps {
1943
fileHandle: Model.S3.S3ObjectLocation
2044
}
2145

22-
export function DownloadFile({ fileHandle }: DownloadFileProps) {
46+
export function DownloadFile({
47+
fileHandle,
48+
...props
49+
}: DownloadFileProps & M.ButtonProps<'a'>) {
2350
const url = AWS.Signer.useDownloadUrl(fileHandle)
2451
const classes = useDownloadButtonStyles()
2552
return (
@@ -28,6 +55,7 @@ export function DownloadFile({ fileHandle }: DownloadFileProps) {
2855
download
2956
href={url}
3057
startIcon={<Icons.ArrowDownwardOutlined />}
58+
{...props}
3159
>
3260
Download file
3361
</M.Button>

0 commit comments

Comments
 (0)