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