Remove "forwardRef" usage

This commit is contained in:
schroda
2025-09-24 00:51:13 +02:00
parent bcb5e391f0
commit a2d7091712
19 changed files with 1055 additions and 1101 deletions

View File

@@ -6,16 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import React, {
ForwardedRef,
forwardRef,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
type JSX,
} from 'react';
import React, { ForwardedRef, Ref, useCallback, useLayoutEffect, useMemo, useRef, useState, type JSX } from 'react';
import Grid, { GridTypeMap } from '@mui/material/Grid';
import Box, { BoxProps } from '@mui/material/Box';
import { GridItemProps } from 'react-virtuoso';
@@ -33,11 +24,11 @@ import { GridLayout } from '@/base/Base.types.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx';
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
const GridContainer = ({ children, ref, ...props }: GridTypeMap['props'] & { ref?: Ref<HTMLDivElement> }) => (
<Grid {...props} ref={ref} container spacing={1}>
{children}
</Grid>
));
);
const GridItemContainerWithDimension = (
dimensions: number,
@@ -86,115 +77,108 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
isSelectModeActive?: boolean;
selectedMangaIds?: Required<MangaType['id']>[];
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
ref?: ForwardedRef<HTMLDivElement | null>;
};
const HorizontalGrid = forwardRef(
(
{
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps,
ref: ForwardedRef<HTMLDivElement | null>,
) => (
<Grid
ref={ref}
container
spacing={1}
sx={{
width: '100%',
overflowX: 'auto',
display: '-webkit-inline-box',
flexWrap: 'nowrap',
}}
>
{isLoading ? (
<LoadingPlaceholder />
) : (
mangas.map((manga) => (
<GridItemContainer key={manga.id}>
{createMangaCard(
manga,
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)}
</GridItemContainer>
))
)}
</Grid>
),
const HorizontalGrid = ({
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
ref,
}: DefaultGridProps) => (
<Grid
ref={ref}
container
spacing={1}
sx={{
width: '100%',
overflowX: 'auto',
display: '-webkit-inline-box',
flexWrap: 'nowrap',
}}
>
{isLoading ? (
<LoadingPlaceholder />
) : (
mangas.map((manga) => (
<GridItemContainer key={manga.id}>
{createMangaCard(
manga,
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)}
</GridItemContainer>
))
)}
</Grid>
);
export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location';
const VerticalGrid = forwardRef(
(
{
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
hasNextPage,
loadMore,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps & {
hasNextPage: boolean;
loadMore: () => void;
},
ref: ForwardedRef<HTMLDivElement | null>,
) => (
<>
<Box ref={ref}>
<VirtuosoGridPersisted
persistKey={MANGA_GRID_SNAPSHOT_KEY}
useWindowScroll
increaseViewportBy={window.innerHeight * 0.5}
totalCount={mangas.length}
components={{
List: GridContainer,
Item: GridItemContainer,
}}
endReached={() => loadMore()}
computeItemKey={(index) => mangas[index].id}
itemContent={(index) =>
createMangaCard(
mangas[index],
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)
}
/>
</Box>
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/* eslint-disable-next-line no-nested-ternary */}
{isSelectModeActive && gridLayout === GridLayout.List ? (
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
) : // eslint-disable-next-line no-nested-ternary
isLoading ? (
<LoadingPlaceholder />
) : hasNextPage ? (
<div style={{ height: '75px' }} />
) : null}
</>
),
const VerticalGrid = ({
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
hasNextPage,
loadMore,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
ref,
}: DefaultGridProps & {
hasNextPage: boolean;
loadMore: () => void;
}) => (
<>
<Box ref={ref}>
<VirtuosoGridPersisted
persistKey={MANGA_GRID_SNAPSHOT_KEY}
useWindowScroll
increaseViewportBy={window.innerHeight * 0.5}
totalCount={mangas.length}
components={{
List: GridContainer,
Item: GridItemContainer,
}}
endReached={() => loadMore()}
computeItemKey={(index) => mangas[index].id}
itemContent={(index) =>
createMangaCard(
mangas[index],
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)
}
/>
</Box>
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/* eslint-disable-next-line no-nested-ternary */}
{isSelectModeActive && gridLayout === GridLayout.List ? (
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
) : // eslint-disable-next-line no-nested-ternary
isLoading ? (
<LoadingPlaceholder />
) : hasNextPage ? (
<div style={{ height: '75px' }} />
) : null}
</>
);
export interface IMangaGridProps

View File

@@ -7,7 +7,7 @@
*/
import { useTranslation } from 'react-i18next';
import { BaseSyntheticEvent, ChangeEvent, useMemo, forwardRef, ForwardedRef } from 'react';
import { BaseSyntheticEvent, ChangeEvent, useMemo, ForwardedRef } from 'react';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
@@ -19,94 +19,91 @@ import { SelectableCollectionReturnType } from '@/features/collection/hooks/useS
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MUIUtil } from '@/lib/mui/MUI.util.ts';
export const MangaOptionButton = forwardRef(
(
{
id,
selected,
handleSelection,
asCheckbox = false,
popupState,
}: {
id: number;
selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
asCheckbox?: boolean;
popupState: PopupState;
},
ref: ForwardedRef<HTMLButtonElement | null>,
) => {
const { t } = useTranslation();
export const MangaOptionButton = ({
id,
selected,
handleSelection,
asCheckbox = false,
popupState,
ref,
}: {
id: number;
selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
asCheckbox?: boolean;
popupState: PopupState;
ref?: ForwardedRef<HTMLButtonElement | null>;
}) => {
const { t } = useTranslation();
const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]);
const bindTriggerProps = useMemo(() => bindTrigger(popupState), [popupState]);
const preventDefaultAction = (e: BaseSyntheticEvent) => {
e.stopPropagation();
e.preventDefault();
};
const preventDefaultAction = (e: BaseSyntheticEvent) => {
e.stopPropagation();
e.preventDefault();
};
const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => {
preventDefaultAction(e);
handleSelection?.(id, isSelected);
};
const handleSelectionChange = (e: ChangeEvent, isSelected: boolean) => {
preventDefaultAction(e);
handleSelection?.(id, isSelected);
};
if (!handleSelection) {
if (!handleSelection) {
return null;
}
const isSelected = selected !== null;
if (isSelected) {
if (!asCheckbox) {
return null;
}
const isSelected = selected !== null;
if (isSelected) {
if (!asCheckbox) {
return null;
}
return (
<CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
<Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
</CustomTooltip>
);
}
if (asCheckbox) {
return (
<CustomTooltip title={t('global.button.options')}>
<IconButton
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
aria-label="more"
>
<MoreVertIcon />
</IconButton>
</CustomTooltip>
);
}
return (
<CustomTooltip title={t('global.button.options')}>
<Button
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
className="manga-option-button"
size="small"
variant="contained"
sx={{
minWidth: 'unset',
paddingX: '0',
paddingY: '2.5px',
visibility: popupState.isOpen ? 'visible' : 'hidden',
pointerEvents: 'none',
'@media not (pointer: fine)': {
visibility: 'hidden',
width: 0,
height: 0,
p: 0,
m: 0,
},
}}
>
<MoreVertIcon />
</Button>
<CustomTooltip title={t(selected ? 'global.button.deselect' : 'global.button.select')}>
<Checkbox {...MUIUtil.preventRippleProp()} checked={selected} onChange={handleSelectionChange} />
</CustomTooltip>
);
},
);
}
if (asCheckbox) {
return (
<CustomTooltip title={t('global.button.options')}>
<IconButton
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
aria-label="more"
>
<MoreVertIcon />
</IconButton>
</CustomTooltip>
);
}
return (
<CustomTooltip title={t('global.button.options')}>
<Button
ref={ref}
{...MUIUtil.preventRippleProp(bindTriggerProps, { onClick: preventDefaultAction })}
className="manga-option-button"
size="small"
variant="contained"
sx={{
minWidth: 'unset',
paddingX: '0',
paddingY: '2.5px',
visibility: popupState.isOpen ? 'visible' : 'hidden',
pointerEvents: 'none',
'@media not (pointer: fine)': {
visibility: 'hidden',
width: 0,
height: 0,
p: 0,
m: 0,
},
}}
>
<MoreVertIcon />
</Button>
</CustomTooltip>
);
};