@xh/hoist 80.0.0-SNAPSHOT.1768323341476 → 80.0.0-SNAPSHOT.1768415875152

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 (51) hide show
  1. package/CHANGELOG.md +27 -3
  2. package/admin/tabs/activity/tracking/ActivityTracking.scss +7 -7
  3. package/admin/tabs/userData/roles/details/RoleDetails.scss +6 -6
  4. package/build/types/cmp/form/FormModel.d.ts +1 -1
  5. package/build/types/cmp/form/field/BaseFieldModel.d.ts +7 -3
  6. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +3 -3
  7. package/build/types/cmp/grid/Grid.d.ts +2 -2
  8. package/build/types/data/Field.d.ts +2 -1
  9. package/build/types/data/Store.d.ts +7 -4
  10. package/build/types/data/StoreRecord.d.ts +5 -0
  11. package/build/types/data/impl/RecordValidator.d.ts +9 -10
  12. package/build/types/data/impl/StoreValidator.d.ts +8 -7
  13. package/build/types/data/index.d.ts +1 -0
  14. package/build/types/data/validation/Rule.d.ts +5 -40
  15. package/build/types/data/validation/Types.d.ts +56 -0
  16. package/build/types/data/validation/constraints.d.ts +1 -1
  17. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  18. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  19. package/cmp/form/FormModel.ts +2 -2
  20. package/cmp/form/field/BaseFieldModel.ts +38 -18
  21. package/cmp/form/field/SubformsFieldModel.ts +5 -5
  22. package/cmp/grid/Grid.scss +31 -8
  23. package/cmp/grid/columns/Column.ts +24 -10
  24. package/cmp/input/HoistInput.scss +19 -1
  25. package/cmp/input/HoistInputModel.ts +10 -2
  26. package/data/Field.ts +2 -1
  27. package/data/Store.ts +16 -4
  28. package/data/StoreRecord.ts +11 -0
  29. package/data/impl/RecordValidator.ts +46 -28
  30. package/data/impl/StoreValidator.ts +22 -9
  31. package/data/index.ts +1 -0
  32. package/data/validation/Rule.ts +12 -52
  33. package/data/validation/Types.ts +81 -0
  34. package/data/validation/constraints.ts +2 -1
  35. package/desktop/appcontainer/OptionsDialog.scss +1 -1
  36. package/desktop/cmp/form/FormField.scss +136 -42
  37. package/desktop/cmp/form/FormField.ts +74 -40
  38. package/desktop/cmp/input/CodeInput.scss +13 -1
  39. package/desktop/cmp/input/RadioInput.scss +16 -4
  40. package/desktop/cmp/input/SwitchInput.scss +23 -5
  41. package/desktop/cmp/input/TextArea.scss +9 -1
  42. package/desktop/cmp/rest/impl/RestForm.scss +1 -1
  43. package/desktop/cmp/viewmanager/ViewManager.scss +7 -15
  44. package/kit/blueprint/styles.scss +4 -4
  45. package/kit/onsen/styles.scss +10 -2
  46. package/mobile/cmp/form/FormField.scss +52 -19
  47. package/mobile/cmp/form/FormField.ts +30 -21
  48. package/package.json +1 -1
  49. package/styles/XH.scss +1 -0
  50. package/styles/vars.scss +77 -12
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -19,6 +19,7 @@ import {
19
19
  } from '@xh/hoist/core';
20
20
  import '@xh/hoist/desktop/register';
21
21
  import {instanceManager} from '@xh/hoist/core/impl/InstanceManager';
22
+ import {maxSeverity, ValidationResult} from '@xh/hoist/data';
22
23
  import {fmtDate, fmtDateTime, fmtJson, fmtNumber} from '@xh/hoist/format';
23
24
  import {Icon} from '@xh/hoist/icon';
24
25
  import {tooltip} from '@xh/hoist/kit/blueprint';
@@ -26,7 +27,7 @@ import {isLocalDate} from '@xh/hoist/utils/datetime';
26
27
  import {errorIf, getTestId, logWarn, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
27
28
  import {getLayoutProps, getReactElementName, useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
28
29
  import classNames from 'classnames';
29
- import {isBoolean, isDate, isEmpty, isFinite, isNil, isUndefined, kebabCase} from 'lodash';
30
+ import {first, isBoolean, isDate, isEmpty, isFinite, isNil, isUndefined, kebabCase} from 'lodash';
30
31
  import {Children, cloneElement, ReactElement, ReactNode, useContext, useState} from 'react';
31
32
  import './FormField.scss';
32
33
 
@@ -122,16 +123,19 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
122
123
  const isRequired = model?.isRequired || false,
123
124
  readonly = model?.readonly || false,
124
125
  disabled = props.disabled || model?.disabled,
125
- validationDisplayed = model?.validationDisplayed || false,
126
- notValid = model?.isNotValid || false,
127
- displayNotValid = validationDisplayed && notValid,
128
- errors = model?.errors || [],
126
+ severityToDisplay = model?.validationDisplayed
127
+ ? maxSeverity(model.validationResults)
128
+ : null,
129
+ displayInvalid = severityToDisplay === 'error',
130
+ validationResultsToDisplay = severityToDisplay
131
+ ? model.validationResults.filter(v => v.severity === severityToDisplay)
132
+ : [],
129
133
  requiredStr = defaultProp('requiredIndicator', props, formContext, '*'),
130
134
  requiredIndicator =
131
135
  isRequired && !readonly && requiredStr
132
136
  ? span({
133
137
  item: ' ' + requiredStr,
134
- className: 'xh-form-field-required-indicator'
138
+ className: 'xh-form-field__required-indicator'
135
139
  })
136
140
  : null;
137
141
 
@@ -163,19 +167,24 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
163
167
 
164
168
  // Styles
165
169
  const classes = [];
166
- if (childElementName) classes.push(`xh-form-field-${kebabCase(childElementName)}`);
167
- if (isRequired) classes.push('xh-form-field-required');
168
- if (inline) classes.push('xh-form-field-inline');
169
- if (minimal) classes.push('xh-form-field-minimal');
170
- if (readonly) classes.push('xh-form-field-readonly');
171
- if (disabled) classes.push('xh-form-field-disabled');
172
- if (displayNotValid) classes.push('xh-form-field-invalid');
170
+ if (childElementName) classes.push(`xh-form-field--${kebabCase(childElementName)}`);
171
+ if (isRequired) classes.push('xh-form-field--required');
172
+ if (inline) classes.push('xh-form-field--inline');
173
+ if (minimal) classes.push('xh-form-field--minimal');
174
+ if (readonly) classes.push('xh-form-field--readonly');
175
+ if (disabled) classes.push('xh-form-field--disabled');
176
+
177
+ if (severityToDisplay) {
178
+ classes.push(`xh-form-field--${severityToDisplay}`);
179
+ if (displayInvalid) classes.push('xh-form-field--invalid');
180
+ }
173
181
 
182
+ // Test ID handling
174
183
  const testId = getFormFieldTestId(props, formContext, model?.name);
175
184
  useOnMount(() => instanceManager.registerModelWithTestId(testId, model));
176
185
  useOnUnmount(() => instanceManager.unregisterModelWithTestId(testId));
177
186
 
178
- // generate actual element child to render
187
+ // Generate actual element child to render
179
188
  let childEl: ReactElement =
180
189
  !child || readonly
181
190
  ? readonlyChild({
@@ -189,7 +198,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
189
198
  childIsSizeable,
190
199
  childId,
191
200
  disabled,
192
- displayNotValid,
201
+ displayNotValid: severityToDisplay === 'error',
193
202
  leftErrorIcon,
194
203
  commitOnChange,
195
204
  testId: getTestId(testId, 'input')
@@ -198,16 +207,42 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
198
207
  if (minimal) {
199
208
  childEl = tooltip({
200
209
  item: childEl,
201
- className: `xh-input ${displayNotValid ? 'xh-input-invalid' : ''}`,
210
+ className: classNames(
211
+ 'xh-input',
212
+ severityToDisplay && `xh-input--${severityToDisplay}`,
213
+ displayInvalid && 'xh-input--invalid'
214
+ ),
202
215
  targetTagName:
203
216
  !blockChildren.includes(childElementName) || childWidth ? 'span' : 'div',
204
217
  position: tooltipPosition,
205
218
  boundary: tooltipBoundary,
206
- disabled: !displayNotValid,
207
- content: getErrorTooltipContent(errors)
219
+ disabled: !severityToDisplay,
220
+ content: getValidationTooltipContent(validationResultsToDisplay)
208
221
  });
209
222
  }
210
223
 
224
+ // Generate inlined validation messages, if any to show and not rendering in minimal mode.
225
+ let validationMsgEl: ReactElement = null;
226
+ if (severityToDisplay && !minimal) {
227
+ const validationMsgCls = `xh-form-field__inner__validation-msg xh-form-field__inner__validation-msg--${severityToDisplay}`,
228
+ firstMsg = first(validationResultsToDisplay)?.message;
229
+
230
+ validationMsgEl =
231
+ validationResultsToDisplay.length > 1
232
+ ? tooltip({
233
+ openOnTargetFocus: false,
234
+ className: validationMsgCls,
235
+ item: firstMsg + ' (...)',
236
+ content: getValidationTooltipContent(
237
+ validationResultsToDisplay
238
+ ) as ReactElement
239
+ })
240
+ : div({
241
+ className: validationMsgCls,
242
+ item: firstMsg
243
+ });
244
+ }
245
+
211
246
  return box({
212
247
  ref,
213
248
  key: model?.xhId,
@@ -217,7 +252,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
217
252
  items: [
218
253
  labelEl({
219
254
  omit: !label,
220
- className: 'xh-form-field-label',
255
+ className: 'xh-form-field__label',
221
256
  items: [label, requiredIndicator],
222
257
  htmlFor: clickableLabel ? childId : null,
223
258
  style: {
@@ -228,23 +263,19 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
228
263
  }),
229
264
  div({
230
265
  className: classNames(
231
- 'xh-form-field-inner',
232
- childIsSizeable ? 'xh-form-field-inner--flex' : 'xh-form-field-inner--block'
266
+ 'xh-form-field__inner',
267
+ childIsSizeable
268
+ ? 'xh-form-field__inner--flex'
269
+ : 'xh-form-field__inner--block'
233
270
  ),
234
271
  items: [
235
272
  childEl,
236
273
  div({
237
- className: 'xh-form-field-info',
274
+ className: 'xh-form-field__inner__info-msg',
238
275
  omit: !info,
239
276
  item: info
240
277
  }),
241
- tooltip({
242
- omit: minimal || !displayNotValid,
243
- openOnTargetFocus: false,
244
- className: 'xh-form-field-error-msg',
245
- item: errors ? errors[0] : null,
246
- content: getErrorTooltipContent(errors) as ReactElement
247
- })
278
+ validationMsgEl
248
279
  ]
249
280
  })
250
281
  ]
@@ -262,7 +293,7 @@ const readonlyChild = hoistCmp.factory<ReadonlyChildProps>({
262
293
  render({model, readonlyRenderer, testId}) {
263
294
  const value = model ? model['value'] : null;
264
295
  return div({
265
- className: 'xh-form-field-readonly-display',
296
+ className: 'xh-form-field__readonly-display',
266
297
  [TEST_ID]: testId,
267
298
  item: readonlyRenderer(value, model)
268
299
  });
@@ -323,7 +354,6 @@ const editableChild = hoistCmp.factory<FieldModel>({
323
354
  //--------------------------------
324
355
  // Helper Functions
325
356
  //---------------------------------
326
-
327
357
  export function defaultReadonlyRenderer(value: any): ReactNode {
328
358
  if (isLocalDate(value)) return fmtDate(value);
329
359
  if (isDate(value)) return fmtDateTime(value);
@@ -359,21 +389,25 @@ function getValidChild(children) {
359
389
  return child;
360
390
  }
361
391
 
362
- function getErrorTooltipContent(errors: string[]): ReactElement | string {
363
- // If no errors, something other than null must be returned.
392
+ function getValidationTooltipContent(validationResults: ValidationResult[]): ReactElement | string {
393
+ // If no ValidationResults, something other than null must be returned.
364
394
  // If null is returned, as of Blueprint v5, the Blueprint Tooltip component causes deep re-renders of its target
365
395
  // when content changes from null <-> not null.
366
396
  // In `formField` `minimal:true` mode with `commitonchange:true`, this causes the
367
397
  // TextInput component to lose focus when its validation state changes, which is undesirable.
368
398
  // It is not clear if this is a bug or intended behavior in BP v5, but this workaround prevents the issue.
369
399
  // `Tooltip:content` has been a required prop since at least BP v4, but something about the way it is used in BP v5 changed.
370
- if (isEmpty(errors)) return 'Is Valid';
371
-
372
- if (errors.length === 1) return errors[0];
373
- return ul({
374
- className: 'xh-form-field-error-tooltip',
375
- items: errors.map((it, idx) => li({key: idx, item: it}))
376
- });
400
+ if (isEmpty(validationResults)) {
401
+ return 'Is Valid';
402
+ } else if (validationResults.length === 1) {
403
+ return first(validationResults).message;
404
+ } else {
405
+ const severity = first(validationResults).severity;
406
+ return ul({
407
+ className: `xh-form-field__validation-tooltip xh-form-field__validation-tooltip--${severity}`,
408
+ items: validationResults.map((it, idx) => li({key: idx, item: it.message}))
409
+ });
410
+ }
377
411
  }
378
412
 
379
413
  function defaultProp<N extends keyof Partial<FormFieldProps>>(
@@ -29,12 +29,24 @@
29
29
  .xh-code-input {
30
30
  height: 100px;
31
31
 
32
- &.xh-input-invalid {
32
+ &.xh-input--invalid {
33
33
  div.CodeMirror {
34
34
  border: var(--xh-form-field-invalid-border);
35
35
  }
36
36
  }
37
37
 
38
+ &.xh-input--warning {
39
+ div.CodeMirror {
40
+ border: var(--xh-form-field-warning-border);
41
+ }
42
+ }
43
+
44
+ &.xh-input--info {
45
+ div.CodeMirror {
46
+ border: var(--xh-form-field-info-border);
47
+ }
48
+ }
49
+
38
50
  &.xh-input-disabled {
39
51
  .CodeMirror {
40
52
  background-color: var(--xh-input-disabled-bg);
@@ -10,13 +10,25 @@
10
10
  margin-right: var(--xh-pad-double-px);
11
11
  }
12
12
 
13
- &.xh-input-invalid .xh-radio-input-option .bp6-control-indicator {
14
- border: var(--xh-form-field-invalid-border);
15
-
16
- &::before {
13
+ &.xh-input--invalid,
14
+ &.xh-input--warning,
15
+ &.xh-input--info {
16
+ .xh-radio-input-option .bp6-control-indicator::before {
17
17
  margin: -1px;
18
18
  }
19
19
  }
20
+
21
+ &.xh-input--invalid .xh-radio-input-option .bp6-control-indicator {
22
+ border: var(--xh-form-field-invalid-border);
23
+ }
24
+
25
+ &.xh-input--warning .xh-radio-input-option .bp6-control-indicator {
26
+ border: var(--xh-form-field-warning-border);
27
+ }
28
+
29
+ &.xh-input--info .xh-radio-input-option .bp6-control-indicator {
30
+ border: var(--xh-form-field-info-border);
31
+ }
20
32
  }
21
33
 
22
34
  // Toolbar specific style
@@ -5,12 +5,30 @@
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- .xh-switch-input.xh-input-invalid {
9
- .bp6-control-indicator {
10
- border: var(--xh-form-field-invalid-border);
11
-
12
- &::before {
8
+ .xh-switch-input {
9
+ &.xh-input--invalid,
10
+ &.xh-input--warning,
11
+ &.xh-input--info {
12
+ .bp6-control-indicator::before {
13
13
  margin: 1px;
14
14
  }
15
15
  }
16
+
17
+ &.xh-input--invalid {
18
+ .bp6-control-indicator {
19
+ border: var(--xh-form-field-invalid-border);
20
+ }
21
+ }
22
+
23
+ &.xh-input--warning {
24
+ .bp6-control-indicator {
25
+ border: var(--xh-form-field-warning-border);
26
+ }
27
+ }
28
+
29
+ &.xh-input--info {
30
+ .bp6-control-indicator {
31
+ border: var(--xh-form-field-info-border);
32
+ }
33
+ }
16
34
  }
@@ -12,7 +12,15 @@
12
12
  // Suppress resize handles added by browser - we want to manage the size more closely.
13
13
  resize: none;
14
14
 
15
- &.xh-input-invalid {
15
+ &.xh-input--invalid {
16
16
  border: var(--xh-form-field-invalid-border);
17
17
  }
18
+
19
+ &.xh-input--warning {
20
+ border: var(--xh-form-field-warning-border);
21
+ }
22
+
23
+ &.xh-input--info {
24
+ border: var(--xh-form-field-info-border);
25
+ }
18
26
  }
@@ -18,7 +18,7 @@
18
18
  border: 1px solid var(--xh-form-field-box-shadow-color-top);
19
19
  }
20
20
 
21
- .xh-form-field-label {
21
+ .xh-form-field__label {
22
22
  margin-right: var(--xh-pad-px);
23
23
  font-weight: bold;
24
24
  }
@@ -9,27 +9,19 @@
9
9
  &__form {
10
10
  padding: var(--xh-pad-px);
11
11
 
12
- .xh-form-field.xh-form-field-readonly {
13
- &:not(.xh-form-field-inline) {
14
- .xh-form-field-label {
15
- border-bottom: var(--xh-border-solid);
12
+ .xh-form-field {
13
+ &--readonly {
14
+ &:not(.xh-form-field--inline) {
15
+ .xh-form-field__label {
16
+ border-bottom: var(--xh-border-solid);
17
+ }
16
18
  }
17
19
  }
18
20
 
19
- .xh-form-field-readonly-display {
21
+ &__readonly-display {
20
22
  padding: 0;
21
23
  }
22
24
  }
23
-
24
- .xh-form-field .xh-form-field-info {
25
- line-height: 1.5em;
26
- margin-top: var(--xh-pad-half-px);
27
- white-space: unset;
28
-
29
- .xh-icon {
30
- margin-right: 2px;
31
- }
32
- }
33
25
  }
34
26
 
35
27
  &__metadata {
@@ -350,15 +350,15 @@ textarea.bp6-input,
350
350
  z-index: 16;
351
351
  }
352
352
 
353
- // Controls ship with default bottom and (inline) right margins. We expose variables
354
- // to customize and default those to 0 to avoid adding margins by default.
353
+ // Controls ship with default bottom and (inline) right margins.
354
+ // We zero those out here so padding can be re-applied at HoistInput or FormField layer.
355
355
  // Hoist theme text-color applied to elements not styled with a more specific selector.
356
356
  .bp6-control {
357
357
  color: var(--xh-text-color);
358
- margin-bottom: var(--xh-form-field-margin-bottom);
358
+ margin-bottom: 0;
359
359
 
360
360
  &.bp6-inline {
361
- margin-right: var(--xh-form-field-margin-right);
361
+ margin-inline-end: 0;
362
362
  }
363
363
  }
364
364
 
@@ -24,13 +24,21 @@
24
24
  border: var(--xh-border-solid);
25
25
  border-radius: var(--xh-border-radius-px);
26
26
 
27
- &:not(.xh-input-invalid):focus-within {
27
+ &:not(.xh-input--invalid):not(.xh-input--warning):not(.xh-input--info):focus-within {
28
28
  border-color: var(--xh-focus-outline-color);
29
29
  }
30
30
 
31
- &.xh-input-invalid {
31
+ &.xh-input--invalid {
32
32
  border: var(--xh-form-field-invalid-border);
33
33
  }
34
+
35
+ &.xh-input--warning {
36
+ border: var(--xh-form-field-warning-border);
37
+ }
38
+
39
+ &.xh-input--info {
40
+ border: var(--xh-form-field-info-border);
41
+ }
34
42
  }
35
43
  }
36
44
 
@@ -1,12 +1,30 @@
1
1
  .xh-form-field {
2
2
  display: flex;
3
3
  flex-direction: column;
4
+ padding: var(--xh-form-field-padding);
5
+ margin: var(--xh-form-field-margin);
4
6
 
5
- .xh-form-field-label {
6
- padding: 0 0 3px;
7
+ &__label {
8
+ color: var(--xh-form-field-label-color);
9
+ font-size: var(--xh-form-field-label-font-size);
10
+ font-style: var(--xh-form-field-label-font-style);
11
+ font-weight: var(--xh-form-field-label-font-weight);
12
+ text-transform: var(--xh-form-field-label-text-transform);
13
+
14
+ // Borders + padding (not inline)
15
+ border-bottom: var(--xh-form-field-label-border-bottom);
16
+ margin: var(--xh-form-field-label-margin);
17
+ padding: var(--xh-form-field-label-padding);
18
+
19
+ // Borders + padding (inline)
20
+ .xh-form-field--inline & {
21
+ border-bottom: var(--xh-form-field-label-inline-border-bottom);
22
+ margin: var(--xh-form-field-label-inline-margin);
23
+ padding: var(--xh-form-field-label-inline-padding);
24
+ }
7
25
  }
8
26
 
9
- .xh-form-field-inner {
27
+ &__inner {
10
28
  // Used for unsizeable children
11
29
  &--block {
12
30
  display: block;
@@ -21,31 +39,46 @@
21
39
  }
22
40
  }
23
41
 
24
- .xh-form-field-info,
25
- .xh-form-field-error-msg,
26
- .xh-form-field-pending-msg {
27
- font-size: var(--xh-font-size-small-px);
28
- line-height: calc(var(--xh-font-size-small-px) + var(--xh-pad-px));
29
- color: var(--xh-text-color-muted);
30
- white-space: nowrap;
31
- text-overflow: ellipsis;
32
- overflow: hidden;
42
+ &__info-msg,
43
+ &__validation-msg {
44
+ font-size: var(--xh-form-field-msg-font-size);
45
+ line-height: var(--xh-form-field-msg-line-height);
46
+ margin: var(--xh-form-field-msg-margin);
47
+ text-transform: var(--xh-form-field-msg-text-transform);
48
+ }
49
+
50
+ &__validation-msg {
51
+ &--error {
52
+ color: var(--xh-form-field-invalid-color);
53
+ }
54
+
55
+ &--info {
56
+ color: var(--xh-form-field-info-color);
57
+ }
58
+
59
+ &--warning {
60
+ color: var(--xh-form-field-warning-color);
61
+ }
62
+ }
63
+
64
+ &--invalid .xh-form-field__label {
65
+ color: var(--xh-form-field-invalid-color);
33
66
  }
34
67
 
35
- .xh-form-field-error-msg {
36
- color: var(--xh-red);
68
+ &--warning .xh-form-field__label {
69
+ color: var(--xh-form-field-warning-color);
37
70
  }
38
71
 
39
- &.xh-form-field-invalid .xh-form-field-label {
40
- color: var(--xh-red);
72
+ &--info .xh-form-field__label {
73
+ color: var(--xh-form-field-info-color);
41
74
  }
42
75
 
43
- &.xh-form-field-readonly {
44
- .xh-form-field-error-msg {
76
+ &.xh-form-field--readonly {
77
+ .xh-form-field__validation-msg {
45
78
  display: none;
46
79
  }
47
80
 
48
- .xh-form-field-readonly-display {
81
+ .xh-form-field__readonly-display {
49
82
  padding: var(--xh-pad-px) 0;
50
83
  }
51
84
  }
@@ -8,6 +8,7 @@ import composeRefs from '@seznam/compose-react-refs/composeRefs';
8
8
  import {FieldModel, FormContext, FormContextType, BaseFormFieldProps} from '@xh/hoist/cmp/form';
9
9
  import {box, div, span} from '@xh/hoist/cmp/layout';
10
10
  import {DefaultHoistProps, hoistCmp, HoistProps, TestSupportProps, uses, XH} from '@xh/hoist/core';
11
+ import {maxSeverity} from '@xh/hoist/data';
11
12
  import {fmtDate, fmtDateTime, fmtNumber} from '@xh/hoist/format';
12
13
  import {label as labelCmp} from '@xh/hoist/mobile/cmp/input';
13
14
  import '@xh/hoist/mobile/register';
@@ -15,7 +16,7 @@ import {isLocalDate} from '@xh/hoist/utils/datetime';
15
16
  import {errorIf, throwIf, withDefault} from '@xh/hoist/utils/js';
16
17
  import {getLayoutProps} from '@xh/hoist/utils/react';
17
18
  import classNames from 'classnames';
18
- import {isBoolean, isDate, isEmpty, isFinite, isUndefined} from 'lodash';
19
+ import {first, isBoolean, isDate, isEmpty, isFinite, isUndefined} from 'lodash';
19
20
  import {Children, cloneElement, ReactNode, useContext} from 'react';
20
21
  import './FormField.scss';
21
22
 
@@ -65,16 +66,19 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
65
66
  const isRequired = model?.isRequired || false,
66
67
  readonly = model?.readonly || false,
67
68
  disabled = props.disabled || model?.disabled,
68
- validationDisplayed = model?.validationDisplayed || false,
69
- notValid = model?.isNotValid || false,
70
- displayNotValid = validationDisplayed && notValid,
71
- errors = model?.errors || [],
69
+ severityToDisplay = model?.validationDisplayed
70
+ ? maxSeverity(model.validationResults)
71
+ : null,
72
+ displayInvalid = severityToDisplay === 'error',
73
+ validationResultsToDisplay = severityToDisplay
74
+ ? model.validationResults.filter(v => v.severity === severityToDisplay)
75
+ : [],
72
76
  requiredStr = defaultProp('requiredIndicator', props, formContext, '*'),
73
77
  requiredIndicator =
74
78
  isRequired && !readonly && requiredStr
75
79
  ? span({
76
80
  item: ' ' + requiredStr,
77
- className: 'xh-form-field-required-indicator'
81
+ className: 'xh-form-field__required-indicator'
78
82
  })
79
83
  : null,
80
84
  isPending = model && model.isValidationPending;
@@ -97,11 +101,14 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
97
101
 
98
102
  // Styles
99
103
  const classes = [];
100
- if (isRequired) classes.push('xh-form-field-required');
101
- if (minimal) classes.push('xh-form-field-minimal');
102
- if (readonly) classes.push('xh-form-field-readonly');
103
- if (disabled) classes.push('xh-form-field-disabled');
104
- if (displayNotValid) classes.push('xh-form-field-invalid');
104
+ if (isRequired) classes.push('xh-form-field--required');
105
+ if (minimal) classes.push('xh-form-field--minimal');
106
+ if (readonly) classes.push('xh-form-field--readonly');
107
+ if (disabled) classes.push('xh-form-field--disabled');
108
+ if (severityToDisplay) {
109
+ classes.push(`xh-form-field--${severityToDisplay}`);
110
+ if (displayInvalid) classes.push('xh-form-field--invalid');
111
+ }
105
112
 
106
113
  let childEl =
107
114
  readonly || !child
@@ -124,30 +131,32 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
124
131
  items: [
125
132
  labelCmp({
126
133
  omit: !label,
127
- className: 'xh-form-field-label',
134
+ className: 'xh-form-field__label',
128
135
  items: [label, requiredIndicator]
129
136
  }),
130
137
  div({
131
138
  className: classNames(
132
- 'xh-form-field-inner',
133
- childIsSizeable ? 'xh-form-field-inner--flex' : 'xh-form-field-inner--block'
139
+ 'xh-form-field__inner',
140
+ childIsSizeable
141
+ ? 'xh-form-field__inner--flex'
142
+ : 'xh-form-field__inner--block'
134
143
  ),
135
144
  items: [
136
145
  childEl,
137
146
  div({
138
147
  omit: !info,
139
- className: 'xh-form-field-info',
148
+ className: 'xh-form-field__info-msg',
140
149
  item: info
141
150
  }),
142
151
  div({
143
- omit: minimal || !isPending || !validationDisplayed,
144
- className: 'xh-form-field-pending-msg',
152
+ omit: minimal || !isPending || !severityToDisplay,
153
+ className: `xh-form-field__validation-msg xh-form-field__validation-msg--pending`,
145
154
  item: 'Validating...'
146
155
  }),
147
156
  div({
148
- omit: minimal || !displayNotValid,
149
- className: 'xh-form-field-error-msg',
150
- items: notValid ? errors[0] : null
157
+ omit: minimal || !severityToDisplay,
158
+ className: `xh-form-field__validation-msg xh-form-field__validation-msg--${severityToDisplay}`,
159
+ item: first(validationResultsToDisplay)?.message
151
160
  })
152
161
  ]
153
162
  })
@@ -166,7 +175,7 @@ const readonlyChild = hoistCmp.factory<ReadonlyChildProps>({
166
175
  render({model, readonlyRenderer}) {
167
176
  const value = model ? model['value'] : null;
168
177
  return div({
169
- className: 'xh-form-field-readonly-display',
178
+ className: 'xh-form-field__readonly-display',
170
179
  item: readonlyRenderer(value, model)
171
180
  });
172
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "80.0.0-SNAPSHOT.1768323341476",
3
+ "version": "80.0.0-SNAPSHOT.1768415875152",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
package/styles/XH.scss CHANGED
@@ -19,6 +19,7 @@ body.xh-app {
19
19
  border-color: var(--xh-border-color);
20
20
  color: var(--xh-text-color);
21
21
  font-family: var(--xh-font-family);
22
+ font-feature-settings: var(--xh-font-feature-settings);
22
23
  // Important for default Inter font to ensure numbers are constant-width and line up properly.
23
24
  font-variant-numeric: tabular-nums;
24
25
  font-size: var(--xh-font-size-px);