agroptima-design-system 0.25.2 → 0.25.3

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.3",
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,15 @@ export function Multiselect({
74
76
  }
75
77
  }
76
78
 
79
+ function handleClear(event: React.MouseEvent<HTMLButtonElement>) {
80
+ event.stopPropagation()
81
+ setSelectedOptions([])
82
+ }
83
+
77
84
  return (
78
- <div className={`multiselect-group ${variant}`}>
79
- {!hideLabel && <span className="multiselect-label">{label}</span>}
80
- <div className="multiselect-container" onBlur={handleBlur}>
85
+ <div className={`select-group ${variant}`}>
86
+ {!hideLabel && <span className="select-label">{label}</span>}
87
+ <div className="select-container" onBlur={handleBlur}>
81
88
  <div
82
89
  className={cssClasses}
83
90
  tabIndex={0}
@@ -87,14 +94,24 @@ export function Multiselect({
87
94
  role="alert"
88
95
  >
89
96
  <span>
90
- {selectedOptions.length > 0
97
+ {hasSelectedOptions
91
98
  ? `${selectedOptions.length} ${selectedLabel}`
92
99
  : placeholder}
93
100
  </span>
94
- <Icon name={showOptionsList ? 'AngleUp' : 'AngleDown'} />
101
+ <Icon
102
+ name={showOptionsList ? 'AngleUp' : 'AngleDown'}
103
+ visible={!hasSelectedOptions}
104
+ />
105
+ <IconButton
106
+ icon="Close"
107
+ className="clear-button"
108
+ accessibilityLabel="close"
109
+ onClick={handleClear}
110
+ visible={hasSelectedOptions}
111
+ />
95
112
  </div>
96
113
  {showOptionsList && (
97
- <ul className="multiselect-options" role="listbox">
114
+ <ul className="select-options" role="listbox">
98
115
  {options.map((option) => (
99
116
  <Option
100
117
  key={`${name}-${option.id}`}
@@ -107,7 +124,7 @@ export function Multiselect({
107
124
  )}
108
125
  </div>
109
126
  {helpTexts.map((helpText) => (
110
- <span key={`${name}-${helpText}`} className="multiselect-help-text">
127
+ <span key={`${name}-${helpText}`} className="select-help-text">
111
128
  {helpText}
112
129
  </span>
113
130
  ))}
@@ -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,11 @@ export function Select({
73
75
  }
74
76
  }
75
77
 
78
+ function handleClear(event: React.MouseEvent<HTMLButtonElement>) {
79
+ event.stopPropagation()
80
+ setSelectedOption(EMPTY_OPTION)
81
+ }
82
+
76
83
  return (
77
84
  <div className={classNames('select-group', variant, className)}>
78
85
  {!hideLabel && <span className="select-label">{label}</span>}
@@ -86,7 +93,17 @@ export function Select({
86
93
  role="alert"
87
94
  >
88
95
  <span>{selectedOption.label || placeholder}</span>
89
- <Icon name={showOptionsList ? 'AngleUp' : 'AngleDown'} />
96
+ <Icon
97
+ name={showOptionsList ? 'AngleUp' : 'AngleDown'}
98
+ visible={isEmpty}
99
+ />
100
+ <IconButton
101
+ icon="Close"
102
+ className="clear-button"
103
+ accessibilityLabel="close"
104
+ onClick={handleClear}
105
+ visible={!isEmpty}
106
+ />
90
107
  </div>
91
108
  {showOptionsList && (
92
109
  <ul className="select-options" role="listbox">
@@ -4,6 +4,11 @@ import { Meta } from "@storybook/blocks";
4
4
 
5
5
  # Changelog
6
6
 
7
+ # 0.25.3
8
+
9
+ * Add clear button to Select commponent
10
+ * Add clear button to Multiselect component
11
+
7
12
  # 0.25.2
8
13
 
9
14
  * 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,37 @@ 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 user = userEvent.setup()
117
+ const placeholder = 'Select your favourite videogames...'
118
+ const { getByText } = render(
119
+ <Multiselect
120
+ helpText="This text can help you"
121
+ label="Videogames"
122
+ name="videogames"
123
+ options={[
124
+ {
125
+ id: '1',
126
+ label: 'The Legend of Zelda: Ocarina of Time',
127
+ },
128
+ {
129
+ id: '2',
130
+ label: 'Spyro the Dragon',
131
+ },
132
+ {
133
+ id: '3',
134
+ label: 'Halo',
135
+ },
136
+ ]}
137
+ placeholder={placeholder}
138
+ defaultValue={['2', '1']}
139
+ selectedLabel="videogames selected"
140
+ variant="primary"
141
+ />,
142
+ )
143
+
144
+ await user.click(screen.getByRole('button', { name: /close/i }))
145
+
146
+ expect(getByText(placeholder)).toBeInTheDocument()
147
+ })
114
148
  })
@@ -118,4 +118,37 @@ 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 user = userEvent.setup()
123
+ const placeholder = 'Select your favourite gaming system...'
124
+ const { getByText } = render(
125
+ <Select
126
+ defaultValue="2"
127
+ helpText="This text can help you"
128
+ id="select-videogames"
129
+ label="Videogames"
130
+ name="example"
131
+ options={[
132
+ {
133
+ id: '1',
134
+ label: 'Nintendo Switch',
135
+ },
136
+ {
137
+ id: '2',
138
+ label: 'PlayStation 5',
139
+ },
140
+ {
141
+ id: '3',
142
+ label: 'Xbox Series S/X',
143
+ },
144
+ ]}
145
+ placeholder={placeholder}
146
+ variant="primary"
147
+ />,
148
+ )
149
+
150
+ await user.click(screen.getByRole('button', { name: /close/i }))
151
+
152
+ expect(getByText(placeholder)).toBeInTheDocument()
153
+ })
121
154
  })
@@ -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
- }