Merge pull request #575 from ONLYOFFICE/feature/scrolling-skeleton

Web: Components: InfiniteLoader: added skeleton when quickly scrollin…
This commit is contained in:
Alexey Safronov 2024-08-06 18:47:22 +04:00 committed by GitHub
commit 9231d7faae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 230 additions and 10 deletions

View File

@ -196,6 +196,7 @@ const InfiniteGrid = (props) => {
key={key} key={key}
className={`tiles-loader ${type}`} className={`tiles-loader ${type}`}
isFolder={type === "isFolder"} isFolder={type === "isFolder"}
isRoom={type === "isRoom"}
/>, />,
); );
} }

View File

@ -28,7 +28,7 @@ import { List } from "react-virtualized";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { Base } from "../../themes"; import { Base } from "../../themes";
import { mobile, tablet } from "../../utils"; import { desktop, mobile, tablet } from "../../utils";
import { TViewAs } from "../../types"; import { TViewAs } from "../../types";
const StyledScroll = styled.div` const StyledScroll = styled.div`
@ -157,4 +157,28 @@ StyledScroll.defaultProps = {
theme: Base, theme: Base,
}; };
export { StyledScroll, StyledList }; const paddingCss = css`
@media ${desktop} {
margin-inline-start: 1px;
padding-inline-end: 0;
}
@media ${tablet} {
margin-inline-start: -1px;
}
`;
const StyledItem = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(216px, 1fr));
gap: 14px 16px;
width: 100%;
@media ${tablet} {
gap: 14px;
}
${paddingCss};
`;
export { StyledScroll, StyledList, StyledItem };

View File

@ -24,27 +24,66 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react"; import { useState, useEffect } from "react";
import { isMobile } from "../../utils"; import { isMobile } from "../../utils";
import ListComponent from "./sub-components/List"; import ListComponent from "./sub-components/List";
import GridComponent from "./sub-components/Grid"; import GridComponent from "./sub-components/Grid";
import { InfiniteLoaderProps } from "./InfiniteLoader.types"; import { InfiniteLoaderProps } from "./InfiniteLoader.types";
import { MAX_INFINITE_LOADER_SHIFT } from "../../utils/device";
const InfiniteLoaderComponent = (props: InfiniteLoaderProps) => { const InfiniteLoaderComponent = (props: InfiniteLoaderProps) => {
const { viewAs, isLoading } = props; const { viewAs, isLoading } = props;
const [scrollTop, setScrollTop] = useState(0);
const [showSkeleton, setShowSkeleton] = useState(false);
const scroll = isMobile() const scroll = isMobile()
? document.querySelector("#customScrollBar .scroll-wrapper > .scroller") ? document.querySelector("#customScrollBar .scroll-wrapper > .scroller")
: document.querySelector("#sectionScroll .scroll-wrapper > .scroller"); : document.querySelector("#sectionScroll .scroll-wrapper > .scroller");
const onScroll = (e: Event) => {
const eventTarget = e.target as HTMLElement;
const currentScrollTop = eventTarget.scrollTop;
setScrollTop(currentScrollTop ?? 0);
const scrollShift = scrollTop - currentScrollTop;
if (
scrollShift > MAX_INFINITE_LOADER_SHIFT ||
scrollShift < -MAX_INFINITE_LOADER_SHIFT
) {
setShowSkeleton(true);
setTimeout(() => {
setShowSkeleton(false);
}, 200);
}
};
useEffect(() => {
if (scroll) scroll.addEventListener("scroll", onScroll);
return () => {
if (scroll) scroll.removeEventListener("scroll", onScroll);
};
});
if (isLoading) return null; if (isLoading) return null;
return viewAs === "tile" ? ( return viewAs === "tile" ? (
<GridComponent scroll={scroll ?? window} {...props} /> <GridComponent
scroll={scroll ?? window}
showSkeleton={showSkeleton}
{...props}
/>
) : ( ) : (
<ListComponent scroll={scroll ?? window} {...props} /> <ListComponent
scroll={scroll ?? window}
showSkeleton={showSkeleton}
{...props}
/>
); );
}; };

View File

@ -42,6 +42,7 @@ export interface InfiniteLoaderProps {
className?: string; className?: string;
infoPanelVisible?: boolean; infoPanelVisible?: boolean;
countTilesInRow?: number; countTilesInRow?: number;
showSkeleton?: boolean;
} }
export interface ListComponentProps extends InfiniteLoaderProps { export interface ListComponentProps extends InfiniteLoaderProps {

View File

@ -26,8 +26,10 @@
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
import { InfiniteLoader, WindowScroller, List } from "react-virtualized"; import { InfiniteLoader, WindowScroller, List } from "react-virtualized";
import { StyledList } from "../InfiniteLoader.styled"; import { StyledItem, StyledList } from "../InfiniteLoader.styled";
import { GridComponentProps } from "../InfiniteLoader.types"; import { GridComponentProps } from "../InfiniteLoader.types";
import { TileSkeleton } from "../../../skeletons/tiles";
import { RectangleSkeleton } from "../../../skeletons";
const GridComponent = ({ const GridComponent = ({
hasMoreFiles, hasMoreFiles,
@ -39,12 +41,13 @@ const GridComponent = ({
children, children,
className, className,
scroll, scroll,
showSkeleton,
}: GridComponentProps) => { }: GridComponentProps) => {
const loaderRef = useRef<InfiniteLoader | null>(null); const loaderRef = useRef<InfiniteLoader | null>(null);
const listRef = useRef<List | null>(null); const listRef = useRef<List | null>(null);
useEffect(() => { useEffect(() => {
listRef?.current?.recomputeRowHeights(); // listRef?.current?.recomputeRowHeights(); //TODO: return there will be problems with the height of the tile when clicking on the backspace
}); });
const isItemLoaded = useCallback( const isItemLoaded = useCallback(
@ -58,11 +61,50 @@ const GridComponent = ({
index, index,
style, style,
key, key,
isScrolling,
}: { }: {
index: number; index: number;
style: React.CSSProperties; style: React.CSSProperties;
key: string; key: string;
isScrolling: boolean;
}) => { }) => {
const elem = children[index] as React.ReactElement;
const itemClassNames = elem.props?.className;
const isFolder = itemClassNames?.includes("isFolder");
const isRoom = itemClassNames?.includes("isRoom");
const isHeader =
itemClassNames?.includes("folder_header") ||
itemClassNames?.includes("files_header");
if (isScrolling && showSkeleton) {
const list = [];
let i = 0;
if (isHeader) {
return (
<div key={key} style={style}>
<StyledItem>
<RectangleSkeleton height="22px" width="100px" animate />
</StyledItem>
</div>
);
}
while (i < countTilesInRow) {
list.push(
<TileSkeleton key={key} isFolder={isFolder} isRoom={isRoom} />,
);
i += 1;
}
return (
<div key={key} style={style}>
<StyledItem>{list.map((item) => item)}</StyledItem>
</div>
);
}
return ( return (
<div className="window-item" style={style} key={key}> <div className="window-item" style={style} key={key}>
{children[index]} {children[index]}

View File

@ -47,6 +47,7 @@ const ListComponent = ({
className, className,
scroll, scroll,
infoPanelVisible, infoPanelVisible,
showSkeleton,
}: ListComponentProps) => { }: ListComponentProps) => {
const loaderRef = useRef<InfiniteLoader | null>(null); const loaderRef = useRef<InfiniteLoader | null>(null);
const listRef = useRef<List | null>(null); const listRef = useRef<List | null>(null);
@ -89,13 +90,16 @@ const ListComponent = ({
key, key,
index, index,
style, style,
isScrolling,
}: { }: {
key: string; key: string;
index: number; index: number;
style: React.CSSProperties; style: React.CSSProperties;
isScrolling: boolean;
}) => { }) => {
const isLoaded = isItemLoaded({ index }); const isLoaded = isItemLoaded({ index });
if (!isLoaded) return getLoader(style, key); if (!isLoaded || (isScrolling && showSkeleton))
return getLoader(style, key);
return ( return (
<div className="row-list-item window-item" style={style} key={key}> <div className="row-list-item window-item" style={style} key={key}>
@ -108,10 +112,12 @@ const ListComponent = ({
index, index,
style, style,
key, key,
isScrolling,
}: { }: {
index: number; index: number;
style: React.CSSProperties; style: React.CSSProperties;
key: string; key: string;
isScrolling: boolean;
}) => { }) => {
if (!columnInfoPanelStorageName || !columnStorageName) { if (!columnInfoPanelStorageName || !columnStorageName) {
throw new Error("columnStorageName is required for a table view"); throw new Error("columnStorageName is required for a table view");
@ -122,7 +128,8 @@ const ListComponent = ({
: localStorage.getItem(columnStorageName); : localStorage.getItem(columnStorageName);
const isLoaded = isItemLoaded({ index }); const isLoaded = isItemLoaded({ index });
if (!isLoaded) return getLoader(style, key); if (!isLoaded || (isScrolling && showSkeleton))
return getLoader(style, key);
return ( return (
<div <div

View File

@ -27,11 +27,19 @@
import React from "react"; import React from "react";
import { RectangleSkeleton } from "@docspace/shared/skeletons"; import { RectangleSkeleton } from "@docspace/shared/skeletons";
import { StyledTile, StyledBottom, StyledMainContent } from "./Tiles.styled"; import {
StyledTile,
StyledBottom,
StyledMainContent,
StyledRoomTile,
StyledRoomTileTopContent,
StyledRoomTileBottomContent,
} from "./Tiles.styled";
import type { TileSkeletonProps } from "./Tiles.types"; import type { TileSkeletonProps } from "./Tiles.types";
export const TileSkeleton = ({ export const TileSkeleton = ({
isFolder, isFolder,
isRoom,
title, title,
borderRadius, borderRadius,
backgroundColor, backgroundColor,
@ -84,6 +92,79 @@ export const TileSkeleton = ({
/> />
</StyledBottom> </StyledBottom>
</StyledTile> </StyledTile>
) : isRoom ? (
<StyledTile {...rest}>
<StyledRoomTile>
<StyledRoomTileTopContent>
<RectangleSkeleton
className="first-content"
title={title}
width="32px"
height="32px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate
/>
<RectangleSkeleton
className="second-content"
title={title}
height="22px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate
/>
<RectangleSkeleton
className="option-button"
title={title}
height="16px"
width="16px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate
/>
</StyledRoomTileTopContent>
<StyledRoomTileBottomContent>
<RectangleSkeleton
className="main-content"
title={title}
height="24px"
width="50px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate
/>
<RectangleSkeleton
className="main-content"
title={title}
height="24px"
width="50px"
borderRadius={borderRadius}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
backgroundOpacity={backgroundOpacity}
foregroundOpacity={foregroundOpacity}
speed={speed}
animate
/>
</StyledRoomTileBottomContent>
</StyledRoomTile>
</StyledTile>
) : ( ) : (
<StyledTile {...rest}> <StyledTile {...rest}>
<StyledMainContent> <StyledMainContent>

View File

@ -120,3 +120,26 @@ export const StyledTilesWrapper = styled.div`
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-gap: 16px; grid-gap: 16px;
`; `;
export const StyledRoomTile = styled.div`
border: ${(props) => props.theme.filesSection.tilesView.tile.border};
border-radius: 6px;
height: 120px;
`;
export const StyledRoomTileTopContent = styled.div`
display: grid;
grid-template-columns: 32px 1fr 24px;
gap: 8px;
align-items: center;
height: 61px;
border-bottom: ${(props) => props.theme.filesSection.tilesView.tile.border};
padding: 0 8px 0 16px;
`;
export const StyledRoomTileBottomContent = styled.div`
display: flex;
align-items: center;
padding: 16px;
gap: 4px;
`;

View File

@ -38,4 +38,5 @@ export interface StyledBottomProps {
export interface TileSkeletonProps extends RectangleSkeletonProps { export interface TileSkeletonProps extends RectangleSkeletonProps {
isFolder?: boolean; isFolder?: boolean;
isRoom?: boolean;
} }

View File

@ -26,6 +26,7 @@
export const INFO_PANEL_WIDTH = 400; export const INFO_PANEL_WIDTH = 400;
export const TABLE_HEADER_HEIGHT = 40; export const TABLE_HEADER_HEIGHT = 40;
export const MAX_INFINITE_LOADER_SHIFT = 800;
export function checkIsSSR() { export function checkIsSSR() {
return typeof window === "undefined"; return typeof window === "undefined";