cx 26.6.0 → 26.7.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 (44) hide show
  1. package/build/data/ExposedValueView.js +1 -1
  2. package/build/data/comparer.d.ts +6 -8
  3. package/build/data/comparer.d.ts.map +1 -1
  4. package/build/ui/Prop.d.ts +1 -1
  5. package/build/ui/Prop.d.ts.map +1 -1
  6. package/build/ui/adapter/ArrayAdapter.d.ts +4 -6
  7. package/build/ui/adapter/ArrayAdapter.d.ts.map +1 -1
  8. package/build/ui/adapter/GroupAdapter.d.ts +22 -2
  9. package/build/ui/adapter/GroupAdapter.d.ts.map +1 -1
  10. package/build/ui/adapter/GroupAdapter.js +80 -5
  11. package/build/widgets/drag-drop/DragSource.d.ts +3 -2
  12. package/build/widgets/drag-drop/DragSource.d.ts.map +1 -1
  13. package/build/widgets/drag-drop/DropZone.d.ts +4 -4
  14. package/build/widgets/drag-drop/DropZone.d.ts.map +1 -1
  15. package/build/widgets/drag-drop/ops.d.ts +13 -2
  16. package/build/widgets/drag-drop/ops.d.ts.map +1 -1
  17. package/build/widgets/drag-drop/ops.js +36 -9
  18. package/build/widgets/form/NumberField.js +1 -0
  19. package/build/widgets/grid/Grid.d.ts +19 -3
  20. package/build/widgets/grid/Grid.d.ts.map +1 -1
  21. package/build/widgets/grid/Grid.js +2 -0
  22. package/build/widgets/overlay/captureMouse.d.ts +4 -4
  23. package/build/widgets/overlay/captureMouse.d.ts.map +1 -1
  24. package/build/widgets/overlay/captureMouse.js +8 -3
  25. package/dist/data.js +1 -1
  26. package/dist/manifest.js +766 -766
  27. package/dist/ui.js +94 -9
  28. package/dist/widgets.css +4 -0
  29. package/dist/widgets.js +42 -10
  30. package/package.json +1 -1
  31. package/src/data/ExposedValueView.spec.ts +57 -0
  32. package/src/data/ExposedValueView.ts +1 -1
  33. package/src/data/comparer.ts +6 -7
  34. package/src/ui/Prop.ts +1 -1
  35. package/src/ui/adapter/ArrayAdapter.ts +6 -8
  36. package/src/ui/adapter/GroupAdapter.spec.ts +75 -0
  37. package/src/ui/adapter/GroupAdapter.ts +103 -10
  38. package/src/widgets/drag-drop/DragSource.tsx +3 -3
  39. package/src/widgets/drag-drop/DropZone.tsx +5 -5
  40. package/src/widgets/drag-drop/ops.tsx +54 -12
  41. package/src/widgets/form/NumberField.scss +4 -0
  42. package/src/widgets/form/NumberField.tsx +1 -0
  43. package/src/widgets/grid/Grid.tsx +23 -2
  44. package/src/widgets/overlay/captureMouse.ts +14 -7
@@ -4,11 +4,13 @@ import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
4
4
  import { View } from "../../data/View";
5
5
  import { isDataRecord } from "../../util";
6
6
  import { isArray } from "../../util/isArray";
7
+ import { isDefined } from "../../util/isDefined";
8
+ import { isNonEmptyArray } from "../../util/isNonEmptyArray";
7
9
  import { Culture } from "../Culture";
8
10
  import { Instance } from "../Instance";
9
11
  import { Prop, SortDirection, Sorter, StructuredProp } from "../Prop";
10
12
  import { RenderingContext } from "../RenderingContext";
11
- import { ArrayAdapter, ArrayAdapterConfig, RecordStoreCache } from "./ArrayAdapter";
13
+ import { ArrayAdapter, ArrayAdapterConfig, ExtendedSorter, RecordStoreCache } from "./ArrayAdapter";
12
14
  import { DataAdapterRecord } from "./DataAdapter";
13
15
 
14
16
  export interface GroupKey {
@@ -21,7 +23,17 @@ export interface GroupingConfig {
21
23
  text?: Prop<string>;
22
24
  includeHeader?: boolean;
23
25
  includeFooter?: boolean;
26
+ /** Custom group comparer. Takes precedence over `sortField`/`sortDirection`/`sorters`. */
24
27
  comparer?: ((a: any, b: any) => number) | null;
28
+ /**
29
+ * Sort groups by a single field resolved against the group's `key`, `aggregates` and `name`.
30
+ * Use an aggregate alias (e.g. `"total"`) to sort by an aggregate, or a key field to sort by key.
31
+ */
32
+ sortField?: string;
33
+ /** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
34
+ sortDirection?: SortDirection;
35
+ /** Multi-field group sorting. Each sorter's `field` is resolved against the group's `key`, `aggregates` and `name`. */
36
+ sorters?: Sorter[];
25
37
  header?: any;
26
38
  footer?: any;
27
39
  }
@@ -50,6 +62,8 @@ export interface GroupAdapterConfig extends ArrayAdapterConfig {
50
62
  groupRecordsName?: string;
51
63
  groupings?: GroupingConfig[] | null;
52
64
  groupName?: string;
65
+ /** When enabled, the active record sorters also reorder groups (resolved against each group's key/aggregates/name). */
66
+ sortGroupsBySorters?: boolean;
53
67
  }
54
68
 
55
69
  export class GroupAdapter<T = any> extends ArrayAdapter<T> {
@@ -58,11 +72,41 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
58
72
  declare public groupRecordsName?: string;
59
73
  declare public groupings?: ResolvedGrouping[] | null;
60
74
  declare public groupName: string;
75
+ declare public sortGroupsBySorters?: boolean;
76
+
77
+ /**
78
+ * Per-level comparer derived from the active record sorters, used to reorder groups when
79
+ * `sortGroupsBySorters` is set. A level's entry is `null` when no active sorter maps to that
80
+ * level's key/aggregate, so the grouping's own configured order is kept.
81
+ */
82
+ protected groupSortComparers?: (((a: any, b: any) => number) | null)[];
61
83
 
62
84
  constructor(config?: GroupAdapterConfig) {
63
85
  super(config);
64
86
  }
65
87
 
88
+ public sort(sorters?: ExtendedSorter[]): void {
89
+ super.sort(sorters);
90
+
91
+ // When enabled, derive a group comparer from the active record sorters so that interactive
92
+ // column sorting also reorders the groups. A column only reorders groups at levels where its
93
+ // field is a group key or aggregate; other levels keep their configured order. The record-level
94
+ // value selector is ignored — the field is resolved against the group's key/aggregates/name.
95
+ if (this.sortGroupsBySorters && this.groupings) {
96
+ const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
97
+ const colSorters: ExtendedSorter[] = isNonEmptyArray(sorters)
98
+ ? sorters.map((s) => ({ field: s.field, direction: s.direction, comparer: s.comparer }))
99
+ : [];
100
+
101
+ this.groupSortComparers = this.groupings.map((g) => {
102
+ if (colSorters.length === 0) return null;
103
+ const fields = groupFieldNames(g, this.aggregates);
104
+ const applicable = colSorters.filter((s) => s.field && fields.has(s.field));
105
+ return applicable.length > 0 ? buildGroupComparer(applicable, cultureComparer) : null;
106
+ });
107
+ }
108
+ }
109
+
66
110
  public init(): void {
67
111
  super.init();
68
112
 
@@ -116,8 +160,13 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
116
160
  grouper.processAll(records);
117
161
  let results = grouper.getResults();
118
162
 
119
- if (grouping.comparer && !this.preserveOrder) {
120
- results.sort(grouping.comparer);
163
+ // An active column sort that maps to this level's key/aggregate (when `sortGroupsBySorters` is
164
+ // enabled) takes over; otherwise the grouping's configured comparer (or implicit key order) applies.
165
+ const dynamicComparer = this.sortGroupsBySorters ? this.groupSortComparers?.[level] : undefined;
166
+ const comparer = dynamicComparer ?? grouping.comparer;
167
+
168
+ if (comparer && !this.preserveOrder) {
169
+ results.sort(comparer);
121
170
  }
122
171
 
123
172
  results.forEach((gr) => {
@@ -208,15 +257,22 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
208
257
  g.text,
209
258
  );
210
259
 
260
+ const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
261
+
262
+ // Sort groups by an aggregate/key/name through `sortField`/`sortDirection` or a `sorters` array.
263
+ let sortSorters: Sorter[] | null = null;
264
+ if (isNonEmptyArray(g.sorters)) sortSorters = g.sorters!;
265
+ else if (g.sortField) sortSorters = [{ field: g.sortField, direction: g.sortDirection ?? "ASC" }];
266
+
267
+ // `comparer`, `sortField`/`sorters` and the implicit key order are equivalent alternatives; an
268
+ // explicit `comparer` wins, then declarative sorters, then sorting by key in declaration order.
211
269
  const comparer =
212
270
  g.comparer ??
213
- (groupSorters.length > 0
214
- ? getComparer(
215
- groupSorters,
216
- (x: any) => x.key,
217
- this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined,
218
- )
219
- : null);
271
+ (sortSorters
272
+ ? buildGroupComparer(sortSorters, cultureComparer)
273
+ : groupSorters.length > 0
274
+ ? getComparer(groupSorters, (x: any) => x.key, cultureComparer)
275
+ : null);
220
276
 
221
277
  return {
222
278
  ...g,
@@ -233,6 +289,43 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
233
289
 
234
290
  GroupAdapter.prototype.groupName = "$group";
235
291
  GroupAdapter.prototype.preserveOrder = false;
292
+ GroupAdapter.prototype.sortGroupsBySorters = false;
293
+
294
+ // The set of fields a group can be sorted by at a given level: its key fields, its aggregate aliases
295
+ // and the group name. Used to decide whether an active column sort applies to that grouping level.
296
+ function groupFieldNames(grouping: ResolvedGrouping, adapterAggregates?: StructuredProp): Set<string> {
297
+ const names = new Set<string>();
298
+ for (const k of grouping.grouper.keys) names.add(k.name);
299
+ const aggregates = { ...adapterAggregates, ...grouping.aggregates };
300
+ for (const a in aggregates) names.add(a);
301
+ names.add("name");
302
+ names.add("$name");
303
+ return names;
304
+ }
305
+
306
+ // Reads a sort field straight from the GroupResult — first its key fields, then its aggregates, then
307
+ // its name. Returns a plain selector so groups can be sorted without cloning the result per comparison.
308
+ function groupFieldSelector(field: string): (gr: any) => any {
309
+ if (field === "name" || field === "$name") return (gr) => gr?.name;
310
+ return (gr) => {
311
+ if (gr?.key && field in gr.key) return gr.key[field];
312
+ if (gr?.aggregates && field in gr.aggregates) return gr.aggregates[field];
313
+ return undefined;
314
+ };
315
+ }
316
+
317
+ // Builds a group comparer from a list of sorters. Each sorter resolves by its explicit `value`
318
+ // selector, or by `field` looked up against the group's key/aggregates/name.
319
+ function buildGroupComparer(
320
+ sorters: ExtendedSorter[],
321
+ cultureComparer?: (a: any, b: any) => number,
322
+ ): (a: any, b: any) => number {
323
+ return getComparer(
324
+ sorters.map((s) => (isDefined(s.value) || !s.field ? s : { ...s, value: groupFieldSelector(s.field) })),
325
+ undefined,
326
+ cultureComparer,
327
+ );
328
+ }
236
329
 
237
330
  function serializeKey(data: any): string {
238
331
  if (isDataRecord(data)) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Widget, VDOM } from "../../ui/Widget";
4
4
  import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container";
5
- import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent } from "./ops";
5
+ import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent, DragEndEvent } from "./ops";
6
6
  import { preventFocus } from "../../ui/FocusManager";
7
7
  import { parseStyle } from "../../util/parseStyle";
8
8
  import { Instance } from "../../ui/Instance";
@@ -30,7 +30,7 @@ export interface DragSourceConfig extends StyledContainerConfig {
30
30
 
31
31
  onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: Instance) => any;
32
32
 
33
- onDragEnd?: (e: React.MouseEvent | React.TouchEvent, instance: Instance) => void;
33
+ onDragEnd?: (e: DragEndEvent, instance: Instance) => void;
34
34
 
35
35
  id?: StringProp;
36
36
 
@@ -64,7 +64,7 @@ export class DragSource extends StyledContainerBase<DragSourceConfig, DragSource
64
64
  declare cloneStyle: any;
65
65
  declare draggedStyle: any;
66
66
  declare onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: DragSourceInstance) => any;
67
- declare onDragEnd?: (e: React.MouseEvent | React.TouchEvent, instance: DragSourceInstance) => void;
67
+ declare onDragEnd?: (e: DragEndEvent, instance: DragSourceInstance) => void;
68
68
 
69
69
  constructor(config?: DragSourceConfig) {
70
70
  super(config);
@@ -3,7 +3,7 @@
3
3
  import { Widget, VDOM } from "../../ui/Widget";
4
4
  import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container";
5
5
  import { parseStyle } from "../../util/parseStyle";
6
- import { registerDropZone, DragDropContext, DragEvent } from "./ops";
6
+ import { registerDropZone, DragDropContext, DragEvent, DragEndEvent } from "./ops";
7
7
  import { findScrollableParent } from "../../util/findScrollableParent";
8
8
  import { isNumber } from "../../util/isNumber";
9
9
  import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
@@ -90,8 +90,8 @@ export interface DropZoneConfig extends StyledContainerConfig {
90
90
  /** A callback method invoked when at the beginning of the drag & drop operation. */
91
91
  onDragStart?: string | ((event: DragEvent, instance: Instance) => void);
92
92
 
93
- /** A callback method invoked when at the end of the drag & drop operation. */
94
- onDragEnd?: string | ((event: DragEvent, instance: Instance) => void);
93
+ /** A callback method invoked when at the end of the drag & drop operation. `event.cancelled` is `true` when the operation was cancelled (e.g. via Esc) instead of dropped. */
94
+ onDragEnd?: string | ((event: DragEndEvent, instance: Instance) => void);
95
95
 
96
96
  /** Match height of the item being dragged */
97
97
  matchHeight?: boolean;
@@ -126,7 +126,7 @@ export class DropZone extends StyledContainerBase<DropZoneConfig, DropZoneInstan
126
126
  declare onDragEnter?: string | ((event?: DragEvent, instance?: Instance) => void);
127
127
  declare onDragLeave?: string | ((event?: DragEvent, instance?: Instance) => void);
128
128
  declare onDragStart?: string | ((event?: DragEvent, instance?: Instance) => void);
129
- declare onDragEnd?: string | ((event?: DragEvent, instance?: Instance) => void);
129
+ declare onDragEnd?: string | ((event?: DragEndEvent, instance?: Instance) => void);
130
130
 
131
131
  constructor(config?: DropZoneConfig) {
132
132
  super(config);
@@ -342,7 +342,7 @@ class DropZoneComponent extends VDOM.Component<DropZoneComponentProps, DropZoneC
342
342
  if (this.state.state == "over" && widget.onDrop) instance.invoke("onDrop", e, instance);
343
343
  }
344
344
 
345
- onDragEnd(e: DragEvent) {
345
+ onDragEnd(e: DragEndEvent) {
346
346
  this.setState({
347
347
  state: false,
348
348
  style: null,
@@ -21,6 +21,18 @@ export interface DragEvent {
21
21
  result?: any;
22
22
  }
23
23
 
24
+ export interface DragEndEvent {
25
+ type: "dragend";
26
+ /** The triggering pointer event, or `null` when the operation was cancelled via the keyboard. */
27
+ event: DragEvent["event"] | null;
28
+ /** Cursor position, or `null` when the operation was cancelled via the keyboard. */
29
+ cursor: CursorPosition | null;
30
+ source: DragEvent["source"];
31
+ /** `true` when the operation was cancelled (e.g. by pressing Esc) instead of dropped. */
32
+ cancelled: boolean;
33
+ result?: any;
34
+ }
35
+
24
36
  export interface DragDropOptions {
25
37
  sourceEl?: Element | null;
26
38
  clone?: any;
@@ -38,7 +50,7 @@ export interface IDropZone {
38
50
  onDragStart?: DragEventHandler;
39
51
  onDragAway?: DragEventHandler;
40
52
  onDragNear?: DragEventHandler;
41
- onDragEnd?: DragEventHandler;
53
+ onDragEnd?: (e: DragEndEvent) => void;
42
54
  onDragMeasure?: (
43
55
  e: DragEvent,
44
56
  operation: DragDropOperationContext,
@@ -62,7 +74,6 @@ import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingCli
62
74
  import { VDOM } from "../../ui/VDOM";
63
75
  import { Container } from "../../ui/Container";
64
76
  import { Console } from "../../util";
65
- import { WidgetConfig } from "../../ui";
66
77
 
67
78
  interface Puppet {
68
79
  deltaX: number;
@@ -70,7 +81,7 @@ interface Puppet {
70
81
  el: HTMLDivElement;
71
82
  clone: any;
72
83
  source: any;
73
- onDragEnd?: (e?: DragEvent) => void;
84
+ onDragEnd?: (e?: DragEndEvent) => void;
74
85
  stop?: () => void;
75
86
  }
76
87
 
@@ -82,6 +93,7 @@ let puppet: Puppet | null = null;
82
93
  let scrollTimer: number | null = null;
83
94
  let vscrollParent: Element | null = null;
84
95
  let hscrollParent: Element | null = null;
96
+ let releaseCapture: (() => void) | null = null;
85
97
 
86
98
  export function registerDropZone(dropZone: IDropZone): UnregisterFunction {
87
99
  dropZones.push(dropZone);
@@ -94,7 +106,7 @@ export function registerDropZone(dropZone: IDropZone): UnregisterFunction {
94
106
  export function initiateDragDrop(
95
107
  e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent,
96
108
  options: DragDropOptions = {},
97
- onDragEnd?: (e?: DragEvent) => void,
109
+ onDragEnd?: (e?: DragEndEvent) => void,
98
110
  ): void {
99
111
  if (puppet) {
100
112
  //last operation didn't finish properly
@@ -187,7 +199,16 @@ export function initiateDragDrop(
187
199
 
188
200
  notifyDragMove(e, null);
189
201
 
190
- captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
202
+ releaseCapture = captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
203
+ document.addEventListener("keydown", onDragKeyDown, true);
204
+ }
205
+
206
+ function onDragKeyDown(e: KeyboardEvent): void {
207
+ if (!puppet || e.key !== "Escape") return;
208
+ //cancel the operation without dropping; there is no pointer event to report
209
+ e.preventDefault();
210
+ e.stopPropagation();
211
+ notifyDragDrop(null, true);
191
212
  }
192
213
 
193
214
  function notifyDragMove(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, _captureData: any): void {
@@ -321,29 +342,50 @@ function clearScrollTimer() {
321
342
  }
322
343
  }
323
344
 
324
- function notifyDragDrop(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent): void {
345
+ function notifyDragDrop(
346
+ e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent | null,
347
+ cancelled: boolean = false,
348
+ ): void {
349
+ if (!puppet) return;
350
+
325
351
  clearScrollTimer();
352
+ document.removeEventListener("keydown", onDragKeyDown, true);
326
353
 
327
- let event = getDragEvent(e, "dragdrop");
354
+ if (puppet.stop) puppet.stop();
328
355
 
329
- if (puppet!.stop) puppet!.stop();
356
+ //a keyboard cancellation has no pointer event, so the drop and away notifications are skipped
357
+ let dropEvent = !cancelled && e ? getDragEvent(e, "dragdrop") : null;
330
358
 
331
- if (activeZone && activeZone.onDrop) event.result = activeZone.onDrop(event);
359
+ let result: any;
360
+ if (dropEvent && activeZone && activeZone.onDrop) result = activeZone.onDrop(dropEvent);
361
+
362
+ let endEvent: DragEndEvent = {
363
+ type: "dragend",
364
+ event: e,
365
+ cursor: e ? getCursorPos(e) : null,
366
+ source: puppet.source,
367
+ cancelled,
368
+ result,
369
+ };
332
370
 
333
371
  dropZones.forEach((zone) => {
334
- if (nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(event);
372
+ if (dropEvent && nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(dropEvent);
335
373
 
336
374
  if (!dragStartedZones!.has(zone)) return;
337
375
 
338
- if (zone.onDragEnd) zone.onDragEnd(event);
376
+ if (zone.onDragEnd) zone.onDragEnd(endEvent);
339
377
  });
340
378
 
341
- if (puppet!.onDragEnd) puppet!.onDragEnd(event);
379
+ if (puppet.onDragEnd) puppet.onDragEnd(endEvent);
380
+
381
+ //tear down the mouse/touch capture when cancelling; on a normal drop the capture ends itself
382
+ if (cancelled && releaseCapture) releaseCapture();
342
383
 
343
384
  nearZones = null;
344
385
  activeZone = null;
345
386
  puppet = null;
346
387
  dragStartedZones = null;
388
+ releaseCapture = null;
347
389
  }
348
390
 
349
391
  function getDragEvent(
@@ -42,6 +42,10 @@
42
42
  .#{$state}icon > & {
43
43
  padding-left: cx-calc(cx-top($padding), $cx-default-input-left-tool-size, $cx-default-input-left-tool-spacing);
44
44
  }
45
+
46
+ .#{$state}clear > & {
47
+ padding-right: cx-calc(cx-top($padding), $cx-default-clear-size, $cx-default-clear-spacing);
48
+ }
45
49
  }
46
50
 
47
51
  .#{$element}#{$name}-tool {
@@ -312,6 +312,7 @@ class Input extends VDOM.Component<InputProps, InputState> {
312
312
  visited: state.visited,
313
313
  focus: this.state.focus,
314
314
  icon: !!icon,
315
+ clear: insideButton != null,
315
316
  empty: empty && !data.placeholder,
316
317
  error:
317
318
  data.error &&
@@ -23,6 +23,7 @@ import {
23
23
  NumberProp,
24
24
  Prop,
25
25
  RecordAlias,
26
+ Sorter,
26
27
  SortDirection,
27
28
  SortersProp,
28
29
  StringProp,
@@ -62,6 +63,7 @@ import {
62
63
  ddMouseDown,
63
64
  DragDropOperationContext,
64
65
  DragEvent,
66
+ DragEndEvent,
65
67
  initiateDragDrop,
66
68
  registerDropZone,
67
69
  } from "../drag-drop/ops";
@@ -165,6 +167,15 @@ export interface GridGroupingConfig<T> {
165
167
  name?: StringProp;
166
168
  text?: StringProp;
167
169
  comparer?: (a: GroupingResult<T>, b: GroupingResult<T>) => number;
170
+ /**
171
+ * Sort groups by a single field resolved against the group's key, aggregates and name.
172
+ * Use an aggregate alias to sort by an aggregate, or a key field to sort by key.
173
+ */
174
+ sortField?: string;
175
+ /** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
176
+ sortDirection?: SortDirection;
177
+ /** Multi-field group sorting. Each sorter's `field` is resolved against the group's key, aggregates and name. */
178
+ sorters?: Sorter[];
168
179
  }
169
180
 
170
181
  export interface GridColumnHeaderConfig {
@@ -390,7 +401,7 @@ export interface GridConfig<T = any> extends StyledContainerConfig {
390
401
  onDrop?: string | ((e: GridDragEvent<T>, instance: Instance) => void);
391
402
  onDropTest?: string | ((e: DragEvent, instance: Instance) => boolean);
392
403
  onDragStart?: string | ((e: DragEvent, instance: Instance) => void);
393
- onDragEnd?: string | ((e: DragEvent, instance: Instance) => void);
404
+ onDragEnd?: string | ((e: DragEndEvent, instance: Instance) => void);
394
405
  onDragOver?: string | ((e: GridDragEvent<T>, instance: Instance) => void | boolean);
395
406
 
396
407
  onRowDropTest?: string | ((e: DragEvent, instance: Instance) => boolean);
@@ -502,6 +513,13 @@ export interface GridConfig<T = any> extends StyledContainerConfig {
502
513
 
503
514
  /** When enabled, groups are shown in the same order as the source records. */
504
515
  preserveGroupOrder?: boolean;
516
+
517
+ /**
518
+ * When enabled, sorting by a column header also reorders groups. The active sort field is resolved
519
+ * against each group's key fields, aggregates and name, so it works for both grouping columns and
520
+ * aggregate columns. Ignored when `preserveGroupOrder` is set.
521
+ */
522
+ sortGroups?: boolean;
505
523
  }
506
524
 
507
525
  export interface GridCellInfo {
@@ -635,6 +653,7 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
635
653
  declare onCreateIsRecordDraggable?: any;
636
654
  declare onRef?: any;
637
655
  declare preserveGroupOrder: boolean;
656
+ declare sortGroups: boolean;
638
657
  declare styled: boolean;
639
658
  declare selectable?: boolean;
640
659
  declare recordsAccessor: any;
@@ -844,6 +863,7 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
844
863
  sortOptions: this.sortOptions,
845
864
  groupings: grouping,
846
865
  preserveOrder: this.preserveGroupOrder,
866
+ sortGroupsBySorters: this.sortGroups,
847
867
  },
848
868
  this.dataAdapter,
849
869
  );
@@ -1903,6 +1923,7 @@ Grid.prototype.hoverChannel = "default";
1903
1923
  Grid.prototype.focusable = null; // automatically resolved
1904
1924
  Grid.prototype.allowsFileDrops = false;
1905
1925
  Grid.prototype.preserveGroupOrder = false;
1926
+ Grid.prototype.sortGroups = false;
1906
1927
 
1907
1928
  Widget.alias("grid", Grid);
1908
1929
  Localization.registerPrototype("cx/widgets/Grid", Grid);
@@ -2851,7 +2872,7 @@ class GridComponent extends VDOM.Component<GridComponentProps, GridComponentStat
2851
2872
  return (grid || row || column) && { grid, row, column };
2852
2873
  }
2853
2874
 
2854
- onDragEnd(e: DragEvent) {
2875
+ onDragEnd(e: DragEndEvent) {
2855
2876
  this.setState({
2856
2877
  dropTarget: null,
2857
2878
  dropInsertionIndex: null,
@@ -31,7 +31,7 @@ interface CaptureMouseOptions {
31
31
  export function captureMouse2(
32
32
  e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
33
33
  options: CaptureMouseOptions,
34
- ): void {
34
+ ): () => void {
35
35
  let surface = document.createElement("div");
36
36
  surface.className = "cxb-mousecapture";
37
37
  surface.style.cursor = options.cursor || getComputedStyle(e.currentTarget as Element).cursor;
@@ -67,6 +67,8 @@ export function captureMouse2(
67
67
 
68
68
  e.stopPropagation();
69
69
 
70
+ return tear;
71
+
70
72
  function move(e: Event) {
71
73
  if (!active) {
72
74
  tear();
@@ -110,7 +112,7 @@ export function captureMouse2(
110
112
  export function captureMouseOrTouch2(
111
113
  e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent,
112
114
  options: CaptureMouseOptions,
113
- ): void {
115
+ ): () => void {
114
116
  if (e.type.indexOf("touch") == 0) {
115
117
  let el = e.currentTarget as HTMLElement;
116
118
 
@@ -136,7 +138,12 @@ export function captureMouseOrTouch2(
136
138
  el.addEventListener("touchend", end);
137
139
 
138
140
  e.stopPropagation();
139
- } else captureMouse2(e, options);
141
+
142
+ return () => {
143
+ el.removeEventListener("touchmove", move);
144
+ el.removeEventListener("touchend", end);
145
+ };
146
+ } else return captureMouse2(e, options);
140
147
  }
141
148
 
142
149
  /**
@@ -153,8 +160,8 @@ export function captureMouse(
153
160
  onMouseUp?: (e: MouseEvent | TouchEvent, captureData?: any) => void,
154
161
  captureData?: any,
155
162
  cursor?: string,
156
- ): void {
157
- captureMouse2(e, {
163
+ ): () => void {
164
+ return captureMouse2(e, {
158
165
  onMouseMove,
159
166
  onMouseUp,
160
167
  captureData,
@@ -176,8 +183,8 @@ export function captureMouseOrTouch(
176
183
  onMouseUp?: (e: MouseEvent, captureData?: any) => void,
177
184
  captureData?: any,
178
185
  cursor?: string,
179
- ): void {
180
- captureMouseOrTouch2(e, { onMouseMove, onMouseUp, captureData, cursor });
186
+ ): () => void {
187
+ return captureMouseOrTouch2(e, { onMouseMove, onMouseUp, captureData, cursor });
181
188
  }
182
189
 
183
190
  /**