[ACA-2619][ACA-2616] a11y fixes for Share Link dialog (#5454)

* chore: proper disabling of fields for a11y

* component fixes

* update tests

* update tests

* fix aria labels

* aria-label fixes

* update layout

* update e2e tests

Co-authored-by: Denys Vuika <denys.vuika@gmail.com>
Co-authored-by: Cilibiu Bogdan <bogdan.cilibiu@ness.com>
This commit is contained in:
Mark Steadman
2020-03-19 03:13:42 -05:00
committed by GitHub
parent 57c15a7542
commit 5bcd326891
5 changed files with 106 additions and 70 deletions

View File

@@ -121,6 +121,7 @@ describe('Share file', () => {
it('[C286578] Should disable today option in expiration day calendar', async () => { it('[C286578] Should disable today option in expiration day calendar', async () => {
await contentServicesPage.clickShareButton(); await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed(); await shareDialog.checkDialogIsDisplayed();
await shareDialog.clickExpireToggle();
await shareDialog.clickDateTimePickerButton(); await shareDialog.clickDateTimePickerButton();
await shareDialog.calendarTodayDayIsDisabled(); await shareDialog.calendarTodayDayIsDisabled();
await BrowserActions.closeMenuAndDialogs(); await BrowserActions.closeMenuAndDialogs();
@@ -129,6 +130,7 @@ describe('Share file', () => {
it('[C286548] Should be possible to set expiry date for link', async () => { it('[C286548] Should be possible to set expiry date for link', async () => {
await contentServicesPage.clickShareButton(); await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed(); await shareDialog.checkDialogIsDisplayed();
await shareDialog.clickExpireToggle();
await shareDialog.setDefaultDay(); await shareDialog.setDefaultDay();
await shareDialog.setDefaultHour(); await shareDialog.setDefaultHour();
await shareDialog.setDefaultMinutes(); await shareDialog.setDefaultMinutes();
@@ -142,18 +144,11 @@ describe('Share file', () => {
await BrowserActions.closeMenuAndDialogs(); await BrowserActions.closeMenuAndDialogs();
}); });
it('[C286578] Should disable today option in expiration day calendar', async () => {
await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed();
await shareDialog.clickDateTimePickerButton();
await shareDialog.calendarTodayDayIsDisabled();
await BrowserActions.closeMenuAndDialogs();
});
it('[C310329] Should be possible to set expiry date only for link', async () => { it('[C310329] Should be possible to set expiry date only for link', async () => {
await LocalStorageUtil.setConfigField('sharedLinkDateTimePickerType', JSON.stringify('date')); await LocalStorageUtil.setConfigField('sharedLinkDateTimePickerType', JSON.stringify('date'));
await contentServicesPage.clickShareButton(); await contentServicesPage.clickShareButton();
await shareDialog.checkDialogIsDisplayed(); await shareDialog.checkDialogIsDisplayed();
await shareDialog.clickExpireToggle();
await shareDialog.setDefaultDay(); await shareDialog.setDefaultDay();
await shareDialog.dateTimePickerDialogIsClosed(); await shareDialog.dateTimePickerDialogIsClosed();
const value = await shareDialog.getExpirationDate(); const value = await shareDialog.getExpirationDate();

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { element, by, ElementFinder } from 'protractor'; import { element, by } from 'protractor';
import { BrowserVisibility, TogglePage, BrowserActions, DateTimePickerPage } from '@alfresco/adf-testing'; import { BrowserVisibility, TogglePage, BrowserActions, DateTimePickerPage } from '@alfresco/adf-testing';
import moment = require('moment'); import moment = require('moment');
@@ -23,17 +23,18 @@ export class ShareDialogPage {
togglePage = new TogglePage(); togglePage = new TogglePage();
dateTimePickerPage = new DateTimePickerPage(); dateTimePickerPage = new DateTimePickerPage();
shareDialog: ElementFinder = element(by.css('adf-share-dialog')); shareDialog = element(by.css('adf-share-dialog'));
dialogTitle: ElementFinder = element(by.css('[data-automation-id="adf-share-dialog-title"]')); dialogTitle = element(by.css('[data-automation-id="adf-share-dialog-title"]'));
shareToggle: ElementFinder = element(by.css('[data-automation-id="adf-share-toggle"] label')); shareToggle = element(by.css('[data-automation-id="adf-share-toggle"] label'));
shareToggleChecked: ElementFinder = element(by.css('mat-dialog-container mat-slide-toggle.mat-checked')); expireToggle = element(by.css(`[data-automation-id="adf-expire-toggle"] label`));
shareLink: ElementFinder = element(by.css('[data-automation-id="adf-share-link"]')); shareToggleChecked = element(by.css('mat-dialog-container mat-slide-toggle.mat-checked'));
closeButton: ElementFinder = element(by.css('button[data-automation-id="adf-share-dialog-close"]')); shareLink = element(by.css('[data-automation-id="adf-share-link"]'));
copySharedLinkButton: ElementFinder = element(by.css('.adf-input-action')); closeButton = element(by.css('button[data-automation-id="adf-share-dialog-close"]'));
expirationDateInput: ElementFinder = element(by.css('input[formcontrolname="time"]')); copySharedLinkButton = element(by.css('.adf-input-action'));
confirmationDialog: ElementFinder = element(by.css('adf-confirm-dialog')); expirationDateInput = element(by.css('input[formcontrolname="time"]'));
confirmationCancelButton: ElementFinder = element(by.id('adf-confirm-cancel')); confirmationDialog = element(by.css('adf-confirm-dialog'));
confirmationRemoveButton: ElementFinder = element(by.id('adf-confirm-accept')); confirmationCancelButton = element(by.id('adf-confirm-cancel'));
confirmationRemoveButton = element(by.id('adf-confirm-accept'));
async checkDialogIsDisplayed(): Promise<void> { async checkDialogIsDisplayed(): Promise<void> {
await BrowserVisibility.waitUntilElementIsVisible(this.dialogTitle); await BrowserVisibility.waitUntilElementIsVisible(this.dialogTitle);
@@ -43,6 +44,10 @@ export class ShareDialogPage {
await this.togglePage.enableToggle(this.shareToggle); await this.togglePage.enableToggle(this.shareToggle);
} }
async clickExpireToggle() {
await this.togglePage.enableToggle(this.expireToggle);
}
async clickConfirmationDialogCancelButton(): Promise<void> { async clickConfirmationDialogCancelButton(): Promise<void> {
await BrowserActions.click(this.confirmationCancelButton); await BrowserActions.click(this.confirmationCancelButton);
} }

View File

@@ -7,19 +7,32 @@
<p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p> <p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p>
<div class="adf-share-link--row"> <div class="adf-share-link--row">
<label for="mat-input-1" class="adf-share-link__label">{{ 'SHARE.TITLE' | translate }}</label> <div class="adf-share-link__label">{{ 'SHARE.TITLE' | translate }}</div>
<mat-slide-toggle color="primary" data-automation-id="adf-share-toggle" [checked]="isFileShared" <mat-slide-toggle
[disabled]="!canUpdate || isDisabled" (change)="onSlideShareChange($event)"> color="primary"
data-automation-id="adf-share-toggle"
aria-label="{{ 'SHARE.TITLE' | translate }}"
[checked]="isFileShared"
[disabled]="!canUpdate || isDisabled"
(change)="onSlideShareChange($event)">
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<form [formGroup]="form"> <form [formGroup]="form">
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'> <mat-form-field class="adf-full-width adf-float-label" floatLabel='always'>
<input #sharedLinkInput data-automation-id="adf-share-link" class="adf-share-link__input" matInput <input
cdkFocusInitial placeholder="{{ 'SHARE.PUBLIC-LINK' | translate }}" formControlName="sharedUrl" #sharedLinkInput
data-automation-id="adf-share-link"
class="adf-share-link__input"
matInput
cdkFocusInitial
placeholder="{{ 'SHARE.PUBLIC-LINK' | translate }}"
formControlName="sharedUrl"
readonly="readonly"> readonly="readonly">
<mat-icon class="adf-input-action" matSuffix <mat-icon
class="adf-input-action"
matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard] [clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard]
[target]="sharedLinkInput"> [target]="sharedLinkInput">
link link
@@ -27,16 +40,31 @@
</mat-form-field> </mat-form-field>
<div class="adf-share-link--row"> <div class="adf-share-link--row">
<label for="mat-input-2" class="adf-share-link__label">{{ 'SHARE.EXPIRES' | translate }}</label> <div class="adf-share-link__label">{{ 'SHARE.EXPIRES' | translate }}</div>
<mat-slide-toggle [disabled]="!canUpdate" #slideToggleExpirationDate color="primary" <mat-slide-toggle
data-automation-id="adf-expire-toggle" [checked]="form.controls['time'].value" #slideToggleExpirationDate
(change)="onToggleExpirationDate($event)"> [disabled]="!canUpdate"
</mat-slide-toggle> color="primary"
data-automation-id="adf-expire-toggle"
aria-label="{{ 'SHARE.EXPIRES' | translate }}"
[checked]="time.value"
(change)="onToggleExpirationDate($event)">
</mat-slide-toggle>
</div> </div>
<mat-form-field class="adf-full-width adf-float-label" floatLabel='always'> <mat-form-field class="adf-full-width">
<mat-datetimepicker-toggle #matDatetimepickerToggle="matDatetimepickerToggle" [for]="datetimePicker" matSuffix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle
<mat-datetimepicker #datetimePicker (closed)="onDatetimepickerClosed()" [type]="type" timeInterval="1"></mat-datetimepicker> #matDatetimepickerToggle="matDatetimepickerToggle"
[disabled]="time.disabled"
[for]="datetimePicker"
matSuffix>
</mat-datetimepicker-toggle>
<mat-datetimepicker
#datetimePicker
(closed)="onDatetimepickerClosed()"
[type]="type"
timeInterval="1">
</mat-datetimepicker>
<input class="adf-share-link__input" <input class="adf-share-link__input"
#dateTimePickerInput #dateTimePickerInput
matInput matInput

View File

@@ -16,7 +16,7 @@
*/ */
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed, fakeAsync, async, ComponentFixture } from '@angular/core/testing'; import { TestBed, fakeAsync, ComponentFixture } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material';
import { of, empty } from 'rxjs'; import { of, empty } from 'rxjs';
import { import {
@@ -27,7 +27,9 @@ import {
RenditionsService, RenditionsService,
AppConfigService, AppConfigService,
CoreModule, CoreModule,
AppConfigServiceMock AppConfigServiceMock,
AlfrescoApiService,
AlfrescoApiServiceMock
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { ContentNodeShareModule } from './content-node-share.module'; import { ContentNodeShareModule } from './content-node-share.module';
import { ShareDialogComponent } from './content-node-share.dialog'; import { ShareDialogComponent } from './content-node-share.dialog';
@@ -53,6 +55,7 @@ describe('ShareDialogComponent', () => {
ContentNodeShareModule ContentNodeShareModule
], ],
providers: [ providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: AppConfigService, useClass: AppConfigServiceMock }, { provide: AppConfigService, useClass: AppConfigServiceMock },
{ provide: NotificationService, useValue: notificationServiceMock }, { provide: NotificationService, useValue: notificationServiceMock },
{ provide: MatDialogRef, useValue: { close: () => {}} }, { provide: MatDialogRef, useValue: { close: () => {}} },
@@ -77,6 +80,12 @@ describe('ShareDialogComponent', () => {
properties: {} properties: {}
} }
}; };
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
});
afterEach(() => {
fixture.destroy();
}); });
describe('Error Handling', () => { describe('Error Handling', () => {
@@ -130,8 +139,10 @@ describe('ShareDialogComponent', () => {
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked'); expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked');
}); });
it(`should not toggle share action when file has 'sharedId' property`, async(() => { it(`should not toggle share action when file has 'sharedId' property`, async () => {
spyOn(sharedLinksApiService, 'createSharedLinks'); spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
entry: { id: 'sharedId', sharedId: 'sharedId' }
}));
spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty()); spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty());
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
@@ -143,19 +154,18 @@ describe('ShareDialogComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
fixture.detectChanges(); fixture.detectChanges();
expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled(); expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId'); expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked'); expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked');
});
});
}));
it('should open a confirmation dialog when unshare button is triggered', () => { it('should open a confirmation dialog when unshare button is triggered', () => {
spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) }); spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) });
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough(); spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = { component.data = {
@@ -175,7 +185,7 @@ describe('ShareDialogComponent', () => {
it('should unshare file when confirmation dialog returns true', fakeAsync(() => { it('should unshare file when confirmation dialog returns true', fakeAsync(() => {
spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(true) }); spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(true) });
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough(); spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = { component.data = {
@@ -230,7 +240,6 @@ describe('ShareDialogComponent', () => {
}); });
it('should reset expiration date when toggle is unchecked', () => { it('should reset expiration date when toggle is unchecked', () => {
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.properties['qshare:sharedId'] = '2017-04-15T18:31:37+00:00'; node.entry.properties['qshare:sharedId'] = '2017-04-15T18:31:37+00:00';
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
@@ -282,7 +291,6 @@ describe('ShareDialogComponent', () => {
const date = moment(); const date = moment();
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
node.entry.properties['qshare:sharedId'] = 'sharedId'; node.entry.properties['qshare:sharedId'] = 'sharedId';
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
fixture.componentInstance.form.controls['time'].setValue(null); fixture.componentInstance.form.controls['time'].setValue(null);
component.data = { component.data = {
@@ -308,7 +316,6 @@ describe('ShareDialogComponent', () => {
describe('datetimepicker type', () => { describe('datetimepicker type', () => {
beforeEach(() => { beforeEach(() => {
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({})); spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({}));
node.entry.allowableOperations = ['update']; node.entry.allowableOperations = ['update'];
component.data = { component.data = {
@@ -326,7 +333,7 @@ describe('ShareDialogComponent', () => {
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click')); .dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date); fixture.componentInstance.time.setValue(date);
fixture.detectChanges(); fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
@@ -337,13 +344,13 @@ describe('ShareDialogComponent', () => {
it('it should update node with input date and time when type is `datetime`', () => { it('it should update node with input date and time when type is `datetime`', () => {
const dateTimePickerType = 'datetime'; const dateTimePickerType = 'datetime';
const date = moment('2525-01-01 13:00:00'); const date = moment('2525-01-01 13:00:00');
spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType); spyOn(appConfigService, 'get').and.returnValue(dateTimePickerType);
fixture.detectChanges(); fixture.detectChanges();
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label') fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click')); .dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date); fixture.componentInstance.time.setValue(date);
fixture.detectChanges(); fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', { expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {

View File

@@ -24,13 +24,12 @@ import {
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog, MatSlideToggleChange } from '@angular/material'; import { MAT_DIALOG_DATA, MatDialogRef, MatDialog, MatSlideToggleChange } from '@angular/material';
import { FormGroup, FormControl } from '@angular/forms'; import { FormGroup, FormControl, AbstractControl } from '@angular/forms';
import { Observable, throwError, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { import {
skip, skip,
distinctUntilChanged, distinctUntilChanged,
mergeMap, mergeMap,
catchError,
takeUntil takeUntil
} from 'rxjs/operators'; } from 'rxjs/operators';
import { import {
@@ -62,13 +61,10 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
isDisabled: boolean = false; isDisabled: boolean = false;
form: FormGroup = new FormGroup({ form: FormGroup = new FormGroup({
sharedUrl: new FormControl(''), sharedUrl: new FormControl(''),
time: new FormControl({ value: '', disabled: false }) time: new FormControl({ value: '', disabled: true })
}); });
type = 'datetime'; type = 'datetime';
@ViewChild('matDatetimepickerToggle')
matDatetimepickerToggle;
@ViewChild('slideToggleExpirationDate') @ViewChild('slideToggleExpirationDate')
slideToggleExpirationDate; slideToggleExpirationDate;
@@ -92,10 +88,10 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
this.type = this.appConfigService.get<string>('sharedLinkDateTimePickerType', 'datetime'); this.type = this.appConfigService.get<string>('sharedLinkDateTimePickerType', 'datetime');
if (!this.canUpdate) { if (!this.canUpdate) {
this.form.controls['time'].disable(); this.time.disable();
} }
this.form.controls.time.valueChanges this.time.valueChanges
.pipe( .pipe(
skip(1), skip(1),
distinctUntilChanged(), distinctUntilChanged(),
@@ -103,9 +99,6 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
(updates) => this.updateNode(updates), (updates) => this.updateNode(updates),
(formUpdates) => formUpdates (formUpdates) => formUpdates
), ),
catchError((error) => {
return throwError(error);
}),
takeUntil(this.onDestroy$) takeUntil(this.onDestroy$)
) )
.subscribe(updates => this.updateEntryExpiryDate(updates)); .subscribe(updates => this.updateEntryExpiryDate(updates));
@@ -120,12 +113,15 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
} else { } else {
this.sharedId = properties['qshare:sharedId']; this.sharedId = properties['qshare:sharedId'];
this.isFileShared = true; this.isFileShared = true;
this.updateForm(); this.updateForm();
} }
} }
} }
get time(): AbstractControl {
return this.form.controls['time'];
}
ngOnDestroy() { ngOnDestroy() {
this.onDestroy$.next(true); this.onDestroy$.next(true);
this.onDestroy$.complete(); this.onDestroy$.complete();
@@ -155,17 +151,16 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
onToggleExpirationDate(slideToggle: MatSlideToggleChange) { onToggleExpirationDate(slideToggle: MatSlideToggleChange) {
if (slideToggle.checked) { if (slideToggle.checked) {
this.matDatetimepickerToggle.datetimepicker.open(); this.time.enable();
} else { } else {
this.matDatetimepickerToggle.datetimepicker.close(); this.time.disable();
this.form.controls.time.setValue(null);
} }
} }
onDatetimepickerClosed() { onDatetimepickerClosed() {
this.dateTimePickerInput.nativeElement.blur(); this.dateTimePickerInput.nativeElement.blur();
if (!this.form.controls.time.value) { if (!this.time.value) {
this.slideToggleExpirationDate.checked = false; this.slideToggleExpirationDate.checked = false;
} }
} }
@@ -275,6 +270,12 @@ export class ShareDialogComponent implements OnInit, OnDestroy {
sharedUrl: `${this.baseShareUrl}${this.sharedId}`, sharedUrl: `${this.baseShareUrl}${this.sharedId}`,
time: expiryDate ? moment(expiryDate).local() : null time: expiryDate ? moment(expiryDate).local() : null
}); });
if (expiryDate) {
this.time.enable();
} else {
this.time.disable();
}
} }
private updateNode(date: moment.Moment): Observable<Node> { private updateNode(date: moment.Moment): Observable<Node> {