[ADF-4865] Upload Dialog - row actions not accessible by keyboard alone (#5051)

* access upload dialog information by keyboard

* upload files actions keyboard accessibility

* aria labels translation keys

* refractor styling

* toggle action icons

* update docs

* e2e

* e2e update action reference
This commit is contained in:
Cilibiu Bogdan 2019-09-05 13:08:46 +03:00 committed by Eugenio Romano
parent 2360ccc6d5
commit ef09b077c4
14 changed files with 261 additions and 47 deletions

View File

@ -128,7 +128,8 @@
"ECMBPM", "ECMBPM",
"processwithvariables", "processwithvariables",
"dropdownrestprocess", "dropdownrestprocess",
"devops" "devops",
"mouseenter"
], ],
"dictionaries": [ "dictionaries": [
"html", "html",

View File

@ -292,6 +292,7 @@ for more information about installing and using the source code.
| [Folder Edit directive](content-services/directives/folder-edit.directive.md) | Allows folders to be edited. | [Source](../lib/content-services/folder-directive/folder-edit.directive.ts) | | [Folder Edit directive](content-services/directives/folder-edit.directive.md) | Allows folders to be edited. | [Source](../lib/content-services/folder-directive/folder-edit.directive.ts) |
| [Inherit Permission directive](content-services/directives/inherited-button.directive.md) | Update the current node by adding/removing the inherited permissions. | [Source](../lib/content-services/permission-manager/components/inherited-button.directive.ts) | | [Inherit Permission directive](content-services/directives/inherited-button.directive.md) | Update the current node by adding/removing the inherited permissions. | [Source](../lib/content-services/permission-manager/components/inherited-button.directive.ts) |
| [Node Lock directive](content-services/directives/node-lock.directive.md) | Locks or unlocks a node. | [Source](../lib/content-services/directives/node-lock.directive.ts) | | [Node Lock directive](content-services/directives/node-lock.directive.md) | Locks or unlocks a node. | [Source](../lib/content-services/directives/node-lock.directive.ts) |
| [Toggle Icon directive](content-services/directives/toggle-icon.directive.md) | Toggle icon on mouse and keyboard event. | [Source](../lib/content-services/upload/directives/toggle-icon.directive.ts) |
### Dialogs ### Dialogs

View File

@ -0,0 +1,34 @@
---
Title: Toggle Icon directive
Added: v2.0.0
Status: Active
Last reviewed: 2019-04-09
---
# [Toggle Icon directive](../../../lib/content-services/upload/directives/toggle-icon.directive.ts 'Defined in toggle-icon.directive.ts')
Toggle icon on mouse or keyboard event for a selectable element.
## Example Usage
```html
<button mat-icon-button adf-toggle-icon #toggle="toggleIcon">
<mat-icon *ngIf="!toggle.isToggled">
check_circle
</mat-icon>
<mat-icon *ngIf="toggle.isToggled">
remove_circle
</mat-icon>
<button></button>
</button>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| --------- | --------- | ------------- | ----------------------------------------------- |
| isToggled | `boolean` | false | Is element active by mouseenter or focus event? |
| isFocused | `boolean` | false | Is element focused by keyboard navigation? |

View File

@ -36,6 +36,7 @@ backend services have been tested with each released version of ADF.
- [Identity group service](core/services/identity-group.service.md) - [Identity group service](core/services/identity-group.service.md)
- [Local preference cloud service](process-services-cloud/services/local-preference-cloud.service.md) - [Local preference cloud service](process-services-cloud/services/local-preference-cloud.service.md)
- [User preference cloud service](process-services-cloud/services/user-preference-cloud.service.md) - [User preference cloud service](process-services-cloud/services/user-preference-cloud.service.md)
- [Toggle icon directive](content-services/directives/toggle-icon.directive.md)
<!--v340 end--> <!--v340 end-->

View File

@ -79,7 +79,7 @@ describe('Upload component', async () => {
}); });
it('[C272792] Should be possible to cancel upload of a big file using row cancel icon', async () => { it('[C272792] Should be possible to cancel upload of a big file using row cancel icon', async () => {
await browser.executeScript(' setTimeout(() => {document.querySelector(\'mat-icon[class*="adf-file-uploading-row__action"]\').click();}, 3000)'); await browser.executeScript('setTimeout(() => {document.querySelector("div[data-automation-id=\'cancel-upload-progress\']").click();}, 3000)');
await contentServicesPage.uploadFile(largeFile.location); await contentServicesPage.uploadFile(largeFile.location);

View File

@ -157,7 +157,7 @@ describe('Version component actions', () => {
it('[C307033] Should be possible to cancel the upload of a new version', async () => { it('[C307033] Should be possible to cancel the upload of a new version', async () => {
await browser.refresh(); await browser.refresh();
await contentServicesPage.versionManagerContent(txtFileModel.name); await contentServicesPage.versionManagerContent(txtFileModel.name);
await browser.executeScript(' setTimeout(() => {document.querySelector(\'mat-icon[class*="adf-file-uploading-row__action"]\').click();}, 1000)'); await browser.executeScript(' setTimeout(() => {document.querySelector("div[data-automation-id=\'cancel-upload-progress\']").click();}, 1000)');
await BrowserActions.click(versionManagePage.showNewVersionButton); await BrowserActions.click(versionManagePage.showNewVersionButton);

View File

@ -117,7 +117,8 @@
"REMOVE_FILE": "Remove uploaded file" "REMOVE_FILE": "Remove uploaded file"
}, },
"STATUS": { "STATUS": {
"FILE_CANCELED_STATUS": "Canceled" "FILE_CANCELED_STATUS": "Canceled",
"FILE_DONE_STATUS": "Uploaded"
}, },
"CONFIRMATION": { "CONFIRMATION": {
"BUTTON": { "BUTTON": {
@ -130,12 +131,16 @@
} }
}, },
"ARIA-LABEL": { "ARIA-LABEL": {
"VERSION": "File version", "VERSION": "File version {{ version }}",
"DIALOG": "Upload list", "DIALOG": "Upload list",
"DIALOG_MAXIMIZE": "Maximize upload dialog", "DIALOG_MAXIMIZE": "Maximize upload dialog",
"DIALOG_MINIMIZE": "Minimize upload dialog", "DIALOG_MINIMIZE": "Minimize upload dialog",
"DIALOG_CLOSE": "Close upload dialog", "DIALOG_CLOSE": "Close upload dialog",
"CANCEL_ALL": "Cancel all uploading files", "CANCEL_ALL": "Cancel all uploading files",
"REMOVE_FILE": "Remove uploaded file {{ file }}",
"CANCEL_FILE": "Cancel scheduled file {{ file }}",
"CANCEL_FILE_UPLOAD": "Cancel file upload {{ file }}",
"UPLOAD_FILE_ERROR": "Upload error {{ error }}",
"CONFIRMATION": { "CONFIRMATION": {
"CANCEL": "Confirm cancel", "CANCEL": "Confirm cancel",
"CONTINUE": "Continue uploading" "CONTINUE": "Continue uploading"

View File

@ -23,6 +23,7 @@
</button> </button>
<span <span
tabindex="0"
class="adf-upload-dialog__title" class="adf-upload-dialog__title"
*ngIf="!uploadList.isUploadCancelled()"> *ngIf="!uploadList.isUploadCancelled()">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_PROGRESS' {{ 'FILE_UPLOAD.MESSAGES.UPLOAD_PROGRESS'
@ -34,6 +35,7 @@
</span> </span>
<span <span
tabindex="0"
class="adf-upload-dialog__title" class="adf-upload-dialog__title"
*ngIf="uploadList.isUploadCancelled()"> *ngIf="uploadList.isUploadCancelled()">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_CANCELED' | translate }} {{ 'FILE_UPLOAD.MESSAGES.UPLOAD_CANCELED' | translate }}
@ -41,6 +43,7 @@
</header> </header>
<section class="adf-upload-dialog__info" <section class="adf-upload-dialog__info"
tabindex="0"
*ngIf="totalErrors"> *ngIf="totalErrors">
{{ {{
(totalErrors > 1 (totalErrors > 1

View File

@ -6,54 +6,69 @@
<adf-icon *ngIf="mimeType !== 'default'" value="adf:{{ mimeType }}"></adf-icon> <adf-icon *ngIf="mimeType !== 'default'" value="adf:{{ mimeType }}"></adf-icon>
<span <span
tabindex="0"
class="adf-file-uploading-row__name" class="adf-file-uploading-row__name"
title="{{ file.name }}"> title="{{ file.name }}">
{{ file.name }} {{ file.name }}
</span> </span>
<span *ngIf="isUploadVersion()" class="adf-file-uploading-row__version"> <span *ngIf="isUploadVersion()" class="adf-file-uploading-row__version" tabindex="0" >
<mat-chip color="primary" [attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.VERSION' | translate" [title]="'version' + versionNumber" disabled>{{ <mat-chip color="primary"
versionNumber [attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.VERSION' | translate: { version: versionNumber }"
[title]="'version' + versionNumber" disabled>{{
versionNumber
}}</mat-chip> }}</mat-chip>
</span> </span>
<div <div
*ngIf="file.status === FileUploadStatus.Progress || file.status === FileUploadStatus.Starting" tabindex="0"
role="button"
#toggleIcon="toggleIcon"
adf-toggle-icon
(keyup.enter)="onCancel(file)"
(click)="onCancel(file)" (click)="onCancel(file)"
data-automation-id="cancel-upload-progress"
*ngIf="file.status === FileUploadStatus.Progress || file.status === FileUploadStatus.Starting"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CANCEL_FILE_UPLOAD' | translate: { file: file.name }"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle" class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_FILE' | translate }}"> title="{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_FILE' | translate }}">
<span class="adf-file-uploading-row__status">
<span class="adf-file-uploading-row__status" *ngIf="!toggleIcon.isToggled">
{{ file.progress.loaded | adfFileSize }} / {{ file.progress.total | adfFileSize }} {{ file.progress.loaded | adfFileSize }} / {{ file.progress.total | adfFileSize }}
</span> </span>
<mat-icon <mat-icon *ngIf="toggleIcon.isToggled"
mat-list-icon
class="adf-file-uploading-row__action adf-file-uploading-row__action--cancel"> class="adf-file-uploading-row__action adf-file-uploading-row__action--cancel">
clear clear
</mat-icon> </mat-icon>
</div> </div>
<div <button mat-icon-button
adf-toggle-icon
#toggleIcon="toggleIcon"
*ngIf="file.status === FileUploadStatus.Complete && !isUploadVersion()" *ngIf="file.status === FileUploadStatus.Complete && !isUploadVersion()"
(click)="onRemove(file)" (click)="onRemove(file)"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle" class="adf-file-uploading-row__group"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.REMOVE_FILE' | translate: { file: file.name }"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.REMOVE_FILE' | translate }}"> title="{{ 'ADF_FILE_UPLOAD.BUTTON.REMOVE_FILE' | translate }}">
<mat-icon
mat-list-icon <mat-icon *ngIf="!toggleIcon.isToggled"
class="adf-file-uploading-row__status adf-file-uploading-row__status--done"> class="adf-file-uploading-row__status adf-file-uploading-row__status--done">
check_circle check_circle
</mat-icon> </mat-icon>
<mat-icon <mat-icon *ngIf="toggleIcon.isToggled"
mat-list-icon
class="adf-file-uploading-row__action adf-file-uploading-row__action--remove"> class="adf-file-uploading-row__action adf-file-uploading-row__action--remove">
remove_circle remove_circle
</mat-icon> </mat-icon>
</div> </button>
<div <div
*ngIf="file.status === FileUploadStatus.Complete && isUploadVersion()" *ngIf="file.status === FileUploadStatus.Complete && isUploadVersion()"
class="adf-file-uploading-row__file-version"> class="adf-file-uploading-row__file-version"
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_DONE_STATUS' | translate"
role="status"
>
<mat-icon <mat-icon
mat-list-icon mat-list-icon
class="adf-file-uploading-row__status--done"> class="adf-file-uploading-row__status--done">
@ -61,34 +76,44 @@
</mat-icon> </mat-icon>
</div> </div>
<div <button
adf-toggle-icon
#toggleIconCancel="toggleIcon"
mat-icon-button
*ngIf="file.status === FileUploadStatus.Pending" *ngIf="file.status === FileUploadStatus.Pending"
(click)="onCancel(file)" (click)="onCancel(file)"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"> class="adf-file-uploading-row__group"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.CANCEL_FILE' | translate }}"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CANCEL_FILE' | translate: { file: file.name }">
<mat-icon <mat-icon
mat-list-icon *ngIf="!toggleIconCancel.isToggled"
class="adf-file-uploading-row__status adf-file-uploading-row__status--pending"> class="adf-file-uploading-row__status adf-file-uploading-row__status--pending">
schedule schedule
</mat-icon> </mat-icon>
<mat-icon <mat-icon
mat-list-icon *ngIf="toggleIconCancel.isToggled"
class="adf-file-uploading-row__action adf-file-uploading-row__action--remove"> class="adf-file-uploading-row__action adf-file-uploading-row__action--remove">
remove_circle remove_circle
</mat-icon> </mat-icon>
</div> </button>
<div <div
tabindex="0"
role="status"
*ngIf="file.status === FileUploadStatus.Error" *ngIf="file.status === FileUploadStatus.Error"
class="adf-file-uploading-row__block adf-file-uploading-row__status--error"> class="adf-file-uploading-row__block adf-file-uploading-row__status--error">
<mat-icon mat-list-icon <mat-icon mat-list-icon
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.ERROR' | translate" [attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.UPLOAD_FILE_ERROR' | translate: { error: file.errorCode | adfFileUploadError }"
[matTooltip]="file.errorCode | adfFileUploadError"> [matTooltip]="file.errorCode | adfFileUploadError">
report_problem report_problem
</mat-icon> </mat-icon>
</div> </div>
<div <div
tabindex="0"
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate"
role="status"
*ngIf="showCancelledStatus()" *ngIf="showCancelledStatus()"
class="adf-file-uploading-row__block adf-file-uploading-row__status--cancelled"> class="adf-file-uploading-row__block adf-file-uploading-row__status--cancelled">
{{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }} {{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }}

View File

@ -15,7 +15,7 @@
.adf-file-uploading-row { .adf-file-uploading-row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.5em 1em; padding: 0.3em 1em;
cursor: default; cursor: default;
&:hover { &:hover {
@ -34,28 +34,15 @@
min-width: 100px; min-width: 100px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
line-height: 40px;
} }
&__group--toggle { &__group--toggle {
cursor: pointer; cursor: pointer;
display:flex;
.adf-file-uploading-row__status { align-items: center;
display: flex; height: 40px;
} line-height: 40px;
.adf-file-uploading-row__action {
display: none;
}
&:hover {
.adf-file-uploading-row__status {
display: none;
}
.adf-file-uploading-row__action {
display: flex;
}
}
} }
&__status--done { &__status--done {

View File

@ -0,0 +1,91 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, ViewChild } from '@angular/core';
import { ToggleIconDirective } from './toggle-icon.directive';
import { setupTestBed } from '@alfresco/adf-core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
@Component({
selector: 'adf-test-component',
template: `
<button id="testButton" adf-toggle-icon>test</button>
`
})
class TestComponent {
@ViewChild(ToggleIconDirective) directive: ToggleIconDirective;
}
describe('ToggleIconDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
setupTestBed({
declarations: [
TestComponent,
ToggleIconDirective
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should set toggle to true on mouseenter', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new MouseEvent('mouseenter'));
expect(component.directive.isToggled).toBe(true);
});
it('should set toggle to false on mouseleave if element is not focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new MouseEvent('mouseleave'));
expect(component.directive.isToggled).toBe(false);
});
it('should set toggle and focus to false on mouseleave when element is focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
expect(component.directive.isToggled).toBe(true);
button.dispatchEvent(new MouseEvent('mouseleave'));
expect(component.directive.isToggled).toBe(false);
expect(component.directive.isFocused).toBe(false);
});
it('should set toggle and focus to true when element is focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
expect(component.directive.isToggled).toBe(true);
expect(component.directive.isFocused).toBe(true);
});
it('should set toggle and focus to true when element blur event', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
button.dispatchEvent(new Event('blur'));
expect(component.directive.isToggled).toBe(false);
expect(component.directive.isFocused).toBe(false);
});
});

View File

@ -0,0 +1,62 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[adf-toggle-icon]',
exportAs: 'toggleIcon'
})
export class ToggleIconDirective {
private isFocus: boolean = false;
private toggle: boolean = false;
@HostListener('mouseenter') onMouseEnter() {
if (!this.isFocus) {
this.toggle = true;
}
}
@HostListener('mouseleave') onMouseLeave() {
if (!this.isFocus) {
this.toggle = false;
}
if (this.isFocus && this.toggle) {
this.isFocus = false;
this.toggle = false;
}
}
@HostListener('focus') onFocus() {
this.isFocus = true;
this.toggle = true;
}
@HostListener('blur') onBlur() {
this.isFocus = false;
this.toggle = false;
}
get isToggled(): boolean {
return this.toggle;
}
get isFocused(): boolean {
return this.isFocus;
}
}

View File

@ -24,6 +24,7 @@ export * from './components/file-uploading-list-row.component';
export * from './components/upload-files.event'; export * from './components/upload-files.event';
export * from './directives/file-draggable.directive'; export * from './directives/file-draggable.directive';
export * from './directives/toggle-icon.directive';
export * from './pipes/file-upload-error.pipe'; export * from './pipes/file-upload-error.pipe';

View File

@ -27,6 +27,7 @@ import { UploadDragAreaComponent } from './components/upload-drag-area.component
import { FileUploadErrorPipe } from './pipes/file-upload-error.pipe'; import { FileUploadErrorPipe } from './pipes/file-upload-error.pipe';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { FileDraggableDirective } from './directives/file-draggable.directive'; import { FileDraggableDirective } from './directives/file-draggable.directive';
import { ToggleIconDirective } from './directives/toggle-icon.directive';
@NgModule({ @NgModule({
imports: [ imports: [
@ -42,7 +43,8 @@ import { FileDraggableDirective } from './directives/file-draggable.directive';
FileUploadingDialogComponent, FileUploadingDialogComponent,
FileUploadingListComponent, FileUploadingListComponent,
FileUploadingListRowComponent, FileUploadingListRowComponent,
FileUploadErrorPipe FileUploadErrorPipe,
ToggleIconDirective
], ],
exports: [ exports: [
FileDraggableDirective, FileDraggableDirective,
@ -52,7 +54,8 @@ import { FileDraggableDirective } from './directives/file-draggable.directive';
FileUploadingDialogComponent, FileUploadingDialogComponent,
FileUploadingListComponent, FileUploadingListComponent,
FileUploadingListRowComponent, FileUploadingListRowComponent,
FileUploadErrorPipe FileUploadErrorPipe,
ToggleIconDirective
] ]
}) })
export class UploadModule {} export class UploadModule {}