cx 26.4.4 → 26.6.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 (90) hide show
  1. package/build/charts/axis/Axis.d.ts +8 -0
  2. package/build/charts/axis/Axis.d.ts.map +1 -1
  3. package/build/charts/axis/Axis.js +18 -1
  4. package/build/charts/axis/TimeAxis.js +1 -0
  5. package/build/locale/de-de.js +1 -0
  6. package/build/locale/en-us.js +1 -0
  7. package/build/locale/es-es.js +1 -0
  8. package/build/locale/fr-fr.js +1 -0
  9. package/build/locale/nl-nl.js +1 -0
  10. package/build/locale/pt-pt.js +1 -0
  11. package/build/locale/sr-latn-ba.js +1 -0
  12. package/build/ui/Format.d.ts.map +1 -1
  13. package/build/ui/Format.js +26 -2
  14. package/build/util/Format.d.ts.map +1 -1
  15. package/build/util/Format.js +6 -0
  16. package/build/util/date/dateQuarter.d.ts +7 -0
  17. package/build/util/date/dateQuarter.d.ts.map +1 -0
  18. package/build/util/date/dateQuarter.js +8 -0
  19. package/build/util/date/dayBefore.d.ts +12 -0
  20. package/build/util/date/dayBefore.d.ts.map +1 -0
  21. package/build/util/date/dayBefore.js +15 -0
  22. package/build/util/date/index.d.ts +2 -0
  23. package/build/util/date/index.d.ts.map +1 -1
  24. package/build/util/date/index.js +2 -0
  25. package/build/widgets/form/DateTimePicker.d.ts.map +1 -1
  26. package/build/widgets/form/DateTimePicker.js +53 -31
  27. package/build/widgets/form/Field.d.ts.map +1 -1
  28. package/build/widgets/form/Field.js +2 -1
  29. package/build/widgets/form/LookupField.d.ts +6 -0
  30. package/build/widgets/form/LookupField.d.ts.map +1 -1
  31. package/build/widgets/form/LookupField.js +12 -0
  32. package/build/widgets/form/Wheel.d.ts +8 -0
  33. package/build/widgets/form/Wheel.d.ts.map +1 -1
  34. package/build/widgets/form/Wheel.js +30 -7
  35. package/build/widgets/grid/Grid.d.ts +6 -1
  36. package/build/widgets/grid/Grid.d.ts.map +1 -1
  37. package/build/widgets/grid/Grid.js +3 -2
  38. package/dist/charts.css +6 -0
  39. package/dist/charts.js +18 -1
  40. package/dist/manifest.js +880 -871
  41. package/dist/ui.js +33 -1
  42. package/dist/util.js +32 -0
  43. package/dist/widgets.css +4 -0
  44. package/dist/widgets.js +243 -175
  45. package/package.json +1 -1
  46. package/src/charts/BarGraph.scss +31 -31
  47. package/src/charts/Legend.scss +57 -57
  48. package/src/charts/LegendEntry.scss +35 -35
  49. package/src/charts/LineGraph.scss +28 -28
  50. package/src/charts/RangeMarker.scss +3 -0
  51. package/src/charts/axis/Axis.tsx +31 -1
  52. package/src/charts/axis/TimeAxis.tsx +1 -0
  53. package/src/charts/helpers/SnapPointFinder.ts +136 -136
  54. package/src/charts/helpers/ValueAtFinder.ts +72 -72
  55. package/src/charts/index.scss +1 -0
  56. package/src/data/AugmentedViewBase.ts +89 -89
  57. package/src/data/View.ts +301 -301
  58. package/src/data/createAccessorModelProxy.ts +66 -66
  59. package/src/locale/de-de.ts +1 -0
  60. package/src/locale/en-us.ts +1 -0
  61. package/src/locale/es-es.ts +1 -0
  62. package/src/locale/fr-fr.ts +1 -0
  63. package/src/locale/nl-nl.ts +1 -0
  64. package/src/locale/pt-pt.ts +1 -0
  65. package/src/locale/sr-latn-ba.ts +1 -0
  66. package/src/ui/Format.spec.ts +32 -0
  67. package/src/ui/Format.ts +27 -2
  68. package/src/ui/Repeater.spec.tsx +181 -181
  69. package/src/util/Format.spec.ts +11 -0
  70. package/src/util/Format.ts +7 -0
  71. package/src/util/date/dateQuarter.ts +8 -0
  72. package/src/util/date/dayBefore.ts +15 -0
  73. package/src/util/date/index.ts +2 -0
  74. package/src/util/scss/include.scss +69 -69
  75. package/src/widgets/Button.maps.scss +103 -103
  76. package/src/widgets/form/Calendar.tsx +772 -772
  77. package/src/widgets/form/DateTimePicker.tsx +453 -392
  78. package/src/widgets/form/Field.tsx +2 -1
  79. package/src/widgets/form/LookupField.spec.tsx +149 -0
  80. package/src/widgets/form/LookupField.tsx +27 -0
  81. package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
  82. package/src/widgets/form/Wheel.tsx +36 -7
  83. package/src/widgets/grid/Grid.scss +663 -657
  84. package/src/widgets/grid/Grid.tsx +9 -3
  85. package/src/widgets/grid/variables.scss +47 -47
  86. package/src/widgets/index.ts +63 -63
  87. package/src/widgets/nav/MenuItem.scss +150 -150
  88. package/src/widgets/nav/Tab.ts +122 -122
  89. package/src/widgets/overlay/Overlay.tsx +1029 -1029
  90. package/src/widgets/variables.scss +61 -61
@@ -312,6 +312,7 @@ export class Field<
312
312
  data._readOnly = data.readOnly;
313
313
  data._viewMode = data.mode === "view" || data.viewMode;
314
314
  data._tabOnEnterKey = data.tabOnEnterKey;
315
+ data._visited = data.visited;
315
316
  data.validationValue = this.getValidationValue(data);
316
317
  instance.parentDisabled = context.parentDisabled;
317
318
  instance.parentReadOnly = context.parentReadOnly;
@@ -366,7 +367,7 @@ export class Field<
366
367
  );
367
368
  data.visited = coalesce(
368
369
  context.parentStrict ? context.parentVisited : null,
369
- data.visited,
370
+ data._visited,
370
371
  context.parentVisited,
371
372
  );
372
373
 
@@ -1,5 +1,10 @@
1
1
  import { createAccessorModelProxy } from "../../data/createAccessorModelProxy";
2
2
  import { LookupField } from "./LookupField";
3
+ import { Store } from "../../data/Store";
4
+ import { ValidationGroup } from "./ValidationGroup";
5
+ import { bind } from "../../ui/bind";
6
+ import { createTestRenderer } from "../../util/test/createTestRenderer";
7
+ import assert from "assert";
3
8
 
4
9
  interface User {
5
10
  id: number;
@@ -90,4 +95,148 @@ describe("LookupField", () => {
90
95
  </cx>
91
96
  );
92
97
  });
98
+
99
+ describe("validateOptionExists", () => {
100
+ const options = [
101
+ { id: 1, text: "One" },
102
+ { id: 2, text: "Two" },
103
+ ];
104
+
105
+ it("reports an error when the selected value is missing from options", async () => {
106
+ let widget = (
107
+ <cx>
108
+ <ValidationGroup errors={bind("errors")}>
109
+ <LookupField
110
+ value={bind("value")}
111
+ text={bind("text")}
112
+ options={options}
113
+ validateOptionExists
114
+ />
115
+ </ValidationGroup>
116
+ </cx>
117
+ );
118
+
119
+ let store = new Store();
120
+ store.set("value", 99);
121
+ store.set("text", "Stale");
122
+
123
+ await createTestRenderer(store, widget);
124
+
125
+ let errors = store.get("errors");
126
+ assert.equal(errors.length, 1);
127
+ assert.equal(errors[0].message, "The selected option is no longer available.");
128
+ });
129
+
130
+ it("does not report an error when the selected value matches an option", async () => {
131
+ let widget = (
132
+ <cx>
133
+ <ValidationGroup errors={bind("errors")}>
134
+ <LookupField
135
+ value={bind("value")}
136
+ text={bind("text")}
137
+ options={options}
138
+ validateOptionExists
139
+ />
140
+ </ValidationGroup>
141
+ </cx>
142
+ );
143
+
144
+ let store = new Store();
145
+ store.set("value", 1);
146
+ store.set("text", "One");
147
+
148
+ await createTestRenderer(store, widget);
149
+
150
+ let errors = store.get("errors");
151
+ assert.equal(errors.length, 0);
152
+ });
153
+
154
+ it("does not report an error when the field is empty", async () => {
155
+ let widget = (
156
+ <cx>
157
+ <ValidationGroup errors={bind("errors")}>
158
+ <LookupField
159
+ value={bind("value")}
160
+ text={bind("text")}
161
+ options={options}
162
+ validateOptionExists
163
+ />
164
+ </ValidationGroup>
165
+ </cx>
166
+ );
167
+
168
+ let store = new Store();
169
+
170
+ await createTestRenderer(store, widget);
171
+
172
+ let errors = store.get("errors");
173
+ assert.equal(errors.length, 0);
174
+ });
175
+
176
+ it("does not report an error when options are not provided (server-side mode)", async () => {
177
+ let widget = (
178
+ <cx>
179
+ <ValidationGroup errors={bind("errors")}>
180
+ <LookupField
181
+ value={bind("value")}
182
+ text={bind("text")}
183
+ onQuery={() => []}
184
+ validateOptionExists
185
+ />
186
+ </ValidationGroup>
187
+ </cx>
188
+ );
189
+
190
+ let store = new Store();
191
+ store.set("value", 99);
192
+ store.set("text", "Stale");
193
+
194
+ await createTestRenderer(store, widget);
195
+
196
+ let errors = store.get("errors");
197
+ assert.equal(errors.length, 0);
198
+ });
199
+
200
+ it("reports an error in multiple mode when some ids are not in options", async () => {
201
+ let widget = (
202
+ <cx>
203
+ <ValidationGroup errors={bind("errors")}>
204
+ <LookupField
205
+ multiple
206
+ values={bind("values")}
207
+ options={options}
208
+ validateOptionExists
209
+ />
210
+ </ValidationGroup>
211
+ </cx>
212
+ );
213
+
214
+ let store = new Store();
215
+ store.set("values", [1, 99]);
216
+
217
+ await createTestRenderer(store, widget);
218
+
219
+ let errors = store.get("errors");
220
+ assert.equal(errors.length, 1);
221
+ });
222
+
223
+ it("does not validate by default (back-compat)", async () => {
224
+ let widget = (
225
+ <cx>
226
+ <ValidationGroup errors={bind("errors")}>
227
+ <LookupField value={bind("value")} text={bind("text")} options={options} />
228
+ </ValidationGroup>
229
+ </cx>
230
+ );
231
+
232
+ let store = new Store();
233
+ store.set("value", 99);
234
+ store.set("text", "Stale");
235
+
236
+ await createTestRenderer(store, widget);
237
+
238
+ let errors = store.get("errors");
239
+ assert.equal(errors.length, 0);
240
+ });
241
+ });
93
242
  });
@@ -117,6 +117,12 @@ interface LookupFieldBaseConfig<TOption = any> extends FieldConfig {
117
117
  /** Error message displayed if server query throws an exception. */
118
118
  queryErrorText?: string;
119
119
 
120
+ /** Set to `true` to report a validation error when the selected value is not present in `options`. Only applies when `options` is an array. Default is `false`. */
121
+ validateOptionExists?: boolean;
122
+
123
+ /** Error message displayed when the selected value is not present in `options`. */
124
+ invalidOptionText?: string;
125
+
120
126
  /** Message to be displayed if no entries match the user query. */
121
127
  noResultsText?: string;
122
128
 
@@ -292,6 +298,8 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
292
298
  declare public minOptionsForSearchField: number;
293
299
  declare public loadingText: string;
294
300
  declare public queryErrorText: string;
301
+ declare public validateOptionExists: boolean;
302
+ declare public invalidOptionText: string;
295
303
  declare public noResultsText: string;
296
304
  declare public optionIdField: string;
297
305
  declare public optionTextField: string;
@@ -501,6 +509,22 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
501
509
 
502
510
  (instance as DropdownInstance).lastDropdown = context.lastDropdown;
503
511
 
512
+ if (
513
+ this.validateOptionExists &&
514
+ isArray(data.options) &&
515
+ !this.isEmpty(data)
516
+ ) {
517
+ let invalid = this.multiple
518
+ ? isArray(data.values) && data.records!.length < data.values.length
519
+ : !data.options.some(($option) =>
520
+ areKeysEqual(
521
+ getOptionKey(this.keyBindings!, { $option }),
522
+ data.selectedKeys[0],
523
+ ),
524
+ );
525
+ if (invalid) data.error = this.invalidOptionText;
526
+ }
527
+
504
528
  super.prepareData(context, instance);
505
529
  }
506
530
 
@@ -604,6 +628,9 @@ LookupField.prototype.minOptionsForSearchField = 7;
604
628
  LookupField.prototype.loadingText = "Loading...";
605
629
  LookupField.prototype.queryErrorText =
606
630
  "Error occurred while querying for lookup data.";
631
+ LookupField.prototype.validateOptionExists = false;
632
+ LookupField.prototype.invalidOptionText =
633
+ "The selected option is no longer available.";
607
634
  LookupField.prototype.noResultsText = "No results found.";
608
635
  LookupField.prototype.optionIdField = "id";
609
636
  LookupField.prototype.optionTextField = "text";
@@ -1,8 +1,9 @@
1
1
  import { Store } from "../../data/Store";
2
2
  import { ValidationGroup } from "./ValidationGroup";
3
3
  import { Validator } from "./Validator";
4
+ import { TextField } from "./TextField";
4
5
  import { bind } from "../../ui/bind";
5
- import { createTestRenderer } from "../../util/test/createTestRenderer";
6
+ import { createTestRenderer, act } from "../../util/test/createTestRenderer";
6
7
 
7
8
  import assert from "assert";
8
9
 
@@ -107,6 +108,34 @@ describe("ValidationGroup", () => {
107
108
  assert(visited);
108
109
  });
109
110
 
111
+ it("visited flag changes are propagated to fields after being initialized to false", async () => {
112
+ let widget = (
113
+ <cx>
114
+ <ValidationGroup errors={bind("errors")} visited={bind("form.visited")}>
115
+ <TextField required value={bind("value1")} />
116
+ </ValidationGroup>
117
+ </cx>
118
+ );
119
+
120
+ let store = new Store();
121
+ //initializing the bound value to false used to shadow later updates
122
+ store.set("form.visited", false);
123
+
124
+ const component = await createTestRenderer(store, widget);
125
+
126
+ let errors = store.get("errors");
127
+ assert.equal(errors.length, 1);
128
+ assert.equal(errors[0].visited, false);
129
+
130
+ await act(async () => {
131
+ store.set("form.visited", true);
132
+ });
133
+
134
+ errors = store.get("errors");
135
+ assert.equal(errors.length, 1);
136
+ assert.equal(errors[0].visited, true);
137
+ });
138
+
110
139
  it("disabled flag can be overruled by the field props", async () => {
111
140
  let widget = (
112
141
  <cx>
@@ -65,6 +65,10 @@ Wheel.prototype.baseClass = "wheel";
65
65
  Wheel.prototype.size = 3;
66
66
  Wheel.prototype.styled = true;
67
67
 
68
+ /** A cyclic wheel renders its option list this many times, leaving scroll
69
+ * headroom on both sides of the centre copy. */
70
+ export const WHEEL_BUFFER_COPIES = 3;
71
+
68
72
  export interface WheelComponentProps {
69
73
  size: number;
70
74
  children: React.ReactNode[];
@@ -74,6 +78,11 @@ export interface WheelComponentProps {
74
78
  className?: string;
75
79
  style?: React.CSSProperties;
76
80
  index?: number;
81
+ /** Set to render the option list as an endlessly scrolling wheel. The list
82
+ * is rendered `WHEEL_BUFFER_COPIES` times and the wheel snaps to whichever
83
+ * copy of the selected value is nearest its current position, so a wrap
84
+ * scrolls seamlessly into the adjacent identical copy. */
85
+ cycle?: boolean;
77
86
  onChange: (newIndex: number) => void;
78
87
  onPipeKeyDown?: (fn: (e: React.KeyboardEvent) => void) => void;
79
88
  onMouseDown?: () => void;
@@ -109,23 +118,30 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
109
118
  scrollRef: (el: HTMLDivElement | null) => void;
110
119
 
111
120
  render(): React.ReactNode {
112
- let { size, children, CSS, baseClass, active, className, style, onMouseDown } = this.props;
121
+ let { size, children, CSS, baseClass, active, className, style, onMouseDown, cycle } = this.props;
113
122
  let optionClass = CSS.element(baseClass, "option");
114
123
  let dummyClass = CSS.element(baseClass, "option", { dummy: true });
115
124
 
125
+ // A cyclic wheel repeats the option list so it can scroll past either end.
126
+ let options = children;
127
+ if (cycle) {
128
+ options = [];
129
+ for (let i = 0; i < WHEEL_BUFFER_COPIES; i++) options.push(...children);
130
+ }
131
+
116
132
  let tpad = [],
117
133
  bpad = [],
118
134
  padSize = 0;
119
135
 
120
136
  for (let i = 0; i < (size - 1) / 2; i++) {
121
- tpad.push({ key: -1 - i, child: children[0], cls: dummyClass });
122
- bpad.push({ key: -100 - i, child: children[0], cls: dummyClass });
137
+ tpad.push({ key: -1 - i, child: options[0], cls: dummyClass });
138
+ bpad.push({ key: -100 - i, child: options[0], cls: dummyClass });
123
139
  padSize++;
124
140
  }
125
141
 
126
142
  let displayedOptions = [
127
143
  ...tpad,
128
- ...children.map((c, i) => ({
144
+ ...options.map((c, i) => ({
129
145
  key: i,
130
146
  child: c,
131
147
  cls: optionClass,
@@ -228,7 +244,19 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
228
244
  }
229
245
 
230
246
  UNSAFE_componentWillReceiveProps(props: WheelComponentProps): void {
231
- this.index = props.index || 0;
247
+ let newIndex = props.index || 0;
248
+ // A cyclic wheel renders the option list several times, so the same value
249
+ // appears in several copies. Snap to the copy nearest the wheel's current
250
+ // position rather than the one `props.index` names — a wrap then scrolls
251
+ // one item into the adjacent (identical) copy with no jump, instead of
252
+ // animating all the way back to the centre copy.
253
+ if (props.cycle) {
254
+ let period = props.children.length;
255
+ let value = ((newIndex % period) + period) % period;
256
+ newIndex = value + period * Math.round((this.index - value) / period);
257
+ newIndex = Math.max(0, Math.min(period * WHEEL_BUFFER_COPIES - 1, newIndex));
258
+ }
259
+ this.index = newIndex;
232
260
  this.scrollTo();
233
261
  }
234
262
 
@@ -272,8 +300,9 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
272
300
  }
273
301
 
274
302
  select(newIndex: number): void {
275
- let { children } = this.props;
276
- newIndex = Math.max(0, Math.min(children.length - 1, newIndex));
303
+ let { children, cycle } = this.props;
304
+ let length = children.length * (cycle ? WHEEL_BUFFER_COPIES : 1);
305
+ newIndex = Math.max(0, Math.min(length - 1, newIndex));
277
306
  if (this.index !== newIndex) {
278
307
  this.index = newIndex;
279
308
  this.props.onChange(newIndex);