@geosdi/ngx-ui 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -691,9 +691,12 @@ Selector: `ml-column-filter`. [Source](src/lib/table/table.component.ts).
691
691
 
692
692
  Selector: `ml-tabs`. [Source](src/lib/tabs/tabs.component.ts).
693
693
 
694
- | Binding | Kind | Type | Declaration / default |
695
- | ------- | ----- | ------------------ | --------------------- |
696
- | `value` | model | `ModelSignal<any>` | `model<any>()` |
694
+ | Binding | Kind | Type | Declaration / default |
695
+ | --------------------- | ----- | ------------------ | --------------------- |
696
+ | `value` | model | `ModelSignal<any>` | `model<any>()` |
697
+ | `scrollable` | input | boolean (coerced) | `false` |
698
+ | `previousButtonLabel` | input | string | `Previous tabs` |
699
+ | `nextButtonLabel` | input | string | `Next tabs` |
697
700
 
698
701
  ### TabComponent
699
702
 
@@ -780,3 +783,34 @@ manual release workflow after committing the versioned source.
780
783
  - Independent CSS theme, local SVG icons and public Sass entry points.
781
784
  - `ml-*` component selectors, `ml*` directives and `--ml-*` theme tokens.
782
785
  - Application header offsets and map-specific stacking rules excluded from the package.
786
+
787
+ ### Scrollable tabs (1.1.0)
788
+
789
+ ```html
790
+ <ml-tabs
791
+ [(value)]="selectedTab"
792
+ [scrollable]="true"
793
+ previousButtonLabel="Previous regions"
794
+ nextButtonLabel="Next regions"
795
+ >
796
+ <ml-tablist>
797
+ @for (region of regions; track region.id) {
798
+ <ml-tab [value]="region.id">{{ region.name }}</ml-tab>
799
+ }
800
+ </ml-tablist>
801
+ <ml-tabpanels><!-- Matching ml-tabpanel elements --></ml-tabpanels>
802
+ </ml-tabs>
803
+ ```
804
+
805
+ `scrollable` defaults to `false`. When enabled, tabs stay on one line, native
806
+ scrollbars are hidden, and previous/next buttons appear only on overflow. Buttons
807
+ are disabled at the corresponding end. Touch and trackpad scrolling remain
808
+ available. The active tab is revealed after selection, including keyboard and
809
+ programmatic changes. Resizing and dynamic labels recalculate overflow. Scrolling
810
+ respects reduced motion and right-to-left layout.
811
+
812
+ `previousButtonLabel` and `nextButtonLabel` customize the accessible button labels
813
+ (defaults: `Previous tabs` and `Next tabs`). Existing theme tokens style the buttons;
814
+ no icon package or application CSS is required. The tablist now contains an internal
815
+ `.ml-tablist__viewport`; custom CSS targeting direct children of `ml-tablist` should
816
+ instead target `.ml-tablist__viewport > ml-tab`.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, DestroyRef, Directive, ElementRef, Injectable, ViewContainerRef, afterNextRender, booleanAttribute, computed, contentChild, effect, forwardRef, inject, input, linkedSignal, model, numberAttribute, output, signal, untracked, viewChild } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, Directive, ElementRef, Injectable, ViewContainerRef, afterNextRender, afterRenderEffect, booleanAttribute, computed, contentChild, effect, forwardRef, inject, input, linkedSignal, model, numberAttribute, output, signal, untracked, viewChild } from "@angular/core";
3
3
  import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
4
  import * as i1 from "@angular/forms";
5
5
  import { FormsModule, NG_VALUE_ACCESSOR, NgControl } from "@angular/forms";
@@ -4695,6 +4695,12 @@ let nextId$4 = 0;
4695
4695
  var TabsComponent = class TabsComponent {
4696
4696
  constructor() {
4697
4697
  this.value = model(...ngDevMode ? [void 0, { debugName: "value" }] : /* istanbul ignore next */ []);
4698
+ this.scrollable = input(false, {
4699
+ ...ngDevMode ? { debugName: "scrollable" } : /* istanbul ignore next */ {},
4700
+ transform: booleanAttribute
4701
+ });
4702
+ this.previousButtonLabel = input("Previous tabs", ...ngDevMode ? [{ debugName: "previousButtonLabel" }] : /* istanbul ignore next */ []);
4703
+ this.nextButtonLabel = input("Next tabs", ...ngDevMode ? [{ debugName: "nextButtonLabel" }] : /* istanbul ignore next */ []);
4698
4704
  this.uid = `ml-tabs-${nextId$4++}`;
4699
4705
  }
4700
4706
  isActive(value) {
@@ -4720,13 +4726,36 @@ var TabsComponent = class TabsComponent {
4720
4726
  type: TabsComponent,
4721
4727
  isStandalone: true,
4722
4728
  selector: "ml-tabs",
4723
- inputs: { value: {
4724
- classPropertyName: "value",
4725
- publicName: "value",
4726
- isSignal: true,
4727
- isRequired: false,
4728
- transformFunction: null
4729
- } },
4729
+ inputs: {
4730
+ value: {
4731
+ classPropertyName: "value",
4732
+ publicName: "value",
4733
+ isSignal: true,
4734
+ isRequired: false,
4735
+ transformFunction: null
4736
+ },
4737
+ scrollable: {
4738
+ classPropertyName: "scrollable",
4739
+ publicName: "scrollable",
4740
+ isSignal: true,
4741
+ isRequired: false,
4742
+ transformFunction: null
4743
+ },
4744
+ previousButtonLabel: {
4745
+ classPropertyName: "previousButtonLabel",
4746
+ publicName: "previousButtonLabel",
4747
+ isSignal: true,
4748
+ isRequired: false,
4749
+ transformFunction: null
4750
+ },
4751
+ nextButtonLabel: {
4752
+ classPropertyName: "nextButtonLabel",
4753
+ publicName: "nextButtonLabel",
4754
+ isSignal: true,
4755
+ isRequired: false,
4756
+ transformFunction: null
4757
+ }
4758
+ },
4730
4759
  outputs: { value: "valueChange" },
4731
4760
  host: { classAttribute: "ml-tabs" },
4732
4761
  ngImport: i0,
@@ -4750,24 +4779,100 @@ i0.ɵɵngDeclareClassMetadata({
4750
4779
  template: `<ng-content />`
4751
4780
  }]
4752
4781
  }],
4753
- propDecorators: { value: [{
4754
- type: i0.Input,
4755
- args: [{
4756
- isSignal: true,
4757
- alias: "value",
4758
- required: false
4782
+ propDecorators: {
4783
+ value: [{
4784
+ type: i0.Input,
4785
+ args: [{
4786
+ isSignal: true,
4787
+ alias: "value",
4788
+ required: false
4789
+ }]
4790
+ }, {
4791
+ type: i0.Output,
4792
+ args: ["valueChange"]
4793
+ }],
4794
+ scrollable: [{
4795
+ type: i0.Input,
4796
+ args: [{
4797
+ isSignal: true,
4798
+ alias: "scrollable",
4799
+ required: false
4800
+ }]
4801
+ }],
4802
+ previousButtonLabel: [{
4803
+ type: i0.Input,
4804
+ args: [{
4805
+ isSignal: true,
4806
+ alias: "previousButtonLabel",
4807
+ required: false
4808
+ }]
4809
+ }],
4810
+ nextButtonLabel: [{
4811
+ type: i0.Input,
4812
+ args: [{
4813
+ isSignal: true,
4814
+ alias: "nextButtonLabel",
4815
+ required: false
4816
+ }]
4759
4817
  }]
4760
- }, {
4761
- type: i0.Output,
4762
- args: ["valueChange"]
4763
- }] }
4818
+ }
4764
4819
  });
4765
4820
  var TabListComponent = class TabListComponent {
4766
4821
  constructor() {
4767
4822
  this.host = inject(ElementRef);
4823
+ this.tabs = inject(TabsComponent);
4824
+ this.overflowing = signal(false, ...ngDevMode ? [{ debugName: "overflowing" }] : /* istanbul ignore next */ []);
4825
+ this.atStart = signal(true, ...ngDevMode ? [{ debugName: "atStart" }] : /* istanbul ignore next */ []);
4826
+ this.atEnd = signal(false, ...ngDevMode ? [{ debugName: "atEnd" }] : /* istanbul ignore next */ []);
4827
+ this.viewport = viewChild.required("viewport", ...ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []);
4828
+ const destroyRef = inject(DestroyRef);
4829
+ afterNextRender(() => {
4830
+ const resize = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(() => this.measure());
4831
+ resize?.observe(this.viewport().nativeElement);
4832
+ const changes = new MutationObserver(() => this.measure());
4833
+ changes.observe(this.viewport().nativeElement, {
4834
+ childList: true,
4835
+ subtree: true,
4836
+ characterData: true
4837
+ });
4838
+ destroyRef.onDestroy(() => {
4839
+ resize?.disconnect();
4840
+ changes.disconnect();
4841
+ });
4842
+ this.measure();
4843
+ });
4844
+ afterRenderEffect(() => {
4845
+ this.tabs.value();
4846
+ if (this.tabs.scrollable()) this.revealActive();
4847
+ this.measure();
4848
+ });
4849
+ }
4850
+ measure() {
4851
+ const el = this.viewport().nativeElement;
4852
+ this.overflowing.set(el.scrollWidth > el.clientWidth + 1);
4853
+ const offset = Math.abs(el.scrollLeft);
4854
+ this.atStart.set(offset <= 1);
4855
+ this.atEnd.set(offset + el.clientWidth >= el.scrollWidth - 1);
4856
+ }
4857
+ scroll(direction) {
4858
+ const el = this.viewport().nativeElement;
4859
+ const rtl = getComputedStyle(el).direction === "rtl";
4860
+ el.scrollBy({
4861
+ left: direction * (rtl ? -1 : 1) * el.clientWidth * .75,
4862
+ behavior: typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth"
4863
+ });
4864
+ }
4865
+ revealActive() {
4866
+ const el = this.viewport().nativeElement;
4867
+ const active = el.querySelector("[role=\"tab\"][aria-selected=\"true\"]");
4868
+ if (!active) return;
4869
+ const bounds = el.getBoundingClientRect();
4870
+ const tab = active.getBoundingClientRect();
4871
+ if (tab.left < bounds.left) el.scrollLeft += tab.left - bounds.left;
4872
+ else if (tab.right > bounds.right) el.scrollLeft += tab.right - bounds.right;
4768
4873
  }
4769
4874
  keydown(event) {
4770
- const tabs = Array.from(this.host.nativeElement.querySelectorAll(":scope > ml-tab, :scope > * > ml-tab"));
4875
+ const tabs = Array.from(this.host.nativeElement.querySelectorAll("[role=\"tab\"]"));
4771
4876
  const at = tabs.indexOf(document.activeElement);
4772
4877
  if (at < 0) return;
4773
4878
  const to = {
@@ -4794,18 +4899,60 @@ var TabListComponent = class TabListComponent {
4794
4899
  }
4795
4900
  static {
4796
4901
  this.ɵcmp = i0.ɵɵngDeclareComponent({
4797
- minVersion: "14.0.0",
4902
+ minVersion: "17.0.0",
4798
4903
  version: "22.2.4",
4799
4904
  type: TabListComponent,
4800
4905
  isStandalone: true,
4801
4906
  selector: "ml-tablist",
4802
4907
  host: {
4803
- attributes: { "role": "tablist" },
4804
4908
  listeners: { "keydown": "keydown($event)" },
4909
+ properties: { "class.ml-tablist--scrollable": "tabs.scrollable()" },
4805
4910
  classAttribute: "ml-tablist"
4806
4911
  },
4912
+ viewQueries: [{
4913
+ propertyName: "viewport",
4914
+ first: true,
4915
+ predicate: ["viewport"],
4916
+ descendants: true,
4917
+ isSignal: true
4918
+ }],
4807
4919
  ngImport: i0,
4808
- template: `<ng-content />`,
4920
+ template: `
4921
+ @if (tabs.scrollable() && overflowing()) {
4922
+ <button
4923
+ type="button"
4924
+ class="ml-tablist__scroll-button"
4925
+ [attr.aria-label]="tabs.previousButtonLabel()"
4926
+ [disabled]="atStart()"
4927
+ (click)="scroll(-1)"
4928
+ >
4929
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
4930
+ <path d="m14 6-6 6 6 6" />
4931
+ </svg>
4932
+ </button>
4933
+ }
4934
+ <div
4935
+ #viewport
4936
+ class="ml-tablist__viewport"
4937
+ role="tablist"
4938
+ (scroll)="measure()"
4939
+ >
4940
+ <ng-content />
4941
+ </div>
4942
+ @if (tabs.scrollable() && overflowing()) {
4943
+ <button
4944
+ type="button"
4945
+ class="ml-tablist__scroll-button"
4946
+ [attr.aria-label]="tabs.nextButtonLabel()"
4947
+ [disabled]="atEnd()"
4948
+ (click)="scroll(1)"
4949
+ >
4950
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
4951
+ <path d="m10 6 6 6-6 6" />
4952
+ </svg>
4953
+ </button>
4954
+ }
4955
+ `,
4809
4956
  isInline: true,
4810
4957
  changeDetection: i0.ChangeDetectionStrategy.OnPush
4811
4958
  });
@@ -4823,12 +4970,52 @@ i0.ɵɵngDeclareClassMetadata({
4823
4970
  changeDetection: ChangeDetectionStrategy.OnPush,
4824
4971
  host: {
4825
4972
  class: "ml-tablist",
4826
- role: "tablist",
4973
+ "[class.ml-tablist--scrollable]": "tabs.scrollable()",
4827
4974
  "(keydown)": "keydown($event)"
4828
4975
  },
4829
- template: `<ng-content />`
4976
+ template: `
4977
+ @if (tabs.scrollable() && overflowing()) {
4978
+ <button
4979
+ type="button"
4980
+ class="ml-tablist__scroll-button"
4981
+ [attr.aria-label]="tabs.previousButtonLabel()"
4982
+ [disabled]="atStart()"
4983
+ (click)="scroll(-1)"
4984
+ >
4985
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
4986
+ <path d="m14 6-6 6 6 6" />
4987
+ </svg>
4988
+ </button>
4989
+ }
4990
+ <div
4991
+ #viewport
4992
+ class="ml-tablist__viewport"
4993
+ role="tablist"
4994
+ (scroll)="measure()"
4995
+ >
4996
+ <ng-content />
4997
+ </div>
4998
+ @if (tabs.scrollable() && overflowing()) {
4999
+ <button
5000
+ type="button"
5001
+ class="ml-tablist__scroll-button"
5002
+ [attr.aria-label]="tabs.nextButtonLabel()"
5003
+ [disabled]="atEnd()"
5004
+ (click)="scroll(1)"
5005
+ >
5006
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
5007
+ <path d="m10 6 6 6-6 6" />
5008
+ </svg>
5009
+ </button>
5010
+ }
5011
+ `
4830
5012
  }]
4831
- }]
5013
+ }],
5014
+ ctorParameters: () => [],
5015
+ propDecorators: { viewport: [{
5016
+ type: i0.ViewChild,
5017
+ args: ["viewport", { isSignal: true }]
5018
+ }] }
4832
5019
  });
4833
5020
  var TabComponent = class TabComponent {
4834
5021
  constructor() {