@talixo-ds/options-input 1.0.6 → 1.0.7

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.
Files changed (40) hide show
  1. package/dist/index.css +2 -0
  2. package/dist/index.css.map +1 -0
  3. package/dist/{options-input.d.ts → index.d.mts} +22 -8
  4. package/dist/index.d.ts +57 -2
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +32 -7
  10. package/dist/components/min-max-value-label.d.ts +0 -7
  11. package/dist/components/min-max-value-label.js +0 -4
  12. package/dist/components/min-max-value-label.js.map +0 -1
  13. package/dist/components/options-input-content-item.d.ts +0 -9
  14. package/dist/components/options-input-content-item.js +0 -19
  15. package/dist/components/options-input-content-item.js.map +0 -1
  16. package/dist/components/options-input-dropdown-item.d.ts +0 -12
  17. package/dist/components/options-input-dropdown-item.js +0 -54
  18. package/dist/components/options-input-dropdown-item.js.map +0 -1
  19. package/dist/options-input.js +0 -140
  20. package/dist/options-input.js.map +0 -1
  21. package/dist/types.d.ts +0 -13
  22. package/dist/types.js +0 -2
  23. package/dist/types.js.map +0 -1
  24. package/dist/utils.d.ts +0 -1
  25. package/dist/utils.js +0 -2
  26. package/dist/utils.js.map +0 -1
  27. package/src/__tests__/options-input.spec.tsx +0 -163
  28. package/src/__tests__/utils.spec.ts +0 -12
  29. package/src/components/__tests__/min-max-value-label.spec.tsx +0 -24
  30. package/src/components/__tests__/options-input-content-item.spec.tsx +0 -57
  31. package/src/components/__tests__/options-input-dropdown-item.spec.tsx +0 -126
  32. package/src/components/min-max-value-label.tsx +0 -19
  33. package/src/components/options-input-content-item.tsx +0 -72
  34. package/src/components/options-input-dropdown-item.tsx +0 -161
  35. package/src/index.ts +0 -2
  36. package/src/options-input.tsx +0 -353
  37. package/src/styles.scss +0 -54
  38. package/src/types.ts +0 -14
  39. package/src/utils.ts +0 -2
  40. package/tsconfig.build.json +0 -8
@@ -1,161 +0,0 @@
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 DesignSystemIcons from "@talixo-ds/icons";
13
- import { MinMaxValueLabel } from "./min-max-value-label";
14
- import { capitalize } from "../utils";
15
- import type { OptionsInputOption } from "../types";
16
-
17
- export type OptionsInputDropdownItemProps = {
18
- item: OptionsInputOption;
19
- onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;
20
- onChange: (id: string, value: number | string) => void;
21
- index: number;
22
- displayMinMax?: boolean;
23
- error?: boolean;
24
- };
25
-
26
- const OptionsInputDropdownItem = ({
27
- item: { id, quantity = 0, label, max, min, icon, details, inputQuantity },
28
- onChange,
29
- onBlur,
30
- index,
31
- displayMinMax,
32
- error = false
33
- }: OptionsInputDropdownItemProps) => {
34
- const [shouldDisplayFullDetails, setShouldDisplayFullDetails] =
35
- useState<boolean>(false);
36
- const Icon =
37
- DesignSystemIcons[capitalize(icon) as keyof typeof DesignSystemIcons] || null;
38
-
39
- const onIncrement = (inputId: string) => () => {
40
- const isValueBelowMin = !!(min && quantity < min);
41
-
42
- onChange(inputId, (isValueBelowMin ? min : quantity + 1)!);
43
- };
44
-
45
- const onDecrement = (inputId: string) => () => {
46
- const isValueAboveMax = !!(max && quantity > max);
47
-
48
- return onChange(inputId, (isValueAboveMax ? max : quantity - 1)!);
49
- };
50
-
51
- return (
52
- <>
53
- {!!index && (
54
- <Divider sx={{ color: (theme) => theme.palette.primary.main }} />
55
- )}
56
- <ListItem
57
- sx={{
58
- display: "flex",
59
- justifyContent: "space-between"
60
- }}
61
- className={classNames("options-input__dropdown-item", {
62
- "options-input__dropdown-item--empty": !quantity
63
- })}
64
- >
65
- <Box display="flex" alignItems="center">
66
- <Icon fontSize="small" sx={{ color: "black" }} />
67
- <TextField
68
- onChange={({ target }) => onChange(id, target.value)}
69
- onBlur={onBlur}
70
- value={inputQuantity}
71
- variant="standard"
72
- inputProps={{
73
- inputMode: "numeric",
74
- pattern: "-?[0-9]*",
75
- style: {
76
- textAlign: "center"
77
- },
78
- "data-testid": "dropdown-item-input"
79
- }}
80
- // eslint-disable-next-line react/jsx-no-duplicate-props
81
- InputProps={{ disableUnderline: true }}
82
- className="options-input__dropdown-item-input"
83
- />
84
- <Box
85
- display="flex"
86
- flexDirection="column"
87
- justifyContent="center"
88
- paddingRight={2}
89
- paddingLeft={1}
90
- minWidth="5rem"
91
- >
92
- <Typography
93
- variant="caption"
94
- fontWeight={600}
95
- fontSize={13}
96
- sx={{ my: 0 }}
97
- color="black"
98
- >
99
- {label || id}
100
- </Typography>
101
- {details && (
102
- <Box
103
- position="relative"
104
- height="1rem"
105
- data-testid="option-details-container"
106
- onMouseEnter={() => setShouldDisplayFullDetails(true)}
107
- onMouseLeave={() => setShouldDisplayFullDetails(false)}
108
- >
109
- <Typography
110
- variant="caption"
111
- color="gray"
112
- sx={{
113
- my: 0,
114
- zIndex: 10000,
115
- position: "fixed",
116
- ...(shouldDisplayFullDetails && {
117
- backgroundColor: quantity ? "#ffffff" : "#eeeeee",
118
- border: "thin solid #d3d3d3"
119
- })
120
- }}
121
- data-testid="option-details"
122
- >
123
- {details?.length <= 15 || shouldDisplayFullDetails
124
- ? details
125
- : `${details?.slice(0, 15)}...`}
126
- </Typography>
127
- </Box>
128
- )}
129
- {displayMinMax && <MinMaxValueLabel min={min} max={max} color="gray" />}
130
- </Box>
131
- </Box>
132
- <ButtonGroup
133
- variant="outlined"
134
- size="small"
135
- className="options-input__dropdown-item-buttons"
136
- >
137
- <Button
138
- onClick={onIncrement(id)}
139
- disabled={!!(max && quantity && quantity >= max)}
140
- className="options-input__dropdown-item-button"
141
- role="button"
142
- color={error ? "error" : "primary"}
143
- >
144
- <AddIcon sx={{ color: error ? "black" : "primary" }} />
145
- </Button>
146
- <Button
147
- onClick={onDecrement(id)}
148
- disabled={!!(quantity && min) && quantity <= min}
149
- className="options-input__dropdown-item-button"
150
- role="button"
151
- color={error ? "error" : "primary"}
152
- >
153
- <RemoveIcon sx={{ color: error ? "black" : "primary" }} />
154
- </Button>
155
- </ButtonGroup>
156
- </ListItem>
157
- </>
158
- );
159
- };
160
-
161
- export default OptionsInputDropdownItem;
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { OptionsInput } from "./options-input";
2
- export type { OptionsInputProps } from "./options-input";
@@ -1,353 +0,0 @@
1
- import React, {
2
- useState,
3
- useEffect,
4
- useCallback,
5
- type ReactNode,
6
- useMemo,
7
- useRef
8
- } from "react";
9
- import classNames from "classnames";
10
- import Box from "@mui/material/Box";
11
- import List from "@mui/material/List";
12
- import Popper from "@mui/material/Popper";
13
- import ClickAwayListener from "@mui/material/ClickAwayListener";
14
- import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
15
- import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
16
- import * as DesignSystemIcons from "@talixo-ds/icons";
17
- import type { SxProps } from "@mui/material";
18
- import type { OptionsInputOption, OptionsInputValue } from "./types";
19
- import OptionsInputContentItem from "./components/options-input-content-item";
20
- import OptionsInputDropdownItem from "./components/options-input-dropdown-item";
21
- import { red } from "@mui/material/colors";
22
- import "./styles.scss";
23
-
24
- import "@emotion/react";
25
- import "@emotion/styled";
26
- import { capitalize } from "./utils";
27
-
28
- export type OptionsInputProps = {
29
- /** Array of objects representing options available to choose from */
30
- options: OptionsInputOption[];
31
- /** Object with default values of some options */
32
- defaultValue?: OptionsInputValue;
33
- /** Array with ids of options which should remain displayed even if value is 0 */
34
- persistentOptions?: string[];
35
- /** Boolean to determine if input is disabled */
36
- disabled?: boolean;
37
- /** Boolean to determine if input is readOnly */
38
- readOnly?: boolean;
39
- /** Boolean to determine if min and max input values should be displayed */
40
- displayMinMax?: boolean;
41
- /** Function which handles options input value change */
42
- onChange?: (OptionsInputValue: OptionsInputValue) => void;
43
- /** Function which handles options input focus event */
44
- onFocus?: (OptionsInputValue: OptionsInputValue) => void;
45
- /** Function which handles options input blur event */
46
- onBlur?: (OptionsInputValue: OptionsInputValue) => void;
47
- /** className attached to an input container */
48
- className?: string;
49
- /** id attached to an input container */
50
- id?: string;
51
- /** data test id attached to an input container */
52
- "data-testid"?: string;
53
- /** Gap between input items */
54
- itemsGap?: string | number;
55
- /** Custom styles for container */
56
- containerSx?: SxProps;
57
- /** Flag indicating if there is an validation error */
58
- error?: boolean;
59
- /** Additional content displayed with small font under the input */
60
- helperText?: ReactNode;
61
- /** Custom styles for helper text container */
62
- helperTextSx?: SxProps;
63
- };
64
-
65
- export const OptionsInput = ({
66
- options,
67
- onChange,
68
- onFocus,
69
- onBlur,
70
- persistentOptions = [],
71
- defaultValue,
72
- displayMinMax = false,
73
- disabled = false,
74
- readOnly = false,
75
- id,
76
- className,
77
- itemsGap = 1,
78
- containerSx = [],
79
- error = false,
80
- helperText,
81
- helperTextSx,
82
- ...rest
83
- }: OptionsInputProps) => {
84
- const [currentOptions, setCurrentOptions] = useState<OptionsInputOption[]>([]);
85
- const [inputContainerWidth, setInputContainerWidth] = useState(0);
86
- const inputContainerRef = useRef<HTMLElement>();
87
- const [anchorEl, setAnchorEl] = useState<undefined | HTMLElement>();
88
- const open = !!anchorEl;
89
-
90
- useEffect(
91
- () =>
92
- setCurrentOptions(
93
- options.map((option) => {
94
- const quantity = defaultValue?.[option.id] ?? 0;
95
-
96
- return {
97
- ...option,
98
- quantity,
99
- inputQuantity: quantity
100
- };
101
- })
102
- ),
103
- [options, defaultValue]
104
- );
105
-
106
- useEffect(() => {
107
- setInputContainerWidth(inputContainerRef?.current?.clientWidth ?? 0);
108
- }, [inputContainerRef?.current?.clientWidth]);
109
-
110
- const optionsExceedingBoundaries = useMemo(
111
- () =>
112
- currentOptions.reduce((exceedingOptions, { quantity, id, label }) => {
113
- const option = options.find((option) => option.id === id);
114
-
115
- if (
116
- label &&
117
- quantity !== undefined &&
118
- ((option?.max !== undefined && option?.max < quantity) ||
119
- (option?.min !== undefined && option?.min > quantity))
120
- ) {
121
- return [...exceedingOptions, label];
122
- }
123
-
124
- return exceedingOptions;
125
- }, [] as string[]),
126
- [currentOptions, options]
127
- );
128
-
129
- const isError = useMemo(
130
- () => error || !!optionsExceedingBoundaries.length,
131
- [error, optionsExceedingBoundaries]
132
- );
133
-
134
- const toggleInput = useCallback(
135
- (event: React.MouseEvent<HTMLElement>) => {
136
- const { currentTarget } = event;
137
-
138
- if (!disabled && !readOnly) {
139
- setTimeout(() => {
140
- setAnchorEl((currentAnchor) =>
141
- currentAnchor ? undefined : currentTarget
142
- );
143
- }, 0);
144
- }
145
- },
146
- [disabled, readOnly, setAnchorEl]
147
- );
148
-
149
- const onInputFocus = () => {
150
- if (onFocus) {
151
- onFocus(
152
- currentOptions.reduce(
153
- (currentValues, currentOption) => ({
154
- ...currentValues,
155
- [currentOption.id]: currentOption.quantity
156
- }),
157
- {}
158
- ) as OptionsInputValue
159
- );
160
- }
161
- };
162
-
163
- const onInputBlur = () => {
164
- if (onBlur) {
165
- onBlur(
166
- currentOptions.reduce(
167
- (currentValues, currentOption) => ({
168
- ...currentValues,
169
- [currentOption.id]: currentOption.quantity
170
- }),
171
- {}
172
- ) as OptionsInputValue
173
- );
174
- }
175
- };
176
-
177
- const onValueChange = (optionId: string, newValue: string | number) => {
178
- const newQuantity = Number.isNaN(Number(newValue)) ? 0 : Number(newValue);
179
-
180
- const newCurrentOptions = currentOptions.map((option) => {
181
- const maxQuantity =
182
- newQuantity > (option?.max ?? Infinity) ? option?.max : newQuantity;
183
-
184
- return {
185
- ...option,
186
- ...(optionId === option.id && {
187
- quantity:
188
- newQuantity < (option?.min ?? -Infinity) ? option?.min : maxQuantity,
189
- inputQuantity: newValue
190
- })
191
- };
192
- });
193
-
194
- if (onChange) {
195
- onChange(
196
- newCurrentOptions.reduce(
197
- (currentValues, currentOption) => ({
198
- ...currentValues,
199
- [currentOption.id]: currentOption.quantity
200
- }),
201
- {}
202
- ) as OptionsInputValue
203
- );
204
- }
205
-
206
- setCurrentOptions(newCurrentOptions);
207
- };
208
-
209
- const onDropdownItemBlur = (optionId: string) => () =>
210
- setCurrentOptions(
211
- currentOptions.map((option) => {
212
- if (optionId !== option.id) return option;
213
-
214
- const finalQuantity = Number.isNaN(Number(option?.inputQuantity))
215
- ? 0
216
- : Number(option?.inputQuantity);
217
- const maxQuantity =
218
- finalQuantity > (option?.max ?? Infinity) ? option?.max : finalQuantity;
219
-
220
- return {
221
- ...option,
222
- inputQuantity:
223
- finalQuantity < (option?.min ?? -Infinity) ? option?.min : maxQuantity
224
- };
225
- })
226
- );
227
-
228
- return (
229
- <>
230
- <Box>
231
- <Box
232
- id={id}
233
- onClick={toggleInput}
234
- onBlur={onInputBlur}
235
- onFocus={onInputFocus}
236
- ref={inputContainerRef}
237
- className={classNames("options-input__container", className, {
238
- "options-input__container--open": open,
239
- "options-input__container--disabled": disabled,
240
- "options-input__container--read-only": readOnly,
241
- "options-input__container--error": isError
242
- })}
243
- sx={[
244
- { "&:hover": { borderColor: "#d3d3d3" } },
245
- ...(Array.isArray(containerSx) ? containerSx : [containerSx]),
246
- open && {
247
- borderColor: (theme) => theme.palette.primary.main,
248
- "&:hover": { borderColor: (theme) => theme.palette.primary.main }
249
- },
250
- isError && {
251
- borderColor: red[700],
252
- "&:hover": { borderColor: red[700] }
253
- }
254
- ]}
255
- data-testid={rest["data-testid"] || "options-input-container"}
256
- tabIndex={0}
257
- >
258
- <Box display="flex" gap={itemsGap}>
259
- {currentOptions
260
- .filter(
261
- ({ quantity, id: optionId, icon }) =>
262
- !!(
263
- DesignSystemIcons[
264
- capitalize(icon) as keyof typeof DesignSystemIcons
265
- ] &&
266
- (quantity !== 0 || persistentOptions?.includes(optionId))
267
- )
268
- )
269
- .map((option) => (
270
- <OptionsInputContentItem
271
- key={option.id}
272
- item={option}
273
- disabled={disabled}
274
- displayMinMax={displayMinMax}
275
- error={
276
- !!(option?.label && optionsExceedingBoundaries.includes(option.label))
277
- }
278
- />
279
- ))}
280
- </Box>
281
- {!readOnly &&
282
- (open ? (
283
- <KeyboardArrowUpIcon sx={{ color: isError ? red[700] : "primary" }} />
284
- ) : (
285
- <KeyboardArrowDownIcon
286
- sx={{
287
- color: (theme) =>
288
- disabled ? theme.palette.grey[400] : theme.palette.action.focus
289
- }}
290
- />
291
- ))}
292
- </Box>
293
- {(helperText || isError) && (
294
- <Box
295
- sx={{
296
- fontSize: "small",
297
- ...(helperTextSx || {}),
298
- ...(isError ? { color: red[700] } : {})
299
- }}
300
- marginTop={1}
301
- marginLeft={2}
302
- width={inputContainerWidth}
303
- >
304
- {optionsExceedingBoundaries.length
305
- ? (() => {
306
- const messagePluralForm =
307
- optionsExceedingBoundaries.length > 1 ? "s" : "";
308
-
309
- return `Value${messagePluralForm} for ${optionsExceedingBoundaries.join(", ")} option${messagePluralForm} ${messagePluralForm ? "are" : "is"} out of range.`;
310
- })()
311
- : helperText}
312
- </Box>
313
- )}
314
- </Box>
315
- <ClickAwayListener onClickAway={() => open && setAnchorEl(undefined)}>
316
- <Popper
317
- open={open}
318
- placement="bottom-start"
319
- anchorEl={anchorEl}
320
- sx={(theme) => ({ zIndex: theme.zIndex.modal })}
321
- >
322
- <List
323
- disablePadding
324
- data-testid="options-dropdown-list"
325
- className="options-input__dropdown-items-list"
326
- sx={{
327
- bgcolor: "Background",
328
- border: (theme) =>
329
- `thin solid ${isError ? red[700] : theme.palette.primary.main}`
330
- }}
331
- >
332
- {currentOptions
333
- .filter(
334
- ({ icon }) =>
335
- !!DesignSystemIcons[capitalize(icon) as keyof typeof DesignSystemIcons]
336
- )
337
- .map((option, index) => (
338
- <OptionsInputDropdownItem
339
- key={option.id}
340
- item={option}
341
- onBlur={onDropdownItemBlur(option.id)}
342
- onChange={onValueChange}
343
- index={index}
344
- displayMinMax={displayMinMax}
345
- error={isError}
346
- />
347
- ))}
348
- </List>
349
- </Popper>
350
- </ClickAwayListener>
351
- </>
352
- );
353
- };
package/src/styles.scss DELETED
@@ -1,54 +0,0 @@
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/src/types.ts DELETED
@@ -1,14 +0,0 @@
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
- }
package/src/utils.ts DELETED
@@ -1,2 +0,0 @@
1
- export const capitalize = (str: string) =>
2
- str.charAt(0).toUpperCase() + str.slice(1);
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "baseUrl": "../.."
6
- },
7
- "exclude": ["src/**/__tests__/**"]
8
- }