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

Merge pull request #9371 from weseek/imprv/149280-156770-display-tooltip-hover-toolicon

imprv: Sidebar button displays tooltip
mergify[bot] 1 год назад
Родитель
Сommit
71e30cbcda

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

@@ -30,7 +30,7 @@
   "Tags": "Tags",
   "Close": "Close",
   "Shortcuts": "Shortcuts",
-  "CustomSidebar": "Custom Sidebar",
+  "Custom Sidebar": "Custom Sidebar",
   "eg": "e.g.",
   "add": "Add",
   "Undo": "Undo",

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

@@ -30,7 +30,7 @@
   "Tags": "Étiquettes",
   "Close": "Fermer",
   "Shortcuts": "Raccourcis",
-  "CustomSidebar": "Navigation latérale",
+  "Custom Sidebar": "Navigation latérale",
   "eg": "e.g.",
   "add": "Ajouter",
   "Undo": "Annuler",

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

@@ -30,7 +30,7 @@
   "Tags": "タグ",
   "Close": "閉じる",
   "Shortcuts": "ショートカット",
-  "CustomSidebar": "カスタムサイドバー",
+  "Custom Sidebar": "カスタムサイドバー",
   "eg": "例:",
   "add": "追加",
   "Undo": "元に戻す",

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

@@ -30,7 +30,7 @@
   "Tags": "标签",
   "Close": "Close",
   "Shortcuts": "快捷方式",
-  "CustomSidebar": "Custom Sidebar",
+  "Custom Sidebar": "Custom Sidebar",
   "eg": "e.g.",
   "add": "添加",
   "Undo": "撤销",

+ 1 - 1
apps/app/src/client/components/Sidebar/Custom/CustomSidebar.tsx

@@ -21,7 +21,7 @@ export const CustomSidebar = (): JSX.Element => {
     <div className="pt-4 pb-3 px-3">
       <div className="grw-sidebar-content-header d-flex">
         <h3 className="fs-6 fw-bold mb-0">
-          {t('CustomSidebar')}
+          {t('Custom Sidebar')}
           { !isLoading && <Link href="/Sidebar#edit" className="h6 ms-2"><span className="material-symbols-outlined">edit</span></Link> }
         </h3>
         { !isLoading && <SidebarHeaderReloadButton onClick={() => mutate()} /> }

+ 2 - 2
apps/app/src/client/components/Sidebar/InAppNotification/PrimaryItemForNotification.tsx

@@ -4,9 +4,9 @@ import { SidebarContentsType } from '~/interfaces/ui';
 import { useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 import { useDefaultSocket } from '~/stores/socket-io';
 
-import { PrimaryItem, type Props } from '../SidebarNav/PrimaryItem';
+import { PrimaryItem, type PrimaryItemProps } from '../SidebarNav/PrimaryItem';
 
-type PrimaryItemForNotificationProps = Omit<Props, 'onClick' | 'label' | 'iconName' | 'contents' | 'badgeContents' >
+type PrimaryItemForNotificationProps = Omit<PrimaryItemProps, 'onClick' | 'label' | 'iconName' | 'contents' | 'badgeContents' >
 
 // TODO(after v7 release): https://redmine.weseek.co.jp/issues/138463
 export const PrimaryItemForNotification = memo((props: PrimaryItemForNotificationProps) => {

+ 40 - 19
apps/app/src/client/components/Sidebar/SidebarNav/PrimaryItem.tsx

@@ -1,8 +1,11 @@
-import { FC, useCallback } from 'react';
+import { useCallback } from 'react';
 
-import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
-import { useCollapsedContentsOpened, useCurrentSidebarContents } from '~/stores/ui';
+import { useTranslation } from 'next-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
+import type { SidebarContentsType } from '~/interfaces/ui';
+import { SidebarMode } from '~/interfaces/ui';
+import { useCollapsedContentsOpened, useCurrentSidebarContents, useIsMobile } from '~/stores/ui';
 
 const useIndicator = (sidebarMode: SidebarMode, isSelected: boolean): string => {
   const { data: isCollapsedContentsOpened } = useCollapsedContentsOpened();
@@ -14,7 +17,7 @@ const useIndicator = (sidebarMode: SidebarMode, isSelected: boolean): string =>
   return isSelected ? 'active' : '';
 };
 
-export type Props = {
+export type PrimaryItemProps = {
   contents: SidebarContentsType,
   label: string,
   iconName: string,
@@ -24,7 +27,7 @@ export type Props = {
   onClick?: () => void,
 }
 
-export const PrimaryItem: FC<Props> = (props: Props) => {
+export const PrimaryItem = (props: PrimaryItemProps): JSX.Element => {
   const {
     contents, label, iconName, sidebarMode, badgeContents,
     onClick, onHover,
@@ -33,6 +36,8 @@ export const PrimaryItem: FC<Props> = (props: Props) => {
   const { data: currentContents, mutateAndSave: mutateContents } = useCurrentSidebarContents();
 
   const indicatorClass = useIndicator(sidebarMode, contents === currentContents);
+  const { data: isMobile } = useIsMobile();
+  const { t } = useTranslation();
 
   const selectThisItem = useCallback(() => {
     mutateContents(contents, false);
@@ -62,19 +67,35 @@ export const PrimaryItem: FC<Props> = (props: Props) => {
   const labelForTestId = label.toLowerCase().replace(' ', '-');
 
   return (
-    <button
-      type="button"
-      data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
-      className={`btn btn-primary ${indicatorClass}`}
-      onClick={itemClickedHandler}
-      onMouseEnter={mouseEnteredHandler}
-    >
-      <div className="position-relative">
-        { badgeContents != null && (
-          <span className="position-absolute badge rounded-pill bg-primary">{badgeContents}</span>
-        )}
-        <span className="material-symbols-outlined">{iconName}</span>
-      </div>
-    </button>
+    <>
+      <button
+        type="button"
+        data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
+        className={`btn btn-primary ${indicatorClass}`}
+        onClick={itemClickedHandler}
+        onMouseEnter={mouseEnteredHandler}
+        id={labelForTestId}
+      >
+        <div className="position-relative">
+          { badgeContents != null && (
+            <span className="position-absolute badge rounded-pill bg-primary">{badgeContents}</span>
+          )}
+          <span className="material-symbols-outlined">{iconName}</span>
+        </div>
+      </button>
+      {
+        isMobile === false ? (
+          <UncontrolledTooltip
+            autohide
+            placement="right"
+            target={labelForTestId}
+            fade={false}
+          >
+            {t(label)}
+          </UncontrolledTooltip>
+        ) : <></>
+      }
+    </>
   );
 };
+PrimaryItem.displayName = 'PrimaryItem';

+ 25 - 4
apps/app/src/stores/ui.tsx

@@ -56,18 +56,39 @@ export const useSidebarScrollerRef = (initialData?: RefObject<HTMLDivElement>):
   return useSWRStatic<RefObject<HTMLDivElement>, Error>('sidebarScrollerRef', initialData);
 };
 
+//
 export const useIsMobile = (): SWRResponse<boolean, Error> => {
   const key = isClient() ? 'isMobile' : null;
 
-  let configuration;
+  let configuration = {
+    fallbackData: false,
+  };
+
   if (isClient()) {
-    const userAgent = window.navigator.userAgent.toLowerCase();
+
+    // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection
+    let hasTouchScreen = false;
+    hasTouchScreen = ('maxTouchPoints' in navigator) ? navigator?.maxTouchPoints > 0 : false;
+
+    if (!hasTouchScreen) {
+      const mQ = matchMedia?.('(pointer:coarse)');
+      if (mQ?.media === '(pointer:coarse)') {
+        hasTouchScreen = !!mQ.matches;
+      }
+      else {
+      // Only as a last resort, fall back to user agent sniffing
+        const UA = navigator.userAgent;
+        hasTouchScreen = /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA)
+      || /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
+      }
+    }
+
     configuration = {
-      fallbackData: /iphone|ipad|android/.test(userAgent),
+      fallbackData: hasTouchScreen,
     };
   }
 
-  return useStaticSWR<boolean, Error>(key, undefined, configuration);
+  return useSWRStatic<boolean, Error>(key, undefined, configuration);
 };
 
 export const useIsDeviceLargerThanMd = (): SWRResponse<boolean, Error> => {