mn-angular-lib 1.0.137 → 1.0.139
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.
|
@@ -10542,6 +10542,26 @@ const DEFAULT_SKELETON_TAB_COUNT = 3;
|
|
|
10542
10542
|
class MnTabComponent {
|
|
10543
10543
|
/** The horizontally-scrolling wrapper the edge fade is painted onto. */
|
|
10544
10544
|
scrollContainer;
|
|
10545
|
+
/** The tab row; queried for the active tab so the indicator can measure it. */
|
|
10546
|
+
tabList;
|
|
10547
|
+
/** The shared underline that slides to the active tab. */
|
|
10548
|
+
indicator;
|
|
10549
|
+
/** Pending indicator remeasure, cancelled on destroy so a post-destroy frame can't read a detached ref. */
|
|
10550
|
+
indicatorFrame;
|
|
10551
|
+
/**
|
|
10552
|
+
* True while a click-initiated slide is animating. The active tab's
|
|
10553
|
+
* `font-bold` widens the row, which fires {@link resizeObserver}; without
|
|
10554
|
+
* this guard the observer's snap ({@link updateIndicator} with `animate:
|
|
10555
|
+
* false`) would land the indicator at its target the same frame the slide
|
|
10556
|
+
* starts, so the transition never paints. Set synchronously in
|
|
10557
|
+
* {@link setActive} — before the frame runs — so the guard doesn't depend on
|
|
10558
|
+
* rAF-vs-ResizeObserver callback ordering.
|
|
10559
|
+
*/
|
|
10560
|
+
sliding = false;
|
|
10561
|
+
/** Clears {@link sliding} after the slide finishes; re-armed per click, cancelled on destroy. */
|
|
10562
|
+
slidingTimer;
|
|
10563
|
+
/** Slide duration in ms; matches the indicator's `duration-300` transition. */
|
|
10564
|
+
static SLIDE_MS = 300;
|
|
10545
10565
|
/** How far the fade reaches in from each overflowing edge. */
|
|
10546
10566
|
static FADE = '2rem';
|
|
10547
10567
|
/** Data source containing tab items and default active index. */
|
|
@@ -10605,14 +10625,28 @@ class MnTabComponent {
|
|
|
10605
10625
|
const el = this.scrollContainer?.nativeElement;
|
|
10606
10626
|
if (!el)
|
|
10607
10627
|
return;
|
|
10608
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
10628
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
10629
|
+
this.updateEdgeFades();
|
|
10630
|
+
// Tabs may have reflowed (viewport change, justified widths); snap the
|
|
10631
|
+
// indicator to the new geometry — animating a resize tick reads as jank.
|
|
10632
|
+
// But skip the snap mid-slide: a click's own `font-bold` resizes the row
|
|
10633
|
+
// and fires this observer, and snapping there kills the slide it triggered.
|
|
10634
|
+
if (!this.sliding)
|
|
10635
|
+
this.updateIndicator(false);
|
|
10636
|
+
});
|
|
10609
10637
|
this.resizeObserver.observe(el);
|
|
10610
10638
|
if (el.firstElementChild)
|
|
10611
10639
|
this.resizeObserver.observe(el.firstElementChild);
|
|
10612
10640
|
this.updateEdgeFades();
|
|
10641
|
+
// Place the indicator on the default tab without a slide-in from zero.
|
|
10642
|
+
this.updateIndicator(false);
|
|
10613
10643
|
}
|
|
10614
10644
|
ngOnDestroy() {
|
|
10615
10645
|
this.resizeObserver?.disconnect();
|
|
10646
|
+
if (this.indicatorFrame !== undefined)
|
|
10647
|
+
cancelAnimationFrame(this.indicatorFrame);
|
|
10648
|
+
if (this.slidingTimer !== undefined)
|
|
10649
|
+
clearTimeout(this.slidingTimer);
|
|
10616
10650
|
}
|
|
10617
10651
|
/**
|
|
10618
10652
|
* Paints a fade over whichever edge has tabs scrolled out of view — a soft
|
|
@@ -10652,6 +10686,68 @@ class MnTabComponent {
|
|
|
10652
10686
|
item.onClick?.();
|
|
10653
10687
|
this.currentActive = item;
|
|
10654
10688
|
this.activeChange.emit(item);
|
|
10689
|
+
// Slide the underline to the new tab. Measure on the next frame, after
|
|
10690
|
+
// change detection has applied the active tab's `font-bold` (which widens
|
|
10691
|
+
// it) so the indicator lands on the final, bolded geometry. Guard the slide
|
|
10692
|
+
// against the resize snap the same `font-bold` triggers (see {@link sliding}).
|
|
10693
|
+
this.beginSlide();
|
|
10694
|
+
this.scheduleIndicator(true);
|
|
10695
|
+
}
|
|
10696
|
+
/**
|
|
10697
|
+
* Marks a click-driven slide as in progress and schedules the guard to lift
|
|
10698
|
+
* once the transition has finished. A timeout (not `transitionend`) so the
|
|
10699
|
+
* flag still clears under `motion-reduce`, where no transition event fires.
|
|
10700
|
+
*/
|
|
10701
|
+
beginSlide() {
|
|
10702
|
+
this.sliding = true;
|
|
10703
|
+
if (this.slidingTimer !== undefined)
|
|
10704
|
+
clearTimeout(this.slidingTimer);
|
|
10705
|
+
this.slidingTimer = setTimeout(() => {
|
|
10706
|
+
this.slidingTimer = undefined;
|
|
10707
|
+
this.sliding = false;
|
|
10708
|
+
}, MnTabComponent.SLIDE_MS);
|
|
10709
|
+
}
|
|
10710
|
+
/**
|
|
10711
|
+
* Moves the shared underline to the active tab. When `animate` is false the
|
|
10712
|
+
* move is snapped (no slide) by disabling the transition for one reflow —
|
|
10713
|
+
* used on init, async selection and resize, where a slide would read as jank.
|
|
10714
|
+
* @param animate - Whether the move should slide (true) or snap (false).
|
|
10715
|
+
*/
|
|
10716
|
+
updateIndicator(animate) {
|
|
10717
|
+
const bar = this.indicator?.nativeElement;
|
|
10718
|
+
const list = this.tabList?.nativeElement;
|
|
10719
|
+
if (!bar || !list)
|
|
10720
|
+
return;
|
|
10721
|
+
const active = list.querySelector('[role="tab"][aria-selected="true"]');
|
|
10722
|
+
if (!active) {
|
|
10723
|
+
bar.style.opacity = '0';
|
|
10724
|
+
return;
|
|
10725
|
+
}
|
|
10726
|
+
if (!animate)
|
|
10727
|
+
bar.style.transition = 'none';
|
|
10728
|
+
bar.style.opacity = '1';
|
|
10729
|
+
bar.style.width = `${active.offsetWidth}px`;
|
|
10730
|
+
bar.style.transform = `translateX(${active.offsetLeft}px)`;
|
|
10731
|
+
if (!animate) {
|
|
10732
|
+
// Force a reflow so the snapped values apply before the transition is
|
|
10733
|
+
// restored, then hand animation back to the CSS class.
|
|
10734
|
+
void bar.offsetWidth;
|
|
10735
|
+
bar.style.transition = '';
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
/**
|
|
10739
|
+
* Remeasures the indicator on the next animation frame, so the read happens
|
|
10740
|
+
* after layout reflects the latest active-tab classes. Coalesces bursts and
|
|
10741
|
+
* is cancellable on destroy.
|
|
10742
|
+
* @param animate - Whether the resulting move should slide.
|
|
10743
|
+
*/
|
|
10744
|
+
scheduleIndicator(animate) {
|
|
10745
|
+
if (this.indicatorFrame !== undefined)
|
|
10746
|
+
cancelAnimationFrame(this.indicatorFrame);
|
|
10747
|
+
this.indicatorFrame = requestAnimationFrame(() => {
|
|
10748
|
+
this.indicatorFrame = undefined;
|
|
10749
|
+
this.updateIndicator(animate);
|
|
10750
|
+
});
|
|
10655
10751
|
}
|
|
10656
10752
|
/**
|
|
10657
10753
|
* Returns the resolved badge value for a tab item, supporting both plain numbers and Signal<number>.
|
|
@@ -10670,7 +10766,10 @@ class MnTabComponent {
|
|
|
10670
10766
|
syncActiveTab() {
|
|
10671
10767
|
const items = this.dataSource?.items;
|
|
10672
10768
|
if (!items || items.length === 0) {
|
|
10673
|
-
this.currentActive
|
|
10769
|
+
if (this.currentActive !== undefined) {
|
|
10770
|
+
this.currentActive = undefined;
|
|
10771
|
+
this.scheduleIndicator(false);
|
|
10772
|
+
}
|
|
10674
10773
|
return;
|
|
10675
10774
|
}
|
|
10676
10775
|
if (this.currentActive && items.includes(this.currentActive)) {
|
|
@@ -10679,16 +10778,24 @@ class MnTabComponent {
|
|
|
10679
10778
|
const defaultIndex = this.dataSource.defaultActive;
|
|
10680
10779
|
const index = defaultIndex >= 0 && defaultIndex < items.length ? defaultIndex : 0;
|
|
10681
10780
|
this.currentActive = items[index];
|
|
10781
|
+
// Selection resolved from data (not a user click): snap, don't slide.
|
|
10782
|
+
this.scheduleIndicator(false);
|
|
10682
10783
|
}
|
|
10683
10784
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10684
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.
|
|
10785
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "tabList", first: true, predicate: ["tabList"], descendants: true }, { propertyName: "indicator", first: true, predicate: ["indicator"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
|
|
10685
10786
|
}
|
|
10686
10787
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, decorators: [{
|
|
10687
10788
|
type: Component,
|
|
10688
|
-
args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.
|
|
10789
|
+
args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
|
|
10689
10790
|
}], propDecorators: { scrollContainer: [{
|
|
10690
10791
|
type: ViewChild,
|
|
10691
10792
|
args: ['scrollContainer']
|
|
10793
|
+
}], tabList: [{
|
|
10794
|
+
type: ViewChild,
|
|
10795
|
+
args: ['tabList']
|
|
10796
|
+
}], indicator: [{
|
|
10797
|
+
type: ViewChild,
|
|
10798
|
+
args: ['indicator']
|
|
10692
10799
|
}], dataSource: [{
|
|
10693
10800
|
type: Input
|
|
10694
10801
|
}], scrollable: [{
|