diff --git a/packages/react/src/components/progress-circular/progress-circular.test.tsx b/packages/react/src/components/progress-circular/progress-circular.test.tsx new file mode 100644 index 000000000..d49e16381 --- /dev/null +++ b/packages/react/src/components/progress-circular/progress-circular.test.tsx @@ -0,0 +1,12 @@ +import { renderWithTheme } from '../../test-utils/renderer'; +import { ProgressCircular } from './progress-circular'; + +describe('ProgressCircular', () => { + test('Matches the snapshot', () => { + const tree = renderWithTheme( + , + ); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap b/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap new file mode 100644 index 000000000..08981d646 --- /dev/null +++ b/packages/react/src/components/progress-circular/progress-circular.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProgressCircular Matches the snapshot 1`] = ` +.c0 { + -webkit-transition: stroke-dashoffset 850ms ease; + transition: stroke-dashoffset 850ms ease; +} + + +`; diff --git a/packages/react/src/components/progress-circular/progress-circular.tsx b/packages/react/src/components/progress-circular/progress-circular.tsx new file mode 100644 index 000000000..91606ed4b --- /dev/null +++ b/packages/react/src/components/progress-circular/progress-circular.tsx @@ -0,0 +1,65 @@ +import { VoidFunctionComponent } from 'react'; +import styled, { useTheme } from 'styled-components'; + +const sizes = { + xsmall: 16, + small: 24, + medium: 32, + large: 64, +}; + +type Size = keyof typeof sizes; + +const VIEWBOX = 64; +const STROKE_WIDTH = 8; +const RADIUS = (VIEWBOX - STROKE_WIDTH) / 2; +const CENTER_XY = VIEWBOX / 2; +const CIRCUMFERENCE = RADIUS * 2 * Math.PI; + +const Circle = styled.circle` + transition: stroke-dashoffset 850ms ease; +`; + +export interface ProgressCircularProps { + className?: string; + size?: Size; + inverted?: boolean; + value: number; +} + +// Source: https://css-tricks.com/building-progress-ring-quickly/ +export const ProgressCircular: VoidFunctionComponent = ({ + className, + inverted = false, + size = 'medium', + value, +}) => { + const theme = useTheme(); + const strokeDashoffset = (1 - (value / 100)) * CIRCUMFERENCE; + + return ( + + ); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0f316e27a..5ffd403d8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -25,6 +25,7 @@ export { NumericInput } from './components/numeric-input/numeric-input'; export { PasswordCreationInput } from './components/password-creation-input/password-creation-input'; export { PasswordInput } from './components/password-input/password-input'; export { PhoneInput } from './components/phone-input/phone-input'; +export { ProgressCircular, ProgressCircularProps } from './components/progress-circular/progress-circular'; export { RadioButtonGroup } from './components/radio-button-group/radio-button-group'; export { SearchContextual } from './components/search/search-contextual'; export { SearchGlobal } from './components/search/search-global'; diff --git a/packages/react/src/themes/tokens/component-tokens.ts b/packages/react/src/themes/tokens/component-tokens.ts index 13bd12442..3e27e3aa4 100644 --- a/packages/react/src/themes/tokens/component-tokens.ts +++ b/packages/react/src/themes/tokens/component-tokens.ts @@ -25,6 +25,7 @@ import { defaultMenuTokens, MenuTokens } from './component/menu-tokens'; import { defaultNumericInputTokens, NumericInputTokens } from './component/numeric-input-tokens'; import { defaultPasswordInputTokens, PasswordInputTokens } from './component/password-input-tokens'; import { defaultPhoneInputTokens, PhoneInputTokens } from './component/phone-input-tokens'; +import { defaultProgressCircularTokens, ProgressCircularTokens } from './component/progress-circular-tokens'; import { defaultRadioButtonGroupTokens, RadioButtonGroupTokens } from './component/radio-button-group-tokens'; import { defaultRadioCardTokens, RadioCardTokens } from './component/radio-card-tokens'; import { defaultSearchInputTokens, SearchInputTokens } from './component/search-input-tokens'; @@ -85,6 +86,7 @@ export type ComponentTokens = | NavListTokens | PaginationTokens | ProgressTokens + | ProgressCircularTokens | LinkTokens | BadgeTokens | GlobalBannerTokens @@ -125,6 +127,7 @@ export const defaultComponentTokens: ComponentTokenMap = { ...defaultNavListTokens, ...defaultPaginationTokens, ...defaultProgressTokens, + ...defaultProgressCircularTokens, ...defaultLinkTokens, ...defaultCheckboxTokens, ...defaultChooserTokens, diff --git a/packages/react/src/themes/tokens/component/progress-circular-tokens.ts b/packages/react/src/themes/tokens/component/progress-circular-tokens.ts new file mode 100644 index 000000000..c5028b02c --- /dev/null +++ b/packages/react/src/themes/tokens/component/progress-circular-tokens.ts @@ -0,0 +1,17 @@ +import { AliasTokens } from '../alias-tokens'; +import { RefTokens } from '../ref-tokens'; + +export type ProgressCircularTokens = + | 'progress-circular-color' + | 'progress-circular-inverted-color' + +export type ProgressCircularTokenValue = AliasTokens | RefTokens; + +export type ProgressCircularTokenMap = { + [Token in ProgressCircularTokens]: ProgressCircularTokenValue; +}; + +export const defaultProgressCircularTokens: ProgressCircularTokenMap = { + 'progress-circular-color': 'color-background-brand', + 'progress-circular-inverted-color': 'color-white', +}; diff --git a/packages/storybook/stories/progress-circular.stories.tsx b/packages/storybook/stories/progress-circular.stories.tsx new file mode 100644 index 000000000..0db84c09c --- /dev/null +++ b/packages/storybook/stories/progress-circular.stories.tsx @@ -0,0 +1,47 @@ +import { ProgressCircular } from '@equisoft/design-elements-react'; +import { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Progress Circular', + component: ProgressCircular, + args: { + size: 'medium', + inverted: false, + }, + argTypes: { + className: { + control: { + type: 'text', + }, + }, + inverted: { + control: { + type: 'boolean', + }, + }, + value: { + control: { + type: 'range', + min: 0, + max: 100, + }, + }, + size: { + control: { + type: 'select', + options: ['xsmall', 'small', 'medium', 'large'], + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 60, + inverted: false, + }, +};