Skip to content

Commit

Permalink
refactor: use GitHub native popover to substitue ours (#795)
Browse files Browse the repository at this point in the history
* refactor: a basic demo showing how to use native popover to present our content

* chore: proper delay

* fix: decrease leave time to avoid "It looks like the React-rendered..." issue

* fix: popover container may not exist

* refactor: extract code to make a new componenet called NativePopover

* style: add some padding

* refactor popover

* refactor: NativePopover used in repo-header-labels

* chore: remove react-tooltip package

---------

Co-authored-by: Zi1l <[email protected]>
  • Loading branch information
tyn1998 and l1tok authored Apr 29, 2024
1 parent 3b729bf commit 1126d27
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 260 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"react-dom": "^17.0.2",
"react-hot-loader": "^4.13.0",
"react-modal": "3.15.1",
"react-tooltip": "^4.2.21",
"strip-indent": "^4.0.0"
},
"devDependencies": {
Expand Down
90 changes: 90 additions & 0 deletions src/pages/ContentScripts/components/NativePopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { PropsWithChildren, useEffect } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import elementReady from 'element-ready';
import $ from 'jquery';

interface NativePopoverProps extends PropsWithChildren<any> {
anchor: JQuery<HTMLElement>;
width: number;
// for now, only support top-middle
arrowPosition:
| 'top-left'
| 'top-middle'
| 'top-right'
| 'bottom-left'
| 'bottom-middle'
| 'bottom-right';
}

export const NativePopover = ({
anchor,
width,
arrowPosition,
children,
}: NativePopoverProps): JSX.Element => {
useEffect(() => {
(async () => {
await elementReady('div.Popover');
await elementReady('div.Popover-message');
const $popoverContainer = $('div.Popover');
const $popoverContent = $('div.Popover-message');
let popoverTimer: NodeJS.Timeout | null = null;
let leaveTimer: NodeJS.Timeout | null = null;

const showPopover = () => {
popoverTimer = setTimeout(() => {
const anchorOffset = anchor.offset();
const anchorWidth = anchor.outerWidth();
const anchorHeight = anchor.outerHeight();
if (!anchorOffset || !anchorHeight || !anchorWidth) {
return;
}
const { top, left } = anchorOffset;

$popoverContent.css('padding', '10px 5px');
$popoverContent.css('width', width);
$popoverContainer.css('top', `${top + anchorHeight + 10}px`);
$popoverContainer.css(
'left',
`${left - (width - anchorWidth) / 2}px`
);
$popoverContent.attr(
'class',
`Popover-message Box color-shadow-large Popover-message--${arrowPosition}`
);
render(children, $popoverContent[0]);
$popoverContainer.css('display', 'block');
}, 1000);
};

const hidePopover = () => {
popoverTimer && clearTimeout(popoverTimer);
$popoverContent.addClass('Popover-message--large');
if ($popoverContent.children().length > 0) {
unmountComponentAtNode($popoverContent[0]);
}
$popoverContainer.css('display', 'none');
};

anchor[0].addEventListener('mouseenter', () => {
popoverTimer = null;
leaveTimer && clearTimeout(leaveTimer);
showPopover();
});

anchor[0].addEventListener('mouseleave', () => {
leaveTimer = setTimeout(hidePopover, 200);
});

$popoverContainer[0].addEventListener('mouseenter', () => {
leaveTimer && clearTimeout(leaveTimer);
});

$popoverContainer[0].addEventListener('mouseleave', () => {
leaveTimer = setTimeout(hidePopover, 200);
});
})();
}, []);

return <></>;
};
62 changes: 20 additions & 42 deletions src/pages/ContentScripts/features/repo-fork-tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import React from 'react';
import { render, Container } from 'react-dom';
import elementReady from 'element-ready';
import $ from 'jquery';

import features from '../../../../feature-manager';
import getGithubTheme from '../../../../helpers/get-github-theme';
import View from './view';
import { NativePopover } from '../../components/NativePopover';
import elementReady from 'element-ready';
import {
getRepoName,
hasRepoContainerHeader,
isPublicRepoWithMeta,
} from '../../../../helpers/get-repo-info';
import { getForks } from '../../../../api/repo';
import { RepoMeta, metaStore } from '../../../../api/common';
import View from './view';

const githubTheme = getGithubTheme();
import React from 'react';
import { render } from 'react-dom';
import $ from 'jquery';

const featureId = features.getFeatureID(import.meta.url);
let repoName: string;
let forks: any;
Expand All @@ -25,46 +24,25 @@ const getData = async () => {
meta = (await metaStore.get(repoName)) as RepoMeta;
};

const renderTo = (container: Container) => {
render(<View forks={forks} meta={meta} />, container);
};

const init = async (): Promise<void> => {
repoName = getRepoName();
await getData();

const selector = '#fork-button';
await elementReady(selector);
$(selector).attr({
'data-tip': '',
'data-for': 'fork-tooltip',
'data-class': `floating-window ${githubTheme}`,
'data-place': 'left',
'data-effect': 'solid',
'data-delay-hide': 500,
'data-delay-show': 1000,
style: { color: githubTheme === 'light' ? '#24292f' : '#c9d1d9' },
'data-text-color': githubTheme === 'light' ? '#24292F' : '#C9D1D9',
'data-background-color': githubTheme === 'light' ? 'white' : '#161B22',
});
const container = document.createElement('div');
container.id = featureId;
renderTo(container);
(await elementReady('#repository-container-header'))?.append(container);
const forkButtonSelector = '#fork-button';
await elementReady(forkButtonSelector);
const $forkButton = $(forkButtonSelector);
const placeholderElement = $('<div class="NativePopover" />').appendTo(
'body'
)[0];
render(
<NativePopover anchor={$forkButton} width={280} arrowPosition="top-middle">
<View forks={forks} meta={meta} />
</NativePopover>,
placeholderElement
);
};

const restore = async () => {
// Clicking another repo link in one repo will trigger a turbo:visit,
// so in a restoration visit we should be careful of the current repo.
if (repoName !== getRepoName()) {
repoName = getRepoName();
await getData();
}
// Ideally, we should do nothing if the container already exists. But after a tubor
// restoration visit, tooltip cannot be triggered though it exists in DOM tree. One
// way to solve this is to rerender the view to the container. At least this way works.
renderTo($(`#${featureId}`)[0]);
};
const restore = async () => {};

features.add(featureId, {
asLongAs: [isPublicRepoWithMeta, hasRepoContainerHeader],
Expand Down
5 changes: 2 additions & 3 deletions src/pages/ContentScripts/features/repo-fork-tooltip/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import optionsStorage, {
defaults,
} from '../../../../options-storage';
import generateDataByMonth from '../../../../helpers/generate-data-by-month';
import ReactTooltip from 'react-tooltip';
import ForkChart from './ForkChart';
import { RepoMeta } from '../../../../api/common';

Expand All @@ -30,7 +29,7 @@ const View = ({ forks, meta }: Props): JSX.Element | null => {
if (!forks) return null;

return (
<ReactTooltip id="fork-tooltip" clickable={true}>
<>
<div className="chart-title">
{getMessageByLocale('fork_popup_title', options.locale)}
</div>
Expand All @@ -40,7 +39,7 @@ const View = ({ forks, meta }: Props): JSX.Element | null => {
height={130}
data={generateDataByMonth(forks, meta.updatedAt)}
/>
</ReactTooltip>
</>
);
};

Expand Down
12 changes: 6 additions & 6 deletions src/pages/ContentScripts/features/repo-header-labels/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React from 'react';
import { render, Container } from 'react-dom';
import elementReady from 'element-ready';
import $ from 'jquery';

import features from '../../../../feature-manager';
import View from './view';
import elementReady from 'element-ready';
import {
getRepoName,
hasRepoContainerHeader,
Expand All @@ -16,7 +13,10 @@ import {
getContributor,
} from '../../../../api/repo';
import { RepoMeta, metaStore } from '../../../../api/common';
import View from './view';

import React from 'react';
import { render, Container } from 'react-dom';
import $ from 'jquery';

const featureId = features.getFeatureID(import.meta.url);
let repoName: string;
Expand Down
123 changes: 70 additions & 53 deletions src/pages/ContentScripts/features/repo-header-labels/view.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import React, { useState, useEffect } from 'react';

import getGithubTheme from '../../../../helpers/get-github-theme';
import getMessageByLocale from '../../../../helpers/get-message-by-locale';
import { isNull } from '../../../../helpers/is-null';
import { numberWithCommas } from '../../../../helpers/formatter';
import { NativePopover } from '../../components/NativePopover';
import optionsStorage, {
HypercrxOptions,
defaults,
} from '../../../../options-storage';
import { rocketLight, rocketDark } from './base64';
import ReactTooltip from 'react-tooltip';
import generateDataByMonth from '../../../../helpers/generate-data-by-month';
import ActivityChart from './ActivityChart';
import OpenRankChart from './OpenRankChart';
import ParticipantChart from './ParticipantChart';
import ContributorChart from './ContributorChart';
import { RepoMeta } from '../../../../api/common';

import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
import $ from 'jquery';

const githubTheme = getGithubTheme();

interface Props {
Expand All @@ -36,16 +38,77 @@ const View = ({
}: Props): JSX.Element | null => {
const [options, setOptions] = useState<HypercrxOptions>(defaults);

useEffect(() => {
ReactTooltip.rebuild();
}, []);

useEffect(() => {
(async function () {
setOptions(await optionsStorage.getAll());
})();
}, []);

useEffect(() => {
const placeholderElement = $('<div class="NativePopover" />').appendTo(
'body'
)[0];
render(
<>
<NativePopover
anchor={$('#activity-header-label')}
width={280}
arrowPosition="top-middle"
>
<div className="chart-title">
{getMessageByLocale('header_label_activity', options.locale)}
</div>
<ActivityChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={activityData}
/>
</NativePopover>
<NativePopover
anchor={$('#OpenRank-header-label')}
width={280}
arrowPosition="top-middle"
>
<div className="chart-title">
{getMessageByLocale('header_label_OpenRank', options.locale)}
</div>
<OpenRankChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={openrankData}
/>
</NativePopover>
<NativePopover
anchor={$('#participant-header-label')}
width={280}
arrowPosition="top-middle"
>
<div className="chart-title">
{getMessageByLocale('header_label_contributor', options.locale)}
</div>
<ContributorChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={contributorData}
/>
<div className="chart-title">
{getMessageByLocale('header_label_participant', options.locale)}
</div>
<ParticipantChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={participantData}
/>
</NativePopover>
</>,
placeholderElement
);
}, []);

if (
isNull(activity) ||
isNull(openrank) ||
Expand Down Expand Up @@ -147,52 +210,6 @@ const View = ({
{numberWithCommas(contributorData[contributorData.length - 1][1])}/
{numberWithCommas(participantData[participantData.length - 1][1])}
</span>
<ReactTooltip
id="activity-tooltip"
className={githubTheme === 'dark' ? 'custom-react-tooltip' : ''}
clickable={true}
>
<div className="chart-title">
{getMessageByLocale('header_label_activity', options.locale)}
</div>
<ActivityChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={activityData}
/>
</ReactTooltip>
<ReactTooltip id="openrank-tooltip" clickable={true}>
<div className="chart-title">
{getMessageByLocale('header_label_OpenRank', options.locale)}
</div>
<OpenRankChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={openrankData}
/>
</ReactTooltip>
<ReactTooltip id="participant-tooltip" clickable={true}>
<div className="chart-title">
{getMessageByLocale('header_label_contributor', options.locale)}
</div>
<ContributorChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={contributorData}
/>
<div className="chart-title">
{getMessageByLocale('header_label_participant', options.locale)}
</div>
<ParticipantChart
theme={githubTheme as 'light' | 'dark'}
width={270}
height={130}
data={participantData}
/>
</ReactTooltip>
</div>
);
};
Expand Down
Loading

0 comments on commit 1126d27

Please sign in to comment.