aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/loaders/spinner
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/spinner
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/spinner')
-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.tsx37
-rw-r--r--src/components/atoms/loaders/spinner/spinner.test.tsx53
-rw-r--r--src/components/atoms/loaders/spinner/spinner.tsx42
5 files changed, 202 insertions, 0 deletions
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/spinner.stories.tsx b/src/components/atoms/loaders/spinner/spinner.stories.tsx
new file mode 100644
index 0000000..e9dfae4
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.stories.tsx
@@ -0,0 +1,37 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Spinner as SpinnerComponent } from './spinner';
+
+/**
+ * Spinner - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Loaders',
+ component: SpinnerComponent,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'Loading message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SpinnerComponent>;
+
+const Template: ComponentStory<typeof SpinnerComponent> = (args) => (
+ <SpinnerComponent {...args} />
+);
+
+/**
+ * Loaders Stories - Spinner
+ */
+export const Spinner = Template.bind({});
+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>
+ );
+};