@widergy/energy-ui 3.171.3 → 3.172.0

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/CHANGELOG.md CHANGED
@@ -1,9 +1,16 @@
1
- ## [3.171.3](https://github.com/widergy/energy-ui/compare/v3.171.2...v3.171.3) (2026-06-29)
1
+ # [3.172.0](https://github.com/widergy/energy-ui/compare/v3.171.3...v3.172.0) (2026-07-02)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
+
6
+ * [AXCH-1900] fixed bottom option in utselectv1 ([#830](https://github.com/widergy/energy-ui/issues/830)) ([c331ebf](https://github.com/widergy/energy-ui/commit/c331ebfb0cc2d2f02617943ae8ef36c566579793))
7
+
8
+ ## [3.171.3](https://github.com/widergy/energy-ui/compare/v3.171.2...v3.171.3) (2026-06-29)
9
+
10
+ ### Correcciones
5
11
 
6
- * [CX-2712] selected marker z index google map ([#829](https://github.com/widergy/energy-ui/issues/829)) ([7a90960](https://github.com/widergy/energy-ui/commit/7a90960a618da21faf18f2e8d8fa3e3c09be321e))
12
+ * Al tocar un marcador en el mapa, este ahora queda visible por encima de otros marcadores cercanos o superpuestos. [#829](https://github.com/widergy/Energy-UI/pull/829) [CX-2712](https://widergy.atlassian.net/browse/CX-2712)
13
+ * Mejoras internas de la plataforma. [`caa1f1a`](https://github.com/widergy/Energy-UI/commit/caa1f1a)
7
14
 
8
15
  ## [3.171.2](https://github.com/widergy/energy-ui/compare/v3.171.1...v3.171.2) (2026-06-27)
9
16
 
@@ -15,15 +15,17 @@ const UTPaper = _ref => {
15
15
  let {
16
16
  children,
17
17
  classes,
18
+ className,
18
19
  ...otherProps
19
20
  } = _ref;
20
21
  return /*#__PURE__*/_react.default.createElement(_core.Paper, _extends({}, otherProps, {
21
- className: classes.paper
22
+ className: "".concat(classes.paper).concat(className ? " ".concat(className) : '')
22
23
  }), children);
23
24
  };
24
25
  UTPaper.propTypes = {
25
26
  children: _propTypes.object,
26
27
  classes: (0, _propTypes.objectOf)(_propTypes.string),
28
+ className: _propTypes.string,
27
29
  otherProps: _propTypes.object
28
30
  };
29
31
  var _default = exports.default = (0, _WithTheme.default)(_theme.retrieveStyle)(UTPaper);
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.SeleccionMultiple = exports.ListaLarga = exports.ListaCorta = exports.ConCategorias = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _propTypes = require("prop-types");
9
+ var _2 = _interopRequireDefault(require("."));
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
+ 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); }
13
+ const OPTIONS = [{
14
+ name: 'Electricidad',
15
+ value: 'electricity'
16
+ }, {
17
+ name: 'Gas natural',
18
+ value: 'gas'
19
+ }, {
20
+ name: 'Agua potable',
21
+ value: 'water'
22
+ }, {
23
+ name: 'Energía solar',
24
+ value: 'solar'
25
+ }, {
26
+ name: 'Biomasa',
27
+ value: 'biomass'
28
+ }];
29
+ const OPTIONS_WITH_CATEGORIES = [{
30
+ name: 'Residencial bajo',
31
+ value: 'res-low',
32
+ category: 'Residencial'
33
+ }, {
34
+ name: 'Residencial medio',
35
+ value: 'res-mid',
36
+ category: 'Residencial'
37
+ }, {
38
+ name: 'Residencial alto',
39
+ value: 'res-high',
40
+ category: 'Residencial'
41
+ }, {
42
+ name: 'Comercial pequeño',
43
+ value: 'com-sm',
44
+ category: 'Comercial'
45
+ }, {
46
+ name: 'Comercial grande',
47
+ value: 'com-lg',
48
+ category: 'Comercial'
49
+ }, {
50
+ name: 'Industrial liviano',
51
+ value: 'ind-sm',
52
+ category: 'Industrial'
53
+ }, {
54
+ name: 'Industrial pesado',
55
+ value: 'ind-lg',
56
+ category: 'Industrial'
57
+ }];
58
+ const LONG_OPTIONS = Array.from({
59
+ length: 20
60
+ }, (_, i) => ({
61
+ name: "Opci\xF3n ".concat(i + 1),
62
+ value: "opt-".concat(i + 1)
63
+ }));
64
+ const wrapperStyle = {
65
+ maxWidth: '360px',
66
+ padding: '1rem'
67
+ };
68
+ const SelectWrapper = _ref => {
69
+ let {
70
+ multiple,
71
+ ...props
72
+ } = _ref;
73
+ const [value, setValue] = (0, _react.useState)(multiple ? [] : null);
74
+ return /*#__PURE__*/_react.default.createElement("div", {
75
+ style: wrapperStyle
76
+ }, /*#__PURE__*/_react.default.createElement(_2.default, _extends({}, props, {
77
+ multiple: multiple,
78
+ onChange: setValue,
79
+ value: value,
80
+ version: "V1"
81
+ })));
82
+ };
83
+ SelectWrapper.propTypes = {
84
+ multiple: _propTypes.bool,
85
+ title: _propTypes.string
86
+ };
87
+ var _default = exports.default = {
88
+ component: _2.default,
89
+ title: 'Energy-UI/UTSelect/Con opción fija'
90
+ };
91
+ const ListaLarga = exports.ListaLarga = {
92
+ name: 'Lista larga',
93
+ args: {
94
+ fixedBottomOption: {
95
+ name: 'Otro motivo',
96
+ value: 'other'
97
+ },
98
+ options: LONG_OPTIONS,
99
+ placeholder: 'Seleccioná una opción',
100
+ title: 'Con opción fija al final (lista larga)'
101
+ },
102
+ parameters: {
103
+ docs: {
104
+ description: {
105
+ 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".'
106
+ }
107
+ }
108
+ },
109
+ render: args => /*#__PURE__*/_react.default.createElement(SelectWrapper, args)
110
+ };
111
+ const ListaCorta = exports.ListaCorta = {
112
+ name: 'Lista corta',
113
+ args: {
114
+ fixedBottomOption: {
115
+ name: 'Otro motivo',
116
+ value: 'other'
117
+ },
118
+ options: OPTIONS,
119
+ placeholder: 'Seleccioná una opción',
120
+ title: 'Con opción fija al final (lista corta)'
121
+ },
122
+ parameters: {
123
+ docs: {
124
+ description: {
125
+ 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.'
126
+ }
127
+ }
128
+ },
129
+ render: args => /*#__PURE__*/_react.default.createElement(SelectWrapper, args)
130
+ };
131
+ const ConCategorias = exports.ConCategorias = {
132
+ name: 'Con categorías',
133
+ args: {
134
+ fixedBottomOption: {
135
+ name: 'Otro motivo',
136
+ value: 'other'
137
+ },
138
+ options: OPTIONS_WITH_CATEGORIES,
139
+ placeholder: 'Seleccioná una opción',
140
+ title: 'Con categorías y opción fija'
141
+ },
142
+ parameters: {
143
+ docs: {
144
+ description: {
145
+ 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.'
146
+ }
147
+ }
148
+ },
149
+ render: args => /*#__PURE__*/_react.default.createElement(SelectWrapper, args)
150
+ };
151
+ const SeleccionMultiple = exports.SeleccionMultiple = {
152
+ name: 'Selección múltiple',
153
+ args: {
154
+ fixedBottomOption: {
155
+ name: 'Otro motivo',
156
+ value: 'other'
157
+ },
158
+ multiple: true,
159
+ options: OPTIONS,
160
+ placeholder: 'Seleccioná una o más opciones',
161
+ title: 'Selección múltiple con opción fija'
162
+ },
163
+ parameters: {
164
+ docs: {
165
+ description: {
166
+ 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.'
167
+ }
168
+ }
169
+ },
170
+ render: args => /*#__PURE__*/_react.default.createElement(SelectWrapper, args)
171
+ };
@@ -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.
@@ -16,11 +16,13 @@ var _stylesModule = _interopRequireDefault(require("./styles.module.scss"));
16
16
  var _constants2 = require("./constants");
17
17
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
18
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
+ 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); }
19
20
  const ListboxComponent = _ref => {
20
21
  let {
21
22
  CustomRow,
22
23
  dataTestId,
23
24
  filteredOptions,
25
+ fixedBottomOption,
24
26
  handleSelectionChange,
25
27
  horizontalSpacing = _constants.SPACING.SMALL,
26
28
  itemDataTestId,
@@ -32,28 +34,54 @@ const ListboxComponent = _ref => {
32
34
  } = _ref;
33
35
  const hasNoOptions = (0, _react.useMemo)(() => (0, _isEmpty.default)(filteredOptions), [filteredOptions]);
34
36
  const maxHeightValid = (0, _react.useMemo)(() => (0, _isFinite.default)(maxHeight) ? maxHeight : _constants2.DEFAULT_MAX_HEIGHT, [maxHeight]);
35
- return hasNoOptions ? /*#__PURE__*/_react.default.createElement(_UTLabel.default, {
36
- colorTheme: "gray",
37
- className: _stylesModule.default.noOptionsLabel
38
- }, noMatchesText) : /*#__PURE__*/_react.default.createElement(_reactPerfectScrollbar.default, {
39
- style: {
40
- maxHeight: maxHeightValid
37
+ const fixedSectionRef = (0, _react.useRef)(null);
38
+ const [reservedHeight, setReservedHeight] = (0, _react.useState)(0);
39
+ (0, _react.useLayoutEffect)(() => {
40
+ if (fixedSectionRef.current) {
41
+ setReservedHeight(fixedSectionRef.current.offsetHeight);
41
42
  }
42
- }, /*#__PURE__*/_react.default.createElement(_UTCheckList.default, {
43
- CustomRow: CustomRow,
44
- dataTestId: dataTestId,
45
- horizontalSpacing: horizontalSpacing,
43
+ }, [fixedBottomOption]);
44
+ const scrollMaxHeight = maxHeightValid - reservedHeight;
45
+ const handleFixedOptionChange = newValues => {
46
+ if (!multiple && newValues.length === 0) {
47
+ handleSelectionChange([fixedBottomOption.value]);
48
+ } else {
49
+ handleSelectionChange(newValues);
50
+ }
51
+ };
52
+ const checkListProps = {
53
+ CustomRow,
54
+ horizontalSpacing,
46
55
  isSimple: !multiple,
47
- itemDataTestId: itemDataTestId,
56
+ itemDataTestId,
48
57
  onChange: handleSelectionChange,
49
- options: filteredOptions,
50
58
  reversed: true,
51
59
  showSelectAll: false,
52
60
  value: multiple ? value : [value],
53
- variant: "button",
54
- version: "V1",
55
- verticalSpacing: verticalSpacing
56
- }));
61
+ variant: 'button',
62
+ version: 'V1',
63
+ verticalSpacing
64
+ };
65
+ return /*#__PURE__*/_react.default.createElement("div", {
66
+ className: _stylesModule.default.listbox
67
+ }, hasNoOptions ? /*#__PURE__*/_react.default.createElement(_UTLabel.default, {
68
+ colorTheme: "gray",
69
+ className: _stylesModule.default.noOptionsLabel
70
+ }, noMatchesText) : /*#__PURE__*/_react.default.createElement(_reactPerfectScrollbar.default, {
71
+ style: {
72
+ maxHeight: scrollMaxHeight
73
+ }
74
+ }, /*#__PURE__*/_react.default.createElement(_UTCheckList.default, _extends({}, checkListProps, {
75
+ dataTestId: dataTestId,
76
+ options: filteredOptions
77
+ }))), fixedBottomOption && /*#__PURE__*/_react.default.createElement("div", {
78
+ ref: fixedSectionRef
79
+ }, /*#__PURE__*/_react.default.createElement("div", {
80
+ className: _stylesModule.default.fixedOptionDivider
81
+ }), /*#__PURE__*/_react.default.createElement(_UTCheckList.default, _extends({}, checkListProps, {
82
+ onChange: handleFixedOptionChange,
83
+ options: [fixedBottomOption]
84
+ }))));
57
85
  };
58
86
  ListboxComponent.propTypes = {
59
87
  CustomRow: _propTypes.elementType,
@@ -62,6 +90,10 @@ ListboxComponent.propTypes = {
62
90
  name: _propTypes.string,
63
91
  value: (0, _propTypes.oneOfType)([_propTypes.string, _propTypes.number])
64
92
  })),
93
+ fixedBottomOption: (0, _propTypes.shape)({
94
+ name: _propTypes.string,
95
+ value: (0, _propTypes.oneOfType)([_propTypes.string, _propTypes.number])
96
+ }),
65
97
  handleSelectionChange: _propTypes.func,
66
98
  horizontalSpacing: _propTypes.string,
67
99
  itemDataTestId: _propTypes.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
+ }
@@ -37,7 +37,10 @@ const UTSelect = _ref => {
37
37
  disableFilterOptions = false,
38
38
  error,
39
39
  errorDataTestId,
40
+ fixedBottomOption,
40
41
  freeWidth = false,
42
+ hideSingleCategoryTitle = false,
43
+ paperClassName,
41
44
  helpText,
42
45
  icon,
43
46
  inputSize,
@@ -72,26 +75,38 @@ const UTSelect = _ref => {
72
75
  clonedOptions.forEach(option => {
73
76
  if (!categoryOrder.has(option.category)) categoryOrder.set(option.category, categoryOrder.size);
74
77
  });
75
- return clonedOptions.sort((a, b) => categoryOrder.get(a.category) - categoryOrder.get(b.category));
76
- }, [options]);
78
+ const sorted = clonedOptions.sort((a, b) => categoryOrder.get(a.category) - categoryOrder.get(b.category));
79
+ if (hideSingleCategoryTitle && categoryOrder.size <= 1) {
80
+ return sorted.map(_ref2 => {
81
+ let {
82
+ category: _category,
83
+ ...rest
84
+ } = _ref2;
85
+ return rest;
86
+ });
87
+ }
88
+ return sorted;
89
+ }, [options, hideSingleCategoryTitle]);
90
+ const displayOptions = (0, _react.useMemo)(() => fixedBottomOption ? [...optionsSortedByCategory, fixedBottomOption] : optionsSortedByCategory, [optionsSortedByCategory, fixedBottomOption]);
77
91
  const [isPopperOpen, setIsPopperOpen] = (0, _react.useState)(false);
78
- const [searchTerm, setSearchTerm] = (0, _react.useState)((0, _utils2.getDisplayValue)(value, optionsSortedByCategory, multiple));
92
+ const [searchTerm, setSearchTerm] = (0, _react.useState)((0, _utils2.getDisplayValue)(value, displayOptions, multiple));
79
93
  const [showClearButton, setShowClearButton] = (0, _react.useState)(!!searchTerm);
80
94
  const [sortedOptions, setSortedOptions] = (0, _react.useState)(optionsSortedByCategory);
81
95
  const inputRef = (0, _react.useRef)(null);
82
96
  (0, _react.useEffect)(() => {
83
- if (withAutoReset && (0, _utils2.shouldReset)(value, optionsSortedByCategory, multiple)) {
97
+ const fixedIsSelected = fixedBottomOption && (multiple ? Array.isArray(value) && value.includes(fixedBottomOption.value) : value === (fixedBottomOption === null || fixedBottomOption === void 0 ? void 0 : fixedBottomOption.value));
98
+ if (withAutoReset && !fixedIsSelected && (0, _utils2.shouldReset)(value, optionsSortedByCategory, multiple)) {
84
99
  onChange(null);
85
100
  setSortedOptions((0, _utils2.sortOptions)(optionsSortedByCategory, null, multiple));
86
101
  }
87
- }, [multiple, onChange, optionsSortedByCategory, value, withAutoReset]);
102
+ }, [multiple, onChange, optionsSortedByCategory, value, withAutoReset, fixedBottomOption]);
88
103
  (0, _react.useEffect)(() => {
89
104
  if (!isPopperOpen) {
90
- const displayValue = (0, _utils2.getDisplayValue)(value, optionsSortedByCategory, multiple);
105
+ const displayValue = (0, _utils2.getDisplayValue)(value, displayOptions, multiple);
91
106
  setSearchTerm(displayValue);
92
107
  setShowClearButton(displayValue);
93
108
  }
94
- }, [value, optionsSortedByCategory, multiple, isPopperOpen]);
109
+ }, [value, displayOptions, multiple, isPopperOpen]);
95
110
  (0, _react.useEffect)(() => {
96
111
  if (isPopperOpen) {
97
112
  setSortedOptions((0, _utils2.sortOptions)(optionsSortedByCategory, value, multiple));
@@ -105,17 +120,21 @@ const UTSelect = _ref => {
105
120
  }, [isPopperOpen]);
106
121
  const validationData = (0, _react.useMemo)(() => error && (0, _utils.formatErrorToValidation)(error), [error]);
107
122
  const isPicker = variant === _constants.VARIANTS.picker;
108
- const filteredOptions = (0, _react.useMemo)(() => disableFilterOptions || isPicker ? sortedOptions : sortedOptions.filter(option => "".concat(option.name).toLowerCase().includes(searchTerm.toLowerCase())), [sortedOptions, searchTerm, disableFilterOptions, isPicker]);
123
+ const filteredOptions = (0, _react.useMemo)(() => {
124
+ const base = disableFilterOptions || isPicker ? sortedOptions : sortedOptions.filter(option => "".concat(option.name).toLowerCase().includes(searchTerm.toLowerCase()));
125
+ if (!fixedBottomOption) return base;
126
+ return base.filter(opt => opt.value !== fixedBottomOption.value);
127
+ }, [sortedOptions, searchTerm, disableFilterOptions, isPicker, fixedBottomOption]);
109
128
  const handleSearchChange = (0, _react.useCallback)(searchValue => {
110
129
  onChangeSearchTerm === null || onChangeSearchTerm === void 0 || onChangeSearchTerm(searchValue);
111
130
  setSearchTerm(searchValue);
112
131
  }, [onChangeSearchTerm]);
113
132
  const clearSearchInputValue = (0, _react.useCallback)(inputValue => {
114
133
  setIsPopperOpen(false);
115
- const displayValue = (0, _utils2.getDisplayValue)(inputValue, optionsSortedByCategory, multiple);
134
+ const displayValue = (0, _utils2.getDisplayValue)(inputValue, displayOptions, multiple);
116
135
  handleSearchChange(displayValue);
117
136
  setShowClearButton(displayValue !== '');
118
- }, [_utils2.getDisplayValue, optionsSortedByCategory, multiple, handleSearchChange]);
137
+ }, [_utils2.getDisplayValue, displayOptions, multiple, handleSearchChange]);
119
138
  const handleSelectionChange = newValues => {
120
139
  const selectedValue = multiple ? newValues : newValues.length > 0 ? newValues[0] : '';
121
140
  onChange(selectedValue);
@@ -161,13 +180,14 @@ const UTSelect = _ref => {
161
180
  CustomRow,
162
181
  dataTestId: listDataTestId,
163
182
  filteredOptions,
183
+ fixedBottomOption,
164
184
  handleSelectionChange,
165
185
  itemDataTestId,
166
186
  multiple,
167
187
  noMatchesText,
168
188
  value,
169
189
  ...listProps
170
- }), [CustomRow, filteredOptions, handleSelectionChange, multiple, noMatchesText, value, listProps]);
190
+ }), [CustomRow, filteredOptions, fixedBottomOption, handleSelectionChange, multiple, noMatchesText, value, listProps]);
171
191
  const menuWidthRef = (0, _react.useRef)(menuWidth);
172
192
  menuWidthRef.current = menuWidth;
173
193
  const CustomPopper = (0, _react.useCallback)(props => {
@@ -191,11 +211,15 @@ const UTSelect = _ref => {
191
211
  actions: titleActions,
192
212
  required: required,
193
213
  size: titleVariant
194
- }, title), /*#__PURE__*/_react.default.createElement(_lab.Autocomplete, _extends({}, freeWidth && !menuWidth ? {
214
+ }, title), /*#__PURE__*/_react.default.createElement(_lab.Autocomplete, _extends({
195
215
  classes: {
196
- popper: _stylesModule.default.popper
197
- }
198
- } : {}, {
216
+ ...(freeWidth && !menuWidth ? {
217
+ popper: _stylesModule.default.popper
218
+ } : {}),
219
+ ...(paperClassName ? {
220
+ paper: paperClassName
221
+ } : {})
222
+ },
199
223
  PopperComponent: CustomPopper,
200
224
  disabled: disabled || readOnly,
201
225
  getOptionLabel: option => "".concat(option.name),
@@ -24,7 +24,13 @@ const utselectTypes = exports.utselectTypes = {
24
24
  disableFilterOptions: _propTypes.bool,
25
25
  error: _propTypes.string,
26
26
  errorDataTestId: _propTypes.string,
27
+ fixedBottomOption: (0, _propTypes.shape)({
28
+ name: _propTypes.string,
29
+ value: (0, _propTypes.oneOfType)([_propTypes.string, _propTypes.number])
30
+ }),
27
31
  freeWidth: _propTypes.bool,
32
+ hideSingleCategoryTitle: _propTypes.bool,
33
+ paperClassName: _propTypes.string,
28
34
  helpText: _propTypes.string,
29
35
  icon: _propTypes.string,
30
36
  inputSize: _propTypes.string,
@@ -8,15 +8,17 @@ const UTPaper = _ref => {
8
8
  let {
9
9
  children,
10
10
  classes,
11
+ className,
11
12
  ...otherProps
12
13
  } = _ref;
13
14
  return /*#__PURE__*/React.createElement(Paper, _extends({}, otherProps, {
14
- className: classes.paper
15
+ className: "".concat(classes.paper).concat(className ? " ".concat(className) : '')
15
16
  }), children);
16
17
  };
17
18
  UTPaper.propTypes = {
18
19
  children: object,
19
20
  classes: objectOf(string),
21
+ className: string,
20
22
  otherProps: object
21
23
  };
22
24
  export default WithTheme(retrieveStyle)(UTPaper);
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widergy/energy-ui",
3
- "version": "3.171.3",
3
+ "version": "3.172.0",
4
4
  "description": "Widergy Web Components",
5
5
  "author": "widergy",
6
6
  "license": "MIT",