@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.
- package/CHANGELOG.md +27 -3
- package/admin/tabs/activity/tracking/ActivityTracking.scss +7 -7
- package/admin/tabs/userData/roles/details/RoleDetails.scss +6 -6
- package/build/types/cmp/form/FormModel.d.ts +1 -1
- package/build/types/cmp/form/field/BaseFieldModel.d.ts +7 -3
- package/build/types/cmp/form/field/SubformsFieldModel.d.ts +3 -3
- package/build/types/cmp/grid/Grid.d.ts +2 -2
- package/build/types/data/Field.d.ts +2 -1
- package/build/types/data/Store.d.ts +7 -4
- package/build/types/data/StoreRecord.d.ts +5 -0
- package/build/types/data/impl/RecordValidator.d.ts +9 -10
- package/build/types/data/impl/StoreValidator.d.ts +8 -7
- package/build/types/data/index.d.ts +1 -0
- package/build/types/data/validation/Rule.d.ts +5 -40
- package/build/types/data/validation/Types.d.ts +56 -0
- package/build/types/data/validation/constraints.d.ts +1 -1
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
- package/cmp/form/FormModel.ts +2 -2
- package/cmp/form/field/BaseFieldModel.ts +38 -18
- package/cmp/form/field/SubformsFieldModel.ts +5 -5
- package/cmp/grid/Grid.scss +31 -8
- package/cmp/grid/columns/Column.ts +24 -10
- package/cmp/input/HoistInput.scss +19 -1
- package/cmp/input/HoistInputModel.ts +10 -2
- package/data/Field.ts +2 -1
- package/data/Store.ts +16 -4
- package/data/StoreRecord.ts +11 -0
- package/data/impl/RecordValidator.ts +46 -28
- package/data/impl/StoreValidator.ts +22 -9
- package/data/index.ts +1 -0
- package/data/validation/Rule.ts +12 -52
- package/data/validation/Types.ts +81 -0
- package/data/validation/constraints.ts +2 -1
- package/desktop/appcontainer/OptionsDialog.scss +1 -1
- package/desktop/cmp/form/FormField.scss +136 -42
- package/desktop/cmp/form/FormField.ts +74 -40
- package/desktop/cmp/input/CodeInput.scss +13 -1
- package/desktop/cmp/input/RadioInput.scss +16 -4
- package/desktop/cmp/input/SwitchInput.scss +23 -5
- package/desktop/cmp/input/TextArea.scss +9 -1
- package/desktop/cmp/rest/impl/RestForm.scss +1 -1
- package/desktop/cmp/viewmanager/ViewManager.scss +7 -15
- package/kit/blueprint/styles.scss +4 -4
- package/kit/onsen/styles.scss +10 -2
- package/mobile/cmp/form/FormField.scss +52 -19
- package/mobile/cmp/form/FormField.ts +30 -21
- package/package.json +1 -1
- package/styles/XH.scss +1 -0
- package/styles/vars.scss +77 -12
- 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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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-
|
|
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
|
|
167
|
-
if (isRequired) classes.push('xh-form-field
|
|
168
|
-
if (inline) classes.push('xh-form-field
|
|
169
|
-
if (minimal) classes.push('xh-form-field
|
|
170
|
-
if (readonly) classes.push('xh-form-field
|
|
171
|
-
if (disabled) classes.push('xh-form-field
|
|
172
|
-
|
|
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
|
-
//
|
|
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:
|
|
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: !
|
|
207
|
-
content:
|
|
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-
|
|
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-
|
|
232
|
-
childIsSizeable
|
|
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-
|
|
274
|
+
className: 'xh-form-field__inner__info-msg',
|
|
238
275
|
omit: !info,
|
|
239
276
|
item: info
|
|
240
277
|
}),
|
|
241
|
-
|
|
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-
|
|
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
|
|
363
|
-
// If no
|
|
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(
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
}
|
|
@@ -9,27 +9,19 @@
|
|
|
9
9
|
&__form {
|
|
10
10
|
padding: var(--xh-pad-px);
|
|
11
11
|
|
|
12
|
-
.xh-form-field
|
|
13
|
-
|
|
14
|
-
.xh-form-field
|
|
15
|
-
|
|
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
|
-
|
|
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.
|
|
354
|
-
//
|
|
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:
|
|
358
|
+
margin-bottom: 0;
|
|
359
359
|
|
|
360
360
|
&.bp6-inline {
|
|
361
|
-
margin-
|
|
361
|
+
margin-inline-end: 0;
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
|
package/kit/onsen/styles.scss
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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-
|
|
36
|
-
color: var(--xh-
|
|
68
|
+
&--warning .xh-form-field__label {
|
|
69
|
+
color: var(--xh-form-field-warning-color);
|
|
37
70
|
}
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
color: var(--xh-
|
|
72
|
+
&--info .xh-form-field__label {
|
|
73
|
+
color: var(--xh-form-field-info-color);
|
|
41
74
|
}
|
|
42
75
|
|
|
43
|
-
&.xh-form-field
|
|
44
|
-
.xh-form-
|
|
76
|
+
&.xh-form-field--readonly {
|
|
77
|
+
.xh-form-field__validation-msg {
|
|
45
78
|
display: none;
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
.xh-form-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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-
|
|
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
|
|
101
|
-
if (minimal) classes.push('xh-form-field
|
|
102
|
-
if (readonly) classes.push('xh-form-field
|
|
103
|
-
if (disabled) classes.push('xh-form-field
|
|
104
|
-
if (
|
|
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-
|
|
134
|
+
className: 'xh-form-field__label',
|
|
128
135
|
items: [label, requiredIndicator]
|
|
129
136
|
}),
|
|
130
137
|
div({
|
|
131
138
|
className: classNames(
|
|
132
|
-
'xh-form-
|
|
133
|
-
childIsSizeable
|
|
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-
|
|
148
|
+
className: 'xh-form-field__info-msg',
|
|
140
149
|
item: info
|
|
141
150
|
}),
|
|
142
151
|
div({
|
|
143
|
-
omit: minimal || !isPending || !
|
|
144
|
-
className:
|
|
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 || !
|
|
149
|
-
className:
|
|
150
|
-
|
|
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-
|
|
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.
|
|
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);
|