mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-44098 Bottom tab navigation buttons (#11834)
* feat: enable tab navigation buttons * feat: better strings and button positioning * feat: reduce overall padding * feat: adjust buttons based on ux mockups * test: add some tests * fix: build error * fix: rename property to showBottomTabNavButtons for clarity * feat: simplify changes * test: copilot suggestion * fix: prevent upstream visual regression issues. * fix: copilot suggestions * fix: height change
This commit is contained in:
@@ -86,6 +86,30 @@ const expectElementToBeValid = (testingUtils: UnitTestingUtils, fieldId: string)
|
||||
expect(invalidElementContainer).toBeFalsy();
|
||||
};
|
||||
|
||||
const buildTabbedForm = (tabCount: number, hiddenTabIndices: number[] = []): FormModel => {
|
||||
const tabs = Array.from({ length: tabCount }, (_, i) => ({
|
||||
id: `tab-${i}`,
|
||||
title: `Tab ${i}`
|
||||
}));
|
||||
const fields = tabs.map((tab) => ({
|
||||
id: `container-${tab.id}`,
|
||||
type: 'container',
|
||||
tab: tab.id,
|
||||
numberOfColumns: 1,
|
||||
fields: { 1: [{ id: `text-${tab.id}`, type: 'text', name: `Text in ${tab.title}` }] }
|
||||
}));
|
||||
|
||||
const form = new FormModel({ tabs, fields });
|
||||
|
||||
hiddenTabIndices.forEach((tabIndex) => {
|
||||
if (form.tabs[tabIndex]) {
|
||||
form.tabs[tabIndex].isVisible = false;
|
||||
}
|
||||
});
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
describe('Form Renderer Component', () => {
|
||||
let formRendererComponent: FormRendererComponent<any>;
|
||||
let fixture: ComponentFixture<FormRendererComponent<any>>;
|
||||
@@ -909,6 +933,117 @@ describe('Form Renderer Component', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab navigation', () => {
|
||||
describe('visibleTabs', () => {
|
||||
it('should return only tabs where isVisible is true', () => {
|
||||
formRendererComponent.formDefinition = buildTabbedForm(3, [1]);
|
||||
expect(formRendererComponent.visibleTabs().length).toBe(2);
|
||||
expect(formRendererComponent.visibleTabs().every((t) => t.isVisible)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return all tabs when none are hidden', () => {
|
||||
formRendererComponent.formDefinition = buildTabbedForm(3);
|
||||
expect(formRendererComponent.visibleTabs().length).toBe(3);
|
||||
});
|
||||
|
||||
it('should return empty array when all tabs are hidden', () => {
|
||||
formRendererComponent.formDefinition = buildTabbedForm(3, [0, 1, 2]);
|
||||
expect(formRendererComponent.visibleTabs().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canNavigateNext and canNavigatePrevious', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('formDefinition', buildTabbedForm(3));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not allow navigating previous on the first tab', () => {
|
||||
expect(formRendererComponent.canNavigatePrevious).toBeFalse();
|
||||
expect(formRendererComponent.canNavigateNext).toBeTrue();
|
||||
});
|
||||
|
||||
it('should allow navigating previous and next between the first and last tabs', async () => {
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(formRendererComponent.canNavigatePrevious).toBeTrue();
|
||||
expect(formRendererComponent.canNavigateNext).toBeTrue();
|
||||
});
|
||||
|
||||
it('should not allow navigating next on the last tab', async () => {
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
formRendererComponent.tabGroup.selectedIndexChange.emit(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
formRendererComponent.tabGroup.selectedIndexChange.emit(2);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(formRendererComponent.canNavigatePrevious).toBeTrue();
|
||||
expect(formRendererComponent.canNavigateNext).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToNextTab and navigateToPreviousTab', () => {
|
||||
beforeEach(() => {
|
||||
fixture.componentRef.setInput('formDefinition', buildTabbedForm(3));
|
||||
fixture.detectChanges();
|
||||
expect(formRendererComponent.tabGroup).toBeDefined();
|
||||
});
|
||||
|
||||
it('should increment selectedIndex when navigating to next tab', async () => {
|
||||
const initialIndex = formRendererComponent.tabGroup.selectedIndex;
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(formRendererComponent.tabGroup.selectedIndex).toBe(initialIndex + 1);
|
||||
});
|
||||
|
||||
it('should decrement selectedIndex when navigating to previous tab', async () => {
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const indexAfterNext = formRendererComponent.tabGroup.selectedIndex;
|
||||
formRendererComponent.navigateToPreviousTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(formRendererComponent.tabGroup.selectedIndex).toBe(indexAfterNext - 1);
|
||||
});
|
||||
|
||||
it('should not go below 0 when navigating previous on the first tab', async () => {
|
||||
formRendererComponent.navigateToPreviousTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(formRendererComponent.tabGroup.selectedIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should not exceed last index when navigating next on the last tab', async () => {
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
formRendererComponent.tabGroup.selectedIndexChange.emit(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
formRendererComponent.tabGroup.selectedIndexChange.emit(2);
|
||||
fixture.detectChanges();
|
||||
const lastIndex = formRendererComponent.tabGroup.selectedIndex;
|
||||
formRendererComponent.navigateToNextTab();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(formRendererComponent.tabGroup.selectedIndex).toBe(lastIndex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Repeatable section', () => {
|
||||
const repeatableSectionField = new FormFieldModel(new FormModel(), {
|
||||
id: 'RepeatableSection0tbw2y',
|
||||
|
||||
@@ -16,12 +16,24 @@
|
||||
*/
|
||||
|
||||
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, DestroyRef, inject, Injector, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Injector,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
signal,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { filter } from 'rxjs';
|
||||
import { filter, Subscription } from 'rxjs';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatTabGroup, MatTabsModule } from '@angular/material/tabs';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { FormRulesManager, formRulesManagerFactory } from '../models/form-rules.model';
|
||||
import { FormService } from '../services/form.service';
|
||||
@@ -82,11 +94,54 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
@Input({ required: true })
|
||||
formDefinition: FormModel;
|
||||
set formDefinition(formDefinition: FormModel) {
|
||||
this._formDefinition = formDefinition;
|
||||
this.syncCurrentTabIndex();
|
||||
}
|
||||
|
||||
get formDefinition(): FormModel {
|
||||
return this._formDefinition;
|
||||
}
|
||||
|
||||
@Input()
|
||||
readOnly = false;
|
||||
|
||||
@ViewChild(MatTabGroup)
|
||||
set tabGroup(tabGroup: MatTabGroup | undefined) {
|
||||
this.tabGroupSelectionSubscription?.unsubscribe();
|
||||
this._tabGroup = tabGroup;
|
||||
|
||||
if (tabGroup) {
|
||||
this.syncCurrentTabIndex(tabGroup.selectedIndex);
|
||||
this.tabGroupSelectionSubscription = tabGroup.selectedIndexChange.subscribe((index) => this.syncCurrentTabIndex(index));
|
||||
}
|
||||
}
|
||||
|
||||
get tabGroup(): MatTabGroup {
|
||||
return this._tabGroup as MatTabGroup;
|
||||
}
|
||||
|
||||
private readonly currentTabIndex = signal(0);
|
||||
private _formDefinition: FormModel;
|
||||
private _tabGroup?: MatTabGroup;
|
||||
private tabGroupSelectionSubscription?: Subscription;
|
||||
|
||||
get canNavigateNext(): boolean {
|
||||
return this.currentTabIndex() < this.visibleTabCount - 1;
|
||||
}
|
||||
|
||||
get canNavigatePrevious(): boolean {
|
||||
return this.currentTabIndex() > 0;
|
||||
}
|
||||
|
||||
get visibleTabCount(): number {
|
||||
return this.visibleTabs().length;
|
||||
}
|
||||
|
||||
get selectedTabIndex(): number {
|
||||
return this.currentTabIndex();
|
||||
}
|
||||
|
||||
debugMode: boolean;
|
||||
|
||||
fields: FormFieldModel[];
|
||||
@@ -106,15 +161,37 @@ export class FormRendererComponent<T> implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.tabGroupSelectionSubscription?.unsubscribe();
|
||||
this.formRulesManager.destroy();
|
||||
}
|
||||
|
||||
hasTabs(): boolean {
|
||||
return this.formDefinition.tabs && this.formDefinition.tabs.length > 0;
|
||||
return this.formDefinition?.tabs && this.formDefinition.tabs.length > 0;
|
||||
}
|
||||
|
||||
visibleTabs(): TabModel[] {
|
||||
return this.formDefinition.tabs.filter((tab) => tab.isVisible);
|
||||
return this.formDefinition?.tabs?.filter((tab) => tab.isVisible) ?? [];
|
||||
}
|
||||
|
||||
navigateToNextTab(): void {
|
||||
if (this.tabGroup && this.canNavigateNext) {
|
||||
this.tabGroup.selectedIndex = (this.tabGroup.selectedIndex ?? 0) + 1;
|
||||
this.syncCurrentTabIndex(this.tabGroup.selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToPreviousTab(): void {
|
||||
if (this.tabGroup && this.canNavigatePrevious) {
|
||||
this.tabGroup.selectedIndex = (this.tabGroup.selectedIndex ?? 0) - 1;
|
||||
this.syncCurrentTabIndex(this.tabGroup.selectedIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private syncCurrentTabIndex(index = this.tabGroup?.selectedIndex): void {
|
||||
const maxTabIndex = Math.max(this.visibleTabCount - 1, 0);
|
||||
const currentIndex = index ?? 0;
|
||||
|
||||
this.currentTabIndex.set(Math.min(Math.max(currentIndex, 0), maxTabIndex));
|
||||
}
|
||||
|
||||
getNumberOfColumns(content: ContainerModel): number {
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
"NO_LABEL": "Cancel"
|
||||
}
|
||||
},
|
||||
"BUTTON": {
|
||||
"PREVIOUS_TAB": "Previous",
|
||||
"PREVIOUS_TAB_TITLE": "Navigate to previous tab",
|
||||
"NEXT_TAB": "Next",
|
||||
"NEXT_TAB_TITLE": "Navigate to next tab"
|
||||
},
|
||||
"FIELD_STYLE": {
|
||||
"FONT_SIZE": "Font size",
|
||||
"FONT_WEIGHT": "Font weight",
|
||||
|
||||
Reference in New Issue
Block a user