@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
package/CHANGELOG.md CHANGED
@@ -1,9 +1,26 @@
1
- ## [3.171.3](https://github.com/widergy/energy-ui/compare/v3.171.2...v3.171.3) (2026-06-29)
1
+ ## [3.172.1](https://github.com/widergy/energy-ui/compare/v3.172.0...v3.172.1) (2026-07-03)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
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))
6
+ * [DIS-1108] ajustes utbutton ([#824](https://github.com/widergy/energy-ui/issues/824)) ([46b1ef8](https://github.com/widergy/energy-ui/commit/46b1ef8e07217bd75451b962236f98790c544f4b))
7
+
8
+ # [3.172.0](https://github.com/widergy/energy-ui/compare/v3.171.3...v3.172.0) (2026-07-02)
9
+
10
+ ### Novedades y Mejoras
11
+
12
+ * En los selectores desplegables, ahora es posible fijar una opción al pie de la lista que permanece siempre visible, incluso al filtrar por búsqueda o al no encontrar resultados. [#830](https://github.com/widergy/Energy-UI/pull/830) [AXCH-1900](https://widergy.atlassian.net/browse/AXCH-1900)
13
+
14
+ ### Correcciones
15
+
16
+ * Mejoras internas de la plataforma. [`56be941`](https://github.com/widergy/Energy-UI/commit/56be941)
17
+
18
+ ## [3.171.3](https://github.com/widergy/energy-ui/compare/v3.171.2...v3.171.3) (2026-06-29)
19
+
20
+ ### Correcciones
21
+
22
+ * 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)
23
+ * Mejoras internas de la plataforma. [`caa1f1a`](https://github.com/widergy/Energy-UI/commit/caa1f1a)
7
24
 
8
25
  ## [3.171.2](https://github.com/widergy/energy-ui/compare/v3.171.1...v3.171.2) (2026-06-27)
9
26
 
@@ -24,6 +24,8 @@ const ICON_PLACEMENTS = exports.ICON_PLACEMENTS = {
24
24
  const DEFAULT_PROPS = exports.DEFAULT_PROPS = {
25
25
  colorTheme: 'secondary',
26
26
  disabled: false,
27
+ fullHeight: false,
28
+ fullWidth: false,
27
29
  hideTextOnResponsive: false,
28
30
  iconPlacement: ICON_PLACEMENTS.left,
29
31
  loading: false,
@@ -29,6 +29,8 @@ const UTButton = _ref => {
29
29
  count,
30
30
  dataTestId,
31
31
  disabled,
32
+ fullHeight,
33
+ fullWidth,
32
34
  hideTextOnResponsive,
33
35
  Icon,
34
36
  iconPlacement,
@@ -58,7 +60,6 @@ const UTButton = _ref => {
58
60
  } = (0, _react.useMemo)(() => (0, _utils.getIconProps)({
59
61
  className: icon,
60
62
  colorTheme,
61
- hasContent: !!children,
62
63
  hideTextOnResponsive,
63
64
  Icon,
64
65
  iconPlacement,
@@ -66,7 +67,7 @@ const UTButton = _ref => {
66
67
  RightIcon,
67
68
  size,
68
69
  variant
69
- }), [colorTheme, children, hideTextOnResponsive, icon, Icon, iconPlacement, LeftIcon, RightIcon, size, variant]);
70
+ }), [colorTheme, hideTextOnResponsive, icon, Icon, iconPlacement, LeftIcon, RightIcon, size, variant]);
70
71
  const LeftIconToShow = LeftIconComponent && /*#__PURE__*/_react.default.createElement(LeftIconComponent, _extends({}, leftIconBaseProps, leftIconProps));
71
72
  const RightIconToShow = RightIconComponent && /*#__PURE__*/_react.default.createElement(RightIconComponent, _extends({}, rightIconBaseProps, rightIconProps));
72
73
  const counterColorTheme = (0, _utils.getCounterColorTheme)(colorTheme, variant);
@@ -80,7 +81,7 @@ const UTButton = _ref => {
80
81
  }), children), RightIconToShow);
81
82
  return /*#__PURE__*/_react.default.createElement(_Button.default, {
82
83
  classes: materialButtonClasses,
83
- className: "\n ".concat(_stylesModule.default["padding".concat((0, _componentUtils.capitalize)(size))], "\n ").concat(hideTextOnResponsive && _stylesModule.default.hideTextOnResponsive, "\n ").concat(!hideTextOnResponsive && !children && _stylesModule.default.noChildren, "\n "),
84
+ className: "\n ".concat(_stylesModule.default["padding".concat((0, _componentUtils.capitalize)(size))], "\n ").concat(fullHeight && _stylesModule.default.fullHeight, "\n ").concat(fullWidth && _stylesModule.default.fullWidth, "\n ").concat(hideTextOnResponsive && _stylesModule.default.hideTextOnResponsive, "\n ").concat(!hideTextOnResponsive && !children && _stylesModule.default.noChildren, "\n "),
84
85
  "data-testid": dataTestId,
85
86
  disabled: disabled || loading,
86
87
  onClick: onClick,
@@ -98,6 +99,8 @@ UTButton.propTypes = {
98
99
  count: _propTypes.number,
99
100
  dataTestId: _propTypes.string,
100
101
  disabled: _propTypes.bool,
102
+ fullHeight: _propTypes.bool,
103
+ fullWidth: _propTypes.bool,
101
104
  hideTextOnResponsive: _propTypes.bool,
102
105
  /**
103
106
  * @deprecated Use LeftIcon or RightIcon instead.
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.Small = exports.Medium = exports.Large = void 0;
6
+ exports.default = exports.Small = exports.Medium = exports.Large = exports.FullHeight = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
7
8
  var _ = _interopRequireDefault(require(".."));
8
9
  var _storiesConstants = require("./storiesConstants");
9
10
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -37,4 +38,18 @@ const Large = exports.Large = {
37
38
  children: 'Large Size',
38
39
  size: 'large'
39
40
  }
41
+ };
42
+ const FullHeight = exports.FullHeight = {
43
+ args: {
44
+ children: 'Full Height',
45
+ fullHeight: true
46
+ },
47
+ render: args => /*#__PURE__*/_react.default.createElement("div", {
48
+ style: {
49
+ display: 'flex',
50
+ height: 80,
51
+ border: '1px dashed #ccc',
52
+ padding: 8
53
+ }
54
+ }, /*#__PURE__*/_react.default.createElement(_.default, args))
40
55
  };
@@ -93,6 +93,30 @@ const COMMON_ARG_TYPES = exports.COMMON_ARG_TYPES = {
93
93
  }
94
94
  }
95
95
  },
96
+ fullHeight: {
97
+ control: 'boolean',
98
+ description: 'Si es `true`, el botón ocupa el alto completo de su contenedor padre. Desactiva el `min-height` por tamaño.',
99
+ table: {
100
+ defaultValue: {
101
+ summary: _constants.DEFAULT_PROPS.fullHeight
102
+ },
103
+ type: {
104
+ summary: 'boolean'
105
+ }
106
+ }
107
+ },
108
+ fullWidth: {
109
+ control: 'boolean',
110
+ description: 'Si es `true`, el botón ocupa el ancho completo de su contenedor padre. Desactiva el `min-width`.',
111
+ table: {
112
+ defaultValue: {
113
+ summary: _constants.DEFAULT_PROPS.fullWidth
114
+ },
115
+ type: {
116
+ summary: 'boolean'
117
+ }
118
+ }
119
+ },
96
120
  hideTextOnResponsive: {
97
121
  control: 'boolean',
98
122
  description: 'Si es `true` y hay ícono, el contenido del botón no se muestra en dispositivos móviles.',
@@ -222,6 +246,8 @@ const COMMON_ARGS = exports.COMMON_ARGS = {
222
246
  colorTheme: _constants.DEFAULT_PROPS.colorTheme,
223
247
  dataTestId: 'customButtonId',
224
248
  disabled: _constants.DEFAULT_PROPS.disabled,
249
+ fullHeight: _constants.DEFAULT_PROPS.fullHeight,
250
+ fullWidth: _constants.DEFAULT_PROPS.fullWidth,
225
251
  hideTextOnResponsive: _constants.DEFAULT_PROPS.hideTextOnResponsive,
226
252
  loading: _constants.DEFAULT_PROPS.loading,
227
253
  size: _constants.DEFAULT_PROPS.size,
@@ -65,6 +65,16 @@ $small-icon: var(--UT-button-icon-size-sm, 1.25rem);
65
65
  padding: var(--UT-button-padding-xs, 0.25rem) var(--UT-button-padding-sm, 0.5rem);
66
66
  }
67
67
 
68
+ .fullHeight {
69
+ min-height: unset;
70
+ height: 100%;
71
+ }
72
+
73
+ .fullWidth {
74
+ min-width: unset;
75
+ width: 100%;
76
+ }
77
+
68
78
  .hideTextOnResponsive {
69
79
  .textContainer {
70
80
  @media #{$mobile} {
@@ -50,7 +50,7 @@ const variantsColorTheme = (theme, colorTheme, variant) => {
50
50
  }
51
51
  },
52
52
  [_constants.VARIANTS_NAMES.outlined]: {
53
- border: "1px solid ".concat(actionColorName === _constants.COLORS_MAPPER.secondary ? neutralTheme['05'] : lightTheme['04']),
53
+ border: "1px solid ".concat(lightTheme['04']),
54
54
  fill: colorTheme === _constants.COLORS_MAPPER.gray ? actionTheme['04'] : actionTheme['05'],
55
55
  '&:hover': {
56
56
  backgroundColor: colorTheme === _constants.COLORS_MAPPER.gray ? lightTheme['03'] : actionTheme['01'],
@@ -15,7 +15,6 @@ const buildSingleIconProps = _ref => {
15
15
  let {
16
16
  className,
17
17
  colorTheme,
18
- hasContent,
19
18
  hideTextOnResponsive,
20
19
  Icon,
21
20
  size,
@@ -24,7 +23,7 @@ const buildSingleIconProps = _ref => {
24
23
  if (!Icon) return null;
25
24
  return {
26
25
  IconComponent: (0, _componentUtils.isUTIcon)(Icon) ? _UTIcon.default : Icon,
27
- className: "\n ".concat(_stylesModule.default.baseIcon, "\n ").concat(className, "\n ").concat(hasContent ? _stylesModule.default.icon : _stylesModule.default["iconOnly".concat((0, _componentUtils.capitalize)(size))], "\n ").concat(hideTextOnResponsive ? _stylesModule.default["adaptableIcon".concat((0, _componentUtils.capitalize)(size))] : '', "\n "),
26
+ className: "\n ".concat(_stylesModule.default.baseIcon, "\n ").concat(className, "\n ").concat(_stylesModule.default.icon, "\n ").concat(hideTextOnResponsive ? _stylesModule.default["adaptableIcon".concat((0, _componentUtils.capitalize)(size))] : '', "\n "),
28
27
  ...((0, _componentUtils.isUTIcon)(Icon) ? {
29
28
  colorTheme: getButtonElementColorTheme(colorTheme, variant),
30
29
  name: Icon
@@ -35,7 +34,6 @@ const getIconProps = _ref2 => {
35
34
  let {
36
35
  className,
37
36
  colorTheme,
38
- hasContent,
39
37
  hideTextOnResponsive,
40
38
  Icon,
41
39
  iconPlacement,
@@ -49,7 +47,6 @@ const getIconProps = _ref2 => {
49
47
  const commonParams = {
50
48
  className,
51
49
  colorTheme,
52
- hasContent,
53
50
  hideTextOnResponsive,
54
51
  size,
55
52
  variant
@@ -11,7 +11,7 @@ const backgroundColor = function (theme, area) {
11
11
  const colorTheme = areaProps.colorTheme || _Palette.COLOR_THEMES.light;
12
12
  const actionPaletteColors = ((_theme$Palette = theme.Palette) === null || _theme$Palette === void 0 ? void 0 : _theme$Palette.actions) || theme.Palette;
13
13
  const areaColor = actionPaletteColors[colorTheme];
14
- const shade = areaProps.shade || (_Palette.COLOR_THEMES.light === colorTheme ? '03' : '01');
14
+ const shade = areaProps.shade || (_Palette.COLOR_THEMES.light === colorTheme ? _Palette.COLOR_SHADES.shade03 : _Palette.COLOR_SHADES.shade01);
15
15
  return {
16
16
  backgroundColor: area ? areaColor[shade] : 'transparent'
17
17
  };
@@ -134,4 +134,4 @@ If the component has any `children`, it will automatically become collapsible wi
134
134
 
135
135
  ```javascript
136
136
  <UTDataElement area title="Highlighted Item" Data="Important information" Icon="IconStar" />
137
- ```
137
+ ```
@@ -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
+ }