@talixo-ds/options-input 1.0.5 → 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} +24 -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 -138
  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 -347
  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,347 +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
- };
62
-
63
- export const OptionsInput = ({
64
- options,
65
- onChange,
66
- onFocus,
67
- onBlur,
68
- persistentOptions = [],
69
- defaultValue,
70
- displayMinMax = false,
71
- disabled = false,
72
- readOnly = false,
73
- id,
74
- className,
75
- itemsGap = 1,
76
- containerSx = [],
77
- error = false,
78
- helperText,
79
- ...rest
80
- }: OptionsInputProps) => {
81
- const [currentOptions, setCurrentOptions] = useState<OptionsInputOption[]>([]);
82
- const [inputContainerWidth, setInputContainerWidth] = useState(0);
83
- const inputContainerRef = useRef<HTMLElement>();
84
- const [anchorEl, setAnchorEl] = useState<undefined | HTMLElement>();
85
- const open = !!anchorEl;
86
-
87
- useEffect(
88
- () =>
89
- setCurrentOptions(
90
- options.map((option) => {
91
- const quantity = defaultValue?.[option.id] ?? 0;
92
-
93
- return {
94
- ...option,
95
- quantity,
96
- inputQuantity: quantity
97
- };
98
- })
99
- ),
100
- [options, defaultValue]
101
- );
102
-
103
- useEffect(() => {
104
- setInputContainerWidth(inputContainerRef?.current?.clientWidth ?? 0);
105
- }, [inputContainerRef?.current?.clientWidth]);
106
-
107
- const optionsExceedingBoundaries = useMemo(
108
- () =>
109
- currentOptions.reduce((exceedingOptions, { quantity, id, label }) => {
110
- const option = options.find((option) => option.id === id);
111
-
112
- if (
113
- label &&
114
- quantity !== undefined &&
115
- ((option?.max !== undefined && option?.max < quantity) ||
116
- (option?.min !== undefined && option?.min > quantity))
117
- ) {
118
- return [...exceedingOptions, label];
119
- }
120
-
121
- return exceedingOptions;
122
- }, [] as string[]),
123
- [currentOptions, options]
124
- );
125
-
126
- const isError = useMemo(
127
- () => error || !!optionsExceedingBoundaries.length,
128
- [error, optionsExceedingBoundaries]
129
- );
130
-
131
- const toggleInput = useCallback(
132
- (event: React.MouseEvent<HTMLElement>) => {
133
- const { currentTarget } = event;
134
-
135
- if (!disabled && !readOnly) {
136
- setTimeout(() => {
137
- setAnchorEl((currentAnchor) =>
138
- currentAnchor ? undefined : currentTarget
139
- );
140
- }, 0);
141
- }
142
- },
143
- [disabled, readOnly, setAnchorEl]
144
- );
145
-
146
- const onInputFocus = () => {
147
- if (onFocus) {
148
- onFocus(
149
- currentOptions.reduce(
150
- (currentValues, currentOption) => ({
151
- ...currentValues,
152
- [currentOption.id]: currentOption.quantity
153
- }),
154
- {}
155
- ) as OptionsInputValue
156
- );
157
- }
158
- };
159
-
160
- const onInputBlur = () => {
161
- if (onBlur) {
162
- onBlur(
163
- currentOptions.reduce(
164
- (currentValues, currentOption) => ({
165
- ...currentValues,
166
- [currentOption.id]: currentOption.quantity
167
- }),
168
- {}
169
- ) as OptionsInputValue
170
- );
171
- }
172
- };
173
-
174
- const onValueChange = (optionId: string, newValue: string | number) => {
175
- const newQuantity = Number.isNaN(Number(newValue)) ? 0 : Number(newValue);
176
-
177
- const newCurrentOptions = currentOptions.map((option) => {
178
- const maxQuantity =
179
- newQuantity > (option?.max ?? Infinity) ? option?.max : newQuantity;
180
-
181
- return {
182
- ...option,
183
- ...(optionId === option.id && {
184
- quantity:
185
- newQuantity < (option?.min ?? -Infinity) ? option?.min : maxQuantity,
186
- inputQuantity: newValue
187
- })
188
- };
189
- });
190
-
191
- if (onChange) {
192
- onChange(
193
- newCurrentOptions.reduce(
194
- (currentValues, currentOption) => ({
195
- ...currentValues,
196
- [currentOption.id]: currentOption.quantity
197
- }),
198
- {}
199
- ) as OptionsInputValue
200
- );
201
- }
202
-
203
- setCurrentOptions(newCurrentOptions);
204
- };
205
-
206
- const onDropdownItemBlur = (optionId: string) => () =>
207
- setCurrentOptions(
208
- currentOptions.map((option) => {
209
- if (optionId !== option.id) return option;
210
-
211
- const finalQuantity = Number.isNaN(Number(option?.inputQuantity))
212
- ? 0
213
- : Number(option?.inputQuantity);
214
- const maxQuantity =
215
- finalQuantity > (option?.max ?? Infinity) ? option?.max : finalQuantity;
216
-
217
- return {
218
- ...option,
219
- inputQuantity:
220
- finalQuantity < (option?.min ?? -Infinity) ? option?.min : maxQuantity
221
- };
222
- })
223
- );
224
-
225
- return (
226
- <>
227
- <Box
228
- id={id}
229
- onClick={toggleInput}
230
- onBlur={onInputBlur}
231
- onFocus={onInputFocus}
232
- ref={inputContainerRef}
233
- className={classNames("options-input__container", className, {
234
- "options-input__container--open": open,
235
- "options-input__container--disabled": disabled,
236
- "options-input__container--read-only": readOnly,
237
- "options-input__container--error": isError
238
- })}
239
- sx={[
240
- { "&:hover": { borderColor: "#d3d3d3" } },
241
- ...(Array.isArray(containerSx) ? containerSx : [containerSx]),
242
- open && {
243
- borderColor: (theme) => theme.palette.primary.main,
244
- "&:hover": { borderColor: (theme) => theme.palette.primary.main }
245
- },
246
- isError && {
247
- borderColor: red[700],
248
- "&:hover": { borderColor: red[700] }
249
- }
250
- ]}
251
- data-testid={rest["data-testid"] || "options-input-container"}
252
- tabIndex={0}
253
- >
254
- <Box display="flex" gap={itemsGap}>
255
- {currentOptions
256
- .filter(
257
- ({ quantity, id: optionId, icon }) =>
258
- !!(
259
- DesignSystemIcons[
260
- capitalize(icon) as keyof typeof DesignSystemIcons
261
- ] &&
262
- (quantity !== 0 || persistentOptions?.includes(optionId))
263
- )
264
- )
265
- .map((option) => (
266
- <OptionsInputContentItem
267
- key={option.id}
268
- item={option}
269
- disabled={disabled}
270
- displayMinMax={displayMinMax}
271
- error={
272
- !!(option?.label && optionsExceedingBoundaries.includes(option.label))
273
- }
274
- />
275
- ))}
276
- </Box>
277
- {!readOnly &&
278
- (open ? (
279
- <KeyboardArrowUpIcon sx={{ color: isError ? red[700] : "primary" }} />
280
- ) : (
281
- <KeyboardArrowDownIcon
282
- sx={{
283
- color: (theme) =>
284
- disabled ? theme.palette.grey[400] : theme.palette.action.focus
285
- }}
286
- />
287
- ))}
288
- </Box>
289
- {(helperText || isError) && (
290
- <Box
291
- sx={{
292
- ...(isError ? { color: red[700] } : {})
293
- }}
294
- marginTop={1}
295
- marginLeft={2}
296
- width={inputContainerWidth}
297
- fontSize="small"
298
- >
299
- {optionsExceedingBoundaries.length
300
- ? (() => {
301
- const messagePluralForm =
302
- optionsExceedingBoundaries.length > 1 ? "s" : "";
303
-
304
- return `Value${messagePluralForm} for ${optionsExceedingBoundaries.join(", ")} option${messagePluralForm} ${messagePluralForm ? "are" : "is"} out of range.`;
305
- })()
306
- : helperText}
307
- </Box>
308
- )}
309
- <ClickAwayListener onClickAway={() => open && setAnchorEl(undefined)}>
310
- <Popper
311
- open={open}
312
- placement="bottom-start"
313
- anchorEl={anchorEl}
314
- sx={(theme) => ({ zIndex: theme.zIndex.modal })}
315
- >
316
- <List
317
- disablePadding
318
- data-testid="options-dropdown-list"
319
- className="options-input__dropdown-items-list"
320
- sx={{
321
- bgcolor: "Background",
322
- border: (theme) =>
323
- `thin solid ${isError ? red[700] : theme.palette.primary.main}`
324
- }}
325
- >
326
- {currentOptions
327
- .filter(
328
- ({ icon }) =>
329
- !!DesignSystemIcons[capitalize(icon) as keyof typeof DesignSystemIcons]
330
- )
331
- .map((option, index) => (
332
- <OptionsInputDropdownItem
333
- key={option.id}
334
- item={option}
335
- onBlur={onDropdownItemBlur(option.id)}
336
- onChange={onValueChange}
337
- index={index}
338
- displayMinMax={displayMinMax}
339
- error={isError}
340
- />
341
- ))}
342
- </List>
343
- </Popper>
344
- </ClickAwayListener>
345
- </>
346
- );
347
- };
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
- }