Repository: gitify-app/gitify
Pull request #1006 (2024-04-14 12:10:02)

feat: tooltips component with example
![Screenshot 2024-04-14 at 8 08 43 AM](https://github.com/gitify-app/gitify/assets/386277/c9f1905e-cf8e-4599-86e6-c2de0e4a4c04)


setchy: Will merge following #1005

Patch:
diff --git a/src/components/fields/Checkbox.tsx b/src/components/fields/Checkbox.tsx
index 20ca3164..e420d5ed 100644
--- a/src/components/fields/Checkbox.tsx
+++ b/src/components/fields/Checkbox.tsx
@@ -1,9 +1,11 @@
 import type { FC, ReactNode } from 'react';
+import { Tooltip } from './Tooltip';
 
 export interface ICheckbox {
   name: string;
   label: string;
   helpText?: ReactNode | string;
+  tooltip?: ReactNode | string;
   checked: boolean;
   disabled?: boolean;
   onChange: (evt: React.ChangeEvent<HTMLInputElement>) => void;
@@ -33,6 +35,7 @@ export const Checkbox: FC<ICheckbox> = (props: ICheckbox) => {
             }
           >
             {props.label}
+            {props.tooltip && <Tooltip tooltip={props.tooltip} />}
           </label>
         </div>
       </div>
diff --git a/src/components/fields/Tooltip.tsx b/src/components/fields/Tooltip.tsx
new file mode 100644
index 00000000..3a9232fa
--- /dev/null
+++ b/src/components/fields/Tooltip.tsx
@@ -0,0 +1,28 @@
+import { QuestionIcon } from '@primer/octicons-react';
+import { type FC, type ReactNode, useState } from 'react';
+
+export interface ITooltip {
+  tooltip: ReactNode | string;
+}
+
+export const Tooltip: FC<ITooltip> = (props: ITooltip) => {
+  const [showTooltip, setShowTooltip] = useState(false);
+
+  return (
+    <span
+      title="tooltip"
+      className="relative"
+      onMouseEnter={() => setShowTooltip(true)}
+      onMouseLeave={() => setShowTooltip(false)}
+    >
+      <QuestionIcon className="text-blue-500 ml-1" />
+      {showTooltip && (
+        <div className="absolute bg-white dark:bg-gray-sidebar border border-gray-300 rounded p-2 shadow">
+          <div className="text-gray-700 dark:text-white text-xs text-left">
+            {props.tooltip}
+          </div>
+        </div>
+      )}
+    </span>
+  );
+};
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index 25a975eb..28228dfc 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -142,6 +142,19 @@ export const SettingsRoute: FC = () => {
               updateSetting('detailedNotifications', evt.target.checked)
             }
             disabled={!repoScope}
+            tooltip={
+              <div>
+                <div className="pb-3">
+                  Enrich notifications with author or last commenter profile
+                  information, state and GitHub-like colors.
+                </div>
+                <div className="text-orange-600">
+                  ⚠️ Users with a large number of unread notifications <i>may</i>{' '}
+                  experience rate limiting under certain circumstances. Disable
+                  this setting if you experience this.
+                </div>
+              </div>
+            }
           />
           <Checkbox
             name="showAccountHostname"


Test patch:
diff --git a/src/components/fields/Tooltip.test.tsx b/src/components/fields/Tooltip.test.tsx
new file mode 100644
index 00000000..863847ee
--- /dev/null
+++ b/src/components/fields/Tooltip.test.tsx
@@ -0,0 +1,28 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+
+import * as TestRenderer from 'react-test-renderer';
+
+import { type ITooltip, Tooltip } from './Tooltip';
+
+describe('components/fields/Tooltip.tsx', () => {
+  const props: ITooltip = {
+    tooltip: 'This is some tooltip text',
+  };
+
+  it('should render', () => {
+    const tree = TestRenderer.create(<Tooltip {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should display on mouse enter / leave', () => {
+    render(<Tooltip {...props} />);
+
+    const tooltipElement = screen.getByTitle('tooltip');
+
+    fireEvent.mouseEnter(tooltipElement);
+    expect(tooltipElement).toMatchSnapshot();
+
+    fireEvent.mouseLeave(tooltipElement);
+    expect(tooltipElement).toMatchSnapshot();
+  });
+});
diff --git a/src/components/fields/__snapshots__/Tooltip.test.tsx.snap b/src/components/fields/__snapshots__/Tooltip.test.tsx.snap
new file mode 100644
index 00000000..c2ed6592
--- /dev/null
+++ b/src/components/fields/__snapshots__/Tooltip.test.tsx.snap
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/fields/Tooltip.tsx should display on mouse enter / leave 1`] = `
+<span
+  class="relative"
+  title="tooltip"
+>
+  <svg
+    aria-hidden="true"
+    class="text-blue-500 ml-1"
+    fill="currentColor"
+    focusable="false"
+    height="16"
+    style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+    viewBox="0 0 16 16"
+    width="16"
+  >
+    <path
+      d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+    />
+  </svg>
+  <div
+    class="absolute bg-white dark:bg-gray-sidebar border border-gray-300 rounded p-2 shadow"
+  >
+    <div
+      class="text-gray-700 dark:text-white text-xs text-left"
+    >
+      This is some tooltip text
+    </div>
+  </div>
+</span>
+`;
+
+exports[`components/fields/Tooltip.tsx should display on mouse enter / leave 2`] = `
+<span
+  class="relative"
+  title="tooltip"
+>
+  <svg
+    aria-hidden="true"
+    class="text-blue-500 ml-1"
+    fill="currentColor"
+    focusable="false"
+    height="16"
+    style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+    viewBox="0 0 16 16"
+    width="16"
+  >
+    <path
+      d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+    />
+  </svg>
+</span>
+`;
+
+exports[`components/fields/Tooltip.tsx should render 1`] = `
+<span
+  className="relative"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+  title="tooltip"
+>
+  <svg
+    aria-hidden="true"
+    className="text-blue-500 ml-1"
+    fill="currentColor"
+    focusable="false"
+    height={16}
+    style={
+      {
+        "display": "inline-block",
+        "overflow": "visible",
+        "userSelect": "none",
+        "verticalAlign": "text-bottom",
+      }
+    }
+    viewBox="0 0 16 16"
+    width={16}
+  >
+    <path
+      d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+    />
+  </svg>
+</span>
+`;
diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap
index 61785791..0f3cbb6d 100644
--- a/src/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/routes/__snapshots__/Settings.test.tsx.snap
@@ -23,6 +23,25 @@ exports[`routes/Settings.tsx should not be able to enable detailed notifications
       style="text-decoration: line-through;"
     >
       Detailed notifications (requires repo scope)
+      <span
+        class="relative"
+        title="tooltip"
+      >
+        <svg
+          aria-hidden="true"
+          class="text-blue-500 ml-1"
+          fill="currentColor"
+          focusable="false"
+          height="16"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="16"
+        >
+          <path
+            d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+          />
+        </svg>
+      </span>
     </label>
   </div>
 </div>
@@ -175,6 +194,25 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = `
               style=""
             >
               Detailed notifications
+              <span
+                class="relative"
+                title="tooltip"
+              >
+                <svg
+                  aria-hidden="true"
+                  class="text-blue-500 ml-1"
+                  fill="currentColor"
+                  focusable="false"
+                  height="16"
+                  style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+                  viewBox="0 0 16 16"
+                  width="16"
+                >
+                  <path
+                    d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+                  />
+                </svg>
+              </span>
             </label>
           </div>
         </div>