Skip to content

Commit

Permalink
⌨️ Keyboard shortcuts/navigation for create and upload (#736)
Browse files Browse the repository at this point in the history
  • Loading branch information
MontaGhanmy authored Dec 19, 2024
1 parent 946fabb commit 57dfbaf
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 13 deletions.
50 changes: 44 additions & 6 deletions tdrive/frontend/src/app/atoms/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import React, { useEffect } from 'react';
import _ from 'lodash';

export type ButtonTheme = 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white' | 'green';
import { addShortcut, removeShortcut } from 'app/features/global/services/shortcut-service';

export type ButtonTheme =
| 'primary'
| 'secondary'
| 'danger'
| 'default'
| 'outline'
| 'dark'
| 'white'
| 'green';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
theme?: ButtonTheme;
Expand All @@ -12,6 +21,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
disabled?: boolean;
children?: React.ReactNode;
shortcut?: string;
testClassId?: string;
}

Expand Down Expand Up @@ -43,9 +53,7 @@ export const Button = (props: ButtonProps) => {
className =
'text-zinc-300 border-0 bg-zinc-900 hover:bg-zinc-800 hover:text-white active:bg-zinc-900';

if (props.theme === 'green')
className =
'text-zinc-300 border-0 bg-green-700';
if (props.theme === 'green') className = 'text-zinc-300 border-0 bg-green-700';

if (disabled) className += ' opacity-50 pointer-events-none';

Expand All @@ -59,10 +67,31 @@ export const Button = (props: ButtonProps) => {
else className = className + ' w-9 !p-0 justify-center';
}

useEffect(() => {
const handler = (event?: KeyboardEvent) => {
// Disable default behavior for the shortcut
if (event) {
event.preventDefault();
event.stopPropagation();
}
props.onClick && props.onClick({} as any);
};

const shortcut = props.shortcut || '';

// Add shortcut with default behavior prevention
addShortcut({
shortcut,
handler: event => {
handler(event as KeyboardEvent);
},
});
}, [props.onClick, props.shortcut]);
const testId = props.testClassId ? `testid:${props.testClassId}` : '';

return (
<button
aria-keyshortcuts={props.shortcut}
type="button"
className={
' inline-flex items-center px-4 py-2 border font-medium rounded-md focus:outline-none ' +
Expand Down Expand Up @@ -108,6 +137,15 @@ export const Button = (props: ButtonProps) => {
/>
)}
{props.children}
{props.shortcut && props.theme !== 'white' && (
<span
className={`ml-2 text-xs w-5 h-5 leading-[22px] text-center rounded-sm opacity-100 ${
props.theme === 'primary' ? 'text-gray-200 bg-[#004591]' : 'text-gray-400 bg-gray-200'
}`}
>
{props.shortcut}
</span>
)}
</button>
);
};
9 changes: 6 additions & 3 deletions tdrive/frontend/src/app/atoms/modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Fragment, ReactNode, useCallback, useEffect, useState } from 'react';
import { atom, useRecoilState } from 'recoil';
import { DismissIcon } from '../icons-colored';
import { Title } from '../text';
import { Button } from '@atoms/button/button';

const ModalsCountState = atom({
key: 'ModalsState',
Expand Down Expand Up @@ -126,13 +127,15 @@ export const Modal = (props: {
>
{props.closable !== false && (
<div className="absolute top-0 right-0 pt-4 pr-4">
<button
<Button
type="button"
className="hover:opacity-75 focus:outline-none "
onClick={() => props.onClose && props.onClose()}
shortcut="esc"
theme="white"
>
{props.closeIcon ? props.closeIcon : <DismissIcon />}
</button>
</Button>
</div>
)}
{didOpenOnce && props.children}
Expand Down Expand Up @@ -177,7 +180,7 @@ export const ModalContent = (props: {
}
>
<Title className="pr-8 overflow-hidden text-ellipsis">{props.title}</Title>
<div className="mt-2">
<div className="mt-4">
<p className="text-sm text-gray-500">{props.text || ''}</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,26 @@ export const defaultShortcutsMap = {
};

export const addShortcut = (shortcut: ShortcutType | ShortcutType[]) => {
return shortcuts.add(shortcut);
return shortcuts.add({
...shortcut,
handler: (event: any) => {
const target = event.target as HTMLElement;
if (
['input', 'textarea'].includes(target.tagName.toLowerCase()) ||
target.isContentEditable
) {
return;
} else {
if (shortcut instanceof Array) {
shortcut.forEach(s => {
s.handler && s.handler(event);
});
} else {
shortcut.handler && shortcut.handler(event);
}
}
},
});
};

export const removeShortcut = (shortcut: ShortcutType | ShortcutType[]) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,21 @@ export const CreateModal = ({
};

const CreateModalOption = (props: { testClassId?: string; icon: ReactNode; text: string; onClick: () => void }) => {
// on press enter
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
props.onClick();
}
};

const testId = props.testClassId ? `testid:${props.testClassId}` : '';

return (
<div
onClick={props.onClick}
className={`flex flex-row p-4 dark:bg-zinc-900 dark:text-white bg-zinc-100 hover:bg-opacity-75 cursor-pointer rounded-md m-2 ${testId}`}
className={`flex flex-row p-4 dark:bg-zinc-900 dark:text-white bg-zinc-100 hover:bg-opacity-75 cursor-pointer rounded-md m-2 focus:bg-zinc-800 dark:focus:bg-zinc-800 outline-none focus:border-none ${testId}`}
tabIndex={0}
onKeyUp={handleKeyPress}
>
<div className="flex items-center justify-center">{props.icon}</div>
<div className="grow flex items-center ml-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,25 @@ export const UploadModal = ({
);
};

const CreateModalOption = (props: { icon: ReactNode; text: string; onClick: () => void; testClassId?: string; }) => {
const CreateModalOption = (props: {
icon: ReactNode;
text: string;
onClick: () => void;
testClassId?: string;
}) => {
// on press enter
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter') {
props.onClick();
}
};

return (
<div
onClick={props.onClick}
className={`flex flex-row p-4 dark:bg-zinc-900 dark:text-white bg-zinc-100 hover:bg-opacity-75 cursor-pointer rounded-md m-2 testid:${props.testClassId}`}
className={`flex flex-row p-4 dark:bg-zinc-900 dark:text-white bg-zinc-100 hover:bg-opacity-75 cursor-pointer rounded-md m-2 focus:bg-zinc-800 dark:focus:bg-zinc-800 outline-none focus:border-none testid:${props.testClassId}`}
tabIndex={0}
onKeyUp={handleKeyPress}
>
<div className="flex items-center justify-center">{props.icon}</div>
<div className="grow flex items-center ml-2">
Expand Down
2 changes: 2 additions & 0 deletions tdrive/frontend/src/app/views/client/side-bar/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export default () => {

<Button
onClick={() => uploadItemModal()}
shortcut='U'
size="lg"
theme="primary"
className="w-full mb-2 justify-center"
Expand All @@ -176,6 +177,7 @@ export default () => {
</Button>
<Button
onClick={() => openItemModal()}
shortcut='C'
size="lg"
theme="secondary"
className="w-full mb-2 justify-center"
Expand Down

0 comments on commit 57dfbaf

Please sign in to comment.