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

Merge branch 'master' into feat/notification

Shun Miyazawa 4 лет назад
Родитель
Сommit
f961301f5e

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

@@ -454,6 +454,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

@@ -454,6 +454,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

@@ -433,6 +433,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;

+ 33 - 7
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -1,14 +1,16 @@
 import React, {
-  FC, useState, useCallback,
+  FC, useState, useCallback, useRef,
 } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
 import { IPage } from '~/interfaces/page';
+import { IFocusable } from '~/client/interfaces/focusable';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import SearchForm from '../SearchForm';
+import { useGlobalSearchFormRef } from '~/stores/ui';
 
 
 type Props = {
@@ -21,8 +23,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: unknown[]) => {
     const page = data[0] as IPage; // should be single page selected
@@ -53,6 +60,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">
@@ -61,26 +70,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}
         />
-        <div className="btn-group-submit-search">
-          <span className="btn-link text-decoration-none" onClick={search}>
-            <i className="icon-magnifier"></i>
+        { isIndicatorShown && (
+          <span className="grw-shortcut-key-indicator">
+            <code className="bg-transparent text-muted">/</code>
           </span>
-        </div>
+        ) }
       </div>
     </div>
   );

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

@@ -84,6 +84,8 @@ type Props = {
   dropup?: boolean,
   keyword?: string,
   onChange?: (data: unknown[]) => void,
+  onBlur?: () => void,
+  onFocus?: () => void,
   onSubmit?: (input: string) => void,
   onInputChange?: (text: string) => void,
 };
@@ -93,7 +95,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);
@@ -129,8 +131,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

@@ -94,7 +94,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 { useIsEditable } from './context';
+import { IFocusable } from '~/client/interfaces/focusable';
 
 const logger = loggerFactory('growi:stores:ui');
 
@@ -215,3 +217,10 @@ export const usePageCreateModalOpened = (isOpened?: boolean): SWRResponse<boolea
   const initialData = false;
   return useStaticSWR('isPageCreateModalOpened', isOpened || null, { fallbackData: initialData });
 };
+
+export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
+  return useStaticSWR(
+    'globalSearchTypeahead',
+    initialData ?? null,
+  );
+};

+ 11 - 10
packages/app/src/styles/_search.scss

@@ -55,8 +55,8 @@
 // input styles
 .grw-global-search {
   .search-clear {
-    top: 3px;
-    right: 26px;
+    top: 4px;
+    right: 4px;
   }
 
   .dropdown-toggle {
@@ -71,7 +71,7 @@
     border-top-right-radius: 40px;
     border-bottom-right-radius: 40px;
     .rbt-input-main {
-      padding-right: 58px;
+      padding-right: 36px;
       // corner radius
       border-top-right-radius: 40px;
       border-bottom-right-radius: 40px;
@@ -101,18 +101,19 @@
     }
   }
 
-  .btn-group-submit-search {
+  .grw-shortcut-key-indicator {
     position: absolute;
     top: 0;
-    right: 0;
-
-    z-index: 3;
+    right: 10px;
 
     display: flex;
     align-items: center;
-    justify-content: center;
-    width: 32px;
-    height: 32px;
+    height: 30px;
+
+    code {
+      padding-right: 0.4rem;
+      padding-left: 0.4rem;
+    }
   }
 }