From 0947af5459c8147e2a93936959cd765e9913b971 Mon Sep 17 00:00:00 2001 From: "Yang Wooseong (Andrew)" Date: Wed, 29 May 2024 10:19:23 +0900 Subject: [PATCH] Add `AlphaSpinner` component (#2237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Self Checklist - [x] I wrote a PR title in **English** and added an appropriate **label** to the PR. - [x] I wrote the commit message in **English** and to follow [**the Conventional Commits specification**](https://www.conventionalcommits.org/en/v1.0.0/). - [x] I [added the **changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) about the changes that needed to be released. (or didn't have to) - [x] I wrote or updated **documentation** related to the changes. (or didn't have to) - [x] I wrote or updated **tests** related to the changes. (or didn't have to) - [x] I tested the changes in various browsers. (or didn't have to) - Windows: Chrome, Edge, (Optional) Firefox - macOS: Chrome, Edge, Safari, (Optional) Firefox ## Related Issue - Fixes #2236 ## Summary - `AlphaSpinner` 컴포넌트를 구현합니다. ## Details - 기존의 `Spinner` 컴포넌트와 다르게 곡선의 끝부분이 둥글게 디자인되어 있어서 border속성을 이용해서 구현하기가 어려웠습니다. 그래서 `circle` 엘리먼트를 2개 만들고 Indicator에 해당하는 곡선의 stroke-dasharray 속성을 조절하는 것으로 구현했습니다. - 버튼 컴포넌트 안에서 `AlphaSpinner`를 사용해야 하는데, 버튼의 아이콘 크기에 맞게 크기를 조절을 해야합니다. 약간 고민이 필요해보여 후속 pr로 작업하겠습니다. ### Breaking change? (Yes/No) - No ## References - [스펙(internal)](https://www.notion.so/Spinner-e1e30e3fdcb14a3ba94b900d24e0d58e?d=85604f3bca254e758c57c5949412d470&pvs=5#b887eb2d1ad843458f0ba2a90eee430e) - [디자인(internal)](https://www.figma.com/design/KyhPPZeeC0JBmTclJGe3nn/Status?node-id=6%3A69&t=eqYaVwNelOoBo1zv-1) - 참고한 구현방식: https://atlassian.design/components/spinner/examples --- .changeset/sweet-knives-divide.md | 5 ++ .../AlphaSpinner/AlphaSpinner.stories.tsx | 23 +++++++ .../AlphaSpinner/Spinner.module.scss | 66 +++++++++++++++++++ .../components/AlphaSpinner/Spinner.test.tsx | 34 ++++++++++ .../src/components/AlphaSpinner/Spinner.tsx | 50 ++++++++++++++ .../components/AlphaSpinner/Spinner.types.ts | 21 ++++++ .../src/components/AlphaSpinner/index.ts | 2 + packages/bezier-react/src/index.ts | 1 + 8 files changed, 202 insertions(+) create mode 100644 .changeset/sweet-knives-divide.md create mode 100644 packages/bezier-react/src/components/AlphaSpinner/AlphaSpinner.stories.tsx create mode 100644 packages/bezier-react/src/components/AlphaSpinner/Spinner.module.scss create mode 100644 packages/bezier-react/src/components/AlphaSpinner/Spinner.test.tsx create mode 100644 packages/bezier-react/src/components/AlphaSpinner/Spinner.tsx create mode 100644 packages/bezier-react/src/components/AlphaSpinner/Spinner.types.ts create mode 100644 packages/bezier-react/src/components/AlphaSpinner/index.ts diff --git a/.changeset/sweet-knives-divide.md b/.changeset/sweet-knives-divide.md new file mode 100644 index 0000000000..cb36173d73 --- /dev/null +++ b/.changeset/sweet-knives-divide.md @@ -0,0 +1,5 @@ +--- +"@channel.io/bezier-react": patch +--- + +Add `AlphaSpinner` component diff --git a/packages/bezier-react/src/components/AlphaSpinner/AlphaSpinner.stories.tsx b/packages/bezier-react/src/components/AlphaSpinner/AlphaSpinner.stories.tsx new file mode 100644 index 0000000000..67479e56ba --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/AlphaSpinner.stories.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import { type Meta, type StoryFn } from '@storybook/react' + +import { Spinner } from './Spinner' +import { type SpinnerProps } from './Spinner.types' + +const meta: Meta = { + component: Spinner, +} + +export default meta + +const Template: StoryFn = ({ ...args }) => + +export const Primary = { + render: Template, + + args: { + size: 'm', + variant: 'secondary', + }, +} diff --git a/packages/bezier-react/src/components/AlphaSpinner/Spinner.module.scss b/packages/bezier-react/src/components/AlphaSpinner/Spinner.module.scss new file mode 100644 index 0000000000..77b004b1cd --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/Spinner.module.scss @@ -0,0 +1,66 @@ +@use '../../styles/mixins/dimension'; + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.Spinner { + --b-spinner-size: initial; + --b-spinner-track-color: initial; + --b-spinner-indicator-color: initial; + --b-spinner-stroke-width: initial; + --b-spinner-stroke-dasharray: initial; + + @include dimension.square(var(--b-spinner-size)); + + display: inline-flex; + animation: rotate 1s linear infinite; + + & .track { + fill: none; + stroke: var(--b-spinner-track-color); + stroke-linecap: round; + stroke-width: var(--b-spinner-stroke-width); + } + + & .indicator { + fill: none; + stroke: var(--b-spinner-indicator-color); + stroke-dasharray: var(--b-spinner-stroke-dasharray); + stroke-linecap: round; + stroke-width: var(--b-spinner-stroke-width); + } + + &:where(.size-s) { + --b-spinner-size: 28px; + --b-spinner-stroke-width: 4px; + --b-spinner-stroke-dasharray: 40 9999; + } + + &:where(.size-m) { + --b-spinner-size: 50px; + --b-spinner-stroke-width: 6px; + --b-spinner-stroke-dasharray: 60 9999; + } + + &:where(.variant-primary) { + --b-spinner-track-color: var(--alpha-color-primary-bg-lightest); + --b-spinner-indicator-color: var(--alpha-color-fg-blue-normal); + } + + &:where(.variant-secondary) { + --b-spinner-track-color: var(--alpha-color-bg-black-light); + --b-spinner-indicator-color: var(--alpha-color-fg-black-light); + } + + &:where(.variant-on-overlay) { + --b-spinner-track-color: var(--alpha-color-bg-absolute-white-lightest); + --b-spinner-indicator-color: var(--alpha-color-fg-absolute-white-light); + } +} diff --git a/packages/bezier-react/src/components/AlphaSpinner/Spinner.test.tsx b/packages/bezier-react/src/components/AlphaSpinner/Spinner.test.tsx new file mode 100644 index 0000000000..96ec2697ea --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/Spinner.test.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +import { render } from '~/src/utils/test' + +import { SPINNER_TEST_ID, Spinner } from './Spinner' + +describe('Spinner >', () => { + const renderSpinner = (props?: React.ComponentProps) => + render() + + it('should render', () => { + const { getByTestId } = renderSpinner() + const renderedSpinner = getByTestId(SPINNER_TEST_ID) + expect(renderedSpinner).toBeInTheDocument() + }) + + it('should render as a span element by default', () => { + const { getByTestId } = renderSpinner() + const renderedSpinner = getByTestId(SPINNER_TEST_ID) + expect(renderedSpinner.tagName).toBe('SPAN') + }) + + it('should forward ref', () => { + const ref = React.createRef() + renderSpinner({ ref }) + expect(ref.current).toBeInTheDocument() + }) + + it('should receive size', () => { + const { getByTestId } = renderSpinner({ size: 'm' }) + const renderedSpinner = getByTestId(SPINNER_TEST_ID) + expect(renderedSpinner).toHaveClass('size-m') + }) +}) diff --git a/packages/bezier-react/src/components/AlphaSpinner/Spinner.tsx b/packages/bezier-react/src/components/AlphaSpinner/Spinner.tsx new file mode 100644 index 0000000000..1aaa6c1c82 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/Spinner.tsx @@ -0,0 +1,50 @@ +import React, { forwardRef } from 'react' + +import classNames from 'classnames' + +import { type SpinnerProps } from './Spinner.types' + +import styles from './Spinner.module.scss' + +export const SPINNER_TEST_ID = 'bezier-spinner' + +export const Spinner = forwardRef( + function Spinner( + { className, size, variant = 'secondary', ...rest }, + forwardedRef + ) { + return ( + + + + + + + + ) + } +) diff --git a/packages/bezier-react/src/components/AlphaSpinner/Spinner.types.ts b/packages/bezier-react/src/components/AlphaSpinner/Spinner.types.ts new file mode 100644 index 0000000000..368364e3f4 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/Spinner.types.ts @@ -0,0 +1,21 @@ +import { + type BezierComponentProps, + type ColorProps, + type SizeProps, +} from '~/src/types/props' + +type SpinnerSize = 's' | 'm' + +interface SpinnerOwnProps { + /** + * The style variant of Spinner. + * @default 'secondary' + */ + variant?: 'primary' | 'secondary' | 'on-overlay' +} + +export interface SpinnerProps + extends Omit, keyof ColorProps>, + SizeProps, + ColorProps, + SpinnerOwnProps {} diff --git a/packages/bezier-react/src/components/AlphaSpinner/index.ts b/packages/bezier-react/src/components/AlphaSpinner/index.ts new file mode 100644 index 0000000000..a45a7b9962 --- /dev/null +++ b/packages/bezier-react/src/components/AlphaSpinner/index.ts @@ -0,0 +1,2 @@ +export { Spinner as AlphaSpinner } from './Spinner' +export { type SpinnerProps as AlphaSpinnerProps } from './Spinner.types' diff --git a/packages/bezier-react/src/index.ts b/packages/bezier-react/src/index.ts index 70ff6b8b4d..acc1807223 100644 --- a/packages/bezier-react/src/index.ts +++ b/packages/bezier-react/src/index.ts @@ -12,6 +12,7 @@ export * from '~/src/components/AlphaDialogPrimitive' export * from '~/src/components/AlphaFloatingButton' export * from '~/src/components/AlphaFloatingIconButton' export * from '~/src/components/AlphaIconButton' +export * from '~/src/components/AlphaSpinner' export * from '~/src/components/AlphaTooltipPrimitive' export * from '~/src/components/AppProvider' export * from '~/src/components/AutoFocus'