agroptima-design-system 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,6 +52,13 @@ const config: StorybookConfig = {
52
52
  imageRule['exclude'] = /\.svg$/i
53
53
  }
54
54
 
55
+ if (config.resolve) {
56
+ config.resolve.alias = {
57
+ ...config.resolve.alias,
58
+ '@': resolve(__dirname, '../src'),
59
+ }
60
+ }
61
+
55
62
  return config
56
63
  },
57
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -1,6 +1,7 @@
1
1
  import { IconButton, IconButtonProps } from './IconButton'
2
2
  import { Icon } from './Icon'
3
3
  import './Alert.scss'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export type Variant = 'info' | 'success' | 'warning' | 'error'
6
7
 
@@ -22,14 +23,15 @@ export enum IconVariant {
22
23
  export function Alert({
23
24
  id,
24
25
  variant = 'success',
25
- className = '',
26
+ className,
26
27
  fitContent = false,
27
28
  text,
28
29
  button,
29
30
  ...props
30
31
  }: AlertProps): React.JSX.Element {
31
- const fitContentClass = fitContent ? 'fit-content' : ''
32
- const cssClasses = ['alert', variant, className, fitContentClass].join(' ')
32
+ const cssClasses = classNames('alert', variant, className, {
33
+ 'fit-content': fitContent,
34
+ })
33
35
 
34
36
  return (
35
37
  <div
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './Badge.scss'
2
3
 
3
4
  export type Variant =
@@ -18,11 +19,12 @@ export interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
18
19
 
19
20
  export function Badge({
20
21
  variant = 'info',
22
+ className,
21
23
  text,
22
24
  accessibilityLabel,
23
25
  ...props
24
26
  }: BadgeProps): React.JSX.Element {
25
- const cssClasses = ['badge', variant].join(' ')
27
+ const cssClasses = classNames('badge', variant, className)
26
28
 
27
29
  return (
28
30
  <span
@@ -1,6 +1,7 @@
1
1
  import NextLink from 'next/link'
2
2
  import './Button.scss'
3
3
  import { Icon, IconType } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export interface BaseButtonProps {
6
7
  label: string
@@ -56,15 +57,15 @@ export function Button({
56
57
  if (loading) {
57
58
  leftIcon = 'Loading'
58
59
  }
59
- const cssClasses = ['button', variant].join(' ')
60
+ const cssClasses = classNames('button', variant, props.className)
60
61
 
61
62
  if (hasHref(props)) {
62
63
  return (
63
64
  <NextLink
64
65
  href={props.href || ''}
65
- className={cssClasses}
66
66
  aria-label={accessibilityLabel || label}
67
67
  {...props}
68
+ className={cssClasses}
68
69
  >
69
70
  {leftIcon && <Icon name={leftIcon} />}
70
71
  {label}
@@ -75,10 +76,10 @@ export function Button({
75
76
 
76
77
  return (
77
78
  <button
78
- className={cssClasses}
79
79
  disabled={loading || disabled}
80
80
  aria-label={accessibilityLabel || label}
81
81
  {...props}
82
+ className={cssClasses}
82
83
  >
83
84
  {leftIcon && <Icon name={leftIcon} />}
84
85
  {label}
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './CardsTable.scss'
2
3
 
3
4
  export type Variant = 'primary'
@@ -8,13 +9,13 @@ export interface CardsTableProps
8
9
  }
9
10
 
10
11
  export function CardsTable({
12
+ className,
11
13
  summary,
12
14
  variant = 'primary',
13
15
  children,
14
16
  ...props
15
17
  }: CardsTableProps): React.JSX.Element {
16
- const cssClasses = ['cards-table-list', variant].join(' ')
17
-
18
+ const cssClasses = classNames('cards-table-list', variant, className)
18
19
  return (
19
20
  <table summary={summary} role="table" className={cssClasses} {...props}>
20
21
  {children}
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './CardsTable.scss'
2
3
  import React from 'react'
3
4
 
@@ -20,12 +21,9 @@ export function CardsTableCell({
20
21
  className,
21
22
  ...props
22
23
  }: CardsTableCellProps): React.JSX.Element {
23
- const cssClasses = [
24
- 'cell',
25
- noWrap ? 'no-wrap' : '',
26
- `alignment-${align}`,
27
- className,
28
- ].join(' ')
24
+ const cssClasses = classNames('cell', `alignment-${align}`, className, {
25
+ 'no-wrap': noWrap,
26
+ })
29
27
  return (
30
28
  <td role="cell" className={cssClasses} {...props}>
31
29
  {children}
@@ -1,3 +1,5 @@
1
+ import { classNames } from '@/utils/classNames'
2
+
1
3
  export interface CardsTableHeaderProps
2
4
  extends React.ComponentPropsWithoutRef<'th'> {}
3
5
 
@@ -6,7 +8,7 @@ export function CardsTableHeader({
6
8
  className,
7
9
  ...props
8
10
  }: CardsTableHeaderProps) {
9
- const cssClasses = ['header', className].join(' ')
11
+ const cssClasses = classNames('header', className)
10
12
  return (
11
13
  <th scope="col" role="columnheader" className={cssClasses} {...props}>
12
14
  {children}
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './Checkbox.scss'
2
3
 
3
4
  export type Variant = 'primary'
@@ -17,26 +18,28 @@ export function Checkbox({
17
18
  disabled,
18
19
  variant = 'primary',
19
20
  id,
21
+ name,
20
22
  ...props
21
23
  }: CheckboxProps) {
22
- const disabledClass = disabled ? 'disabled' : ''
23
- const cssClasses = ['checkbox', variant].join(' ')
24
+ const identifier = id || name
25
+ const inputCss = classNames('checkbox', variant)
26
+ const labelCss = classNames('checkbox-label-container', {
27
+ disabled: disabled,
28
+ })
24
29
 
25
30
  return (
26
31
  <div className={`checkbox-group ${variant}`}>
27
32
  <input
28
- id={id}
33
+ id={identifier}
34
+ name={name}
29
35
  type="checkbox"
30
- className={cssClasses}
36
+ className={inputCss}
31
37
  disabled={disabled}
32
38
  aria-label={accessibilityLabel}
33
39
  {...props}
34
40
  />
35
41
 
36
- <label
37
- className={`checkbox-label-container ${disabledClass}`}
38
- htmlFor={id}
39
- >
42
+ <label className={labelCss} htmlFor={identifier}>
40
43
  <span className="background-icon"></span>
41
44
  {!hideLabel && <span className="label">{label}</span>}
42
45
  </label>
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import { Button, ButtonProps } from './Button'
2
3
  import './EmptyState.scss'
3
4
  import { Icon, IconType } from './Icon'
@@ -12,12 +13,13 @@ export interface EmptyStateProps extends React.ComponentPropsWithoutRef<'div'> {
12
13
  }
13
14
 
14
15
  export function EmptyState({
16
+ className,
15
17
  icon = 'EmptyState',
16
18
  text = 'No data',
17
19
  variant = 'primary',
18
20
  button,
19
21
  }: EmptyStateProps): React.JSX.Element {
20
- const cssClasses = ['empty-state', variant].join(' ')
22
+ const cssClasses = classNames('empty-state', variant, className)
21
23
 
22
24
  return (
23
25
  <div className={cssClasses}>
@@ -1,6 +1,7 @@
1
1
  import './Icon.scss'
2
2
 
3
3
  import * as icons from '../icons'
4
+ import { classNames } from '@/utils/classNames'
4
5
  export type IconType = keyof typeof icons
5
6
 
6
7
  export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
@@ -9,11 +10,9 @@ export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
9
10
  }
10
11
 
11
12
  export const Icon: React.FC<IconProps> = ({ name, className, ...props }) => {
12
- const cssClasses = [
13
- 'icon',
14
- className,
15
- name === 'Loading' ? 'rotate' : '',
16
- ].join(' ')
13
+ const cssClasses = classNames('icon', className, {
14
+ rotate: name === 'Loading',
15
+ })
17
16
  return (
18
17
  <span role="img" title={name} className={cssClasses}>
19
18
  {icons[name](props)}
@@ -1,6 +1,7 @@
1
1
  import NextLink from 'next/link'
2
2
  import './IconButton.scss'
3
3
  import { Icon, IconType } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export type Variant = 'primary'
6
7
 
@@ -29,15 +30,15 @@ export function IconButton({
29
30
  variant = 'primary',
30
31
  ...props
31
32
  }: IconButtonProps) {
32
- const cssClasses = ['icon-button', variant].join(' ')
33
+ const cssClasses = classNames('icon-button', variant, props.className)
33
34
 
34
35
  if (hasHref(props)) {
35
36
  return (
36
37
  <NextLink
37
38
  href={props.href || ''}
38
- className={cssClasses}
39
39
  aria-label={accessibilityLabel}
40
40
  {...props}
41
+ className={cssClasses}
41
42
  >
42
43
  <Icon name={icon} />
43
44
  </NextLink>
@@ -46,10 +47,10 @@ export function IconButton({
46
47
 
47
48
  return (
48
49
  <button
49
- className={cssClasses}
50
50
  disabled={disabled}
51
51
  aria-label={accessibilityLabel}
52
52
  {...props}
53
+ className={cssClasses}
53
54
  >
54
55
  <Icon name={icon} />
55
56
  </button>
@@ -1,6 +1,8 @@
1
- import './Input.scss'
2
1
  import React, { useState } from 'react'
3
2
  import { Icon, IconType } from './Icon'
3
+ import { classNames } from '@/utils/classNames'
4
+ import './Input.scss'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type InputVariant = 'primary'
6
8
 
@@ -18,6 +20,7 @@ export interface InputProps extends React.ComponentPropsWithoutRef<'input'> {
18
20
  export function Input({
19
21
  label,
20
22
  accessibilityLabel,
23
+ className,
21
24
  hideLabel = false,
22
25
  icon,
23
26
  helpText,
@@ -29,10 +32,13 @@ export function Input({
29
32
  errors,
30
33
  ...props
31
34
  }: InputProps): React.JSX.Element {
35
+ const identifier = id || name
32
36
  const [showPassword, setShowPassword] = useState(false)
33
- const iconClass = icon ? 'with-icon' : ''
34
- const invalidClass = errors ? 'invalid' : ''
35
- const cssClasses = ['input', iconClass, invalidClass].join(' ')
37
+ const cssClasses = classNames('input', className, {
38
+ 'with-icon': icon,
39
+ invalid: errors?.length,
40
+ })
41
+ const helpTexts = buildHelpText(helpText, errors)
36
42
 
37
43
  function handlePasswordIcon() {
38
44
  return showPassword ? 'ShowOff' : 'Show'
@@ -51,14 +57,14 @@ export function Input({
51
57
  return (
52
58
  <div className={`input-group ${variant}`}>
53
59
  {!hideLabel && (
54
- <label className="input-label" htmlFor={id}>
60
+ <label className="input-label" htmlFor={identifier}>
55
61
  {label}
56
62
  </label>
57
63
  )}
58
64
  <div className="input-container">
59
65
  {icon && <Icon className="left-icon" name={icon} />}
60
66
  <input
61
- id={id}
67
+ id={identifier}
62
68
  className={cssClasses}
63
69
  disabled={disabled}
64
70
  type={handleInputType()}
@@ -74,17 +80,11 @@ export function Input({
74
80
  />
75
81
  )}
76
82
  </div>
77
- {helpText && !errors && (
78
- <span className="input-help-text">{helpText}</span>
79
- )}
80
- {errors &&
81
- errors?.map((error, index) => {
82
- return (
83
- <span key={`error-${index}`} className="input-help-text">
84
- {error}
85
- </span>
86
- )
87
- })}
83
+ {helpTexts.map((helpText) => (
84
+ <span key={`${identifier}-${helpText}`} className="input-help-text">
85
+ {helpText}
86
+ </span>
87
+ ))}
88
88
  </div>
89
89
  )
90
90
  }
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import { Button, ButtonProps } from './Button'
2
3
  import { Icon } from './Icon'
3
4
  import './Modal.scss'
@@ -21,13 +22,14 @@ export interface ModalProps extends React.ComponentPropsWithoutRef<'div'> {
21
22
 
22
23
  export function Modal({
23
24
  id,
25
+ className,
24
26
  variant = 'info',
25
27
  title,
26
28
  buttons,
27
29
  children,
28
30
  ...props
29
31
  }: ModalProps): React.JSX.Element {
30
- const cssClasses = ['modal', variant].join(' ')
32
+ const cssClasses = classNames('modal', variant, className)
31
33
 
32
34
  return (
33
35
  <div className="modal-container">
@@ -1,6 +1,8 @@
1
1
  import './Multiselect.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type Variant = 'primary'
6
8
  export type Option = { id: string; label: string }
@@ -20,6 +22,7 @@ export interface MultiselectProps
20
22
  }
21
23
 
22
24
  export function Multiselect({
25
+ className,
23
26
  placeholder,
24
27
  helpText,
25
28
  variant = 'primary',
@@ -34,23 +37,17 @@ export function Multiselect({
34
37
  selected,
35
38
  ...props
36
39
  }: MultiselectProps): React.JSX.Element {
40
+ const helpTexts = buildHelpText(helpText, errors)
37
41
  const [showOptionsList, setShowOptionsList] = useState(false)
38
42
  const [selectedOptionsIds, setSelectedOptionsIds] = useState<string[]>(
39
43
  selected?.map((option) => option.id) || [],
40
44
  )
41
-
42
- const optionsListOpenClass = showOptionsList ? 'open' : ''
43
- const filledSelectClass = selectedOptionsIds.length > 0 ? 'filled' : ''
44
- const disabledClass = disabled ? 'disabled' : ''
45
- const invalidClass = errors ? 'invalid' : ''
46
-
47
- const cssClasses = [
48
- 'selected-option',
49
- optionsListOpenClass,
50
- filledSelectClass,
51
- disabledClass,
52
- invalidClass,
53
- ].join(' ')
45
+ const cssClasses = classNames('selected-option', className, {
46
+ open: showOptionsList,
47
+ filled: selectedOptionsIds.length > 0,
48
+ disabled: disabled,
49
+ invalid: errors?.length,
50
+ })
54
51
 
55
52
  function handleOptionsList() {
56
53
  if (!disabled) setShowOptionsList(!showOptionsList)
@@ -130,19 +127,11 @@ export function Multiselect({
130
127
  </ul>
131
128
  )}
132
129
  </div>
133
- {helpText && !errors && (
134
- <span className={`multiselect-help-text ${invalidClass}`}>
130
+ {helpTexts.map((helpText) => (
131
+ <span key={`${name}-${helpText}`} className="multiselect-help-text">
135
132
  {helpText}
136
133
  </span>
137
- )}
138
- {errors &&
139
- errors?.map((error, index) => {
140
- return (
141
- <span key={`error-${index}`} className="multiselect-help-text">
142
- {error}
143
- </span>
144
- )
145
- })}
134
+ ))}
146
135
  <input
147
136
  type="hidden"
148
137
  name={name}
@@ -1,6 +1,8 @@
1
1
  import './Select.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type Variant = 'primary'
6
8
  export type Option = { id: string; label: string }
@@ -23,6 +25,7 @@ export interface SelectProps extends InputPropsWithoutOnChange {
23
25
  }
24
26
 
25
27
  export function Select({
28
+ className,
26
29
  placeholder,
27
30
  helpText,
28
31
  variant = 'primary',
@@ -37,24 +40,19 @@ export function Select({
37
40
  onChange,
38
41
  ...props
39
42
  }: SelectProps): React.JSX.Element {
43
+ const helpTexts = buildHelpText(helpText, errors)
40
44
  const [showOptionsList, setShowOptionsList] = useState(false)
41
45
  const [selectedOption, setSelectedOption] = useState<Option>({
42
46
  id: selected?.id || '',
43
47
  label: selected?.label || '',
44
48
  })
45
49
 
46
- const optionsListOpenClass = showOptionsList ? 'open' : ''
47
- const filledSelectClass = selectedOption.id ? 'filled' : ''
48
- const disabledClass = disabled ? 'disabled' : ''
49
- const invalidClass = errors ? 'invalid' : ''
50
-
51
- const cssClasses = [
52
- 'selected-option',
53
- optionsListOpenClass,
54
- filledSelectClass,
55
- disabledClass,
56
- invalidClass,
57
- ].join(' ')
50
+ const cssClasses = classNames('selected-option', className, {
51
+ open: showOptionsList,
52
+ filled: selectedOption.id,
53
+ disabled: disabled,
54
+ invalid: errors?.length,
55
+ })
58
56
 
59
57
  function handleOptionsList() {
60
58
  if (!disabled) setShowOptionsList(!showOptionsList)
@@ -113,17 +111,11 @@ export function Select({
113
111
  </ul>
114
112
  )}
115
113
  </div>
116
- {helpText && !errors && (
117
- <span className="select-help-text">{helpText}</span>
118
- )}
119
- {errors &&
120
- errors?.map((error, index) => {
121
- return (
122
- <span key={`error-${index}`} className="select-help-text">
123
- {error}
124
- </span>
125
- )
126
- })}
114
+ {helpTexts.map((helpText) => (
115
+ <span key={`${name}-${helpText}`} className="select-help-text">
116
+ {helpText}
117
+ </span>
118
+ ))}
127
119
  <input type="hidden" name={name} value={selectedOption.id} {...props} />
128
120
  </div>
129
121
  )
@@ -0,0 +1,58 @@
1
+ import { classNames } from '@/utils/classNames'
2
+ import { buildHelpText } from '@/utils/buildHelpText'
3
+ import './Input.scss'
4
+
5
+ export type TextAreaVariant = 'primary'
6
+
7
+ export interface TextAreaProps
8
+ extends React.ComponentPropsWithoutRef<'textarea'> {
9
+ label: string
10
+ accessibilityLabel?: string
11
+ hideLabel?: boolean
12
+ helpText?: string
13
+ variant?: TextAreaVariant
14
+ id?: string
15
+ errors?: string[]
16
+ }
17
+
18
+ export default function TextArea({
19
+ id,
20
+ label,
21
+ className,
22
+ accessibilityLabel,
23
+ hideLabel = false,
24
+ helpText,
25
+ variant = 'primary',
26
+ disabled,
27
+ name,
28
+ errors,
29
+ ...props
30
+ }: TextAreaProps) {
31
+ const identifier = id || name
32
+ const cssClasses = classNames('input', className, { invalid: errors?.length })
33
+ const helpTexts = buildHelpText(helpText, errors)
34
+ return (
35
+ <div className={`input-group ${variant}`}>
36
+ {!hideLabel && (
37
+ <label className="input-label" htmlFor={identifier}>
38
+ {label}
39
+ </label>
40
+ )}
41
+ <div className="input-container">
42
+ <textarea
43
+ id={identifier}
44
+ className={cssClasses}
45
+ disabled={disabled}
46
+ name={name}
47
+ aria-label={accessibilityLabel || label}
48
+ {...props}
49
+ />
50
+ </div>
51
+ {helpTexts.map((helpText) => (
52
+ <span key={`${identifier}-${helpText}`} className="input-help-text">
53
+ {helpText}
54
+ </span>
55
+ ))}
56
+ </div>
57
+ )
58
+ }
@@ -3,6 +3,10 @@ import { Meta } from "@storybook/addon-docs";
3
3
  <Meta title="Changelog" />
4
4
  # Changelog
5
5
 
6
+ ## 0.11.0
7
+ - Added TextArea component to Storybook.
8
+ - Id prop is now optional in Input, Select, Multiselect and TextArea components.
9
+
6
10
  ## 0.10.0
7
11
 
8
12
  - Cards Table components now are responsive.
@@ -31,7 +31,7 @@ const meta = {
31
31
  type: {
32
32
  description: 'Input type property',
33
33
  },
34
- leftIcon: {
34
+ icon: {
35
35
  description: 'Input left icon from a list of values',
36
36
  },
37
37
  id: {
@@ -0,0 +1,78 @@
1
+ import TextArea from '../atoms/TextArea'
2
+ import { StoryObj } from '@storybook/react'
3
+
4
+ const meta = {
5
+ title: 'Design System/Atoms/Textarea',
6
+ component: TextArea,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ label: {
10
+ description: 'Label for the textarea',
11
+ },
12
+ accessibilityLabel: {
13
+ description:
14
+ 'Describes the textarea purpose. If empty, label content will be used',
15
+ },
16
+ placeholder: {
17
+ description: 'Optional textarea placeholder text',
18
+ },
19
+ variant: {
20
+ description: 'Textarea variant used',
21
+ },
22
+ disabled: {
23
+ description: 'Is the textarea in disabled state?',
24
+ },
25
+ helpText: {
26
+ description: 'Optional help text',
27
+ },
28
+ name: {
29
+ description: 'Textarea name property',
30
+ },
31
+ id: {
32
+ description: 'Value needed for the label relation',
33
+ },
34
+ errors: {
35
+ description:
36
+ 'Optional array of errors. If passed, the errors are listed and invalid style is applied.',
37
+ },
38
+ },
39
+ }
40
+
41
+ const figmaPrimaryDesign = {
42
+ design: {
43
+ type: 'figma',
44
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=2371-2157&mode=dev',
45
+ },
46
+ }
47
+
48
+ export default meta
49
+ type Story = StoryObj<typeof meta>
50
+
51
+ export const Primary: Story = {
52
+ args: {
53
+ label: 'Textarea',
54
+ accessibilityLabel: 'Fill the textarea',
55
+ placeholder: 'Write here...',
56
+ variant: 'primary',
57
+ disabled: false,
58
+ helpText: 'This text can help you',
59
+ name: 'textarea',
60
+ id: 'textarea',
61
+ },
62
+ parameters: figmaPrimaryDesign,
63
+ }
64
+
65
+ export const WithErrors: Story = {
66
+ args: {
67
+ label: 'Textarea',
68
+ accessibilityLabel: 'Fill the form textarea',
69
+ placeholder: 'Write here...',
70
+ variant: 'primary',
71
+ disabled: false,
72
+ helpText: 'This text can help you',
73
+ name: 'textarea',
74
+ id: 'textarea',
75
+ errors: ['error1', 'error2'],
76
+ },
77
+ parameters: figmaPrimaryDesign,
78
+ }
@@ -0,0 +1,12 @@
1
+ export function buildHelpText(
2
+ helpText: string | undefined,
3
+ errors: string[] | undefined,
4
+ ): string[] {
5
+ if (Boolean(errors?.length)) {
6
+ return errors || []
7
+ }
8
+ if (helpText) {
9
+ return [helpText]
10
+ }
11
+ return []
12
+ }
@@ -0,0 +1,23 @@
1
+ type classNameProp = string | { [key: string]: any } | undefined
2
+
3
+ export const classNames = (...classNames: classNameProp[]): string => {
4
+ const resultClasses: string[] = []
5
+
6
+ classNames.forEach((className) => {
7
+ if (className === undefined) {
8
+ return
9
+ }
10
+
11
+ if (typeof className === 'string') {
12
+ return resultClasses.push(className)
13
+ }
14
+
15
+ Object.keys(className).forEach((key) => {
16
+ if (Boolean(className[key])) {
17
+ resultClasses.push(key)
18
+ }
19
+ })
20
+ })
21
+
22
+ return resultClasses.join(' ')
23
+ }
@@ -0,0 +1,13 @@
1
+ import { classNames } from '@/utils/classNames'
2
+
3
+ describe('classNames', () => {
4
+ it('returns a string of classes', () => {
5
+ expect(classNames('class1', 'class2')).toBe('class1 class2')
6
+ })
7
+ it('ignores undefined', () => {
8
+ expect(classNames('class1', undefined, 'class2')).toBe('class1 class2')
9
+ })
10
+ it('returns a string of classes with object', () => {
11
+ expect(classNames({ class1: true, class2: false })).toBe('class1')
12
+ })
13
+ })