Merge pull request #3541 from crazyserver/MOBILE-4065

Mobile 4065
main
Dani Palou 2023-02-07 15:28:25 +01:00 committed by GitHub
commit dab503bb9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 230 additions and 161 deletions

20
package-lock.json generated
View File

@ -4319,11 +4319,11 @@
}
},
"@ionic/angular": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.9.2.tgz",
"integrity": "sha512-5GzKg+l4au3xFECky2v/USlRsmTAXgvNO5Zalt7NUXc//VJIL2lQvswojE6FBWuM/xR5W0CWbJdFth19TaZWVQ==",
"version": "5.9.4",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.9.4.tgz",
"integrity": "sha512-U/85FePF48VaZXTudTwpVXDqhGmYfarl/7vki7a4umnIORnWtHqD2/pXsqqZ/O1EcbALwULYIeVXAfkFpPd2wQ==",
"requires": {
"@ionic/core": "5.9.2",
"@ionic/core": "5.9.4",
"tslib": "^1.9.3"
},
"dependencies": {
@ -4666,9 +4666,9 @@
}
},
"@ionic/core": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.2.tgz",
"integrity": "sha512-1ZqSBS8R6tGQsc+LsLxIRv0q3Ww6jwgJXLvdn6FmVWfpPbBvT+CjCuU9hqJ5qwM+atErblUMYSexvvpws8lGAA==",
"version": "5.9.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.9.4.tgz",
"integrity": "sha512-Ngz9yVT6fIiGdSxxBer8uJxP4w6PasvohYpLxhtMgYiWnyIu0vZra2ui3HrYukCzUo5/SbNPiUr1l7cj1E+7qw==",
"requires": {
"@stencil/core": "^2.4.0",
"ionicons": "^5.5.3",
@ -5852,9 +5852,9 @@
}
},
"@stencil/core": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.11.0.tgz",
"integrity": "sha512-/IubCWhVXCguyMUp/3zGrg3c882+RJNg/zpiKfyfJL3kRCOwe+/MD8OoAXVGdd+xAohZKIi1Ik+EHFlsptsjLg=="
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.2.tgz",
"integrity": "sha512-r+vbxsGNcBaV1VDOYW25lv4QfXTlNoIb5GpUX7rZ+cr59yqYCZC5tlV+IzX6YgHKW62ulCc9M3RYtTfHtNbNNw=="
},
"@storybook/addon-controls": {
"version": "6.1.21",

View File

@ -73,7 +73,7 @@
"@ionic-native/status-bar": "5.36.0",
"@ionic-native/web-intent": "5.36.0",
"@ionic-native/zip": "5.36.0",
"@ionic/angular": "5.9.2",
"@ionic/angular": "5.9.4",
"@moodlehq/cordova-plugin-file-opener": "3.0.5-moodle.1",
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",

View File

@ -33,7 +33,7 @@
</ion-grid>
<core-swipe-slides [manager]="manager">
<ng-template let-month="item">
<ng-template let-month="item" let-activeView="active">
<!-- Calendar view. -->
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
<div role="rowgroup">
@ -57,9 +57,9 @@
"today": month.isCurrentMonth && day.istoday,
"weekend": day.isweekend,
"duration_finish": day.haslastdayofevent
}' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell" tabindex="0"
(ariaButtonClick)="dayClicked(day.mday)">
<p class="addon-calendar-day-number" role="button">
}' [class.addon-calendar-event-past-day]="month.isPastMonth || day.ispast" role="cell"
(ariaButtonClick)="dayClicked(day.mday)" [tabindex]="activeView ? 0 : -1">
<p class="addon-calendar-day-number">
<span aria-hidden="true">{{ day.mday }}</span>
<span class="sr-only">{{ day.periodName | translate }}</span>
</p>
@ -72,8 +72,8 @@
<div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents">
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
[class.addon-calendar-event-past]="event.ispast" role="button" tabindex="0"
(ariaButtonClick)="eventClicked(event, $event)">
[class.addon-calendar-event-past]="event.ispast" (ariaButtonClick)="eventClicked(event, $event)"
[tabindex]="activeView ? 0 : -1">
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>
<ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock"
[attr.aria-label]="'core.notsent' | translate"></ion-icon>

View File

@ -25,7 +25,6 @@
@include border-end(1px, solid var(--addon-calendar-border-color));
overflow: hidden;
min-height: 60px;
cursor: pointer;
&:first-child {
@include padding-horizontal(10px, null);

View File

@ -74,26 +74,20 @@
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions?.getItemAriaCurrent(discussion)"
(click)="discussions?.select(discussion)" button>
<ion-label>
<div class="addon-mod-forum-discussion-title">
<p class="ion-text-wrap item-heading">
<ion-icon name="fas-map-pin" *ngIf="discussion.pinned"
[attr.aria-label]="'addon.mod_forum.discussionpinned' | translate"></ion-icon>
<ion-icon name="fas-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"
[attr.aria-label]="'addon.mod_forum.favourites' | translate"></ion-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id"
[courseId]="courseId">
</core-format-text>
<ion-icon name="fas-lock" *ngIf="discussion.locked" class="addon-mod-forum-locked-icon"
[attr.aria-label]="'addon.mod_forum.discussionlocked' | translate"></ion-icon>
</p>
<ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite" fill="clear"
[attr.aria-label]="('core.displayoptions' | translate)" (click)="showOptionsMenu($event, discussion)">
<ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true">
</ion-icon>
</ion-button>
</div>
<p class="addon-mod-forum-discussion-title ion-text-wrap item-heading">
<ion-icon name="fas-map-pin" *ngIf="discussion.pinned"
[attr.aria-label]="'addon.mod_forum.discussionpinned' | translate"></ion-icon>
<ion-icon name="fas-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"
[attr.aria-label]="'addon.mod_forum.favourites' | translate"></ion-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module && module.id"
[courseId]="courseId">
</core-format-text>
<ion-icon name="fas-lock" *ngIf="discussion.locked" class="addon-mod-forum-locked-icon"
[attr.aria-label]="'addon.mod_forum.discussionlocked' | translate"></ion-icon>
</p>
<div class="addon-mod-forum-discussion-info">
<core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId">
<core-user-avatar *ngIf="discussion.userfullname" [user]="discussion" slot="start" [courseId]="courseId"
[linkProfile]="false">
</core-user-avatar>
<div class="addon-mod-forum-discussion-author">
<span *ngIf="discussion.userfullname">{{discussion.userfullname}}</span>
@ -136,6 +130,11 @@
</ion-col>
</ion-row>
</ion-label>
<ion-button *ngIf="canPin || discussion.canlock || discussion.canfavourite" fill="clear"
[attr.aria-label]="('core.displayoptions' | translate)" (click)="showOptionsMenu($event, discussion)" slot="end">
<ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true">
</ion-icon>
</ion-button>
</ion-item>
<core-infinite-loading [enabled]="discussions && discussions.loaded && !discussions.completed" [error]="fetchFailed"

View File

@ -7,7 +7,6 @@
}
.addon-mod-forum-discussion.item {
ion-label {
margin-top: 4px;
@ -35,21 +34,30 @@
@include margin(0, 8px, 0, 0);
}
.addon-mod-forum-discussion-title,
.addon-mod-forum-discussion-info {
display: flex;
align-items: center;
}
.addon-mod-forum-discussion-title .item-heading,
.addon-mod-forum-discussion-info .addon-mod-forum-discussion-author {
flex-grow: 1;
}
.addon-mod-forum-discussion-title {
@include margin-horizontal(null, 8px);
line-height: 18px;
}
.addon-mod-forum-discussion-more-info.ios {
font-size: 0.9rem;
}
ion-button {
position: absolute;
@include position (4px, 8px, null, null);
}
}
.core-group-selector {

View File

@ -29,19 +29,19 @@ export class CoreAriaRoleTab<T = unknown> {
* @param e Event.
*/
keyDown(tabFindIndex: string, e: KeyboardEvent): void {
if (e.key == ' ' ||
e.key == 'Enter' ||
e.key == 'Home' ||
e.key == 'End' ||
(this.isHorizontal() && (e.key == 'ArrowRight' || e.key == 'ArrowLeft')) ||
(!this.isHorizontal() && (e.key == 'ArrowUp' ||e.key == 'ArrowDown'))
if (e.key === ' ' ||
e.key === 'Enter' ||
e.key === 'Home' ||
e.key === 'End' ||
(this.isHorizontal() && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) ||
(!this.isHorizontal() && (e.key === 'ArrowUp' ||e.key === 'ArrowDown'))
) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
if (e.key == ' ' || e.key == 'Enter') {
if (e.key === ' ' || e.key === 'Enter') {
this.selectTabCandidate = tabFindIndex;
}
}
@ -64,7 +64,7 @@ export class CoreAriaRoleTab<T = unknown> {
e.stopPropagation();
e.stopImmediatePropagation();
if (e.key == ' ' || e.key == 'Enter') {
if (e.key === ' ' || e.key === 'Enter') {
if (this.selectTabCandidate === tabFindIndex) {
this.selectTab(tabFindIndex, e);
}

View File

@ -132,7 +132,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @param event The mouse event.
*/
doNotBlur(event: Event): void {
if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
if (event.type === 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
@ -147,7 +147,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @returns Wether space or enter have been pressed.
*/
protected isValidKeyboardKey(event: KeyboardEvent): boolean {
return event.key == ' ' || event.key == 'Enter';
return event.key === ' ' || event.key === 'Enter';
}
}

View File

@ -1,5 +1,5 @@
<ion-slides *ngIf="loaded" (ionSlideWillChange)="slideWillChange()" (ionSlideDidChange)="slideDidChange()" [options]="options">
<ion-slide *ngFor="let item of items; index as index">
<ion-slide *ngFor="let item of items; index as index" [attr.aria-hidden]="!isActive(index)">
<ng-container *ngIf="template" [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{item: item, active: isActive(index)}">
</ng-container>
</ion-slide>

View File

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

View File

@ -5,9 +5,6 @@
width: var(--core-avatar-size);
height: var(--core-avatar-size);
.clickable {
cursor: pointer;
}
img {
border-radius: 50%;
width: var(--core-avatar-size);

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, OnInit, Output, EventEmitter } from '@angular/core';
import { Directive, ElementRef, OnInit, Output, EventEmitter, OnChanges, SimpleChanges, Input } from '@angular/core';
import { CoreDom } from '@singletons/dom';
/**
@ -21,10 +21,11 @@ import { CoreDom } from '@singletons/dom';
@Directive({
selector: '[ariaButtonClick]',
})
export class CoreAriaButtonClickDirective implements OnInit {
export class CoreAriaButtonClickDirective implements OnInit, OnChanges {
protected element: HTMLElement;
@Input() disabled = false;
@Output() ariaButtonClick = new EventEmitter();
constructor(
@ -34,10 +35,27 @@ export class CoreAriaButtonClickDirective implements OnInit {
}
/**
* Initialize actions.
* @inheritdoc
*/
ngOnInit(): void {
CoreDom.onActivate(this.element, (event) => this.ariaButtonClick.emit(event));
CoreDom.initializeClickableElementA11y(this.element, (event) => this.ariaButtonClick.emit(event));
}
/**
* @inheritdoc
*/
ngOnChanges(changes: SimpleChanges): void {
if (!changes.disabled) {
return;
}
if (this.element.getAttribute('tabindex') === '0' && this.disabled) {
this.element.setAttribute('tabindex', '-1');
}
if (this.element.getAttribute('tabindex') === '-1' && !this.disabled) {
this.element.setAttribute('tabindex', '0');
}
}
}

View File

@ -610,12 +610,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
return;
}
if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
element.setAttribute('tabindex', '0');
element.setAttribute('role', 'button');
}
CoreDom.onActivate(element, async (event) => {
CoreDom.initializeClickableElementA11y(element, async (event) => {
event.preventDefault();
event.stopPropagation();

View File

@ -57,12 +57,7 @@ export class CoreLinkDirective implements OnInit {
* Function executed when the component is initialized.
*/
ngOnInit(): void {
if (this.element.tagName != 'BUTTON' && this.element.tagName != 'A') {
this.element.setAttribute('tabindex', '0');
this.element.setAttribute('role', 'button');
}
CoreDom.onActivate(this.element, (event) => this.performAction(event));
CoreDom.initializeClickableElementA11y(this.element, (event) => this.performAction(event));
}
/**

View File

@ -32,7 +32,7 @@
class="expandable-status-icon" (ariaButtonClick)="toggleExpand($event, section)"
[attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-index-section-' + section.id"
[class.expandable-status-icon-expanded]="section.expanded" tabindex="0">
[class.expandable-status-icon-expanded]="section.expanded">
</ion-icon>
<ion-icon *ngIf="!section.hasVisibleModules" name="" slot="start" aria-hidden="true" class="expandable-status-icon">
</ion-icon>

View File

@ -1,10 +1,11 @@
<ion-card *ngIf="module.handlerData && module.visibleoncoursepage !== 0" [class.core-course-module-with-view]="moduleHasView">
<ion-card *ngIf="module.handlerData && module.visibleoncoursepage !== 0" [class.core-course-module-with-view]="moduleHasView"
(click)="moduleClicked($event)" [button]="module.handlerData.action && module.uservisible"
[attr.aria-label]="module.handlerData.a11yTitle ? module.handlerData.a11yTitle : null">
<ng-container *ngIf="!module.handlerData.loading">
<ion-item id="core-course-module-{{module.id}}" detail="false"
class="ion-text-wrap core-course-module-handler core-module-main-item {{module.handlerData.class}}"
(click)="moduleClicked($event)" [attr.aria-label]="module.handlerData.a11yTitle" [ngClass]="{
<ion-item id="core-course-module-{{module.id}}"
class="ion-text-wrap core-course-module-handler core-module-main-item {{module.handlerData.class}}" [ngClass]="{
'item-dimmed': module.visible === 0 || module.uservisible === false
}" [button]="module.handlerData.action && module.uservisible">
}">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon" [modname]="module.modname"
[componentId]="module.instance">

View File

@ -1,8 +1,8 @@
<ion-card [class.core-course-list-item]="layout == 'list' || layout == 'listwithenrol'"
[class.core-course-list-card]="layout == 'card' || layout == 'summarycard'" [class.item-dimmed]="course.hidden">
[class.core-course-list-card]="layout == 'card' || layout == 'summarycard'" [class.item-dimmed]="course.hidden" (click)="openCourse()"
button [attr.aria-label]="course.displayname || course.fullname">
<div *ngIf="layout == 'card' || layout == 'summarycard'" (click)="openCourse()" class="core-course-thumb"
[class.core-course-color-img]="course.courseImage">
<div *ngIf="layout == 'card' || layout == 'summarycard'" class="core-course-thumb" [class.core-course-color-img]="course.courseImage">
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" />
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" class="course-icon" aria-hidden="true">
</ion-icon>
@ -27,7 +27,7 @@
</div>
</ng-container>
<ion-item class="ion-text-wrap" button detail="false" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname">
<ion-item class="ion-text-wrap">
<ng-container *ngIf="layout == 'list' || layout == 'listwithenrol'">
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon core-course-thumb"

View File

@ -1,38 +1,40 @@
<ion-item button class="ion-text-wrap" (click)="action('download')" *ngIf="downloadCourseEnabled" detail="false">
<ion-icon *ngIf="!prefetch.loading" [name]="prefetch.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-spinner *ngIf="prefetch.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-label>
<h2>{{ prefetch.statusTranslatable | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('delete')" detail="false"
*ngIf="prefetch.status == 'downloaded' || prefetch.status == 'outdated'">
<ion-icon name="fas-trash" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'addon.storagemanager.deletedata' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('hide')" *ngIf="!course.hidden" detail="false">
<ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.hidecourse' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('show')" *ngIf="course.hidden" detail="false">
<ion-icon name="fas-eye-slash" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.show' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('favourite')" *ngIf="!course.isfavourite" detail="false">
<ion-icon name="fas-star" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.addtofavourites' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('unfavourite')" *ngIf="course.isfavourite" detail="false">
<ion-icon name="far-star" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.courses.removefromfavourites' | translate }}</h2>
</ion-label>
</ion-item>
<ion-list>
<ion-item button class="ion-text-wrap" (click)="action('download')" *ngIf="downloadCourseEnabled" detail="false">
<ion-icon *ngIf="!prefetch.loading" [name]="prefetch.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-spinner *ngIf="prefetch.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-label>
<p class="item-heading">{{ prefetch.statusTranslatable | translate }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('delete')" detail="false"
*ngIf="prefetch.status == 'downloaded' || prefetch.status == 'outdated'">
<ion-icon name="fas-trash" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'addon.storagemanager.deletedata' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('hide')" *ngIf="!course.hidden" detail="false">
<ion-icon name="fas-eye" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.courses.hidecourse' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('show')" *ngIf="course.hidden" detail="false">
<ion-icon name="fas-eye-slash" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.courses.show' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('favourite')" *ngIf="!course.isfavourite" detail="false">
<ion-icon name="fas-star" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.courses.addtofavourites' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap" (click)="action('unfavourite')" *ngIf="course.isfavourite" detail="false">
<ion-icon name="far-star" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.courses.removefromfavourites' | translate }}</p>
</ion-label>
</ion-item>
</ion-list>

View File

@ -15,9 +15,9 @@
</div>
<div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden" (click)="toolbarPrev($event)"
<button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarPrevHidden ? 'true' : null" (click)="toolbarPrev($event)"
(keyup)="toolbarPrev($event)" (mousedown)="downAction($event)" (keydown)="downAction($event)"
[attr.aria-label]="'core.previous' | translate">
[attr.aria-label]="'core.previous' | translate" [tabindex]="toolbarPrevHidden ? -1 : 0">
<ion-icon name="fas-chevron-left" aria-hidden="true"></ion-icon>
</button>
<ion-slides [options]="slidesOpts" [dir]="direction" (ionSlideDidChange)="updateToolbarArrows()">
@ -25,69 +25,70 @@
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong" [title]="'core.editor.bold' | translate"
(click)="buttonAction($event, 'bold', 'strong')" (keyup)="buttonAction($event, 'bold', 'strong')"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-bold" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em" [title]="'core.editor.italic' | translate"
(click)="buttonAction($event, 'italic', 'em')" (keyup)="buttonAction($event, 'italic', 'em')"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-italic" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.u" [title]="'core.editor.underline' | translate"
(click)="buttonAction($event, 'underline', 'u')" (keyup)="buttonAction($event, 'underline', 'u')"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-underline" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strike" [title]="'core.editor.strike' | translate"
(click)="buttonAction($event, 'strikethrough', 'strike')" (keyup)="buttonAction($event, 'strikethrough', 'strike')"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-strikethrough" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.p" [title]="'core.editor.p' | translate"
(click)="buttonAction($event, 'p', 'block')" (keyup)="buttonAction($event, 'p', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)">
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-paragraph" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h3" [title]="'core.editor.h3' | translate"
(click)="buttonAction($event, 'h3', 'block')" (keyup)="buttonAction($event, 'h3', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)">
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">3</span>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h4" [title]="'core.editor.h4' | translate"
(click)="buttonAction($event, 'h4', 'block')" (keyup)="buttonAction($event, 'h4', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)">
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">4</span>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h5" [title]="'core.editor.h5' | translate"
(click)="buttonAction($event, 'h5', 'block')" (keyup)="buttonAction($event, 'h5', 'block')" (mousedown)="downAction($event)"
(keydown)="downAction($event)">
(keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-heading" aria-hidden="true"></ion-icon><span aria-hidden="true">5</span>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ul" [title]="'core.editor.unorderedlist' | translate"
(click)="buttonAction($event, 'insertUnorderedList')" (mousedown)="downAction($event)" (keydown)="downAction($event)">
(click)="buttonAction($event, 'insertUnorderedList')" (mousedown)="downAction($event)" (keydown)="downAction($event)"
tabindex="0">
<ion-icon name="fas-list-ul" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ol" [title]="'core.editor.orderedlist' | translate"
(click)="buttonAction($event, 'insertOrderedList')" (keyup)="buttonAction($event, 'insertOrderedList')"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-list-ol" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
@ -105,20 +106,20 @@
</ion-slide>
<ion-slide>
<button [attr.aria-pressed]="!rteEnabled" [title]="'core.editor.toggle' | translate" (click)="toggleEditor($event)"
(keyup)="toggleEditor($event)" (mousedown)="downAction($event)" (keydown)="downAction($event)">
(keyup)="toggleEditor($event)" (mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-code" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
<ion-slide *ngIf="isPhone">
<button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event, true)" (keyup)="hideToolbar($event, true)"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-times" aria-hidden="true"></ion-icon>
</button>
</ion-slide>
</ion-slides>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarNextHidden"
<button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarNextHidden ? 'true' : null"
[attr.aria-label]="'core.next' | translate" (click)="toolbarNext($event)" (keyup)="toolbarNext($event)"
(mousedown)="downAction($event)" (keydown)="downAction($event)">
(mousedown)="downAction($event)" (keydown)="downAction($event)" [tabindex]="toolbarNextHidden ? -1 : 0">
<ion-icon name="fas-chevron-right" aria-hidden="true"></ion-icon>
</button>
</div>

View File

@ -149,8 +149,9 @@
background-color: var(--toobar-background);
}
&.toolbar-arrow-hidden {
opacity: 0;
&[disabled],
&:disabled {
opacity: .5;
}
}
}

View File

@ -391,7 +391,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.stopBubble(event);
const move = event.key == 'ArrowLeft' ? -1 : +1;
const move = event.key === 'ArrowLeft' ? -1 : +1;
const cursor = this.getCurrentCursorPosition(this.editorElement);
this.setCurrentCursorPosition(this.editorElement, cursor + move);
@ -754,7 +754,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @returns Wether space or enter have been pressed.
*/
protected isValidKeyboardKey(event: KeyboardEvent): boolean {
return event.key == ' ' || event.key == 'Enter';
return event.key === ' ' || event.key === 'Enter';
}
/**

View File

@ -353,7 +353,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
* @param e Event.
*/
keyDown(e: KeyboardEvent): void {
if (e.key == 'Escape') {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
}
@ -365,7 +365,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
* @param e Event.
*/
keyUp(e: KeyboardEvent): void {
if (e.key == 'Escape') {
if (e.key === 'Escape') {
this.cancel(e);
}
}

View File

@ -1,4 +1,4 @@
<core-user-avatar *ngIf="(alwaysShow || isMainScreen) && siteInfo" [user]="siteInfo" class="core-bar-button-image clickable"
[linkProfile]="false" (ariaButtonClick)="openUserMenu($event)" [userTour]="userTour" role="button" tabindex="0"
[linkProfile]="false" (ariaButtonClick)="openUserMenu($event)" [userTour]="userTour"
[attr.aria-label]="'core.user.useraccount' | translate">
</core-user-avatar>

View File

@ -514,22 +514,57 @@ export class CoreDom {
*
* @param element Element to listen to events.
* @param callback Callback to call when clicked or the key is pressed.
* @deprecated since 4.1.1: Use initializeClickableElementA11y instead.
*/
static onActivate(element: HTMLElement, callback: (event: MouseEvent | KeyboardEvent) => void): void {
static onActivate(
element: HTMLElement & {disabled?: boolean},
callback: (event: MouseEvent | KeyboardEvent) => void,
): void {
this.initializeClickableElementA11y(element, callback);
}
/**
* Initializes a clickable element a11y calling the click action when pressed enter or space
* and adding tabindex and role if needed.
*
* @param element Element to listen to events.
* @param callback Callback to call when clicked or the key is pressed.
*/
static initializeClickableElementA11y(
element: HTMLElement & {disabled?: boolean},
callback: (event: MouseEvent | KeyboardEvent) => void,
): void {
element.addEventListener('click', (event) => callback(event));
element.addEventListener('keydown', (event) => {
if ((event.key == ' ' || event.key == 'Enter')) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
}
});
element.addEventListener('keyup', (event) => {
if ((event.key == ' ' || event.key == 'Enter')) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
callback(event);
}
});
if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
// Set tabindex if not previously set.
if (element.getAttribute('tabindex') === null) {
element.setAttribute('tabindex', element.disabled ? '-1' : '0');
}
// Set role if not previously set.
if (!element.getAttribute('role')) {
element.setAttribute('role', 'button');
}
element.classList.add('clickable');
}
}
}

View File

@ -81,6 +81,7 @@
@mixin core-focus-style() {
box-shadow: inset 0 0 var(--a11y-focus-width) 1px var(--a11y-focus-color);
border-radius: var(--border-radius);
// Thicker option:
// border: var(--a11y-focus-width) solid var(--a11y-focus-color);
}

View File

@ -897,12 +897,16 @@ img[core-external-content]:not([src]) {
}
ion-card {
box-shadow: var(--box-shadow);
margin: var(--ion-card-vertical-margin) var(--ion-card-horizontal-margin);
border-width: var(--border-width);
border-style: var(--border-style);
border-color: var(--border-color);
box-shadow: var(--box-shadow);
border-radius: var(--border-radius);
margin: var(--ion-card-vertical-margin) var(--ion-card-horizontal-margin);
&::part(native) {
--border-width: 0;
}
ion-item:only-child {
--inner-border-width: 0px;
@ -1514,19 +1518,24 @@ ion-item.item-input.item-multiple-inputs {
}
// Focus highlight for accessibility.
ion-item.item-input.ion-focused:not(:focus),
.ion-focused,
ion-item.ion-activatable.ion-focused:not(:focus),
.ion-focused:not(.item-multiple-inputs):not(:focus),
ion-input.has-focus,
.ion-focused ion-toggle:focus-within,
.ion-focused ion-select:focus-within,
.ion-focused ion-checkbox:focus-within,
.ion-focused ion-radio:focus-within {
ion-card:focus {
@include core-focus();
}
.ion-focused.item-multiple-inputs,
.ion-focused.ion-activatable {
ion-toggle:focus-within,
ion-select:focus-within,
ion-checkbox:focus-within,
ion-radio:focus-within {
@include core-focus();
}
}
// Treat cases where there's a focusable element inside an item, like a button.
ion-item.ion-focused:not(:focus),
ion-item.item-input:not(.item-multiple-inputs):not(:focus),
ion-item.item-has-focus:not(.item-multiple-inputs):not(:focus),
ion-item.item-input ion-input.has-focus {
position: relative;
&::after {
@ -1576,13 +1585,17 @@ ion-item.item {
outline: none;
}
textarea, button, select, input, a {
textarea, button, select, input, a, .clickable {
&:focus {
@include core-focus-style();
outline: none;
}
}
.ion-focused:not(.item-multiple-inputs):not(:focus) .clickable:focus {
box-shadow: none;
}
ion-loading:focus-visible,
ion-alert:focus-visible,
ion-popover:focus-visible,