aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/loaders
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-29 11:47:06 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit81b1e0e05919eb368a66aef47adcf7738af76f29 (patch)
tree758577d3f58d7025f84bca5bac9bc0da8432deb7 /src/components/atoms/loaders
parent3272ac336da52364ace5ed76d8f609d4088ffc06 (diff)
refactor(components): rewrite Spinner component
* Message should be set as children * Default message is no longer available (depending on use case, the consumer might prefer aria-label instead) * It is now possible to define the message position
Diffstat (limited to 'src/components/atoms/loaders')
-rw-r--r--src/components/atoms/loaders/spinner.module.scss48
-rw-r--r--src/components/atoms/loaders/spinner.test.tsx15
-rw-r--r--src/components/atoms/loaders/spinner.tsx35
-rw-r--r--src/components/atoms/loaders/spinner/index.ts1
-rw-r--r--src/components/atoms/loaders/spinner/spinner.module.scss69
-rw-r--r--src/components/atoms/loaders/spinner/spinner.stories.tsx (renamed from src/components/atoms/loaders/spinner.stories.tsx)17
-rw-r--r--src/components/atoms/loaders/spinner/spinner.test.tsx53
-rw-r--r--src/components/atoms/loaders/spinner/spinner.tsx42
8 files changed, 171 insertions, 109 deletions
diff --git a/src/components/atoms/loaders/spinner.module.scss b/src/components/atoms/loaders/spinner.module.scss
deleted file mode 100644
index 3e05cb3..0000000
--- a/src/components/atoms/loaders/spinner.module.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-
-.wrapper {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: center;
- gap: var(--spacing-2xs);
- margin: var(--spacing-md) 0;
-}
-
-.ball {
- width: fun.convert-px(8);
- height: fun.convert-px(8);
- background: linear-gradient(
- to right,
- var(--color-primary-light) 0%,
- var(--color-primary-lighter) 100%
- );
- border-radius: 50%;
- animation: spinner 1.4s infinite ease-in-out both;
-
- &:first-child {
- animation-delay: -0.32s;
- }
-
- &:nth-child(2) {
- animation-delay: -0.16s;
- }
-}
-
-.text {
- margin-left: var(--spacing-xs);
- color: var(--color-primary-darker);
- text-align: center;
-}
-
-@keyframes spinner {
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
-
- 40% {
- transform: scale(1);
- }
-}
diff --git a/src/components/atoms/loaders/spinner.test.tsx b/src/components/atoms/loaders/spinner.test.tsx
deleted file mode 100644
index 553c3ef..0000000
--- a/src/components/atoms/loaders/spinner.test.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Spinner } from './spinner';
-
-describe('Spinner', () => {
- it('renders a spinner loader', () => {
- render(<Spinner />);
- expect(screen.getByText('Loading...')).toBeInTheDocument();
- });
-
- it('renders a spinner loader with a custom message', () => {
- render(<Spinner message="Submitting" />);
- expect(screen.getByText('Submitting')).toBeInTheDocument();
- });
-});
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
deleted file mode 100644
index 968290b..0000000
--- a/src/components/atoms/loaders/spinner.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { FC } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './spinner.module.scss';
-
-export type SpinnerProps = {
- /**
- * The loading message. Default: "Loading...".
- */
- message?: string;
-};
-
-/**
- * Spinner component
- *
- * Render a loading message with animation.
- */
-export const Spinner: FC<SpinnerProps> = ({ message }) => {
- const intl = useIntl();
-
- return (
- <div className={styles.wrapper}>
- <div className={styles.ball}></div>
- <div className={styles.ball}></div>
- <div className={styles.ball}></div>
- <div className={styles.text}>
- {message ??
- intl.formatMessage({
- defaultMessage: 'Loading...',
- description: 'Spinner: loading text',
- id: 'q9cJQe',
- })}
- </div>
- </div>
- );
-};
diff --git a/src/components/atoms/loaders/spinner/index.ts b/src/components/atoms/loaders/spinner/index.ts
new file mode 100644
index 0000000..cd17217
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/index.ts
@@ -0,0 +1 @@
+export * from './spinner';
diff --git a/src/components/atoms/loaders/spinner/spinner.module.scss b/src/components/atoms/loaders/spinner/spinner.module.scss
new file mode 100644
index 0000000..97882a4
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.module.scss
@@ -0,0 +1,69 @@
+@use "../../../../styles/abstracts/functions" as fun;
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ width: fit-content;
+
+ &--left {
+ flex-flow: row-reverse wrap;
+ }
+
+ &--right {
+ flex-flow: row wrap;
+ }
+
+ &--bottom {
+ flex-flow: column nowrap;
+ }
+
+ &--top {
+ flex-flow: column-reverse nowrap;
+ }
+}
+
+.icon {
+ --ball-size: #{fun.convert-px(8)};
+
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ width: calc((var(--ball-size) * 3) + var(--spacing-xs));
+
+ &__ball {
+ width: var(--ball-size);
+ height: var(--ball-size);
+ background: linear-gradient(
+ to right,
+ var(--color-primary-light) 0%,
+ var(--color-primary-lighter) 100%
+ );
+ border-radius: 50%;
+ animation: spinner 1.4s infinite ease-in-out both;
+
+ &:first-child {
+ animation-delay: -0.32s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: -0.16s;
+ }
+ }
+}
+
+.body {
+ color: var(--color-primary-darker);
+}
+
+@keyframes spinner {
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0);
+ }
+
+ 40% {
+ transform: scale(1);
+ }
+}
diff --git a/src/components/atoms/loaders/spinner.stories.tsx b/src/components/atoms/loaders/spinner/spinner.stories.tsx
index 197d06c..e9dfae4 100644
--- a/src/components/atoms/loaders/spinner.stories.tsx
+++ b/src/components/atoms/loaders/spinner/spinner.stories.tsx
@@ -1,14 +1,14 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { Spinner as SpinnerComponent } from './spinner';
/**
* Spinner - Storybook Meta
*/
export default {
- title: 'Atoms/Loaders/Spinner',
+ title: 'Atoms/Loaders',
component: SpinnerComponent,
argTypes: {
- message: {
+ children: {
control: {
type: 'text',
},
@@ -29,14 +29,9 @@ const Template: ComponentStory<typeof SpinnerComponent> = (args) => (
);
/**
- * Loaders Stories - Default Spinner
+ * Loaders Stories - Spinner
*/
export const Spinner = Template.bind({});
-
-/**
- * Loaders Stories - Spinner with custom message
- */
-export const SpinnerCustomMessage = Template.bind({});
-SpinnerCustomMessage.args = {
- message: 'Submitting...',
+Spinner.args = {
+ children: 'Submitting...',
};
diff --git a/src/components/atoms/loaders/spinner/spinner.test.tsx b/src/components/atoms/loaders/spinner/spinner.test.tsx
new file mode 100644
index 0000000..733648b
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.test.tsx
@@ -0,0 +1,53 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Spinner } from './spinner';
+
+describe('Spinner', () => {
+ it('renders a spinner', () => {
+ const { container } = render(<Spinner />);
+ expect(container).toBeInTheDocument();
+ });
+
+ it('can render a spinner with a custom message', () => {
+ const customMsg = 'Submitting';
+
+ render(<Spinner>{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg)).toBeInTheDocument();
+ });
+
+ it('can render a spinner with a custom message at the bottom', () => {
+ const customMsg = 'necessitatibus';
+
+ render(<Spinner position="bottom">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--bottom'
+ );
+ });
+
+ it('can render a spinner with a custom message on the left', () => {
+ const customMsg = 'eos';
+
+ render(<Spinner position="left">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--left'
+ );
+ });
+
+ it('can render a spinner with a custom message on the right', () => {
+ const customMsg = 'neque';
+
+ render(<Spinner position="right">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--right'
+ );
+ });
+
+ it('can render a spinner with a custom message on the top', () => {
+ const customMsg = 'vero';
+
+ render(<Spinner position="top">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--top'
+ );
+ });
+});
diff --git a/src/components/atoms/loaders/spinner/spinner.tsx b/src/components/atoms/loaders/spinner/spinner.tsx
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.tsx
@@ -0,0 +1,42 @@
+import type { FC, HTMLAttributes, ReactNode } from 'react';
+import type { Position } from '../../../../types';
+import styles from './spinner.module.scss';
+
+export type SpinnerProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
+ /**
+ * The loading message.
+ */
+ children?: ReactNode;
+ /**
+ * Define the position of the loading message if any.
+ *
+ * @default 'right'
+ */
+ position?: Exclude<Position, 'center'>;
+};
+
+/**
+ * Spinner component
+ *
+ * Render a loading message with animation.
+ */
+export const Spinner: FC<SpinnerProps> = ({
+ children,
+ className = '',
+ position = 'right',
+ ...props
+}) => {
+ const positionClass = styles[`wrapper--${position}`];
+ const wrapperClass = `${styles.wrapper} ${positionClass} ${className}`;
+
+ return (
+ <div {...props} className={wrapperClass}>
+ <div aria-hidden className={styles.icon}>
+ <div className={styles.icon__ball} />
+ <div className={styles.icon__ball} />
+ <div className={styles.icon__ball} />
+ </div>
+ <div className={styles.body}>{children}</div>
+ </div>
+ );
+};