Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
382e3ae
feat(KeyboardShortcuts): initial commit
maxpatiiuk Jul 13, 2024
1b4d5d6
feat(KeyboardShortcuts): complete base features
maxpatiiuk Jul 13, 2024
9309bd6
refactor(KeyboardShortcuts): move code into separate folder
maxpatiiuk Jul 13, 2024
979975a
refactor(KeyboardShortcuts): use key codes over key names
maxpatiiuk Jul 13, 2024
be5b560
refactor(KeyboardShortcuts): rename from "Keyboard"
maxpatiiuk Jul 13, 2024
98269a5
fix(KeyboardShortcuts): polish UI
maxpatiiuk Jul 14, 2024
09f8a31
feat(KeyboardShortcuts): begin integrating with UI
maxpatiiuk Jul 14, 2024
ec31b19
fix(KeyboardShortcuts): correctly localize shortcuts on windows
maxpatiiuk Jul 14, 2024
83fb8a5
feat(TreeView): improve layout
maxpatiiuk Jul 21, 2024
9e41157
feat(KeyboardShortcuts): Add shortcuts for forms, tree and queries
maxpatiiuk Jul 21, 2024
7f08bdd
fix(KeyboardShortcuts): prevent adding duplicate shortcuts
maxpatiiuk Dec 25, 2024
3b2afc1
fix(KeyboardShortcuts): don't permit binding to Shift+Tab
maxpatiiuk Dec 25, 2024
139d255
feat(KeyboardShortcuts): add custom URL shortcuts
maxpatiiuk Dec 26, 2024
580584e
fix(KeyboardShortcuts): respond to review feedback
maxpatiiuk Jul 26, 2025
ea892c2
refactor(QueryBuilder): self-review code
maxpatiiuk Jul 26, 2025
db1a62f
docs(KeyboardShortcuts): add instructions on adding new shortcuts
maxpatiiuk Jul 26, 2025
408db75
build(KeyboardShortcuts): avoid cyclical import
maxpatiiuk Jul 26, 2025
186cc9f
tests(Preferences): fix localization tests
maxpatiiuk Jul 26, 2025
29e1cfd
feat(keyboard-shortcuts): merge dormant shortcut branch
grantfitzsimmons Sep 14, 2026
e093dc0
fix(keyboard-shortcuts): resolve merge conflicts
grantfitzsimmons Sep 14, 2026
49b3f3e
fix(keyboard-shortcuts): import link localization
grantfitzsimmons Sep 14, 2026
8ab203c
fix(keyboard-shortcuts): restore preference paths
grantfitzsimmons Sep 14, 2026
a4d07d6
fix(keyboard-shortcuts): restore query builder shortcuts
grantfitzsimmons Sep 14, 2026
87ade76
fix(keyboard-shortcuts): restore tree shortcuts
grantfitzsimmons Sep 14, 2026
d911d75
feat(keyboard-shortcuts): expose user tool shortcuts
grantfitzsimmons Sep 14, 2026
aa8c127
feat(keyboard-shortcuts): show user tools in preferences
grantfitzsimmons Sep 14, 2026
3bc8c77
fix(preferences): balance navigation and content columns
grantfitzsimmons Sep 14, 2026
57bbbbe
fix(keyboard-shortcuts): separate user tools from custom pages
grantfitzsimmons Sep 14, 2026
bf417ee
feat(dialogs): add close and save shortcuts
grantfitzsimmons Sep 14, 2026
b067b8b
feat(keyboard-shortcuts): configure dialog shortcuts
grantfitzsimmons Sep 14, 2026
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
68 changes: 53 additions & 15 deletions specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type { LocalizedString } from 'typesafe-i18n';
import { commonText } from '../../localization/common';
import { formsText } from '../../localization/forms';
import type { RA } from '../../utils/types';
import { localized } from '../../utils/types';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import type { ViewDescription } from '../FormParse';
import type { cellAlign, cellVerticalAlign } from '../FormParse/cells';
import { userPreferences } from '../Preferences/userPreferences';
import { Button } from './Button';
import { className } from './className';
import type { icons } from './Icons';
Expand All @@ -19,23 +21,24 @@ const dataEntryButton = (
className: string,
title: LocalizedString,
icon: keyof typeof icons
) =>
function (
) => {
const component = (
props: Omit<TagProps<'button'>, 'children' | 'type'> & {
readonly onClick:
| ((event: React.MouseEvent<HTMLButtonElement>) => void)
| undefined;
}
): JSX.Element {
return (
<Button.Icon
className={`${className} ${props.className ?? ''}`}
icon={icon}
title={title}
{...props}
/>
);
};
): JSX.Element => (
<Button.Icon
className={`${className} ${props.className ?? ''}`}
icon={icon}
title={title}
{...props}
/>
);
Object.defineProperty(component, 'name', { value: icon });
return component;
};

export const columnDefinitionsToCss = (
columns: RA<number | undefined>,
Expand All @@ -54,6 +57,12 @@ export const columnDefinitionsToCss = (
* This is called DataEntry instead of Form because "Form" is already taken
*/

const DataEntryAdd = dataEntryButton(
className.dataEntryAdd,
commonText.add(),
'plus'
);

export const DataEntry = {
Grid: wrap<
'div',
Expand Down Expand Up @@ -145,7 +154,29 @@ export const DataEntry = {
})
),
SubFormTitle: wrap('DataEntry.SubFormTitle', 'h3', className.formTitle),
Add: dataEntryButton(className.dataEntryAdd, commonText.add(), 'plus'),
Add({
enableShortcut,
onClick: handleClick,
title = commonText.add(),
...rest
}: Omit<Parameters<typeof DataEntryAdd>[0], 'onClick'> & {
readonly onClick: (() => void) | undefined;
readonly enableShortcut: boolean;
}): JSX.Element {
const addButtonShortcut = userPreferences.useKeyboardShortcut(
'form',
'recordSet',
'addResource',
enableShortcut && rest.disabled !== true ? handleClick : undefined
);
return (
<DataEntryAdd
title={`${title}${addButtonShortcut}`}
onClick={handleClick}
{...rest}
/>
);
},
View: dataEntryButton(className.dataEntryView, commonText.view(), 'eye'),
Edit: dataEntryButton(className.dataEntryEdit, commonText.edit(), 'pencil'),
Clone: dataEntryButton(
Expand All @@ -170,14 +201,21 @@ export const DataEntry = {
readonly className?: string;
readonly resource: SpecifyResource<AnySchema> | undefined;
}): JSX.Element | null {
const ref = React.useRef<HTMLAnchorElement>(null);
const keyboardShortcut = userPreferences.useKeyboardShortcut(
'form',
'queryComboBox',
'openRelatedRecordInNewTab',
resource === undefined ? undefined : (): void => ref.current?.click()
);
return typeof resource === 'object' && !resource.isNew() ? (
<Link.NewTab
aria-label={commonText.openInNewTab()}
className={`${className.dataEntryVisit} ${localClassName}`}
forwardRef={ref}
href={resource.viewUrl()}
title={commonText.openInNewTab()}
title={localized(`${commonText.openInNewTab()}${keyboardShortcut}`)}
/>
) : null;
},
};
/* eslint-enable @typescript-eslint/naming-convention */
10 changes: 4 additions & 6 deletions specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const icons = {
collection: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" /></svg>,
cube: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 17a1 1 0 001.447.894l4-2A1 1 0 0017 15V9.236a1 1 0 00-1.447-.894l-4 2a1 1 0 00-.553.894V17zM15.211 6.276a1 1 0 000-1.788l-4.764-2.382a1 1 0 00-.894 0L4.789 4.488a1 1 0 000 1.788l4.764 2.382a1 1 0 00.894 0l4.764-2.382zM4.447 8.342A1 1 0 003 9.236V15a1 1 0 00.553.894l4 2A1 1 0 009 17v-5.764a1 1 0 00-.553-.894l-4-2z" /></svg>,
cubeTransparent: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1zM5.618 4.504a1 1 0 01-.372 1.364L5.016 6l.23.132a1 1 0 11-.992 1.736L4 7.723V8a1 1 0 01-2 0V6a.996.996 0 01.52-.878l1.734-.99a1 1 0 011.364.372zm8.764 0a1 1 0 011.364-.372l1.733.99A1.002 1.002 0 0118 6v2a1 1 0 11-2 0v-.277l-.254.145a1 1 0 11-.992-1.736l.23-.132-.23-.132a1 1 0 01-.372-1.364zm-7 4a1 1 0 011.364-.372L10 8.848l1.254-.716a1 1 0 11.992 1.736L11 10.58V12a1 1 0 11-2 0v-1.42l-1.246-.712a1 1 0 01-.372-1.364zM3 11a1 1 0 011 1v1.42l1.246.712a1 1 0 11-.992 1.736l-1.75-1A1 1 0 012 14v-2a1 1 0 011-1zm14 0a1 1 0 011 1v2a1 1 0 01-.504.868l-1.75 1a1 1 0 11-.992-1.736L16 13.42V12a1 1 0 011-1zm-9.618 5.504a1 1 0 011.364-.372l.254.145V16a1 1 0 112 0v.277l.254-.145a1 1 0 11.992 1.736l-1.735.992a.995.995 0 01-1.022 0l-1.735-.992a1 1 0 01-.372-1.364z" fillRule="evenodd" /></svg>,
cursorClick: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6.672 1.911a1 1 0 10-1.932.518l.259.966a1 1 0 001.932-.518l-.26-.966zM2.429 4.74a1 1 0 10-.517 1.932l.966.259a1 1 0 00.517-1.932l-.966-.26zm8.814-.569a1 1 0 00-1.415-1.414l-.707.707a1 1 0 101.415 1.415l.707-.708zm-7.071 7.072l.707-.707A1 1 0 003.465 9.12l-.708.707a1 1 0 001.415 1.415zm3.2-5.171a1 1 0 00-1.3 1.3l4 10a1 1 0 001.823.075l1.38-2.759 3.018 3.02a1 1 0 001.414-1.415l-3.019-3.02 2.76-1.379a1 1 0 00-.076-1.822l-10-4z" fillRule="evenodd" /></svg>,
database: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z" /><path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z" /><path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z" /></svg>,
document: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" fillRule="evenodd" /></svg>,
documentReport: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v7a1 1 0 102 0V8z" fillRule="evenodd" /></svg>,
Expand All @@ -86,13 +87,10 @@ export const icons = {
exclamationCircle: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" fillRule="evenodd" /></svg>,
externalLink: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" /><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" /></svg>,
eye: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /><path clipRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" fillRule="evenodd" /></svg>,
filter: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" fillRule="evenodd" /></svg>,
fingerPrint: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6.625 2.655A9 9 0 0119 11a1 1 0 11-2 0 7 7 0 00-9.625-6.492 1 1 0 11-.75-1.853zM4.662 4.959A1 1 0 014.75 6.37 6.97 6.97 0 003 11a1 1 0 11-2 0 8.97 8.97 0 012.25-5.953 1 1 0 011.412-.088z" fillRule="evenodd" /><path clipRule="evenodd" d="M5 11a5 5 0 1110 0 1 1 0 11-2 0 3 3 0 10-6 0c0 1.677-.345 3.276-.968 4.729a1 1 0 11-1.838-.789A9.964 9.964 0 005 11zm8.921 2.012a1 1 0 01.831 1.145 19.86 19.86 0 01-.545 2.436 1 1 0 11-1.92-.558c.207-.713.371-1.445.49-2.192a1 1 0 011.144-.83z" fillRule="evenodd" /><path clipRule="evenodd" d="M10 10a1 1 0 011 1c0 2.236-.46 4.368-1.29 6.304a1 1 0 01-1.838-.789A13.952 13.952 0 009 11a1 1 0 011-1z" fillRule="evenodd" /></svg>,
gallery:<svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clipRule="evenodd" d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" fillRule="evenodd" />
</svg>,
globe: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clipRule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.56-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.56.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" fillRule="evenodd" />
</svg>,
gallery:<svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" fillRule="evenodd" /></svg>,
globe: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.56-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.56.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" fillRule="evenodd" /></svg>,
hashtag: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M9.243 3.03a1 1 0 01.727 1.213L9.53 6h2.94l.56-2.243a1 1 0 111.94.486L14.53 6H17a1 1 0 110 2h-2.97l-1 4H15a1 1 0 110 2h-2.47l-.56 2.242a1 1 0 11-1.94-.485L10.47 14H7.53l-.56 2.242a1 1 0 11-1.94-.485L5.47 14H3a1 1 0 110-2h2.97l1-4H5a1 1 0 110-2h2.47l.56-2.243a1 1 0 011.213-.727zM9.03 8l-1 4h2.938l1-4H9.031z" fillRule="evenodd" /></svg>,
// This icon is not from Heroicons. It was drawn by @grantfitzsimmons
history: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.043,6.984c-0.382-0.935-0.942-1.753-1.665-2.433c-0.767-0.722-1.572-1.249-2.459-1.611 c-0.911-0.391-1.94-0.597-2.979-0.597c-1.147,0-2.207,0.251-3.239,0.768C5.87,3.526,5.136,4.05,4.512,4.671V3.746 c0-0.47-0.397-0.867-0.867-0.867c-0.486,0-0.867,0.381-0.867,0.867v2.866c0,0.577,0.469,1.046,1.045,1.046h2.825 c0.51,0,0.909-0.399,0.909-0.908c0-0.471-0.355-0.826-0.826-0.826H5.752C6.191,5.5,6.688,5.131,7.258,4.802 c0.797-0.46,1.688-0.71,2.579-0.723c1.683-0.052,3.085,0.532,4.291,1.698c1.178,1.139,1.775,2.532,1.775,4.141 c0,1.729-0.555,3.125-1.696,4.267c-1.18,1.18-2.616,1.778-4.267,1.778c-1.647,0-3.016-0.568-4.181-1.733 c-0.983-1.022-1.542-2.259-1.661-3.685C4.041,10.084,3.677,9.75,3.233,9.75c-0.257,0-0.499,0.104-0.662,0.288 c-0.158,0.176-0.23,0.406-0.205,0.638c0.127,1.823,0.858,3.413,2.171,4.727c1.496,1.495,3.313,2.254,5.402,2.254 c0.977,0,1.947-0.18,2.983-0.558c0.977-0.443,1.805-1.001,2.46-1.655c0.74-0.739,1.282-1.547,1.659-2.469 c0.395-0.966,0.595-1.979,0.595-3.016C17.638,8.937,17.443,7.964,17.043,6.984z"/> <path d="M10.022,5.595H9.94c-0.426,0-0.785,0.359-0.785,0.784v3.333c0,0.411,0.158,0.796,0.445,1.084l2.345,2.303 c0.175,0.175,0.402,0.264,0.637,0.264c0.2,0,0.405-0.064,0.587-0.194c0.258-0.181,0.328-0.4,0.341-0.553 c0.021-0.251-0.09-0.519-0.31-0.737l-2.31-2.231V6.462C10.89,5.976,10.509,5.595,10.022,5.595z"/></svg>,
Expand Down
11 changes: 10 additions & 1 deletion specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type { LocalizedString } from 'typesafe-i18n';

import { commonText } from '../../localization/common';
import type { IR, RA, RR } from '../../utils/types';
import { className } from './className';
import type { IconProps } from './Icons';
Expand Down Expand Up @@ -46,7 +47,15 @@ export const Link = {
children: (
<>
{props.children}
<span aria-hidden>{icons.externalLink}</span>
<span
title={
(props.children === undefined ? props.title : undefined) ??
commonText.opensInNewTab()
}
>
<span className="sr-only">{commonText.opensInNewTab()}</span>
{icons.externalLink}
</span>
</>
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ snapshot(DataEntry.Footer, { children: 'Test' });
snapshot(DataEntry.SubForm, { children: 'Test' });
snapshot(DataEntry.SubFormHeader, { children: 'Test' });
snapshot(DataEntry.SubFormTitle, { children: 'Test' });
snapshot(DataEntry.Add, { onClick: f.never });
snapshot(DataEntry.Add, { onClick: f.never, enableShortcut: true });
snapshot(DataEntry.View, { onClick: f.never });
snapshot(DataEntry.Edit, { onClick: f.never });
snapshot(DataEntry.Clone, { onClick: f.never });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ exports[`H3 renders without errors 1`] = `
exports[`Key renders without errors 1`] = `
<DocumentFragment>
<kbd
class="bg-gray-200 border-1 dark:border-none dark:bg-neutral-700 rounded-sm mx-1 p-0.5"
class="bg-gray-200 border-1 dark:border-none dark:bg-neutral-700 rounded-sm mx-1 p-0.5 text-xl"
>
View
</kbd>
Expand Down
2 changes: 1 addition & 1 deletion specifyweb/frontend/js_src/lib/components/Atoms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const Summary = wrap<
export const Key = wrap(
'Key',
'kbd',
'bg-gray-200 border-1 dark:border-none dark:bg-neutral-700 rounded-sm mx-1 p-0.5'
'bg-gray-200 border-1 dark:border-none dark:bg-neutral-700 rounded-sm mx-1 p-0.5 text-xl'
);

const defaultOneRem = 16;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function COJODialog({

return (
<>
<DataEntry.Add onClick={handleOpen} />
<DataEntry.Add enableShortcut onClick={handleOpen} />
{isOpen && (
<Dialog
buttons={commonText.cancel()}
Expand All @@ -100,6 +100,7 @@ export function COJODialog({
<TableIcon label name={table.name} />
{table.label}
<DataEntry.Add
enableShortcut
onClick={(): void => {
setState('Add');
setResourceTable(table);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ export function FormTable<SCHEMA extends AnySchema>({
{isExpanded[resource.cid] === true && (
<FormMeta
className="flex-1"
enableKeyboardShortcut={false}
resource={resource}
viewDescription={expandedViewDefinition}
/>
Expand Down Expand Up @@ -594,6 +595,7 @@ export function FormTable<SCHEMA extends AnySchema>({
) : undefined}
{hasTablePermission(relationship.relatedTable.name, 'create') ? (
<DataEntry.Add
enableShortcut={dialog !== false}
onClick={(): void => {
const resource = new relationship.relatedTable.Resource();
handleAddResources([resource]);
Expand Down
15 changes: 13 additions & 2 deletions specifyweb/frontend/js_src/lib/components/FormMeta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ProtectedAction,
ProtectedTool,
} from '../Permissions/PermissionDenied';
import { userPreferences } from '../Preferences/userPreferences';
import { canMerge } from '../QueryBuilder/Results';
import { UnloadProtectsContext } from '../Router/UnloadProtect';
import { AutoNumbering } from './AutoNumbering';
Expand All @@ -44,30 +45,40 @@ import { ShareRecord } from './ShareRecord';
import { SubViewMeta } from './SubViewMeta';

/**
* Form preferences host context aware user preferences and other meta-actions.
* Form preferences, context aware user preferences, and other meta-actions.
* List of available features: https://github.com/specify/specify7/issues/1330
*/
export function FormMeta({
resource,
className,
viewDescription,
enableKeyboardShortcut,
}: {
readonly resource: SpecifyResource<AnySchema> | undefined;
readonly className?: string;
readonly viewDescription: ViewDescription | undefined;
readonly enableKeyboardShortcut: boolean;
}): JSX.Element | null {
const [isOpen, _, handleClose, handleToggle] = useBooleanState();
const [isReadOnly = false] = useCachedState('forms', 'readOnlyMode');
const subView = React.useContext(SubViewContext);
const isInFormEditor = React.useContext(InFormEditorContext);

const keyboardShortcut = userPreferences.useKeyboardShortcut(
'form',
'actions',
'openFormMeta',
enableKeyboardShortcut && resource !== undefined ? handleToggle : undefined
);

return isInFormEditor && typeof viewDescription === 'object' ? (
<FormEditorLink viewDescription={viewDescription} />
) : typeof resource === 'object' ? (
<>
<Button.Small
aria-label={formsText.formMeta()}
className={className}
title={formsText.formMeta()}
title={`${formsText.formMeta()}${keyboardShortcut}`}
onClick={handleToggle}
>
{icons.cog}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export function CollectionOneToManyPlugin({
typeof data === 'object' ? (
<DataEntry.Add
aria-pressed={state.type === 'SearchState'}
enableShortcut={false}
onClick={(): void =>
setState(
state.type === 'SearchState'
Expand Down
Loading
Loading