[ACS-10309] screen reader personal files new process dialog does not announce any semantics (#5008)

* [ACS-10309] Fixed reading for screen reader for save search dialog

* [ACS-10309] Unit tests
This commit is contained in:
AleksanderSklorz
2026-01-26 11:02:49 +01:00
committed by GitHub
parent 0bc5bcc137
commit 7d0133af35
5 changed files with 98 additions and 38 deletions
@@ -1,5 +1,9 @@
<div class="aca-save-search-dialog__header"> <div class="aca-save-search-dialog__header">
<h2 class="aca-save-search-dialog__title">{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL_HEADER" | translate}}</h2> <h2
id="aca-save-search-dialog-title"
class="aca-save-search-dialog__title">
{{"APP.BROWSE.SEARCH.SAVE_SEARCH.MODAL_HEADER" | translate}}
</h2>
<button <button
mat-icon-button mat-icon-button
mat-dialog-close mat-dialog-close
@@ -20,7 +24,6 @@
matInput matInput
required required
[formControlName]="'name'" [formControlName]="'name'"
adf-auto-focus
/> />
<mat-error *ngIf="form.controls['name'].touched"> <mat-error *ngIf="form.controls['name'].touched">
@@ -34,7 +34,7 @@ import { A11yModule } from '@angular/cdk/a11y';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { NotificationService } from '@alfresco/adf-core'; import { NotificationService } from '@alfresco/adf-core';
import { AutoFocusDirective, forbidOnlySpaces } from '@alfresco/adf-content-services'; import { forbidOnlySpaces } from '@alfresco/adf-content-services';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { UniqueSearchNameValidator } from './unique-search-name-validator'; import { UniqueSearchNameValidator } from './unique-search-name-validator';
@@ -53,7 +53,6 @@ import { SavedSearchesContextService } from '../../../../services/saved-searches
A11yModule, A11yModule,
MatCheckboxModule, MatCheckboxModule,
FormsModule, FormsModule,
AutoFocusDirective,
ReactiveFormsModule, ReactiveFormsModule,
MatDialogModule MatDialogModule
], ],
@@ -0,0 +1,27 @@
/*!
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Alfresco Example Content Application
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
*/
export interface SaveSearchDirectiveDialogData {
searchUrl: string;
}
@@ -24,11 +24,13 @@
import { Component, DebugElement } from '@angular/core'; import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { of, Subject } from 'rxjs'; import { of, Subject } from 'rxjs';
import { SaveSearchDirective } from './save-search.directive'; import { SaveSearchDirective } from './save-search.directive';
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component'; import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
import { SaveSearchDirectiveDialogData } from '../dialog/save-search-directive-dialog-data';
import { OverlayContainer } from '@angular/cdk/overlay';
@Component({ @Component({
selector: 'app-test-component', selector: 'app-test-component',
@@ -49,11 +51,6 @@ describe('SaveSearchDirective', () => {
let element: DebugElement; let element: DebugElement;
let dialog: MatDialog; let dialog: MatDialog;
const event = {
type: 'click',
preventDefault: jasmine.createSpy('preventDefault')
};
beforeEach(() => { beforeEach(() => {
void TestBed.configureTestingModule({ void TestBed.configureTestingModule({
imports: [SaveSearchDirective, TestComponent], imports: [SaveSearchDirective, TestComponent],
@@ -75,18 +72,46 @@ describe('SaveSearchDirective', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('Click on save search directive', () => {
it('should prevent the default click action', () => { it('should prevent the default click action', () => {
element.triggerEventHandler('click', event); const event = new MouseEvent('click');
spyOn(event, 'preventDefault');
element.nativeElement.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled(); expect(event.preventDefault).toHaveBeenCalled();
}); });
it('should open the dialog with the correct configuration', () => { it('should open the dialog with the correct configuration', () => {
spyOn(dialog, 'open'); spyOn(dialog, 'open');
element.triggerEventHandler('click', event); element.nativeElement.click();
const expectedConfig = { const expectedConfig: MatDialogConfig<SaveSearchDirectiveDialogData> = {
data: { searchUrl: 'encodedQuery' }, data: { searchUrl: 'encodedQuery' },
restoreFocus: true restoreFocus: true,
ariaLabelledBy: 'aca-save-search-dialog-title'
};
expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig);
});
it('should call setAttribute on container element', () => {
const overlayContainer = TestBed.inject(OverlayContainer);
const containerElement = document.createElement('div');
spyOn(containerElement, 'setAttribute');
spyOn(overlayContainer, 'getContainerElement').and.returnValue(containerElement);
element.nativeElement.click();
expect(containerElement.setAttribute).toHaveBeenCalledWith('role', 'dialog');
});
it('should open the dialog with the correct configuration', () => {
spyOn(dialog, 'open');
element.nativeElement.click();
const expectedConfig: MatDialogConfig<SaveSearchDirectiveDialogData> = {
data: { searchUrl: 'encodedQuery' },
restoreFocus: true,
ariaLabelledBy: 'aca-save-search-dialog-title'
}; };
expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig); expect(dialog.open).toHaveBeenCalledWith(SaveSearchDialogComponent, expectedConfig);
@@ -99,7 +124,7 @@ describe('SaveSearchDirective', () => {
afterClosed: () => afterClosed$.asObservable() afterClosed: () => afterClosed$.asObservable()
} as MatDialogRef<SaveSearchDialogComponent>); } as MatDialogRef<SaveSearchDialogComponent>);
element.triggerEventHandler('click', event); element.nativeElement.click();
afterClosed$.next(true); afterClosed$.next(true);
afterClosed$.complete(); afterClosed$.complete();
@@ -108,3 +133,4 @@ describe('SaveSearchDirective', () => {
expect(fixture.componentInstance.isSavedWithSuccess).toBe(true); expect(fixture.componentInstance.isSavedWithSuccess).toBe(true);
}); });
}); });
});
@@ -26,10 +26,8 @@ import { DestroyRef, Directive, ElementRef, EventEmitter, HostListener, inject,
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component'; import { SaveSearchDialogComponent } from '../dialog/save-search-dialog.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { OverlayContainer } from '@angular/cdk/overlay';
interface SaveSearchDirectiveDialogData { import { SaveSearchDirectiveDialogData } from '../dialog/save-search-directive-dialog-data';
searchUrl: string;
}
@Directive({ @Directive({
selector: '[acaSaveSearch]', selector: '[acaSaveSearch]',
@@ -48,6 +46,8 @@ export class SaveSearchDirective {
private readonly dialogRef = inject(MatDialog); private readonly dialogRef = inject(MatDialog);
private readonly elementRef = inject(ElementRef<HTMLElement>); private readonly elementRef = inject(ElementRef<HTMLElement>);
constructor(private readonly overlayContainer: OverlayContainer) {}
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
@@ -56,7 +56,12 @@ export class SaveSearchDirective {
} }
private openDialog(): void { private openDialog(): void {
const dialog = this.dialogRef.open(SaveSearchDialogComponent, { ...this.getDialogConfig(), restoreFocus: true }); this.overlayContainer.getContainerElement().setAttribute('role', 'dialog');
const dialog = this.dialogRef.open(SaveSearchDialogComponent, {
...this.getDialogConfig(),
restoreFocus: true,
ariaLabelledBy: 'aca-save-search-dialog-title'
});
dialog dialog
.afterClosed() .afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))