@xh/hoist 87.1.1 → 87.3.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.
Files changed (270) hide show
  1. package/.npmignore +39 -0
  2. package/CHANGELOG.md +108 -20
  3. package/admin/AdminUtils.ts +64 -0
  4. package/admin/App.scss +88 -0
  5. package/admin/columns/UserData.ts +30 -3
  6. package/admin/detail/RestDetailModel.ts +64 -0
  7. package/admin/detail/RestDetailPanel.ts +106 -0
  8. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
  9. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
  11. package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
  12. package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
  13. package/admin/tabs/general/config/ConfigPanel.ts +41 -53
  14. package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
  15. package/admin/tabs/general/config/ConfigValue.scss +14 -0
  16. package/admin/tabs/general/config/ConfigValue.ts +104 -65
  17. package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
  18. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
  19. package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
  20. package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
  21. package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
  22. package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
  23. package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
  24. package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
  25. package/appcontainer/AboutDialogModel.ts +1 -0
  26. package/appcontainer/AppContainerModel.ts +3 -0
  27. package/appcontainer/AppStateModel.ts +1 -0
  28. package/appcontainer/BannerSourceModel.ts +1 -0
  29. package/appcontainer/ChangelogDialogModel.ts +1 -0
  30. package/appcontainer/ColChooserOptionsModel.ts +1 -0
  31. package/appcontainer/ExceptionDialogModel.ts +1 -0
  32. package/appcontainer/FeedbackDialogModel.ts +1 -0
  33. package/appcontainer/ImpersonationBarModel.ts +1 -0
  34. package/appcontainer/MessageSourceModel.ts +1 -0
  35. package/appcontainer/OptionsDialogModel.ts +1 -0
  36. package/appcontainer/PageStateModel.ts +1 -0
  37. package/appcontainer/RouterModel.ts +2 -0
  38. package/appcontainer/SizingModeModel.ts +1 -0
  39. package/appcontainer/ThemeModel.ts +1 -0
  40. package/appcontainer/ToastSourceModel.ts +1 -0
  41. package/appcontainer/UserAgentModel.ts +1 -0
  42. package/appcontainer/ViewportSizeModel.ts +1 -0
  43. package/build/types/admin/AdminUtils.d.ts +29 -0
  44. package/build/types/admin/columns/UserData.d.ts +3 -0
  45. package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
  46. package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
  47. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
  48. package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
  49. package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
  50. package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
  51. package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
  52. package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
  53. package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
  54. package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
  55. package/build/types/appcontainer/AppStateModel.d.ts +1 -0
  56. package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
  57. package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
  58. package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
  59. package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
  60. package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
  61. package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
  62. package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
  63. package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
  64. package/build/types/appcontainer/PageStateModel.d.ts +1 -0
  65. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  66. package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
  67. package/build/types/appcontainer/ThemeModel.d.ts +1 -0
  68. package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
  69. package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
  70. package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
  71. package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
  72. package/build/types/cmp/card/Card.d.ts +2 -2
  73. package/build/types/cmp/chart/ChartModel.d.ts +2 -0
  74. package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
  75. package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
  76. package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
  77. package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
  78. package/build/types/cmp/daterange/Types.d.ts +152 -0
  79. package/build/types/cmp/daterange/index.d.ts +4 -0
  80. package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
  81. package/build/types/cmp/form/FormModel.d.ts +3 -1
  82. package/build/types/cmp/grid/GridModel.d.ts +2 -0
  83. package/build/types/cmp/grid/Types.d.ts +2 -0
  84. package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
  85. package/build/types/cmp/grid/impl/ColumnWidthCalculator.d.ts +1 -1
  86. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
  87. package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
  88. package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
  89. package/build/types/cmp/tab/TabModel.d.ts +3 -1
  90. package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
  91. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
  92. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
  93. package/build/types/core/HoistBase.d.ts +7 -0
  94. package/build/types/core/HoistProps.d.ts +26 -1
  95. package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
  96. package/build/types/core/types/Telemetry.d.ts +1 -1
  97. package/build/types/data/Store.d.ts +3 -1
  98. package/build/types/data/StoreSelectionModel.d.ts +3 -1
  99. package/build/types/data/cube/Cube.d.ts +5 -2
  100. package/build/types/data/cube/View.d.ts +2 -0
  101. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
  102. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
  103. package/build/types/desktop/cmp/button/Button.d.ts +2 -2
  104. package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
  105. package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
  106. package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
  107. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
  108. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
  109. package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
  110. package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
  111. package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
  112. package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
  113. package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
  114. package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
  115. package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
  116. package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
  117. package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
  118. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
  119. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
  120. package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
  121. package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
  122. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
  123. package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
  124. package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
  125. package/build/types/desktop/cmp/input/index.d.ts +1 -0
  126. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
  127. package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
  128. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
  129. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  130. package/build/types/desktop/hooks/UseHotkeys.d.ts +4 -3
  131. package/build/types/icon/Icon.d.ts +3 -0
  132. package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
  133. package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
  134. package/build/types/kit/react-day-picker/index.d.ts +3 -3
  135. package/build/types/mobile/cmp/button/Button.d.ts +2 -2
  136. package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
  137. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
  138. package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
  139. package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
  140. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +8 -2
  141. package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
  142. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  143. package/build/types/svc/InspectorService.d.ts +1 -0
  144. package/build/types/utils/js/LangUtils.d.ts +24 -5
  145. package/cmp/README.md +1 -0
  146. package/cmp/ag-grid/AgGridModel.ts +5 -0
  147. package/cmp/badge/Badge.ts +5 -3
  148. package/cmp/card/Card.ts +4 -3
  149. package/cmp/chart/ChartModel.ts +11 -1
  150. package/cmp/clock/Clock.ts +1 -1
  151. package/cmp/dataview/DataViewModel.ts +7 -1
  152. package/cmp/daterange/DateRangePickerModel.ts +628 -0
  153. package/cmp/daterange/DateRangePresets.ts +274 -0
  154. package/cmp/daterange/DateRangeUtils.ts +557 -0
  155. package/cmp/daterange/README.md +434 -0
  156. package/cmp/daterange/Types.ts +203 -0
  157. package/cmp/daterange/index.ts +10 -0
  158. package/cmp/filter/FilterChooserModel.ts +7 -4
  159. package/cmp/form/FormModel.ts +6 -0
  160. package/cmp/form/README.md +1 -0
  161. package/cmp/grid/GridModel.ts +24 -4
  162. package/cmp/grid/Types.ts +3 -0
  163. package/cmp/grid/filter/GridFilterModel.ts +3 -1
  164. package/cmp/grid/impl/ColumnWidthCalculator.ts +5 -10
  165. package/cmp/grouping/GroupingChooserModel.ts +6 -1
  166. package/cmp/input/HoistInputProps.ts +50 -2
  167. package/cmp/input/README.md +42 -0
  168. package/cmp/layout/Box.ts +3 -2
  169. package/cmp/tab/TabContainerModel.ts +9 -1
  170. package/cmp/tab/TabModel.ts +5 -0
  171. package/cmp/treemap/SplitTreeMapModel.ts +12 -2
  172. package/cmp/treemap/TreeMapModel.ts +6 -1
  173. package/cmp/viewmanager/ViewManagerModel.ts +6 -1
  174. package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
  175. package/core/HoistBase.ts +11 -0
  176. package/core/HoistProps.ts +27 -0
  177. package/core/README.md +46 -0
  178. package/core/impl/InstallServices.ts +5 -1
  179. package/core/model/RootRefreshContextModel.ts +2 -0
  180. package/core/types/Telemetry.ts +1 -1
  181. package/data/README.md +3 -1
  182. package/data/Store.ts +7 -0
  183. package/data/StoreSelectionModel.ts +5 -1
  184. package/data/cube/Cube.ts +12 -3
  185. package/data/cube/View.ts +5 -1
  186. package/desktop/README.md +2 -0
  187. package/desktop/appcontainer/LoginPanel.ts +2 -0
  188. package/desktop/cmp/button/Button.scss +71 -42
  189. package/desktop/cmp/button/Button.ts +10 -1
  190. package/desktop/cmp/button/ButtonGroup.ts +4 -1
  191. package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
  192. package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
  193. package/desktop/cmp/dash/DashConfig.ts +3 -0
  194. package/desktop/cmp/dash/DashViewModel.ts +14 -1
  195. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
  196. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
  197. package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
  198. package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
  199. package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
  200. package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
  201. package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
  202. package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
  203. package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
  204. package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
  205. package/desktop/cmp/daterange/index.ts +8 -0
  206. package/desktop/cmp/dock/DockContainerModel.ts +6 -1
  207. package/desktop/cmp/dock/DockViewModel.ts +6 -1
  208. package/desktop/cmp/filechooser/FileChooser.ts +4 -0
  209. package/desktop/cmp/form/FormField.ts +1 -0
  210. package/desktop/cmp/grid/editors/DateEditor.ts +11 -2
  211. package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
  212. package/desktop/cmp/input/Checkbox.ts +1 -0
  213. package/desktop/cmp/input/CodeInput.ts +1 -0
  214. package/desktop/cmp/input/DateInput.scss +1 -1
  215. package/desktop/cmp/input/DateInput.ts +1 -0
  216. package/desktop/cmp/input/IntentInput.scss +202 -0
  217. package/desktop/cmp/input/IntentInput.ts +213 -0
  218. package/desktop/cmp/input/NumberInput.ts +11 -2
  219. package/desktop/cmp/input/Picker.ts +1 -0
  220. package/desktop/cmp/input/RadioInput.ts +2 -1
  221. package/desktop/cmp/input/SegmentedControl.scss +57 -5
  222. package/desktop/cmp/input/SegmentedControl.ts +66 -11
  223. package/desktop/cmp/input/Select.ts +9 -7
  224. package/desktop/cmp/input/Slider.ts +2 -0
  225. package/desktop/cmp/input/SwitchInput.ts +1 -0
  226. package/desktop/cmp/input/TextArea.ts +11 -2
  227. package/desktop/cmp/input/TextInput.ts +11 -2
  228. package/desktop/cmp/input/index.ts +1 -0
  229. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
  230. package/desktop/cmp/panel/Panel.ts +8 -5
  231. package/desktop/cmp/panel/PanelModel.ts +8 -0
  232. package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
  233. package/desktop/cmp/rest/RestGridModel.ts +4 -0
  234. package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
  235. package/desktop/cmp/toolbar/Toolbar.scss +20 -6
  236. package/desktop/hooks/UseHotkeys.ts +41 -12
  237. package/docs/README.md +2 -0
  238. package/docs/build-and-publish.md +23 -7
  239. package/docs/doc-registry.json +8 -0
  240. package/docs/version-compatibility.md +2 -0
  241. package/icon/Icon.ts +9 -0
  242. package/icon/index.ts +24 -0
  243. package/inspector/README.md +8 -2
  244. package/inspector/impl/InspectorUtils.ts +12 -0
  245. package/inspector/instances/DiagnosticsModel.ts +3 -2
  246. package/inspector/instances/InstancesModel.ts +81 -14
  247. package/inspector/instances/InstancesPanel.ts +6 -0
  248. package/kit/react-day-picker/index.ts +3 -3
  249. package/mobile/appcontainer/LoginPanel.ts +2 -0
  250. package/mobile/cmp/button/Button.ts +16 -3
  251. package/mobile/cmp/form/FormField.ts +1 -0
  252. package/mobile/cmp/input/Checkbox.ts +1 -0
  253. package/mobile/cmp/input/DateInput.ts +1 -0
  254. package/mobile/cmp/input/Label.ts +13 -8
  255. package/mobile/cmp/input/NumberInput.ts +11 -2
  256. package/mobile/cmp/input/SearchInput.ts +1 -0
  257. package/mobile/cmp/input/SegmentedControl.scss +36 -6
  258. package/mobile/cmp/input/SegmentedControl.ts +37 -11
  259. package/mobile/cmp/input/Select.ts +2 -0
  260. package/mobile/cmp/input/SwitchInput.ts +1 -0
  261. package/mobile/cmp/input/TextArea.ts +11 -2
  262. package/mobile/cmp/input/TextInput.ts +11 -2
  263. package/mobile/cmp/navigator/NavigatorModel.ts +20 -8
  264. package/mobile/cmp/navigator/PageModel.ts +6 -1
  265. package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
  266. package/package.json +19 -19
  267. package/styles/vars.scss +32 -2
  268. package/svc/InspectorService.ts +2 -0
  269. package/svc/TraceService.ts +5 -2
  270. package/utils/js/LangUtils.ts +37 -5
@@ -0,0 +1,213 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
+ import {div} from '@xh/hoist/cmp/layout';
9
+ import {elementFactory, hoistCmp, HoistProps, Intent} from '@xh/hoist/core';
10
+ import '@xh/hoist/desktop/register';
11
+ import {Icon} from '@xh/hoist/icon';
12
+ import {computed, makeObservable} from '@xh/hoist/mobx';
13
+ import {getTestId, TEST_ID} from '@xh/hoist/utils/js';
14
+ import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react';
15
+ import classNames from 'classnames';
16
+ import {capitalize, filter, isEmpty} from 'lodash';
17
+ import {KeyboardEvent} from 'react';
18
+ import './IntentInput.scss';
19
+
20
+ export const INTENTS: Intent[] = ['primary', 'success', 'warning', 'danger'];
21
+
22
+ export interface IntentInputProps extends HoistProps, HoistInputProps {
23
+ /** True to render in a compact mode with reduced sizing for space-constrained contexts. */
24
+ compact?: boolean;
25
+
26
+ /**
27
+ * True to offer a null value, rendered as an additional outlined swatch ahead of the intents
28
+ * and holding the check while the value is null. Defaults to false, leaving the control a
29
+ * required single-select.
30
+ */
31
+ enableClear?: boolean;
32
+
33
+ /**
34
+ * Intents to offer, in render order. Defaults to all four Hoist intents - specify a subset
35
+ * to restrict or reorder the available choices.
36
+ */
37
+ intents?: Intent[];
38
+
39
+ /**
40
+ * True to render each intent's name alongside its swatch. Defaults to false, for the most
41
+ * compact presentation - note swatches always carry their intent name as a tooltip and as
42
+ * their accessible label, so an unlabelled control remains identifiable.
43
+ */
44
+ showNames?: boolean;
45
+ }
46
+
47
+ /**
48
+ * An input for selecting a Hoist Intent, rendered as a compact row of color swatches drawn in
49
+ * the intent colors themselves. The selected swatch is marked with a check and a surrounding
50
+ * ring; names can optionally be rendered alongside each swatch.
51
+ *
52
+ * Set `enableClear` to offer a null value, rendered as an additional outlined swatch ahead of
53
+ * the intents.
54
+ *
55
+ * Designed to fit within a toolbar or a dense form without additional chrome. Swatches are
56
+ * rendered as an ARIA radiogroup, supporting selection via click, enter/space, and arrow keys.
57
+ */
58
+ export const [IntentInput, intentInput] = hoistCmp.withFactory<IntentInputProps>({
59
+ displayName: 'IntentInput',
60
+ className: 'xh-intent-input',
61
+ render(props, ref) {
62
+ return useHoistInputModel(cmp, props, ref, IntentInputModel);
63
+ }
64
+ });
65
+ (IntentInput as any).hasLayoutSupport = true;
66
+
67
+ //-----------------------
68
+ // Implementation
69
+ //-----------------------
70
+ const buttonEl = elementFactory('button');
71
+
72
+ class IntentInputModel extends HoistInputModel {
73
+ override xhImpl = true;
74
+
75
+ @computed
76
+ get intents(): Intent[] {
77
+ const {intents} = this.componentProps;
78
+ return isEmpty(intents) ? INTENTS : intents;
79
+ }
80
+
81
+ /**
82
+ * Selectable values in render order - the offered intents, preceded by a null entry when
83
+ * `enableClear` is set. Null is a value here like any other, so it selects, renders a check,
84
+ * and takes part in keyboard nav without any special-casing.
85
+ */
86
+ @computed
87
+ get options(): Intent[] {
88
+ const {intents} = this;
89
+ return this.componentProps.enableClear ? [null, ...intents] : intents;
90
+ }
91
+
92
+ get enabledButtons(): HTMLButtonElement[] {
93
+ const btns = this.domEl?.querySelectorAll('button') ?? [];
94
+ return filter(btns, (b: HTMLButtonElement) => !b.disabled) as HTMLButtonElement[];
95
+ }
96
+
97
+ constructor() {
98
+ super();
99
+ makeObservable(this);
100
+ }
101
+
102
+ onSwatchClick = (intent: Intent) => {
103
+ this.noteValueChange(intent);
104
+ };
105
+
106
+ /**
107
+ * Arrow keys move the selection to the adjacent option, as expected of a radiogroup. Wraps
108
+ * around at either end, and starts from the first option when the current value is not among
109
+ * those offered.
110
+ */
111
+ onKeyDown = (e: KeyboardEvent) => {
112
+ const delta = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : null;
113
+ if (delta == null) return;
114
+
115
+ const {options, renderValue} = this,
116
+ currIdx = options.indexOf(renderValue);
117
+
118
+ e.preventDefault();
119
+
120
+ const nextIdx = currIdx === -1 ? 0 : (currIdx + delta + options.length) % options.length;
121
+ this.noteValueChange(options[nextIdx]);
122
+ this.enabledButtons[nextIdx]?.focus();
123
+ };
124
+
125
+ override blur() {
126
+ this.enabledButtons.forEach(it => it.blur());
127
+ }
128
+
129
+ override focus() {
130
+ this.enabledButtons[0]?.focus();
131
+ }
132
+ }
133
+
134
+ const cmp = hoistCmp.factory<IntentInputModel>(({model, className, ...props}, ref) => {
135
+ const {
136
+ // HoistInput props - exclude from passthrough to the container
137
+ bind,
138
+ disabled,
139
+ onChange,
140
+ onCommit,
141
+ tabIndex,
142
+ value,
143
+ commitOnChange,
144
+ // Consumed by model
145
+ enableClear,
146
+ intents,
147
+ // Consumed by this component
148
+ compact,
149
+ showNames,
150
+ testId,
151
+ domAttrs,
152
+ // Remainder passed to the container div
153
+ ...rest
154
+ } = getNonLayoutProps(props);
155
+
156
+ const {renderValue} = model;
157
+
158
+ return div({
159
+ ...rest,
160
+ className: classNames(
161
+ className,
162
+ compact && 'xh-intent-input--compact',
163
+ showNames && 'xh-intent-input--with-names'
164
+ ),
165
+ ref,
166
+ role: 'radiogroup',
167
+ onFocus: model.onFocus,
168
+ onBlur: model.onBlur,
169
+ onKeyDown: disabled ? null : model.onKeyDown,
170
+ ...getLayoutProps(props),
171
+ [TEST_ID]: testId,
172
+ ...domAttrs,
173
+ items: model.options.map(intent => {
174
+ const isSelected = renderValue === intent,
175
+ isNull = intent == null,
176
+ name = isNull ? 'None' : capitalize(intent);
177
+
178
+ return buttonEl({
179
+ key: intent ?? 'none',
180
+ type: 'button',
181
+ className: classNames(
182
+ 'xh-intent-input__swatch',
183
+ `xh-intent-input__swatch--${intent ?? 'none'}`,
184
+ isSelected && 'xh-intent-input__swatch--selected'
185
+ ),
186
+ role: 'radio',
187
+ 'aria-checked': isSelected,
188
+ 'aria-label': name,
189
+ title: name,
190
+ disabled,
191
+ tabIndex,
192
+ onClick: () => model.onSwatchClick(intent),
193
+ [TEST_ID]: getTestId(testId, intent ?? 'none'),
194
+ items: [
195
+ div({
196
+ className: 'xh-intent-input__swatch__fill',
197
+ // Check is always rendered, and hidden via `visibility` when unselected.
198
+ // Omitting it instead would change the swatch's baseline, which shifts
199
+ // baseline-aligned siblings (e.g. an inline FormField label) on every
200
+ // selection change - see the note in IntentInput.scss.
201
+ item: Icon.check({
202
+ className: classNames(
203
+ 'xh-intent-input__swatch__check',
204
+ !isSelected && 'xh-intent-input__swatch__check--hidden'
205
+ )
206
+ })
207
+ }),
208
+ div({omit: !showNames, className: 'xh-intent-input__swatch__name', item: name})
209
+ ]
210
+ });
211
+ })
212
+ });
213
+ });
@@ -4,7 +4,13 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
7
+ import {
8
+ getPasswordManagerAttrs,
9
+ HoistInputModel,
10
+ HoistInputProps,
11
+ PasswordManagerSupportProps,
12
+ useHoistInputModel
13
+ } from '@xh/hoist/cmp/input';
8
14
  import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
9
15
  import '@xh/hoist/desktop/register';
10
16
  import {fmtNumber, NumericPrecision, parseNumber, Precision, ZeroPad} from '@xh/hoist/format';
@@ -16,7 +22,8 @@ import type {Property} from 'csstype';
16
22
  import {debounce, isNaN, isNil, isNumber, round} from 'lodash';
17
23
  import {KeyboardEventHandler, ReactElement, ReactNode, Ref, useLayoutEffect} from 'react';
18
24
 
19
- export interface NumberInputProps extends HoistProps, LayoutProps, StyleProps, HoistInputProps {
25
+ export interface NumberInputProps
26
+ extends HoistProps, LayoutProps, StyleProps, HoistInputProps, PasswordManagerSupportProps {
20
27
  value?: number;
21
28
 
22
29
  /** True to focus the control on render. */
@@ -278,6 +285,8 @@ const cmp = hoistCmp.factory<NumberInputModel>(({model, className, ...props}, re
278
285
  textAlign: withDefault(props.textAlign, 'right')
279
286
  },
280
287
  [TEST_ID]: props.testId,
288
+ ...getPasswordManagerAttrs(props.enablePasswordManagers),
289
+ ...props.domAttrs,
281
290
  onBlur: model.onBlur,
282
291
  onFocus: model.onFocus,
283
292
  onKeyDown: model.onKeyDown,
@@ -478,6 +478,7 @@ const triggerButton = hoistCmp.factory<PickerModel>(({model, props, nothingSelec
478
478
  width: withDefault(width, 160),
479
479
  style: props.style,
480
480
  [TEST_ID]: getTestId(props, 'trigger'),
481
+ ...props.domAttrs,
481
482
  onClick: () => {
482
483
  if (model.popoverIsOpen) {
483
484
  model.closePopover();
@@ -113,6 +113,7 @@ const cmp = hoistCmp.factory<RadioInputModel>(({model, className, ...props}, ref
113
113
  inline: props.inline,
114
114
  selectedValue: model.renderValue,
115
115
  onChange: model.onChange,
116
- [TEST_ID]: props.testId
116
+ [TEST_ID]: props.testId,
117
+ ...props.domAttrs
117
118
  });
118
119
  });
@@ -59,20 +59,71 @@
59
59
  }
60
60
  }
61
61
 
62
- // Outlined mode - border around control tray with no inner background.
62
+ // Outlined mode (default) - a border around the control tray. Independent of the tray fill,
63
+ // which is governed by --no-tray-bg below.
63
64
  &--outlined .bp6-segmented-control {
64
- background-color: transparent;
65
65
  border: 1px solid var(--xh-border-color);
66
66
  }
67
67
 
68
+ // Not filling - shrink the tray to its options. The tray is a block-level flex container, so it
69
+ // spans its parent regardless; BP's fill class governs only whether the options themselves grow.
70
+ // Without this, an unfilled control leaves an empty run of tray to the right of its options.
71
+ &:not(#{&}--fill) .bp6-segmented-control {
72
+ width: fit-content;
73
+ }
74
+
75
+ // Equal segment widths - zeroing the flex basis makes each option take an identical share of the
76
+ // tray. BP's flex-grow alone divides only the leftover space, leaving options at differing widths.
77
+ // The min-width keeps that equal share from shrinking an option below its own label. A zero
78
+ // basis otherwise lets each option fall to BP's fixed 30px floor, which is unrelated to its
79
+ // content: the horizontal padding is eaten first, then a single-word label spills past the
80
+ // border, since it can neither wrap nor ellipsize. Options only stop sharing equally once there
81
+ // is genuinely not enough room, and then only the ones that need the space keep it.
82
+ &--fill#{&}--equal-widths .bp6-segmented-control > .bp6-button {
83
+ flex-basis: 0;
84
+ min-width: min-content;
85
+ }
86
+
87
+ // Tray background suppressed - a lighter presentation, with no fill behind the options.
88
+ &--no-tray-bg .bp6-segmented-control {
89
+ background-color: transparent;
90
+ }
91
+
92
+ // Option dividers - internal structure for a tray whose options are otherwise undifferentiated,
93
+ // most notably in the empty state where no selected option breaks up the row. Drawn in the gap
94
+ // to the left of each option after the first, and suppressed on either side of the selected
95
+ // option so they never crowd it.
96
+ &--dividers .bp6-segmented-control {
97
+ > .bp6-button {
98
+ position: relative;
99
+ }
100
+
101
+ > .bp6-button + .bp6-button::before {
102
+ content: '';
103
+ position: absolute;
104
+ left: calc(var(--xh-segmented-control-pad-px) / -2);
105
+ top: 20%;
106
+ bottom: 20%;
107
+ width: 1px;
108
+ background-color: var(--xh-border-color);
109
+ }
110
+
111
+ > .bp6-button:not(.bp6-minimal)::before,
112
+ > .bp6-button:not(.bp6-minimal) + .bp6-button::before {
113
+ display: none;
114
+ }
115
+ }
116
+
68
117
  @each $intent in (primary, success, warning, danger) {
69
118
  &--outlined#{&}--#{$intent} .bp6-segmented-control {
70
119
  border-color: var(--xh-intent-#{$intent});
71
120
  }
72
121
  }
73
122
 
74
- // Compact mode - reduced sizing for toolbars and space-constrained contexts.
75
- &--compact .bp6-segmented-control {
123
+ // Compact mode - via the `compact` prop, or implicitly within a compact Toolbar, whose
124
+ // item height the standard sizing above would overflow.
125
+ &--compact .bp6-segmented-control,
126
+ .xh-toolbar--compact & .bp6-segmented-control {
76
127
  padding: 0;
77
128
  gap: 0;
78
129
 
@@ -89,7 +140,8 @@
89
140
  }
90
141
 
91
142
  // Compact + outlined - shrink buttons to account for the border.
92
- &--compact#{&}--outlined .bp6-segmented-control > .bp6-button {
143
+ &--compact#{&}--outlined .bp6-segmented-control > .bp6-button,
144
+ .xh-toolbar--compact &#{&}--outlined .bp6-segmented-control > .bp6-button {
93
145
  --compact-btn-h: calc(var(--xh-tbar-compact-item-height-px) - 2px);
94
146
  min-height: var(--compact-btn-h);
95
147
  max-height: var(--compact-btn-h);
@@ -28,8 +28,17 @@ export interface SegmentedControlProps extends HoistProps, HoistInputProps {
28
28
  compact?: boolean;
29
29
 
30
30
  /**
31
- * True (default) to stretch the control to fill available width,
32
- * distributing space equally among options.
31
+ * True (default) to render all segments at an equal width when {@link fill} is enabled,
32
+ * dividing the available width into equal parts regardless of label length - the conventional
33
+ * segmented-control appearance. Set false to instead size each segment to its content and
34
+ * share only the leftover space, so a longer label yields a wider segment. No effect when
35
+ * `fill` is false.
36
+ */
37
+ equalSegmentWidths?: boolean;
38
+
39
+ /**
40
+ * True (default) to stretch the control to fill available width. Set false to size the control
41
+ * to its options. See `equalSegmentWidths` for how the filled width is divided up.
33
42
  */
34
43
  fill?: boolean;
35
44
 
@@ -48,10 +57,25 @@ export interface SegmentedControlProps extends HoistProps, HoistInputProps {
48
57
  options: Array<SegmentedControlOption | SegmentedControlNullOption | OptionPrimitive>;
49
58
 
50
59
  /**
51
- * True to render with an outlined style - a border around the control tray
52
- * with no inner background fill. Border color follows the current intent.
60
+ * True (default) to render a border around the control tray. Border color follows the current
61
+ * intent. Note this controls the tray border only - it does not remove the tray background,
62
+ * which is governed independently by `showTrayBackground`.
53
63
  */
54
64
  outlined?: boolean;
65
+
66
+ /**
67
+ * True (default) to fill the control tray with a background, setting it off from the surface
68
+ * behind it. Set false to omit that fill for a lighter presentation.
69
+ */
70
+ showTrayBackground?: boolean;
71
+
72
+ /**
73
+ * True to render dividers between options, adding internal structure to the tray. Dividers
74
+ * are suppressed on either side of the selected option so they do not compete with it.
75
+ * Defaults to 'auto', showing dividers only while no option is selected - the state in which
76
+ * the tray offers no other cue that its options are separate and selectable.
77
+ */
78
+ showOptionDividers?: boolean | 'auto';
55
79
  }
56
80
 
57
81
  /**
@@ -77,6 +101,14 @@ export const [SegmentedControl, segmentedControl] = hoistCmp.withFactory<Segment
77
101
  //-----------------------
78
102
  // Implementation
79
103
  //-----------------------
104
+
105
+ /**
106
+ * Key handed to the Blueprint control when no option is selected. Option keys are always
107
+ * `String(index)`, so this cannot collide with a real option - and unlike a nullish value, it
108
+ * keeps Blueprint from falling back to its own internal selection state. See usage note below.
109
+ */
110
+ const NO_SELECTION_KEY = '__xh_no_selection__';
111
+
80
112
  interface NormalizedOption extends SegmentedControlOption {
81
113
  label: string;
82
114
  intent?: Intent;
@@ -109,11 +141,15 @@ class SegmentedControlModel extends HoistInputModel {
109
141
  });
110
142
  }
111
143
 
112
- /** Map the current render value to the string key used by the Blueprint control. */
144
+ /**
145
+ * Key of the option matching the current render value, or null if no option matches -
146
+ * including whenever the bound value is itself null. Note this is the model's own view of
147
+ * the selection: the key actually handed to Blueprint is resolved at its point of use below.
148
+ */
113
149
  @computed
114
- get selectedKey(): string {
150
+ get selectedKey(): string | null {
115
151
  const {renderValue, normalizedOptions} = this;
116
- return normalizedOptions.find(o => o.value === renderValue)?._key;
152
+ return normalizedOptions.find(o => o.value === renderValue)?._key ?? null;
117
153
  }
118
154
 
119
155
  get enabledButtons(): HTMLButtonElement[] {
@@ -154,13 +190,21 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
154
190
  options,
155
191
  // Consumed by this component
156
192
  compact,
193
+ equalSegmentWidths = true,
194
+ fill = true,
157
195
  intent,
158
- outlined,
196
+ outlined = true,
197
+ showTrayBackground = true,
198
+ showOptionDividers = 'auto',
159
199
  testId,
200
+ domAttrs,
160
201
  // Remainder passed to BP SegmentedControl
161
202
  ...bpProps
162
203
  } = getNonLayoutProps(props);
163
204
 
205
+ // 'auto' dividers track the empty state - shown only while nothing is selected.
206
+ const dividers = showOptionDividers === 'auto' ? model.selectedKey == null : showOptionDividers;
207
+
164
208
  // Resolve the effective intent per option (own intent wins over control-level default),
165
209
  // applied via a per-button className that our SCSS keys its solid/hint coloring off of.
166
210
  const defaultIntent = intent && intent !== 'none' ? intent : null,
@@ -181,20 +225,31 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
181
225
  className: classNames(
182
226
  className,
183
227
  compact && 'xh-segmented-control--compact',
228
+ fill && 'xh-segmented-control--fill',
229
+ fill && equalSegmentWidths && 'xh-segmented-control--equal-widths',
184
230
  defaultIntent && `xh-segmented-control--${defaultIntent}`,
185
- outlined && 'xh-segmented-control--outlined'
231
+ outlined && 'xh-segmented-control--outlined',
232
+ !showTrayBackground && 'xh-segmented-control--no-tray-bg',
233
+ dividers && 'xh-segmented-control--dividers'
186
234
  ),
187
235
  ref,
188
236
  onFocus: model.onFocus,
189
237
  onBlur: model.onBlur,
190
238
  ...getLayoutProps(props),
191
239
  [TEST_ID]: props.testId,
240
+ ...domAttrs,
192
241
  item: bpSegmentedControl({
193
242
  ...bpProps,
194
- fill: bpProps.fill ?? true,
243
+ fill,
195
244
  size: compact ? 'small' : undefined,
196
245
  options: bpOptions,
197
- value: model.selectedKey,
246
+ // Blueprint resolves its selection as `value ?? internalState`, where the internal
247
+ // state is set on every click even when controlled. Passing a nullish value for a
248
+ // selection matching no option would fall back to the last-clicked segment,
249
+ // leaving it highlighted after the bound value is cleared. Pass a sentinel key
250
+ // instead, keeping the control fully controlled - Blueprint handles a value matching
251
+ // no option natively (see `isAnySelected`) and emits no warning.
252
+ value: model.selectedKey ?? NO_SELECTION_KEY,
198
253
  onValueChange: model.onValueChange,
199
254
  disabled
200
255
  })
@@ -27,7 +27,7 @@ import {
27
27
  reactWindowedSelect
28
28
  } from '@xh/hoist/kit/react-select';
29
29
  import {action, bindable, makeObservable, observable, override} from '@xh/hoist/mobx';
30
- import {debouncePromise, wait} from '@xh/hoist/promise';
30
+ import {debouncePromise, wait, waitFor} from '@xh/hoist/promise';
31
31
  import {elemWithin, getTestId, mergeDeep, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
32
32
  import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
33
33
  import classNames from 'classnames';
@@ -820,13 +820,14 @@ const cmp = hoistCmp.factory<SelectInputModel>(({model, className, ...props}, re
820
820
  rsProps.inputValue = model.inputValue || '';
821
821
  rsProps.onInputChange = model.onInputChange;
822
822
  rsProps.controlShouldRenderValue = !model.hasFocus;
823
+ }
824
+
825
+ // Scroll selected option into view on open - react-select's own does not fire for portalled menus.
826
+ if (!model.multiMode && model.renderValue) {
823
827
  rsProps.onMenuOpen = () => {
824
- wait().then(() => {
825
- const selectedEl = document.getElementsByClassName(
826
- 'xh-select__option--is-selected'
827
- )[0];
828
- selectedEl?.scrollIntoView({block: 'end'});
829
- });
828
+ const getSel = () =>
829
+ document.getElementsByClassName('xh-select__option--is-selected')[0];
830
+ waitFor(() => !!getSel()).then(() => getSel().scrollIntoView({block: 'end'}));
830
831
  };
831
832
  }
832
833
 
@@ -874,6 +875,7 @@ const cmp = hoistCmp.factory<SelectInputModel>(({model, className, ...props}, re
874
875
  }
875
876
  },
876
877
  testId: props.testId,
878
+ domAttrs: props.domAttrs,
877
879
  ...layoutProps,
878
880
  width: withDefault(width, 200),
879
881
  height: height,
@@ -114,6 +114,8 @@ const cmp = hoistCmp.factory<SliderInputModel>(({model, className, ...props}, re
114
114
  ...layoutProps,
115
115
  width: withDefault(width, 200),
116
116
  className,
117
+ testId: props.testId,
118
+ domAttrs: props.domAttrs,
117
119
 
118
120
  onBlur: model.onBlur,
119
121
  onFocus: model.onFocus,
@@ -63,6 +63,7 @@ const cmp = hoistCmp.factory<SwitchInputModel>(({model, className, ...props}, re
63
63
  className,
64
64
 
65
65
  [TEST_ID]: props.testId,
66
+ ...props.domAttrs,
66
67
  onBlur: model.onBlur,
67
68
  onFocus: model.onFocus,
68
69
  onChange: e => model.noteValueChange(e.target.checked),
@@ -4,7 +4,13 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
7
+ import {
8
+ getPasswordManagerAttrs,
9
+ HoistInputModel,
10
+ HoistInputProps,
11
+ PasswordManagerSupportProps,
12
+ useHoistInputModel
13
+ } from '@xh/hoist/cmp/input';
8
14
  import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
9
15
  import '@xh/hoist/desktop/register';
10
16
  import {textArea as bpTextarea} from '@xh/hoist/kit/blueprint';
@@ -13,7 +19,8 @@ import {useComposedRefs, getLayoutProps} from '@xh/hoist/utils/react';
13
19
  import {Ref} from 'react';
14
20
  import './TextArea.scss';
15
21
 
16
- export interface TextAreaProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps {
22
+ export interface TextAreaProps
23
+ extends HoistProps, HoistInputProps, PasswordManagerSupportProps, LayoutProps, StyleProps {
17
24
  value?: string;
18
25
 
19
26
  /** True to focus the control on render. */
@@ -91,6 +98,8 @@ const cmp = hoistCmp.factory<TextAreaInputModel>(({model, className, ...props},
91
98
  spellCheck: withDefault(props.spellCheck, false),
92
99
  tabIndex: props.tabIndex,
93
100
  [TEST_ID]: props.testId,
101
+ ...getPasswordManagerAttrs(props.enablePasswordManagers),
102
+ ...props.domAttrs,
94
103
  id: props.id,
95
104
  className,
96
105
  style: {
@@ -4,7 +4,13 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
7
+ import {
8
+ getPasswordManagerAttrs,
9
+ HoistInputModel,
10
+ HoistInputProps,
11
+ PasswordManagerSupportProps,
12
+ useHoistInputModel
13
+ } from '@xh/hoist/cmp/input';
8
14
  import {div} from '@xh/hoist/cmp/layout';
9
15
  import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
10
16
  import {button} from '@xh/hoist/desktop/cmp/button';
@@ -17,7 +23,8 @@ import type {Property} from 'csstype';
17
23
  import {isEmpty} from 'lodash';
18
24
  import {FocusEvent, KeyboardEventHandler, ReactElement, ReactNode, Ref} from 'react';
19
25
 
20
- export interface TextInputProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps {
26
+ export interface TextInputProps
27
+ extends HoistProps, HoistInputProps, PasswordManagerSupportProps, LayoutProps, StyleProps {
21
28
  value?: string;
22
29
 
23
30
  /**
@@ -155,6 +162,8 @@ const cmp = hoistCmp.factory<TextInputProps & {model: TextInputModel}>(
155
162
  textAlign: withDefault(props.textAlign, 'left')
156
163
  },
157
164
  [TEST_ID]: props.testId,
165
+ ...getPasswordManagerAttrs(props.enablePasswordManagers),
166
+ ...props.domAttrs,
158
167
  onChange: model.onChange,
159
168
  onKeyDown: model.onKeyDown
160
169
  }),
@@ -3,6 +3,7 @@ export * from './Checkbox';
3
3
  export * from './CheckboxButton';
4
4
  export * from './CodeInput';
5
5
  export * from './DateInput';
6
+ export * from './IntentInput';
6
7
  export * from './JsonInput';
7
8
  export * from './NumberInput';
8
9
  export * from './Picker';
@@ -49,6 +49,9 @@ export interface LeftRightChooserConfig {
49
49
 
50
50
  /** @internal */
51
51
  xhImpl?: boolean;
52
+
53
+ /** See {@link HoistBase.xhName}. */
54
+ xhName?: string;
52
55
  }
53
56
 
54
57
  /** Data record object for a LeftRightChooser value item. */
@@ -146,11 +149,13 @@ export class LeftRightChooserModel extends HoistModel {
146
149
  rightGroupingExpanded = true,
147
150
  rightEmptyText = null,
148
151
  showCounts = true,
152
+ xhName = null,
149
153
  xhImpl = false
150
154
  }: LeftRightChooserConfig) {
151
155
  super();
152
156
  makeObservable(this);
153
157
  this.xhImpl = xhImpl;
158
+ this.xhName = xhName;
154
159
 
155
160
  this.onChange = onChange;
156
161
  this._ungroupedName = ungroupedName;