@flywheel-io/vision 21.1.2 → 21.1.3

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.
@@ -3177,10 +3177,26 @@ class FwDialogComponent {
3177
3177
  return classes;
3178
3178
  }
3179
3179
  ngOnInit() {
3180
- this.dialogRef?.containerInstance?._addAriaLabelledBy(this.headerId);
3180
+ // `<fw-dialog>` is usable outside an overlay, so there may be no dialogRef at all.
3181
+ const container = this.dialogRef?.containerInstance;
3182
+ if (container) {
3183
+ // Deferred by one microtask on purpose: `_addAriaLabelledBy` mutates the container's
3184
+ // `_ariaLabelledByQueue`, which `CdkDialogContainer` exposes as a host binding. A component's
3185
+ // host bindings are evaluated in its parent view *before* Angular descends into child
3186
+ // components, so mutating the queue synchronously here changes a value the container already
3187
+ // read in the same change-detection pass, which throws NG0100 in dev builds. Same approach as
3188
+ // Angular Material's `MatDialogLayoutSection`.
3189
+ Promise.resolve().then(() => container._addAriaLabelledBy(this.headerId));
3190
+ }
3181
3191
  }
3182
3192
  ngOnDestroy() {
3183
- this.dialogRef?.containerInstance?._removeAriaLabelledBy(this.headerId);
3193
+ const container = this.dialogRef?.containerInstance;
3194
+ if (container) {
3195
+ // Deferred to stay ordered behind the deferred add in `ngOnInit` — otherwise a dialog created
3196
+ // and destroyed within the same task would remove the id before it was ever added and leave
3197
+ // it orphaned in the queue.
3198
+ Promise.resolve().then(() => container._removeAriaLabelledBy(this.headerId));
3199
+ }
3184
3200
  }
3185
3201
  handleCloseButton() {
3186
3202
  this.closeWithAnimation();
@@ -4324,6 +4340,16 @@ class FwMenuItemComponent {
4324
4340
  this.focused = model(false, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
4325
4341
  this.selected = model(false, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
4326
4342
  this.subscriptions = [];
4343
+ this.isDestroyed = false;
4344
+ }
4345
+ /**
4346
+ * Whether this item's view has been torn down. Owners that iterate their items (`fw-menu`) have to
4347
+ * check this before writing to the item's `model()`s: neither `QueryList` nor a signal query is
4348
+ * emptied by view destruction, so an item can still be reachable from a query after the menu it
4349
+ * lives in was destroyed - writing to it then logs NG0953.
4350
+ */
4351
+ get destroyed() {
4352
+ return this.isDestroyed;
4327
4353
  }
4328
4354
  scrollIntoView(options = { behavior: 'smooth', block: 'nearest' }) {
4329
4355
  // eslint-disable-next-line @rx-angular/prefer-no-layout-sensitive-apis
@@ -4341,6 +4367,7 @@ class FwMenuItemComponent {
4341
4367
  this.updateLayout();
4342
4368
  }
4343
4369
  ngOnDestroy() {
4370
+ this.isDestroyed = true;
4344
4371
  for (const subscription of this.subscriptions) {
4345
4372
  subscription.unsubscribe();
4346
4373
  }
@@ -4487,9 +4514,15 @@ class FwMenuComponent {
4487
4514
  }
4488
4515
  writeValue(value) {
4489
4516
  this.value.set(value);
4517
+ // Lay the items out BEFORE calling out to any listener. `change` is wired to `fw-select`'s
4518
+ // `handleClick`, which closes the options panel and so synchronously destroys every
4519
+ // `fw-menu-item` rendered in it. View teardown does not empty a `QueryList`, so laying out
4520
+ // afterwards writes to `model()`s on already-destroyed nodes, which logs NG0953 once per item
4521
+ // whose `selected` flag changed. Everything `updateLayout()` reads derives from `this.value()`,
4522
+ // set on the line above, so the resulting layout is identical either way.
4523
+ this.updateLayout();
4490
4524
  this.onChange(value);
4491
4525
  this.change.emit(value);
4492
- this.updateLayout();
4493
4526
  }
4494
4527
  registerOnChange(fn) {
4495
4528
  this.onChange = fn;
@@ -4528,6 +4561,12 @@ class FwMenuComponent {
4528
4561
  if (this.menuItems) {
4529
4562
  const itemRole = this.computeItemRole();
4530
4563
  this.menuItems.forEach((item) => {
4564
+ // A destroyed item is still reachable through the QueryList; writing to its models would
4565
+ // emit on a dead OutputRef (NG0953). Belt and braces for any other listener that tears the
4566
+ // menu down from underneath a layout pass.
4567
+ if (item.destroyed) {
4568
+ return;
4569
+ }
4531
4570
  item.itemRole.set(itemRole);
4532
4571
  const size = this.size();
4533
4572
  if (size !== undefined) {
@@ -6361,7 +6400,9 @@ class FwSelectMenuComponent {
6361
6400
  return displayFn(selectedMenuItem);
6362
6401
  }
6363
6402
  const selectedOption = options.find((opt) => opt[this.valueProperty()]?.toString() === currentValue);
6364
- return selectedOption?.[this.titleProperty()] || '';
6403
+ // `||` would swallow a legitimate `0` title and leave the input showing its placeholder, and it
6404
+ // would also let a non-string title through into this `linkedSignal<string>` untouched
6405
+ return String(selectedOption?.[this.titleProperty()] ?? '');
6365
6406
  }, ...(ngDevMode ? [{ debugName: "selectTitle" }] : /* istanbul ignore next */ []));
6366
6407
  // Watch for menu items changes and re-subscribe
6367
6408
  this.menuItemsWatcher = effect(() => {
@@ -8526,8 +8567,12 @@ class FwStepperComponent {
8526
8567
  return;
8527
8568
  }
8528
8569
  this.activeStep.set(step);
8529
- this.stepChange.emit(parseInt(step?.toString())); // weirdly passing out step directly fails the eqeqeq
8570
+ // Same ordering rule as FwMenuComponent.writeValue: lay the steps out before calling out to a
8571
+ // listener, so a handler that removes the stepper cannot leave `updateSteps()` writing to
8572
+ // `model()`s on destroyed `fw-step`s (NG0953). `updateSteps()` only reads `activeStep()`, set
8573
+ // on the line above, so the layout is unchanged.
8530
8574
  this.updateSteps();
8575
+ this.stepChange.emit(parseInt(step?.toString())); // weirdly passing out step directly fails the eqeqeq
8531
8576
  }
8532
8577
  updateSteps() {
8533
8578
  if (this.steps && this.steps.length > 0) {