From c4773b946b18063fb07949fe4582c46b7acb76e8 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Mon, 25 Sep 2023 15:22:10 +0300 Subject: [PATCH] AdminX handling '/' in textfields vs. searchbar shortcut (#18283) refs. https://github.com/TryGhost/Product/issues/3349 - We've added a keyboard shortcut '/' to focus in on the searchfield in AdminX. However this didn't handle the case when the focus is already in a textfield and when tried to enter e.g. "https://", then at the '/' character it focused on the searchfield. --------- Co-authored-by: Ronald Langeveld --- apps/admin-x-settings/.storybook/preview.tsx | 6 ++- apps/admin-x-settings/src/App.tsx | 23 +++++++----- .../admin-x-ds/global/form/CodeEditorView.tsx | 12 ++++++ .../src/admin-x-ds/global/form/HtmlEditor.tsx | 19 ++++++++-- .../src/admin-x-ds/global/form/TextArea.tsx | 12 ++++++ .../global/form/TextField.stories.tsx | 6 ++- .../src/admin-x-ds/global/form/TextField.tsx | 18 ++++++++- .../admin-x-ds/global/form/URLTextField.tsx | 4 ++ .../providers/DesignSystemProvider.tsx | 37 +++++++++++++++++++ .../src/components/Sidebar.tsx | 6 ++- 10 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 apps/admin-x-settings/src/admin-x-ds/providers/DesignSystemProvider.tsx diff --git a/apps/admin-x-settings/.storybook/preview.tsx b/apps/admin-x-settings/.storybook/preview.tsx index 0dbc4c1766..e24b544c09 100644 --- a/apps/admin-x-settings/.storybook/preview.tsx +++ b/apps/admin-x-settings/.storybook/preview.tsx @@ -2,6 +2,8 @@ import React from 'react'; import '../src/styles/demo.css'; import type { Preview } from "@storybook/react"; +import '../src/admin-x-ds/providers/DesignSystemProvider'; +import DesignSystemProvider from '../src/admin-x-ds/providers/DesignSystemProvider'; const preview: Preview = { parameters: { @@ -29,7 +31,9 @@ const preview: Preview = { background: (scheme === 'dark' ? '#131416' : '') }}> {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} - + + + ); }, ], diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index ae793d2129..51ee40b82c 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -1,3 +1,4 @@ +import DesignSystemProvider from './admin-x-ds/providers/DesignSystemProvider'; import GlobalDataProvider from './components/providers/GlobalDataProvider'; import MainContent from './MainContent'; import NiceModal from '@ebay/nice-modal-react'; @@ -53,16 +54,18 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, t -
- - - - -
+ +
+ + + + +
+
diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/CodeEditorView.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/CodeEditorView.tsx index 6b6a1c6153..c2ff1293c6 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/CodeEditorView.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/CodeEditorView.tsx @@ -5,6 +5,7 @@ import React, {forwardRef, useEffect, useId, useRef, useState} from 'react'; import clsx from 'clsx'; import {EditorView} from '@codemirror/view'; import {Extension} from '@codemirror/state'; +import {useFocusContext} from '../../providers/DesignSystemProvider'; export interface CodeEditorProps extends Omit { title?: string; @@ -43,6 +44,15 @@ const CodeEditorView = forwardRef(function const sizeRef = useRef(null); const [width, setWidth] = useState(100); const [resolvedExtensions, setResolvedExtensions] = React.useState(null); + const {setFocusState} = useFocusContext(); + + const handleFocus = () => { + setFocusState(true); + }; + + const handleBlur = () => { + setFocusState(false); + }; useEffect(() => { Promise.all(extensions).then(setResolvedExtensions); @@ -76,7 +86,9 @@ const CodeEditorView = forwardRef(function height={height === 'full' ? '100%' : height} theme={theme} value={value} + onBlur={handleBlur} onChange={onChange} + onFocus={handleFocus} {...props} /> {title && {title}} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx index 5ce6b5ca85..72155d35d2 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/HtmlEditor.tsx @@ -1,5 +1,6 @@ import * as Sentry from '@sentry/react'; import React, {ReactNode, Suspense, useCallback, useMemo} from 'react'; +import {useFocusContext} from '../../providers/DesignSystemProvider'; export interface HtmlEditorProps { value?: string @@ -107,6 +108,14 @@ const KoenigWrapper: React.FC = ({ } console.error(error); // eslint-disable-line }, []); + const {setFocusState} = useFocusContext(); + + const handleBlur = () => { + if (onBlur) { + onBlur(); + } + setFocusState(false); + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const koenig = useMemo(() => new Proxy({} as { [key: string]: any }, { @@ -154,7 +163,7 @@ const KoenigWrapper: React.FC = ({ placeholderClassName='koenig-lexical-editor-input-placeholder' placeholderText={placeholder} singleParagraph={true} - onBlur={onBlur} + onBlur={handleBlur} > @@ -174,9 +183,13 @@ const HtmlEditor: React.FC { + setFocusState(true); + }; return
-
+
Loading editor...

}> diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.tsx index 9326a31adf..8dac3f38b0 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextArea.tsx @@ -3,6 +3,7 @@ import React, {useId} from 'react'; import Heading from '../Heading'; import Hint from '../Hint'; import clsx from 'clsx'; +import {useFocusContext} from '../../providers/DesignSystemProvider'; type ResizeOptions = 'both' | 'vertical' | 'horizontal' | 'none'; type FontStyles = 'sans' | 'mono'; @@ -40,6 +41,15 @@ const TextArea: React.FC = ({ ...props }) => { const id = useId(); + const {setFocusState} = useFocusContext(); + + const handleFocus = () => { + setFocusState(true); + }; + + const handleBlur = () => { + setFocusState(false); + }; let styles = clsx( 'peer order-2 rounded-sm border px-3 py-2 dark:text-white', @@ -78,7 +88,9 @@ const TextArea: React.FC = ({ placeholder={placeholder} rows={rows} value={value} + onBlur={handleBlur} onChange={onChange} + onFocus={handleFocus} {...props}> {title && {title}} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx index cbc2044451..21357662c0 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx @@ -10,7 +10,11 @@ const meta = { title: 'Global / Form / Textfield', component: TextField, tags: ['autodocs'], - decorators: [(_story: () => ReactNode) => (
{_story()}
)], + decorators: [(_story: () => ReactNode) => ( +
+ {_story()} +
+ )], argTypes: { hint: { control: 'text' diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx index f05beddb6f..a9ff985ffc 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.tsx @@ -2,6 +2,7 @@ import Heading from '../Heading'; import Hint from '../Hint'; import React, {useId} from 'react'; import clsx from 'clsx'; +import {useFocusContext} from '../../providers/DesignSystemProvider'; export type TextFieldProps = React.InputHTMLAttributes & { inputRef?: React.RefObject; @@ -50,6 +51,18 @@ const TextField: React.FC = ({ ...props }) => { const id = useId(); + const {setFocusState} = useFocusContext(); + + const handleFocus = () => { + setFocusState(true); + }; + + const handleBlur = (e: React.FocusEvent) => { + if (onBlur) { + onBlur(e); + } + setFocusState(false); + }; const disabledBorderClasses = border && 'border-grey-300 dark:border-grey-900'; const enabledBorderClasses = border && 'border-grey-500 hover:border-grey-700 focus:border-black dark:border-grey-800 dark:hover:border-grey-700 dark:focus:border-grey-500'; @@ -77,8 +90,9 @@ const TextField: React.FC = ({ placeholder={placeholder} type={type} value={value} - onBlur={onBlur} + onBlur={handleBlur} onChange={onChange} + onFocus={handleFocus} {...props} />; if (rightPlaceholder) { @@ -120,7 +134,7 @@ const TextField: React.FC = ({
); } else { - return field; + return (field); } }; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/URLTextField.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/URLTextField.tsx index 08c2aaa858..ddb0685698 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/URLTextField.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/URLTextField.tsx @@ -1,6 +1,7 @@ import React, {useEffect, useState} from 'react'; import TextField, {TextFieldProps} from './TextField'; import validator from 'validator'; +import {useFocusContext} from '../../providers/DesignSystemProvider'; const formatUrl = (value: string, baseUrl?: string) => { let url = value.trim(); @@ -103,6 +104,7 @@ const URLTextField: React.FC & { onChange: (value: string) => void; }> = ({baseUrl, value, transformPathWithoutSlash, onChange, ...props}) => { const [displayedUrl, setDisplayedUrl] = useState(''); + const {setFocusState} = useFocusContext(); useEffect(() => { setDisplayedUrl(formatUrl(value || '', baseUrl).display); @@ -123,6 +125,7 @@ const URLTextField: React.FC & { setDisplayedUrl(urls.display); onChange(urls.save); + setFocusState(false); }; const handleFocus: React.FocusEventHandler = (e) => { @@ -132,6 +135,7 @@ const URLTextField: React.FC & { } props.onFocus?.(e); + setFocusState(true); }; const handleKeyDown: React.KeyboardEventHandler = (e) => { diff --git a/apps/admin-x-settings/src/admin-x-ds/providers/DesignSystemProvider.tsx b/apps/admin-x-settings/src/admin-x-ds/providers/DesignSystemProvider.tsx new file mode 100644 index 0000000000..77a4d8b103 --- /dev/null +++ b/apps/admin-x-settings/src/admin-x-ds/providers/DesignSystemProvider.tsx @@ -0,0 +1,37 @@ +// FocusContext.tsx +import React, {createContext, useContext, useState} from 'react'; + +interface DesignSystemContextType { + isAnyTextFieldFocused: boolean; + setFocusState: (value: boolean) => void; +} + +const DesignSystemContext = createContext(undefined); + +export const useFocusContext = () => { + const context = useContext(DesignSystemContext); + if (!context) { + throw new Error('useFocusContext must be used within a FocusProvider'); + } + return context; +}; + +interface DesignSystemProviderProps { + children: React.ReactNode; +} + +const DesignSystemProvider: React.FC = ({children}) => { + const [isAnyTextFieldFocused, setIsAnyTextFieldFocused] = useState(false); + + const setFocusState = (value: boolean) => { + setIsAnyTextFieldFocused(value); + }; + + return ( + + {children} + + ); +}; + +export default DesignSystemProvider; \ No newline at end of file diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index bd79a8edc6..91d82924a8 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -12,6 +12,7 @@ import {searchKeywords as generalSearchKeywords} from './settings/general/Genera import {getSettingValues} from '../api/settings'; import {searchKeywords as membershipSearchKeywords} from './settings/membership/MembershipSettings'; import {searchKeywords as siteSearchKeywords} from './settings/site/SiteSettings'; +import {useFocusContext} from '../admin-x-ds/providers/DesignSystemProvider'; import {useGlobalData} from './providers/GlobalDataProvider'; import {useSearch} from './providers/ServiceProvider'; @@ -19,11 +20,12 @@ const Sidebar: React.FC = () => { const {filter, setFilter} = useSearch(); const {updateRoute} = useRouting(); const searchInputRef = useRef(null); + const {isAnyTextFieldFocused} = useFocusContext(); // Focus in on search field when pressing CMD+K/CTRL+K useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { - if (e.key === '/') { + if (e.key === '/' && !isAnyTextFieldFocused) { e?.preventDefault(); if (searchInputRef.current) { searchInputRef.current.focus(); @@ -34,7 +36,7 @@ const Sidebar: React.FC = () => { return () => { window.removeEventListener('keydown', handleKeyPress); }; - }, []); + }); // Auto-focus on searchfield on page load useEffect(() => {