diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.html b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.html index 72859e528..147aa176d 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.html +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.html @@ -9,6 +9,7 @@
diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts index 937c0448a..5b4b51598 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts @@ -22,7 +22,7 @@ * from Hyland Software. If not, see . */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RuleCompositeConditionUiComponent } from './rule-composite-condition.ui-component'; import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { DebugElement } from '@angular/core'; @@ -33,6 +33,7 @@ import { } from '../../mock/conditions.mock'; import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { FocusTrapFactory } from '@angular/cdk/a11y'; describe('RuleCompositeConditionUiComponent', () => { let fixture: ComponentFixture; @@ -166,4 +167,36 @@ describe('RuleCompositeConditionUiComponent', () => { expect(unitTestingUtils.getAllByCSS('[conditions-group-label-nested]')).toBeTruthy(); }); }); + + describe('Focus management', () => { + it('should focus the newly added simple condition', fakeAsync(() => { + fixture.detectChanges(); + const focusTrapFactory = TestBed.inject(FocusTrapFactory); + const focusTrapSpy = jasmine.createSpyObj('FocusTrap', ['focusFirstTabbableElement', 'destroy']); + spyOn(focusTrapFactory, 'create').and.returnValue(focusTrapSpy); + + unitTestingUtils.clickByDataAutomationId('add-condition-button'); + fixture.detectChanges(); + tick(); + + expect(focusTrapFactory.create).toHaveBeenCalled(); + expect(focusTrapSpy.focusFirstTabbableElement).toHaveBeenCalled(); + expect(focusTrapSpy.destroy).toHaveBeenCalled(); + })); + + it('should focus the newly added composite condition', fakeAsync(() => { + fixture.detectChanges(); + const focusTrapFactory = TestBed.inject(FocusTrapFactory); + const focusTrapSpy = jasmine.createSpyObj('FocusTrap', ['focusFirstTabbableElement', 'destroy']); + spyOn(focusTrapFactory, 'create').and.returnValue(focusTrapSpy); + + unitTestingUtils.clickByDataAutomationId('add-group-button'); + fixture.detectChanges(); + tick(); + + expect(focusTrapFactory.create).toHaveBeenCalled(); + expect(focusTrapSpy.focusFirstTabbableElement).toHaveBeenCalled(); + expect(focusTrapSpy.destroy).toHaveBeenCalled(); + })); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.ts index 3de4e6ad8..67ba846c7 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.ts @@ -22,7 +22,22 @@ * from Hyland Software. If not, see . */ -import { Component, forwardRef, HostBinding, Input, OnChanges, OnDestroy, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { + AfterViewInit, + Component, + DestroyRef, + ElementRef, + forwardRef, + HostBinding, + inject, + Input, + OnChanges, + QueryList, + SimpleChanges, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { RuleCompositeCondition } from '../../model/rule-composite-condition.model'; import { ControlValueAccessor, FormArray, FormControl, FormGroup, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; import { RuleSimpleCondition } from '../../model/rule-simple-condition.model'; @@ -34,6 +49,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component'; +import { FocusTrapFactory } from '@angular/cdk/a11y'; @Component({ imports: [ @@ -60,16 +76,21 @@ import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-compo } ] }) -export class RuleCompositeConditionUiComponent implements ControlValueAccessor, OnDestroy, OnChanges { +export class RuleCompositeConditionUiComponent implements ControlValueAccessor, OnChanges, AfterViewInit { @HostBinding('class.aca-secondaryBackground') @Input() secondaryBackground = false; + @HostBinding('class.aca-childCompositeCondition') @Input() childCondition = false; + @Input() readOnly = false; + @ViewChildren('conditionRow', { read: ElementRef }) + private readonly conditionRows: QueryList>; + readonly isOrImplemented = false; form = new FormGroup({ @@ -79,23 +100,31 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor, simpleConditions: new FormArray([]) }); - private formSubscription = this.form.valueChanges.subscribe((value: any) => { - this.onChange(value); - this.onTouch(); - }); - public invertedControl = this.form.get('inverted') as FormControl; public booleanModeControl = this.form.get('booleanMode') as FormControl; + private readonly destroyRef = inject(DestroyRef); + private readonly focusTrapFactory = inject(FocusTrapFactory); + + constructor() { + this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value: RuleCompositeCondition) => { + this.onChange(value); + this.onTouch(); + }); + } + get compositeConditionsFormArray(): FormArray { return this.form.get('compositeConditions') as FormArray; } + get simpleConditionsFormArray(): FormArray { return this.form.get('simpleConditions') as FormArray; } + get conditionFormControls(): FormControl[] { return [...(this.compositeConditionsFormArray.controls as FormControl[]), ...(this.simpleConditionsFormArray.controls as FormControl[])]; } + get hasNoConditions(): boolean { return this.conditionFormControls.length === 0; } @@ -103,6 +132,12 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor, onChange: (condition: RuleCompositeCondition) => void = () => undefined; onTouch: () => void = () => undefined; + ngAfterViewInit(): void { + this.conditionRows.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.focusLastCondition(); + }); + } + writeValue(value: RuleCompositeCondition) { this.form.get('inverted').setValue(value.inverted); this.form.get('booleanMode').setValue(value.booleanMode); @@ -148,10 +183,6 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor, this.compositeConditionsFormArray.push(new FormControl(newCondition)); } - ngOnDestroy() { - this.formSubscription.unsubscribe(); - } - ngOnChanges(changes: SimpleChanges) { const readOnly = changes['readOnly']?.currentValue; @@ -165,4 +196,19 @@ export class RuleCompositeConditionUiComponent implements ControlValueAccessor, } } } + + private focusLastCondition(): void { + const rows = this.conditionRows.toArray(); + const lastRow = rows[rows.length - 1]?.nativeElement; + + if (!lastRow) { + return; + } + + setTimeout(() => { + const focusTrap = this.focusTrapFactory.create(lastRow); + focusTrap.focusFirstTabbableElement(); + focusTrap.destroy(); + }); + } }