Skip to content

Commit 53b7893

Browse files
authored
Keyboard shortcuts (#58)
* Add keyboard shortcuts * Fix issue with focus * Fix other focus issue * Fix issue with delete key when editing * Add context menu tests * Highlight selected groups
1 parent 7332f6c commit 53b7893

File tree

4 files changed

+336
-10
lines changed

4 files changed

+336
-10
lines changed

packages/base/src/panelview/components/layers.tsx

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import {
1313
ReactWidget,
1414
caretDownIcon
1515
} from '@jupyterlab/ui-components';
16+
import { Message } from '@lumino/messaging';
1617
import { Panel } from '@lumino/widgets';
17-
import React, { MouseEvent, useEffect, useState } from 'react';
18+
import React, {
19+
MouseEvent as ReactMouseEvent,
20+
useEffect,
21+
useState
22+
} from 'react';
1823
import { icons } from '../../constants';
1924
import { nonVisibilityIcon, visibilityIcon } from '../../icons';
2025
import { IControlPanelModel } from '../../types';
@@ -44,7 +49,7 @@ export namespace LayersPanel {
4449
type: SelectionType;
4550
item: string;
4651
nodeId?: string;
47-
event: MouseEvent;
52+
event: ReactMouseEvent;
4853
}
4954
}
5055

@@ -55,6 +60,7 @@ export class LayersPanel extends Panel {
5560
constructor(options: LayersPanel.IOptions) {
5661
super();
5762
this._model = options.model;
63+
this._lastSelectedNodeId = '';
5864
this.id = 'jupytergis::layerTree';
5965
this.addClass(LAYERS_PANEL_CLASS);
6066

@@ -68,6 +74,38 @@ export class LayersPanel extends Panel {
6874
);
6975
}
7076

77+
protected onAfterAttach(msg: Message): void {
78+
super.onAfterAttach(msg);
79+
const node = this.node;
80+
node.addEventListener('mouseup', this);
81+
}
82+
83+
protected onBeforeDetach(msg: Message): void {
84+
super.onBeforeDetach(msg);
85+
const node = this.node;
86+
node.removeEventListener('mouseup', this);
87+
}
88+
89+
handleEvent(event: Event): void {
90+
switch (event.type) {
91+
case 'mouseup':
92+
this._mouseUpEvent(event as MouseEvent);
93+
break;
94+
default:
95+
break;
96+
}
97+
}
98+
99+
private _mouseUpEvent(event: MouseEvent): void {
100+
// If we click on empty space in the layer panel, keep the focus on the last selected element
101+
const node = document.getElementById(this._lastSelectedNodeId);
102+
if (!node) {
103+
return;
104+
}
105+
106+
node.focus();
107+
}
108+
71109
/**
72110
* Function to call when a layer is selected from a component of the panel.
73111
*
@@ -79,12 +117,20 @@ export class LayersPanel extends Panel {
79117
nodeId,
80118
event
81119
}: LayersPanel.IClickHandlerParams) => {
82-
if (!this._model) {
120+
if (!this._model || !nodeId) {
83121
return;
84122
}
85123

86124
const { jGISModel } = this._model;
87125
const selectedValue = jGISModel?.localState?.selected?.value;
126+
const node = document.getElementById(nodeId);
127+
128+
if (!node) {
129+
return;
130+
}
131+
132+
node.tabIndex = 0;
133+
node.focus();
88134

89135
// Early return if no selection exists
90136
if (!selectedValue) {
@@ -120,6 +166,7 @@ export class LayersPanel extends Panel {
120166
...selectedValue,
121167
[item]: { type, selectedNodeId: nodeId }
122168
};
169+
this._lastSelectedNodeId = nodeId;
123170

124171
jGISModel.syncSelected(updatedSelectedValue, this.id);
125172
}
@@ -132,11 +179,13 @@ export class LayersPanel extends Panel {
132179
type,
133180
selectedNodeId: nodeId
134181
};
182+
this._lastSelectedNodeId = nodeId;
135183
}
136184
this._model?.jGISModel?.syncSelected(selection, this.id);
137185
}
138186

139187
private _model: IControlPanelModel | undefined;
188+
private _lastSelectedNodeId: string;
140189
}
141190

142191
/**
@@ -179,12 +228,10 @@ function LayersBodyComponent(props: IBodyProps): JSX.Element {
179228
};
180229
model?.sharedModel.layersChanged.connect(updateLayers);
181230
model?.sharedModel.layerTreeChanged.connect(updateLayers);
182-
model?.clientStateChanged.connect(updateLayers);
183231

184232
return () => {
185233
model?.sharedModel.layersChanged.disconnect(updateLayers);
186234
model?.sharedModel.layerTreeChanged.disconnect(updateLayers);
187-
model?.clientStateChanged.disconnect(updateLayers);
188235
};
189236
}, [model]);
190237

@@ -240,12 +287,31 @@ function LayerGroupComponent(props: ILayerGroupProps): JSX.Element {
240287
const [open, setOpen] = useState<boolean>(false);
241288
const name = group?.name ?? 'Undefined group';
242289
const layers = group?.layers ?? [];
290+
const [selected, setSelected] = useState<boolean>(
291+
// TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does
292+
isSelected(group.name, gisModel)
293+
);
243294

244295
useEffect(() => {
245296
setId(DOMUtils.createDomID());
246297
}, []);
247298

248-
const handleRightClick = (event: MouseEvent<HTMLElement>) => {
299+
/**
300+
* Listen to the changes on the current layer.
301+
*/
302+
useEffect(() => {
303+
const onClientSharedStateChanged = () => {
304+
// TODO Support follow mode and remoteUser state
305+
setSelected(isSelected(group.name, gisModel));
306+
};
307+
gisModel?.clientStateChanged.connect(onClientSharedStateChanged);
308+
309+
return () => {
310+
gisModel?.clientStateChanged.disconnect(onClientSharedStateChanged);
311+
};
312+
}, [gisModel]);
313+
314+
const handleRightClick = (event: ReactMouseEvent<HTMLElement>) => {
249315
const childId = event.currentTarget.children.namedItem(id)?.id;
250316
onClick({ type: 'group', item: name, nodeId: childId, event });
251317
};
@@ -255,7 +321,7 @@ function LayerGroupComponent(props: ILayerGroupProps): JSX.Element {
255321
<div
256322
onClick={() => setOpen(!open)}
257323
onContextMenu={handleRightClick}
258-
className={LAYER_GROUP_HEADER_CLASS}
324+
className={`${LAYER_GROUP_HEADER_CLASS} ${selected ? ' jp-mod-selected' : ''}`}
259325
>
260326
<LabIcon.resolveReact
261327
icon={caretDownIcon}
@@ -355,7 +421,7 @@ function LayerComponent(props: ILayerProps): JSX.Element {
355421
gisModel?.sharedModel?.updateLayer(layerId, layer);
356422
};
357423

358-
const setSelection = (event: MouseEvent<HTMLElement>) => {
424+
const setSelection = (event: ReactMouseEvent<HTMLElement>) => {
359425
const childId = event.currentTarget.children.namedItem(id)?.id;
360426
onClick({ type: 'layer', item: layerId, nodeId: childId, event });
361427
};

packages/base/style/leftPanel.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
| Distributed under the terms of the Modified BSD License.
44
|---------------------------------------------------------------------------- */
55

6+
.jp-gis-layerPanel {
7+
display: flex;
8+
flex-direction: column;
9+
}
10+
611
.jp-gis-sourcePanel,
712
.jp-gis-layerPanel {
813
min-height: 3em;
@@ -46,7 +51,8 @@
4651
}
4752

4853
.jp-gis-layer.jp-mod-selected,
49-
.jp-gis-source.jp-mod-selected {
54+
.jp-gis-source.jp-mod-selected,
55+
.jp-gis-layerGroupHeader.jp-mod-selected {
5056
background: var(--jp-editor-selected-background);
5157
}
5258

@@ -84,7 +90,7 @@
8490
align-items: center;
8591
}
8692

87-
.jp-gis-layer:focus-within {
93+
.jp-gis-layerText:focus {
8894
outline: none;
8995
}
9096

python/jupytergis_lab/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,30 @@ const plugin: JupyterFrontEndPlugin<void> = {
104104
rank: 1
105105
});
106106

107+
app.commands.addKeyBinding({
108+
command: CommandIDs.removeLayer,
109+
keys: ['Delete'],
110+
selector: '.jp-gis-layerTitle .jp-gis-layerText'
111+
});
112+
107113
app.contextMenu.addItem({
108114
command: CommandIDs.renameLayer,
109115
selector: '.jp-gis-layerTitle',
110116
rank: 1
111117
});
112118

119+
app.commands.addKeyBinding({
120+
command: CommandIDs.renameLayer,
121+
keys: ['F2'],
122+
selector: '.jp-gis-layerTitle .jp-gis-layerText'
123+
});
124+
125+
app.commands.addKeyBinding({
126+
command: CommandIDs.renameLayer,
127+
keys: ['F2'],
128+
selector: '.jp-gis-layerTitle'
129+
});
130+
113131
const moveLayerSubmenu = new Menu({ commands: app.commands });
114132
moveLayerSubmenu.title.label = translator
115133
.load('jupyterlab')
@@ -139,12 +157,24 @@ const plugin: JupyterFrontEndPlugin<void> = {
139157
rank: 1
140158
});
141159

160+
app.commands.addKeyBinding({
161+
command: CommandIDs.removeGroup,
162+
keys: ['Delete'],
163+
selector: '.jp-gis-layerGroupHeader .jp-gis-layerText'
164+
});
165+
142166
app.contextMenu.addItem({
143167
command: CommandIDs.renameGroup,
144168
selector: '.jp-gis-layerGroupHeader',
145169
rank: 1
146170
});
147171

172+
app.commands.addKeyBinding({
173+
command: CommandIDs.renameGroup,
174+
keys: ['F2'],
175+
selector: '.jp-gis-layerGroupHeader .jp-gis-layerText'
176+
});
177+
148178
if (mainMenu) {
149179
populateMenus(mainMenu, isEnabled);
150180
}

0 commit comments

Comments
 (0)