Skip to content

Commit

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

For now this adds following keybindings:

- `alt + arrow up`: go up one directory
- `alt + arrow down`: activate selected item, enter directory
- `Enter`: activate selected item, enter directory
- `ctrl + shift + L`: focus files breadcrumbs to manually edit it
- `F2`: rename selected file
- `shift + N`: create new directory
- `ctrl + c`: copy file/directory
- `ctrl + v`: paste file/directory
- `ctrl + a`: select all files

Alt + arrow left/right is already supported by cockpit files browser
history.
  • Loading branch information
tomasmatus committed Aug 6, 2024
1 parent 43bab93 commit eca07b3
Show file tree
Hide file tree
Showing 8 changed files with 519 additions and 64 deletions.
60 changes: 60 additions & 0 deletions src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,63 @@
.pf-v5-c-menu-toggle {
padding-inline: var(--pf-v5-global--spacer--md) calc(var(--pf-v5-global--spacer--md) * 0.75);
}

.shortcuts-dialog {
h2 + .pf-v5-c-description-list {
margin-block-start: var(--pf-v5-global--spacer--md);
}

.pf-v5-l-flex {
// Add standard spacing between the description lists that are in a flex
// (PF Flex does odd stuff by default)
gap: var(--pf-v5-global--spacer--lg) var(--pf-v5-global--spacer--md);

> .pf-v5-c-content {
// Have the content prefer 20em and wrap if too narrow
flex: 1 1 20em;
}
}

.pf-v5-c-description-list {
// PF's gap is weirdly too big; let's use the PF standard size that's used everywhere else
--pf-v5-c-content--dl--ColumnGap: var(--pf-v5-global--spacer--md);
// We're setting this up as a table on the list, so they're consistent
display: grid;
// Fixing the width to the keyboard shortcuts
grid-template-columns: auto 1fr;
// Fix PF's negative margin at the end bug (as it's handled by grid layout anyway)
margin-block-end: 0;

.pf-v5-c-description-list__group {
// Ignore the grid of the group and use the grid from the description list, so everything lines up properly
display: contents;
}
}

kbd {
// Description lists bold the dt; we don't want the keys too look too bold
font-weight: normal;
}

// Style key combos
.keystroke {
display: flex;
align-items: center;
color: var(--pf-v5-global--Color--200);
font-size: var(--pf-v5-global--FontSize--xs);
gap: var(--pf-v5-global--spacer--xs);
}

// Style individual keys
.key {
display: inline-block;
background-color: var(--pf-v5-global--BackgroundColor--200);
border-radius: var(--pf-v5-global--BorderRadius--sm);
border: 1px solid var(--pf-v5-global--BorderColor--100);
color: var(--pf-v5-global--Color--100);
padding-block: var(--pf-v5-global--spacer--xs);
padding-inline: var(--pf-v5-global--spacer--sm);
box-shadow: inset 1px 1px 0 var(--pf-v5-global--BackgroundColor--100);
white-space: nowrap;
}
}
22 changes: 22 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ export const Application = () => {
[options, path]
);

useEffect(() => {
const onKeyboardNav = (e: KeyboardEvent) => {
switch (e.key) {
case "L":
if (e.ctrlKey && !e.altKey) {
e.preventDefault();
document.dispatchEvent(new Event("manual-change-dir"));
}
break;

default:
break;
}
};

document.addEventListener("keydown", onKeyboardNav);

return () => {
document.removeEventListener("keydown", onKeyboardNav);
};
}, []);

if (loading)
return <EmptyStatePanel loading />;

Expand Down
145 changes: 145 additions & 0 deletions src/dialogs/keyboardShortcutsHelp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';

import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import {
DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm
} from "@patternfly/react-core/dist/esm/components/DescriptionList/index";
import { Modal, ModalVariant } from "@patternfly/react-core/dist/esm/components/Modal";
import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
import { Flex, } from "@patternfly/react-core/dist/esm/layouts/Flex";

import cockpit from 'cockpit';
import { useDialogs } from 'dialogs';

const _ = cockpit.gettext;

export const KeyboardShortcutsHelp = () => {
const Dialogs = useDialogs();

const footer = (
<Button variant="secondary" onClick={Dialogs.close}>{_("Close")}</Button>
);

const toDescriptionListGroups = (item: [React.JSX.Element, string, string]) => {
return (
<DescriptionListGroup key={item[2] + "-listgroup"}>
<DescriptionListTerm>
{item[0]}
</DescriptionListTerm>
<DescriptionListDescription>
{item[1]}
</DescriptionListDescription>
</DescriptionListGroup>
);
};

const navShortcuts: Array<[React.JSX.Element, string, string]> = [
[
<kbd className="keystroke" key="go-up">
<kbd className="key">Alt</kbd> + <kbd className="key">{'\u{2191}'}</kbd>
</kbd>,
_("Go up a directory"),
"go-up",
], [
<kbd className="keystroke" key="go-back">
<kbd className="key">Alt</kbd> + <kbd className="key">{'\u{2190}'}</kbd>
</kbd>,
_("Go back"),
"go-back",
], [
<kbd className="keystroke" key="go-forward">
<kbd className="key">Alt</kbd> + <kbd className="key">{'\u{2192}'}</kbd>
</kbd>,
_("Go forward"),
"go-forward",
], [
<kbd className="keystroke" key="activate">
<kbd className="key">Alt</kbd> + <kbd className="key">{'\u{2193}'}</kbd>
</kbd>,
_("Activate selected item, enter directory"),
"activate",
], [
<kbd className="keystroke" key="activate-enter">
<kbd className="key">Enter</kbd>
</kbd>,
_("Activate selected item, enter directory"),
"activate-enter",
], [
<kbd className="keystroke" key="edit-path">
<kbd className="key">Ctrl</kbd> +
<kbd className="key">Shift</kbd> +
<kbd className="key">L</kbd>
</kbd>,
_("Edit path"),
"edit-path",
]
];

const editShortcuts: Array<[React.JSX.Element, string, string]> = [
[
<kbd className="key" key="rename">F2</kbd>,
_("Rename selected file or directory"),
"rename",
], [
<kbd className="keystroke" key="create-dir">
<kbd className="key">Shift</kbd> +
<kbd className="key" key="mkdir">N</kbd>
</kbd>,
_("Create new directory"),
"mkdir",
], [
<kbd className="keystroke" key="copy">
<kbd className="key">Ctrl</kbd> + <kbd className="key">C</kbd>
</kbd>,
_("Copy selected file or directory"),
"copy",
], [
<kbd className="keystroke" key="paste">
<kbd className="key">Ctrl</kbd> + <kbd className="key">V</kbd>
</kbd>,
_("Paste file or directory"),
"paste",
], [
<kbd className="keystroke" key="select-all">
<kbd className="key">Ctrl</kbd> + <kbd className="key">A</kbd>
</kbd>,
_("Select all"),
"select-all",
]
];

return (
<Modal
position="top"
title={_("Keyboard shortcuts")}
variant={ModalVariant.large}
className="shortcuts-dialog"
onClose={Dialogs.close}
footer={footer}
isOpen
>
<Flex>
<TextContent>
<Text component={TextVariants.h2}>{_("Navigation")}</Text>
<DescriptionList
isHorizontal
isFluid
isFillColumns
>
{navShortcuts.map(toDescriptionListGroups)}
</DescriptionList>
</TextContent>
<TextContent>
<Text component={TextVariants.h2}>{_("Editing")}</Text>
<DescriptionList
isHorizontal
isFluid
isFillColumns
>
{editShortcuts.map(toDescriptionListGroups)}
</DescriptionList>
</TextContent>
</Flex>
</Modal>
);
};
20 changes: 14 additions & 6 deletions src/files-breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import React, { useCallback, useEffect } from "react";

import { AlertVariant } from "@patternfly/react-core/dist/esm/components/Alert";
import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb";
Expand Down Expand Up @@ -252,6 +252,19 @@ export function FilesBreadcrumbs({ path }: { path: string }) {
const [editMode, setEditMode] = React.useState(false);
const [newPath, setNewPath] = React.useState<string | null>(null);

const enableEditMode = useCallback(() => {
setEditMode(true);
setNewPath(path);
}, [path]);

useEffect(() => {
document.addEventListener("manual-change-dir", enableEditMode);

return () => {
document.removeEventListener("manual-change-dir", enableEditMode);
};
}, [enableEditMode]);

const handleInputKey = (event: React.KeyboardEvent<HTMLInputElement>) => {
// Don't propogate navigation specific events
if (event.key === "ArrowDown" || event.key === "ArrowUp" ||
Expand All @@ -267,11 +280,6 @@ export function FilesBreadcrumbs({ path }: { path: string }) {
}
};

const enableEditMode = () => {
setEditMode(true);
setNewPath(path);
};

const changePath = () => {
setEditMode(false);
cockpit.assert(newPath !== null, "newPath cannot be null");
Expand Down
Loading

0 comments on commit eca07b3

Please sign in to comment.