From 2aff3a79f76dc2f1287ffb51c6886dc9768c74bb Mon Sep 17 00:00:00 2001 From: hextion <100ishundred@gmail.com> Date: Mon, 12 Aug 2024 23:33:39 +0300 Subject: [PATCH] feat: new Spinner --- .changeset/cuddly-jeans-laugh.md | 20 ++ packages/action-button/src/Component.tsx | 1 + ...on-button-disabled-loading-sprite-snap.png | 4 +- packages/action-button/tsconfig.json | 3 +- .../src/__snapshots__/Component.test.tsx.snap | 68 +++--- .../src/components/base-button/Component.tsx | 1 + packages/button/tsconfig.json | 3 +- .../__testfixtures__/transform.input.tsx | 20 ++ .../__testfixtures__/transform.output.tsx | 20 ++ .../src/spinner/__tests__/transform.test.ts | 6 + packages/codemod/src/spinner/transform.ts | 111 +++++++++ .../src/__snapshots__/Component.test.tsx.snap | 34 +-- .../screens/initial/countdown-section.tsx | 2 +- packages/confirmation/tsconfig.json | 3 +- .../src/__snapshots__/Component.test.tsx.snap | 34 +-- .../file-upload-item-v1/src/Component.tsx | 2 +- ...e-0-show-delete-0-upload-status-1-snap.png | 4 +- ...e-0-show-delete-0-upload-status-2-snap.png | 4 +- ...e-0-upload-status-2-show-delete-0-snap.png | 4 +- ...e-0-upload-status-3-show-delete-0-snap.png | 4 +- .../file-upload-item-v1/src/index.module.css | 5 - packages/file-upload-item/tsconfig.json | 3 +- packages/shared/src/fnUtils.ts | 12 +- packages/shared/src/index.ts | 1 + packages/shared/src/object.ts | 26 +++ packages/spinner/package.json | 1 + .../src/Component.screenshots.test.tsx | 95 ++++++++ packages/spinner/src/Component.test.tsx | 73 +++++- packages/spinner/src/Component.tsx | 212 +++++++++-------- .../spinner-dark-preview-snap.png | 4 +- .../spinner-main-props-sprite-snap.png | 4 +- .../spinner-preset-dark-preview-snap.png | 3 + .../spinner-preset-main-props-sprite-snap.png | 3 + .../spinner-preset-preview-snap.png | 3 + .../spinner-preview-snap.png | 4 +- .../src/__snapshots__/Component.test.tsx.snap | 217 +++++++++++++++++- .../src/component.screenshots.test.tsx | 64 ------ packages/spinner/src/default.module.css | 6 +- .../spinner/src/docs/Component.stories.mdx | 34 ++- packages/spinner/src/docs/description.mdx | 32 ++- packages/spinner/src/docs/development.mdx | 24 +- packages/spinner/src/index.module.css | 29 +-- packages/spinner/src/inverted.module.css | 6 +- packages/spinner/src/preset.module.css | 13 ++ packages/spinner/src/vars.css | 13 ++ packages/spinner/tsconfig.json | 7 +- tsconfig.json | 3 +- 47 files changed, 904 insertions(+), 341 deletions(-) create mode 100644 .changeset/cuddly-jeans-laugh.md create mode 100644 packages/codemod/src/spinner/__testfixtures__/transform.input.tsx create mode 100644 packages/codemod/src/spinner/__testfixtures__/transform.output.tsx create mode 100644 packages/codemod/src/spinner/__tests__/transform.test.ts create mode 100644 packages/codemod/src/spinner/transform.ts create mode 100644 packages/shared/src/object.ts create mode 100644 packages/spinner/src/Component.screenshots.test.tsx create mode 100644 packages/spinner/src/__image_snapshots__/spinner-preset-dark-preview-snap.png create mode 100644 packages/spinner/src/__image_snapshots__/spinner-preset-main-props-sprite-snap.png create mode 100644 packages/spinner/src/__image_snapshots__/spinner-preset-preview-snap.png delete mode 100644 packages/spinner/src/component.screenshots.test.tsx create mode 100644 packages/spinner/src/preset.module.css create mode 100644 packages/spinner/src/vars.css diff --git a/.changeset/cuddly-jeans-laugh.md b/.changeset/cuddly-jeans-laugh.md new file mode 100644 index 0000000000..19b17752aa --- /dev/null +++ b/.changeset/cuddly-jeans-laugh.md @@ -0,0 +1,20 @@ +--- +'@alfalab/core-components-spinner': major +'@alfalab/core-components-shared': minor +--- + +Крупное обновление Спиннера + +* Обновленный вид спиннера. +* Добавлены новые пропсы для тонкой настройки внешнего вида: + - `preset` - преднастроенный вариант спиннера; + - `size` - теперь отвечает за размер кольца спиннера; + - `lineWidth` - толщина линии спиннера; + - `style` - позволяет регулировать отступы, цвет и т.п. +* Добавлен `codemod` для бесшовной миграции `Spinner`: + ```bash + npx @alfalab/core-components-codemod --transformers=spinner --glob='src/**/*.tsx' + ``` + | Внимание | + |---| + | `codemod` может не работать в случаях использования [Spread Operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals) в коде. | diff --git a/packages/action-button/src/Component.tsx b/packages/action-button/src/Component.tsx index 7935e379d2..cd9c8d2bc3 100644 --- a/packages/action-button/src/Component.tsx +++ b/packages/action-button/src/Component.tsx @@ -150,6 +150,7 @@ export const ActionButton = forwardRef {showLoader ? ( @@ -320,27 +321,27 @@ Object { id=":r1:" >
@@ -354,8 +355,9 @@ Object { href="https://some-url" > @@ -363,27 +365,27 @@ Object { id=":r1:" >
@@ -454,8 +456,9 @@ Object { type="button" > @@ -463,27 +466,27 @@ Object { id=":r0:" >
@@ -497,8 +500,9 @@ Object { type="button" > @@ -506,27 +510,27 @@ Object { id=":r0:" >
diff --git a/packages/button/src/components/base-button/Component.tsx b/packages/button/src/components/base-button/Component.tsx index 6f494021d9..c202d08eba 100644 --- a/packages/button/src/components/base-button/Component.tsx +++ b/packages/button/src/components/base-button/Component.tsx @@ -166,6 +166,7 @@ export const BaseButton = React.forwardRef< {showLoader && ( { + const someProps = { size: 48 }; + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/codemod/src/spinner/__testfixtures__/transform.output.tsx b/packages/codemod/src/spinner/__testfixtures__/transform.output.tsx new file mode 100644 index 0000000000..c8253d5370 --- /dev/null +++ b/packages/codemod/src/spinner/__testfixtures__/transform.output.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { Spinner } from '@alfalab/core-components/spinner'; + +export const Component = () => { + const someProps = { size: 48 }; + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/codemod/src/spinner/__tests__/transform.test.ts b/packages/codemod/src/spinner/__tests__/transform.test.ts new file mode 100644 index 0000000000..68504ef34d --- /dev/null +++ b/packages/codemod/src/spinner/__tests__/transform.test.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line prefer-destructuring +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +jest.autoMockOff(); + +defineTest(__dirname, 'transform', null, 'transform', { parser: 'tsx' }); diff --git a/packages/codemod/src/spinner/transform.ts b/packages/codemod/src/spinner/transform.ts new file mode 100644 index 0000000000..1fc48d817b --- /dev/null +++ b/packages/codemod/src/spinner/transform.ts @@ -0,0 +1,111 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-shadow */ +import { ASTPath, JSXElement, Transform } from 'jscodeshift'; + +import { log } from '../utils'; + +const spinnerTransform: Transform = (fileInfo, api) => { + const j = api.jscodeshift; + const source = j(fileInfo.source); + + source.findJSXElements('Spinner').forEach((path) => { + j(path).replaceWith((path: ASTPath) => { + const jsxOpeningElement = j(path).find(j.JSXOpeningElement, { + name: { name: 'Spinner' }, + }); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { value: 'xs' }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(16)), + ), + ); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { expression: { value: 16 } }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(16)), + ), + ); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { value: 's' }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(24)), + ), + ); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { expression: { value: 24 } }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(24)), + ), + ); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { value: 'm' }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(48)), + ), + ); + + jsxOpeningElement + .find(j.JSXAttribute, { + name: { name: 'size' }, + value: { expression: { value: 48 } }, + }) + .replaceWith(() => + j.jsxAttribute( + j.jsxIdentifier('preset'), + j.jsxExpressionContainer(j.literal(48)), + ), + ); + + if ( + jsxOpeningElement.find(j.JSXSpreadAttribute).length > 0 && + jsxOpeningElement.find(j.JSXAttribute, { + name: { name: 'size' }, + }).length === 0 + ) { + log( + `Не удалось определить значение 'size' компонента 'Spinner', используется spread оператор:\n${fileInfo.path}:${path.node.openingElement.loc?.start.line}\n`, + 'warning', + ); + } + + return path.node; + }); + }); + + return source.toSource({ + quote: 'single', + wrapColumn: 1000, + }); +}; + +export default spinnerTransform; diff --git a/packages/confirmation/src/__snapshots__/Component.test.tsx.snap b/packages/confirmation/src/__snapshots__/Component.test.tsx.snap index 0cad763a99..6d57f5ba11 100644 --- a/packages/confirmation/src/__snapshots__/Component.test.tsx.snap +++ b/packages/confirmation/src/__snapshots__/Component.test.tsx.snap @@ -141,8 +141,9 @@ exports[`Confirmation Snapshot tests should match snapshot with CODE_CHECKING st class="loaderWrap countdownContainer typographyTheme" > @@ -150,27 +151,27 @@ exports[`Confirmation Snapshot tests should match snapshot with CODE_CHECKING st id=":r0:" >
@@ -340,8 +341,9 @@ exports[`Confirmation Snapshot tests should match snapshot with CODE_SENDING sta class="loaderWrap countdownContainer typographyTheme" > @@ -349,27 +351,27 @@ exports[`Confirmation Snapshot tests should match snapshot with CODE_SENDING sta id=":r1:" >
diff --git a/packages/confirmation/src/components/screens/initial/countdown-section.tsx b/packages/confirmation/src/components/screens/initial/countdown-section.tsx index ada328d93b..30813ef891 100644 --- a/packages/confirmation/src/components/screens/initial/countdown-section.tsx +++ b/packages/confirmation/src/components/screens/initial/countdown-section.tsx @@ -50,7 +50,7 @@ export const CountdownSection: FC = ({ [styles.typographyTheme]: !mobile, })} > - + {state === 'CODE_CHECKING' ? texts.codeChecking : texts.codeSending} diff --git a/packages/confirmation/tsconfig.json b/packages/confirmation/tsconfig.json index dc667d5408..626e9af437 100644 --- a/packages/confirmation/tsconfig.json +++ b/packages/confirmation/tsconfig.json @@ -8,7 +8,8 @@ "paths": { "@alfalab/core-components-*": ["../*/src"], "@alfalab/core-components-code-input/*": ["../code-input/src/*"], - "@alfalab/core-components-button/*": ["../button/src/*"] + "@alfalab/core-components-button/*": ["../button/src/*"], + "@alfalab/core-components-spinner/*": ["../spinner/src/*"] } }, "references": [ diff --git a/packages/custom-button/src/__snapshots__/Component.test.tsx.snap b/packages/custom-button/src/__snapshots__/Component.test.tsx.snap index 7632a18aaa..67e42c4d45 100644 --- a/packages/custom-button/src/__snapshots__/Component.test.tsx.snap +++ b/packages/custom-button/src/__snapshots__/Component.test.tsx.snap @@ -304,8 +304,9 @@ Object { type="button" > @@ -313,27 +314,27 @@ Object { id=":r1:" >
@@ -348,8 +349,9 @@ Object { type="button" > @@ -357,27 +359,27 @@ Object { id=":r1:" >
diff --git a/packages/file-upload-item-v1/src/Component.tsx b/packages/file-upload-item-v1/src/Component.tsx index cae364314c..0fd391f09d 100644 --- a/packages/file-upload-item-v1/src/Component.tsx +++ b/packages/file-upload-item-v1/src/Component.tsx @@ -176,7 +176,7 @@ export const FileUploadItemV1: React.FC = ({ case 'UPLOADING': return (
- +
); default: { diff --git a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-1-snap.png b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-1-snap.png index a42e40f628..0bde35491d 100644 --- a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-1-snap.png +++ b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e43c9d0f2b1c013d6dd25f606469742b925e4922b3b694c9acca15f244d355b -size 3328 +oid sha256:39d2ec3c53409b5f84c927d4f3ba82ac53e5193286627cc70b18426f00bb2503 +size 3389 diff --git a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-2-snap.png b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-2-snap.png index beb67d06b4..45f0d2e152 100644 --- a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-2-snap.png +++ b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-hide-meta-when-upload-status-success-name-0-upload-date-0-size-0-show-delete-0-upload-status-2-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ff4301023d6e0b402619c4573bf155b85e1dc6a3cca09000866d1b261c266e2 -size 3880 +oid sha256:db6e306a4f06a154d329f949575910cfde556d0946d0fe7b0395a17474ed1983 +size 3970 diff --git a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-2-show-delete-0-snap.png b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-2-show-delete-0-snap.png index a42e40f628..0bde35491d 100644 --- a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-2-show-delete-0-snap.png +++ b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-2-show-delete-0-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e43c9d0f2b1c013d6dd25f606469742b925e4922b3b694c9acca15f244d355b -size 3328 +oid sha256:39d2ec3c53409b5f84c927d4f3ba82ac53e5193286627cc70b18426f00bb2503 +size 3389 diff --git a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-3-show-delete-0-snap.png b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-3-show-delete-0-snap.png index beb67d06b4..45f0d2e152 100644 --- a/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-3-show-delete-0-snap.png +++ b/packages/file-upload-item-v1/src/__image_snapshots__/file-upload-item-v-1-name-with-statuses-name-0-upload-status-3-show-delete-0-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ff4301023d6e0b402619c4573bf155b85e1dc6a3cca09000866d1b261c266e2 -size 3880 +oid sha256:db6e306a4f06a154d329f949575910cfde556d0946d0fe7b0395a17474ed1983 +size 3970 diff --git a/packages/file-upload-item-v1/src/index.module.css b/packages/file-upload-item-v1/src/index.module.css index 0ec796c568..ec3cb70186 100644 --- a/packages/file-upload-item-v1/src/index.module.css +++ b/packages/file-upload-item-v1/src/index.module.css @@ -114,11 +114,6 @@ height: 24px; } -.spinner { - width: 20px; - height: 20px; -} - .uploadPercent { margin-top: var(--gap-4); margin-left: var(--gap-24); diff --git a/packages/file-upload-item/tsconfig.json b/packages/file-upload-item/tsconfig.json index ed1d881cd3..91068a2004 100644 --- a/packages/file-upload-item/tsconfig.json +++ b/packages/file-upload-item/tsconfig.json @@ -7,7 +7,8 @@ "baseUrl": ".", "paths": { "@alfalab/core-components-*": ["../*/src"], - "@alfalab/core-components-button/*": ["../button/src/*"] + "@alfalab/core-components-button/*": ["../button/src/*"], + "@alfalab/core-components-spinner/*": ["../spinner/src/*"] } }, "references": [ diff --git a/packages/shared/src/fnUtils.ts b/packages/shared/src/fnUtils.ts index 81226c3f14..e9817f5d3d 100644 --- a/packages/shared/src/fnUtils.ts +++ b/packages/shared/src/fnUtils.ts @@ -1,8 +1,12 @@ /** * Возвращает true, если значение равно null или undefined */ -function isNil(value: T): value is T & (null | undefined) { - return value == null; +export function isNullable(value: T): value is T & (null | undefined) { + return !isNonNullable(value); +} + +export function isNonNullable(value: T): value is NonNullable { + return value != null; } /** @@ -14,10 +18,10 @@ function clamp(value: T, min: T, max: T): T { return (value instanceof Date ? new Date(clampedValue) : clampedValue) as T; } -function noop() {} +export function noop() {} export const fnUtils = { clamp, noop, - isNil, + isNil: isNullable, }; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d3bf7eea7e..c48d737de4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -12,3 +12,4 @@ export * from './exhaustiveCheck'; export * from './context/PortalContext'; export * from './getComponentBreakpoint'; export * from './warning'; +export * from './object'; diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts new file mode 100644 index 0000000000..c2bfedb496 --- /dev/null +++ b/packages/shared/src/object.ts @@ -0,0 +1,26 @@ +import { isNonNullable } from './fnUtils'; + +export function isObject(value: T): value is T & object { + return isNonNullable(value) && typeof value === 'object'; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// https://stackoverflow.com/a/74608626 +type Intersect = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never; +// https://stackoverflow.com/a/52991061 +// eslint-disable-next-line @typescript-eslint/ban-types +type OptionalKeys = { [K in keyof T]-?: {} extends Pick ? K : never }[keyof T]; + +export function hasOwnProperty>( + val: T, + prop: K, +): val is T & { [P in K]-?: T[P] }; +export function hasOwnProperty(val: T, prop: K): boolean; +export function hasOwnProperty>( + val: T, + prop: K, +): val is Extract ? { [P in K]?: any } : { [P in K]: any }>; +export function hasOwnProperty(val: T, prop: PropertyKey) { + return Object.prototype.hasOwnProperty.call(val, prop); +} +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/spinner/package.json b/packages/spinner/package.json index e841d2e0e3..fc7a197354 100644 --- a/packages/spinner/package.json +++ b/packages/spinner/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@alfalab/hooks": "^1.13.0", + "@alfalab/core-components-shared": "^0.13.0", "classnames": "^2.3.1", "tslib": "^2.4.0" }, diff --git a/packages/spinner/src/Component.screenshots.test.tsx b/packages/spinner/src/Component.screenshots.test.tsx new file mode 100644 index 0000000000..bd02447024 --- /dev/null +++ b/packages/spinner/src/Component.screenshots.test.tsx @@ -0,0 +1,95 @@ +import { + setupScreenshotTesting, + createSpriteStorybookUrl, + createPreview, +} from '../../screenshot-utils'; + +const screenshotTesting = setupScreenshotTesting({ + it, + beforeAll, + afterAll, + expect, +}); + +describe('Spinner', () => + createPreview( + { + componentName: 'Spinner', + knobs: { + size: 40, + lineWidth: 4, + style: JSON.stringify({ padding: 4 }), + visible: true, + }, + }, + 'transform:scale(3.2)', + )); + +describe( + 'Spinner | main props', + screenshotTesting({ + cases: [ + [ + 'sprite', + createSpriteStorybookUrl({ + componentName: 'Spinner', + knobs: { + size: [48, 64, 80], + lineWidth: [4, 8, 12], + visible: [false, true], + style: [ + JSON.stringify({ color: 'var(--color-light-decorative-text-red)' }), + JSON.stringify({ color: 'var(--color-light-decorative-text-blue)' }), + ], + }, + size: { width: 100, height: 100 }, + }), + ], + ], + screenshotOpts: { + fullPage: true, + }, + viewport: { + width: 250, + height: 100, + }, + }), +); + +describe('Spinner preset', () => + createPreview( + { + componentName: 'Spinner', + knobs: { + preset: 48, + visible: true, + }, + }, + 'transform:scale(3.2)', + )); + +describe( + 'Spinner preset | main props', + screenshotTesting({ + cases: [ + [ + 'sprite', + createSpriteStorybookUrl({ + componentName: 'Spinner', + knobs: { + preset: [24, 48, 16], + visible: [false, true], + }, + size: { width: 100, height: 60 }, + }), + ], + ], + screenshotOpts: { + fullPage: true, + }, + viewport: { + width: 250, + height: 100, + }, + }), +); diff --git a/packages/spinner/src/Component.test.tsx b/packages/spinner/src/Component.test.tsx index 9898ad561c..611f7de788 100644 --- a/packages/spinner/src/Component.test.tsx +++ b/packages/spinner/src/Component.test.tsx @@ -1,15 +1,21 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { devWarning } from '@alfalab/core-components-shared'; import { Spinner } from './index'; jest.mock('@alfalab/hooks', () => ({ useId: () => 1 })); +jest.mock('@alfalab/core-components-shared', () => { + const original = jest.requireActual('@alfalab/core-components-shared'); + return Object.assign({ __esModule: true }, original, { devWarning: jest.fn() }); +}); + const testId = 'spinner'; describe('Snapshots tests', () => { it('should display correctly', () => { - const { container } = render(); + const { container } = render(); expect(container).toMatchSnapshot(); }); @@ -19,7 +25,15 @@ describe('Attributes tests', () => { it('should set data-test-id attribute', async () => { const className = 'custom'; - render(); + render( + , + ); const spinnerContentWrap = document.querySelector(`.${className}`); @@ -31,27 +45,62 @@ describe('Attributes tests', () => { describe('Render tests', () => { it('should unmount without errors', async () => { - const { unmount } = render(); + const { unmount } = render(); expect(unmount).not.toThrowError(); }); it('should have visible class if prop visible is true', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + , + ); expect(getByTestId(testId)).toHaveClass('visible'); }); +}); - it('should set `size` class', () => { - const size = 24; - const { container } = render(); - - expect(container.firstElementChild).toHaveClass(`size-${size}`); +describe('Spinner props', () => { + it('should support `size` and `lineWidth`', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it('should set correct size', () => { + const { container } = render( + , + ); + expect(container.firstElementChild).toHaveStyle({ + height: '40px', + width: '40px', + padding: '4px', + }); + }); + it('should support `style`', () => { + const color = '#EC2D20'; + const padding = 2; + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + expect(container.firstElementChild).toHaveStyle({ color, padding: `${padding}px` }); }); - it('should use default `size`', () => { - const { container } = render(); + it('should warn color via styles', () => { + render(); + expect(devWarning).toBeCalledWith( + expect.stringContaining( + "[Spinner]: Палитра, в контексте которой используется спиннер (проп 'colors') игнорируется.", + ), + ); + }); - expect(container.firstElementChild).toHaveClass('size-24'); + test.each([16, 24, 48] as const)('should render preset %p correctly', (preset) => { + const { container } = render(); + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/spinner/src/Component.tsx b/packages/spinner/src/Component.tsx index 7c3e0fe953..a66792dbf5 100644 --- a/packages/spinner/src/Component.tsx +++ b/packages/spinner/src/Component.tsx @@ -1,122 +1,124 @@ import React, { FC } from 'react'; import cn from 'classnames'; +import { devWarning, hasOwnProperty, isNonNullable } from '@alfalab/core-components-shared'; import { useId } from '@alfalab/hooks'; import defaultColors from './default.module.css'; import styles from './index.module.css'; import invertedColors from './inverted.module.css'; +import presetStyles from './preset.module.css'; + +export type SpinnerProps = + | { + /** + * Палитра, в контексте которой используется спиннер + * @default default + */ + colors?: 'default' | 'inverted'; + + /** + * Управление видимостью компонента + * @default false + */ + visible?: boolean; + + /** + * Дополнительный класс + */ + className?: string; + + /** + * Идентификатор компонента в DOM + */ + id?: string; + + /** + * Идентификатор для систем автоматизированного тестирования + */ + dataTestId?: string; + + /** + * Дополнительные инлайн стили для cпиннера + */ + style?: React.CSSProperties; + } & ( + | { + /** + * Размер спиннера (кольца) + */ + size: number; + + /** + * Толщина линии спинера (кольца) + */ + lineWidth: number; + } + | { + /** + * Преднастроенный вариант + */ + preset: 16 | 24 | 48; + } + ); const colorStyles = { default: defaultColors, inverted: invertedColors, }; -export type SpinnerProps = { - /** - * Управление видимостью компонента - */ - visible?: boolean; - - /** - * Размер компонента - * @description xs, s, m deprecated, используйте вместо них 16, 24, 48 соответственно - */ - size?: 'xs' | 's' | 'm' | 16 | 24 | 48; - - /** - * Дополнительный класс - */ - className?: string; - - /** - * Идентификатор компонента в DOM - */ - id?: string; - - /** - * Идентификатор для систем автоматизированного тестирования - */ - dataTestId?: string; - - /** - * Палитра, в контексте которой используется спиннер - */ - colors?: 'default' | 'inverted'; -}; - -const CONFIG = { - xs: { - padding: 1, - lineWidth: 2, - size: 18, - }, - s: { - padding: 2, - lineWidth: 2, - size: 24, - }, - m: { - padding: 4, - lineWidth: 4, - size: 48, - }, - 16: { - padding: 1, - lineWidth: 2, - size: 18, - }, - 24: { - padding: 2, - lineWidth: 2, - size: 24, - }, - 48: { - padding: 4, - lineWidth: 4, - size: 48, - }, +const PRESET_CONFIG = { + 16: [2, 14, 'preset16'], + 24: [2, 20, 'preset24'], + 48: [4, 40, 'preset48'], } as const; -export const SIZE_TO_CLASSNAME_MAP = { - xs: 'size-16', - s: 'size-24', - m: 'size-48', - 16: 'size-16', - 24: 'size-24', - 48: 'size-48', -}; +export const Spinner: FC = (props) => { + const { style, visible, id, className, dataTestId, colors = 'default' } = props; + let size: number; + let lineWidth: number; + let presetClassname: string | undefined; + + if (hasOwnProperty(props, 'preset')) { + const { preset } = props; + const config = PRESET_CONFIG[preset]; + const [, , styleKey] = config; + + [lineWidth, size] = config; + presetClassname = presetStyles[styleKey]; + } else { + size = props.size; + lineWidth = props.lineWidth; + } -export const Spinner: FC = ({ - size: sizeFromProps = 24, - colors = 'default', - visible, - id, - className, - dataTestId, -}) => { + const color = style?.color; + + if (isNonNullable(color)) { + devWarning( + `[Spinner]: Палитра, в контексте которой используется спиннер (проп 'colors') игнорируется. Используется цвет 'style.color' ${color}`, + ); + } const uniqId = useId(); - const { size, padding, lineWidth } = CONFIG[sizeFromProps]; - const spinnerSize = size - padding * 2; - const spinnerOrigin = spinnerSize / 2 + padding; - const spinnerRadius = spinnerSize / 2 - lineWidth / 2; - const rotationAngle /* deg */ = Math.ceil( - (Math.asin(lineWidth / 2 / spinnerRadius) * 180) / Math.PI, - ); - const gap /* deg */ = 2; + const radius = size / 2 - lineWidth / 2; + const rotationAngle /* deg */ = Math.ceil((Math.asin(lineWidth / 2 / radius) * 180) / Math.PI); + const gap /* deg */ = 90; + const pathLength /* deg */ = 360; + const strokeDasharray = `${pathLength - gap - rotationAngle} ${gap + rotationAngle}`; + const gradient = `conic-gradient(from ${rotationAngle}deg, transparent ${ + gap - rotationAngle * 2 + }deg, currentColor)`; return ( = ({ -
+
); diff --git a/packages/spinner/src/__image_snapshots__/spinner-dark-preview-snap.png b/packages/spinner/src/__image_snapshots__/spinner-dark-preview-snap.png index 9cce1f511f..529618dc29 100644 --- a/packages/spinner/src/__image_snapshots__/spinner-dark-preview-snap.png +++ b/packages/spinner/src/__image_snapshots__/spinner-dark-preview-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36b691165d60cccef829549a8fc83896e8304fc959e3f410a92cfa71472c5956 -size 8457 +oid sha256:1b1b59f23238563a93601d7e85ab4c0ad02ce1763271ba5a61fd245402b06aa7 +size 8316 diff --git a/packages/spinner/src/__image_snapshots__/spinner-main-props-sprite-snap.png b/packages/spinner/src/__image_snapshots__/spinner-main-props-sprite-snap.png index cc39c79352..178e4be1df 100644 --- a/packages/spinner/src/__image_snapshots__/spinner-main-props-sprite-snap.png +++ b/packages/spinner/src/__image_snapshots__/spinner-main-props-sprite-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3952b2ceefdd22f3f1a0beeb2b0b5b7918fc3f87c0e3a96fa9843f656a2b8e56 -size 8464 +oid sha256:d9a3f4991676173bf1c3ec2bdce5dc8540fbe452f2f30415c63f546faa7788bb +size 228864 diff --git a/packages/spinner/src/__image_snapshots__/spinner-preset-dark-preview-snap.png b/packages/spinner/src/__image_snapshots__/spinner-preset-dark-preview-snap.png new file mode 100644 index 0000000000..2f319f1033 --- /dev/null +++ b/packages/spinner/src/__image_snapshots__/spinner-preset-dark-preview-snap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bc1f130adf09dd1941f2d2f5cbfe2ac9a2e10ee532f5da3b873dbe5dd0e29ee +size 8289 diff --git a/packages/spinner/src/__image_snapshots__/spinner-preset-main-props-sprite-snap.png b/packages/spinner/src/__image_snapshots__/spinner-preset-main-props-sprite-snap.png new file mode 100644 index 0000000000..d4d35f55f9 --- /dev/null +++ b/packages/spinner/src/__image_snapshots__/spinner-preset-main-props-sprite-snap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35c3843d5579ed923a9b736a0a8f365a863e47b7dadc876dab2e380967dd0c94 +size 8689 diff --git a/packages/spinner/src/__image_snapshots__/spinner-preset-preview-snap.png b/packages/spinner/src/__image_snapshots__/spinner-preset-preview-snap.png new file mode 100644 index 0000000000..6df1cb9ac8 --- /dev/null +++ b/packages/spinner/src/__image_snapshots__/spinner-preset-preview-snap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf101281f14526420b02e3dc8ae6df9636e309d47fc8e1c98001dd756dd44407 +size 9125 diff --git a/packages/spinner/src/__image_snapshots__/spinner-preview-snap.png b/packages/spinner/src/__image_snapshots__/spinner-preview-snap.png index 0504f89c7d..c2d59709e1 100644 --- a/packages/spinner/src/__image_snapshots__/spinner-preview-snap.png +++ b/packages/spinner/src/__image_snapshots__/spinner-preview-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c09d3253a5274e6cd5e315e41cd559feb551a45f869fea671cfa96ab5950d28b -size 8394 +oid sha256:85f81f7ef1722097b268de3b444358577a6f3b635d171fc0067f3dbe6fc24d72 +size 9125 diff --git a/packages/spinner/src/__snapshots__/Component.test.tsx.snap b/packages/spinner/src/__snapshots__/Component.test.tsx.snap index 3b0d228448..bbdf19476f 100644 --- a/packages/spinner/src/__snapshots__/Component.test.tsx.snap +++ b/packages/spinner/src/__snapshots__/Component.test.tsx.snap @@ -3,8 +3,9 @@ exports[`Snapshots tests should display correctly 1`] = `
@@ -12,27 +13,227 @@ exports[`Snapshots tests should display correctly 1`] = ` id="1" > +
+ + +
+`; + +exports[`Spinner props should render preset 16 correctly 1`] = ` +
+ + + + + + + +
+ + +
+`; + +exports[`Spinner props should render preset 24 correctly 1`] = ` +
+ + + + + + + +
+ + +
+`; + +exports[`Spinner props should render preset 48 correctly 1`] = ` +
+ + + + + + + +
+ + +
+`; + +exports[`Spinner props should support \`size\` and \`lineWidth\` 1`] = ` +
+ + + + + + + +
+ + +
+`; + +exports[`Spinner props should support \`style\` 1`] = ` +
+ + + + + + +
diff --git a/packages/spinner/src/component.screenshots.test.tsx b/packages/spinner/src/component.screenshots.test.tsx deleted file mode 100644 index 448ac265d4..0000000000 --- a/packages/spinner/src/component.screenshots.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Page } from 'playwright'; -import { - setupScreenshotTesting, - createSpriteStorybookUrl, - createPreview, -} from '../../screenshot-utils'; - -const screenshotTesting = setupScreenshotTesting({ - it, - beforeAll, - afterAll, - expect, -}); - -const evalFunc = async (page: Page) => { - // Не работают идентификаторы,содержащие ":", почему непонятно. - await page.$$eval('*[id*=":"]', (el) => - el.forEach((e) => e.setAttribute('id', e.getAttribute('id')!.replace(/:/g, ''))), - ); - - await page.$$eval('*[stroke*=":"]', (el) => - el.forEach((e) => e.setAttribute('stroke', e.getAttribute('stroke')!.replace(/:/g, ''))), - ); -}; - -describe('Spinner', () => - createPreview( - { - componentName: 'Spinner', - knobs: { - size: 48, - visible: true, - }, - }, - 'transform:scale(3.2)', - { evaluate: evalFunc }, - )); - -describe( - 'Spinner | main props', - screenshotTesting({ - cases: [ - [ - 'sprite', - createSpriteStorybookUrl({ - componentName: 'Spinner', - knobs: { - size: [24, 48, 16], - visible: [false, true], - }, - size: { width: 100, height: 60 }, - }), - ], - ], - evaluate: evalFunc, - screenshotOpts: { - fullPage: true, - }, - viewport: { - width: 250, - height: 100, - }, - }), -); diff --git a/packages/spinner/src/default.module.css b/packages/spinner/src/default.module.css index 1496d411cf..376ecb412f 100644 --- a/packages/spinner/src/default.module.css +++ b/packages/spinner/src/default.module.css @@ -1,8 +1,4 @@ -@import '../../vars/src/index.css'; - -:root { - --spinner-default-color: var(--color-light-neutral-translucent-1300); -} +@import './vars.css'; .component { color: var(--spinner-default-color); diff --git a/packages/spinner/src/docs/Component.stories.mdx b/packages/spinner/src/docs/Component.stories.mdx index 613388d5d1..5d49a95131 100644 --- a/packages/spinner/src/docs/Component.stories.mdx +++ b/packages/spinner/src/docs/Component.stories.mdx @@ -1,5 +1,5 @@ import { Meta, Story, Markdown } from '@storybook/addon-docs'; -import { boolean, select } from '@storybook/addon-knobs'; +import { boolean, select, number, object } from '@storybook/addon-knobs'; import { ComponentHeader, Tabs } from 'storybook/blocks'; import { Spinner } from '@alfalab/core-components-spinner'; @@ -30,7 +30,37 @@ import Changelog from '../../CHANGELOG.md?raw'; }} > +
+ ); + })} + + + + {React.createElement(() => { + const colors = select('colors', ['default', 'inverted'], 'default'); + return ( +
+ diff --git a/packages/spinner/src/docs/description.mdx b/packages/spinner/src/docs/description.mdx index 8f855c2ba3..440ae1a7fe 100644 --- a/packages/spinner/src/docs/description.mdx +++ b/packages/spinner/src/docs/description.mdx @@ -3,14 +3,14 @@ У компонента есть стандартные размеры: 16, 24 и 48px. ```jsx live -const SIZES = [16, 24, 48]; +const PRESETS = [16, 24, 48]; render( - {SIZES.map((size) => ( -
- + {PRESETS.map((preset) => ( +
+
))} @@ -18,6 +18,26 @@ render( ); ``` +## Кастомизация + +Спиннеру можно задать кастомную высоту, цвет, толщину линии и отступы внутри контейнера. + +```jsx live + +render( + + +
+ +
+
+ +
+
+
, +); +``` + ## Использование в других компонентах Часто используется в [Button](?path=/docs/button--docs). @@ -30,7 +50,7 @@ render(
-
@@ -41,7 +61,7 @@ render( Label
-
diff --git a/packages/spinner/src/docs/development.mdx b/packages/spinner/src/docs/development.mdx index 3e782ae55c..587bc64305 100644 --- a/packages/spinner/src/docs/development.mdx +++ b/packages/spinner/src/docs/development.mdx @@ -1,18 +1,30 @@ -import { ArgsTable } from '@storybook/addon-docs'; +import { ArgTypes } from '@storybook/addon-docs'; import { CssVars } from 'storybook/blocks'; -import { Spinner as SpinnerTS } from '../Component'; -import styles from '!!raw-loader!../index.module.css'; +import { Spinner } from '../Component'; +import vars from '!!raw-loader!../vars.css'; -## Подключение +## Подключениe ```jsx import { Spinner } from '@alfalab/core-components/spinner'; ``` +## Использование + +```jsx + +``` + +или преднастроенный вариант + +```jsx + +``` + ## Свойства - + ## Переменные - + diff --git a/packages/spinner/src/index.module.css b/packages/spinner/src/index.module.css index e7b94c55f0..d019af0d94 100644 --- a/packages/spinner/src/index.module.css +++ b/packages/spinner/src/index.module.css @@ -1,14 +1,7 @@ -@import '../../vars/src/index.css'; - -:root { - --spinner-display-visible: inline-block; - --spinner-animation-duration: 0.8s; - --spinner-animation-timing-function: linear; -} +@import './vars.css'; .spinner { display: none; - transform: rotate(-90deg); animation: spin_animation var(--spinner-animation-duration) infinite var(--spinner-animation-timing-function); } @@ -18,33 +11,17 @@ vertical-align: middle; } -.size-16 { - width: 16px; - height: 16px; -} - -.size-24 { - width: 24px; - height: 24px; -} - -.size-48 { - width: 48px; - height: 48px; -} - @keyframes spin_animation { from { - transform: rotate(-90deg); + transform: rotate(0deg); } to { - transform: rotate(270deg); + transform: rotate(360deg); } } .gradient { width: 100%; height: 100%; - background-image: conic-gradient(transparent, currentColor); } diff --git a/packages/spinner/src/inverted.module.css b/packages/spinner/src/inverted.module.css index 8c053dc09a..1e24e347ec 100644 --- a/packages/spinner/src/inverted.module.css +++ b/packages/spinner/src/inverted.module.css @@ -1,8 +1,4 @@ -@import '../../vars/src/index.css'; - -:root { - --spinner-inverted-color: var(--color-light-neutral-translucent-1300-inverted); -} +@import './vars.css'; .component { color: var(--spinner-inverted-color); diff --git a/packages/spinner/src/preset.module.css b/packages/spinner/src/preset.module.css new file mode 100644 index 0000000000..dde1232882 --- /dev/null +++ b/packages/spinner/src/preset.module.css @@ -0,0 +1,13 @@ +@import './vars.css'; + +.preset16 { + padding: 1px; +} + +.preset24 { + padding: var(--gap-3xs); +} + +.preset48 { + padding: var(--gap-2xs); +} diff --git a/packages/spinner/src/vars.css b/packages/spinner/src/vars.css new file mode 100644 index 0000000000..e219c10a1b --- /dev/null +++ b/packages/spinner/src/vars.css @@ -0,0 +1,13 @@ +@import '../../vars/src/index.css'; + +:root { + --spinner-display-visible: inline-block; + --spinner-animation-duration: 0.8s; + --spinner-animation-timing-function: linear; + + /* default */ + --spinner-default-color: var(--color-light-neutral-translucent-1300); + + /* inverted */ + --spinner-inverted-color: var(--color-light-neutral-translucent-1300-inverted); +} diff --git a/packages/spinner/tsconfig.json b/packages/spinner/tsconfig.json index 7ea93a53d8..d4e6a557e9 100644 --- a/packages/spinner/tsconfig.json +++ b/packages/spinner/tsconfig.json @@ -7,6 +7,9 @@ "baseUrl": ".", "paths": { "@alfalab/core-components-*": ["../*/src"] - } - } + }, + }, + "references": [ + { "path": "../shared" } + ] } diff --git a/tsconfig.json b/tsconfig.json index 89359562e7..a21f257553 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,7 +40,8 @@ "@alfalab/core-components-input-autocomplete/*": ["packages/input-autocomplete/src/*"], "@alfalab/core-components-step-input/*": ["packages/step-input/src/*"], "@alfalab/core-components-number-input/*": ["packages/number-input/src/*"], - "@alfalab/core-components-navigation-bar-private/*": ["packages/navigation-bar-private/src/*"] + "@alfalab/core-components-navigation-bar-private/*": ["packages/navigation-bar-private/src/*"], + "@alfalab/core-components-spinner/*": ["packages/spinner/src/*"] } }, "exclude": ["node_modules", "dist", "**/*.stories*", "**/*.test*"],