MOBILE-3753 a11y: More aria role button to a new directive

main
Pau Ferrer Ocaña 2021-05-12 16:35:45 +02:00
parent 4c59d4ce81
commit a68d84c4d4
7 changed files with 73 additions and 153 deletions

View File

@ -58,9 +58,7 @@
[class.addon-calendar-event-past-day]="isPastMonth || day.ispast" [class.addon-calendar-event-past-day]="isPastMonth || day.ispast"
role="button cell" role="button cell"
tabindex="0" tabindex="0"
(click)="dayClicked(day.mday)" (ariaButtonClick)="dayClicked(day.mday)"
(keyup)="dayAction.keyUp($event, day.mday)"
(keydown)="dayAction.keyDown($event)"
> >
<p class="addon-calendar-day-number"> <p class="addon-calendar-day-number">
<span aria-hidden="true">{{ day.mday }}</span> <span aria-hidden="true">{{ day.mday }}</span>
@ -80,9 +78,7 @@
[class.addon-calendar-event-past]="event.ispast" [class.addon-calendar-event-past]="event.ispast"
role="button" role="button"
tabindex="0" tabindex="0"
(click)="eventClicked(event, $event)" (ariaButtonClick)="eventClicked(event, $event)"
(keyup)="eventAction.keyUp($event, event)"
(keydown)="eventAction.keyDown($event)"
> >
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" <ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"

View File

@ -41,7 +41,6 @@ import { AddonCalendarOffline } from '../../services/calendar-offline';
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses'; import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreAriaRoleButton } from '@classes/aria-role-button';
/** /**
* Component that displays a calendar. * Component that displays a calendar.
@ -68,8 +67,6 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
timeFormat?: string; timeFormat?: string;
isCurrentMonth = false; isCurrentMonth = false;
isPastMonth = false; isPastMonth = false;
dayAction: AddonCalendarDayButton;
eventAction: AddonCalendarEventButton;
protected year?: number; protected year?: number;
protected month?: number; protected month?: number;
@ -90,10 +87,6 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
constructor( constructor(
differs: KeyValueDiffers, differs: KeyValueDiffers,
) { ) {
this.dayAction = new AddonCalendarDayButton(this);
this.eventAction = new AddonCalendarEventButton(this);
this.currentSiteId = CoreSites.getCurrentSiteId(); this.currentSiteId = CoreSites.getCurrentSiteId();
if (CoreLocalNotifications.isAvailable()) { if (CoreLocalNotifications.isAvailable()) {
@ -535,31 +528,3 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
} }
} }
/**
* Helper class to manage day button.
*/
class AddonCalendarDayButton extends CoreAriaRoleButton<AddonCalendarCalendarComponent> {
/**
* @inheritdoc
*/
click(event: Event, day: number): void {
this.componentInstance.dayClicked(day);
}
}
/**
* Helper class to manage event button.
*/
class AddonCalendarEventButton extends CoreAriaRoleButton<AddonCalendarCalendarComponent> {
/**
* @inheritdoc
*/
click(event: Event, calendarEvent: AddonCalendarEventToDisplay): void {
this.componentInstance.eventClicked(calendarEvent, event);
}
}

View File

@ -1,70 +0,0 @@
// (C) Copyright 2015 Moodle Pty 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.
export abstract class CoreAriaRoleButton<T = unknown> {
componentInstance: T;
constructor(componentInstance: T) {
this.componentInstance = componentInstance;
}
/**
* A11y key functionality that prevents keyDown events.
*
* @param event Event.
*/
keyDown(event: KeyboardEvent): void {
if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) {
event.preventDefault();
event.stopPropagation();
}
}
/**
* A11y key functionality that translates space and enter keys to click action.
*
* @param event Event.
* @param args Additional args.
*/
keyUp(event: KeyboardEvent, ...args: unknown[]): void {
if ((event.key == ' ' || event.key == 'Enter') && this.isAllowed()) {
event.preventDefault();
event.stopPropagation();
this.click(event, ...args);
}
}
/**
* A11y click functionality.
*
* @param event Event.
* @param args Additional args.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
click(event?: Event, ...args: unknown[]): void {
// Nothing defined here.
}
/**
* Checks if action is allowed in class.
*
* @returns If allowed.
*/
isAllowed(): boolean {
return true;
}
}

View File

@ -4,18 +4,15 @@
[alt]="'core.pictureof' | translate:{$a: fullname}" [alt]="'core.pictureof' | translate:{$a: fullname}"
core-external-content core-external-content
onError="this.src='assets/img/user-avatar.png'" onError="this.src='assets/img/user-avatar.png'"
(click)="gotoProfile($event)" (ariaButtonClick)="gotoProfile($event)"
[attr.aria-hidden]="!linkProfile" [attr.aria-hidden]="!linkProfile"
[attr.role]="linkProfile ? 'button' : null" [attr.role]="linkProfile ? 'button' : null"
(keydown)="buttonAction.keyDown($event)"
(keyup)="buttonAction.keyUp($event)"
[attr.tabindex]="linkProfile ? 0 : null" [attr.tabindex]="linkProfile ? 0 : null"
[class.clickable]="linkProfile" [class.clickable]="linkProfile"
> >
<img *ngIf="!avatarUrl" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" <img *ngIf="!avatarUrl" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}"
(click)="gotoProfile($event)" [attr.aria-hidden]="!linkProfile" [attr.role]="linkProfile ? 'button' : null" (ariaButtonClick)="gotoProfile($event)" [attr.aria-hidden]="!linkProfile" [attr.role]="linkProfile ? 'button' : null"
(keydown)="buttonAction.keyDown($event)" (keyup)="buttonAction.keyUp($event)"
[attr.tabindex]="linkProfile ? 0 : null"> [attr.tabindex]="linkProfile ? 0 : null">
<span *ngIf="checkOnline && isOnline()" class="contact-status online" role="status" [attr.aria-label]="'core.online' | translate"> <span *ngIf="checkOnline && isOnline()" class="contact-status online" role="status" [attr.aria-label]="'core.online' | translate">

View File

@ -20,7 +20,6 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreUserProvider, CoreUserBasicData } from '@features/user/services/user'; import { CoreUserProvider, CoreUserBasicData } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreAriaRoleButton } from '@classes/aria-role-button';
/** /**
* Component to display a "user avatar". * Component to display a "user avatar".
@ -39,15 +38,13 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
@Input() profileUrl?: string; @Input() profileUrl?: string;
@Input() linkProfile = true; // Avoid linking to the profile if wanted. @Input() linkProfile = true; // Avoid linking to the profile if wanted.
@Input() fullname?: string; @Input() fullname?: string;
@Input() userId?: number; // If provided or found it will be used to link the image to the profile. @Input() protected userId?: number; // If provided or found it will be used to link the image to the profile.
@Input() courseId?: number; @Input() protected courseId?: number;
@Input() checkOnline = false; // If want to check and show online status. @Input() checkOnline = false; // If want to check and show online status.
@Input() extraIcon?: string; // Extra icon to show near the avatar. @Input() extraIcon?: string; // Extra icon to show near the avatar.
avatarUrl?: string; avatarUrl?: string;
buttonAction: CoreUserAvatarButton;
// Variable to check if we consider this user online or not. // Variable to check if we consider this user online or not.
// @TODO: Use setting when available (see MDL-63972) so we can use site setting. // @TODO: Use setting when available (see MDL-63972) so we can use site setting.
protected timetoshowusers = 300000; // Miliseconds default. protected timetoshowusers = 300000; // Miliseconds default.
@ -55,7 +52,6 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
protected pictureObserver: CoreEventObserver; protected pictureObserver: CoreEventObserver;
constructor() { constructor() {
this.currentUserId = CoreSites.getCurrentSiteUserId(); this.currentUserId = CoreSites.getCurrentSiteUserId();
this.pictureObserver = CoreEvents.on( this.pictureObserver = CoreEvents.on(
@ -67,15 +63,12 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
}, },
CoreSites.getCurrentSiteId(), CoreSites.getCurrentSiteId(),
); );
this.buttonAction = new CoreUserAvatarButton(this);
} }
/** /**
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.setFields(); this.setFields();
} }
@ -137,14 +130,19 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
* @param event Click event. * @param event Click event.
*/ */
gotoProfile(event: Event): void { gotoProfile(event: Event): void {
if (!this.buttonAction.isAllowed()) { if (!this.linkProfile || !this.userId) {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.buttonAction.click(); CoreNavigator.navigateToSitePath('user', {
params: {
userId: this.userId,
courseId: this.courseId,
},
});
} }
/** /**
@ -156,32 +154,6 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
} }
/**
* Helper class to manage rol button.
*/
class CoreUserAvatarButton extends CoreAriaRoleButton<CoreUserAvatarComponent> {
/**
* @inheritdoc
*/
click(): void {
CoreNavigator.navigateToSitePath('user', {
params: {
userId: this.componentInstance.userId,
courseId: this.componentInstance.courseId,
},
});
}
/**
* @inheritdoc
*/
isAllowed(): boolean {
return this.componentInstance.linkProfile && !!this.componentInstance.userId;
}
}
/** /**
* Type with all possible formats of user. * Type with all possible formats of user.
*/ */

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Moodle Pty 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, ElementRef, OnInit, Output, EventEmitter } from '@angular/core';
/**
* Directive to emulate click and key actions following aria role button.
*/
@Directive({
selector: '[ariaButtonClick]',
})
export class CoreAriaButtonClickDirective implements OnInit {
protected element: HTMLElement;
@Output() ariaButtonClick = new EventEmitter();
constructor(
element: ElementRef,
) {
this.element = element.nativeElement;
}
/**
* Initialize actions.
*/
ngOnInit(): void {
this.element.addEventListener('click', async (event) => {
this.ariaButtonClick.emit(event);
});
this.element.addEventListener('keydown', async (event) => {
if ((event.key == ' ' || event.key == 'Enter')) {
event.preventDefault();
event.stopPropagation();
}
});
this.element.addEventListener('keyup', async (event) => {
if ((event.key == ' ' || event.key == 'Enter')) {
this.ariaButtonClick.emit(event);
}
});
}
}

View File

@ -24,6 +24,7 @@ import { CoreLinkDirective } from './link';
import { CoreLongPressDirective } from './long-press'; import { CoreLongPressDirective } from './long-press';
import { CoreSupressEventsDirective } from './supress-events'; import { CoreSupressEventsDirective } from './supress-events';
import { CoreUserLinkDirective } from './user-link'; import { CoreUserLinkDirective } from './user-link';
import { CoreAriaButtonClickDirective } from './aria-button';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -37,6 +38,7 @@ import { CoreUserLinkDirective } from './user-link';
CoreLongPressDirective, CoreLongPressDirective,
CoreSupressEventsDirective, CoreSupressEventsDirective,
CoreUserLinkDirective, CoreUserLinkDirective,
CoreAriaButtonClickDirective,
], ],
exports: [ exports: [
CoreAutoFocusDirective, CoreAutoFocusDirective,
@ -49,6 +51,7 @@ import { CoreUserLinkDirective } from './user-link';
CoreLongPressDirective, CoreLongPressDirective,
CoreSupressEventsDirective, CoreSupressEventsDirective,
CoreUserLinkDirective, CoreUserLinkDirective,
CoreAriaButtonClickDirective,
], ],
}) })
export class CoreDirectivesModule {} export class CoreDirectivesModule {}