Browse Source

Merge pull request #10938 from growilabs/feat/180268-180441-add-tooltips-to-editor-toolbar

feat: add tooltips to editor toolbar
Shun Miyazawa 4 days ago
parent
commit
1b7eb7d452

+ 17 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -1082,5 +1082,22 @@
     "success-toaster": "Latest text synchronized",
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "error-toaster": "Synchronization of the latest text failed"
+  },
+  "toolbar": {
+    "attachments": "Attachments",
+    "bold": "Bold",
+    "bullet_list": "Bullet List",
+    "checklist": "Checklist",
+    "code": "Code",
+    "diagram": "Diagram",
+    "emoji": "Emoji",
+    "heading": "Heading",
+    "italic": "Italic",
+    "numbered_list": "Numbered List",
+    "quote": "Quote",
+    "strikethrough": "Strikethrough",
+    "table": "Table",
+    "template": "Template",
+    "text_formatting": "Text Formatting"
   }
 }

+ 17 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -1074,5 +1074,22 @@
     "success-toaster": "Dernière révision synchronisée",
     "skipped-toaster": "Le mode édition doit être activé pour déclencher la synchronisation. Synchronisation annulée.",
     "error-toaster": "Synchronisation échouée"
+  },
+  "toolbar": {
+    "attachments": "Pièces jointes",
+    "bold": "Gras",
+    "bullet_list": "Liste à puces",
+    "checklist": "Liste de contrôle",
+    "code": "Code",
+    "diagram": "Diagramme",
+    "emoji": "Emoji",
+    "heading": "Titre",
+    "italic": "Italique",
+    "numbered_list": "Liste numérotée",
+    "quote": "Citation",
+    "strikethrough": "Barré",
+    "table": "Tableau",
+    "template": "Modèle",
+    "text_formatting": "Mise en forme du texte"
   }
 }

+ 17 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -1115,5 +1115,22 @@
     "success-toaster": "最新の本文を同期しました",
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "error-toaster": "最新の本文の同期に失敗しました"
+  },
+  "toolbar": {
+    "attachments": "添付ファイル",
+    "bold": "太字",
+    "bullet_list": "箇条書きリスト",
+    "checklist": "チェックリスト",
+    "code": "コード",
+    "diagram": "ダイアグラム",
+    "emoji": "絵文字",
+    "heading": "見出し",
+    "italic": "イタリック",
+    "numbered_list": "番号付きリスト",
+    "quote": "引用",
+    "strikethrough": "取り消し線",
+    "table": "テーブル",
+    "template": "テンプレート",
+    "text_formatting": "テキスト書式"
   }
 }

+ 17 - 0
apps/app/public/static/locales/ko_KR/translation.json

@@ -1042,5 +1042,22 @@
     "success-toaster": "최신 텍스트 동기화됨",
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "error-toaster": "최신 텍스트 동기화 실패"
+  },
+  "toolbar": {
+    "attachments": "첨부 파일",
+    "bold": "굵게",
+    "bullet_list": "글머리 기호 목록",
+    "checklist": "체크리스트",
+    "code": "코드",
+    "diagram": "다이어그램",
+    "emoji": "이모지",
+    "heading": "제목",
+    "italic": "기울임꼴",
+    "numbered_list": "번호 매기기 목록",
+    "quote": "인용",
+    "strikethrough": "취소선",
+    "table": "표",
+    "template": "템플릿",
+    "text_formatting": "텍스트 서식"
   }
 }

+ 17 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -1087,5 +1087,22 @@
     "success-toaster": "同步最新文本",
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "error-toaster": "同步最新文本失败"
+  },
+  "toolbar": {
+    "attachments": "附件",
+    "bold": "粗体",
+    "bullet_list": "无序列表",
+    "checklist": "清单",
+    "code": "代码",
+    "diagram": "图表",
+    "emoji": "表情符号",
+    "heading": "标题",
+    "italic": "斜体",
+    "numbered_list": "有序列表",
+    "quote": "引用",
+    "strikethrough": "删除线",
+    "table": "表格",
+    "template": "模板",
+    "text_formatting": "文本格式"
   }
 }

+ 1 - 0
packages/editor/package.json

@@ -58,6 +58,7 @@
     "csv-to-markdown-table": "^1.4.1",
     "emoji-mart": "^5.6.0",
     "i18next": "^23.16.5",
+    "react-i18next": "^15.1.1",
     "lib0": "^0.2.94",
     "markdown-table": "^3.0.3",
     "react-dropzone": "^14.2.3",

+ 12 - 1
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx

@@ -1,10 +1,12 @@
-import { type JSX, useState } from 'react';
+import { type JSX, useId, useState } from 'react';
 import { AcceptedUploadFileType } from '@growi/core';
+import { useTranslation } from 'react-i18next';
 import {
   Dropdown,
   DropdownItem,
   DropdownMenu,
   DropdownToggle,
+  UncontrolledTooltip,
 } from 'reactstrap';
 
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
@@ -26,6 +28,9 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
 
   const [isOpen, setOpen] = useState(false);
 
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   return (
     <>
       <Dropdown
@@ -35,6 +40,7 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
         className="lh-1"
       >
         <DropdownToggle
+          id={id}
           className={`${btnAttachmentToggleClass} btn-toolbar-button rounded-circle`}
           color="unset"
         >
@@ -74,6 +80,11 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
           <LinkEditButton editorKey={editorKey} />
         </DropdownMenu>
       </Dropdown>
+      {!isOpen && (
+        <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+          {t('toolbar.attachments')}
+        </UncontrolledTooltip>
+      )}
     </>
   );
 };

+ 21 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useDrawioModalForEditorActions } from '../../../../states/modal/drawio-for-editor';
 
@@ -9,17 +11,27 @@ type Props = {
 export const DiagramButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
   const { open: openDrawioModal } = useDrawioModalForEditorActions();
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const onClickDiagramButton = useCallback(() => {
     openDrawioModal(editorKey);
   }, [editorKey, openDrawioModal]);
+
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickDiagramButton}
-    >
-      {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
-      <span className="growi-custom-icons fs-6">drawer_io</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickDiagramButton}
+      >
+        {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
+        <span className="growi-custom-icons fs-6">drawer_io</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.diagram')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 15 - 2
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EmojiButton.tsx

@@ -4,9 +4,11 @@ import {
   type JSX,
   useCallback,
   useEffect,
+  useId,
   useState,
 } from 'react';
-import { Modal } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
+import { Modal, UncontrolledTooltip } from 'reactstrap';
 
 import { useResolvedTheme } from '../../../../states/ui/resolved-theme';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -24,6 +26,9 @@ type Props = {
 export const EmojiButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
 
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const [isOpen, setIsOpen] = useState(false);
   const [Picker, setPicker] = useState<ComponentType<PickerProps> | null>(null);
   const [emojiData, setEmojiData] = useState<unknown>(null);
@@ -89,9 +94,17 @@ export const EmojiButton = (props: Props): JSX.Element => {
 
   return (
     <>
-      <button type="button" className="btn btn-toolbar-button" onClick={toggle}>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={toggle}
+      >
         <span className="material-symbols-outlined fs-5">emoji_emotions</span>
       </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.emoji')}
+      </UncontrolledTooltip>
       {isOpen && Picker != null && emojiData != null && (
         <div className="mb-2 d-none d-md-block">
           <Modal

+ 20 - 8
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TableButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useHandsontableModalForEditorActions } from '../../../../states/modal/handsontable';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -9,6 +11,10 @@ type Props = {
 
 export const TableButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
+
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
   const { open: openTableModal } = useHandsontableModalForEditorActions();
   const editor = codeMirrorEditor?.view;
@@ -17,12 +23,18 @@ export const TableButton = (props: Props): JSX.Element => {
   }, [editor, openTableModal]);
 
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickTableButton}
-    >
-      <span className="material-symbols-outlined fs-5">table</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickTableButton}
+      >
+        <span className="material-symbols-outlined fs-5">table</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.table')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 21 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TemplateButton.tsx

@@ -1,4 +1,6 @@
-import { type JSX, useCallback } from 'react';
+import { type JSX, useCallback, useId } from 'react';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 import { useTemplateModalActions } from '../../../../states/modal/template';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -9,6 +11,10 @@ type Props = {
 
 export const TemplateButton = (props: Props): JSX.Element => {
   const { editorKey } = props;
+
+  const id = useId();
+  const { t } = useTranslation('translation');
+
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
   const { open: openTemplateModal } = useTemplateModalActions();
 
@@ -23,13 +29,19 @@ export const TemplateButton = (props: Props): JSX.Element => {
   }, [codeMirrorEditor?.view, openTemplateModal]);
 
   return (
-    <button
-      type="button"
-      className="btn btn-toolbar-button"
-      onClick={onClickTempleteButton}
-      data-testid="open-template-button"
-    >
-      <span className="material-symbols-outlined fs-5">file_copy</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className="btn btn-toolbar-button"
+        onClick={onClickTempleteButton}
+        data-testid="open-template-button"
+      >
+        <span className="material-symbols-outlined fs-5">file_copy</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.template')}
+      </UncontrolledTooltip>
+    </>
   );
 };

+ 83 - 9
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -1,5 +1,6 @@
-import { type JSX, useCallback, useState } from 'react';
-import { Collapse } from 'reactstrap';
+import { type JSX, useCallback, useId, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Collapse, UncontrolledTooltip } from 'reactstrap';
 
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
 import { useCodeMirrorEditorIsolated } from '../../../stores/codemirror-editor';
@@ -16,16 +17,24 @@ type TogglarProps = {
 const TextFormatToolsToggler = (props: TogglarProps): JSX.Element => {
   const { isOpen, onClick } = props;
 
+  const id = useId();
+  const { t } = useTranslation('translation');
   const activeClass = isOpen ? 'active' : '';
 
   return (
-    <button
-      type="button"
-      className={`btn btn-toolbar-button ${btnTextFormatToolsTogglerClass} ${activeClass}`}
-      onClick={onClick}
-    >
-      <span className="material-symbols-outlined fs-3">match_case</span>
-    </button>
+    <>
+      <button
+        id={id}
+        type="button"
+        className={`btn btn-toolbar-button ${btnTextFormatToolsTogglerClass} ${activeClass}`}
+        onClick={onClick}
+      >
+        <span className="material-symbols-outlined fs-3">match_case</span>
+      </button>
+      <UncontrolledTooltip placement="top" target={CSS.escape(id)}>
+        {t('toolbar.text_formatting')}
+      </UncontrolledTooltip>
+    </>
   );
 };
 
@@ -37,6 +46,8 @@ type TextFormatToolsType = {
 export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
   const { editorKey, onTextFormatToolsCollapseChange } = props;
   const [isOpen, setOpen] = useState(false);
+  const baseId = useId();
+  const { t } = useTranslation('translation');
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
 
   const toggle = useCallback(() => {
@@ -66,13 +77,21 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
       >
         <div className="d-flex px-1 gap-1" style={{ width: '220px' }}>
           <button
+            id={`${baseId}-bold`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('**', '**')}
           >
             <span className="material-symbols-outlined fs-5">format_bold</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-bold`)}
+          >
+            {t('toolbar.bold')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-italic`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('*', '*')}
@@ -81,7 +100,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_italic
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-italic`)}
+          >
+            {t('toolbar.italic')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-strikethrough`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('~', '~')}
@@ -90,7 +116,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_strikethrough
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-strikethrough`)}
+          >
+            {t('toolbar.strikethrough')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-heading`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('#', true)}
@@ -98,14 +131,28 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">header</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-heading`)}
+          >
+            {t('toolbar.heading')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-code`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertMarkdownElements('`', '`')}
           >
             <span className="material-symbols-outlined fs-5">code</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-code`)}
+          >
+            {t('toolbar.code')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-bullet-list`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('-')}
@@ -114,7 +161,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_list_bulleted
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-bullet-list`)}
+          >
+            {t('toolbar.bullet_list')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-numbered-list`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('1.')}
@@ -123,7 +177,14 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
               format_list_numbered
             </span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-numbered-list`)}
+          >
+            {t('toolbar.numbered_list')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-quote`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('>')}
@@ -131,13 +192,26 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             {/* TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015 */}
             <span className="growi-custom-icons">format_quote</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-quote`)}
+          >
+            {t('toolbar.quote')}
+          </UncontrolledTooltip>
           <button
+            id={`${baseId}-checklist`}
             type="button"
             className="btn btn-toolbar-button"
             onClick={() => onClickInsertPrefix('- [ ]')}
           >
             <span className="material-symbols-outlined fs-5">checklist</span>
           </button>
+          <UncontrolledTooltip
+            placement="top"
+            target={CSS.escape(`${baseId}-checklist`)}
+          >
+            {t('toolbar.checklist')}
+          </UncontrolledTooltip>
         </div>
       </Collapse>
     </div>

+ 3 - 0
pnpm-lock.yaml

@@ -1389,6 +1389,9 @@ importers:
       react-hook-form:
         specifier: ^7.45.4
         version: 7.52.0(react@18.2.0)
+      react-i18next:
+        specifier: ^15.1.1
+        version: 15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
       react-toastify:
         specifier: ^9.1.3
         version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)