Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/components/issues/IssuesList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import {
DebouncedSearchInput,
useDebouncedSearchDraft,
Expand Down Expand Up @@ -272,10 +278,23 @@ const IssuesList: React.FC<IssuesListProps> = ({
);

const isLargeScreen = useMediaQuery(theme.breakpoints.up('xl'));
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
useEffect(() => {
setPortalTarget(document.getElementById('tabs-options-portal'));
}, []);
// Seed synchronously so tab/filter re-mounts don't flash an empty Filters
// slot in the sidebar between the first render and the post-paint effect.
// useLayoutEffect handles the rare initial-mount miss before paint.
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(() =>
typeof document === 'undefined'
? null
: document.getElementById('tabs-options-portal'),
);
// Always validate (don't just skip when set) — on cross-page navigation
// the useState initializer runs while the previous page's
// #tabs-options-portal is still attached, so portalTarget can be a stale
// node that gets removed at commit. Re-resolve before paint.
useLayoutEffect(() => {
const current = document.getElementById('tabs-options-portal');
if (current === portalTarget) return;
setPortalTarget(current);
}, [portalTarget]);

const [optionsAnchorEl, setOptionsAnchorEl] = useState<HTMLElement | null>(
null,
Expand Down
22 changes: 18 additions & 4 deletions src/components/leaderboard/TopMinersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
useRef,
Expand Down Expand Up @@ -378,7 +379,14 @@ const TopMinersTable: React.FC<TopMinersTableProps> = ({
}, [filteredMiners.length, visibleCount, setVisibleCount]);

const isLargeScreen = useMediaQuery(theme.breakpoints.up('xl'));
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
// Seed synchronously so leaderboard timeline/filter switches don't flash
// an empty Filters slot in the sidebar. useLayoutEffect catches the rare
// initial-mount miss before paint.
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(() =>
typeof document === 'undefined'
? null
: document.getElementById('tabs-options-portal'),
);
const observerTarget = useRef<HTMLDivElement>(null);
const [stackedLayoutPage, setStackedLayoutPage] = useState(0);

Expand Down Expand Up @@ -441,9 +449,15 @@ const TopMinersTable: React.FC<TopMinersTableProps> = ({
/>
) : null;

useEffect(() => {
setPortalTarget(document.getElementById('tabs-options-portal'));
}, []);
// Always validate (don't just skip when set) — on cross-page navigation
// the useState initializer runs while the previous page's
// #tabs-options-portal is still attached, so portalTarget can be a stale
// node that gets removed at commit. Re-resolve before paint.
useLayoutEffect(() => {
const current = document.getElementById('tabs-options-portal');
if (current === portalTarget) return;
setPortalTarget(current);
}, [portalTarget]);

useEffect(() => {
if (!isLargeScreen || !observerTarget.current || remainingMiners <= 0) {
Expand Down
24 changes: 20 additions & 4 deletions src/pages/WatchlistPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
useRef,
Expand Down Expand Up @@ -572,12 +573,27 @@ const OptionsLabel: React.FC<{ children: React.ReactNode }> = ({

/* ─── WatchlistPortal: sidebar panel on xl, popover button otherwise ─── */
const WatchlistPortal: React.FC<WatchlistOptionsButtonProps> = (props) => {
const [target, setTarget] = useState<HTMLElement | null>(null);
// Seed from the DOM synchronously so on tab switches (where the parent's
// #tabs-options-portal Box is already in the document) the first render
// already returns the Portal — no intermediate "fallback" paint, no flash
// of an empty Filters slot in the sidebar.
const [target, setTarget] = useState<HTMLElement | null>(() =>
typeof document === 'undefined'
? null
: document.getElementById('tabs-options-portal'),
);
const isLargeScreen = useMediaQuery(theme.breakpoints.up('xl'));

useEffect(() => {
setTarget(document.getElementById('tabs-options-portal'));
}, []);
// Always validate (don't just skip when set) — on cross-page navigation
// the useState initializer runs while the OLD page's #tabs-options-portal
// is still attached, so `target` can be a stale node that gets removed at
// commit. useLayoutEffect re-resolves before paint and replaces the
// stale node with the current page's portal target.
useLayoutEffect(() => {
const current = document.getElementById('tabs-options-portal');
if (current === target) return;
setTarget(current);
}, [target]);

if (target && isLargeScreen) {
return (
Expand Down
Loading