diff --git a/.changeset/blue-dryers-knock.md b/.changeset/blue-dryers-knock.md new file mode 100644 index 0000000000..c730042a6d --- /dev/null +++ b/.changeset/blue-dryers-knock.md @@ -0,0 +1,7 @@ +--- +'@storefront-ui/react': minor +'@storefront-ui/vue': minor +'@storefront-ui/shared': minor +--- + +Added SfBadge component in react and vue diff --git a/apps/docs/components/components/badge.md b/apps/docs/components/components/badge.md new file mode 100644 index 0000000000..3aaaa2f25f --- /dev/null +++ b/apps/docs/components/components/badge.md @@ -0,0 +1,97 @@ +--- +layout: AtomLayout +hideBreadcrumbs: true +--- + +# Badge + +::: slot usage + +## Examples + +### Basic usage + +The badge component must be wrapped with a container that has `class="relative"`. You must provide such a container by yourself, but it gives you the flexibility to put the badge wherever you want. Bagde comes with a "dot" variant, which hides the content. When given content is of type number (or string that could be parsed to number), you can set a maximum limit of that number using `max` prop. + + + +<<<../../preview/nuxt/pages/showcases/Badge/BadgeBasic.vue + + +<<<../../preview/next/pages/showcases/Badge/BadgeBasic.tsx#source + + + +### Placement + +You can align the Badge in every corner of the container. + + + +<<<../../preview/nuxt/pages/showcases/Badge/BadgePlacement.vue + + +<<<../../preview/next/pages/showcases/Badge/BadgePlacement.tsx#source + + + +### Custom outline + +A nifty effect that makes the Badge a bit more attractive is to add an outline that separates the Badge from an element. + + + +<<<../../preview/nuxt/pages/showcases/Badge/BadgeOutline.vue + + +<<<../../preview/next/pages/showcases/Badge/BadgeOutline.tsx#source + + + +### Avatars + +A common use case for the Badge is to place it on a user's avatar. + + + +<<<../../preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue + + +<<<../../preview/next/pages/showcases/Badge/BadgeAvatar.tsx#source + + + +## Playground + + + +::: + +::: slot api + +## Props + +| Prop name | Type | Default value | Possible values | +| ----------- | ------------------ | ------------- | ------------------------------------------------------ | +| `content` | `string | number` | | +| `max` | `number` | `99` | | +| `placement` | `SfBadgePlacement` | `top-right` | `top-right`, `top-left`, `bottom-right`, `bottom-left` | +| `variant` | `SfBadgeVariant` | `standard` | `standard`, `dot` | + +::: + +::: slot source + + + + +<<<../../../packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue + + + + +<<<../../../packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx + + + +::: diff --git a/apps/preview/next/pages/examples/SfBadge.tsx b/apps/preview/next/pages/examples/SfBadge.tsx new file mode 100644 index 0000000000..152c5128c5 --- /dev/null +++ b/apps/preview/next/pages/examples/SfBadge.tsx @@ -0,0 +1,58 @@ +import { SfBadge, SfButton, SfIconShoppingCart, SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/react'; +import { prepareControls } from '../../components/utils/Controls'; +import ComponentExample from '../../components/utils/ComponentExample'; +import { ExamplePageLayout } from '../examples'; + +function Example() { + const { state, controls } = prepareControls( + [ + { + type: 'text', + modelName: 'content', + description: 'Content to display in the badge.', + propType: 'string | number', + }, + { + type: 'text', + modelName: 'max', + description: 'Maximum number of counter to show.', + propType: 'number', + propDefaultValue: '99', + }, + { + type: 'select', + modelName: 'variant', + description: 'Badge can have content or be a simple dot.', + options: Object.values(SfBadgeVariant), + propType: 'SfBadgeVariant', + propDefaultValue: 'standard', + }, + { + type: 'select', + modelName: 'placement', + description: 'Position of the badge relatively to a container.', + options: Object.values(SfBadgePlacement), + propType: 'SfBadgePlacement', + propDefaultValue: 'top-right', + }, + ], + { + content: '1', + max: 99, + variant: SfBadgeVariant.standard, + placement: SfBadgePlacement['top-right'], + }, + ); + + return ( + + + + + + + ); +} + +Example.getLayout = ExamplePageLayout; +export default Example; diff --git a/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx b/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx new file mode 100644 index 0000000000..a3343d249b --- /dev/null +++ b/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx @@ -0,0 +1,25 @@ +import { ShowcasePageLayout } from '../../showcases'; +// #region source +import { SfBadge } from '@storefront-ui/react'; + +export default function BadgeAvatar() { + return ( + + ); +} + +// #endregion source +BadgeAvatar.getLayout = ShowcasePageLayout; diff --git a/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx b/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx new file mode 100644 index 0000000000..f0ff8f75d8 --- /dev/null +++ b/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx @@ -0,0 +1,32 @@ +import { ShowcasePageLayout } from '../../showcases'; +// #region source +import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react'; + +export default function BadgeBasic() { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); +} + +// #endregion source +BadgeBasic.getLayout = ShowcasePageLayout; diff --git a/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx b/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx new file mode 100644 index 0000000000..9a9a3470f6 --- /dev/null +++ b/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx @@ -0,0 +1,34 @@ +import { ShowcasePageLayout } from '../../showcases'; +// #region source +import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react'; + +export default function BadgeOutline() { + return ( +
+
+ + + + +
+ +
+ + + + +
+
+ ); +} + +// #endregion source +BadgeOutline.getLayout = ShowcasePageLayout; diff --git a/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx b/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx new file mode 100644 index 0000000000..ce9cffa89c --- /dev/null +++ b/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx @@ -0,0 +1,32 @@ +import { ShowcasePageLayout } from '../../showcases'; +// #region source +import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react'; + +export default function BadgePlacement() { + return ( +
+ + + + + + + + + + + + + + + + + + + +
+ ); +} + +// #endregion source +BadgePlacement.getLayout = ShowcasePageLayout; diff --git a/apps/preview/nuxt/pages/examples/SfBadge.vue b/apps/preview/nuxt/pages/examples/SfBadge.vue new file mode 100644 index 0000000000..c2a4815618 --- /dev/null +++ b/apps/preview/nuxt/pages/examples/SfBadge.vue @@ -0,0 +1,68 @@ + + + diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue new file mode 100644 index 0000000000..7a8e6cab5d --- /dev/null +++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue new file mode 100644 index 0000000000..fe99b04b96 --- /dev/null +++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue new file mode 100644 index 0000000000..73f70093a2 --- /dev/null +++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue new file mode 100644 index 0000000000..b101271f2f --- /dev/null +++ b/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue @@ -0,0 +1,30 @@ + + + diff --git a/apps/preview/shared/public/@assets/woman_avatar.png b/apps/preview/shared/public/@assets/woman_avatar.png new file mode 100644 index 0000000000..9cf8911ab3 Binary files /dev/null and b/apps/preview/shared/public/@assets/woman_avatar.png differ diff --git a/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx new file mode 100644 index 0000000000..2936b4c64b --- /dev/null +++ b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx @@ -0,0 +1,40 @@ +import { SfBadgePlacement } from '@storefront-ui/shared'; +import classNames from 'classnames'; +import type { SfBadgeProps } from './types'; + +export default function SfBadge({ + content, + variant, + max = 99, + placement = SfBadgePlacement['top-right'], + className, + ...attributes +}: SfBadgeProps): JSX.Element { + const isDot = variant === 'dot'; + let displayValue = content; + if (isDot) { + displayValue = ''; + } else if (!Number.isNaN(content) && Number(content) > max) { + displayValue = `${max}+`; + } + return ( + + {displayValue} + + ); +} diff --git a/packages/sfui/frameworks/react/components/SfBadge/index.ts b/packages/sfui/frameworks/react/components/SfBadge/index.ts new file mode 100644 index 0000000000..dada50f037 --- /dev/null +++ b/packages/sfui/frameworks/react/components/SfBadge/index.ts @@ -0,0 +1,3 @@ +export { default as SfBadge } from './SfBadge'; + +export * from './types'; diff --git a/packages/sfui/frameworks/react/components/SfBadge/types.ts b/packages/sfui/frameworks/react/components/SfBadge/types.ts new file mode 100644 index 0000000000..b5be16ab32 --- /dev/null +++ b/packages/sfui/frameworks/react/components/SfBadge/types.ts @@ -0,0 +1,11 @@ +import { SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/shared'; +import type { PropsWithStyle } from '@storefront-ui/react'; + +export { SfBadgePlacement, SfBadgeVariant }; + +export interface SfBadgeProps extends PropsWithStyle { + content?: string | number; + max?: number; + placement?: `${SfBadgePlacement}`; + variant?: `${SfBadgeVariant}`; +} diff --git a/packages/sfui/frameworks/react/index.ts b/packages/sfui/frameworks/react/index.ts index 3840d65295..099301d7f9 100644 --- a/packages/sfui/frameworks/react/index.ts +++ b/packages/sfui/frameworks/react/index.ts @@ -14,6 +14,7 @@ export * from './shared'; // Components export * from './components/SfAccordionItem'; export * from './components/SfButton'; +export * from './components/SfBadge'; export * from './components/SfCheckbox'; export * from './components/SfChip'; export * from './components/SfCounter'; diff --git a/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue b/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue new file mode 100644 index 0000000000..fd9ee57b0c --- /dev/null +++ b/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/sfui/frameworks/vue/components/SfBadge/index.ts b/packages/sfui/frameworks/vue/components/SfBadge/index.ts new file mode 100644 index 0000000000..6d448fd4bb --- /dev/null +++ b/packages/sfui/frameworks/vue/components/SfBadge/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export { default as SfBadge } from './SfBadge.vue'; diff --git a/packages/sfui/frameworks/vue/components/SfBadge/types.ts b/packages/sfui/frameworks/vue/components/SfBadge/types.ts new file mode 100644 index 0000000000..f7bbe8e8d3 --- /dev/null +++ b/packages/sfui/frameworks/vue/components/SfBadge/types.ts @@ -0,0 +1 @@ +export { SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/shared'; diff --git a/packages/sfui/frameworks/vue/index.ts b/packages/sfui/frameworks/vue/index.ts index 755d33b212..b847b120ef 100644 --- a/packages/sfui/frameworks/vue/index.ts +++ b/packages/sfui/frameworks/vue/index.ts @@ -13,6 +13,7 @@ export * from './shared'; // Components export * from './components/SfAccordionItem'; +export * from './components/SfBadge'; export * from './components/SfButton'; export * from './components/SfCheckbox'; export * from './components/SfChip'; diff --git a/packages/sfui/shared/types/SfBadge.ts b/packages/sfui/shared/types/SfBadge.ts new file mode 100644 index 0000000000..75d0fce717 --- /dev/null +++ b/packages/sfui/shared/types/SfBadge.ts @@ -0,0 +1,11 @@ +export enum SfBadgeVariant { + standard = 'standard', + dot = 'dot', +} + +export enum SfBadgePlacement { + 'top-right' = 'top-right', + 'top-left' = 'top-left', + 'bottom-right' = 'bottom-right', + 'bottom-left' = 'bottom-left', +} diff --git a/packages/sfui/shared/types/index.ts b/packages/sfui/shared/types/index.ts index 4e8a6652da..3d61ec6171 100644 --- a/packages/sfui/shared/types/index.ts +++ b/packages/sfui/shared/types/index.ts @@ -1,4 +1,5 @@ export * from './SfAlert'; +export * from './SfBadge'; export * from './SfButton'; export * from './SfChip'; export * from './SfCounter'; diff --git a/packages/tests/components/SfBadge/SfBadge.PageObject.ts b/packages/tests/components/SfBadge/SfBadge.PageObject.ts new file mode 100644 index 0000000000..d7283bc554 --- /dev/null +++ b/packages/tests/components/SfBadge/SfBadge.PageObject.ts @@ -0,0 +1,13 @@ +import { BasePage } from '../../utils/BasePage'; + +export default class SfBadgeBaseObject extends BasePage { + hasContent(content: string | number) { + this.container.contains(content); + return this; + } + + hasNoContent() { + this.container.should('be.empty'); + return this; + } +} diff --git a/packages/tests/components/SfBadge/SfBadge.cy.tsx b/packages/tests/components/SfBadge/SfBadge.cy.tsx new file mode 100644 index 0000000000..8056f36c53 --- /dev/null +++ b/packages/tests/components/SfBadge/SfBadge.cy.tsx @@ -0,0 +1,109 @@ +/// +import React from 'react'; +import { SfBadgeVariant, SfBadgePlacement } from '@storefront-ui/react'; +import { mount, Wrapper, useComponent } from '../../utils/mount'; + +const { vue: SfBadgeVue, react: SfBadgeReact } = useComponent('SfBadge'); +import SfBadgeBaseObject from './SfBadge.PageObject'; + +describe('SfBadge', () => { + const page = () => new SfBadgeBaseObject('badge'); + + type InitializeComponentParams = { + content?: string | number; + variant?: SfBadgeVariant; + max?: number; + placement?: SfBadgePlacement; + }; + + const initializeComponent = ({ + content, + variant = SfBadgeVariant.standard, + max = 99, + placement = SfBadgePlacement['top-left'], + }: InitializeComponentParams = {}) => { + return mount({ + vue: { + component: SfBadgeVue, + props: { + content, + variant, + max, + placement, + }, + }, + react: , + }); + }; + + it('initial state', () => { + initializeComponent(); + page().makeSnapshot(); + }); + + describe('when prop variant is set to ', () => { + Object.values(SfBadgeVariant).forEach((componentVariant) => { + describe(`${componentVariant}`, () => { + it(`should render correct ${componentVariant} variant`, () => { + initializeComponent({ variant: componentVariant }); + + page().makeSnapshot(); + }); + }); + }); + }); + + describe('when prop placement is set to ', () => { + Object.values(SfBadgePlacement).forEach((componentPlacement) => { + describe(`${componentPlacement}`, () => { + it(`should render correct ${componentPlacement} variant`, () => { + initializeComponent({ placement: componentPlacement }); + + page().makeSnapshot(); + }); + }); + }); + }); + + describe('when prop max is set to', () => { + const maxValue = Math.floor(Math.random() * (99 - 20) + 22); + it(`should render content within ${maxValue}`, () => { + initializeComponent({ max: maxValue, content: 10 }); + + page().hasContent(10).makeSnapshot(); + }); + }); + + describe('when prop content is bigger than max value', () => { + const maxValue = Math.floor(Math.random() * 20); + it(`should render max value: ${maxValue}`, () => { + initializeComponent({ max: maxValue, content: 40 }); + + page().hasContent(maxValue).makeSnapshot(); + }); + }); + + describe('when prop content is set to string', () => { + it(`should render the passed string content `, () => { + initializeComponent({ content: 'test' }); + + page().hasContent('test').makeSnapshot(); + }); + }); + + describe('when prop content is set to number', () => { + it(`should render the passed number content `, () => { + initializeComponent({ content: 10 }); + + page().hasContent(10).makeSnapshot(); + }); + }); + + describe('when prop variant is set to dot', () => { + it(`should not render any content`, () => { + initializeComponent({ variant: SfBadgeVariant.dot, content: 10 }); + + page().hasNoContent().makeSnapshot(); + }); + }); +});