Skip to content
This repository has been archived by the owner on Mar 23, 2024. It is now read-only.

Commit

Permalink
feat: ✨ add skeleton (#117)
Browse files Browse the repository at this point in the history
* feat: ✨ add skeleton

* docs: 📝 add docs for skeleton

* docs: 📝 add more story cases for storybook

* fix: 🩹 fix tests for get elapsed time
  • Loading branch information
velenyx committed Sep 25, 2023
1 parent e84ea2d commit e056736
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 3 deletions.
6 changes: 6 additions & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"env": {
"browser": true,
"es2021": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
Expand All @@ -21,6 +26,7 @@
}
],
"rules": {
"react/display-name": 0,
// I suggest you add those two rules:
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('getElapsedTime', () => {
expect(getElapsedTime(pastTime)).toBe('0s ago');
const pastTime2 = new Date(currentTime.getTime() - 100); // 100 millisecond ago
expect(getElapsedTime(pastTime2)).toBe('0s ago');
const pastTime3 = new Date(currentTime.getTime() - 999); // 999 millisecond ago
const pastTime3 = new Date(currentTime.getTime() - 997); // 997 millisecond ago
expect(getElapsedTime(pastTime3)).toBe('0s ago');
});

Expand Down
55 changes: 55 additions & 0 deletions client/src/shared/ui/skeleton/skeleton.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@keyframes react-loading-skeleton {
100% {
transform: translateX(100%);
}
}

.reactLoadingSkeleton {
--base-color: #313131;
--highlight-color: #525252;
--animation-duration: 1.5s;
--animation-direction: normal;
--pseudo-element-display: block; /* Enable animation */

background-color: var(--base-color);

width: 100%;
border-radius: 0.25rem;
display: inline-flex;
line-height: 1;

position: relative;
user-select: none;
overflow: hidden;
z-index: 1; /* Necessary for overflow: hidden to work correctly in Safari */
}

.reactLoadingSkeleton::after {
content: ' ';
display: var(--pseudo-element-display);
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
background-repeat: no-repeat;
background-image: linear-gradient(
90deg,
var(--base-color),
var(--highlight-color),
var(--base-color)
);
transform: translateX(-100%);

animation-name: react-loading-skeleton;
animation-direction: var(--animation-direction);
animation-duration: var(--animation-duration);
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}

/*@media (prefers-reduced-motion) {*/
/* .reactLoadingSkeleton {*/
/* --pseudo-element-display: none; !* Disable animation *!*/
/* }*/
/*}*/
218 changes: 218 additions & 0 deletions client/src/shared/ui/skeleton/skeleton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Skeleton, SkeletonProps } from './skeleton';

// Default props for Skeleton
const skeletonProps: SkeletonProps = {
count: 1,
inline: false,
height: '100px',
width: '100%',
borderRadius: '4px',
};

// Defining meta information for Storybook
type Story = StoryObj<typeof Skeleton>;
const SkeletonTemplate: Story = {
render: args => (
<div style={{ width: '100%' }}>
<Skeleton {...args} />
</div>
),
};

export const Playground = { ...SkeletonTemplate };
Playground.args = skeletonProps;

// Skeleton with multiple elements
export const MultipleElements = { ...SkeletonTemplate };
MultipleElements.args = {
...skeletonProps,
count: 5,
};

// Inline Skeleton elements
export const InlineElements = { ...SkeletonTemplate };
InlineElements.args = {
...skeletonProps,
inline: true,
};

// Skeleton with custom dimensions
export const CustomDimensions = { ...SkeletonTemplate };
CustomDimensions.args = {
...skeletonProps,
height: '50px',
width: '50px',
};

// Skeleton with rounded corners
export const RoundedCorners = { ...SkeletonTemplate };
RoundedCorners.args = {
...skeletonProps,
borderRadius: '50%',
};

// Skeleton with custom style
export const CustomStyle = { ...SkeletonTemplate };
CustomStyle.args = {
...skeletonProps,
style: { backgroundColor: 'lightgray' },
};

// Skeleton with custom wrapper
export const CustomWrapper = { ...SkeletonTemplate };
CustomWrapper.args = {
...skeletonProps,
wrapper: ({ children }) => (
<div style={{ padding: '10px', backgroundColor: 'whitesmoke' }}>{children}</div>
),
};

// Skeleton representing a loading text line
export const TextLine = { ...SkeletonTemplate };
TextLine.args = {
...skeletonProps,
width: '80%',
height: '16px',
};

// Skeleton representing a loading avatar
export const Avatar = { ...SkeletonTemplate };
Avatar.args = {
...skeletonProps,
width: '48px',
height: '48px',
borderRadius: '50%',
};

// Skeleton representing a loading card
export const Card = { ...SkeletonTemplate };
Card.args = {
...skeletonProps,
width: '300px',
height: '400px',
borderRadius: '8px',
};

// Skeleton representing a loading button
export const Button = { ...SkeletonTemplate };
Button.args = {
...skeletonProps,
width: '120px',
height: '36px',
borderRadius: '4px',
};

// Skeleton representing a loading image
export const Image = { ...SkeletonTemplate };
Image.args = {
...skeletonProps,
width: '200px',
height: '200px',
borderRadius: '8px',
};

// Skeleton representing loading list items
export const ListItems = { ...SkeletonTemplate };
ListItems.args = {
...skeletonProps,
count: 5,
};

const skeletonArgTypes = {
count: {
control: 'number',
description: 'The number of skeleton elements to render.',
defaultValue: 1,
table: {
type: { summary: 'number' },
defaultValue: { summary: 1 },
},
},
inline: {
control: 'boolean',
description: 'Whether the skeleton elements should be rendered inline.',
defaultValue: false,
table: {
type: { summary: 'boolean' },
defaultValue: { summary: false },
},
},
wrapper: {
control: 'object',
description: 'An optional wrapper component for the skeleton elements.',
table: {
type: { summary: 'React.FunctionComponent' },
},
},
className: {
control: 'text',
description: 'A custom class name for the skeleton elements.',
table: {
type: { summary: 'string' },
},
},
height: {
control: 'text',
description: 'The height of the skeleton elements.',
defaultValue: '20px',
table: {
type: { summary: 'string | number' },
defaultValue: { summary: '20px' },
},
},
width: {
control: 'text',
description: 'The width of the skeleton elements.',
defaultValue: '100%',
table: {
type: { summary: 'string | number' },
defaultValue: { summary: '100%' },
},
},
borderRadius: {
control: 'text',
description: 'The border radius of the skeleton elements.',
defaultValue: '4px',
table: {
type: { summary: 'string' },
defaultValue: { summary: '4px' },
},
},
containerTestId: {
control: 'text',
description: 'The data-testid attribute for the container element.',
table: {
type: { summary: 'string' },
},
},
skeletonTestId: {
control: 'text',
description: 'The data-testid attribute for the skeleton elements.',
table: {
type: { summary: 'string' },
},
},
containerClassName: {
control: 'text',
description: 'A custom class name for the container element.',
table: {
type: { summary: 'string' },
},
},
style: {
control: 'object',
description: 'Custom styles for the skeleton elements.',
table: {
type: { summary: 'CSSProperties' },
},
},
};

export default {
title: 'shared/Skeleton',
component: Skeleton,
tags: ['autodocs'],
argTypes: skeletonArgTypes,
} as Meta;
31 changes: 31 additions & 0 deletions client/src/shared/ui/skeleton/skeleton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';

import { Skeleton } from './skeleton';

describe('Skeleton component', () => {
it('should render with provided height and width', () => {
const height = 100;
const width = 200;
render(<Skeleton height={height} skeletonTestId='skeleton123-test' width={width} />);
const skeletonContainer = screen.getByTestId('skeleton123-test');
expect(skeletonContainer).toHaveStyle(`height: ${height}px`);
expect(skeletonContainer).toHaveStyle(`width: ${width}px`);
});
it('should render with provided border radius', () => {
const borderRadius = '50%';
render(<Skeleton borderRadius={borderRadius} skeletonTestId='skeleton1234-test' />);
const skeletonContainer = screen.getByTestId('skeleton1234-test');
expect(skeletonContainer).toHaveStyle(`border-radius: ${borderRadius}`);
});
it('should render with the provided className', () => {
const className = 'custom-class';
render(<Skeleton className={className} skeletonTestId='skeleton12345-test' />);
const skeletonContainer = screen.getByTestId('skeleton12345-test');
expect(skeletonContainer).toHaveClass(className);
});
it('should render without any provided props', () => {
render(<Skeleton skeletonTestId='skeleton123456-test' />);
const skeletonContainer = screen.getByTestId('skeleton123456-test');
expect(skeletonContainer).toBeInTheDocument();
});
});
Loading

2 comments on commit e056736

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for teameights ready!

✅ Preview
https://teameights-74as2s2wf-exortme1ster.vercel.app

Built with commit e056736.
This pull request is being automatically deployed with vercel-action

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for teameights-storybook ready!

✅ Preview
https://teameights-storybook-972hiyh7n-exortme1ster.vercel.app

Built with commit e056736.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.