@talixo-ds/options-input 0.0.1
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/components/min-max-value-label.tsx +20 -0
- package/components/options-input-content-item.tsx +64 -0
- package/components/options-input-dropdown-item.tsx +148 -0
- package/components/tests/__snapshots__/min-max-value-label.spec.tsx.snap +17 -0
- package/components/tests/__snapshots__/options-input-content-item.spec.tsx.snap +138 -0
- package/components/tests/__snapshots__/options-input-dropdown-item.spec.tsx.snap +134 -0
- package/components/tests/min-max-value-label.spec.tsx +24 -0
- package/components/tests/options-input-content-item.spec.tsx +56 -0
- package/components/tests/options-input-dropdown-item.spec.tsx +84 -0
- package/index.ts +2 -0
- package/options-input.tsx +273 -0
- package/package.json +11 -0
- package/styles.scss +54 -0
- package/types.ts +14 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Typography from '@mui/material/Typography';
|
|
3
|
+
|
|
4
|
+
export type MinMaxValueLabelProps = {
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const MinMaxValueLabel = ({
|
|
11
|
+
min,
|
|
12
|
+
max,
|
|
13
|
+
color,
|
|
14
|
+
}: MinMaxValueLabelProps) => (
|
|
15
|
+
<Typography variant="caption" color={color} sx={{ my: 0 }}>
|
|
16
|
+
{!Number.isNaN(Number(min)) && `min: ${min}`}
|
|
17
|
+
{!Number.isNaN(Number(max)) && !Number.isNaN(Number(min)) && ', '}
|
|
18
|
+
{!Number.isNaN(Number(max)) && `max: ${max}`}
|
|
19
|
+
</Typography>
|
|
20
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import Typography from '@mui/material/Typography';
|
|
5
|
+
import * as MuiIcons from '@mui/icons-material';
|
|
6
|
+
import * as DesignSystemIcons from '@talixo-ds/component.icons';
|
|
7
|
+
import { MinMaxValueLabel } from './min-max-value-label';
|
|
8
|
+
import { OptionsInputOption } from '../types';
|
|
9
|
+
|
|
10
|
+
export type OptionsInputContentItemProps = {
|
|
11
|
+
item: OptionsInputOption;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
displayMinMax?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const OptionsInputContentItem = ({
|
|
17
|
+
item: { quantity, details, label, max, min, icon },
|
|
18
|
+
displayMinMax = false,
|
|
19
|
+
disabled = false,
|
|
20
|
+
}: OptionsInputContentItemProps) => {
|
|
21
|
+
const Icon = DesignSystemIcons[icon] || MuiIcons[icon];
|
|
22
|
+
const itemsColor = quantity === 0 || disabled ? '#a4a5b2' : '#000000';
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Box
|
|
26
|
+
display="flex"
|
|
27
|
+
alignItems="center"
|
|
28
|
+
gap={0.5}
|
|
29
|
+
color={itemsColor}
|
|
30
|
+
data-testid="option-item"
|
|
31
|
+
>
|
|
32
|
+
{label ? (
|
|
33
|
+
<Tooltip
|
|
34
|
+
title={
|
|
35
|
+
<Box display="flex" flexDirection="column">
|
|
36
|
+
<Typography variant="caption" fontWeight={600} sx={{ my: 0 }}>
|
|
37
|
+
{label}
|
|
38
|
+
</Typography>
|
|
39
|
+
{details && (
|
|
40
|
+
<Typography variant="caption" sx={{ my: 0 }}>
|
|
41
|
+
{details}
|
|
42
|
+
</Typography>
|
|
43
|
+
)}
|
|
44
|
+
{displayMinMax && <MinMaxValueLabel min={min} max={max} />}
|
|
45
|
+
</Box>
|
|
46
|
+
}
|
|
47
|
+
placement="top"
|
|
48
|
+
arrow
|
|
49
|
+
>
|
|
50
|
+
<span>
|
|
51
|
+
<Icon fontSize="medium" color={itemsColor} />
|
|
52
|
+
</span>
|
|
53
|
+
</Tooltip>
|
|
54
|
+
) : (
|
|
55
|
+
<Icon fontSize="medium" color={itemsColor} />
|
|
56
|
+
)}
|
|
57
|
+
<Typography variant="h6" color={itemsColor}>
|
|
58
|
+
{quantity}
|
|
59
|
+
</Typography>
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default OptionsInputContentItem;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import Typography from '@mui/material/Typography';
|
|
5
|
+
import ButtonGroup from '@mui/material/ButtonGroup';
|
|
6
|
+
import Divider from '@mui/material/Divider';
|
|
7
|
+
import TextField from '@mui/material/TextField';
|
|
8
|
+
import ListItem from '@mui/material/ListItem';
|
|
9
|
+
import Button from '@mui/material/Button';
|
|
10
|
+
import AddIcon from '@mui/icons-material/Add';
|
|
11
|
+
import RemoveIcon from '@mui/icons-material/Remove';
|
|
12
|
+
import * as MuiIcons from '@mui/icons-material';
|
|
13
|
+
import * as DesignSystemIcons from '@talixo-ds/component.icons';
|
|
14
|
+
import { MinMaxValueLabel } from './min-max-value-label';
|
|
15
|
+
import { OptionsInputOption } from '../types';
|
|
16
|
+
|
|
17
|
+
export type OptionsInputDropdownItemProps = {
|
|
18
|
+
item: OptionsInputOption;
|
|
19
|
+
onBlur: () => void;
|
|
20
|
+
onChange: (id: string, value: number | string) => void;
|
|
21
|
+
index: number;
|
|
22
|
+
displayMinMax?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const OptionsInputDropdownItem = ({
|
|
26
|
+
item: { id, quantity = 0, label, max, min, icon, details, inputQuantity },
|
|
27
|
+
onChange,
|
|
28
|
+
onBlur,
|
|
29
|
+
index,
|
|
30
|
+
displayMinMax,
|
|
31
|
+
}: OptionsInputDropdownItemProps) => {
|
|
32
|
+
const [shouldDisplayFullDetails, setShouldDisplayFullDetails] =
|
|
33
|
+
useState<boolean>(false);
|
|
34
|
+
const Icon = DesignSystemIcons[icon] || MuiIcons[icon];
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
{!!index && (
|
|
39
|
+
<Divider sx={{ color: (theme) => theme.palette.primary.main }} />
|
|
40
|
+
)}
|
|
41
|
+
<ListItem
|
|
42
|
+
sx={{
|
|
43
|
+
display: 'flex',
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
}}
|
|
46
|
+
className={classNames('options-input__dropdown-item', {
|
|
47
|
+
'options-input__dropdown-item--empty': !quantity,
|
|
48
|
+
})}
|
|
49
|
+
>
|
|
50
|
+
<Box display="flex" alignItems="center">
|
|
51
|
+
<Icon fontSize="small" color="black" />
|
|
52
|
+
<TextField
|
|
53
|
+
onChange={({ target }) => onChange(id, target.value)}
|
|
54
|
+
onBlur={onBlur}
|
|
55
|
+
value={inputQuantity}
|
|
56
|
+
variant="standard"
|
|
57
|
+
inputProps={{
|
|
58
|
+
inputMode: 'numeric',
|
|
59
|
+
pattern: '-?[0-9]*',
|
|
60
|
+
style: {
|
|
61
|
+
textAlign: 'center',
|
|
62
|
+
},
|
|
63
|
+
'data-testid': 'dropdown-item-input',
|
|
64
|
+
}}
|
|
65
|
+
// eslint-disable-next-line react/jsx-no-duplicate-props
|
|
66
|
+
InputProps={{ disableUnderline: true }}
|
|
67
|
+
className="options-input__dropdown-item-input"
|
|
68
|
+
/>
|
|
69
|
+
<Box
|
|
70
|
+
display="flex"
|
|
71
|
+
flexDirection="column"
|
|
72
|
+
justifyContent="center"
|
|
73
|
+
paddingRight={2}
|
|
74
|
+
paddingLeft={1}
|
|
75
|
+
minWidth="5rem"
|
|
76
|
+
>
|
|
77
|
+
<Typography
|
|
78
|
+
variant="caption"
|
|
79
|
+
fontWeight={600}
|
|
80
|
+
fontSize={13}
|
|
81
|
+
sx={{ my: 0 }}
|
|
82
|
+
color="black"
|
|
83
|
+
>
|
|
84
|
+
{label || id}
|
|
85
|
+
</Typography>
|
|
86
|
+
{details && (
|
|
87
|
+
<Box
|
|
88
|
+
position="relative"
|
|
89
|
+
height="1rem"
|
|
90
|
+
data-testid="option-details-container"
|
|
91
|
+
onMouseEnter={() => setShouldDisplayFullDetails(true)}
|
|
92
|
+
onMouseLeave={() => setShouldDisplayFullDetails(false)}
|
|
93
|
+
>
|
|
94
|
+
<Typography
|
|
95
|
+
variant="caption"
|
|
96
|
+
color="gray"
|
|
97
|
+
sx={{
|
|
98
|
+
my: 0,
|
|
99
|
+
zIndex: 10000,
|
|
100
|
+
position: 'fixed',
|
|
101
|
+
...(shouldDisplayFullDetails && {
|
|
102
|
+
backgroundColor: quantity ? '#ffffff' : '#eeeeee',
|
|
103
|
+
border: 'thin solid #d3d3d3',
|
|
104
|
+
}),
|
|
105
|
+
}}
|
|
106
|
+
data-testid="option-details"
|
|
107
|
+
>
|
|
108
|
+
{details?.length <= 15 || shouldDisplayFullDetails
|
|
109
|
+
? details
|
|
110
|
+
: `${details?.slice(0, 15)}...`}
|
|
111
|
+
</Typography>
|
|
112
|
+
</Box>
|
|
113
|
+
)}
|
|
114
|
+
{displayMinMax && (
|
|
115
|
+
<MinMaxValueLabel min={min} max={max} color="gray" />
|
|
116
|
+
)}
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
<ButtonGroup
|
|
120
|
+
variant="outlined"
|
|
121
|
+
size="small"
|
|
122
|
+
className="options-input__dropdown-item-buttons"
|
|
123
|
+
>
|
|
124
|
+
<Button
|
|
125
|
+
onClick={() => onChange(id, quantity + 1)}
|
|
126
|
+
disabled={quantity === max}
|
|
127
|
+
className="options-input__dropdown-item-button"
|
|
128
|
+
role="button"
|
|
129
|
+
color="primary"
|
|
130
|
+
>
|
|
131
|
+
<AddIcon sx={{ color: 'primary' }} />
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
onClick={() => onChange(id, quantity - 1)}
|
|
135
|
+
disabled={quantity === min}
|
|
136
|
+
className="options-input__dropdown-item-button"
|
|
137
|
+
role="button"
|
|
138
|
+
color="primary"
|
|
139
|
+
>
|
|
140
|
+
<RemoveIcon sx={{ color: 'primary' }} />
|
|
141
|
+
</Button>
|
|
142
|
+
</ButtonGroup>
|
|
143
|
+
</ListItem>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default OptionsInputDropdownItem;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`MinMaxValueLabel should match snapshot 1`] = `
|
|
4
|
+
<ForwardRef(Typography)
|
|
5
|
+
color="black"
|
|
6
|
+
sx={
|
|
7
|
+
{
|
|
8
|
+
"my": 0,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
variant="caption"
|
|
12
|
+
>
|
|
13
|
+
min: 0
|
|
14
|
+
,
|
|
15
|
+
max: 100
|
|
16
|
+
</ForwardRef(Typography)>
|
|
17
|
+
`;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`OptionsInputDropdownItem should match snapshot 1`] = `
|
|
4
|
+
<ForwardRef(Box)
|
|
5
|
+
alignItems="center"
|
|
6
|
+
color="#a4a5b2"
|
|
7
|
+
data-testid="option-item"
|
|
8
|
+
display="flex"
|
|
9
|
+
gap={0.5}
|
|
10
|
+
>
|
|
11
|
+
<ForwardRef(Tooltip)
|
|
12
|
+
arrow={true}
|
|
13
|
+
placement="top"
|
|
14
|
+
title={
|
|
15
|
+
<ForwardRef(Box)
|
|
16
|
+
display="flex"
|
|
17
|
+
flexDirection="column"
|
|
18
|
+
>
|
|
19
|
+
<ForwardRef(Typography)
|
|
20
|
+
fontWeight={600}
|
|
21
|
+
sx={
|
|
22
|
+
{
|
|
23
|
+
"my": 0,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
variant="caption"
|
|
27
|
+
>
|
|
28
|
+
luggage
|
|
29
|
+
</ForwardRef(Typography)>
|
|
30
|
+
<ForwardRef(Typography)
|
|
31
|
+
sx={
|
|
32
|
+
{
|
|
33
|
+
"my": 0,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
variant="caption"
|
|
37
|
+
>
|
|
38
|
+
This is your luggage
|
|
39
|
+
</ForwardRef(Typography)>
|
|
40
|
+
<MinMaxValueLabel
|
|
41
|
+
max={10}
|
|
42
|
+
min={-10}
|
|
43
|
+
/>
|
|
44
|
+
</ForwardRef(Box)>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<span>
|
|
48
|
+
<div
|
|
49
|
+
color="#a4a5b2"
|
|
50
|
+
fontSize="medium"
|
|
51
|
+
/>
|
|
52
|
+
</span>
|
|
53
|
+
</ForwardRef(Tooltip)>
|
|
54
|
+
<ForwardRef(Typography)
|
|
55
|
+
color="#a4a5b2"
|
|
56
|
+
variant="h6"
|
|
57
|
+
>
|
|
58
|
+
0
|
|
59
|
+
</ForwardRef(Typography)>
|
|
60
|
+
</ForwardRef(Box)>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
exports[`OptionsInputDropdownItem should not render tooltip when there is no label 1`] = `
|
|
64
|
+
<ForwardRef(Box)
|
|
65
|
+
alignItems="center"
|
|
66
|
+
color="#a4a5b2"
|
|
67
|
+
data-testid="option-item"
|
|
68
|
+
display="flex"
|
|
69
|
+
gap={0.5}
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
color="#a4a5b2"
|
|
73
|
+
fontSize="medium"
|
|
74
|
+
/>
|
|
75
|
+
<ForwardRef(Typography)
|
|
76
|
+
color="#a4a5b2"
|
|
77
|
+
variant="h6"
|
|
78
|
+
>
|
|
79
|
+
0
|
|
80
|
+
</ForwardRef(Typography)>
|
|
81
|
+
</ForwardRef(Box)>
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
exports[`OptionsInputDropdownItem should render proper alternative styling 1`] = `
|
|
85
|
+
<ForwardRef(Box)
|
|
86
|
+
alignItems="center"
|
|
87
|
+
color="#000000"
|
|
88
|
+
data-testid="option-item"
|
|
89
|
+
display="flex"
|
|
90
|
+
gap={0.5}
|
|
91
|
+
>
|
|
92
|
+
<ForwardRef(Tooltip)
|
|
93
|
+
arrow={true}
|
|
94
|
+
placement="top"
|
|
95
|
+
title={
|
|
96
|
+
<ForwardRef(Box)
|
|
97
|
+
display="flex"
|
|
98
|
+
flexDirection="column"
|
|
99
|
+
>
|
|
100
|
+
<ForwardRef(Typography)
|
|
101
|
+
fontWeight={600}
|
|
102
|
+
sx={
|
|
103
|
+
{
|
|
104
|
+
"my": 0,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
variant="caption"
|
|
108
|
+
>
|
|
109
|
+
luggage
|
|
110
|
+
</ForwardRef(Typography)>
|
|
111
|
+
<ForwardRef(Typography)
|
|
112
|
+
sx={
|
|
113
|
+
{
|
|
114
|
+
"my": 0,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
variant="caption"
|
|
118
|
+
>
|
|
119
|
+
This is your luggage
|
|
120
|
+
</ForwardRef(Typography)>
|
|
121
|
+
</ForwardRef(Box)>
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<span>
|
|
125
|
+
<div
|
|
126
|
+
color="#000000"
|
|
127
|
+
fontSize="medium"
|
|
128
|
+
/>
|
|
129
|
+
</span>
|
|
130
|
+
</ForwardRef(Tooltip)>
|
|
131
|
+
<ForwardRef(Typography)
|
|
132
|
+
color="#000000"
|
|
133
|
+
variant="h6"
|
|
134
|
+
>
|
|
135
|
+
1
|
|
136
|
+
</ForwardRef(Typography)>
|
|
137
|
+
</ForwardRef(Box)>
|
|
138
|
+
`;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`OptionsInputDropdownItem should match snapshot 1`] = `
|
|
4
|
+
<React.Fragment>
|
|
5
|
+
<ForwardRef(Divider)
|
|
6
|
+
sx={
|
|
7
|
+
{
|
|
8
|
+
"color": [Function],
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/>
|
|
12
|
+
<ForwardRef(ListItem)
|
|
13
|
+
className="options-input__dropdown-item options-input__dropdown-item--empty"
|
|
14
|
+
sx={
|
|
15
|
+
{
|
|
16
|
+
"display": "flex",
|
|
17
|
+
"justifyContent": "space-between",
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
>
|
|
21
|
+
<ForwardRef(Box)
|
|
22
|
+
alignItems="center"
|
|
23
|
+
display="flex"
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
color="black"
|
|
27
|
+
fontSize="small"
|
|
28
|
+
/>
|
|
29
|
+
<ForwardRef(TextField)
|
|
30
|
+
InputProps={
|
|
31
|
+
{
|
|
32
|
+
"disableUnderline": true,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
className="options-input__dropdown-item-input"
|
|
36
|
+
inputProps={
|
|
37
|
+
{
|
|
38
|
+
"data-testid": "dropdown-item-input",
|
|
39
|
+
"inputMode": "numeric",
|
|
40
|
+
"pattern": "-?[0-9]*",
|
|
41
|
+
"style": {
|
|
42
|
+
"textAlign": "center",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onBlur={[MockFunction]}
|
|
47
|
+
onChange={[Function]}
|
|
48
|
+
value={0}
|
|
49
|
+
variant="standard"
|
|
50
|
+
/>
|
|
51
|
+
<ForwardRef(Box)
|
|
52
|
+
display="flex"
|
|
53
|
+
flexDirection="column"
|
|
54
|
+
justifyContent="center"
|
|
55
|
+
minWidth="5rem"
|
|
56
|
+
paddingLeft={1}
|
|
57
|
+
paddingRight={2}
|
|
58
|
+
>
|
|
59
|
+
<ForwardRef(Typography)
|
|
60
|
+
color="black"
|
|
61
|
+
fontSize={13}
|
|
62
|
+
fontWeight={600}
|
|
63
|
+
sx={
|
|
64
|
+
{
|
|
65
|
+
"my": 0,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
variant="caption"
|
|
69
|
+
>
|
|
70
|
+
luggage-id
|
|
71
|
+
</ForwardRef(Typography)>
|
|
72
|
+
<ForwardRef(Box)
|
|
73
|
+
data-testid="option-details-container"
|
|
74
|
+
height="1rem"
|
|
75
|
+
onMouseEnter={[Function]}
|
|
76
|
+
onMouseLeave={[Function]}
|
|
77
|
+
position="relative"
|
|
78
|
+
>
|
|
79
|
+
<ForwardRef(Typography)
|
|
80
|
+
color="gray"
|
|
81
|
+
data-testid="option-details"
|
|
82
|
+
sx={
|
|
83
|
+
{
|
|
84
|
+
"my": 0,
|
|
85
|
+
"position": "fixed",
|
|
86
|
+
"zIndex": 10000,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
variant="caption"
|
|
90
|
+
>
|
|
91
|
+
This is your lu...
|
|
92
|
+
</ForwardRef(Typography)>
|
|
93
|
+
</ForwardRef(Box)>
|
|
94
|
+
</ForwardRef(Box)>
|
|
95
|
+
</ForwardRef(Box)>
|
|
96
|
+
<ForwardRef(ButtonGroup)
|
|
97
|
+
className="options-input__dropdown-item-buttons"
|
|
98
|
+
size="small"
|
|
99
|
+
variant="outlined"
|
|
100
|
+
>
|
|
101
|
+
<ForwardRef(Button)
|
|
102
|
+
className="options-input__dropdown-item-button"
|
|
103
|
+
color="primary"
|
|
104
|
+
disabled={false}
|
|
105
|
+
onClick={[Function]}
|
|
106
|
+
role="button"
|
|
107
|
+
>
|
|
108
|
+
<Memo
|
|
109
|
+
sx={
|
|
110
|
+
{
|
|
111
|
+
"color": "primary",
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
</ForwardRef(Button)>
|
|
116
|
+
<ForwardRef(Button)
|
|
117
|
+
className="options-input__dropdown-item-button"
|
|
118
|
+
color="primary"
|
|
119
|
+
disabled={false}
|
|
120
|
+
onClick={[Function]}
|
|
121
|
+
role="button"
|
|
122
|
+
>
|
|
123
|
+
<Memo
|
|
124
|
+
sx={
|
|
125
|
+
{
|
|
126
|
+
"color": "primary",
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
</ForwardRef(Button)>
|
|
131
|
+
</ForwardRef(ButtonGroup)>
|
|
132
|
+
</ForwardRef(ListItem)>
|
|
133
|
+
</React.Fragment>
|
|
134
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ShallowRenderer from 'react-test-renderer/shallow';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { MinMaxValueLabel } from '../min-max-value-label';
|
|
5
|
+
|
|
6
|
+
const props = {
|
|
7
|
+
min: 0,
|
|
8
|
+
max: 100,
|
|
9
|
+
color: 'black',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('MinMaxValueLabel', () => {
|
|
13
|
+
let component: JSX.Element;
|
|
14
|
+
const renderer = ShallowRenderer.createRenderer();
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
renderer.render(<MinMaxValueLabel {...props} />);
|
|
18
|
+
component = renderer.getRenderOutput();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should match snapshot', () => {
|
|
22
|
+
expect(component).toMatchSnapshot();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ShallowRenderer from 'react-test-renderer/shallow';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import OptionsInputContentItem from '../options-input-content-item';
|
|
5
|
+
|
|
6
|
+
jest.mock('@mui/icons-material', () => ({
|
|
7
|
+
luggage: 'div',
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock('@talixo-ds/component.icons', () => ({
|
|
11
|
+
football: 'div',
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const props = {
|
|
15
|
+
item: {
|
|
16
|
+
id: 'luggage',
|
|
17
|
+
icon: 'luggage',
|
|
18
|
+
label: 'luggage',
|
|
19
|
+
details: 'This is your luggage',
|
|
20
|
+
min: -10,
|
|
21
|
+
max: 10,
|
|
22
|
+
quantity: 0,
|
|
23
|
+
inputQuantity: 0,
|
|
24
|
+
},
|
|
25
|
+
displayMinMax: true,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('OptionsInputDropdownItem', () => {
|
|
29
|
+
let component: JSX.Element;
|
|
30
|
+
const renderer = ShallowRenderer.createRenderer();
|
|
31
|
+
|
|
32
|
+
it('should match snapshot', () => {
|
|
33
|
+
renderer.render(<OptionsInputContentItem {...props} />);
|
|
34
|
+
|
|
35
|
+
component = renderer.getRenderOutput();
|
|
36
|
+
expect(component).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should render proper alternative styling', () => {
|
|
40
|
+
renderer.render(
|
|
41
|
+
<OptionsInputContentItem item={{ ...props.item, quantity: 1 }} />
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
component = renderer.getRenderOutput();
|
|
45
|
+
expect(component).toMatchSnapshot();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should not render tooltip when there is no label', () => {
|
|
49
|
+
renderer.render(
|
|
50
|
+
<OptionsInputContentItem item={{ ...props.item, label: undefined }} />
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
component = renderer.getRenderOutput();
|
|
54
|
+
expect(component).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ShallowRenderer from 'react-test-renderer/shallow';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import OptionsInputDropdownItem from '../options-input-dropdown-item';
|
|
6
|
+
|
|
7
|
+
jest.mock('@mui/icons-material', () => ({
|
|
8
|
+
luggage: 'div',
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock('@talixo-ds/component.icons', () => ({
|
|
12
|
+
football: 'div',
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const onChangeMock = jest.fn();
|
|
16
|
+
const onBlurMock = jest.fn();
|
|
17
|
+
|
|
18
|
+
const props = {
|
|
19
|
+
item: {
|
|
20
|
+
id: 'luggage-id',
|
|
21
|
+
icon: 'luggage',
|
|
22
|
+
details:
|
|
23
|
+
'This is your luggage. Please, keep your attention not to lose it.',
|
|
24
|
+
min: -10,
|
|
25
|
+
max: 10,
|
|
26
|
+
quantity: 0,
|
|
27
|
+
inputQuantity: 0,
|
|
28
|
+
},
|
|
29
|
+
onBlur: onBlurMock,
|
|
30
|
+
onChange: onChangeMock,
|
|
31
|
+
index: 1,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('OptionsInputDropdownItem', () => {
|
|
35
|
+
let component: JSX.Element;
|
|
36
|
+
const renderer = ShallowRenderer.createRenderer();
|
|
37
|
+
|
|
38
|
+
it('should match snapshot', () => {
|
|
39
|
+
renderer.render(<OptionsInputDropdownItem {...props} />);
|
|
40
|
+
component = renderer.getRenderOutput();
|
|
41
|
+
|
|
42
|
+
expect(component).toMatchSnapshot();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('events', () => {
|
|
46
|
+
beforeEach(() => render(<OptionsInputDropdownItem {...props} />));
|
|
47
|
+
|
|
48
|
+
it('should call onChange on text field edit', async () => {
|
|
49
|
+
const input = screen.getByTestId('dropdown-item-input');
|
|
50
|
+
|
|
51
|
+
await fireEvent.change(input, { target: { value: '10' } });
|
|
52
|
+
expect(onChangeMock).toHaveBeenCalledWith('luggage-id', '10');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should call onChange on increment button click', () => {
|
|
56
|
+
const buttons = screen.getAllByRole('button');
|
|
57
|
+
|
|
58
|
+
fireEvent.click(buttons[0]);
|
|
59
|
+
expect(onChangeMock).toHaveBeenCalledWith('luggage-id', 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should call onChange on decrement button click', () => {
|
|
63
|
+
const buttons = screen.getAllByRole('button');
|
|
64
|
+
|
|
65
|
+
fireEvent.click(buttons[1]);
|
|
66
|
+
expect(onChangeMock).toHaveBeenCalledWith('luggage-id', -1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should expand details content', () => {
|
|
70
|
+
expect(screen.getByTestId('option-details')).toHaveTextContent(
|
|
71
|
+
props.item.details.slice(0, 15)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const optionDetailsContainer = screen.getByTestId(
|
|
75
|
+
'option-details-container'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
fireEvent.mouseEnter(optionDetailsContainer);
|
|
79
|
+
expect(screen.getByTestId('option-details')).toHaveTextContent(
|
|
80
|
+
props.item.details
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
package/index.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import List from '@mui/material/List';
|
|
5
|
+
import Popper from '@mui/material/Popper';
|
|
6
|
+
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
|
7
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
8
|
+
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
9
|
+
import * as MuiIcons from '@mui/icons-material';
|
|
10
|
+
import * as DesignSystemIcons from '@talixo-ds/component.icons';
|
|
11
|
+
import type { SxProps } from '@mui/material';
|
|
12
|
+
import { OptionsInputOption, OptionsInputValue } from './types';
|
|
13
|
+
import OptionsInputContentItem from './components/options-input-content-item';
|
|
14
|
+
import OptionsInputDropdownItem from './components/options-input-dropdown-item';
|
|
15
|
+
import './styles.scss';
|
|
16
|
+
|
|
17
|
+
import '@emotion/react';
|
|
18
|
+
import '@emotion/styled';
|
|
19
|
+
|
|
20
|
+
export type OptionsInputProps = {
|
|
21
|
+
/** Array of objects representing options available to choose from */
|
|
22
|
+
options: OptionsInputOption[];
|
|
23
|
+
/** Object with default values of some options */
|
|
24
|
+
defaultValue?: OptionsInputValue;
|
|
25
|
+
/** Array with ids of options which should remain displayed even if value is 0 */
|
|
26
|
+
persistentOptions?: string[];
|
|
27
|
+
/** Boolean to determine if input is disabled */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Boolean to determine if input is readOnly */
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
/** Boolean to determine if min and max input values should be displayed */
|
|
32
|
+
displayMinMax?: boolean;
|
|
33
|
+
/** Function which handles options input value change */
|
|
34
|
+
onChange?: (OptionsInputValue) => void;
|
|
35
|
+
/** Function which handles options input focus event */
|
|
36
|
+
onFocus?: (OptionsInputValue) => void;
|
|
37
|
+
/** Function which handles options input blur event */
|
|
38
|
+
onBlur?: (OptionsInputValue) => void;
|
|
39
|
+
/** className attached to an input container */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** id attached to an input container */
|
|
42
|
+
id?: string;
|
|
43
|
+
/** data test id attached to an input container */
|
|
44
|
+
'data-testid'?: string;
|
|
45
|
+
/** Gap between input items */
|
|
46
|
+
itemsGap?: string | number;
|
|
47
|
+
/** Custom styles for container */
|
|
48
|
+
containerSx?: SxProps;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const OptionsInput = ({
|
|
52
|
+
options,
|
|
53
|
+
onChange,
|
|
54
|
+
onFocus,
|
|
55
|
+
onBlur,
|
|
56
|
+
persistentOptions = [],
|
|
57
|
+
defaultValue,
|
|
58
|
+
displayMinMax = false,
|
|
59
|
+
disabled = false,
|
|
60
|
+
readOnly = false,
|
|
61
|
+
id,
|
|
62
|
+
className,
|
|
63
|
+
itemsGap = 1,
|
|
64
|
+
containerSx = [],
|
|
65
|
+
...rest
|
|
66
|
+
}: OptionsInputProps) => {
|
|
67
|
+
const [currentOptions, setCurrentOptions] = useState<OptionsInputOption[]>(
|
|
68
|
+
[]
|
|
69
|
+
);
|
|
70
|
+
const [anchorEl, setAnchorEl] = useState<undefined | HTMLElement>();
|
|
71
|
+
const open = !!anchorEl;
|
|
72
|
+
|
|
73
|
+
useEffect(
|
|
74
|
+
() =>
|
|
75
|
+
setCurrentOptions(
|
|
76
|
+
options.map((option) => {
|
|
77
|
+
const defaultQuantity = defaultValue?.[option.id] ?? 0;
|
|
78
|
+
const minQuantity =
|
|
79
|
+
defaultQuantity < option?.min ? option?.min : defaultQuantity;
|
|
80
|
+
const quantity =
|
|
81
|
+
minQuantity > option?.max ? option?.max : minQuantity;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...option,
|
|
85
|
+
quantity,
|
|
86
|
+
inputQuantity: quantity,
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
),
|
|
90
|
+
[options, defaultValue]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const toggleInput = (event: React.MouseEvent<HTMLElement>) => {
|
|
94
|
+
const { currentTarget } = event;
|
|
95
|
+
|
|
96
|
+
if (!disabled && !readOnly) {
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
setAnchorEl(anchorEl ? undefined : currentTarget);
|
|
99
|
+
}, 0);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onInputFocus = () => {
|
|
104
|
+
if (onFocus) {
|
|
105
|
+
onFocus(
|
|
106
|
+
currentOptions.reduce(
|
|
107
|
+
(currentValues, currentOption) => ({
|
|
108
|
+
...currentValues,
|
|
109
|
+
[currentOption.id]: currentOption.quantity,
|
|
110
|
+
}),
|
|
111
|
+
{}
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onInputBlur = () => {
|
|
118
|
+
if (onBlur) {
|
|
119
|
+
onBlur(
|
|
120
|
+
currentOptions.reduce(
|
|
121
|
+
(currentValues, currentOption) => ({
|
|
122
|
+
...currentValues,
|
|
123
|
+
[currentOption.id]: currentOption.quantity,
|
|
124
|
+
}),
|
|
125
|
+
{}
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onValueChange = (optionId: string, newValue: string | number) => {
|
|
132
|
+
const newQuantity = Number.isNaN(Number(newValue)) ? 0 : Number(newValue);
|
|
133
|
+
|
|
134
|
+
const newCurrentOptions = currentOptions.map((option) => {
|
|
135
|
+
const maxQuantity =
|
|
136
|
+
newQuantity > (option.max || Infinity) ? option.max : newQuantity;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...option,
|
|
140
|
+
...(optionId === option.id && {
|
|
141
|
+
quantity:
|
|
142
|
+
newQuantity < (option.min || -Infinity) ? option.min : maxQuantity,
|
|
143
|
+
inputQuantity: newValue,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (onChange) {
|
|
149
|
+
onChange(
|
|
150
|
+
newCurrentOptions.reduce(
|
|
151
|
+
(currentValues, currentOption) => ({
|
|
152
|
+
...currentValues,
|
|
153
|
+
[currentOption.id]: currentOption.quantity,
|
|
154
|
+
}),
|
|
155
|
+
{}
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setCurrentOptions(newCurrentOptions);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const onDropdownItemBlur = (optionId: string) => () =>
|
|
164
|
+
setCurrentOptions(
|
|
165
|
+
currentOptions.map((option) => {
|
|
166
|
+
if (optionId !== option.id) return option;
|
|
167
|
+
|
|
168
|
+
const finalQuantity = Number.isNaN(Number(option.inputQuantity))
|
|
169
|
+
? 0
|
|
170
|
+
: Number(option.inputQuantity);
|
|
171
|
+
const maxQuantity =
|
|
172
|
+
finalQuantity > (option.max || Infinity) ? option.max : finalQuantity;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...option,
|
|
176
|
+
inputQuantity:
|
|
177
|
+
finalQuantity < (option.min || -Infinity)
|
|
178
|
+
? option.min
|
|
179
|
+
: maxQuantity,
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<>
|
|
186
|
+
<Box
|
|
187
|
+
id={id}
|
|
188
|
+
onClick={toggleInput}
|
|
189
|
+
onBlur={onInputBlur}
|
|
190
|
+
onFocus={onInputFocus}
|
|
191
|
+
className={classNames('options-input__container', {
|
|
192
|
+
'options-input__container--open': open,
|
|
193
|
+
'options-input__container--disabled': disabled,
|
|
194
|
+
'options-input__container--read-only': readOnly,
|
|
195
|
+
[className]: !!className,
|
|
196
|
+
})}
|
|
197
|
+
sx={[
|
|
198
|
+
{ '&:hover': { borderColor: '#d3d3d3' } },
|
|
199
|
+
...(Array.isArray(containerSx) ? containerSx : [containerSx]),
|
|
200
|
+
open && {
|
|
201
|
+
borderColor: (theme) => theme.palette.primary.main,
|
|
202
|
+
'&:hover': { borderColor: (theme) => theme.palette.primary.main },
|
|
203
|
+
},
|
|
204
|
+
]}
|
|
205
|
+
data-testid={rest['data-testid'] || 'options-input-container'}
|
|
206
|
+
tabIndex={0}
|
|
207
|
+
>
|
|
208
|
+
<Box display="flex" gap={itemsGap}>
|
|
209
|
+
{currentOptions
|
|
210
|
+
.filter(
|
|
211
|
+
({ quantity, id: optionId, icon }) =>
|
|
212
|
+
!!(MuiIcons?.[icon] || DesignSystemIcons?.[icon]) &&
|
|
213
|
+
(quantity !== 0 || persistentOptions.includes(optionId))
|
|
214
|
+
)
|
|
215
|
+
.map((option) => (
|
|
216
|
+
<OptionsInputContentItem
|
|
217
|
+
key={option.id}
|
|
218
|
+
item={option}
|
|
219
|
+
disabled={disabled}
|
|
220
|
+
displayMinMax={displayMinMax}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
</Box>
|
|
224
|
+
{!readOnly &&
|
|
225
|
+
(open ? (
|
|
226
|
+
<KeyboardArrowUpIcon color="primary" />
|
|
227
|
+
) : (
|
|
228
|
+
<KeyboardArrowDownIcon
|
|
229
|
+
sx={{
|
|
230
|
+
color: (theme) =>
|
|
231
|
+
disabled
|
|
232
|
+
? theme.palette.grey[400]
|
|
233
|
+
: theme.palette.action.focus,
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</Box>
|
|
238
|
+
<ClickAwayListener onClickAway={() => open && setAnchorEl(undefined)}>
|
|
239
|
+
<Popper
|
|
240
|
+
open={open}
|
|
241
|
+
placement="bottom-start"
|
|
242
|
+
anchorEl={anchorEl}
|
|
243
|
+
sx={(theme) => ({ zIndex: theme.zIndex.modal })}
|
|
244
|
+
>
|
|
245
|
+
<List
|
|
246
|
+
disablePadding
|
|
247
|
+
data-testid="options-dropdown-list"
|
|
248
|
+
className="options-input__dropdown-items-list"
|
|
249
|
+
sx={{
|
|
250
|
+
bgcolor: 'Background',
|
|
251
|
+
border: (theme) => `thin solid ${theme.palette.primary.main}`,
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
{currentOptions
|
|
255
|
+
.filter(
|
|
256
|
+
({ icon }) => !!(MuiIcons?.[icon] || DesignSystemIcons?.[icon])
|
|
257
|
+
)
|
|
258
|
+
.map((option, index) => (
|
|
259
|
+
<OptionsInputDropdownItem
|
|
260
|
+
key={option.id}
|
|
261
|
+
item={option}
|
|
262
|
+
onBlur={onDropdownItemBlur(option.id)}
|
|
263
|
+
onChange={onValueChange}
|
|
264
|
+
index={index}
|
|
265
|
+
displayMinMax={displayMinMax}
|
|
266
|
+
/>
|
|
267
|
+
))}
|
|
268
|
+
</List>
|
|
269
|
+
</Popper>
|
|
270
|
+
</ClickAwayListener>
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
};
|
package/package.json
ADDED
package/styles.scss
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.options-input {
|
|
2
|
+
&__container {
|
|
3
|
+
border: thin solid transparent;
|
|
4
|
+
border-radius: 0.25rem;
|
|
5
|
+
display: flex;
|
|
6
|
+
justify-content: space-between;
|
|
7
|
+
align-items: center;
|
|
8
|
+
padding: 0.25rem;
|
|
9
|
+
gap: 1rem;
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
width: fit-content;
|
|
12
|
+
|
|
13
|
+
&--open {
|
|
14
|
+
border-bottom-width: 0;
|
|
15
|
+
border-bottom-left-radius: 0;
|
|
16
|
+
border-bottom-right-radius: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&--disabled {
|
|
20
|
+
cursor: unset;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&--read-only {
|
|
24
|
+
cursor: unset;
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
border-color: transparent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&__dropdown-item {
|
|
33
|
+
gap: 1rem;
|
|
34
|
+
|
|
35
|
+
&--empty {
|
|
36
|
+
background-color: #eeeeee;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&__dropdown-item-input {
|
|
41
|
+
width: 2rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&__dropdown-item-buttons {
|
|
45
|
+
& > button[role="button"] {
|
|
46
|
+
min-width: 2rem;
|
|
47
|
+
padding: 0;
|
|
48
|
+
|
|
49
|
+
&[disabled] > svg {
|
|
50
|
+
color: #0000001f;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface OptionsInputOption {
|
|
2
|
+
id: string;
|
|
3
|
+
icon: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
details?: string;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
quantity?: number;
|
|
9
|
+
inputQuantity?: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OptionsInputValue {
|
|
13
|
+
[key: string]: number;
|
|
14
|
+
}
|