Просмотр исходного кода

Merge branch 'master' into dev/5.0.x

Yuki Takei 4 лет назад
Родитель
Сommit
ea33d3509a

+ 1 - 0
packages/app/resource/locales/en_US/translation.json

@@ -446,6 +446,7 @@
       "Open/Close shortcut help": "Open/Close<br>shortcut help",
       "Edit Page": "Edit Page",
       "Create Page": "Create Page",
+      "Search": "Search",
       "Show Contributors": "Show Contributors",
       "MirrorMode": "Mirror Mode",
       "Konami Code": "Konami Code",

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -446,6 +446,7 @@
       "Open/Close shortcut help": "ショートカットヘルプ<br>の表示/非表示",
       "Edit Page": "ページ編集",
       "Create Page": "ページ作成",
+      "Search": "検索",
       "Show Contributors": "コントリビューター<br>を表示",
       "MirrorMode": "ミラーモード",
       "Konami Code": "コナミコマンド",

+ 1 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -425,6 +425,7 @@
 			"Open/Close shortcut help": "打开/关闭快捷方式帮助",
 			"Edit Page": "编辑页面",
 			"Create Page": "创建页面",
+      "Search": "搜索",
 			"Show Contributors": "显示参与者",
 			"Konami Code": "Konami Code",
 			"konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"

+ 2 - 0
packages/app/src/components/Hotkeys/HotkeysManager.jsx

@@ -7,6 +7,7 @@ import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
 import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
 import CreatePage from './Subscribers/CreatePage';
 import EditPage from './Subscribers/EditPage';
+import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
 
 // define supported components list
 const SUPPORTED_COMPONENTS = [
@@ -15,6 +16,7 @@ const SUPPORTED_COMPONENTS = [
   ShowShortcutsModal,
   CreatePage,
   EditPage,
+  FocusToGlobalSearch,
 ];
 
 const KEY_SET = new Set();

+ 34 - 0
packages/app/src/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx

@@ -0,0 +1,34 @@
+import { FC, useEffect } from 'react';
+
+import { useIsEditable } from '~/stores/context';
+import { useGlobalSearchFormRef } from '~/stores/ui';
+
+const FocusToGlobalSearch = (props) => {
+  const { data: isEditable } = useIsEditable();
+  const { data: globalSearchFormRef } = useGlobalSearchFormRef();
+
+  // setup effect
+  useEffect(() => {
+    if (!isEditable) {
+      return;
+    }
+
+    // ignore when dom that has 'modal in' classes exists
+    if (document.getElementsByClassName('modal in').length > 0) {
+      return;
+    }
+
+    globalSearchFormRef.current.focus();
+
+    // remove this
+    props.onDeleteRender();
+  }, [globalSearchFormRef, isEditable, props]);
+
+  return null;
+};
+
+FocusToGlobalSearch.getHotkeyStrokes = () => {
+  return [['/']];
+};
+
+export default FocusToGlobalSearch;

+ 34 - 3
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -1,15 +1,17 @@
 import React, {
-  FC, useState, useCallback,
+  FC, useState, useCallback, useRef,
 } from 'react';
 import { useTranslation } from 'react-i18next';
 import assert from 'assert';
 
 import AppContainer from '~/client/services/AppContainer';
 import { IPageSearchResultData } from '~/interfaces/search';
+import { IFocusable } from '~/client/interfaces/focusable';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import SearchForm from '../SearchForm';
+import { useGlobalSearchFormRef } from '~/stores/ui';
 
 
 type Props = {
@@ -22,8 +24,13 @@ const GlobalSearch: FC<Props> = (props: Props) => {
   const { appContainer, dropup } = props;
   const { t } = useTranslation();
 
+  const globalSearchFormRef = useRef<IFocusable>(null);
+
+  useGlobalSearchFormRef(globalSearchFormRef);
+
   const [text, setText] = useState('');
   const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
+  const [isFocused, setFocused] = useState<boolean>(false);
 
   const gotoPage = useCallback((data: IPageSearchResultData[]) => {
     assert(data.length > 0);
@@ -56,6 +63,8 @@ const GlobalSearch: FC<Props> = (props: Props) => {
 
   const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
 
+  const isIndicatorShown = !isFocused && (text.length === 0);
+
   return (
     <div className={`form-group mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
       <div className="input-group flex-nowrap">
@@ -64,21 +73,43 @@ const GlobalSearch: FC<Props> = (props: Props) => {
             {scopeLabel}
           </button>
           <div className="dropdown-menu">
-            <button className="dropdown-item" type="button" onClick={() => setScopeChildren(false)}>
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setScopeChildren(false);
+                globalSearchFormRef.current?.focus();
+              }}
+            >
               { t('header_search_box.item_label.All pages') }
             </button>
-            <button className="dropdown-item" type="button" onClick={() => setScopeChildren(true)}>
+            <button
+              className="dropdown-item"
+              type="button"
+              onClick={() => {
+                setScopeChildren(true);
+                globalSearchFormRef.current?.focus();
+              }}
+            >
               { t('header_search_box.item_label.This tree') }
             </button>
           </div>
         </div>
         <SearchForm
+          ref={globalSearchFormRef}
           isSearchServiceReachable={isSearchServiceReachable}
           dropup={dropup}
           onChange={gotoPage}
+          onBlur={() => setFocused(false)}
+          onFocus={() => setFocused(true)}
           onInputChange={text => setText(text)}
           onSubmit={search}
         />
+        { isIndicatorShown && (
+          <span className="grw-shortcut-key-indicator">
+            <code className="bg-transparent text-muted">/</code>
+          </span>
+        ) }
       </div>
     </div>
   );

+ 15 - 3
packages/app/src/components/SearchForm.tsx

@@ -85,6 +85,8 @@ type Props = {
   dropup?: boolean,
   keyword?: string,
   onChange?: (data: IPageSearchResultData[]) => void,
+  onBlur?: () => void,
+  onFocus?: () => void,
   onSubmit?: (input: string) => void,
   onInputChange?: (text: string) => void,
 };
@@ -94,7 +96,7 @@ const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, r
   const { t } = useTranslation();
   const {
     isSearchServiceReachable, dropup,
-    onChange, onSubmit, onInputChange,
+    onChange, onBlur, onFocus, onSubmit, onInputChange,
   } = props;
 
   const [searchError, setSearchError] = useState<Error | null>(null);
@@ -130,8 +132,18 @@ const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, r
       onSubmit={onSubmit}
       onInputChange={onInputChange}
       onSearchError={err => setSearchError(err)}
-      onBlur={() => setShownHelp(false)}
-      onFocus={() => setShownHelp(true)}
+      onBlur={() => {
+        setShownHelp(false);
+        if (onBlur != null) {
+          onBlur();
+        }
+      }}
+      onFocus={() => {
+        setShownHelp(true);
+        if (onFocus != null) {
+          onFocus();
+        }
+      }}
       helpElement={<SearchFormHelp isShownHelp={isShownHelp} isReachable={isSearchServiceReachable} />}
       keywordOnInit={props.keyword}
     />

+ 6 - 0
packages/app/src/components/SearchTypeahead.tsx

@@ -92,7 +92,13 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
 
     setInput('');
     changeKeyword('');
+    setPages([]);
+
     focusToTypeahead();
+
+    if (onInputChange != null) {
+      onInputChange('');
+    }
   };
 
   /**

+ 5 - 1
packages/app/src/server/views/modal/shortcuts.html

@@ -26,6 +26,10 @@
                     <th>{{ t('modal_shortcuts.global.Edit Page') }}:</th>
                     <td><span class="key">E</span></td>
                   </tr>
+                  <tr>
+                    <th>{{ t('modal_shortcuts.global.Search') }}:</th>
+                    <td><span class="key">/</span></td>
+                  </tr>
                   <tr>
                     <th>{{ t('modal_shortcuts.global.Show Contributors') }}:</th>
                     <td>
@@ -79,7 +83,7 @@
               <table class="table">
                 <tr>
                   <th>{{ t('modal_shortcuts.commentform.Post') }}:</th>
-                  <td><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
+                  <td class="text-nowrap"><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
                 </tr>
                 <tr>
                   <th>{{ t('modal_shortcuts.editor.Delete Line') }}:</th>

+ 9 - 0
packages/app/src/stores/ui.tsx

@@ -5,11 +5,13 @@ import useSWRImmutable from 'swr/immutable';
 
 import { Breakpoint, addBreakpointListener } from '@growi/ui';
 
+import { RefObject } from 'react';
 import { SidebarContentsType } from '~/interfaces/ui';
 import loggerFactory from '~/utils/logger';
 
 import { useStaticSWR } from './use-static-swr';
 import { useCurrentPagePath, useIsEditable } from './context';
+import { IFocusable } from '~/client/interfaces/focusable';
 
 const logger = loggerFactory('growi:stores:ui');
 
@@ -276,3 +278,10 @@ export const useCreateModalPath = (): SWRResponse<string, Error> => {
     },
   );
 };
+
+export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
+  return useStaticSWR(
+    'globalSearchTypeahead',
+    initialData ?? null,
+  );
+};

+ 15 - 0
packages/app/src/styles/_search.scss

@@ -83,6 +83,21 @@
       border: none;
     }
   }
+
+  .grw-shortcut-key-indicator {
+    position: absolute;
+    top: 0;
+    right: 4px;
+
+    display: flex;
+    align-items: center;
+    height: 30px;
+
+    code {
+      padding-right: 0.4rem;
+      padding-left: 0.4rem;
+    }
+  }
 }
 
 // layout for GlobalSearch