mn-angular-lib 1.0.99 → 1.0.101

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.
@@ -253,6 +253,11 @@ const mnButtonVariants = tv({
253
253
  disabled: {
254
254
  true: 'opacity-50 pointer-events-none',
255
255
  },
256
+ // Busy state: dim and block interaction like `disabled`, and hint the wait cursor.
257
+ // The spinner itself is rendered in the template (see mn-button.html).
258
+ loading: {
259
+ true: 'opacity-50 pointer-events-none cursor-wait',
260
+ },
256
261
  wrap: {
257
262
  true: 'whitespace-normal',
258
263
  false: 'whitespace-nowrap',
@@ -333,6 +338,7 @@ const mnButtonVariants = tv({
333
338
  color: 'primary',
334
339
  borderRadius: 'lg',
335
340
  disabled: false,
341
+ loading: false,
336
342
  wrap: false,
337
343
  hover: true,
338
344
  },
@@ -340,6 +346,7 @@ const mnButtonVariants = tv({
340
346
 
341
347
  class MnButton {
342
348
  data = {};
349
+ // A loading button is treated as non-interactive: it blocks clicks and is unfocusable,
343
350
  // Bind the computed classes to the host element
344
351
  get hostClasses() {
345
352
  return mnButtonVariants({
@@ -348,33 +355,45 @@ class MnButton {
348
355
  color: this.data.color,
349
356
  borderRadius: this.data.borderRadius,
350
357
  disabled: this.data.disabled,
358
+ loading: this.data.loading,
351
359
  wrap: this.data.wrap,
352
360
  hover: this.data.hover,
353
361
  });
354
362
  }
363
+ // Signals assistive tech that the control is busy while a loading action runs.
364
+ get ariaBusy() {
365
+ return this.data.loading ? 'true' : null;
366
+ }
355
367
  // For accessibility (works for both <button> and <a>)
356
368
  get ariaDisabled() {
357
369
  return this.data.disabled ? 'true' : null;
358
370
  }
359
371
  // Only meaningful for <button>. For <a> it does nothing semantically.
360
372
  get disabledAttr() {
361
- return this.data.disabled ? '' : null;
373
+ return this.isBlocked ? '' : null;
362
374
  }
363
- // Make disabled anchors unfocusable + prevent activation
375
+ // Make disabled/loading anchors unfocusable + prevent activation
364
376
  get tabIndex() {
365
- return this.data.disabled ? '-1' : null;
377
+ return this.isBlocked ? '-1' : null;
378
+ }
379
+ // so an in-flight action cannot be triggered a second time.
380
+ get isBlocked() {
381
+ return !!(this.data.disabled || this.data.loading);
366
382
  }
367
383
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
368
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnButton, isStandalone: true, selector: "button[mnButton], a[mnButton]", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses", "attr.aria-disabled": "this.ariaDisabled", "attr.disabled": "this.disabledAttr", "attr.tabindex": "this.tabIndex" } }, ngImport: i0, template: "<ng-content></ng-content>\n" });
384
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnButton, isStandalone: true, selector: "button[mnButton], a[mnButton]", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses", "attr.aria-busy": "this.ariaBusy", "attr.aria-disabled": "this.ariaDisabled", "attr.disabled": "this.disabledAttr", "attr.tabindex": "this.tabIndex" } }, ngImport: i0, template: "@if (data.loading) {\n <span\n aria-hidden=\"true\"\n class=\"inline-block h-4 w-4 shrink-0 mr-2 animate-spin rounded-full border-2 border-current border-r-transparent\"\n ></span>\n}\n<ng-content></ng-content>\n" });
369
385
  }
370
386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnButton, decorators: [{
371
387
  type: Component,
372
- args: [{ selector: 'button[mnButton], a[mnButton]', standalone: true, template: "<ng-content></ng-content>\n" }]
388
+ args: [{ selector: 'button[mnButton], a[mnButton]', standalone: true, template: "@if (data.loading) {\n <span\n aria-hidden=\"true\"\n class=\"inline-block h-4 w-4 shrink-0 mr-2 animate-spin rounded-full border-2 border-current border-r-transparent\"\n ></span>\n}\n<ng-content></ng-content>\n" }]
373
389
  }], propDecorators: { data: [{
374
390
  type: Input
375
391
  }], hostClasses: [{
376
392
  type: HostBinding,
377
393
  args: ['class']
394
+ }], ariaBusy: [{
395
+ type: HostBinding,
396
+ args: ['attr.aria-busy']
378
397
  }], ariaDisabled: [{
379
398
  type: HostBinding,
380
399
  args: ['attr.aria-disabled']
@@ -6621,7 +6640,14 @@ class MnModalShellComponent {
6621
6640
  config;
6622
6641
  modalRef;
6623
6642
  isClosing = false;
6624
- isStacked = false;
6643
+ /**
6644
+ * Whether another modal is stacked on top of this one. Set imperatively by
6645
+ * `MnModalService` on the already-rendered shell below the newly opened one, so it must
6646
+ * be a signal: mutating a plain field there changes the host `[class]` after the view was
6647
+ * checked (NG0100 ExpressionChangedAfterItHasBeenChecked) and does not schedule change
6648
+ * detection in a zoneless app. A signal write both notifies the host binding and schedules CD.
6649
+ */
6650
+ isStacked = signal(false, ...(ngDevMode ? [{ debugName: "isStacked" }] : []));
6625
6651
  ModalKind = ModalKind;
6626
6652
  previouslyFocusedElement = null;
6627
6653
  focusTrapListener = null;
@@ -6796,7 +6822,7 @@ class MnModalShellComponent {
6796
6822
  ? this.config.animation
6797
6823
  : this.config.animation?.type || 'slide';
6798
6824
  const animation = ` anim-${animType}`;
6799
- const stacked = this.isStacked ? ' is-stacked' : '';
6825
+ const stacked = this.isStacked() ? ' is-stacked' : '';
6800
6826
  const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
6801
6827
  const swiping = this.swipeDismissing ? ' swipe-dismissing' : '';
6802
6828
  return `modal-shell modal-${size}${animation}${stacked}${mobileSheet}${swiping}`;
@@ -7061,7 +7087,7 @@ class MnModalService {
7061
7087
  // Update stack and dim previous modal
7062
7088
  if (this.modalStack.length > 0) {
7063
7089
  const prevModal = this.modalStack[this.modalStack.length - 1];
7064
- prevModal.component.isStacked = true;
7090
+ prevModal.component.isStacked.set(true);
7065
7091
  }
7066
7092
  this.modalStack.push(modalRef);
7067
7093
  // Attach to application
@@ -7078,7 +7104,7 @@ class MnModalService {
7078
7104
  this.modalStack.splice(index, 1);
7079
7105
  if (this.modalStack.length > 0) {
7080
7106
  const topModal = this.modalStack[this.modalStack.length - 1];
7081
- topModal.component.isStacked = false;
7107
+ topModal.component.isStacked.set(false);
7082
7108
  }
7083
7109
  }
7084
7110
  });