MOBILE-3753 a11y: More aria role button to a new directive
parent
4c59d4ce81
commit
a68d84c4d4
|
@ -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"
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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">
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}
|
||||||
|
|
Loading…
Reference in New Issue