aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-29 15:49:14 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:40 +0100
commit9128c224c65f8f2a172b22a443ccb4573c7acd90 (patch)
treebc435554174a5ed4c3f8808190cb94016f8d28f0 /src/components/atoms
parent81b1e0e05919eb368a66aef47adcf7738af76f29 (diff)
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
Diffstat (limited to 'src/components/atoms')
-rw-r--r--src/components/atoms/loaders/progress-bar.fixture.tsx5
-rw-r--r--src/components/atoms/loaders/progress-bar.module.scss43
-rw-r--r--src/components/atoms/loaders/progress-bar.test.tsx21
-rw-r--r--src/components/atoms/loaders/progress-bar.tsx57
-rw-r--r--src/components/atoms/loaders/progress-bar/index.ts1
-rw-r--r--src/components/atoms/loaders/progress-bar/progress-bar.module.scss85
-rw-r--r--src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx (renamed from src/components/atoms/loaders/progress-bar.stories.tsx)29
-rw-r--r--src/components/atoms/loaders/progress-bar/progress-bar.test.tsx45
-rw-r--r--src/components/atoms/loaders/progress-bar/progress-bar.tsx104
9 files changed, 240 insertions, 150 deletions
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.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(
- <ProgressBar
- id={id}
- label={label}
- min={min}
- max={max}
- current={current}
- />
- );
- 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<ProgressBarProps> = ({
- current,
- id,
- label,
- min,
- max,
-}) => {
- return (
- <div className={styles.progress}>
- <label htmlFor={id} className={styles.progress__info}>
- {label}
- </label>
- <progress
- aria-valuemin={min}
- aria-valuemax={max}
- aria-valuenow={current}
- className={styles.progress__bar}
- id={id}
- max={max}
- value={current}
- >
- {current}/{max}
- </progress>
- </div>
- );
-};
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.stories.tsx b/src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx
index 799aacb..fb600fb 100644
--- a/src/components/atoms/loaders/progress-bar.stories.tsx
+++ b/src/components/atoms/loaders/progress-bar/progress-bar.stories.tsx
@@ -1,6 +1,5 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { 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
@@ -19,16 +18,6 @@ export default {
required: true,
},
},
- id: {
- control: {
- type: 'text',
- },
- description: 'The progress bar id.',
- type: {
- name: 'string',
- required: true,
- },
- },
label: {
control: {
type: 'text',
@@ -49,16 +38,6 @@ export default {
required: true,
},
},
- min: {
- control: {
- type: 'number',
- },
- description: 'The minimal value.',
- type: {
- name: 'number',
- required: true,
- },
- },
},
} as ComponentMeta<typeof ProgressBarComponent>;
@@ -66,14 +45,16 @@ const Template: ComponentStory<typeof ProgressBarComponent> = (args) => (
<ProgressBarComponent {...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,
- id,
label,
- min,
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(<ProgressBar current={current} label={label} max={max} />);
+
+ 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(<ProgressBar current={current} isLoading label={label} max={max} />);
+
+ 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(
+ <ProgressBar current={current} isCentered label={label} max={max} />
+ );
+
+ 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<HTMLDivElement>,
+ '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 (
+ <div {...props} className={wrapperClass} ref={ref}>
+ <Label
+ className={styles.label}
+ htmlFor={progressBarId}
+ // eslint-disable-next-line react/jsx-no-literals -- Size allowed
+ size="md"
+ >
+ {label}
+ </Label>
+ <div
+ className={progressClass}
+ style={{ '--currentProgress': `-${progressPercent}` } as CSSProperties}
+ >
+ <progress
+ aria-valuemin={0}
+ aria-valuemax={max}
+ aria-valuenow={current}
+ id={progressBarId}
+ max={max}
+ value={isLoading ? undefined : current}
+ >
+ {progressValueFallback}
+ </progress>
+ <div aria-hidden className={styles.progress__bar} />
+ </div>
+ </div>
+ );
+};
+
+/**
+ * ProgressBar component
+ *
+ * Render a progress bar.
+ */
+export const ProgressBar = forwardRef(ProgressBarWithRef);