Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion web/pgadmin/browser/static/js/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,12 @@ _.extend(pgBrowser.keyboardNavigation, {
// to give React a chance to paint the panel before we move into it.
pgAdmin.Browser.Events.trigger(SHOW_OBJECT_EXPLORER_EVENT);
setTimeout(()=>{
document.querySelector('[id="id-object-explorer"]')?.focus();
const panel = document.querySelector('[id="id-object-explorer"]');
// Focus the tree rather than the panel around it. The panel is a plain
// div with no tabindex, so focusing it has never done anything; the
// tree carries tabindex="-1" and can actually take focus, which is
// what makes the arrow keys work once the shortcut has been pressed.
(panel?.querySelector('.file-tree') ?? panel)?.focus();
tree.t.select(tree.i);
}, 0);
},
Expand Down
11 changes: 11 additions & 0 deletions web/pgadmin/static/js/Theme/overrides/reactaspen.override.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export default function reactAspenOverride(theme) {
display: 'inline-block',
position: 'relative',
width: '100%',
// The tree carries tabindex="-1" and the Object Explorer shortcut
// focuses it deliberately, so draw the focus indicator ourselves.
// Left to the browser this is an outline-style: auto ring, which takes
// the host's accent colour - orange in one browser, blue in another -
// and Safari may not draw it at all, leaving keyboard users with no
// indication of where focus has landed. The negative offset keeps the
// outline inside the scrolling container so it is not clipped.
'&:focus-visible': {
outline: '1px solid ' + theme.otherVars.activeBorder,
outlineOffset: '-1px',
},
'&, & *': {
boxSizing: 'border-box',
},
Expand Down
84 changes: 84 additions & 0 deletions web/regression/javascript/browser/keyboard_left_tree_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

// keyboard.js reaches pgadmin.js by relative path, which skips the
// sources/pgadmin alias that maps to the fake, so point it there explicitly.
jest.mock('../../../pgadmin/static/js/pgadmin', () =>
jest.requireActual('../fake_pgadmin'));

import pgAdmin from 'sources/pgadmin';
import '../../../pgadmin/browser/static/js/keyboard';

/* The Object Explorer shortcut is meant to put the keyboard into the tree, so
* that the arrow keys move between nodes. It focused the rc-dock tab pane
* around the tree, which is a plain div with no tabindex and therefore cannot
* take focus at all, so the shortcut only ever selected a node and left focus
* wherever it was. */
describe('keyboardNavigation.bindLeftTree', () => {
let select;

const buildObjectExplorer = ({withTree = true} = {}) => {
const pane = document.createElement('div');
pane.id = 'id-object-explorer';
pane.className = 'dock-tabpane dock-tabpane-active';

let tree = null;
if (withTree) {
tree = document.createElement('div');
tree.className = 'file-tree';
// As react-aspen renders it: programmatically focusable, not tabbable.
tree.setAttribute('tabindex', '-1');
pane.appendChild(tree);
}

document.body.appendChild(pane);
return {pane, tree};
};

beforeEach(() => {
jest.useFakeTimers();
document.body.innerHTML = '';
select = jest.fn();
pgAdmin.Browser.keyboardNavigation.getTreeDetails = () => ({
t: {select}, i: 'some-tree-item',
});
});

afterEach(() => {
jest.useRealTimers();
document.body.innerHTML = '';
});

it('moves focus into the tree', () => {
const {tree} = buildObjectExplorer();

pgAdmin.Browser.keyboardNavigation.bindLeftTree();
jest.runAllTimers();

expect(document.activeElement).toBe(tree);
expect(select).toHaveBeenCalledWith('some-tree-item');
});

it('falls back to the panel when there is no tree to focus', () => {
buildObjectExplorer({withTree: false});

expect(() => {
pgAdmin.Browser.keyboardNavigation.bindLeftTree();
jest.runAllTimers();
}).not.toThrow();
expect(select).toHaveBeenCalled();
});

it('does not throw when the Object Explorer is not in the DOM', () => {
expect(() => {
pgAdmin.Browser.keyboardNavigation.bindLeftTree();
jest.runAllTimers();
}).not.toThrow();
});
});
Loading