⌨️ Keyboard shortcuts/navigation for create and upload (#736)
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { addShortcut, removeShortcut } from 'app/features/global/services/shortcut-service';
|
||||
|
||||
export type ButtonTheme = 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white' | 'green';
|
||||
export type ButtonTheme =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'danger'
|
||||
| 'default'
|
||||
| 'outline'
|
||||
| 'dark'
|
||||
| 'white'
|
||||
| 'green';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
theme?: ButtonTheme;
|
||||
@@ -12,6 +21,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
children?: React.ReactNode;
|
||||
shortcut?: string;
|
||||
testClassId?: string;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 ' +
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -166,6 +166,7 @@ export default () => {
|
||||
|
||||
<Button
|
||||
onClick={() => uploadItemModal()}
|
||||
shortcut='U'
|
||||
size="lg"
|
||||
theme="primary"
|
||||
className="w-full mb-2 justify-center"
|
||||
@@ -176,6 +177,7 @@ export default () => {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => openItemModal()}
|
||||
shortcut='C'
|
||||
size="lg"
|
||||
theme="secondary"
|
||||
className="w-full mb-2 justify-center"
|
||||
|
||||
Reference in New Issue
Block a user