Skip to content

Commit bd753ed

Browse files
Ensure double click event is not ignored in the browser tree
1 parent 99e1f00 commit bd753ed

File tree

4 files changed

+75
-31
lines changed

4 files changed

+75
-31
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useSingleAndDoubleClick } from '../../../custom_hooks';
2+
import * as React from 'react';
3+
import PropTypes from 'prop-types';
4+
import CustomPropTypes from '../../../../js/custom_prop_types';
5+
6+
export default function DoubleClickHandler({onSingleClick, onDoubleClick, children}){
7+
const onClick = useSingleAndDoubleClick(onSingleClick, onDoubleClick) ;
8+
return(
9+
<div onClick={onClick}>
10+
{children}
11+
</div>
12+
);
13+
}
14+
DoubleClickHandler.propTypes = {
15+
onSingleClick: PropTypes.func,
16+
onDoubleClick: PropTypes.func,
17+
children: CustomPropTypes.children
18+
};

web/pgadmin/static/js/components/PgTree/FileTreeItem/index.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
//
88
//////////////////////////////////////////////////////////////
99

10-
import cn from 'classnames';
1110
import * as React from 'react';
1211
import { ClasslistComposite } from 'aspen-decorations';
1312
import { Directory, FileEntry, IItemRendererProps, ItemType, RenamePromptHandle, FileType, FileOrDir} from 'react-aspen';
14-
import {IFileTreeXTriggerEvents, FileTreeXEvent } from '../types';
15-
import _ from 'lodash';
13+
import { FileTreeXEvent } from '../types';
1614
import { Notificar } from 'notificar';
17-
15+
import _ from 'lodash';
16+
import cn from 'classnames';
17+
import DoubleClickHandler from './DoubleClickHandler';
1818
interface IItemRendererXProps {
1919
/**
2020
* In this implementation, decoration are null when item is `PromptHandle`
@@ -48,7 +48,6 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
4848
public static readonly renderHeight: number = 24;
4949
private static readonly itemIdToRefMap: Map<number, HTMLDivElement> = new Map();
5050
private static readonly refToItemIdMap: Map<number, HTMLDivElement> = new Map();
51-
private readonly fileTreeEvent: IFileTreeXTriggerEvents;
5251

5352
constructor(props) {
5453
super(props);
@@ -58,7 +57,6 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
5857

5958
public render() {
6059
const { item, itemType, decorations } = this.props;
61-
6260
const isRenamePrompt = itemType === ItemType.RenamePrompt;
6361
const isNewPrompt = itemType === ItemType.NewDirectoryPrompt || itemType === ItemType.NewFilePrompt;
6462
const isDirExpanded = itemType === ItemType.Directory
@@ -93,7 +91,6 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
9391
data-depth={item.depth}
9492
onContextMenu={this.handleContextMenu}
9593
onClick={this.handleClick}
96-
onDoubleClick={this.handleDoubleClick}
9794
onDragStart={this.handleDragStartItem}
9895
onMouseEnter={this.handleMouseEnter}
9996
onMouseLeave={this.handleMouseLeave}
@@ -107,8 +104,8 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
107104
: null
108105
}
109106

110-
<span className='file-label'>
111-
{
107+
<DoubleClickHandler onDoubleClick={this.handleDoubleClick} onSingleClick={this.handleClick} >
108+
<span className='file-label'>{
112109
item._metadata?.data?.icon ?
113110
<i className={cn('file-icon', item._metadata?.data?.icon ? item._metadata.data.icon : fileOrDir)} /> : null
114111
}
@@ -121,7 +118,8 @@ export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRen
121118
{tag.text}
122119
</div>
123120
))}
124-
</span>
121+
</span>
122+
</DoubleClickHandler>
125123
</div>);
126124
}
127125

web/pgadmin/static/js/custom_hooks.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ export function useInterval(callback, delay) {
2929
}, [delay]);
3030
}
3131

32+
/* React hook for handling double and single click events */
33+
export function useSingleAndDoubleClick(handleSingleClick, handleDoubleClick, delay = 250) {
34+
const [state, setState] = useState({ click: 0, props: undefined });
35+
36+
useEffect(() => {
37+
const timer = setTimeout(() => {
38+
// simple click
39+
if (state.click === 1){
40+
handleSingleClick(state.props);
41+
setState({ click: 0, props: state.props });
42+
}
43+
}, delay);
44+
45+
if (state.click === 2) {
46+
handleDoubleClick(state.props);
47+
setState({ click: 0, props: state.props });
48+
}
49+
50+
return () => clearTimeout(timer);
51+
}, [state, handleSingleClick, handleDoubleClick, delay ]);
52+
53+
return (props) => {
54+
setState((prevState) => ({ click: prevState.click + 1, props }));
55+
};
56+
}
57+
58+
3259
export function useDelayedCaller(callback) {
3360
let timer;
3461
useEffect(() => {

web/regression/feature_utils/tree_area_locators.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def server_group_node(server_group_name):
1919
@staticmethod
2020
def server_group_node_exp_status(server_group_name):
2121
return "//i[@class='directory-toggle open']/following-sibling::" \
22-
"span//span[starts-with(text(),'%s')]" % server_group_name
22+
"div//span[starts-with(text(),'%s')]" % server_group_name
2323

2424
# Server Node
2525
@staticmethod
@@ -31,7 +31,7 @@ def server_node(server_name):
3131
@staticmethod
3232
def server_node_exp_status(server_name):
3333
return "//i[@class='directory-toggle open']/following-sibling::" \
34-
"span//span[starts-with(text(),'%s')]" % server_name
34+
"div//span[starts-with(text(),'%s')]" % server_name
3535

3636
# Server Connection
3737
@staticmethod
@@ -43,36 +43,37 @@ def server_connection_status_element(server_name):
4343
# Databases Node
4444
@staticmethod
4545
def databases_node(server_name):
46-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
46+
return "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
4747
"following-sibling::div//span[text()='Databases']" % server_name
4848

4949
@staticmethod
5050
def databases_node_exp_status(server_name):
51-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
52-
"following-sibling::div//span[span[text()='Databases']]/" \
51+
return "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
52+
"following-sibling::div//div[span[span[text()='Databases']]]/" \
5353
"preceding-sibling::i[@class='directory-toggle open']" \
5454
% server_name
5555

5656
# Database Node
5757
@staticmethod
5858
def database_node(database_name):
59-
return "//div[@data-depth='4']/span/span[text()='%s']" % database_name
59+
return "//div[@data-depth='4']/div/span/span[text()='%s']" \
60+
% database_name
6061

6162
@staticmethod
6263
def database_node_exp_status(database_name):
6364
return "//i[@class='directory-toggle open']/following-sibling::" \
64-
"span//span[text()='%s']" % database_name
65+
"div//span[text()='%s']" % database_name
6566

6667
# Schemas Node
6768
@staticmethod
6869
def schemas_node(database_name):
69-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
70+
return "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
7071
"following-sibling::div//span[text()='Schemas']" % database_name
7172

7273
@staticmethod
7374
def schemas_node_exp_status(database_name):
74-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
75-
"following-sibling::div//span[span[text()='Schemas']]/" \
75+
return "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
76+
"following-sibling::div//div[span[span[text()='Schemas']]]/" \
7677
"preceding-sibling::i[@class='directory-toggle open']" \
7778
% database_name
7879

@@ -85,28 +86,28 @@ def schema_node(schema_name):
8586
@staticmethod
8687
def schema_node_exp_status(schema_name):
8788
return "//i[@class='directory-toggle open']/" \
88-
"following-sibling::span//span[text()='%s']" % schema_name
89+
"following-sibling::div//span[text()='%s']" % schema_name
8990

9091
# Tables Node
9192
@staticmethod
9293
def tables_node(schema_name):
93-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
94+
return "//div[divdiv[[span[span[starts-with(text(),'%s')]]]]]/" \
9495
"following-sibling::div//span[text()='Tables']" % schema_name
9596

9697
@staticmethod
9798
def tables_node_exp_status(schema_name):
9899
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
99-
"following-sibling::div//span[span[text()='Tables']]/" \
100+
"following-sibling::div//div[span[span[text()='Tables']]]/" \
100101
"preceding-sibling::i[@class='directory-toggle open']"\
101102
% schema_name
102103

103104
# Schema child
104105
child_node_exp_status = \
105-
"//div[div[span[span[starts-with(text(),'%s')]]]]/" \
106-
"following-sibling::div//span[span[text()='%s']]/" \
106+
"//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
107+
"following-sibling::div//div[span[span[text()='%s']]]/" \
107108
"preceding-sibling::i[@class='directory-toggle open']"
108109

109-
child_node = "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
110+
child_node = "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
110111
"following-sibling::div//span[text()='%s']"
111112

112113
@staticmethod
@@ -120,8 +121,8 @@ def schema_child_node(schema_name, child_node_name):
120121

121122
@staticmethod
122123
def schema_child_node_expand_icon_xpath(schema_name, child_node_name):
123-
return "//div[div[span[span[starts-with(text(),'%s')]]]]/" \
124-
"following-sibling::div//span[text()='%s']/../" \
124+
return "//div[div[div[span[span[starts-with(text(),'%s')]]]]]/" \
125+
"following-sibling::div//div[span[text()='%s']]/../" \
125126
"preceding-sibling::i" % (schema_name, child_node_name)
126127

127128
# Database child
@@ -147,17 +148,17 @@ def server_child_node(server_name, child_node_name):
147148
# Table Node
148149
@staticmethod
149150
def table_node(table_name):
150-
return "//div[@data-depth='8']/span/span[text()='%s']" % table_name
151+
return "//div[@data-depth='8']/div/span/span[text()='%s']" % table_name
151152

152153
# Function Node
153154
@staticmethod
154155
def function_node(table_name):
155-
return "//div[@data-depth='8']/span/span[text()='%s']" % table_name
156+
return "//div[@data-depth='8']/div/span/span[text()='%s']" % table_name
156157

157158
# Role Node
158159
@staticmethod
159160
def role_node(role_name):
160-
return "//div[@data-depth='4']/span/span[text()='%s']" % role_name
161+
return "//div[@data-depth='4']/div/span/span[text()='%s']" % role_name
161162

162163
# Context element option
163164
@staticmethod

0 commit comments

Comments
 (0)