Settings keyboard shortcuts (#19836)

ref DES-166

Accessing settings via a keyboard shortcut is a great productivity
booster for advanced users and it is missing from Ghost today.
This commit is contained in:
Peter Zimon 2024-03-13 08:46:22 +01:00 committed by GitHub
parent 19da5c6af4
commit 9d9707e6f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 93 additions and 12 deletions

View File

@ -127,6 +127,9 @@ const Select: React.FC<SelectProps> = ({
document.activeElement.blur();
}
setFocusState(false);
// Prevent the event from bubbling up to the window level
event.stopPropagation();
}
};

View File

@ -107,6 +107,9 @@ const Modal: React.FC<ModalProps> = ({
});
}
});
// Prevent the event from bubbling up to the window level
event.stopPropagation();
}
};

View File

@ -2,7 +2,7 @@ import ExitSettingsButton from './components/ExitSettingsButton';
import Settings from './components/Settings';
import Sidebar from './components/Sidebar';
import Users from './components/settings/general/Users';
import {Heading, topLevelBackdropClasses} from '@tryghost/admin-x-design-system';
import {Heading, confirmIfDirty, topLevelBackdropClasses, useGlobalDirtyState} from '@tryghost/admin-x-design-system';
import {ReactNode, useEffect} from 'react';
import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users';
import {toast} from 'react-hot-toast';
@ -11,7 +11,7 @@ import {useRouting} from '@tryghost/admin-x-framework/routing';
const Page: React.FC<{children: ReactNode}> = ({children}) => {
return <>
<div className='sticky top-0 z-30 bg-white px-[5vmin] py-4 dark:bg-grey-975 tablet:fixed tablet:bg-transparent tablet:px-6 dark:tablet:bg-transparent xl:p-12' id="done-button-container">
<div className='fixed right-0 top-2 z-50 flex justify-end p-8 dark:bg-grey-975 tablet:fixed tablet:top-0 tablet:bg-transparent tablet:px-8 dark:tablet:bg-transparent' id="done-button-container">
<ExitSettingsButton />
</div>
<div className="w-full dark:bg-grey-975 tablet:fixed tablet:left-0 tablet:top-0 tablet:flex tablet:h-full" id="admin-x-settings-content">
@ -23,6 +23,25 @@ const Page: React.FC<{children: ReactNode}> = ({children}) => {
const MainContent: React.FC = () => {
const {currentUser} = useGlobalData();
const {route, updateRoute, loadingModal} = useRouting();
const {isDirty} = useGlobalDirtyState();
const navigateAway = () => {
window.location.hash = '/dashboard';
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
confirmIfDirty(isDirty, navigateAway);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
useEffect(() => {
// resets any toasts that may have been left open on initial load
@ -53,12 +72,12 @@ const MainContent: React.FC = () => {
return (
<Page>
{loadingModal && <div className={`fixed inset-0 z-40 h-[calc(100vh-55px)] w-[100vw] tablet:h-[100vh] ${topLevelBackdropClasses}`} />}
<div className="no-scrollbar fixed inset-x-0 top-[52px] z-[35] flex-1 basis-[320px] bg-white px-8 pb-8 dark:bg-grey-975 tablet:relative tablet:inset-x-auto tablet:top-auto tablet:h-full tablet:overflow-y-scroll tablet:bg-grey-50 tablet:pb-0 dark:tablet:bg-black" id="admin-x-settings-sidebar-scroller">
<div className="no-scrollbar fixed inset-x-0 top-0 z-[35] flex-1 basis-[320px] bg-white p-8 dark:bg-grey-975 tablet:relative tablet:inset-x-auto tablet:top-auto tablet:h-full tablet:overflow-y-scroll tablet:bg-grey-50 tablet:py-0 dark:tablet:bg-black" id="admin-x-settings-sidebar-scroller">
<div className="relative w-full">
<Sidebar />
</div>
</div>
<div className="relative h-full flex-1 overflow-y-scroll pt-11 tablet:basis-[800px]" id="admin-x-settings-scroller">
<div className="relative h-full flex-1 overflow-y-scroll bg-white pt-11 dark:bg-black tablet:basis-[800px]" id="admin-x-settings-scroller">
<Settings />
</div>
</Page>

View File

@ -9,7 +9,7 @@ const ExitSettingsButton: React.FC = () => {
};
return (
<Button data-testid="exit-settings" id="done-button" label='&larr; Done' link={true} onClick={() => confirmIfDirty(isDirty, navigateAway)} />
<Button className='text-grey-700 hover:!text-black' data-testid="exit-settings" icon='close' id="done-button" label='' link={true} title='Close (ESC)' onClick={() => confirmIfDirty(isDirty, navigateAway)} />
);
};

View File

@ -10,7 +10,7 @@ import SiteSettings from './settings/site/SiteSettings';
const Settings: React.FC = () => {
return (
<>
<div className='mb-[40vh] px-8 pt-8 tablet:max-w-[760px] tablet:px-14 xl:pt-0'>
<div className='mb-[40vh] px-8 pt-16 tablet:max-w-[760px] tablet:px-14 tablet:pt-0'>
<GeneralSettings />
<SiteSettings />
<MembershipSettings />

View File

@ -78,6 +78,26 @@ const Sidebar: React.FC = () => {
}
}, [checkVisible, setNoResult, filter]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && filter) {
// Blur the field
searchInputRef.current?.blur();
// Prevent the event from bubbling up to the window level
event.stopPropagation();
}
};
// Add the event listener to the searchInputRef field
searchInputRef.current?.addEventListener('keydown', handleKeyDown);
// Clean up the event listener when the component unmounts
return () => {
searchInputRef.current?.removeEventListener('keydown', handleKeyDown);
};
}, [filter]);
const {settings, config} = useGlobalData();
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
const hasStripeEnabled = checkStripeEnabled(settings || [], config || {});
@ -107,12 +127,12 @@ const Sidebar: React.FC = () => {
return (
<div className='ml-auto flex w-full flex-col pt-0 tablet:max-w-[240px]' data-testid="sidebar">
<div className='sticky top-0 flex content-stretch items-end bg-grey-50 dark:bg-grey-975 tablet:h-28 dark:tablet:bg-black xl:h-20'>
<div className='sticky top-0 flex content-stretch items-end dark:bg-grey-975 tablet:h-20 tablet:bg-grey-50 dark:tablet:bg-black xl:h-20'>
<div className='relative w-full'>
<Icon className='absolute left-3 top-3 z-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
<TextField
autoComplete="off"
className='flex h-10 w-full items-center rounded-lg border border-transparent bg-white px-[33px] py-1.5 text-[14px] shadow-[0_0_1px_rgba(21,23,26,0.25),0_1px_3px_rgba(0,0,0,0.03),0_8px_10px_-12px_rgba(0,0,0,.1)] transition-colors hover:shadow-sm focus:border-green focus:bg-white focus:shadow-[0_0_0_2px_rgba(48,207,67,0.25)] focus:outline-2 dark:border-transparent dark:bg-grey-925 dark:text-white dark:placeholder:text-grey-800 dark:focus:border-green dark:focus:bg-grey-950'
className='mr-12 flex h-10 w-full items-center rounded-lg border border-transparent bg-white px-[33px] py-1.5 text-[14px] shadow-[0_0_1px_rgba(21,23,26,0.25),0_1px_3px_rgba(0,0,0,0.03),0_8px_10px_-12px_rgba(0,0,0,.1)] transition-colors hover:shadow-sm focus:border-green focus:bg-white focus:shadow-[0_0_0_2px_rgba(48,207,67,0.25)] focus:outline-2 dark:border-transparent dark:bg-grey-925 dark:text-white dark:placeholder:text-grey-800 dark:focus:border-green dark:focus:bg-grey-950 tablet:mr-0'
containerClassName='w-100'
inputRef={searchInputRef}
placeholder="Search settings"

View File

@ -11,11 +11,41 @@
@media (max-width: 860px) {
.gh-update-banner ~ * #admin-x-settings-sidebar-scroller {
position: sticky;
margin-bottom: -60px;
position: fixed;
top: 48px;
}
.gh-update-banner ~ * #done-button-container {
top: 0;
top: 60px;
}
}
@media (max-width: 600px) {
.gh-update-banner ~ * #admin-x-settings-sidebar-scroller {
position: fixed;
top: 72px;
}
.gh-update-banner ~ * #done-button-container {
top: 84px;
}
}
.admin-x-base {
opacity: 1;
animation-name: openAnimation;
animation-iteration-count: 1;
animation-timing-function: ease;
animation-duration: 0.35s;
}
@keyframes openAnimation {
0% {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0px) scale(1.0);
}
}

View File

@ -100,7 +100,7 @@
</div>
<div class="flex items-center pe-all">
{{#if (or (gh-user-can-admin this.session.user) this.session.user.isEditor)}}
<LinkTo class="gh-nav-bottom-tabicon" @route="settings-x" @current-when={{this.isSettingsRoute}} data-test-nav="settings">{{svg-jar "settings"}}</LinkTo>
<LinkTo class="gh-nav-bottom-tabicon" @route="settings-x" @current-when={{this.isSettingsRoute}} data-test-nav="settings">{{svg-jar "settings" title="Settings (CTRL/⌘ + ,)"}}</LinkTo>
{{/if}}
<div class="nightshift-toggle-container">
<div class="nightshift-toggle {{if this.feature.nightShift "on"}}" {{action (toggle "nightShift" this.feature)}}>

View File

@ -57,6 +57,7 @@ export default class Main extends Component.extend(ShortcutsMixin) {
let shortcuts = {};
shortcuts[`${ctrlOrCmd}+k`] = {action: 'openSearchModal'};
shortcuts[`${ctrlOrCmd}+,`] = {action: 'openSettings'};
this.shortcuts = shortcuts;
}
@ -98,6 +99,11 @@ export default class Main extends Component.extend(ShortcutsMixin) {
return this.modals.open(SearchModal);
}
@action
openSettings() {
this.router.transitionTo('settings-x');
}
@action
toggleBillingModal() {
this.billing.openBillingWindow(this.router.currentURL);