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 +1 -1
- package/src/atoms/Icon.tsx +9 -1
- package/src/atoms/Multiselect.tsx +26 -9
- package/src/atoms/QuantitySelector.scss +1 -1
- package/src/atoms/Select.scss +28 -4
- package/src/atoms/Select.tsx +18 -1
- package/src/stories/Changelog.mdx +5 -0
- package/tests/Multiselect.spec.tsx +35 -1
- package/tests/Select.spec.tsx +33 -0
- package/src/atoms/Multiselect.scss +0 -157
package/package.json
CHANGED
package/src/atoms/Icon.tsx
CHANGED
|
@@ -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> = ({
|
|
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 './
|
|
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:
|
|
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={`
|
|
79
|
-
{!hideLabel && <span className="
|
|
80
|
-
<div className="
|
|
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
|
-
{
|
|
97
|
+
{hasSelectedOptions
|
|
91
98
|
? `${selectedOptions.length} ${selectedLabel}`
|
|
92
99
|
: placeholder}
|
|
93
100
|
</span>
|
|
94
|
-
<Icon
|
|
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="
|
|
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="
|
|
127
|
+
<span key={`${name}-${helpText}`} className="select-help-text">
|
|
111
128
|
{helpText}
|
|
112
129
|
</span>
|
|
113
130
|
))}
|
package/src/atoms/Select.scss
CHANGED
|
@@ -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
|
}
|
package/src/atoms/Select.tsx
CHANGED
|
@@ -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
|
|
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">
|
|
@@ -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('
|
|
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
|
})
|
package/tests/Select.spec.tsx
CHANGED
|
@@ -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
|
-
}
|