@xh/hoist 59.2.0 → 59.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 (104) hide show
  1. package/CHANGELOG.md +63 -14
  2. package/admin/AppComponent.ts +1 -1
  3. package/admin/tabs/activity/ActivityTab.ts +1 -1
  4. package/admin/tabs/general/GeneralTab.ts +2 -2
  5. package/admin/tabs/general/config/ConfigPanel.ts +1 -0
  6. package/admin/tabs/monitor/MonitorTab.ts +1 -1
  7. package/admin/tabs/server/ServerTab.ts +1 -1
  8. package/admin/tabs/userData/UserDataTab.ts +1 -1
  9. package/cmp/ag-grid/AgGrid.scss +51 -25
  10. package/cmp/ag-grid/AgGrid.ts +8 -2
  11. package/cmp/badge/Badge.ts +18 -5
  12. package/cmp/chart/Chart.ts +13 -11
  13. package/cmp/clock/Clock.ts +6 -5
  14. package/cmp/dataview/DataView.ts +5 -3
  15. package/cmp/form/Form.ts +25 -6
  16. package/cmp/grid/Grid.ts +41 -25
  17. package/cmp/grid/GridModel.ts +2 -2
  18. package/cmp/grid/Types.ts +1 -1
  19. package/cmp/grid/columns/Column.ts +45 -2
  20. package/cmp/grid/impl/GridHScrollbar.ts +140 -0
  21. package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
  22. package/cmp/input/HoistInputModel.ts +4 -4
  23. package/cmp/input/HoistInputProps.ts +3 -1
  24. package/cmp/layout/Box.ts +4 -2
  25. package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
  26. package/cmp/store/StoreFilterField.ts +2 -2
  27. package/cmp/tab/TabContainer.ts +1 -1
  28. package/cmp/zoneGrid/Types.ts +47 -0
  29. package/cmp/zoneGrid/ZoneGrid.ts +62 -0
  30. package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
  31. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
  32. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
  33. package/cmp/zoneGrid/index.ts +3 -0
  34. package/core/HoistComponent.ts +23 -10
  35. package/core/HoistProps.ts +25 -6
  36. package/core/XH.ts +49 -27
  37. package/core/elem.ts +11 -3
  38. package/core/impl/InstanceManager.ts +24 -1
  39. package/core/model/HoistModel.ts +4 -4
  40. package/data/RecordAction.ts +7 -4
  41. package/data/StoreRecord.ts +8 -1
  42. package/desktop/appcontainer/AppContainer.ts +2 -0
  43. package/desktop/cmp/appbar/AppBar.ts +8 -6
  44. package/desktop/cmp/button/Button.ts +14 -3
  45. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  46. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  47. package/desktop/cmp/button/index.ts +1 -0
  48. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  49. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  50. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  51. package/desktop/cmp/form/FormField.ts +34 -10
  52. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  53. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  54. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  55. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  56. package/desktop/cmp/input/Checkbox.ts +3 -3
  57. package/desktop/cmp/input/CodeInput.ts +2 -1
  58. package/desktop/cmp/input/DateInput.ts +128 -123
  59. package/desktop/cmp/input/JsonInput.ts +1 -1
  60. package/desktop/cmp/input/NumberInput.ts +3 -2
  61. package/desktop/cmp/input/RadioInput.ts +3 -1
  62. package/desktop/cmp/input/Select.ts +31 -4
  63. package/desktop/cmp/input/SwitchInput.ts +2 -1
  64. package/desktop/cmp/input/TextArea.ts +3 -3
  65. package/desktop/cmp/input/TextInput.ts +51 -47
  66. package/desktop/cmp/panel/Panel.ts +21 -19
  67. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  68. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  69. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  70. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  71. package/desktop/cmp/rest/Actions.ts +10 -5
  72. package/desktop/cmp/rest/RestGrid.ts +20 -6
  73. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  74. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  75. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  76. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  77. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  78. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  79. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  80. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  81. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  82. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  83. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  84. package/dynamics/desktop.ts +2 -0
  85. package/dynamics/mobile.ts +2 -0
  86. package/inspector/instances/InstancesModel.ts +2 -2
  87. package/mobile/appcontainer/AppContainer.ts +2 -0
  88. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  89. package/mobile/cmp/button/index.ts +1 -0
  90. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  91. package/mobile/cmp/input/Select.scss +1 -0
  92. package/mobile/cmp/input/Select.ts +7 -0
  93. package/mobile/cmp/input/TextInput.ts +1 -0
  94. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  95. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  96. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  97. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  98. package/package.json +4 -3
  99. package/styles/vars.scss +3 -3
  100. package/svc/InspectorService.ts +1 -1
  101. package/utils/js/DomUtils.ts +10 -0
  102. package/utils/js/LangUtils.ts +10 -0
  103. package/utils/js/TestUtils.ts +9 -0
  104. package/utils/js/index.ts +1 -0
package/cmp/grid/Grid.ts CHANGED
@@ -7,8 +7,9 @@
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {agGrid, AgGrid} from '@xh/hoist/cmp/ag-grid';
9
9
  import {getTreeStyleClasses} from '@xh/hoist/cmp/grid';
10
+ import {gridHScrollbar} from '@xh/hoist/cmp/grid/impl/GridHScrollbar';
10
11
  import {getAgGridMenuItems} from '@xh/hoist/cmp/grid/impl/MenuSupport';
11
- import {div, fragment, frame} from '@xh/hoist/cmp/layout';
12
+ import {div, fragment, frame, vframe} from '@xh/hoist/cmp/layout';
12
13
  import {
13
14
  hoistCmp,
14
15
  HoistModel,
@@ -16,29 +17,18 @@ import {
16
17
  LayoutProps,
17
18
  lookup,
18
19
  PlainObject,
20
+ TestSupportProps,
19
21
  useLocalModel,
20
22
  uses,
21
23
  XH
22
24
  } from '@xh/hoist/core';
25
+ import {RecordSet} from '@xh/hoist/data/impl/RecordSet';
23
26
  import {
24
27
  colChooser as desktopColChooser,
25
28
  gridFilterDialog,
26
29
  ModalSupportModel
27
30
  } from '@xh/hoist/dynamics/desktop';
28
31
  import {colChooser as mobileColChooser} from '@xh/hoist/dynamics/mobile';
29
- import {computed, observer} from '@xh/hoist/mobx';
30
- import {wait} from '@xh/hoist/promise';
31
- import {consumeEvent, isDisplayed, logDebug, logWithDebug} from '@xh/hoist/utils/js';
32
- import {getLayoutProps} from '@xh/hoist/utils/react';
33
- import classNames from 'classnames';
34
- import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
35
- import {createRef} from 'react';
36
- import './Grid.scss';
37
- import {GridModel} from './GridModel';
38
- import {columnGroupHeader} from './impl/ColumnGroupHeader';
39
- import {columnHeader} from './impl/ColumnHeader';
40
- import {RowKeyNavSupport} from './impl/RowKeyNavSupport';
41
- import {RecordSet} from '@xh/hoist/data/impl/RecordSet';
42
32
  import {Icon} from '@xh/hoist/icon';
43
33
 
44
34
  import type {
@@ -49,8 +39,19 @@ import type {
49
39
  GridReadyEvent,
50
40
  ProcessCellForExportParams
51
41
  } from '@xh/hoist/kit/ag-grid';
42
+ import {computed, observer} from '@xh/hoist/mobx';
43
+ import {wait} from '@xh/hoist/promise';
44
+ import {consumeEvent, isDisplayed, logDebug, logWithDebug} from '@xh/hoist/utils/js';
45
+ import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
46
+ import classNames from 'classnames';
47
+ import {debounce, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
48
+ import './Grid.scss';
49
+ import {GridModel} from './GridModel';
50
+ import {columnGroupHeader} from './impl/ColumnGroupHeader';
51
+ import {columnHeader} from './impl/ColumnHeader';
52
+ import {RowKeyNavSupport} from './impl/RowKeyNavSupport';
52
53
 
53
- export interface GridProps extends HoistProps<GridModel>, LayoutProps {
54
+ export interface GridProps extends HoistProps<GridModel>, LayoutProps, TestSupportProps {
54
55
  /**
55
56
  * Options for ag-Grid's API.
56
57
  *
@@ -90,7 +91,7 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
90
91
  model: uses(GridModel),
91
92
  className: 'xh-grid',
92
93
 
93
- render({model, className, ...props}, ref) {
94
+ render({model, className, testId, ...props}, ref) {
94
95
  const {store, treeMode, treeStyle, highlightRowOnClick, colChooserModel, filterModel} =
95
96
  model,
96
97
  impl = useLocalModel(GridLocalModel),
@@ -106,14 +107,24 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
106
107
  highlightRowOnClick ? 'xh-grid--highlight-row-on-click' : null
107
108
  );
108
109
 
110
+ const {enableFullWidthScroll} = model.experimental,
111
+ container = enableFullWidthScroll ? vframe : frame;
112
+
109
113
  return fragment(
110
- frame({
114
+ container({
111
115
  className,
112
- item: agGrid({
113
- model: model.agGridModel,
114
- ...getLayoutProps(props),
115
- ...impl.agOptions
116
- }),
116
+ items: [
117
+ agGrid({
118
+ model: model.agGridModel,
119
+ ...getLayoutProps(props),
120
+ ...impl.agOptions
121
+ }),
122
+ gridHScrollbar({
123
+ omit: !enableFullWidthScroll,
124
+ gridLocalModel: impl
125
+ })
126
+ ],
127
+ testId,
117
128
  onKeyDown: impl.onKeyDown,
118
129
  ref: composeRefs(impl.viewRef, ref)
119
130
  }),
@@ -123,18 +134,18 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
123
134
  }
124
135
  });
125
136
 
126
- (Grid as any).MULTIFIELD_ROW_HEIGHT = 38;
137
+ (Grid as any).MULTIFIELD_ROW_HEIGHT = 42;
127
138
 
128
139
  //------------------------
129
140
  // Implementation
130
141
  //------------------------
131
- class GridLocalModel extends HoistModel {
142
+ export class GridLocalModel extends HoistModel {
132
143
  override xhImpl = true;
133
144
 
134
145
  @lookup(GridModel)
135
146
  private model: GridModel;
136
147
  agOptions: GridOptions;
137
- viewRef = createRef<HTMLElement>();
148
+ viewRef = createObservableRef<HTMLElement>();
138
149
  private rowKeyNavSupport: RowKeyNavSupport;
139
150
  private prevRs: RecordSet;
140
151
 
@@ -270,6 +281,11 @@ class GridLocalModel extends HoistModel {
270
281
  };
271
282
  }
272
283
 
284
+ // Support for FullWidthScroll
285
+ if (model.experimental.enableFullWidthScroll) {
286
+ ret.suppressHorizontalScroll = true;
287
+ }
288
+
273
289
  return ret;
274
290
  }
275
291
 
@@ -132,7 +132,7 @@ export interface GridConfig {
132
132
  /** Config with which to create a GridFilterModel, or `true` to enable default. Desktop only.*/
133
133
  filterModel?: GridFilterModelConfig | boolean;
134
134
 
135
- /** Config with which to create aColChooserModel, or boolean `true` to enable default.*/
135
+ /** Config with which to create a ColChooserModel, or boolean `true` to enable default.*/
136
136
  colChooserModel?: ColChooserConfig | boolean;
137
137
 
138
138
  /**
@@ -1461,7 +1461,7 @@ export class GridModel extends HoistModel {
1461
1461
  if (omit) return null;
1462
1462
 
1463
1463
  if (this.isGroupSpec(config)) {
1464
- if (config.borders) borderedGroup = config;
1464
+ if (config.borders !== false) borderedGroup = config;
1465
1465
  const children = compact(
1466
1466
  config.children.map(c => this.buildColumn(c, borderedGroup))
1467
1467
  ) as Array<ColumnGroup | Column>;
package/cmp/grid/Types.ts CHANGED
@@ -113,7 +113,7 @@ export interface GridFilterModelConfig {
113
113
  fieldSpecs?: Array<string | GridFilterFieldSpecConfig>;
114
114
 
115
115
  /** Default properties to be assigned to all fieldSpecs created by this model. */
116
- fieldSpecDefaults?: GridFilterFieldSpecConfig;
116
+ fieldSpecDefaults?: Omit<GridFilterFieldSpecConfig, 'field'>;
117
117
  }
118
118
 
119
119
  /**
@@ -190,6 +190,13 @@ export interface ColumnSpec {
190
190
  */
191
191
  sortValue?: string | ColumnSortValueFn;
192
192
 
193
+ /**
194
+ * Values to match or functions to check to determine if a value should always be sorted
195
+ * to the bottom, regardless of sort order. If more than one entry is provided, values will be
196
+ * sorted according to the order they appear here.
197
+ */
198
+ sortToBottom?: Some<unknown | ((v: unknown) => boolean)>;
199
+
193
200
  /** Function to compare cell values for sorting.*/
194
201
  comparator?: ColumnComparator;
195
202
 
@@ -434,6 +441,7 @@ export class Column {
434
441
  sortingOrder: ColumnSortSpec[];
435
442
  absSort: boolean;
436
443
  sortValue: string | ColumnSortValueFn;
444
+ sortToBottom: Array<(v: unknown) => boolean>;
437
445
  comparator: ColumnComparator;
438
446
  resizable: boolean;
439
447
  sortable: boolean;
@@ -506,6 +514,7 @@ export class Column {
506
514
  absSort,
507
515
  sortingOrder,
508
516
  sortValue,
517
+ sortToBottom,
509
518
  comparator,
510
519
  resizable,
511
520
  movable,
@@ -600,6 +609,7 @@ export class Column {
600
609
  this.absSort = withDefault(absSort, false);
601
610
  this.sortingOrder = this.parseSortingOrder(sortingOrder);
602
611
  this.sortValue = sortValue;
612
+ this.sortToBottom = this.parseSortToBottom(sortToBottom);
603
613
  this.comparator = comparator;
604
614
 
605
615
  this.resizable = withDefault(resizable, true);
@@ -897,12 +907,20 @@ export class Column {
897
907
  if (this.comparator === undefined) {
898
908
  // Use default comparator with appropriate inputs
899
909
  ret.comparator = (valueA, valueB, agNodeA, agNodeB) => {
900
- const recordA = agNodeA?.data,
910
+ const {gridModel, colId} = this,
911
+ // Note: sortCfg and agNodes can be undefined if comparator called during show
912
+ // of agGrid column header set filter menu.
913
+ sortCfg = find(gridModel.sortBy, {colId}),
914
+ sortDir = sortCfg?.sort || 'asc',
915
+ recordA = agNodeA?.data,
901
916
  recordB = agNodeB?.data;
902
917
 
903
918
  valueA = this.getSortValue(valueA, recordA);
904
919
  valueB = this.getSortValue(valueB, recordB);
905
920
 
921
+ const sortToBottom = this.sortToBottomComparator(valueA, valueB, sortDir);
922
+ if (sortToBottom !== 0) return sortToBottom;
923
+
906
924
  return this.defaultComparator(valueA, valueB);
907
925
  };
908
926
  } else {
@@ -921,7 +939,7 @@ export class Column {
921
939
  recordB,
922
940
  column: this,
923
941
  gridModel,
924
- defaultComparator: (a, b) => this.defaultComparator(a, b),
942
+ defaultComparator: this.defaultComparator,
925
943
  agNodeA,
926
944
  agNodeB
927
945
  };
@@ -929,6 +947,9 @@ export class Column {
929
947
  valueA = this.getSortValue(valueA, recordA);
930
948
  valueB = this.getSortValue(valueB, recordB);
931
949
 
950
+ const sortToBottom = this.sortToBottomComparator(valueA, valueB, sortDir);
951
+ if (sortToBottom !== 0) return sortToBottom;
952
+
932
953
  return this.comparator(valueA, valueB, sortDir, abs, params);
933
954
  };
934
955
  }
@@ -979,6 +1000,23 @@ export class Column {
979
1000
  return sortCfg ? sortCfg.comparator(v1, v2) : GridSorter.defaultComparator(v1, v2);
980
1001
  };
981
1002
 
1003
+ private sortToBottomComparator = (v1, v2, sortDir: 'asc' | 'desc') => {
1004
+ const {sortToBottom} = this;
1005
+
1006
+ if (isNil(sortToBottom)) return 0;
1007
+
1008
+ for (let fn of sortToBottom) {
1009
+ const v1ToBottom = fn(v1),
1010
+ v2ToBottom = fn(v2);
1011
+ const isAsc = sortDir === 'asc';
1012
+ if (v1ToBottom != v2ToBottom) {
1013
+ return v1ToBottom ? (isAsc ? 1 : -1) : isAsc ? -1 : 1;
1014
+ }
1015
+ }
1016
+
1017
+ return 0;
1018
+ };
1019
+
982
1020
  private defaultSetValueFn = ({value, record, store, field}) => {
983
1021
  const data = {id: record.id};
984
1022
  data[field] = value;
@@ -1011,6 +1049,11 @@ export class Column {
1011
1049
  return sortingOrder?.map(spec => (isString(spec) || spec === null ? {sort: spec} : spec));
1012
1050
  }
1013
1051
 
1052
+ private parseSortToBottom(sortToBottom): Array<(v: unknown) => boolean> {
1053
+ if (isNil(sortToBottom)) return null;
1054
+ return castArray(sortToBottom).map(v => (isFunction(v) ? v : it => it === v));
1055
+ }
1056
+
1014
1057
  private parseFilterable(filterable) {
1015
1058
  if (!filterable) return false;
1016
1059
 
@@ -0,0 +1,140 @@
1
+ import {GridLocalModel, GridModel} from '@xh/hoist/cmp/grid';
2
+ import {div} from '@xh/hoist/cmp/layout';
3
+ import {hoistCmp, HoistModel, HoistProps, useLocalModel} from '@xh/hoist/core';
4
+ import {makeObservable} from '@xh/hoist/mobx';
5
+ import {observeResize} from '@xh/hoist/utils/js';
6
+ import {sumBy} from 'lodash';
7
+ import {action, observable} from 'mobx';
8
+ import {createRef, RefObject} from 'react';
9
+
10
+ /**
11
+ * Implementation for Grid's full-width horizontal scrollbar, to span pinned columns
12
+ * @internal
13
+ */
14
+
15
+ export interface GridHScrollbarProps extends HoistProps<GridModel> {
16
+ gridLocalModel: GridLocalModel;
17
+ }
18
+
19
+ export const gridHScrollbar = hoistCmp.factory<GridHScrollbarProps>({
20
+ className: 'xh-grid__grid-hscrollbar',
21
+ render({className}) {
22
+ const impl = useLocalModel(GridHScrollbarModel),
23
+ {scrollerRef, viewWidth, visibleColumnWidth, SCROLLBAR_SIZE} = impl;
24
+
25
+ if (viewWidth > visibleColumnWidth) return null;
26
+
27
+ return div({
28
+ className,
29
+ item: div({
30
+ className: `${className}__filler`,
31
+ style: {
32
+ height: SCROLLBAR_SIZE,
33
+ width: visibleColumnWidth
34
+ }
35
+ }),
36
+ onScroll: e => {
37
+ impl.scrollViewport((e.target as HTMLDivElement).scrollLeft);
38
+ },
39
+ ref: scrollerRef,
40
+ style: {
41
+ height: SCROLLBAR_SIZE, // TODO: make this a property on GridModel to apply to both scrollbars
42
+ overflowX: 'auto',
43
+ overflowY: 'hidden'
44
+ }
45
+ });
46
+ }
47
+ });
48
+
49
+ class GridHScrollbarModel extends HoistModel {
50
+ readonly SCROLLBAR_SIZE = 10;
51
+ readonly scrollerRef = createRef<HTMLDivElement>();
52
+
53
+ @observable viewWidth: number;
54
+ @observable private isVerticalScrollbarVisible = false;
55
+
56
+ /** Observe AG's viewport to detect when vertical scrollbar visibility changes */
57
+ private agViewportResizeObserver: ResizeObserver;
58
+ /** Observe overall view to detect when horizontal scrollbar is needed */
59
+ private viewResizeObserver: ResizeObserver;
60
+
61
+ get visibleColumnWidth(): number {
62
+ const {gridModel, SCROLLBAR_SIZE} = this;
63
+ return (
64
+ sumBy(gridModel.columnState, ({colId, hidden, width}) => {
65
+ if (hidden) return 0;
66
+ const minWidth = gridModel.getColumn(colId).minWidth ?? 0;
67
+ if (width) return Math.max(width, minWidth);
68
+ return minWidth;
69
+ }) + (this.isVerticalScrollbarVisible ? SCROLLBAR_SIZE : 0)
70
+ );
71
+ }
72
+
73
+ private get agViewport(): HTMLDivElement {
74
+ return this.viewRef.current.querySelector('.ag-center-cols-viewport');
75
+ }
76
+
77
+ private get agVerticalScrollContainer(): HTMLDivElement {
78
+ return this.viewRef.current.querySelector('.ag-body-vertical-scroll-container');
79
+ }
80
+
81
+ private get gridModel(): GridModel {
82
+ return this.componentProps.model as GridModel;
83
+ }
84
+
85
+ private get viewRef(): RefObject<HTMLElement> {
86
+ return this.componentProps.gridLocalModel.viewRef;
87
+ }
88
+
89
+ constructor() {
90
+ super();
91
+ makeObservable(this);
92
+ }
93
+
94
+ scrollScroller(left: number) {
95
+ this.scrollerRef.current.scrollLeft = left;
96
+ }
97
+
98
+ scrollViewport(left: number) {
99
+ this.agViewport.scrollLeft = left;
100
+ }
101
+
102
+ override afterLinked() {
103
+ this.addReaction({
104
+ when: () => this.viewRef.current && this.gridModel.isReady,
105
+ run: () => {
106
+ const {agViewport, viewRef} = this;
107
+ this.viewWidth = viewRef.current.clientWidth;
108
+ agViewport.addEventListener('scroll', e =>
109
+ this.scrollScroller((e.target as HTMLDivElement).scrollLeft)
110
+ );
111
+ this.agViewportResizeObserver = observeResize(
112
+ () => this.onAgViewportResized(),
113
+ agViewport,
114
+ {debounce: 100}
115
+ );
116
+ this.viewResizeObserver = observeResize(
117
+ rect => this.onViewResized(rect),
118
+ viewRef.current,
119
+ {debounce: 100}
120
+ );
121
+ }
122
+ });
123
+ }
124
+
125
+ override destroy() {
126
+ super.destroy();
127
+ this.agViewportResizeObserver?.disconnect();
128
+ this.viewResizeObserver?.disconnect();
129
+ }
130
+
131
+ @action
132
+ private onAgViewportResized() {
133
+ this.isVerticalScrollbarVisible = !!this.agVerticalScrollContainer.clientHeight;
134
+ }
135
+
136
+ @action
137
+ private onViewResized({width}: DOMRect) {
138
+ this.viewWidth = width;
139
+ }
140
+ }
@@ -98,7 +98,7 @@ function renderMainField(value, renderer, context) {
98
98
 
99
99
  function renderSubField({colId, label}, context) {
100
100
  const {record, gridModel} = context,
101
- column = gridModel.findColumn(gridModel.columns, colId);
101
+ column = gridModel.getColumn(colId);
102
102
 
103
103
  throwIf(!column, `Subfield ${colId} not found`);
104
104
 
@@ -4,14 +4,14 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {DefaultHoistProps, HoistModel, HoistModelClass, useLocalModel} from '@xh/hoist/core';
8
7
  import {FieldModel} from '@xh/hoist/cmp/form';
9
- import {action, computed, observable, makeObservable} from '@xh/hoist/mobx';
8
+ import {DefaultHoistProps, HoistModel, HoistModelClass, useLocalModel} from '@xh/hoist/core';
9
+ import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
10
+ import {createObservableRef} from '@xh/hoist/utils/react';
10
11
  import classNames from 'classnames';
11
12
  import {isEqual} from 'lodash';
12
- import {ForwardedRef, ReactElement, ReactInstance, useImperativeHandle, FocusEvent} from 'react';
13
+ import {FocusEvent, ForwardedRef, ReactElement, ReactInstance, useImperativeHandle} from 'react';
13
14
  import {findDOMNode} from 'react-dom';
14
- import {createObservableRef} from '@xh/hoist/utils/react';
15
15
  import './HoistInput.scss';
16
16
 
17
17
  /**
@@ -5,7 +5,9 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- export interface HoistInputProps {
8
+ import {TestSupportProps} from '@xh/hoist/core';
9
+
10
+ export interface HoistInputProps extends TestSupportProps {
9
11
  /**
10
12
  * Field or model property name from which this component should read and write its value
11
13
  * in controlled mode. Can be set by parent FormField.
package/cmp/layout/Box.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp, BoxProps, HoistProps} from '@xh/hoist/core';
7
+ import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core';
8
+ import {TEST_ID} from '@xh/hoist/utils/js';
8
9
  import {splitLayoutProps} from '@xh/hoist/utils/react';
9
10
  import {merge} from 'lodash';
10
11
  import {div} from './Tags';
@@ -31,11 +32,12 @@ export const [Box, box] = hoistCmp.withFactory<BoxComponentProps>({
31
32
  // Note `model` destructured off of non-layout props to avoid setting
32
33
  // model as a bogus DOM attribute. This low-level component may easily be passed one from
33
34
  // a parent that has not properly managed its own props.
34
- let [layoutProps, {children, model, ...restProps}] = splitLayoutProps(props);
35
+ let [layoutProps, {children, model, testId, ...restProps}] = splitLayoutProps(props);
35
36
 
36
37
  restProps = merge(
37
38
  {style: {display: 'flex', overflow: 'hidden', position: 'relative'}},
38
39
  {style: layoutProps},
40
+ {[TEST_ID]: testId},
39
41
  restProps
40
42
  );
41
43