Просмотр исходного кода

Merge pull request #7055 from weseek/imprv/implement-skeltons-of-sidebar

imprv: implement skeltons of sidebar
kymn 3 лет назад
Родитель
Сommit
1814070286

+ 4 - 2
packages/app/src/components/Sidebar.tsx

@@ -17,6 +17,7 @@ import {
 import DrawerToggler from './Navbar/DrawerToggler';
 import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
 import { SidebarNav } from './Sidebar/SidebarNav';
+import { SidebarSkeleton } from './Sidebar/Skeleton/SidebarSkeleton';
 import { StickyStretchableScrollerProps } from './StickyStretchableScroller';
 
 import styles from './Sidebar.module.scss';
@@ -57,8 +58,9 @@ const GlobalNavigation = () => {
 
 const SidebarContentsWrapper = () => {
   const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('./StickyStretchableScroller')
-    .then(mod => mod.StickyStretchableScroller), { ssr: false });
-  const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
+    .then(mod => mod.StickyStretchableScroller), { ssr: false, loading: () => <SidebarSkeleton /> });
+  const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents')
+    .then(mod => mod.SidebarContents), { ssr: false, loading: () => <SidebarSkeleton /> });
   const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
 
   const calcViewHeight = useCallback(() => {

+ 12 - 0
packages/app/src/components/Sidebar/CustomSidebar.module.scss

@@ -1,7 +1,19 @@
 @use '~/styles/organisms/wiki-custom-sidebar.scss';
+@use '~/styles/mixins' as *;
 
 .grw-custom-sidebar-content :global {
   .wiki {
     @extend %grw-custom-sidebar-content;
   }
+
+  .grw-custom-sidebar-skeleton-text {
+    @include grw-skeleton-text($font-size:15px, $line-height:21.42px);
+    max-width: 160px;
+    margin: 15px 0;
+  }
+
+  .grw-custom-sidebar-skeleton-text-full {
+    @extend .grw-custom-sidebar-skeleton-text;
+    max-width: 100%;
+  }
 }

+ 11 - 15
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -10,6 +10,8 @@ import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from '../Page/RevisionRenderer';
 
+import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
+import CustomSidebarContentSkeleton from './Skeleton/CustomSidebarContentSkeleton';
 
 import styles from './CustomSidebar.module.scss';
 
@@ -19,11 +21,9 @@ const logger = loggerFactory('growi:cli:CustomSidebar');
 
 const SidebarNotFound = () => {
   return (
-    <div className="grw-sidebar-content-header h5 text-center p-3">
+    <div className="grw-sidebar-content-header h5 text-center py-3">
       <Link href="/Sidebar#edit">
-        <a href="/Sidebar#edit">
-          <i className="icon-magic-wand"></i> Create <strong>/Sidebar</strong> page
-        </a>
+        <a><i className="icon-magic-wand"></i> Create <strong>/Sidebar</strong> page</a>
       </Link>
     </div>
   );
@@ -43,28 +43,24 @@ const CustomSidebar: FC = () => {
   const markdown = (page?.revision as IRevision | undefined)?.body;
 
   return (
-    <>
-      <div className="grw-sidebar-content-header p-3 d-flex">
+    <div className="px-3">
+      <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">
           {t('CustomSidebar')}
-          <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
+          <Link href="/Sidebar"><a className="h6 ml-2"><i className="icon-pencil"></i></a></Link>
         </h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
-          <i className="icon icon-reload"></i>
-        </button>
+        <SidebarHeaderReloadButton onClick={() => mutate()} />
       </div>
 
       {
         isLoading && (
-          <div className="text-muted text-center">
-            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
-          </div>
+          <CustomSidebarContentSkeleton />
         )
       }
 
       {
         (!isLoading && markdown != null) && (
-          <div className={`p-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
+          <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
             <RevisionRenderer
               rendererOptions={rendererOptions}
               markdown={markdown}
@@ -78,7 +74,7 @@ const CustomSidebar: FC = () => {
           <SidebarNotFound />
         )
       }
-    </>
+    </div>
   );
 };
 

+ 22 - 20
packages/app/src/components/Sidebar/PageTree.tsx

@@ -10,6 +10,17 @@ import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 import ItemsTree from './PageTree/ItemsTree';
 import { PrivateLegacyPagesLink } from './PageTree/PrivateLegacyPagesLink';
+import PageTreeContentSkeleton from './Skeleton/PageTreeContentSkeleton';
+
+const PageTreeHeader = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="grw-sidebar-content-header py-3 d-flex">
+      <h3 className="mb-0">{t('Page Tree')}</h3>
+    </div>
+  );
+};
 
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
@@ -24,14 +35,10 @@ const PageTree: FC = memo(() => {
 
   if (migrationStatus == null) {
     return (
-      <>
-        <div className="grw-sidebar-content-header p-3">
-          <h3 className="mb-0">{t('Page Tree')}</h3>
-        </div>
-        <div className="text-muted text-center mt-3">
-          <i className="fa fa-lg fa-spinner fa-pulse mr-1"></i>
-        </div>
-      </>
+      <div className="px-3">
+        <PageTreeHeader />
+        <PageTreeContentSkeleton />
+      </div>
     );
   }
 
@@ -39,15 +46,13 @@ const PageTree: FC = memo(() => {
     // TODO : improve design
     // Story : https://redmine.weseek.co.jp/issues/83755
     return (
-      <>
-        <div className="grw-sidebar-content-header p-3">
-          <h3 className="mb-0">{t('Page Tree')}</h3>
-        </div>
+      <div className="px-3">
+        <PageTreeHeader />
         <div className="mt-5 mx-2 text-center">
           <h3 className="text-gray">{t('v5_page_migration.page_tree_not_avaliable')}</h3>
           <a href="/admin">{t('v5_page_migration.go_to_settings')}</a>
         </div>
-      </>
+      </div>
     );
   }
 
@@ -61,11 +66,8 @@ const PageTree: FC = memo(() => {
   const path = currentPath || '/';
 
   return (
-    <>
-      <div className="grw-sidebar-content-header p-3">
-        <h3 className="mb-0">{t('Page Tree')}</h3>
-      </div>
-
+    <div className="px-3">
+      <PageTreeHeader />
       <ItemsTree
         isEnableActions={!isGuestUser}
         targetPath={path}
@@ -74,13 +76,13 @@ const PageTree: FC = memo(() => {
       />
 
       {!isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
-        <div className="grw-pagetree-footer border-top p-3 w-100">
+        <div className="grw-pagetree-footer border-top py-3 w-100">
           <div className="private-legacy-pages-link px-3 py-2">
             <PrivateLegacyPagesLink />
           </div>
         </div>
       )}
-    </>
+    </div>
   );
 });
 

+ 14 - 1
packages/app/src/components/Sidebar/PageTree/ItemsTree.module.scss

@@ -1,9 +1,22 @@
 @use '~/styles/variables' as var;
+@use '~/styles/mixins' as *;
 $grw-sidebar-content-header-height: 58px;
 $grw-sidebar-content-footer-height: 50px;
 $grw-pagetree-item-padding-left: 10px;
+$grw-pagetree-item-container-height: 40px;
 
 .grw-pagetree {
+
+  .grw-pagetree-item-skeleton-text {
+    @include grw-skeleton-text($font-size:16px, $line-height:$grw-pagetree-item-container-height);
+    padding-left: 12px;
+  }
+
+  .grw-pagetree-item-skeleton-text-child {
+    @extend .grw-pagetree-item-skeleton-text;
+    padding-left: 12px + $grw-pagetree-item-padding-left;
+  }
+
   :global {
     min-height: calc(100vh - (var.$grw-navbar-height + var.$grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
 
@@ -56,7 +69,7 @@ $grw-pagetree-item-padding-left: 10px;
     .grw-pagetree-item-container {
       .grw-triangle-container {
         min-width: 35px;
-        height: 40px;
+        height: $grw-pagetree-item-container-height;
       }
     }
   }

+ 4 - 2
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -24,6 +24,8 @@ import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
+import PageTreeContentSkeleton from '../Skeleton/PageTreeContentSkeleton';
+
 import Item from './Item';
 import { ItemNode } from './ItemNode';
 
@@ -272,7 +274,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
   if (initialItemNode != null) {
     return (
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group p-3`} ref={rootElemRef}>
+      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
         <Item
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
@@ -288,7 +290,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     );
   }
 
-  return <></>;
+  return <PageTreeContentSkeleton />;
 };
 
 export default ItemsTree;

+ 17 - 0
packages/app/src/components/Sidebar/RecentChanges.module.scss

@@ -1,3 +1,5 @@
+@use '~/styles/mixins' as *;
+
 .grw-recent-changes-resize-button :global {
   font-size: 12px;
   line-height: normal;
@@ -15,6 +17,21 @@
 }
 
 .list-group-item :global {
+  .grw-recent-changes-skeleton-small {
+    @include grw-skeleton-text($font-size:14px, $line-height:16px);
+    max-width: 120px;
+  }
+
+  .grw-recent-changes-skeleton-h5 {
+    @include grw-skeleton-h5;
+    max-width: 120px;
+  }
+
+  .grw-recent-changes-skeleton-date {
+    @include grw-skeleton-text($font-size:10px, $line-height:12px);
+    width: 90px;
+  }
+
   .grw-recent-changes-item-lower {
     height: 17.5px;
   }

+ 43 - 67
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -16,6 +16,8 @@ import loggerFactory from '~/utils/logger';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
 import InfiniteScroll from './InfiniteScroll';
+import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
+import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
 
 import TagLabelsStyles from '../Page/TagLabels.module.scss';
 import styles from './RecentChanges.module.scss';
@@ -23,11 +25,15 @@ import styles from './RecentChanges.module.scss';
 
 const logger = loggerFactory('growi:History');
 
-type PageItemProps = {
+type PageItemLowerProps = {
   page: IPageHasId,
 }
 
-const PageItemLower = memo(({ page }: PageItemProps): JSX.Element => {
+type PageItemProps = PageItemLowerProps & {
+  isSmall: boolean
+}
+
+const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
   return (
     <div className="d-flex justify-content-between grw-recent-changes-item-lower pt-1">
       <div className="d-flex">
@@ -44,8 +50,7 @@ const PageItemLower = memo(({ page }: PageItemProps): JSX.Element => {
 });
 PageItemLower.displayName = 'PageItemLower';
 
-
-const LargePageItem = memo(({ page }: PageItemProps): JSX.Element => {
+const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
@@ -75,67 +80,38 @@ const LargePageItem = memo(({ page }: PageItemProps): JSX.Element => {
   });
 
   return (
-    <li className={`list-group-item ${styles['list-group-item']} py-3 px-0`}>
+    <li className={`list-group-item ${styles['list-group-item']} ${isSmall ? 'py-2' : 'py-3'} px-0`}>
       <div className="d-flex w-100">
         <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
         <div className="flex-grow-1 ml-2">
           { !dPagePath.isRoot && <FormerLink /> }
-          <h5 className="my-2">
+          <h5 className={isSmall ? 'my-0 text-truncate' : 'my-2'}>
             <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
             {locked}
           </h5>
-          <div className="grw-tag-labels mt-1 mb-2">
+          {!isSmall && <div className="grw-tag-labels mt-1 mb-2">
             { tagElements }
-          </div>
+          </div>}
           <PageItemLower page={page} />
         </div>
       </div>
     </li>
   );
 });
-LargePageItem.displayName = 'LargePageItem';
-
-
-const SmallPageItem = memo(({ page }: PageItemProps): JSX.Element => {
-  const dPagePath = new DevidedPagePath(page.path, false, true);
-  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-  const FormerLink = () => (
-    <div className="grw-page-path-text-muted-container small">
-      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
-    </div>
-  );
-
-  let locked;
-  if (page.grant !== 1) {
-    locked = <span><i className="icon-lock ml-2" /></span>;
-  }
-
-  return (
-    <li className={`list-group-item ${styles['list-group-item']} py-2 px-0`}>
-      <div className="d-flex w-100">
-        <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
-        <div className="flex-grow-1 ml-2">
-          { !dPagePath.isRoot && <FormerLink /> }
-          <h5 className="my-0 text-truncate">
-            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            {locked}
-          </h5>
-          <PageItemLower page={page} />
-        </div>
-      </div>
-    </li>
-  );
-});
-SmallPageItem.displayName = 'SmallPageItem';
+PageItem.displayName = 'PageItem';
 
 const RecentChanges = (): JSX.Element => {
+
   const PER_PAGE = 20;
   const { t } = useTranslation();
-  const swr = useSWRInifinitexRecentlyUpdated();
+  const swrInifinitexRecentlyUpdated = useSWRInifinitexRecentlyUpdated();
+  const { data: dataRecentlyUpdated, error, mutate: mutateRecentlyUpdated } = swrInifinitexRecentlyUpdated;
+
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
-  const isEmpty = swr.data?.[0].length === 0;
-  const isReachingEnd = isEmpty || (swr.data && swr.data[swr.data.length - 1]?.length < PER_PAGE);
+  const isEmpty = dataRecentlyUpdated?.[0].length === 0;
+  const isLoading = error == null && dataRecentlyUpdated === undefined;
+  const isReachingEnd = isEmpty || (dataRecentlyUpdated && dataRecentlyUpdated[dataRecentlyUpdated.length - 1]?.length < PER_PAGE);
+
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
       setIsRecentChangesSidebarSmall(true);
@@ -153,12 +129,10 @@ const RecentChanges = (): JSX.Element => {
   }, [retrieveSizePreferenceFromLocalStorage]);
 
   return (
-    <div data-testid="grw-recent-changes">
-      <div className="grw-sidebar-content-header p-3 d-flex">
-        <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => swr.mutate()}>
-          <i className="icon icon-reload"></i>
-        </button>
+    <div className="px-3" data-testid="grw-recent-changes">
+      <div className="grw-sidebar-content-header py-3 d-flex">
+        <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
+        <SidebarHeaderReloadButton onClick={() => mutateRecentlyUpdated()}/>
         <div className="d-flex align-items-center">
           <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} custom-control custom-switch ml-1`}>
             <input
@@ -173,21 +147,23 @@ const RecentChanges = (): JSX.Element => {
           </div>
         </div>
       </div>
-      <div className="grw-recent-changes p-3">
-        <ul className="list-group list-group-flush">
-          <InfiniteScroll
-            swrInifiniteResponse={swr}
-            isReachingEnd={isReachingEnd}
-          >
-            {pages => pages.map(page => (
-              isRecentChangesSidebarSmall
-                ? <SmallPageItem key={page._id} page={page} />
-                : <LargePageItem key={page._id} page={page} />
-            ))
-            }
-          </InfiniteScroll>
-        </ul>
-      </div>
+      {
+        isLoading ? <RecentChangesContentSkeleton /> : (
+          <div className="grw-recent-changes py-3">
+            <ul className="list-group list-group-flush">
+              <InfiniteScroll
+                swrInifiniteResponse={swrInifinitexRecentlyUpdated}
+                isReachingEnd={isReachingEnd}
+              >
+                {pages => pages.map(
+                  page => <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} />,
+                )
+                }
+              </InfiniteScroll>
+            </ul>
+          </div>
+        )
+      }
     </div>
   );
 

+ 14 - 0
packages/app/src/components/Sidebar/SidebarHeaderReloadButton.tsx

@@ -0,0 +1,14 @@
+import React from 'react';
+
+type Props = {
+  onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
+};
+
+export const SidebarHeaderReloadButton = ({ onClick }: Props) => {
+
+  return (
+    <button type="button" className="btn btn-sm ml-auto py-0 grw-btn-reload" onClick={onClick}>
+      <i className="icon icon-reload"></i>
+    </button>
+  );
+};

+ 18 - 0
packages/app/src/components/Sidebar/Skeleton/CustomSidebarContentSkeleton.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+
+import styles from '../CustomSidebar.module.scss';
+
+const CustomSidebarContentSkeleton = (): JSX.Element => {
+
+  return (
+    <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
+      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text-full ${styles['grw-custom-sidebar-skeleton-text-full']}`} />
+      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text-full ${styles['grw-custom-sidebar-skeleton-text-full']}`} />
+      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text ${styles['grw-custom-sidebar-skeleton-text']}`} />
+    </div>
+  );
+};
+
+export default CustomSidebarContentSkeleton;

+ 18 - 0
packages/app/src/components/Sidebar/Skeleton/PageTreeContentSkeleton.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+
+import styles from '../PageTree/ItemsTree.module.scss';
+
+const PageTreeContentSkeleton = (): JSX.Element => {
+
+  return (
+    <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} >
+      <Skeleton additionalClass={`${styles['grw-pagetree-item-skeleton-text']} pr-3`} />
+      <Skeleton additionalClass={`${styles['grw-pagetree-item-skeleton-text-child']} pr-3`} />
+      <Skeleton additionalClass={`${styles['grw-pagetree-item-skeleton-text-child']} pr-3`} />
+    </ul>
+  );
+};
+
+export default PageTreeContentSkeleton;

+ 40 - 0
packages/app/src/components/Sidebar/Skeleton/RecentChangesContentSkeleton.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+
+import styles from '../RecentChanges.module.scss';
+
+const SkeletonItem = () => {
+
+  const isSmall = window.localStorage.isRecentChangesSidebarSmall === 'true';
+
+  return (
+    <li className={`list-group-item ${styles['list-group-item']} ${isSmall ? 'py-2' : 'py-3'} px-0`}>
+      <div className="d-flex w-100">
+        <Skeleton additionalClass='rounded-circle picture' roundedPill />
+        <div className="flex-grow-1 ml-2">
+          <Skeleton additionalClass={`grw-recent-changes-skeleton-small ${styles['grw-recent-changes-skeleton-small']}`} />
+          <Skeleton additionalClass={`grw-recent-changes-skeleton-h5 ${styles['grw-recent-changes-skeleton-h5']} ${isSmall ? 'my-0' : 'my-2'}`} />
+          <div className="d-flex justify-content-end grw-recent-changes-item-lower pt-1">
+            <Skeleton additionalClass={`grw-recent-changes-skeleton-date ${styles['grw-recent-changes-skeleton-date']}`} />
+          </div>
+        </div>
+      </div>
+    </li>
+  );
+};
+
+const RecentChangesContentSkeleton = (): JSX.Element => {
+
+  return (
+    <div className="grw-recent-changes py-3">
+      <ul className="list-group list-group-flush">
+        <SkeletonItem />
+        <SkeletonItem />
+        <SkeletonItem />
+        <li className={'list-group-item p-0'}></li>
+      </ul>
+    </div>);
+};
+
+export default RecentChangesContentSkeleton;

+ 6 - 0
packages/app/src/components/Sidebar/Skeleton/SidebarSkeleton.module.scss

@@ -0,0 +1,6 @@
+@use '~/styles/mixins' as *;
+
+.grw-sidebar-content-header-skeleton {
+  @include grw-skeleton-h3;
+  max-width: 100%;
+}

+ 50 - 0
packages/app/src/components/Sidebar/Skeleton/SidebarSkeleton.tsx

@@ -0,0 +1,50 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+import { SidebarContentsType } from '~/interfaces/ui';
+import { useCurrentSidebarContents } from '~/stores/ui';
+
+import CustomSidebarContentSkeleton from './CustomSidebarContentSkeleton';
+import PageTreeContentSkeleton from './PageTreeContentSkeleton';
+import RecentChangesContentSkeleton from './RecentChangesContentSkeleton';
+import TagContentSkeleton from './TagContentSkeleton';
+
+import styles from './SidebarSkeleton.module.scss';
+
+export const SidebarHeaderSkeleton = (): JSX.Element => {
+  return (
+    <div className="grw-sidebar-content-header py-3">
+      <Skeleton additionalClass={styles['grw-sidebar-content-header-skeleton']} />
+    </div>
+  );
+};
+
+export const SidebarSkeleton = (): JSX.Element => {
+
+  const { data: currentSidebarContents } = useCurrentSidebarContents();
+
+  let SidebarContentSkeleton: () => JSX.Element;
+  switch (currentSidebarContents) {
+
+    case SidebarContentsType.TAG:
+      SidebarContentSkeleton = TagContentSkeleton;
+      break;
+    case SidebarContentsType.RECENT:
+      SidebarContentSkeleton = RecentChangesContentSkeleton;
+      break;
+    case SidebarContentsType.CUSTOM:
+      SidebarContentSkeleton = CustomSidebarContentSkeleton;
+      break;
+    case SidebarContentsType.TREE:
+    default:
+      SidebarContentSkeleton = PageTreeContentSkeleton;
+      break;
+  }
+
+  return (
+    <div className={currentSidebarContents === SidebarContentsType.TAG ? 'px-4' : 'px-3'}>
+      <SidebarHeaderSkeleton />
+      <SidebarContentSkeleton />
+    </div>
+  );
+};

+ 23 - 0
packages/app/src/components/Sidebar/Skeleton/TagContentSkeleton.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+
+import styles from '../Tag.module.scss';
+
+export const TagListSkeleton = (): JSX.Element => {
+  return (
+    <Skeleton additionalClass={`${styles['grw-tag-list-skeleton']} w-100 rounded overflow-hidden`} />
+  );
+};
+
+const TagContentSkeleton = (): JSX.Element => {
+
+  return (
+    <>
+      <Skeleton additionalClass={`${styles['grw-tag-skeleton-h3']} my-3`} />
+      <TagListSkeleton />
+    </>
+  );
+};
+
+export default TagContentSkeleton;

+ 10 - 0
packages/app/src/components/Sidebar/Tag.module.scss

@@ -0,0 +1,10 @@
+@use '~/styles/mixins' as *;
+
+.grw-tag-skeleton-h3 {
+  @include grw-skeleton-h3;
+  max-width: 120px;
+}
+
+.grw-tag-list-skeleton {
+  height: 90px;
+}

+ 5 - 10
packages/app/src/components/Sidebar/Tag.tsx

@@ -9,6 +9,9 @@ import { useSWRxTagsList } from '~/stores/tag';
 import TagCloudBox from '../TagCloudBox';
 import TagList from '../TagList';
 
+import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
+import { TagListSkeleton } from './Skeleton/TagContentSkeleton';
+
 
 const PAGING_LIMIT = 10;
 const TAG_CLOUD_LIMIT = 20;
@@ -44,22 +47,14 @@ const Tag: FC = () => {
     <div className="grw-container-convertible px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">{t('Tags')}</h3>
-        <button
-          type="button"
-          className="btn btn-sm ml-auto grw-btn-reload"
-          onClick={onReload}
-        >
-          <i className="icon icon-reload"></i>
-        </button>
+        <SidebarHeaderReloadButton onClick={() => onReload()}/>
       </div>
 
       <h3 className="my-3">{t('tag_list')}</h3>
 
       { isLoading
         ? (
-          <div className="text-muted text-center">
-            <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
-          </div>
+          <TagListSkeleton />
         )
         : (
           <div data-testid="grw-tags-list">

+ 1 - 1
packages/app/src/components/Skeleton.tsx

@@ -12,7 +12,7 @@ export const Skeleton = (props: SkeletonProps): JSX.Element => {
 
   return (
     <div className={`${additionalClass ?? ''}`}>
-      <div className={`grw-skeleton h-100 w-100 ${roundedPill ?? ''}`}></div>
+      <div className={`grw-skeleton h-100 w-100 ${roundedPill && 'rounded-pill'}`}></div>
     </div>
   );
 };

+ 21 - 0
packages/app/src/styles/_mixins.scss

@@ -160,3 +160,24 @@
     content: $code;
   }
 }
+
+@mixin grw-skeleton-text($font-size, $line-height) {
+  height: $line-height;
+  padding: (($line-height - $font-size)  / 2) 0;
+}
+/*
+.example {
+  @include grw-skeleton-text($font-size:$size, $line-height:$height);
+  max-width: 100%;
+}
+*/
+
+// values from './bootstrap/override'
+
+@mixin grw-skeleton-h3 {
+  @include grw-skeleton-text(21px, 30px);
+}
+
+@mixin grw-skeleton-h5 {
+  @include grw-skeleton-text(16px, 18px);
+}