Skip to content

Commit 560d25a

Browse files
committed
MOBILE-4595 a11y: Fix storage manager in screen readers
We cannot use ion-accordion because the header is always a button and screen readers do not support having semantic elements inside buttons.
1 parent 7a9eb90 commit 560d25a

File tree

3 files changed

+90
-79
lines changed

3 files changed

+90
-79
lines changed

src/addons/storagemanager/pages/course-storage/course-storage.html

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,21 @@ <h1>{{ 'addon.storagemanager.coursedownloads' | translate }}</h1>
4545
</ion-card-header>
4646
</ion-card>
4747

48-
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" [value]="accordionMultipleValue">
49-
<ng-container *ngFor="let section of sections">
50-
<ng-container *ngTemplateOutlet="sectionCard; context: { section }" />
51-
</ng-container>
52-
</ion-accordion-group>
48+
<ng-container *ngFor="let section of sections">
49+
<ng-container *ngTemplateOutlet="sectionCard; context: { section }" />
50+
</ng-container>
5351

5452
</core-loading>
5553
</ion-content>
5654

5755

5856
<ng-template #sectionCard let-section="section">
5957
<ion-card class="section" *ngIf="section.contents.length > 0" [id]="'addons-course-storage-'+section.id">
60-
<ion-accordion [value]="section.id" toggleIconSlot="start">
61-
<ion-item [detail]="false" slot="header" class="card-header">
58+
<div class="ion-activatable ripple-parent">
59+
<ion-item [detail]="false" class="card-header" (click)="toggleSection(section)" tappable>
6260
<ion-label>
63-
<p class="item-heading ion-text-wrap">
61+
<p class="item-heading ion-text-wrap" [attr.aria-expanded]="section.expanded"
62+
(ariaButtonClick)="toggleSection(section)">
6463
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
6564
[adaptImg]="false" />
6665
</p>
@@ -78,8 +77,8 @@ <h1>{{ 'addon.storagemanager.coursedownloads' | translate }}</h1>
7877
[progress]="section.total === undefined || section.total === 0 ? -1 : (section.count / section.total) * 100" />
7978
</p>
8079
</ion-label>
81-
<div class="storage-buttons" slot="end" *ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
82-
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
80+
<div slot="end" class="storage-buttons" *ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
81+
<div *ngIf="downloadEnabled" class="core-button-spinner">
8382
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus !== statusDownloaded"
8483
[status]="section.downloadStatus" [enabled]="true" (action)="prefetchSection(section)"
8584
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true"
@@ -99,57 +98,62 @@ <h1>{{ 'addon.storagemanager.coursedownloads' | translate }}</h1>
9998
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }" />
10099
</ion-button>
101100
</div>
101+
@if (section.expanded) {
102+
<ion-icon slot="start" size="small" name="fas-chevron-down" aria-hidden="true" />
103+
} @else {
104+
<ion-icon slot="start" size="small" name="fas-chevron-right" aria-hidden="true" />
105+
}
102106
</ion-item>
103-
<ion-card-content slot="content">
104-
<ng-container *ngIf="section.expanded">
105-
<ng-container *ngFor="let modOrSubsection of section.contents">
106-
@if (!isModule(modOrSubsection)) {
107-
<ng-container *ngTemplateOutlet="sectionCard; context: { section: modOrSubsection }" />
108-
} @else {
109-
<ion-item class="core-course-storage-activity"
110-
*ngIf="downloadEnabled || (!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0)">
111-
<core-mod-icon slot="start" *ngIf="modOrSubsection.handlerData.icon"
112-
[modicon]="modOrSubsection.handlerData.icon" [modname]="modOrSubsection.modname"
113-
[componentId]="modOrSubsection.instance" [fallbackTranslation]="modOrSubsection.modplural"
114-
[isBranded]="modOrSubsection.branded" />
115-
<ion-label class="ion-text-wrap">
116-
<p class="item-heading {{modOrSubsection.handlerData!.class}} addon-storagemanager-module-size">
117-
<core-format-text [text]="modOrSubsection.handlerData.title" [courseId]="modOrSubsection.course"
118-
contextLevel="module" [contextInstanceId]="modOrSubsection.id" [adaptImg]="false" />
119-
</p>
120-
<ion-badge [color]="modOrSubsection.downloadStatus === statusDownloaded ? 'success' : 'light'"
121-
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0">
122-
<ion-icon name="fam-cloud-done" *ngIf="modOrSubsection.downloadStatus === statusDownloaded"
123-
[attr.aria-label]="'core.downloaded' | translate" />{{ modOrSubsection.totalSize |
124-
coreBytesToSize }}
125-
</ion-badge>
126-
<ion-badge color="light" *ngIf="modOrSubsection.calculatingSize ||
107+
<ion-ripple-effect />
108+
</div>
109+
<ion-card-content>
110+
<ng-container *ngIf="section.expanded">
111+
<ng-container *ngFor="let modOrSubsection of section.contents">
112+
@if (!isModule(modOrSubsection)) {
113+
<ng-container *ngTemplateOutlet="sectionCard; context: { section: modOrSubsection }" />
114+
} @else {
115+
<ion-item class="core-course-storage-activity"
116+
*ngIf="downloadEnabled || (!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0)">
117+
<core-mod-icon slot="start" *ngIf="modOrSubsection.handlerData.icon" [modicon]="modOrSubsection.handlerData.icon"
118+
[modname]="modOrSubsection.modname" [componentId]="modOrSubsection.instance"
119+
[fallbackTranslation]="modOrSubsection.modplural" [isBranded]="modOrSubsection.branded" />
120+
<ion-label class="ion-text-wrap">
121+
<p class="item-heading {{modOrSubsection.handlerData!.class}} addon-storagemanager-module-size">
122+
<core-format-text [text]="modOrSubsection.handlerData.title" [courseId]="modOrSubsection.course"
123+
contextLevel="module" [contextInstanceId]="modOrSubsection.id" [adaptImg]="false" />
124+
</p>
125+
<ion-badge [color]="modOrSubsection.downloadStatus === statusDownloaded ? 'success' : 'light'"
126+
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0">
127+
<ion-icon name="fam-cloud-done" *ngIf="modOrSubsection.downloadStatus === statusDownloaded"
128+
[attr.aria-label]="'core.downloaded' | translate" />{{ modOrSubsection.totalSize |
129+
coreBytesToSize }}
130+
</ion-badge>
131+
<ion-badge color="light" *ngIf="modOrSubsection.calculatingSize ||
127132
(section.isDownloading && modOrSubsection.downloadStatus === statusDownloaded)">
128-
{{ 'core.calculating' | translate }}
129-
</ion-badge>
130-
</ion-label>
133+
{{ 'core.calculating' | translate }}
134+
</ion-badge>
135+
</ion-label>
131136

132-
<div class="storage-buttons" slot="end">
133-
<core-download-refresh *ngIf="downloadEnabled && modOrSubsection.handlerData?.showDownloadButton &&
137+
<div class="storage-buttons" slot="end">
138+
<core-download-refresh *ngIf="downloadEnabled && modOrSubsection.handlerData?.showDownloadButton &&
134139
modOrSubsection.downloadStatus !== statusDownloaded" [status]="modOrSubsection.downloadStatus" [enabled]="true"
135-
[canTrustDownload]="true" [loading]="modOrSubsection.spinner || modOrSubsection.handlerData.spinner"
136-
(action)="prefetchModule(modOrSubsection)"
137-
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
138-
[statusSubject]="modOrSubsection.name" />
139-
<ion-button fill="clear" (click)="deleteForModule($event, modOrSubsection)"
140-
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0" color="danger">
141-
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
140+
[canTrustDownload]="true" [loading]="modOrSubsection.spinner || modOrSubsection.handlerData.spinner"
141+
(action)="prefetchModule(modOrSubsection)"
142+
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
143+
[statusSubject]="modOrSubsection.name" />
144+
<ion-button fill="clear" (click)="deleteForModule($event, modOrSubsection)"
145+
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0" color="danger">
146+
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
142147
'addon.storagemanager.deletedatafrom' | translate: { name: modOrSubsection.name }" />
143-
</ion-button>
144-
<p *ngIf="!downloadEnabled || !modOrSubsection.handlerData?.showDownloadButton" class="sr-only">
145-
{{ 'core.notdownloadable' | translate }}
146-
</p>
147-
</div>
148-
</ion-item>
149-
}
150-
</ng-container>
148+
</ion-button>
149+
<p *ngIf="!downloadEnabled || !modOrSubsection.handlerData?.showDownloadButton" class="sr-only">
150+
{{ 'core.notdownloadable' | translate }}
151+
</p>
152+
</div>
153+
</ion-item>
154+
}
151155
</ng-container>
152-
</ion-card-content>
153-
</ion-accordion>
156+
</ng-container>
157+
</ion-card-content>
154158
</ion-card>
155159
</ng-template>

src/addons/storagemanager/pages/course-storage/course-storage.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414

1515
.item-heading {
1616
font: var(--mdl-typography-heading4-font);
17+
min-height: auto;
18+
}
19+
20+
ion-icon[slot=start] {
21+
margin-inline-end: var(--mdl-spacing-2);
22+
background-color: var(--gray-100);
23+
border-radius: 50%;
24+
padding: var(--mdl-spacing-1);
1725
}
1826
}
1927

@@ -42,6 +50,13 @@
4250
}
4351
}
4452
}
53+
54+
.ripple-parent {
55+
position: relative;
56+
ion-ripple-effect {
57+
z-index: 1;
58+
}
59+
}
4560
}
4661

4762
ion-badge {

src/addons/storagemanager/pages/course-storage/course-storage.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
CoreCourseModulePrefetchDelegate,
2828
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
2929
import { CoreCourses } from '@features/courses/services/courses';
30-
import { AccordionGroupChangeEventDetail } from '@ionic/angular';
3130
import { CoreLoadings } from '@services/overlays/loadings';
3231
import { CoreNavigator } from '@services/navigator';
3332
import { CoreSites } from '@services/sites';
@@ -57,7 +56,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
5756
sections: AddonStorageManagerCourseSection[] = [];
5857
totalSize = 0;
5958
calculatingSize = true;
60-
accordionMultipleValue: string[] = [];
6159

6260
downloadEnabled = false;
6361
downloadCourseEnabled = false;
@@ -126,23 +124,29 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
126124

127125
this.loaded = true;
128126

127+
let prioritizedSectionId: number | undefined;
128+
129129
if (initialSectionId !== undefined && initialSectionId > 0) {
130-
this.accordionMultipleValue.push(initialSectionId.toString());
131-
this.accordionGroupChange();
130+
CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
131+
if (section.id === initialSectionId) {
132+
section.expanded = true;
133+
prioritizedSectionId = section.id;
134+
}
135+
});
132136

133137
CoreDom.scrollToElement(
134138
this.elementRef.nativeElement,
135139
`#addons-course-storage-${initialSectionId}`,
136140
{ addYAxis: -10 },
137141
);
138142
} else {
139-
this.accordionMultipleValue.push(this.sections[0].id.toString());
140-
this.accordionGroupChange();
143+
this.sections[0].expanded = true;
144+
prioritizedSectionId = this.sections[0].id;
141145
}
142146

143147
try {
144148
await Promise.all([
145-
this.updateSizes(this.sections, Number(this.accordionMultipleValue[0])),
149+
this.updateSizes(this.sections, prioritizedSectionId),
146150
this.initCoursePrefetch(),
147151
this.initModulePrefetch(),
148152
]);
@@ -764,24 +768,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
764768
}
765769

766770
/**
767-
* Toggle expand status.
771+
* Toggle section expand status.
768772
*
769-
* @param event Event object. If not defined, use the current value.
773+
* @param section Section.
770774
*/
771-
accordionGroupChange(event?: AccordionGroupChangeEventDetail): void {
772-
const sectionIds = event?.value as string[] ?? this.accordionMultipleValue;
773-
const allSections = CoreCourseHelper.flattenSections(this.sections);
774-
allSections.forEach((section) => {
775-
section.expanded = false;
776-
});
777-
778-
sectionIds.forEach((sectionId) => {
779-
const section = allSections.find((section) => section.id === Number(sectionId));
780-
781-
if (section) {
782-
section.expanded = true;
783-
}
784-
});
775+
toggleSection(section: AddonStorageManagerCourseSection): void {
776+
section.expanded = !section.expanded;
785777
}
786778

787779
/**

0 commit comments

Comments
 (0)