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

Merge branch 'master' into imprv/83338-grant-swr

stevenfukase 4 лет назад
Родитель
Сommit
d8e2ab614e

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

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

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

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

+ 6 - 3
packages/app/src/client/services/ContextExtractor.tsx

@@ -5,7 +5,7 @@ import {
   useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   usePageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
-  useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
+  useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
   useSlackChannels,
 } from '~/stores/context';
 import {
@@ -41,10 +41,13 @@ const ContextExtractorOnce: FC = () => {
   const pageId = mainContent?.getAttribute('data-page-id') || null;
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
 
+  // createdAt
   const createdAtAttribute = mainContent?.getAttribute('data-page-created-at');
   const createdAt: Date | null = (createdAtAttribute != null) ? new Date(createdAtAttribute) : null;
+  // updatedAt
+  const updatedAtAttribute = mainContent?.getAttribute('data-page-updated-at');
+  const updatedAt: Date | null = (updatedAtAttribute != null) ? new Date(updatedAtAttribute) : null;
 
-  const updatedAt = mainContent?.getAttribute('data-page-updated-at');
   const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
   const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull);
   const isTrashPage = _isTrashPage(path);
@@ -106,7 +109,7 @@ const ContextExtractorOnce: FC = () => {
   useShareLinkId(shareLinkId);
   useShareLinksNumber(shareLinksNumber);
   useTemplateTagData(templateTagData);
-  useUpdatedAt(updatedAt);
+  useCurrentUpdatedAt(updatedAt);
   useCreator(creator);
   useRevisionAuthor(revisionAuthor);
 

+ 1 - 0
packages/app/src/client/services/PageContainer.js

@@ -65,6 +65,7 @@ export default class PageContainer extends Container {
       sumOfLikers: 0,
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
+      // please use useCurrentUpdatedAt instead
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
 

+ 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;

+ 5 - 2
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -16,6 +16,9 @@ const AuthorInfo = (props) => {
   const infoLabelForSubNav = mode === 'create'
     ? 'Created by'
     : 'Updated by';
+  const nullinfoLabelForFooter = mode === 'create'
+    ? 'Created by'
+    : 'Updated by';
   const infoLabelForFooter = mode === 'create'
     ? 'Created at'
     : 'Last revision posted at';
@@ -29,7 +32,7 @@ const AuthorInfo = (props) => {
     }
     catch (err) {
       if (err instanceof RangeError) {
-        return <p>Created by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+        return <p>{nullinfoLabelForFooter} <UserPicture user={user} size="sm" /> {userLabel}</p>;
       }
       return;
     }
@@ -60,7 +63,7 @@ const AuthorInfo = (props) => {
 };
 
 AuthorInfo.propTypes = {
-  date: PropTypes.string.isRequired,
+  date: PropTypes.instanceOf(Date),
   user: PropTypes.object,
   mode: PropTypes.oneOf(['create', 'update']),
   locate: PropTypes.oneOf(['subnav', 'footer']),

+ 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>
   );

+ 4 - 4
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -8,10 +8,10 @@ import LinkedPagePath from '~/models/linked-page-path';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { useCurrentCreatedAt } from '~/stores/context';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 import CopyDropdown from '../Page/CopyDropdown';
 import TagLabels from '../Page/TagLabels';
@@ -71,16 +71,16 @@ const GrowiSubNavigation = (props) => {
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+  const { data: createdAt } = useCurrentCreatedAt();
+  const { data: updatedAt } = useCurrentUpdatedAt();
 
   const {
     appContainer, pageContainer, isCompactMode,
   } = props;
   const {
-    pageId, path, creator, updatedAt, revisionAuthor, isPageExist,
+    pageId, path, creator, revisionAuthor, isPageExist,
   } = pageContainer.state;
 
-  const { data: createdAt } = useCurrentCreatedAt();
-
   const { isGuestUser } = appContainer;
   const isEditorMode = editorMode !== EditorMode.View;
   // Tags cannot be edited while the new page and editorMode is view

+ 3 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -7,6 +7,7 @@ import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { useCurrentUpdatedAt } from '~/stores/context';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';
@@ -15,8 +16,9 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const {
-    path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    path, isDeleted, lastUpdateUsername, deletedUserName, deletedAt, isAbleToDeleteCompletely,
   } = pageContainer.state;
+  const { data: updatedAt } = useCurrentUpdatedAt();
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
   const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);

+ 5 - 2
packages/app/src/components/PageContentFooter.jsx

@@ -6,16 +6,19 @@ import AuthorInfo from './Navbar/AuthorInfo';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
-import { usePath, useCurrentCreatedAt } from '~/stores/context';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 const PageContentFooter = (props) => {
   const { pageContainer } = props;
   const { data: createdAt } = useCurrentCreatedAt();
+  const { data: updatedAt } = useCurrentUpdatedAt();
+
 
   const {
-    creator, updatedAt, revisionAuthor,
+    creator, revisionAuthor,
   } = pageContainer.state;
 
+
   return (
     <div className="page-content-footer py-4 d-edit-none d-print-none">
       <div className="grw-container-convertible">

+ 0 - 1
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -292,7 +292,6 @@ class LinkEditModal extends React.PureComponent {
                 inputName="link"
                 placeholder={t('link_edit.placeholder_of_link_input')}
                 keywordOnInit={this.state.linkInputValue}
-                behaviorOfResetBtn="clear"
                 autoFocus
               />
               <div className="d-none d-sm-block input-group-append">

+ 0 - 1
packages/app/src/components/PagePathAutoComplete.jsx

@@ -41,7 +41,6 @@ const PagePathAutoComplete = (props) => {
       onChange={inputChangeHandler}
       onInputChange={props.onInputChange}
       inputName="new_path"
-      behaviorOfResetBtn="clear"
       placeholder="Input page path"
       keywordOnInit={getKeywordOnInit(initializedPath)}
       autoFocus={props.autoFocus}

+ 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}
     />

+ 20 - 24
packages/app/src/components/SearchTypeahead.tsx

@@ -1,6 +1,6 @@
 import React, {
   FC, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
-  KeyboardEvent, useCallback, useRef, useState,
+  KeyboardEvent, useCallback, useRef, useState, MouseEvent,
 } from 'react';
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
@@ -17,15 +17,12 @@ import { IPage } from '~/interfaces/page';
 
 type ResetFormButtonProps = {
   keywordOnInit: string,
-  behaviorOfResetBtn: 'restore' | 'clear',
   input: string,
-  onReset: () => void,
+  onReset: (e: MouseEvent<HTMLButtonElement>) => void,
 }
 
 const ResetFormButton: FC<ResetFormButtonProps> = (props: ResetFormButtonProps) => {
-  const isClearBtn = props.behaviorOfResetBtn === 'clear';
-  const initialKeyword = isClearBtn ? '' : props.keywordOnInit;
-  const isHidden = props.input === initialKeyword;
+  const isHidden = props.input.length === 0;
 
   return isHidden ? (
     <span />
@@ -45,7 +42,6 @@ type Props = TypeaheadProps & {
   keywordOnInit?: string,
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   helpElement?: any,
-  behaviorOfResetBtn?: 'restore' | 'clear',
 };
 
 // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
@@ -60,7 +56,6 @@ type TypeaheadInstanceFactory = {
 
 const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
   const {
-    keywordOnInit,
     onSearchSuccess, onSearchError, onInputChange, onSubmit,
     emptyLabel, helpElement,
   } = props;
@@ -74,18 +69,18 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
 
   const typeaheadRef = useRef<TypeaheadInstanceFactory>(null);
 
+  const focusToTypeahead = () => {
+    const instance = typeaheadRef.current?.getInstance();
+    if (instance != null) {
+      instance.focus();
+    }
+  };
 
   // publish focus()
   useImperativeHandle(ref, () => ({
-    focus() {
-      const instance = typeaheadRef.current?.getInstance();
-      if (instance != null) {
-        instance.focus();
-      }
-    },
+    focus: focusToTypeahead,
   }));
 
-
   const changeKeyword = (text: string | undefined) => {
     const instance = typeaheadRef.current?.getInstance();
     if (instance != null) {
@@ -94,12 +89,18 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     }
   };
 
-  const restoreInitialData = () => {
-    changeKeyword(keywordOnInit);
-  };
+  const resetForm = (e: MouseEvent<HTMLButtonElement>) => {
+    e.preventDefault();
 
-  const clearKeyword = () => {
+    setInput('');
     changeKeyword('');
+    setPages([]);
+
+    focusToTypeahead();
+
+    if (onInputChange != null) {
+      onInputChange('');
+    }
   };
 
   /**
@@ -187,9 +188,6 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     inputProps.name = props.inputName;
   }
 
-  const isClearBtn = props.behaviorOfResetBtn === 'clear';
-  const resetForm = isClearBtn ? clearKeyword : restoreInitialData;
-
   const renderMenuItemChildren = (page: IPage) => (
     <span>
       <UserPicture user={page.lastUpdateUser} size="sm" noLink />
@@ -226,7 +224,6 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         keywordOnInit={props.keywordOnInit!}
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        behaviorOfResetBtn={props.behaviorOfResetBtn!}
         input={input}
         onReset={resetForm}
       />
@@ -239,7 +236,6 @@ const ForwardedSearchTypeahead = forwardRef(SearchTypeahead);
 ForwardedSearchTypeahead.defaultProps = {
   placeholder: '',
   keywordOnInit: '',
-  behaviorOfResetBtn: 'restore',
   autoFocus: false,
 };
 

+ 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>

+ 1 - 1
packages/app/src/server/views/widget/page_content.html

@@ -20,7 +20,7 @@
   data-page-created-at="{{ page.createdAt|datetz('Y/m/d H:i:s') }}"
   data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"
   data-page-last-update-username="{% if page && page.lastUpdateUser %}{{ page.lastUpdateUser.name }}{% endif %}"
-  data-page-updated-at="{% if page %}{{ page.updatedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-updated-at="{{ page.updatedAt|datetz('Y/m/d H:i:s') }}"
   data-page-delete-username="{% if page && page.deleteUser %}{{ page.deleteUser.name }}{% endif %}"
   data-page-deleted-at="{% if page && page.deletedAt %}{{ page.deletedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"

+ 2 - 2
packages/app/src/stores/context.tsx

@@ -34,8 +34,8 @@ export const useCurrentCreatedAt = (initialData?: Nullable<Date>): SWRResponse<N
   return useStaticSWR<Nullable<Date>, Error>('createdAt', initialData ?? null);
 };
 
-export const useUpdatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('updatedAt', initialData ?? null);
+export const useCurrentUpdatedAt = (initialData?: Nullable<Date>): SWRResponse<Nullable<Date>, Error> => {
+  return useStaticSWR<Nullable<Date>, Error>('updatedAt', initialData ?? null);
 };
 
 export const useDeletedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 7 - 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');
 
@@ -218,6 +220,7 @@ export const usePageCreateModalOpened = (isOpened?: boolean): SWRResponse<boolea
   return useStaticSWR('isPageCreateModalOpened', isOpened || null, { fallbackData: initialData });
 };
 
+
 export const useSelectedGrant = (initialData?: Nullable<number>): SWRResponse<Nullable<number>, Error> => {
   return useStaticSWR<Nullable<number>, Error>('grant', initialData ?? null);
 };
@@ -229,3 +232,7 @@ export const useSelectedGrantGroupId = (initialData?: Nullable<string>): SWRResp
 export const useSelectedGrantGroupName = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
   return useStaticSWR<Nullable<string>, Error>('grantGroupName', initialData ?? null);
 };
+
+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;
+    }
   }
 }