From 9128c224c65f8f2a172b22a443ccb4573c7acd90 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 29 Sep 2023 15:49:14 +0200 Subject: refactor(components): rewrite ProgressBar component * Avoid browser vendors by adding an extra div * Add a loading state * Add an option to center the progress bar (no longer the default) * Remove min since it is always 0 --- .../atoms/loaders/progress-bar.fixture.tsx | 5 - .../atoms/loaders/progress-bar.module.scss | 43 --------- .../atoms/loaders/progress-bar.stories.tsx | 79 ---------------- src/components/atoms/loaders/progress-bar.test.tsx | 21 ----- src/components/atoms/loaders/progress-bar.tsx | 57 ----------- src/components/atoms/loaders/progress-bar/index.ts | 1 + .../loaders/progress-bar/progress-bar.module.scss | 85 +++++++++++++++++ .../loaders/progress-bar/progress-bar.stories.tsx | 60 ++++++++++++ .../loaders/progress-bar/progress-bar.test.tsx | 45 +++++++++ .../atoms/loaders/progress-bar/progress-bar.tsx | 104 +++++++++++++++++++++ .../organisms/layout/posts-list.module.scss | 4 + src/components/organisms/layout/posts-list.tsx | 17 +++- 12 files changed, 311 insertions(+), 210 deletions(-) delete mode 100644 src/components/atoms/loaders/progress-bar.fixture.tsx delete mode 100644 src/components/atoms/loaders/progress-bar.module.scss delete mode 100644 src/components/atoms/loaders/progress-bar.stories.tsx delete mode 100644 src/components/atoms/loaders/progress-bar.test.tsx delete mode 100644 src/components/atoms/loaders/progress-bar.tsx create mode 100644 src/components/atoms/loaders/progress-bar/index.ts create mode 100644 src/components/atoms/loaders/progress-bar/progress-bar.module.scss create mode 100644 src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx create mode 100644 src/components/atoms/loaders/progress-bar/progress-bar.test.tsx create mode 100644 src/components/atoms/loaders/progress-bar/progress-bar.tsx (limited to 'src/components') diff --git a/src/components/atoms/loaders/progress-bar.fixture.tsx b/src/components/atoms/loaders/progress-bar.fixture.tsx deleted file mode 100644 index d9b02c3..0000000 --- a/src/components/atoms/loaders/progress-bar.fixture.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export const id = 'amet-eum-ut'; -export const min = 0; -export const max = 50; -export const current = 20; -export const label = '20 loaded out of a total of 50'; diff --git a/src/components/atoms/loaders/progress-bar.module.scss b/src/components/atoms/loaders/progress-bar.module.scss deleted file mode 100644 index ed64ceb..0000000 --- a/src/components/atoms/loaders/progress-bar.module.scss +++ /dev/null @@ -1,43 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; - -.progress { - margin: var(--spacing-sm) auto var(--spacing-md); - text-align: center; - - &__info { - margin-bottom: var(--spacing-2xs); - font-size: var(--font-size-sm); - } - - &__bar[value] { - display: block; - width: clamp(25ch, 20vw, 30ch); - max-width: 100%; - height: fun.convert-px(13); - margin: auto; - appearance: none; - background: var(--color-bg-tertiary); - border: fun.convert-px(1) solid var(--color-primary-darker); - border-radius: 1em; - box-shadow: inset 0 0 fun.convert-px(4) fun.convert-px(1) - var(--color-shadow-light); - - &::-webkit-progress-value { - background-color: var(--color-primary-dark); - border-radius: 1em; - } - - &::-moz-progress-bar { - background-color: var(--color-primary-dark); - border-radius: 1em; - } - - &::-webkit-progress-bar { - background: var(--color-bg-tertiary); - border: fun.convert-px(1) solid var(--color-primary-darker); - border-radius: 1em; - box-shadow: inset 0 0 fun.convert-px(4) fun.convert-px(1) - var(--color-shadow-light); - } - } -} diff --git a/src/components/atoms/loaders/progress-bar.stories.tsx b/src/components/atoms/loaders/progress-bar.stories.tsx deleted file mode 100644 index 799aacb..0000000 --- a/src/components/atoms/loaders/progress-bar.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ProgressBar as ProgressBarComponent } from './progress-bar'; -import { current, id, label, max, min } from './progress-bar.fixture'; - -/** - * ProgressBar - Storybook Meta - */ -export default { - title: 'Atoms/Loaders', - component: ProgressBarComponent, - argTypes: { - current: { - control: { - type: 'number', - }, - description: 'The current value.', - type: { - name: 'number', - required: true, - }, - }, - id: { - control: { - type: 'text', - }, - description: 'The progress bar id.', - type: { - name: 'string', - required: true, - }, - }, - label: { - control: { - type: 'text', - }, - description: 'The progress bar label.', - type: { - name: 'string', - required: true, - }, - }, - max: { - control: { - type: 'number', - }, - description: 'The maximal value.', - type: { - name: 'number', - required: true, - }, - }, - min: { - control: { - type: 'number', - }, - description: 'The minimal value.', - type: { - name: 'number', - required: true, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -/** - * Loaders Stories - Progress bar - */ -export const ProgressBar = Template.bind({}); -ProgressBar.args = { - current, - id, - label, - min, - max, -}; diff --git a/src/components/atoms/loaders/progress-bar.test.tsx b/src/components/atoms/loaders/progress-bar.test.tsx deleted file mode 100644 index 6b01b68..0000000 --- a/src/components/atoms/loaders/progress-bar.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { ProgressBar } from './progress-bar'; -import { current, id, label, max, min } from './progress-bar.fixture'; - -describe('ProgressBar', () => { - it('renders a progress bar', () => { - render( - - ); - expect( - screen.getByRole('progressbar', { name: label }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx deleted file mode 100644 index 195bb21..0000000 --- a/src/components/atoms/loaders/progress-bar.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FC } from 'react'; -import styles from './progress-bar.module.scss'; - -export type ProgressBarProps = { - /** - * Current value. - */ - current: number; - /** - * The progress bar id. - */ - id: string; - /** - * The progress bar label. - */ - label: string; - /** - * Minimal value. - */ - min: number; - /** - * Maximal value. - */ - max: number; -}; - -/** - * ProgressBar component - * - * Render a progress bar. - */ -export const ProgressBar: FC = ({ - current, - id, - label, - min, - max, -}) => { - return ( -
- - - {current}/{max} - -
- ); -}; diff --git a/src/components/atoms/loaders/progress-bar/index.ts b/src/components/atoms/loaders/progress-bar/index.ts new file mode 100644 index 0000000..d71d9b1 --- /dev/null +++ b/src/components/atoms/loaders/progress-bar/index.ts @@ -0,0 +1 @@ +export * from './progress-bar'; diff --git a/src/components/atoms/loaders/progress-bar/progress-bar.module.scss b/src/components/atoms/loaders/progress-bar/progress-bar.module.scss new file mode 100644 index 0000000..3605ae7 --- /dev/null +++ b/src/components/atoms/loaders/progress-bar/progress-bar.module.scss @@ -0,0 +1,85 @@ +@use "../../../../styles/abstracts/functions" as fun; + +.wrapper { + width: fit-content; + text-align: center; + + &--centered { + margin-inline: auto; + } +} + +.label { + margin-bottom: var(--spacing-2xs); + font-size: var(--font-size-sm); + cursor: default; +} + +.progress { + width: clamp(25ch, 20vw, 30ch); + height: fun.convert-px(13); + position: relative; + overflow: hidden; + background: var(--color-bg-tertiary); + border: fun.convert-px(1) solid var(--color-primary-darker); + border-radius: 1em; + box-shadow: inset 0 0 fun.convert-px(4) fun.convert-px(1) + var(--color-shadow-light); + container-type: inline-size; + + &::before { + content: ""; + position: absolute; + width: 15%; + left: 0; + } + + &::before, + &__bar { + background-color: var(--color-primary-dark); + border-radius: 1em; + } + + &::before, + progress { + height: 100%; + opacity: 0; + } + + &__bar, + progress { + width: 100%; + position: absolute; + inset: 0; + } + + &__bar { + transform: translateX(var(--currentProgress)); + transition: all 0.25s linear 0s; + } + + progress { + appearance: none; + } + + &--loading { + &::before { + opacity: 1; + animation: move 1s linear 0s infinite alternate both; + } + } + + &--loading &__bar { + opacity: 0; + } +} + +@keyframes move { + 0% { + transform: translateX(0); + } + + 100% { + transform: translateX(calc(100cqw - 100%)); + } +} diff --git a/src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx b/src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx new file mode 100644 index 0000000..fb600fb --- /dev/null +++ b/src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx @@ -0,0 +1,60 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ProgressBar as ProgressBarComponent } from './progress-bar'; + +/** + * ProgressBar - Storybook Meta + */ +export default { + title: 'Atoms/Loaders', + component: ProgressBarComponent, + argTypes: { + current: { + control: { + type: 'number', + }, + description: 'The current value.', + type: { + name: 'number', + required: true, + }, + }, + label: { + control: { + type: 'text', + }, + description: 'The progress bar label.', + type: { + name: 'string', + required: true, + }, + }, + max: { + control: { + type: 'number', + }, + description: 'The maximal value.', + type: { + name: 'number', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +const max = 50; +const current = 10; +const label = `${current} loaded out of a total of ${max}`; + +/** + * Loaders Stories - Progress bar + */ +export const ProgressBar = Template.bind({}); +ProgressBar.args = { + current, + label, + max, +}; diff --git a/src/components/atoms/loaders/progress-bar/progress-bar.test.tsx b/src/components/atoms/loaders/progress-bar/progress-bar.test.tsx new file mode 100644 index 0000000..1f5d90e --- /dev/null +++ b/src/components/atoms/loaders/progress-bar/progress-bar.test.tsx @@ -0,0 +1,45 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { ProgressBar } from './progress-bar'; + +describe('ProgressBar', () => { + it('renders a progress bar', () => { + const max = 50; + const current = 10; + const label = `${current} loaded out of a total of ${max}`; + + render(); + + expect( + rtlScreen.getByRole('progressbar', { name: label }) + ).toBeInTheDocument(); + }); + + it('can render a progress bar with loading state', () => { + const max = 50; + const current = 10; + const label = `${current} loaded out of a total of ${max}`; + + render(); + + const progressBar = rtlScreen.getByRole('progressbar', { name: label }); + + expect(progressBar).not.toHaveValue(); + expect(progressBar.parentElement).toHaveClass('progress--loading'); + }); + + it('can render a centered progress bar', () => { + const max = 50; + const current = 10; + const label = `${current} loaded out of a total of ${max}`; + + render( + + ); + + expect( + rtlScreen.getByRole('progressbar', { name: label }).parentElement + ?.parentElement + ).toHaveClass('wrapper--centered'); + }); +}); diff --git a/src/components/atoms/loaders/progress-bar/progress-bar.tsx b/src/components/atoms/loaders/progress-bar/progress-bar.tsx new file mode 100644 index 0000000..d885260 --- /dev/null +++ b/src/components/atoms/loaders/progress-bar/progress-bar.tsx @@ -0,0 +1,104 @@ +import { + useId, + type CSSProperties, + type HTMLAttributes, + type ForwardRefRenderFunction, + forwardRef, +} from 'react'; +import { Label } from '../../forms'; +import styles from './progress-bar.module.scss'; + +export type ProgressBarProps = Omit< + HTMLAttributes, + 'children' +> & { + /** + * Current value. + */ + current: number; + /** + * Should the progress bar be centered inside its parent? + * + * @default false + */ + isCentered?: boolean; + /** + * Should the progress bar indicate a loading state? + * + * @default false + */ + isLoading?: boolean; + /** + * The progress bar label. + */ + label: string; + /** + * Maximal value. + */ + max: number; +}; + +const ProgressBarWithRef: ForwardRefRenderFunction< + HTMLDivElement, + ProgressBarProps +> = ( + { + className = '', + current, + isCentered = false, + isLoading = false, + label, + max, + ...props + }, + ref +) => { + const wrapperClass = [ + styles.wrapper, + styles[isCentered ? 'wrapper--centered' : ''], + className, + ].join(' '); + const progressClass = `${styles.progress} ${ + styles[isLoading ? 'progress--loading' : ''] + }`; + const progressBarId = useId(); + const progressValueFallback = `${current}/${max}`; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Percent + const progressPercent = `${((max - current) / max) * 100}%`; + + return ( +
+ +
+ + {progressValueFallback} + +
+
+
+ ); +}; + +/** + * ProgressBar component + * + * Render a progress bar. + */ +export const ProgressBar = forwardRef(ProgressBarWithRef); diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss index 64ad33f..49993da 100644 --- a/src/components/organisms/layout/posts-list.module.scss +++ b/src/components/organisms/layout/posts-list.module.scss @@ -60,3 +60,7 @@ display: flex; margin: auto; } + +.progress { + margin-block: var(--spacing-md); +} diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index 5401ed1..86c3d12 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-statements */ -import { type FC, Fragment, useRef, useCallback } from 'react'; +import { type FC, Fragment, useRef, useCallback, useId } from 'react'; import { useIntl } from 'react-intl'; import { useIsMounted, useSettings } from '../../../utils/hooks'; import { @@ -102,6 +102,7 @@ export const PostsList: FC = ({ const isMounted = useIsMounted(listRef); const { blog } = useSettings(); const lastPostId = posts.length ? posts[posts.length - 1].id : 0; + const progressBarId = useId(); /** * Retrieve the list of posts. @@ -114,7 +115,12 @@ export const PostsList: FC = ({ allPosts: Post[], headingLevel: HeadingLevel = 2 ): JSX.Element => ( -
    +
      {allPosts.map(({ id, ...post }) => (
    1. @@ -190,11 +196,12 @@ export const PostsList: FC = ({ <> {showLoadMoreBtn ? ( -- cgit v1.2.3