Merge pull request #575 from ONLYOFFICE/feature/scrolling-skeleton
Web: Components: InfiniteLoader: added skeleton when quickly scrollin…
This commit is contained in:
commit
9231d7faae
@ -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"}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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]}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
`;
|
||||||
|
@ -38,4 +38,5 @@ export interface StyledBottomProps {
|
|||||||
|
|
||||||
export interface TileSkeletonProps extends RectangleSkeletonProps {
|
export interface TileSkeletonProps extends RectangleSkeletonProps {
|
||||||
isFolder?: boolean;
|
isFolder?: boolean;
|
||||||
|
isRoom?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
Loading…
Reference in New Issue
Block a user