@widergy/energy-ui 3.171.3 → 3.172.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/dist/components/UTButton/constants.js +2 -0
  3. package/dist/components/UTButton/index.js +6 -3
  4. package/dist/components/UTButton/stories/UTButtonSizes.stories.js +16 -1
  5. package/dist/components/UTButton/stories/storiesConstants.js +26 -0
  6. package/dist/components/UTButton/styles.module.scss +10 -0
  7. package/dist/components/UTButton/theme.js +1 -1
  8. package/dist/components/UTButton/utils.js +1 -4
  9. package/dist/components/UTDataCategory/theme.js +1 -1
  10. package/dist/components/UTDataElement/README.md +1 -1
  11. package/dist/components/UTPaper/index.js +3 -1
  12. package/dist/components/UTSelect/UTSelectFixedBottomOption.stories.js +171 -0
  13. package/dist/components/UTSelect/versions/V1/README.md +19 -0
  14. package/dist/components/UTSelect/versions/V1/components/ListboxComponent/index.js +48 -16
  15. package/dist/components/UTSelect/versions/V1/components/ListboxComponent/styles.module.scss +11 -0
  16. package/dist/components/UTSelect/versions/V1/index.js +39 -15
  17. package/dist/components/UTSelect/versions/V1/types.js +6 -0
  18. package/dist/components/UTTracker/UTTracker.stories.js +332 -0
  19. package/dist/esm/components/UTButton/constants.js +2 -0
  20. package/dist/esm/components/UTButton/index.js +6 -3
  21. package/dist/esm/components/UTButton/stories/UTButtonSizes.stories.js +15 -0
  22. package/dist/esm/components/UTButton/stories/storiesConstants.js +26 -0
  23. package/dist/esm/components/UTButton/styles.module.scss +10 -0
  24. package/dist/esm/components/UTButton/theme.js +1 -1
  25. package/dist/esm/components/UTButton/utils.js +1 -4
  26. package/dist/esm/components/UTDataCategory/theme.js +2 -2
  27. package/dist/esm/components/UTDataElement/README.md +1 -1
  28. package/dist/esm/components/UTPaper/index.js +3 -1
  29. package/dist/esm/components/UTSelect/UTSelectFixedBottomOption.stories.js +163 -0
  30. package/dist/esm/components/UTSelect/versions/V1/README.md +19 -0
  31. package/dist/esm/components/UTSelect/versions/V1/components/ListboxComponent/index.js +49 -17
  32. package/dist/esm/components/UTSelect/versions/V1/components/ListboxComponent/styles.module.scss +11 -0
  33. package/dist/esm/components/UTSelect/versions/V1/index.js +39 -15
  34. package/dist/esm/components/UTSelect/versions/V1/types.js +6 -0
  35. package/dist/esm/components/UTTracker/UTTracker.stories.js +325 -0
  36. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
2
+ import React, { useState } from 'react';
3
+ import { bool, string } from 'prop-types';
4
+ import UTSelect from '.';
5
+ const OPTIONS = [{
6
+ name: 'Electricidad',
7
+ value: 'electricity'
8
+ }, {
9
+ name: 'Gas natural',
10
+ value: 'gas'
11
+ }, {
12
+ name: 'Agua potable',
13
+ value: 'water'
14
+ }, {
15
+ name: 'Energía solar',
16
+ value: 'solar'
17
+ }, {
18
+ name: 'Biomasa',
19
+ value: 'biomass'
20
+ }];
21
+ const OPTIONS_WITH_CATEGORIES = [{
22
+ name: 'Residencial bajo',
23
+ value: 'res-low',
24
+ category: 'Residencial'
25
+ }, {
26
+ name: 'Residencial medio',
27
+ value: 'res-mid',
28
+ category: 'Residencial'
29
+ }, {
30
+ name: 'Residencial alto',
31
+ value: 'res-high',
32
+ category: 'Residencial'
33
+ }, {
34
+ name: 'Comercial pequeño',
35
+ value: 'com-sm',
36
+ category: 'Comercial'
37
+ }, {
38
+ name: 'Comercial grande',
39
+ value: 'com-lg',
40
+ category: 'Comercial'
41
+ }, {
42
+ name: 'Industrial liviano',
43
+ value: 'ind-sm',
44
+ category: 'Industrial'
45
+ }, {
46
+ name: 'Industrial pesado',
47
+ value: 'ind-lg',
48
+ category: 'Industrial'
49
+ }];
50
+ const LONG_OPTIONS = Array.from({
51
+ length: 20
52
+ }, (_, i) => ({
53
+ name: "Opci\xF3n ".concat(i + 1),
54
+ value: "opt-".concat(i + 1)
55
+ }));
56
+ const wrapperStyle = {
57
+ maxWidth: '360px',
58
+ padding: '1rem'
59
+ };
60
+ const SelectWrapper = _ref => {
61
+ let {
62
+ multiple,
63
+ ...props
64
+ } = _ref;
65
+ const [value, setValue] = useState(multiple ? [] : null);
66
+ return /*#__PURE__*/React.createElement("div", {
67
+ style: wrapperStyle
68
+ }, /*#__PURE__*/React.createElement(UTSelect, _extends({}, props, {
69
+ multiple: multiple,
70
+ onChange: setValue,
71
+ value: value,
72
+ version: "V1"
73
+ })));
74
+ };
75
+ SelectWrapper.propTypes = {
76
+ multiple: bool,
77
+ title: string
78
+ };
79
+ export default {
80
+ component: UTSelect,
81
+ title: 'Energy-UI/UTSelect/Con opción fija'
82
+ };
83
+ export const ListaLarga = {
84
+ name: 'Lista larga',
85
+ args: {
86
+ fixedBottomOption: {
87
+ name: 'Otro motivo',
88
+ value: 'other'
89
+ },
90
+ options: LONG_OPTIONS,
91
+ placeholder: 'Seleccioná una opción',
92
+ title: 'Con opción fija al final (lista larga)'
93
+ },
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story: 'La prop `fixedBottomOption` agrega una opción fija debajo del scroll, siempre visible independientemente del término de búsqueda. Útil para opciones como "Otro motivo".'
98
+ }
99
+ }
100
+ },
101
+ render: args => /*#__PURE__*/React.createElement(SelectWrapper, args)
102
+ };
103
+ export const ListaCorta = {
104
+ name: 'Lista corta',
105
+ args: {
106
+ fixedBottomOption: {
107
+ name: 'Otro motivo',
108
+ value: 'other'
109
+ },
110
+ options: OPTIONS,
111
+ placeholder: 'Seleccioná una opción',
112
+ title: 'Con opción fija al final (lista corta)'
113
+ },
114
+ parameters: {
115
+ docs: {
116
+ description: {
117
+ story: 'La opción fija también se muestra cuando la lista es corta y no hay scroll. Si el usuario filtra y no hay coincidencias, la opción fija permanece visible.'
118
+ }
119
+ }
120
+ },
121
+ render: args => /*#__PURE__*/React.createElement(SelectWrapper, args)
122
+ };
123
+ export const ConCategorias = {
124
+ name: 'Con categorías',
125
+ args: {
126
+ fixedBottomOption: {
127
+ name: 'Otro motivo',
128
+ value: 'other'
129
+ },
130
+ options: OPTIONS_WITH_CATEGORIES,
131
+ placeholder: 'Seleccioná una opción',
132
+ title: 'Con categorías y opción fija'
133
+ },
134
+ parameters: {
135
+ docs: {
136
+ description: {
137
+ story: 'La opción fija convive con opciones agrupadas por categoría. El divider y la opción fija siempre quedan por debajo de todos los grupos.'
138
+ }
139
+ }
140
+ },
141
+ render: args => /*#__PURE__*/React.createElement(SelectWrapper, args)
142
+ };
143
+ export const SeleccionMultiple = {
144
+ name: 'Selección múltiple',
145
+ args: {
146
+ fixedBottomOption: {
147
+ name: 'Otro motivo',
148
+ value: 'other'
149
+ },
150
+ multiple: true,
151
+ options: OPTIONS,
152
+ placeholder: 'Seleccioná una o más opciones',
153
+ title: 'Selección múltiple con opción fija'
154
+ },
155
+ parameters: {
156
+ docs: {
157
+ description: {
158
+ story: 'Con `multiple={true}` la opción fija se comporta como cualquier otra: se puede seleccionar y deseleccionar de forma independiente. El valor es un array que incluye `fixedBottomOption.value` cuando está marcada.'
159
+ }
160
+ }
161
+ },
162
+ render: args => /*#__PURE__*/React.createElement(SelectWrapper, args)
163
+ };
@@ -20,6 +20,7 @@
20
20
  | disableFilterOptions | bool | false | Disables option filtering, showing all options even when typing in the search field. |
21
21
  | error | string | | Error message to display below the input field. |
22
22
  | errorDataTestId | string | | Test ID for the error validation component. |
23
+ | fixedBottomOption | object | | Option rendered below the scrollable list, always visible regardless of the search term. Shape: `{ name: string, value: string \| number }`. |
23
24
  | freeWidth | bool | false | Allows the dropdown to have a free width instead of matching the input width. |
24
25
  | helpText | string | | Help text displayed below the input field. |
25
26
  | icon | string | | Icon name to display in the input field. |
@@ -67,6 +68,24 @@ The `action` prop accepts an object with the following properties:
67
68
 
68
69
  ## Features
69
70
 
71
+ ### `fixedBottomOption`
72
+
73
+ `fixedBottomOption` adds a pinned option at the bottom of the dropdown list, rendered outside the scrollable area so it's always visible regardless of scroll position or active search term. Useful for "catch-all" options like "Other reason".
74
+
75
+ ```jsx
76
+ <UTSelect
77
+ version="V1"
78
+ options={categorizedOptions}
79
+ fixedBottomOption={{ name: 'Otro motivo', value: 'other' }}
80
+ onChange={handleSelect}
81
+ />
82
+ ```
83
+
84
+ - The fixed option is excluded from `filteredOptions` — if it's also present in `options`, it's automatically deduplicated by value.
85
+ - When no options match the search term, `noMatchesText` is shown but the fixed option remains visible.
86
+ - The option is visually separated from the list by a divider.
87
+ - When selected, `onChange` receives the option's value (same behavior as any other option).
88
+
70
89
  ### `withAutoReset`
71
90
 
72
91
  `withAutoReset` is a prop that controls the behavior of the `UTSelect` component when the selected value is no longer available in the current options. This prop ensures that the selected value resets to `null` if the chosen option is no longer valid. When an auto-reset occurs, the options are re-sorted to ensure the list is up-to-date with the current selections.
@@ -1,5 +1,6 @@
1
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
1
2
  import isEmpty from 'lodash/isEmpty';
2
- import React, { useMemo } from 'react';
3
+ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
3
4
  import ScrollBar from 'react-perfect-scrollbar';
4
5
  import { arrayOf, bool, elementType, func, oneOfType, shape, string, number } from 'prop-types';
5
6
  import isFinite from 'lodash/isFinite';
@@ -13,6 +14,7 @@ const ListboxComponent = _ref => {
13
14
  CustomRow,
14
15
  dataTestId,
15
16
  filteredOptions,
17
+ fixedBottomOption,
16
18
  handleSelectionChange,
17
19
  horizontalSpacing = SPACING.SMALL,
18
20
  itemDataTestId,
@@ -24,28 +26,54 @@ const ListboxComponent = _ref => {
24
26
  } = _ref;
25
27
  const hasNoOptions = useMemo(() => isEmpty(filteredOptions), [filteredOptions]);
26
28
  const maxHeightValid = useMemo(() => isFinite(maxHeight) ? maxHeight : DEFAULT_MAX_HEIGHT, [maxHeight]);
27
- return hasNoOptions ? /*#__PURE__*/React.createElement(UTLabel, {
28
- colorTheme: "gray",
29
- className: styles.noOptionsLabel
30
- }, noMatchesText) : /*#__PURE__*/React.createElement(ScrollBar, {
31
- style: {
32
- maxHeight: maxHeightValid
29
+ const fixedSectionRef = useRef(null);
30
+ const [reservedHeight, setReservedHeight] = useState(0);
31
+ useLayoutEffect(() => {
32
+ if (fixedSectionRef.current) {
33
+ setReservedHeight(fixedSectionRef.current.offsetHeight);
33
34
  }
34
- }, /*#__PURE__*/React.createElement(UTCheckList, {
35
- CustomRow: CustomRow,
36
- dataTestId: dataTestId,
37
- horizontalSpacing: horizontalSpacing,
35
+ }, [fixedBottomOption]);
36
+ const scrollMaxHeight = maxHeightValid - reservedHeight;
37
+ const handleFixedOptionChange = newValues => {
38
+ if (!multiple && newValues.length === 0) {
39
+ handleSelectionChange([fixedBottomOption.value]);
40
+ } else {
41
+ handleSelectionChange(newValues);
42
+ }
43
+ };
44
+ const checkListProps = {
45
+ CustomRow,
46
+ horizontalSpacing,
38
47
  isSimple: !multiple,
39
- itemDataTestId: itemDataTestId,
48
+ itemDataTestId,
40
49
  onChange: handleSelectionChange,
41
- options: filteredOptions,
42
50
  reversed: true,
43
51
  showSelectAll: false,
44
52
  value: multiple ? value : [value],
45
- variant: "button",
46
- version: "V1",
47
- verticalSpacing: verticalSpacing
48
- }));
53
+ variant: 'button',
54
+ version: 'V1',
55
+ verticalSpacing
56
+ };
57
+ return /*#__PURE__*/React.createElement("div", {
58
+ className: styles.listbox
59
+ }, hasNoOptions ? /*#__PURE__*/React.createElement(UTLabel, {
60
+ colorTheme: "gray",
61
+ className: styles.noOptionsLabel
62
+ }, noMatchesText) : /*#__PURE__*/React.createElement(ScrollBar, {
63
+ style: {
64
+ maxHeight: scrollMaxHeight
65
+ }
66
+ }, /*#__PURE__*/React.createElement(UTCheckList, _extends({}, checkListProps, {
67
+ dataTestId: dataTestId,
68
+ options: filteredOptions
69
+ }))), fixedBottomOption && /*#__PURE__*/React.createElement("div", {
70
+ ref: fixedSectionRef
71
+ }, /*#__PURE__*/React.createElement("div", {
72
+ className: styles.fixedOptionDivider
73
+ }), /*#__PURE__*/React.createElement(UTCheckList, _extends({}, checkListProps, {
74
+ onChange: handleFixedOptionChange,
75
+ options: [fixedBottomOption]
76
+ }))));
49
77
  };
50
78
  ListboxComponent.propTypes = {
51
79
  CustomRow: elementType,
@@ -54,6 +82,10 @@ ListboxComponent.propTypes = {
54
82
  name: string,
55
83
  value: oneOfType([string, number])
56
84
  })),
85
+ fixedBottomOption: shape({
86
+ name: string,
87
+ value: oneOfType([string, number])
88
+ }),
57
89
  handleSelectionChange: func,
58
90
  horizontalSpacing: string,
59
91
  itemDataTestId: string,
@@ -1,3 +1,14 @@
1
+ .listbox {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
1
6
  .noOptionsLabel {
2
7
  text-align: center;
3
8
  }
9
+
10
+ .fixedOptionDivider {
11
+ background-color: var(--light04);
12
+ height: 1px;
13
+ margin: var(--padding-md) calc(var(--padding-md) * -1);
14
+ }
@@ -29,7 +29,10 @@ const UTSelect = _ref => {
29
29
  disableFilterOptions = false,
30
30
  error,
31
31
  errorDataTestId,
32
+ fixedBottomOption,
32
33
  freeWidth = false,
34
+ hideSingleCategoryTitle = false,
35
+ paperClassName,
33
36
  helpText,
34
37
  icon,
35
38
  inputSize,
@@ -64,26 +67,38 @@ const UTSelect = _ref => {
64
67
  clonedOptions.forEach(option => {
65
68
  if (!categoryOrder.has(option.category)) categoryOrder.set(option.category, categoryOrder.size);
66
69
  });
67
- return clonedOptions.sort((a, b) => categoryOrder.get(a.category) - categoryOrder.get(b.category));
68
- }, [options]);
70
+ const sorted = clonedOptions.sort((a, b) => categoryOrder.get(a.category) - categoryOrder.get(b.category));
71
+ if (hideSingleCategoryTitle && categoryOrder.size <= 1) {
72
+ return sorted.map(_ref2 => {
73
+ let {
74
+ category: _category,
75
+ ...rest
76
+ } = _ref2;
77
+ return rest;
78
+ });
79
+ }
80
+ return sorted;
81
+ }, [options, hideSingleCategoryTitle]);
82
+ const displayOptions = useMemo(() => fixedBottomOption ? [...optionsSortedByCategory, fixedBottomOption] : optionsSortedByCategory, [optionsSortedByCategory, fixedBottomOption]);
69
83
  const [isPopperOpen, setIsPopperOpen] = useState(false);
70
- const [searchTerm, setSearchTerm] = useState(getDisplayValue(value, optionsSortedByCategory, multiple));
84
+ const [searchTerm, setSearchTerm] = useState(getDisplayValue(value, displayOptions, multiple));
71
85
  const [showClearButton, setShowClearButton] = useState(!!searchTerm);
72
86
  const [sortedOptions, setSortedOptions] = useState(optionsSortedByCategory);
73
87
  const inputRef = useRef(null);
74
88
  useEffect(() => {
75
- if (withAutoReset && shouldReset(value, optionsSortedByCategory, multiple)) {
89
+ const fixedIsSelected = fixedBottomOption && (multiple ? Array.isArray(value) && value.includes(fixedBottomOption.value) : value === (fixedBottomOption === null || fixedBottomOption === void 0 ? void 0 : fixedBottomOption.value));
90
+ if (withAutoReset && !fixedIsSelected && shouldReset(value, optionsSortedByCategory, multiple)) {
76
91
  onChange(null);
77
92
  setSortedOptions(sortOptions(optionsSortedByCategory, null, multiple));
78
93
  }
79
- }, [multiple, onChange, optionsSortedByCategory, value, withAutoReset]);
94
+ }, [multiple, onChange, optionsSortedByCategory, value, withAutoReset, fixedBottomOption]);
80
95
  useEffect(() => {
81
96
  if (!isPopperOpen) {
82
- const displayValue = getDisplayValue(value, optionsSortedByCategory, multiple);
97
+ const displayValue = getDisplayValue(value, displayOptions, multiple);
83
98
  setSearchTerm(displayValue);
84
99
  setShowClearButton(displayValue);
85
100
  }
86
- }, [value, optionsSortedByCategory, multiple, isPopperOpen]);
101
+ }, [value, displayOptions, multiple, isPopperOpen]);
87
102
  useEffect(() => {
88
103
  if (isPopperOpen) {
89
104
  setSortedOptions(sortOptions(optionsSortedByCategory, value, multiple));
@@ -97,17 +112,21 @@ const UTSelect = _ref => {
97
112
  }, [isPopperOpen]);
98
113
  const validationData = useMemo(() => error && formatErrorToValidation(error), [error]);
99
114
  const isPicker = variant === VARIANTS.picker;
100
- const filteredOptions = useMemo(() => disableFilterOptions || isPicker ? sortedOptions : sortedOptions.filter(option => "".concat(option.name).toLowerCase().includes(searchTerm.toLowerCase())), [sortedOptions, searchTerm, disableFilterOptions, isPicker]);
115
+ const filteredOptions = useMemo(() => {
116
+ const base = disableFilterOptions || isPicker ? sortedOptions : sortedOptions.filter(option => "".concat(option.name).toLowerCase().includes(searchTerm.toLowerCase()));
117
+ if (!fixedBottomOption) return base;
118
+ return base.filter(opt => opt.value !== fixedBottomOption.value);
119
+ }, [sortedOptions, searchTerm, disableFilterOptions, isPicker, fixedBottomOption]);
101
120
  const handleSearchChange = useCallback(searchValue => {
102
121
  onChangeSearchTerm === null || onChangeSearchTerm === void 0 || onChangeSearchTerm(searchValue);
103
122
  setSearchTerm(searchValue);
104
123
  }, [onChangeSearchTerm]);
105
124
  const clearSearchInputValue = useCallback(inputValue => {
106
125
  setIsPopperOpen(false);
107
- const displayValue = getDisplayValue(inputValue, optionsSortedByCategory, multiple);
126
+ const displayValue = getDisplayValue(inputValue, displayOptions, multiple);
108
127
  handleSearchChange(displayValue);
109
128
  setShowClearButton(displayValue !== '');
110
- }, [getDisplayValue, optionsSortedByCategory, multiple, handleSearchChange]);
129
+ }, [getDisplayValue, displayOptions, multiple, handleSearchChange]);
111
130
  const handleSelectionChange = newValues => {
112
131
  const selectedValue = multiple ? newValues : newValues.length > 0 ? newValues[0] : '';
113
132
  onChange(selectedValue);
@@ -153,13 +172,14 @@ const UTSelect = _ref => {
153
172
  CustomRow,
154
173
  dataTestId: listDataTestId,
155
174
  filteredOptions,
175
+ fixedBottomOption,
156
176
  handleSelectionChange,
157
177
  itemDataTestId,
158
178
  multiple,
159
179
  noMatchesText,
160
180
  value,
161
181
  ...listProps
162
- }), [CustomRow, filteredOptions, handleSelectionChange, multiple, noMatchesText, value, listProps]);
182
+ }), [CustomRow, filteredOptions, fixedBottomOption, handleSelectionChange, multiple, noMatchesText, value, listProps]);
163
183
  const menuWidthRef = useRef(menuWidth);
164
184
  menuWidthRef.current = menuWidth;
165
185
  const CustomPopper = useCallback(props => {
@@ -183,11 +203,15 @@ const UTSelect = _ref => {
183
203
  actions: titleActions,
184
204
  required: required,
185
205
  size: titleVariant
186
- }, title), /*#__PURE__*/React.createElement(Autocomplete, _extends({}, freeWidth && !menuWidth ? {
206
+ }, title), /*#__PURE__*/React.createElement(Autocomplete, _extends({
187
207
  classes: {
188
- popper: styles.popper
189
- }
190
- } : {}, {
208
+ ...(freeWidth && !menuWidth ? {
209
+ popper: styles.popper
210
+ } : {}),
211
+ ...(paperClassName ? {
212
+ paper: paperClassName
213
+ } : {})
214
+ },
191
215
  PopperComponent: CustomPopper,
192
216
  disabled: disabled || readOnly,
193
217
  getOptionLabel: option => "".concat(option.name),
@@ -18,7 +18,13 @@ export const utselectTypes = {
18
18
  disableFilterOptions: bool,
19
19
  error: string,
20
20
  errorDataTestId: string,
21
+ fixedBottomOption: shape({
22
+ name: string,
23
+ value: oneOfType([string, number])
24
+ }),
21
25
  freeWidth: bool,
26
+ hideSingleCategoryTitle: bool,
27
+ paperClassName: string,
22
28
  helpText: string,
23
29
  icon: string,
24
30
  inputSize: string,