@flywheel-io/vision 21.8.1 → 21.8.2

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.
@@ -2803,11 +2803,11 @@ class FwRadioComponent {
2803
2803
  this.change.emit(event?.target?.value);
2804
2804
  }
2805
2805
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FwRadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2806
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: FwRadioComponent, isStandalone: true, selector: "fw-radio-button", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, focused: { classPropertyName: "focused", publicName: "focused", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", group: "groupChange", disabled: "disabledChange", size: "sizeChange", color: "colorChange", focused: "focusedChange", change: "change" }, ngImport: i0, template: "<div [ngClass]=\"['fw-radio-wrapper', disabled() ? 'disabled' : '']\">\n <div [ngClass]=\"radioStyles\">\n <input\n type=\"radio\"\n class=\"fw-radio\"\n [id]=\"group() + value()\"\n [value]=\"value()\"\n [checked]=\"checked()\"\n [name]=\"group()\"\n [disabled]=\"disabled()\"\n (change)=\"handleChange($event)\"\n (focusout)=\"focused.set(false)\"\n (focusin)=\"focused.set(true)\"\n />\n </div>\n\n @if (title()) {\n <label [htmlFor]=\"group() + value()\" class=\"vision-h4\"\n >{{ title() }}\n <ng-content></ng-content>\n </label>\n }\n</div>\n", styles: [":host .fw-radio-wrapper{display:flex;align-items:center;width:fit-content;cursor:pointer}:host .fw-radio-wrapper label{margin:0 6px 0 0;color:var(--typography-base)}:host .fw-radio-wrapper label:empty{display:none}:host .fw-radio-wrapper .fw-radio{height:36px;width:36px;display:flex;align-items:center;justify-content:center;border-radius:50%}:host .fw-radio-wrapper .fw-radio:hover{background-color:var(--slate-hover)}:host .fw-radio-wrapper .fw-radio.focused{background-color:var(--slate-focus)}:host .fw-radio-wrapper .fw-radio input[type=radio]{appearance:none;display:inline-block;width:20px;height:20px;margin:6px;background-clip:content-box;border:2px solid var(--typography-light);background-color:transparent;border-radius:50%;cursor:pointer}:host .fw-radio-wrapper .fw-radio input[type=radio]:checked{width:16px;height:16px;background-color:var(--typography-light);outline:2px solid var(--typography-light);border:2px solid white;margin:8px}:host .fw-radio-wrapper .fw-radio.primary.focused{background-color:var(--primary-focus)}:host .fw-radio-wrapper .fw-radio.primary.focused input[type=radio]{border:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.primary input[type=radio]:checked{border:2px solid white;background-color:var(--primary-base);outline:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.secondary.focused{background-color:var(--secondary-focus)}:host .fw-radio-wrapper .fw-radio.secondary.focused input[type=radio]{border:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.secondary input[type=radio]:checked{border:2px solid white;background-color:var(--secondary-base);outline:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.disabled{cursor:not-allowed}:host .fw-radio-wrapper .fw-radio.disabled input[type=radio]:checked{border:2px solid white;background-color:var(--typography-light);outline:2px solid var(--typography-light)}:host .fw-radio-wrapper .fw-radio.compact{width:30px;height:30px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]{width:18px;height:18px;margin:5px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]:checked{width:14px;height:14px;margin:7px}:host .fw-radio-wrapper.disabled{cursor:not-allowed;opacity:.4}:host .fw-radio-wrapper.disabled label{color:var(--typography-light)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
2806
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: FwRadioComponent, isStandalone: true, selector: "fw-radio-button", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, focused: { classPropertyName: "focused", publicName: "focused", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", group: "groupChange", disabled: "disabledChange", size: "sizeChange", color: "colorChange", focused: "focusedChange", change: "change" }, ngImport: i0, template: "<label [ngClass]=\"['fw-radio-wrapper', disabled() ? 'disabled' : '']\">\n <div [ngClass]=\"radioStyles\">\n <input\n type=\"radio\"\n class=\"fw-radio\"\n [id]=\"group() + value()\"\n [value]=\"value()\"\n [checked]=\"checked()\"\n [name]=\"group()\"\n [disabled]=\"disabled()\"\n (change)=\"handleChange($event)\"\n (focusout)=\"focused.set(false)\"\n (focusin)=\"focused.set(true)\"\n />\n </div>\n\n @if (title()) {\n <span class=\"fw-radio-label vision-h4\"\n >{{ title() }}\n <ng-content></ng-content>\n </span>\n }\n</label>\n", styles: [":host .fw-radio-wrapper{display:flex;align-items:center;width:fit-content;margin:0;cursor:pointer}:host .fw-radio-wrapper .fw-radio-label{margin:0 6px 0 0;color:var(--typography-base)}:host .fw-radio-wrapper .fw-radio-label:empty{display:none}:host .fw-radio-wrapper .fw-radio{height:36px;width:36px;display:flex;align-items:center;justify-content:center;border-radius:50%}:host .fw-radio-wrapper .fw-radio:hover{background-color:var(--slate-hover)}:host .fw-radio-wrapper .fw-radio.focused{background-color:var(--slate-focus)}:host .fw-radio-wrapper .fw-radio input[type=radio]{appearance:none;display:inline-block;width:20px;height:20px;margin:6px;background-clip:content-box;border:2px solid var(--typography-light);background-color:transparent;border-radius:50%;cursor:inherit}:host .fw-radio-wrapper .fw-radio input[type=radio]:checked{width:16px;height:16px;background-color:var(--typography-light);outline:2px solid var(--typography-light);border:2px solid white;margin:8px}:host .fw-radio-wrapper .fw-radio.primary.focused{background-color:var(--primary-focus)}:host .fw-radio-wrapper .fw-radio.primary.focused input[type=radio]{border:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.primary input[type=radio]:checked{border:2px solid white;background-color:var(--primary-base);outline:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.secondary.focused{background-color:var(--secondary-focus)}:host .fw-radio-wrapper .fw-radio.secondary.focused input[type=radio]{border:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.secondary input[type=radio]:checked{border:2px solid white;background-color:var(--secondary-base);outline:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.disabled{cursor:not-allowed}:host .fw-radio-wrapper .fw-radio.disabled input[type=radio]:checked{border:2px solid white;background-color:var(--typography-light);outline:2px solid var(--typography-light)}:host .fw-radio-wrapper .fw-radio.compact{width:30px;height:30px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]{width:18px;height:18px;margin:5px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]:checked{width:14px;height:14px;margin:7px}:host .fw-radio-wrapper.disabled{cursor:not-allowed;opacity:.4}:host .fw-radio-wrapper.disabled .fw-radio-label{color:var(--typography-light)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
2807
2807
  }
2808
2808
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: FwRadioComponent, decorators: [{
2809
2809
  type: Component,
2810
- args: [{ selector: 'fw-radio-button', imports: [NgClass], template: "<div [ngClass]=\"['fw-radio-wrapper', disabled() ? 'disabled' : '']\">\n <div [ngClass]=\"radioStyles\">\n <input\n type=\"radio\"\n class=\"fw-radio\"\n [id]=\"group() + value()\"\n [value]=\"value()\"\n [checked]=\"checked()\"\n [name]=\"group()\"\n [disabled]=\"disabled()\"\n (change)=\"handleChange($event)\"\n (focusout)=\"focused.set(false)\"\n (focusin)=\"focused.set(true)\"\n />\n </div>\n\n @if (title()) {\n <label [htmlFor]=\"group() + value()\" class=\"vision-h4\"\n >{{ title() }}\n <ng-content></ng-content>\n </label>\n }\n</div>\n", styles: [":host .fw-radio-wrapper{display:flex;align-items:center;width:fit-content;cursor:pointer}:host .fw-radio-wrapper label{margin:0 6px 0 0;color:var(--typography-base)}:host .fw-radio-wrapper label:empty{display:none}:host .fw-radio-wrapper .fw-radio{height:36px;width:36px;display:flex;align-items:center;justify-content:center;border-radius:50%}:host .fw-radio-wrapper .fw-radio:hover{background-color:var(--slate-hover)}:host .fw-radio-wrapper .fw-radio.focused{background-color:var(--slate-focus)}:host .fw-radio-wrapper .fw-radio input[type=radio]{appearance:none;display:inline-block;width:20px;height:20px;margin:6px;background-clip:content-box;border:2px solid var(--typography-light);background-color:transparent;border-radius:50%;cursor:pointer}:host .fw-radio-wrapper .fw-radio input[type=radio]:checked{width:16px;height:16px;background-color:var(--typography-light);outline:2px solid var(--typography-light);border:2px solid white;margin:8px}:host .fw-radio-wrapper .fw-radio.primary.focused{background-color:var(--primary-focus)}:host .fw-radio-wrapper .fw-radio.primary.focused input[type=radio]{border:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.primary input[type=radio]:checked{border:2px solid white;background-color:var(--primary-base);outline:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.secondary.focused{background-color:var(--secondary-focus)}:host .fw-radio-wrapper .fw-radio.secondary.focused input[type=radio]{border:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.secondary input[type=radio]:checked{border:2px solid white;background-color:var(--secondary-base);outline:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.disabled{cursor:not-allowed}:host .fw-radio-wrapper .fw-radio.disabled input[type=radio]:checked{border:2px solid white;background-color:var(--typography-light);outline:2px solid var(--typography-light)}:host .fw-radio-wrapper .fw-radio.compact{width:30px;height:30px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]{width:18px;height:18px;margin:5px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]:checked{width:14px;height:14px;margin:7px}:host .fw-radio-wrapper.disabled{cursor:not-allowed;opacity:.4}:host .fw-radio-wrapper.disabled label{color:var(--typography-light)}\n"] }]
2810
+ args: [{ selector: 'fw-radio-button', imports: [NgClass], template: "<label [ngClass]=\"['fw-radio-wrapper', disabled() ? 'disabled' : '']\">\n <div [ngClass]=\"radioStyles\">\n <input\n type=\"radio\"\n class=\"fw-radio\"\n [id]=\"group() + value()\"\n [value]=\"value()\"\n [checked]=\"checked()\"\n [name]=\"group()\"\n [disabled]=\"disabled()\"\n (change)=\"handleChange($event)\"\n (focusout)=\"focused.set(false)\"\n (focusin)=\"focused.set(true)\"\n />\n </div>\n\n @if (title()) {\n <span class=\"fw-radio-label vision-h4\"\n >{{ title() }}\n <ng-content></ng-content>\n </span>\n }\n</label>\n", styles: [":host .fw-radio-wrapper{display:flex;align-items:center;width:fit-content;margin:0;cursor:pointer}:host .fw-radio-wrapper .fw-radio-label{margin:0 6px 0 0;color:var(--typography-base)}:host .fw-radio-wrapper .fw-radio-label:empty{display:none}:host .fw-radio-wrapper .fw-radio{height:36px;width:36px;display:flex;align-items:center;justify-content:center;border-radius:50%}:host .fw-radio-wrapper .fw-radio:hover{background-color:var(--slate-hover)}:host .fw-radio-wrapper .fw-radio.focused{background-color:var(--slate-focus)}:host .fw-radio-wrapper .fw-radio input[type=radio]{appearance:none;display:inline-block;width:20px;height:20px;margin:6px;background-clip:content-box;border:2px solid var(--typography-light);background-color:transparent;border-radius:50%;cursor:inherit}:host .fw-radio-wrapper .fw-radio input[type=radio]:checked{width:16px;height:16px;background-color:var(--typography-light);outline:2px solid var(--typography-light);border:2px solid white;margin:8px}:host .fw-radio-wrapper .fw-radio.primary.focused{background-color:var(--primary-focus)}:host .fw-radio-wrapper .fw-radio.primary.focused input[type=radio]{border:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.primary input[type=radio]:checked{border:2px solid white;background-color:var(--primary-base);outline:2px solid var(--primary-base)}:host .fw-radio-wrapper .fw-radio.secondary.focused{background-color:var(--secondary-focus)}:host .fw-radio-wrapper .fw-radio.secondary.focused input[type=radio]{border:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.secondary input[type=radio]:checked{border:2px solid white;background-color:var(--secondary-base);outline:2px solid var(--secondary-base)}:host .fw-radio-wrapper .fw-radio.disabled{cursor:not-allowed}:host .fw-radio-wrapper .fw-radio.disabled input[type=radio]:checked{border:2px solid white;background-color:var(--typography-light);outline:2px solid var(--typography-light)}:host .fw-radio-wrapper .fw-radio.compact{width:30px;height:30px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]{width:18px;height:18px;margin:5px}:host .fw-radio-wrapper .fw-radio.compact input[type=radio]:checked{width:14px;height:14px;margin:7px}:host .fw-radio-wrapper.disabled{cursor:not-allowed;opacity:.4}:host .fw-radio-wrapper.disabled .fw-radio-label{color:var(--typography-light)}\n"] }]
2811
2811
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }, { type: i0.Output, args: ["groupChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }, { type: i0.Output, args: ["sizeChange"] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }, { type: i0.Output, args: ["colorChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], focused: [{ type: i0.Input, args: [{ isSignal: true, alias: "focused", required: false }] }, { type: i0.Output, args: ["focusedChange"] }], change: [{
2812
2812
  type: Output
2813
2813
  }] } });
@@ -7160,6 +7160,17 @@ var FieldType;
7160
7160
  FieldType["Float"] = "float";
7161
7161
  })(FieldType || (FieldType = {}));
7162
7162
 
7163
+ // Field types with no keystroke-by-keystroke "typing" concept — their value only ever changes
7164
+ // as a single discrete commit (a click/selection), so visibility/requiredWhenVisible rules that
7165
+ // depend on them can and should be recomputed the instant that value changes, rather than
7166
+ // waiting for an unrelated focusout elsewhere on the form (see recomputeIfDiscreteChanged).
7167
+ const DISCRETE_FIELD_TYPES = new Set([
7168
+ FieldType.Checkbox,
7169
+ FieldType.Switch,
7170
+ FieldType.Radio,
7171
+ FieldType.Date,
7172
+ FieldType.Select,
7173
+ ]);
7163
7174
  /**
7164
7175
  * Renders a {@link CustomForm} schema as a reactive form, resolving JSONLogic-driven
7165
7176
  * visibility and validation rules for each field.
@@ -7298,8 +7309,17 @@ class FwFormRendererComponent {
7298
7309
  const formGroup = new FormGroup(controls, {
7299
7310
  validators: (fg) => this.validateFields(fg, fields),
7300
7311
  });
7312
+ let previousValues = this.normalizeValues(formGroup.value, fields);
7301
7313
  formGroup.valueChanges.pipe(takeUntil(this.formRebuilt$), takeUntilDestroyed(this.destroyRef)).subscribe((values) => {
7302
7314
  const normalized = this.normalizeValues(values, fields);
7315
+ if (this.recomputeIfDiscreteChanged(fields, previousValues, normalized)) {
7316
+ // validateFields already ran for this tick (before valueChanges fired) against the
7317
+ // previous requiredByKey — re-run it so errors/form.valid reflect the new required
7318
+ // state now rather than on the next blur. emitEvent: false avoids re-entering this
7319
+ // subscription; the outer updateValueAndValidity still emits the refreshed status.
7320
+ formGroup.updateValueAndValidity({ emitEvent: false });
7321
+ }
7322
+ previousValues = normalized;
7303
7323
  if (this.submitted() || fields.some((field) => formGroup.get(field.key)?.touched)) {
7304
7324
  // Errors/errorText were just recomputed by the FormGroup validator (validateFields)
7305
7325
  // as part of this same value-change tick — emit a new array reference so the
@@ -7322,8 +7342,9 @@ class FwFormRendererComponent {
7322
7342
  * re-runs validation so the field that was just blurred shows its errorText immediately
7323
7343
  * (validateFields only assigns errorText for touched controls, and the blurred control's
7324
7344
  * CVA already marked it touched before this native `focusout` handler runs). Wired to the
7325
- * form's `focusout` so fields only appear/disappear/error once a field loses focus, rather
7326
- * than on every keystroke.
7345
+ * form's `focusout` so continuous (typed) fields only appear/disappear/error once a field
7346
+ * loses focus, rather than on every keystroke. Discrete fields (radio/select/checkbox/switch/
7347
+ * date) don't need to wait for this — see recomputeIfDiscreteChanged.
7327
7348
  */
7328
7349
  onFieldBlur() {
7329
7350
  const formGroup = this.form();
@@ -7331,12 +7352,39 @@ class FwFormRendererComponent {
7331
7352
  return;
7332
7353
  }
7333
7354
  const normalized = this.normalizeValues(formGroup.value, this.currentFields);
7355
+ this.applyVisibility(normalized);
7356
+ formGroup.updateValueAndValidity();
7357
+ this.flatFields.set([...this.currentFields]);
7358
+ }
7359
+ /**
7360
+ * Recomputes visibility immediately (independent of focus/blur) when a *discrete* field's
7361
+ * value changed on this tick — e.g. a radio/select/checkbox/switch/date selection. Those
7362
+ * controls commit their value in a single click rather than keystroke-by-keystroke, so there's
7363
+ * no "flicker while typing" concern gating them behind onFieldBlur, unlike text-style inputs.
7364
+ *
7365
+ * This also sidesteps a DOM event-ordering race specific to controls whose native element
7366
+ * fires its own `focusout` (e.g. a radio `<input>`): clicking a different option in the same
7367
+ * group fires `focusout` on the previously-focused option — bubbling up and running
7368
+ * onFieldBlur — before `change` fires on the newly-clicked option and writes its value into
7369
+ * the FormGroup. onFieldBlur alone would therefore always evaluate against the *previous*
7370
+ * selection. Recomputing here, from the value-change itself, evaluates against the value that
7371
+ * actually just changed.
7372
+ *
7373
+ * Returns whether visibility was recomputed, so the caller can re-run validation against the
7374
+ * updated requiredByKey.
7375
+ */
7376
+ recomputeIfDiscreteChanged(fields, previousValues, normalized) {
7377
+ const discreteChanged = fields.some((field) => DISCRETE_FIELD_TYPES.has(field.type) && previousValues[field.key] !== normalized[field.key]);
7378
+ if (discreteChanged) {
7379
+ this.applyVisibility(normalized);
7380
+ }
7381
+ return discreteChanged;
7382
+ }
7383
+ applyVisibility(normalized) {
7334
7384
  const { hide, required, hideSections } = this.computeVisibility(this.currentFields, normalized);
7335
7385
  this.hideByKey.set(hide);
7336
7386
  this.requiredByKey.set(required);
7337
7387
  this.hideSectionByKey.set(hideSections);
7338
- formGroup.updateValueAndValidity();
7339
- this.flatFields.set([...this.currentFields]);
7340
7388
  }
7341
7389
  /**
7342
7390
  * Marks every control touched/dirty and triggers validation immediately, then flips