agroptima-design-system 0.25.2 → 0.25.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -8,9 +8,17 @@ export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
8
8
  name: IconType
9
9
  className?: string
10
10
  title?: string
11
+ visible?: boolean
11
12
  }
12
13
 
13
- export const Icon: React.FC<IconProps> = ({ name, className, ...props }) => {
14
+ export const Icon: React.FC<IconProps> = ({
15
+ name,
16
+ className,
17
+ visible = true,
18
+ ...props
19
+ }) => {
20
+ if (!visible) return null
21
+
14
22
  const cssClasses = classNames('icon', className, {
15
23
  rotate: name === 'Loading',
16
24
  })
@@ -1,6 +1,7 @@
1
- import './Multiselect.scss'
1
+ import './Select.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { IconButton } from './Button'
4
5
  import { classNames } from '../utils/classNames'
5
6
  import { buildHelpText } from '../utils/buildHelpText'
6
7
 
@@ -46,9 +47,10 @@ export function Multiselect({
46
47
  const helpTexts = buildHelpText(helpText, errors)
47
48
  const [showOptionsList, setShowOptionsList] = useState(false)
48
49
  const [selectedOptions, setSelectedOptions] = useState<string[]>(defaultValue)
50
+ const hasSelectedOptions = selectedOptions.length > 0
49
51
  const cssClasses = classNames('selected-option', className, {
50
52
  open: showOptionsList,
51
- filled: selectedOptions.length > 0,
53
+ filled: hasSelectedOptions,
52
54
  disabled: disabled,
53
55
  invalid: errors?.length,
54
56
  })
@@ -74,10 +76,16 @@ export function Multiselect({
74
76
  }
75
77
  }
76
78
 
79
+ function handleClear(event: React.MouseEvent<HTMLButtonElement>) {
80
+ event.stopPropagation()
81
+ setSelectedOptions([])
82
+ if (onChange !== undefined) onChange([])
83
+ }
84
+
77
85
  return (
78
- <div className={`multiselect-group ${variant}`}>
79
- {!hideLabel && <span className="multiselect-label">{label}</span>}
80
- <div className="multiselect-container" onBlur={handleBlur}>
86
+ <div className={`select-group ${variant}`}>
87
+ {!hideLabel && <span className="select-label">{label}</span>}
88
+ <div className="select-container" onBlur={handleBlur}>
81
89
  <div
82
90
  className={cssClasses}
83
91
  tabIndex={0}
@@ -87,14 +95,24 @@ export function Multiselect({
87
95
  role="alert"
88
96
  >
89
97
  <span>
90
- {selectedOptions.length > 0
98
+ {hasSelectedOptions
91
99
  ? `${selectedOptions.length} ${selectedLabel}`
92
100
  : placeholder}
93
101
  </span>
94
- <Icon name={showOptionsList ? 'AngleUp' : 'AngleDown'} />
102
+ <Icon
103
+ name={showOptionsList ? 'AngleUp' : 'AngleDown'}
104
+ visible={!hasSelectedOptions}
105
+ />
106
+ <IconButton
107
+ icon="Close"
108
+ className="clear-button"
109
+ accessibilityLabel="close"
110
+ onClick={handleClear}
111
+ visible={hasSelectedOptions}
112
+ />
95
113
  </div>
96
114
  {showOptionsList && (
97
- <ul className="multiselect-options" role="listbox">
115
+ <ul className="select-options" role="listbox">
98
116
  {options.map((option) => (
99
117
  <Option
100
118
  key={`${name}-${option.id}`}
@@ -107,7 +125,7 @@ export function Multiselect({
107
125
  )}
108
126
  </div>
109
127
  {helpTexts.map((helpText) => (
110
- <span key={`${name}-${helpText}`} className="multiselect-help-text">
128
+ <span key={`${name}-${helpText}`} className="select-help-text">
111
129
  {helpText}
112
130
  </span>
113
131
  ))}
@@ -33,7 +33,7 @@
33
33
  margin: 0;
34
34
  }
35
35
  input[type='number'] {
36
- -moz-appearance: textfield;
36
+ appearance: textfield;
37
37
  text-align: center;
38
38
  }
39
39
  }
@@ -14,6 +14,11 @@
14
14
  }
15
15
  }
16
16
 
17
+ .clear-button > .icon {
18
+ width: config.$icon-size-3x;
19
+ height: config.$icon-size-3x;
20
+ }
21
+
17
22
  &.primary {
18
23
  .select-label {
19
24
  @include typography.form-label;
@@ -82,6 +87,20 @@
82
87
  &:hover {
83
88
  background-color: color_alias.$primary-color-50;
84
89
  }
90
+ > .icon {
91
+ > svg {
92
+ border-radius: config.$corner-radius-xxs;
93
+ .checkbox-active_svg__border {
94
+ fill: color_alias.$primary-color-600;
95
+ }
96
+ .checkbox-active_svg__background {
97
+ fill: color_alias.$primary-color-600;
98
+ }
99
+ .checkbox-inactive_svg__border {
100
+ fill: color_alias.$neutral-color-600;
101
+ }
102
+ }
103
+ }
85
104
  }
86
105
  }
87
106
 
@@ -106,14 +125,11 @@
106
125
  > .icon {
107
126
  width: config.$icon-size-3x;
108
127
  height: config.$icon-size-3x;
109
- > svg {
110
- width: 100%;
111
- height: 100%;
112
- }
113
128
  }
114
129
  }
115
130
 
116
131
  .select-options {
132
+ z-index: depth.$z-dropdown-options;
117
133
  margin: 0;
118
134
  padding: config.$space-1x 0rem;
119
135
  text-align: left;
@@ -121,9 +137,17 @@
121
137
  width: 100%;
122
138
 
123
139
  .option {
140
+ display: flex;
141
+ align-items: center;
124
142
  cursor: default;
125
143
  list-style-type: none;
126
144
  padding: config.$space-2x config.$space-3x;
145
+
146
+ > .icon {
147
+ width: config.$icon-size-4x;
148
+ height: config.$icon-size-4x;
149
+ margin-right: config.$space-1x;
150
+ }
127
151
  }
128
152
  }
129
153
  }
@@ -1,6 +1,7 @@
1
1
  import './Select.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { IconButton } from './Button'
4
5
  import { classNames } from '../utils/classNames'
5
6
  import { buildHelpText } from '../utils/buildHelpText'
6
7
 
@@ -47,6 +48,7 @@ export function Select({
47
48
  const defaultOption =
48
49
  options.find((option) => option.id === defaultValue) || EMPTY_OPTION
49
50
  const [selectedOption, setSelectedOption] = useState<Option>(defaultOption)
51
+ const isEmpty = selectedOption.id === EMPTY_OPTION.id
50
52
 
51
53
  const cssClasses = classNames('selected-option', {
52
54
  open: showOptionsList,
@@ -73,6 +75,12 @@ export function Select({
73
75
  }
74
76
  }
75
77
 
78
+ function handleClear(event: React.MouseEvent<HTMLButtonElement>) {
79
+ event.stopPropagation()
80
+ setSelectedOption(EMPTY_OPTION)
81
+ if (onChange !== undefined) onChange('')
82
+ }
83
+
76
84
  return (
77
85
  <div className={classNames('select-group', variant, className)}>
78
86
  {!hideLabel && <span className="select-label">{label}</span>}
@@ -86,7 +94,17 @@ export function Select({
86
94
  role="alert"
87
95
  >
88
96
  <span>{selectedOption.label || placeholder}</span>
89
- <Icon name={showOptionsList ? 'AngleUp' : 'AngleDown'} />
97
+ <Icon
98
+ name={showOptionsList ? 'AngleUp' : 'AngleDown'}
99
+ visible={isEmpty}
100
+ />
101
+ <IconButton
102
+ icon="Close"
103
+ className="clear-button"
104
+ accessibilityLabel="close"
105
+ onClick={handleClear}
106
+ visible={!isEmpty}
107
+ />
90
108
  </div>
91
109
  {showOptionsList && (
92
110
  <ul className="select-options" role="listbox">
@@ -4,6 +4,16 @@ import { Meta } from "@storybook/blocks";
4
4
 
5
5
  # Changelog
6
6
 
7
+ # 0.25.4
8
+
9
+ * Call onChange after clearing Select and Multiselect components
10
+
11
+
12
+ # 0.25.3
13
+
14
+ * Add clear button to Select commponent
15
+ * Add clear button to Multiselect component
16
+
7
17
  # 0.25.2
8
18
 
9
19
  * Add onChange prop to Multiselect component
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
4
  import { Multiselect } from '@/atoms/Multiselect'
5
+ import { Placeholder } from 'storybook/internal/components'
5
6
 
6
7
  describe('Multiselect', () => {
7
8
  it('renders', async () => {
@@ -32,7 +33,7 @@ describe('Multiselect', () => {
32
33
  />,
33
34
  )
34
35
 
35
- expect(getAllByRole('generic')[1]).toHaveClass('multiselect-group primary')
36
+ expect(getAllByRole('generic')[1]).toHaveClass('select-group primary')
36
37
  expect(getByText('Videogames')).toBeInTheDocument()
37
38
  expect(getByText(/Select your favourite videogames.../)).toBeInTheDocument()
38
39
  expect(getByText(/This text can help you/i)).toBeInTheDocument()
@@ -111,4 +112,40 @@ describe('Multiselect', () => {
111
112
  expect(getByText(/error1/i)).toBeInTheDocument()
112
113
  expect(getByText(/error2/i)).toBeInTheDocument()
113
114
  })
115
+ it('clears options selected', async () => {
116
+ const mockChange = jest.fn()
117
+ const user = userEvent.setup()
118
+ const placeholder = 'Select your favourite videogames...'
119
+ const { getByText } = render(
120
+ <Multiselect
121
+ helpText="This text can help you"
122
+ label="Videogames"
123
+ name="videogames"
124
+ options={[
125
+ {
126
+ id: '1',
127
+ label: 'The Legend of Zelda: Ocarina of Time',
128
+ },
129
+ {
130
+ id: '2',
131
+ label: 'Spyro the Dragon',
132
+ },
133
+ {
134
+ id: '3',
135
+ label: 'Halo',
136
+ },
137
+ ]}
138
+ placeholder={placeholder}
139
+ defaultValue={['2', '1']}
140
+ selectedLabel="videogames selected"
141
+ onChange={mockChange}
142
+ variant="primary"
143
+ />,
144
+ )
145
+
146
+ await user.click(screen.getByRole('button', { name: /close/i }))
147
+
148
+ expect(getByText(placeholder)).toBeInTheDocument()
149
+ expect(mockChange).toHaveBeenCalledWith([])
150
+ })
114
151
  })
@@ -118,4 +118,40 @@ describe('Select', () => {
118
118
  expect(getByText(/error1/i)).toBeInTheDocument()
119
119
  expect(getByText(/error2/i)).toBeInTheDocument()
120
120
  })
121
+ it('clears option selected', async () => {
122
+ const mockChange = jest.fn()
123
+ const user = userEvent.setup()
124
+ const placeholder = 'Select your favourite gaming system...'
125
+ const { getByText } = render(
126
+ <Select
127
+ defaultValue="2"
128
+ helpText="This text can help you"
129
+ id="select-videogames"
130
+ label="Videogames"
131
+ name="example"
132
+ options={[
133
+ {
134
+ id: '1',
135
+ label: 'Nintendo Switch',
136
+ },
137
+ {
138
+ id: '2',
139
+ label: 'PlayStation 5',
140
+ },
141
+ {
142
+ id: '3',
143
+ label: 'Xbox Series S/X',
144
+ },
145
+ ]}
146
+ placeholder={placeholder}
147
+ onChange={mockChange}
148
+ variant="primary"
149
+ />,
150
+ )
151
+
152
+ await user.click(screen.getByRole('button', { name: /close/i }))
153
+
154
+ expect(getByText(placeholder)).toBeInTheDocument()
155
+ expect(mockChange).toHaveBeenCalledWith('')
156
+ })
121
157
  })
@@ -1,157 +0,0 @@
1
- @use '../settings/color_alias';
2
- @use '../settings/typography/form' as typography;
3
- @use '../settings/config';
4
- @use '../settings/depth';
5
-
6
- .multiselect-group {
7
- display: flex;
8
- flex-direction: column;
9
- gap: config.$space-2x;
10
-
11
- &:has(.selected-option.invalid) {
12
- & .multiselect-help-text {
13
- @include typography.form-help-text-error;
14
- }
15
- }
16
-
17
- &.primary {
18
- .multiselect-label {
19
- @include typography.form-label;
20
- }
21
-
22
- .selected-option {
23
- border-radius: config.$corner-radius-m;
24
- border: 1px solid color_alias.$neutral-color-600;
25
- background: color_alias.$neutral-white;
26
- @include typography.select-text;
27
-
28
- > .icon {
29
- > svg {
30
- fill: color_alias.$primary-color-600;
31
- path {
32
- fill: color_alias.$primary-color-600;
33
- }
34
- }
35
- }
36
-
37
- &.filled {
38
- @include typography.select-option-text;
39
- }
40
-
41
- &:focus {
42
- outline: color_alias.$primary-color-600;
43
- border: 1px solid color_alias.$primary-color-600;
44
- }
45
-
46
- &.invalid {
47
- border: 1px solid color_alias.$error-color-1000;
48
- }
49
-
50
- &.disabled {
51
- border: 1px solid color_alias.$neutral-color-400;
52
- background: color_alias.$neutral-color-50;
53
- color: color_alias.$neutral-color-400;
54
-
55
- > .icon {
56
- > svg {
57
- fill: color_alias.$neutral-color-400;
58
- path {
59
- fill: color_alias.$neutral-color-400;
60
- }
61
- }
62
- }
63
- }
64
- }
65
-
66
- .multiselect-options {
67
- max-height: 256px;
68
- overflow-y: auto;
69
- overflow-anchor: none;
70
- z-index: depth.$z-dropdown-options;
71
- border-radius: config.$corner-radius-xxs;
72
- background: color_alias.$neutral-white;
73
- box-shadow:
74
- 0px 9px 28px 8px rgba(0, 0, 0, 0.05),
75
- 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
76
- 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
77
-
78
- .option {
79
- background: color_alias.$neutral-white;
80
- @include typography.select-option-text;
81
-
82
- &:hover {
83
- background-color: color_alias.$primary-color-50;
84
- }
85
-
86
- > .icon {
87
- > svg {
88
- border-radius: config.$corner-radius-xxs;
89
- .checkbox-active_svg__border {
90
- fill: color_alias.$primary-color-600;
91
- }
92
- .checkbox-active_svg__background {
93
- fill: color_alias.$primary-color-600;
94
- }
95
- .checkbox-inactive_svg__border {
96
- fill: color_alias.$neutral-color-600;
97
- }
98
- }
99
- }
100
- }
101
- }
102
-
103
- .multiselect-help-text {
104
- @include typography.form-help-text;
105
- }
106
- }
107
-
108
- .multiselect-container {
109
- display: inline-block;
110
- text-align: left;
111
- position: relative;
112
- }
113
-
114
- .selected-option {
115
- display: flex;
116
- justify-content: space-between;
117
- align-items: center;
118
- padding: config.$space-2x config.$space-3x;
119
- cursor: default;
120
-
121
- > .icon {
122
- width: config.$icon-size-3x;
123
- height: config.$icon-size-3x;
124
- > svg {
125
- width: 100%;
126
- height: 100%;
127
- }
128
- }
129
- }
130
-
131
- .multiselect-options {
132
- z-index: depth.$z-dropdown-options;
133
- margin: 0;
134
- padding: config.$space-1x 0rem;
135
- text-align: left;
136
- position: absolute;
137
- width: 100%;
138
-
139
- .option {
140
- display: flex;
141
- align-items: center;
142
- cursor: default;
143
- list-style-type: none;
144
- padding: config.$space-2x config.$space-3x;
145
-
146
- > .icon {
147
- width: config.$icon-size-4x;
148
- height: config.$icon-size-4x;
149
- margin-right: config.$space-1x;
150
- > svg {
151
- width: 100%;
152
- height: 100%;
153
- }
154
- }
155
- }
156
- }
157
- }