Skip to content

Commit

Permalink
up
Browse files Browse the repository at this point in the history
  • Loading branch information
ryota-murakami committed Mar 27, 2024
1 parent bbc61b6 commit 9526b37
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 7 deletions.
8 changes: 1 addition & 7 deletions src/app/globals.css
Expand Up @@ -24,10 +24,4 @@ body {
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
}
100 changes: 100 additions & 0 deletions src/components/Spinner/Spinner.stories.tsx
@@ -0,0 +1,100 @@
import type { Meta, StoryObj } from '@storybook/react';
import React, { use, useState } from 'react';
import classNames from 'classnames';

import { Spinner } from './Spinner';

const meta = {
title: 'Components/Spinner',
component: Spinner,
tags: ['autodocs'],
parameters: {
controls: { expanded: true },
},
} satisfies Meta<typeof Spinner>;

export default meta;

type Story = StoryObj<typeof Spinner>;

/**
* デフォルトのサイズ一覧です<br />
* <b>show code</b>ボタンからコードを確認できます。
*/
export const Default: Story = {
render: () => (
<div className="p-16 grid grid-cols-4 w-fit">
<Spinner variant="primary" size="sm" />
<Spinner variant="primary" size="md" />
<Spinner variant="primary" size="lg" />
<Spinner variant="primary" size="xl" />
</div>
),
};

/**
* ボタンと一緒に表示するサンプルです。<br />
* DaisyUIのloading loading-spinnerクラスでも同じような事ができます。<br />
* <b>show code</b>ボタンからコードを確認できます。
*/
export const Loading: Story = {
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isLoading, setIsLoading] = useState(false);
// eslint-disable-next-line react-hooks/rules-of-hooks
const [data, setData] = useState<null | string>(null);

const handleClick = async () => {
setIsLoading(true);
const res = await new Promise<string>(resolve =>
setTimeout(() => resolve('Fetch Data!'), 3000),
);
setData(res);
setIsLoading(false);
};

return (
<div className="p-16 grid grid-rows-2 gap-1 border-2 border-gray-300 rounded-3xl h-fit w-[600px]">
<h1 className="text-4xl font-bold text-green-500 h-fit">{data}</h1>
<button
className={classNames('btn btn-primary pw-4 font-bold text-lg', {
'pointer-events-none disabled ': isLoading,
})}
onClick={handleClick}>
{isLoading ? <span className="loading loading-spinner"></span> : null}{' '}
Show
{isLoading ? <Spinner variant="primary" size="md" /> : null}
</button>
</div>
);
},
};

/**
* Suspenseを使ったサンプルです。<br />
* Storybook上でNext.jsのServer ComponentsでAxios使う方法の変わりにuse Hooksを使っています。<br />
* <b>show code</b>ボタンからコードを確認できます。
*/
export const Suspense: Story = {
render: () => {
const APIRequest = new Promise<string>(resolve =>
setTimeout(() => resolve('Fetch Data!'), 3000),
);
const FetchMessage = ({ APIRequest }: { APIRequest: Promise<string> }) => {
const res = use(APIRequest);
return (
<div className="w-fit">
<h1 className="text-6xl font-bold text-green-500">{res}</h1>
</div>
);
};

return (
<div className="p-16 grid place-content-center border-2 border-gray-300 rounded-3xl h-[192px] w-[600px]">
<React.Suspense fallback={<Spinner variant="primary" size="md" />}>
<FetchMessage APIRequest={APIRequest} />
</React.Suspense>
</div>
);
},
};
91 changes: 91 additions & 0 deletions src/components/Spinner/Spinner.test.tsx
@@ -0,0 +1,91 @@
import React from 'react';

import { render } from '@testing-library/react';
import { Spinner } from './Spinner';

// Generated by CodiumAI

describe('Spinner', () => {
// Renders a spinner with default size and variant
test('デフォルトのサイズとバリアントでスピナーをレンダリングする', () => {
const { container } = render(<Spinner />);
expect(container).toBeInTheDocument();
});

// Renders a spinner with custom size and variant
test('カスタムのサイズとバリアントでスピナーをレンダリングする', () => {
const { container } = render(<Spinner size="lg" variant="light" />);
expect(container).toBeInTheDocument();
});

// Renders a spinner with no className
test('classNameなしでスピナーをレンダリングする', () => {
const { container } = render(<Spinner />);
expect(container).toBeInTheDocument();
});

// Renders a spinner with data-testid attribute
test('data-testid属性を持つスピナーをレンダリングする', () => {
const { getByTestId } = render(<Spinner />);
expect(getByTestId('loading')).toBeInTheDocument();
});

// Renders a spinner with size set to 'lg'
test('サイズを「lg」に設定したスピナーをレンダリングする', () => {
const { container } = render(<Spinner size="lg" />);
expect(container).toBeInTheDocument();
});

// Renders a spinner with size set to 'sm'
test('サイズを「sm」に設定したスピナーをレンダリングする', () => {
const { container } = render(<Spinner size="sm" />);
expect(container).toBeInTheDocument();
});

// Renders a spinner with variant set to 'light'
test('バリアントを「light」に設定したスピナーをレンダリングする', () => {
const { container } = render(<Spinner variant="light" />);
expect(container).toBeInTheDocument();
});

// test passing all variant and size
test('全てのvariantsとsizesでレンダリングする', () => {
// Render the Spinner component with all variants and sizes
const { container } = render(
<>
<Spinner variant="light" size="lg" />
<Spinner variant="primary" size="md" />
<Spinner variant="light" size="sm" />
<Spinner variant="primary" size="xl" />
</>,
);

// Assert that the Spinner components are rendered with the correct SVG elements
const svgs = container.querySelectorAll('svg');
expect(svgs.length).toBe(4);
expect(svgs[0].getAttribute('class')).toBe(
'animate-spin h-16 w-16 text-white',
);
expect(svgs[1].getAttribute('class')).toBe(
'animate-spin h-8 w-8 text-blue-200',
);
expect(svgs[2].getAttribute('class')).toBe(
'animate-spin h-4 w-4 text-white',
);
expect(svgs[3].getAttribute('class')).toBe(
'animate-spin h-24 w-24 text-blue-200',
);

// Assert that the Spinner components are rendered with the correct circle and path elements
const circles = container.querySelectorAll('circle');
const paths = container.querySelectorAll('path');
expect(circles.length).toBe(4);
expect(paths.length).toBe(4);

// Assert that the Spinner components are rendered with the correct span element
const spans = container.querySelectorAll('span');
expect(spans.length).toBe(4);
expect(spans[0].getAttribute('class')).toBe('sr-only');
expect(spans[0].textContent).toBe('Loading');
});
});
61 changes: 61 additions & 0 deletions src/components/Spinner/Spinner.tsx
@@ -0,0 +1,61 @@
import classNames from 'classnames';
import React, { memo } from 'react';

const sizes = {
lg: 'h-16 w-16',
md: 'h-8 w-8',
sm: 'h-4 w-4',
xl: 'h-24 w-24',
};

const variants = {
light: 'text-white',
primary: 'text-blue-200',
};

export type SpinnerProps = {
className?: string;
size?: keyof typeof sizes;
variant?: keyof typeof variants;
};

/**
* ロード中などに表示するSpinnerです。<br />
* lg、md、sm, xsのサイズがあります。<br />
* いまのところdark themeがないので、variantのlightは使わないと思います。
*
*
*/
export const Spinner: React.FC<React.PropsWithChildren<SpinnerProps>> = memo(
({ className = '', size = 'md', variant = 'primary' }: SpinnerProps) => {
return (
<>
<svg
className={classNames(
'animate-spin',
sizes[size],
variants[variant],
className,
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
data-testid="loading">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span className="sr-only">Loading</span>
</>
);
},
);
Spinner.displayName = 'Spinner';
1 change: 1 addition & 0 deletions src/components/Spinner/index.ts
@@ -0,0 +1 @@
export * from './Spinner';

0 comments on commit 9526b37

Please sign in to comment.