Skip to content

Commit

Permalink
Add basic keyboard shortcuts
Browse files Browse the repository at this point in the history
fixes: cockpit-project#451

For now this adds following keybindings:

- `alt + arrow up`: go up one directory
- `alt + arrow down`: go into currently selected directory
- `F2`: rename selected file
- `ctrl + a`: select all files
- `N`: create new directory
- `L`: focus files breadcrumbs to manually edit it

Alt + arrow left/right is already supported by cockpit files browser
history.

What is probably good for a discussion is if we want to override default
browser behavior for `ctrl + l` which focuses the Address Bar to "focus
files breadcrumbs to manually edit it" or use the keybind that I set
(`L` aka `shift + l`).
  • Loading branch information
tomasmatus committed Jun 18, 2024
1 parent 8f6d5d0 commit ac26b32
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 50 deletions.
2 changes: 1 addition & 1 deletion src/fileActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const ConfirmDeletionDialog = ({
);
};

const RenameItemModal = ({ path, selected }) => {
export const RenameItemModal = ({ path, selected }) => {
const Dialogs = useDialogs();
const { cwdInfo } = useFilesContext();
const [name, setName] = useState(selected.name);
Expand Down
2 changes: 2 additions & 0 deletions src/files-breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export function FilesBreadcrumbs({ path, showHidden, setShowHidden }: { path: st
setEditMode(false);
};

addEventListener("manual-change-dir", enableEditMode);

const fullPath = path.slice(1);
fullPath.unshift(hostname || "server");

Expand Down
171 changes: 124 additions & 47 deletions src/files-card-body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";

import * as timeformat from "timeformat";
import { ContextMenu } from "cockpit-components-context-menu.jsx";
import { fileActions, ConfirmDeletionDialog } from "./fileActions.jsx";
import { fileActions, ConfirmDeletionDialog, RenameItemModal } from "./fileActions.jsx";
import { filterColumnMapping, filterColumns } from "./header";
import { useFilesContext } from "./app";

import "./files-card-body.scss";
import { CreateDirectoryModal } from "./dialogs/mkdir.js";

const _ = cockpit.gettext;

Expand Down Expand Up @@ -131,6 +132,13 @@ export const FilesCardBody = ({
}
}, [path]);

const goUpOneDir = useCallback(() => {
if (path.length > 1) {
const newPath = path.slice(0, path.length - 1).join("/");
cockpit.location.go("/", { path: encodeURIComponent(newPath) });
}
}, [path]);

useEffect(() => {
calculateBoxPerRow();
window.onresize = calculateBoxPerRow;
Expand Down Expand Up @@ -205,53 +213,121 @@ export const FilesCardBody = ({
}
};

const hasNoKeydownModifiers = (event) => {
return !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey;
};

const onKeyboardNav = (e) => {
if (e.key === "ArrowRight") {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = selectedIdx < sortedFiles.length - 1
? selectedIdx + 1
: 0;

return [sortedFiles[newIdx]];
});
} else if (e.key === "ArrowLeft") {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = selectedIdx > 0
? selectedIdx - 1
: sortedFiles.length - 1;

return [sortedFiles[newIdx]];
});
} else if (e.key === "ArrowUp") {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = Math.max(selectedIdx - boxPerRow, 0);

return [sortedFiles[newIdx]];
});
} else if (e.key === "ArrowDown") {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = Math.min(selectedIdx + boxPerRow, sortedFiles.length - 1);

return [sortedFiles[newIdx]];
});
} else if (e.key === "Enter" && selected.length === 1) {
onDoubleClickNavigate(selected[0]);
} else if (e.key === "Delete" && selected.length !== 0) {
const currentPath = path.join("/") + "/";
Dialogs.show(
<ConfirmDeletionDialog
selected={selected} path={currentPath}
setSelected={setSelected}
/>
);
const currentPath = path.join("/") + "/";

switch (e.key) {
case "ArrowRight":
if (hasNoKeydownModifiers(e)) {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = selectedIdx < sortedFiles.length - 1
? selectedIdx + 1
: 0;

return [sortedFiles[newIdx]];
});
}
break;

case "ArrowLeft":
if (hasNoKeydownModifiers(e)) {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = selectedIdx > 0
? selectedIdx - 1
: sortedFiles.length - 1;

return [sortedFiles[newIdx]];
});
}
break;

case "ArrowUp":
if (e.altKey && !e.shiftKey && !e.ctrlKey) {
goUpOneDir();
} else if (hasNoKeydownModifiers(e)) {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = Math.max(selectedIdx - boxPerRow, 0);

return [sortedFiles[newIdx]];
});
}
break;

case "ArrowDown":
if (e.altKey && !e.shiftKey && !e.ctrlKey && selected.length === 1) {
onDoubleClickNavigate(selected[0]);
} else if (hasNoKeydownModifiers(e)) {
setSelected(_selected => {
const firstSelectedName = _selected?.[0]?.name;
const selectedIdx = sortedFiles?.findIndex(file => file.name === firstSelectedName);
const newIdx = Math.min(selectedIdx + boxPerRow, sortedFiles.length - 1);

return [sortedFiles[newIdx]];
});
}
break;

case "Enter":
if (hasNoKeydownModifiers(e) && selected.length === 1) {
onDoubleClickNavigate(selected[0]);
}
break;

case "Delete":
if (hasNoKeydownModifiers(e) && selected.length !== 0) {
Dialogs.show(
<ConfirmDeletionDialog
selected={selected} path={currentPath}
setSelected={setSelected}
/>
);
}
break;

case "F2":
if (hasNoKeydownModifiers(e) && selected.length === 1) {
Dialogs.show(
<RenameItemModal
path={path}
selected={selected[0]}
/>
);
}
break;

case "a":
if (e.ctrlKey && !e.shiftKey && !e.altKey) {
e.preventDefault();
setSelected(sortedFiles);
}
break;

case "L":
if (!e.ctrlKey && !e.altKey) {
dispatchEvent(new Event("manual-change-dir"));
}
break;

case "N":
if (!e.ctrlKey && !e.altKey) {
Dialogs.show(
<CreateDirectoryModal currentPath={currentPath} />
);
}
break;

default:
break;
}
};

Expand Down Expand Up @@ -283,6 +359,7 @@ export const FilesCardBody = ({
boxPerRow,
selected,
onDoubleClickNavigate,
goUpOneDir,
Dialogs,
path,
]);
Expand Down
57 changes: 55 additions & 2 deletions test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -761,9 +761,23 @@ class TestFiles(testlib.MachineCase):
self.rename_item(b, "new dir1", "testdir")
alert_text = "mv: cannot move '/home/admin/new dir1' to '/home/admin/testdir': Operation not permitted"
self.wait_modal_inline_alert(b, alert_text)
b.click("div.pf-v5-c-modal-box__close")
b.click("div.pf-v5-c-modal-box__close button")
m.execute("sudo chattr -i /home/admin/new\\ dir1")

# Rename file
b.click("[data-item='newfile']")
b.key("F2")
b.wait_in_text(".pf-v5-c-modal-box__title-text", "Rename file newfile")
b.set_input_text("#rename-item-input", "teddybear.txt")
b.click(".pf-v5-c-button.pf-m-primary")
b.wait_visible("[data-item='teddybear.txt']")

# Rename modal does not open when multiple files are selected
b.mouse("[data-item='dest']", "click", ctrlKey=True)
b.mouse("[data-item='new dir1']", "click", ctrlKey=True)
b.key("F2")
b.wait_not_present(".pf-v5-c-modal-box__title-text")

def testHiddenItems(self) -> None:
b = self.browser
m = self.machine
Expand Down Expand Up @@ -937,7 +951,10 @@ class TestFiles(testlib.MachineCase):
self.enter_files()

# Check control-clicking
m.execute("touch /home/admin/file1 && touch /home/admin/file2")
create_files = "touch"
for filename in ["file1", "file2", "file3"]:
create_files += f" /home/admin/{filename}"
m.execute(create_files)
b.click("[data-item='file1']")
b.mouse("[data-item='file2']", "click", ctrlKey=True)
b.wait_visible("[data-item='file1'].row-selected")
Expand All @@ -958,7 +975,15 @@ class TestFiles(testlib.MachineCase):
b.wait_visible("[data-item='file1'].row-selected")
b.wait_text("#sidebar-card-header", "file1empty")

# Select all keybind
b.eval_js("window.focus()")
b.key("a", modifiers=2)
b.wait_visible("[data-item='file1'].row-selected")
b.wait_visible("[data-item='file2'].row-selected")
b.wait_visible("[data-item='file3'].row-selected")

# Check context menu
b.click("[data-item='file1']")
b.mouse("[data-item='file2']", "click", ctrlKey=True)
b.wait_visible("[data-item='file2'].row-selected")
b.wait_text("#sidebar-card-header", "admin2 items selected")
Expand Down Expand Up @@ -987,6 +1012,10 @@ class TestFiles(testlib.MachineCase):

self.enter_files()

create_dirs = "mkdir -p"
for dirname in ["testdir", "anotherdir"]:
create_dirs += f" /home/admin/{dirname}"
m.execute(create_dirs)
create_files = ""
for i in range(0, 4):
create_files += f"touch /home/admin/file{i}; "
Expand All @@ -1003,6 +1032,30 @@ class TestFiles(testlib.MachineCase):
b.key("ArrowLeft")
b.wait_visible("[data-item='file0'].row-selected")

# Go up and down in directory hierarchy
b.click("[data-item='testdir']")
b.key("ArrowDown", modifiers=1)
b.wait_text(".breadcrumb-button:nth-of-type(5)", "testdir")
b.key("ArrowUp", modifiers=1)
b.wait_visible("[data-item='testdir']")

# Manually edit path
b.key("L")
b.input_text("/home/admin/anotherdir")
b.key("Enter")
b.wait_text(".breadcrumb-button:nth-of-type(5)", "anotherdir")

# Go back in history twice
# b.key("ArrowLeft", repeat=2, modifiers=1)
b.eval_js("window.history.back()")
b.eval_js("window.history.back()")
b.wait_text(".breadcrumb-button:nth-of-type(5)", "testdir")

# Go forward in history
# b.key("ArrowRight", repeat=1, modifiers=1)
b.eval_js("window.history.forward()")
b.wait_visible("[data-item='testdir']")

# Up / Down depends on the layout, this is tested on mobile where the
# width is two cards.
b.set_layout("mobile")
Expand Down

0 comments on commit ac26b32

Please sign in to comment.