diff --git a/config.xml b/config.xml index a6c5369ec0e..cf8858bbddf 100644 --- a/config.xml +++ b/config.xml @@ -30,9 +30,10 @@ - + + diff --git a/cordova-plugin-moodleapp/plugin.xml b/cordova-plugin-moodleapp/plugin.xml index bbce2501cd8..eee9de23c2d 100644 --- a/cordova-plugin-moodleapp/plugin.xml +++ b/cordova-plugin-moodleapp/plugin.xml @@ -25,6 +25,10 @@ + + + + @@ -37,6 +41,7 @@ + diff --git a/cordova-plugin-moodleapp/src/android/PinchToZoom.java b/cordova-plugin-moodleapp/src/android/PinchToZoom.java new file mode 100644 index 00000000000..38eab6af8bd --- /dev/null +++ b/cordova-plugin-moodleapp/src/android/PinchToZoom.java @@ -0,0 +1,42 @@ +// (C) Copyright 2025 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. + +package com.moodle.moodlemobile; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; + +import android.util.Log; +import android.webkit.WebSettings; +import android.webkit.WebSettings.ZoomDensity; +import android.webkit.WebView; + +public class PinchToZoom extends CordovaPlugin { + + public static final String TAG = "PinchToZoom"; + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + Log.d(TAG, "Initializing pinch-to-zoom"); + + super.initialize(cordova, webView); + + WebSettings settings = ((WebView) webView.getView()).getSettings(); + settings.setBuiltInZoomControls(true); + settings.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM); + settings.setDisplayZoomControls(false); + settings.setSupportZoom(true); + } +} diff --git a/package.json b/package.json index c883fad1d6d..8de2866e17b 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,10 @@ "cordova-plugin-local-notification": { "ANDROID_SUPPORT_V4_VERSION": "26.+" }, - "cordova-plugin-moodleapp": {}, + "cordova-plugin-moodleapp": { + "ANDROIDX_VERSION": "1.0.0", + "ANDROIDX_APPCOMPAT_VERSION": "1.3.1" + }, "cordova-plugin-network-information": {}, "cordova-plugin-prevent-override": {}, "cordova-plugin-screen-orientation": {}, @@ -238,4 +241,4 @@ "nl.kingsquare.cordova.background-audio": {} } } -} +} \ No newline at end of file diff --git a/patches/@ionic+core+8.4.1.patch b/patches/@ionic+core+8.4.1.patch index dae8830b13a..a79030a1560 100644 --- a/patches/@ionic+core+8.4.1.patch +++ b/patches/@ionic+core+8.4.1.patch @@ -152,3 +152,48 @@ index c3d2d8e..bc40d4f 100644 const root = getElementRoot(baseEl); const contentEl = root.querySelector('.popover-content'); const referenceSizeEl = trigger || ((_a = ev === null || ev === void 0 ? void 0 : ev.detail) === null || _a === void 0 ? void 0 : _a.ionShadowTarget) || (ev === null || ev === void 0 ? void 0 : ev.target); +diff --git a/node_modules/@ionic/core/dist/esm/input-shims-0314bbe5.js b/node_modules/@ionic/core/dist/esm/input-shims-0314bbe5.js +index dd9d410..846146f 100644 +--- a/node_modules/@ionic/core/dist/esm/input-shims-0314bbe5.js ++++ b/node_modules/@ionic/core/dist/esm/input-shims-0314bbe5.js +@@ -338,7 +338,8 @@ const enableScrollAssist = (componentEl, inputEl, contentEl, footerEl, keyboardH + const focusOut = () => { + hasKeyboardBeenPresentedForTextField = false; + win === null || win === void 0 ? void 0 : win.removeEventListener('ionKeyboardDidShow', keyboardShow); +- componentEl.removeEventListener('focusout', focusOut); ++ // Patched: Attach focusin/focusout events to inputEl instead of componentEl to allow focusing buttons inside . ++ inputEl.removeEventListener('focusout', focusOut); + }; + /** + * When the input is about to receive +@@ -358,13 +358,15 @@ const enableScrollAssist = (componentEl, inputEl, contentEl, footerEl, keyboardH + } + jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight, addScrollPadding, disableClonedInput, platformHeight); + win === null || win === void 0 ? void 0 : win.addEventListener('ionKeyboardDidShow', keyboardShow); +- componentEl.addEventListener('focusout', focusOut); ++ // Patched: Attach focusin/focusout events to inputEl instead of componentEl to allow focusing buttons inside . ++ inputEl.addEventListener('focusout', focusOut); + }; +- componentEl.addEventListener('focusin', focusIn); ++ // Patched: Attach focusin/focusout events to inputEl instead of componentEl to allow focusing buttons inside . ++ inputEl.addEventListener('focusin', focusIn); + return () => { +- componentEl.removeEventListener('focusin', focusIn); ++ inputEl.removeEventListener('focusin', focusIn); + win === null || win === void 0 ? void 0 : win.removeEventListener('ionKeyboardDidShow', keyboardShow); +- componentEl.removeEventListener('focusout', focusOut); ++ inputEl.removeEventListener('focusout', focusOut); + }; + }; + /** +--- a/node_modules/@ionic/core/dist/esm/ion-item_8.entry.js ++++ b/node_modules/@ionic/core/dist/esm/ion-item_8.entry.js +@@ -109,7 +109,7 @@ const Item = class { + // inputs, then those need to individually get each click + hasCover() { + const inputs = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio'); +- return inputs.length === 1 && !this.multipleInputs; ++ return inputs.length === 1; + } + // If the item has an href or button property it will render a native + // anchor or button that is clickable diff --git a/scripts/langindex.json b/scripts/langindex.json index 66eb705688e..4a31ebf4178 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1743,6 +1743,7 @@ "core.decsep": "langconfig", "core.defaultvalue": "tool_usertours", "core.delete": "moodle", + "core.deleted": "moodle", "core.deletedoffline": "local_moodlemobileapp", "core.deleteduser": "bulkusers", "core.deleting": "local_moodlemobileapp", @@ -2165,6 +2166,7 @@ "core.login.login": "moodle", "core.login.loginbutton": "local_moodlemobileapp", "core.login.loginsteps": "moodle", + "core.login.logoof": "moodle", "core.login.missingemail": "moodle", "core.login.missingfirstname": "moodle", "core.login.missinglastname": "moodle", @@ -2450,6 +2452,7 @@ "core.selectall": "moodle", "core.send": "message", "core.sending": "chat", + "core.sent": "moodle", "core.serverconnection": "local_moodlemobileapp", "core.settings.about": "local_moodlemobileapp", "core.settings.accessstatement": "access", @@ -2487,6 +2490,7 @@ "core.settings.enableanalytics": "local_moodlemobileapp", "core.settings.enableanalyticsdescription": "local_moodlemobileapp", "core.settings.enabledownloadsection": "local_moodlemobileapp", + "core.settings.enablepinchtozoom": "local_moodlemobileapp", "core.settings.enablerichtexteditor": "local_moodlemobileapp", "core.settings.enablerichtexteditordescription": "local_moodlemobileapp", "core.settings.encryptedpushsupported": "local_moodlemobileapp", diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index b7a34cbaf3f..5d0a88dccb3 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -96,12 +96,12 @@

{{ 'addon.block_myoverview.pluginname' | translate }}

-

+

{{'addon.block_myoverview.noresult' | translate}} -

-

+

+

{{'addon.block_myoverview.nocoursesenrolled' | translate}} -

+

{{'addon.block_myoverview.noresultdescription' | translate}} diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.scss b/src/addons/block/myoverview/components/myoverview/myoverview.scss index 50bb3c00af5..abfe545be56 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.scss +++ b/src/addons/block/myoverview/components/myoverview/myoverview.scss @@ -33,6 +33,7 @@ core-empty-box { .item-heading { + font-size: 1rem; font-weight: bold; margin-bottom: 0; } diff --git a/src/addons/block/timeline/components/events/addon-block-timeline-events.html b/src/addons/block/timeline/components/events/addon-block-timeline-events.html index e9a6c41ec28..1f55dd71a34 100644 --- a/src/addons/block/timeline/components/events/addon-block-timeline-events.html +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -9,7 +9,14 @@

-

{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedaydate" }}

+ @if (course) { +

+ } @else { +

+ } + + {{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedaydate" }} +
diff --git a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png index 368bf2370e7..5d58f7e7609 100644 Binary files a/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png and b/src/addons/messages/tests/behat/snapshots/test-basic-usage-of-messages-in-app-view-recent-conversations-and-contacts_22.png differ diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 620db067f7b..df3ce64059c 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -84,70 +84,75 @@

{{ 'addon.mod_forum.errorloadingsortingorderdetails' | translate }}

- - -

- - - - -

-
- -
- {{discussion.userfullname}} -

- - -

-

- {{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}} -

-

-

+
+ + +

+ + + + + + +

+
+ +
+ {{discussion.userfullname}} +

+ + +

+

+ {{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}} +

+

+

+
-
- - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
+ diff --git a/src/addons/mod/forum/components/index/index.scss b/src/addons/mod/forum/components/index/index.scss index e764855371f..bd16c583394 100644 --- a/src/addons/mod/forum/components/index/index.scss +++ b/src/addons/mod/forum/components/index/index.scss @@ -70,4 +70,10 @@ border-top: 1px solid var(--spacer-color); } + .ripple-parent { + position: relative; + ion-ripple-effect { + z-index: 1; + } + } } diff --git a/src/addons/mod/forum/components/post/post.html b/src/addons/mod/forum/components/post/post.html index 1a35dd9eb53..01cb92d3c2b 100644 --- a/src/addons/mod/forum/components/post/post.html +++ b/src/addons/mod/forum/components/post/post.html @@ -111,20 +111,19 @@

- - -
- -
+ + + + +

{{ 'addon.mod_forum.advanced' | translate }}

+
+
+
+ +
+
+
diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index c1ac9d34aeb..5d10ebe19ce 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -60,6 +60,7 @@ import { CoreWSFile } from '@services/ws'; import { CorePromiseUtils } from '@singletons/promise-utils'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreAlerts } from '@services/overlays/alerts'; +import { AccordionGroupCustomEvent } from '@ionic/angular'; /** * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). @@ -232,7 +233,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges // Show advanced fields if any of them has not the default value. this.advanced = this.formData.files.length > 0; - if (!isEditing || !postId || postId <= 0) { this.preparePostData = undefined; } @@ -620,10 +620,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges } /** - * Show or hide advanced form fields. + * Function called when advanced accordion is toggled. */ - toggleAdvanced(): void { - this.advanced = !this.advanced; + onAdvancedChanged(event: AccordionGroupCustomEvent): void { + this.advanced = event.detail.value === 'advanced'; } /** diff --git a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png index 24d1bc043d0..8b048339f05 100644 Binary files a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png and b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png differ diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts index 0c977fad8f4..3eaadd32529 100644 --- a/src/addons/mod/quiz/pages/player/player.ts +++ b/src/addons/mod/quiz/pages/player/player.ts @@ -174,6 +174,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { try { await this.processAttempt(false, false); + + modal.dismissWithStatus('core.sent', true); } catch (error) { // Save attempt failed. Show confirmation. modal.dismiss(); @@ -181,8 +183,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { await CoreAlerts.confirm(Translate.instant('addon.mod_quiz.confirmleavequizonerror')); CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); - } finally { - modal.dismiss(); } return true; @@ -252,10 +252,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { }, 50); } } + + modal.dismissWithStatus('core.sent', true); } catch (error) { - CoreAlerts.showError(error, { default: 'Error performing action.' }); - } finally { modal?.dismiss(); + CoreAlerts.showError(error, { default: 'Error performing action.' }); } } @@ -301,7 +302,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { try { await this.processAttempt(false, false); - modal.dismiss(); + modal.dismissWithStatus('core.sent', true); } catch (error) { CoreAlerts.showError(error, { default: Translate.instant('addon.mod_quiz.errorsaveattempt') }); modal.dismiss(); @@ -468,7 +469,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { await this.refreshAttempt(); await this.loadSummary(); } + + modal.dismissWithStatus('core.sent', true); } catch (error) { + modal?.dismiss(); // eslint-disable-next-line promise/catch-or-return CoreAlerts .showError(error, { default: Translate.instant('addon.mod_quiz.errorsaveattempt') }) @@ -481,8 +485,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { return; }); - } finally { - modal?.dismiss(); } } diff --git a/src/addons/privatefiles/components/file/file.html b/src/addons/privatefiles/components/file/file.html index a21f68e8028..367ea8a0f5d 100644 --- a/src/addons/privatefiles/components/file/file.html +++ b/src/addons/privatefiles/components/file/file.html @@ -1,7 +1,7 @@ - + @if (file) { - + @if (showCheckbox) { @@ -13,7 +13,7 @@

- {{fileName}} + {{fileName}} @if (state === statusDownloaded) { + } diff --git a/src/addons/privatefiles/components/file/file.scss b/src/addons/privatefiles/components/file/file.scss index 5fed5bdbfce..7d5e9632784 100644 --- a/src/addons/privatefiles/components/file/file.scss +++ b/src/addons/privatefiles/components/file/file.scss @@ -11,4 +11,8 @@ --inner-border-width: 0 !important; } + + ion-ripple-effect { + z-index: 1; + } } diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html index eb06b774445..50c6f6ec366 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.html +++ b/src/addons/storagemanager/pages/course-storage/course-storage.html @@ -45,11 +45,9 @@

{{ 'addon.storagemanager.coursedownloads' | translate }}

- - - - - + + + @@ -57,10 +55,11 @@

{{ 'addon.storagemanager.coursedownloads' | translate }}

- - +
+ -

+

@@ -78,8 +77,8 @@

{{ 'addon.storagemanager.coursedownloads' | translate }}

[progress]="section.total === undefined || section.total === 0 ? -1 : (section.count / section.total) * 100" />

-
-
+
+
{{ 'addon.storagemanager.coursedownloads' | translate }}

[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }" />
+ @if (section.expanded) { +
- - - - @if (!isModule(modOrSubsection)) { - - } @else { - - - -

- -

- - {{ modOrSubsection.totalSize | - coreBytesToSize }} - - + + @if (!isModule(modOrSubsection)) { + + } @else { + + + +

+ +

+ + {{ modOrSubsection.totalSize | + coreBytesToSize }} + + - {{ 'core.calculating' | translate }} - -
+ {{ 'core.calculating' | translate }} +
+
-
- + - - + + - -

- {{ 'core.notdownloadable' | translate }} -

-
-
- } -
+ +

+ {{ 'core.notdownloadable' | translate }} +

+ + + }
-
- +
+ diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.scss b/src/addons/storagemanager/pages/course-storage/course-storage.scss index 49255af47e8..607ef2688b6 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.scss +++ b/src/addons/storagemanager/pages/course-storage/course-storage.scss @@ -14,6 +14,14 @@ .item-heading { font: var(--mdl-typography-heading4-font); + min-height: auto; + } + + ion-icon[slot=start] { + margin-inline-end: var(--mdl-spacing-2); + background-color: var(--gray-100); + border-radius: 50%; + padding: var(--mdl-spacing-1); } } @@ -42,6 +50,13 @@ } } } + + .ripple-parent { + position: relative; + ion-ripple-effect { + z-index: 1; + } + } } ion-badge { diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts index 93b65a1b27a..eef520df554 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -27,10 +27,10 @@ import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate'; import { CoreCourses } from '@features/courses/services/courses'; -import { AccordionGroupChangeEventDetail } from '@ionic/angular'; import { CoreLoadings } from '@services/overlays/loadings'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; +import { CoreToasts } from '@services/overlays/toasts'; import { Translate } from '@singletons'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -56,7 +56,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { sections: AddonStorageManagerCourseSection[] = []; totalSize = 0; calculatingSize = true; - accordionMultipleValue: string[] = []; downloadEnabled = false; downloadCourseEnabled = false; @@ -95,7 +94,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { */ async ngOnInit(): Promise { try { - this.courseId = CoreNavigator.getRequiredRouteParam('courseId'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); } catch (error) { CoreAlerts.showError(error); @@ -125,9 +124,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { this.loaded = true; + let prioritizedSectionId: number | undefined; + if (initialSectionId !== undefined && initialSectionId > 0) { - this.accordionMultipleValue.push(initialSectionId.toString()); - this.accordionGroupChange(); + CoreCourseHelper.flattenSections(this.sections).forEach((section) => { + if (section.id === initialSectionId) { + section.expanded = true; + prioritizedSectionId = section.id; + } + }); CoreDom.scrollToElement( this.elementRef.nativeElement, @@ -135,13 +140,13 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { { addYAxis: -10 }, ); } else { - this.accordionMultipleValue.push(this.sections[0].id.toString()); - this.accordionGroupChange(); + this.sections[0].expanded = true; + prioritizedSectionId = this.sections[0].id; } try { await Promise.all([ - this.updateSizes(this.sections, Number(this.accordionMultipleValue[0])), + this.updateSizes(this.sections, prioritizedSectionId), this.initCoursePrefetch(), this.initModulePrefetch(), ]); @@ -393,7 +398,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { const modules = CoreCourse.getSectionsModules(this.sections) .filter((module) => module.totalSize && module.totalSize > 0); - await this.deleteModules(modules); + const success = await this.deleteModules(modules); + + if (success) { + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.deleted', + translateMessage: true, + }); + } } /** @@ -420,7 +433,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { const modules = CoreCourse.getSectionsModules([section]).filter((module) => module.totalSize && module.totalSize > 0); - await this.deleteModules(modules); + const success = await this.deleteModules(modules); + + if (success) { + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.deleted', + translateMessage: true, + }); + } } /** @@ -450,15 +471,24 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { return; } - await this.deleteModules([module]); + const success = await this.deleteModules([module]); + + if (success) { + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.deleted', + translateMessage: true, + }); + } } /** * Deletes the specified modules, showing the loading overlay while it happens. * * @param modules Modules to delete + * @returns True if modules are deleted with no errors. */ - protected async deleteModules(modules: AddonStorageManagerModule[]): Promise { + protected async deleteModules(modules: AddonStorageManagerModule[]): Promise { const modal = await CoreLoadings.show('core.deleting', true); const sections = new Set(); @@ -477,8 +507,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { try { await Promise.all(promises); + + return true; } catch (error) { CoreAlerts.showError(error, { default: Translate.instant('core.errordeletefile') }); + + return false; } finally { modal.dismiss(); @@ -507,12 +541,23 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { async prefetchSection(section: AddonStorageManagerCourseSection): Promise { section.isCalculating = true; this.changeDetectorRef.markForCheck(); + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloading', + translateMessage: true, + }); + try { await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, [section]); try { await CoreCourseHelper.prefetchSections([section], this.courseId); + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloaded', + translateMessage: true, + }); } catch (error) { if (!this.isDestroyed) { CoreAlerts.showError(error, { default: Translate.instant('core.course.errordownloadingsection') }); @@ -551,12 +596,23 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { // Show spinner since this operation might take a while. module.spinner = true; + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloading', + translateMessage: true, + }); try { // Get download size to ask for confirm if it's high. const size = await module.prefetchHandler.getDownloadSize(module, module.course, true); await CoreCourseHelper.prefetchModule(module.prefetchHandler, module, size, module.course, refresh); + + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloaded', + translateMessage: true, + }); } catch (error) { if (!this.isDestroyed) { CoreAlerts.showError(error, { default: Translate.instant('core.errordownloading') }); @@ -645,6 +701,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { event.stopPropagation(); event.preventDefault(); + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloading', + translateMessage: true, + }); + const course = await CoreCourseHelper.getCourseInfo(this.courseId); if (!course) { CoreAlerts.showError(Translate.instant('core.course.errordownloadingcourse')); @@ -663,6 +725,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { }, ); + CoreToasts.show({ + cssClass: 'sr-only', + message: 'core.downloaded', + translateMessage: true, + }); + await this.updateSizes(this.sections); } catch (error) { if (this.isDestroyed) { @@ -700,24 +768,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } /** - * Toggle expand status. + * Toggle section expand status. * - * @param event Event object. If not defined, use the current value. + * @param section Section. */ - accordionGroupChange(event?: AccordionGroupChangeEventDetail): void { - const sectionIds = event?.value as string[] ?? this.accordionMultipleValue; - const allSections = CoreCourseHelper.flattenSections(this.sections); - allSections.forEach((section) => { - section.expanded = false; - }); - - sectionIds.forEach((sectionId) => { - const section = allSections.find((section) => section.id === Number(sectionId)); - - if (section) { - section.expanded = true; - } - }); + toggleSection(section: AddonStorageManagerCourseSection): void { + section.expanded = !section.expanded; } /** diff --git a/src/core/classes/ion-loading.ts b/src/core/classes/ion-loading.ts index ff6b56cef31..a9961c85577 100644 --- a/src/core/classes/ion-loading.ts +++ b/src/core/classes/ion-loading.ts @@ -14,6 +14,7 @@ import { CoreWait } from '@singletons/wait'; import { LoadingController } from '@singletons'; +import { CoreToasts } from '@services/overlays/toasts'; /** * Dismiss listener. @@ -68,6 +69,23 @@ export class CoreIonLoadingElement { this.listeners.forEach(listener => listener()); } + /** + * Dismiss the loading element and present a status message for screen readers. + * + * @param text Status message for screen readers. + * @param needsTranslate Whether the 'text' needs to be translated. + */ + async dismissWithStatus(text: string, needsTranslate?: boolean): Promise { + await Promise.all([ + this.dismiss(), + CoreToasts.show({ + cssClass: 'sr-only', + message: text, + translateMessage: needsTranslate, + }), + ]); + } + /** * Register dismiss listener. * diff --git a/src/core/components/attachments/attachments.ts b/src/core/components/attachments/attachments.ts index d1627a28768..4a58d7e3b32 100644 --- a/src/core/components/attachments/attachments.ts +++ b/src/core/components/attachments/attachments.ts @@ -26,6 +26,8 @@ import { CoreCourses } from '@features/courses/services/courses'; import { CorePromiseUtils } from '@singletons/promise-utils'; import { toBoolean } from '@/core/transforms/boolean'; import { CoreAlerts } from '@services/overlays/alerts'; +import { CoreToasts } from '@services/overlays/toasts'; +import { CoreWSFile } from '@services/ws'; /** * Component to render attachments, allow adding more and delete the current ones. @@ -166,6 +168,18 @@ export class CoreAttachmentsComponent implements OnInit { } } + // Status message for screen readers. + const file = this.files[index]; + if (file) { + const filename = (file as CoreWSFile).filename ?? (file as FileEntry).name; + if (filename) { + CoreToasts.show({ + cssClass: 'sr-only', + message: Translate.instant('core.filedeletedsuccessfully', { filename }), + }); + } + } + // Remove the file from the list. this.files?.splice(index, 1); } diff --git a/src/core/components/combobox/combobox.scss b/src/core/components/combobox/combobox.scss index dec9eec500a..9cca6c1db9c 100644 --- a/src/core/components/combobox/combobox.scss +++ b/src/core/components/combobox/combobox.scss @@ -109,12 +109,18 @@ --background: rgba(var(--ion-text-color-rgb), var(--background-hover-opacity)); } - &:focus, - &:focus-visible, - &.ion-focused { + &:focus:not(.ion-focused), + &:focus-visible:not(.ion-focused) { --background: rgba(var(--ion-text-color-rgb), var(--background-focused-opacity)); } + &.ion-focused { + border-color: var(--ion-background-color); + box-shadow: inset 0 0 0 calc(2px - var(--core-combobox-border-widthh, 0px)) var(--ion-background-color); + outline: var(--a11y-shadow-focus-outline); + z-index: 1; + } + &.ion-activated { --background: rgba(var(--ion-text-color-rgb), var(--background-activated-opacity)); } diff --git a/src/core/components/file/core-file.html b/src/core/components/file/core-file.html index e053a2fbaa9..5d7661c154d 100644 --- a/src/core/components/file/core-file.html +++ b/src/core/components/file/core-file.html @@ -1,10 +1,12 @@ - - + + -

{{fileName}}

+

+ {{fileName}} +

{{ fileSizeReadable }} ยท @@ -25,4 +27,5 @@ + diff --git a/src/core/components/file/core-file.scss b/src/core/components/file/core-file.scss new file mode 100644 index 00000000000..211f65a7801 --- /dev/null +++ b/src/core/components/file/core-file.scss @@ -0,0 +1,5 @@ +:host { + ion-ripple-effect { + z-index: 1; + } +} diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index 949dc20d7ec..e2d6a78c743 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -38,6 +38,7 @@ import { Translate } from '@singletons'; @Component({ selector: 'core-file', templateUrl: 'core-file.html', + styleUrl: 'core-file.scss', }) export class CoreFileComponent implements OnInit, OnDestroy { diff --git a/src/core/components/loading/core-loading.html b/src/core/components/loading/core-loading.html index cb7dc77d435..7ba101bbd3e 100644 --- a/src/core/components/loading/core-loading.html +++ b/src/core/components/loading/core-loading.html @@ -1,5 +1,5 @@

-
diff --git a/src/core/components/local-file/core-local-file.html b/src/core/components/local-file/core-local-file.html index eb5953383b2..878d8bd43a5 100644 --- a/src/core/components/local-file/core-local-file.html +++ b/src/core/components/local-file/core-local-file.html @@ -1,13 +1,15 @@
- - + + -

{{fileName}}

+

+ {{fileName}} +

{{ size }} @@ -45,5 +47,6 @@ + diff --git a/src/core/components/local-file/core-local-file.scss b/src/core/components/local-file/core-local-file.scss new file mode 100644 index 00000000000..211f65a7801 --- /dev/null +++ b/src/core/components/local-file/core-local-file.scss @@ -0,0 +1,5 @@ +:host { + ion-ripple-effect { + z-index: 1; + } +} diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts index afce965af18..30f8b88443c 100644 --- a/src/core/components/local-file/local-file.ts +++ b/src/core/components/local-file/local-file.ts @@ -41,6 +41,7 @@ import { CoreAlerts } from '@services/overlays/alerts'; @Component({ selector: 'core-local-file', templateUrl: 'core-local-file.html', + styleUrl: 'core-local-file.scss', }) export class CoreLocalFileComponent implements OnInit { diff --git a/src/core/components/site-logo/site-logo.html b/src/core/components/site-logo/site-logo.html index c4fff46bead..445eb9c2bb3 100644 --- a/src/core/components/site-logo/site-logo.html +++ b/src/core/components/site-logo/site-logo.html @@ -1,7 +1,7 @@

- - + +

diff --git a/src/core/components/site-logo/site-logo.ts b/src/core/components/site-logo/site-logo.ts index ae88652c8ce..e38f7e9d30b 100644 --- a/src/core/components/site-logo/site-logo.ts +++ b/src/core/components/site-logo/site-logo.ts @@ -20,6 +20,7 @@ import { CoreSite } from '@classes/sites/site'; import { toBoolean } from '@/core/transforms/boolean'; import { CorePromiseUtils } from '@singletons/promise-utils'; import { CoreUnauthenticatedSite } from '@classes/sites/unauthenticated-site'; +import { CoreConstants } from '@/core/constants'; /** * Component to render the current site logo. @@ -46,6 +47,7 @@ export class CoreSiteLogoComponent implements OnInit, OnDestroy { logoLoaded = false; fallbackLogo = ''; showSiteName = true; + appName = CoreConstants.CONFIG.appname; protected updateSiteObserver?: CoreEventObserver; diff --git a/src/core/components/tabs/tabs.scss b/src/core/components/tabs/tabs.scss index 5e9b48cb4b2..ab661b6a2bd 100644 --- a/src/core/components/tabs/tabs.scss +++ b/src/core/components/tabs/tabs.scss @@ -50,7 +50,7 @@ flex-grow: 1; swiper-slide { - border-bottom: 2px solid transparent; + border-bottom: 4px solid transparent; min-width: 100px; height: var(--height); cursor: pointer; diff --git a/src/core/constants.ts b/src/core/constants.ts index f14df0ecf28..64c0065948a 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -142,6 +142,7 @@ export class CoreConstants { static readonly SETTINGS_COLOR_SCHEME = 'CoreSettingsColorScheme'; static readonly SETTINGS_ANALYTICS_ENABLED = 'CoreSettingsAnalyticsEnabled'; static readonly SETTINGS_DONT_SHOW_EXTERNAL_LINK_WARN = 'CoreSettingsDontShowExtLinkWarn'; + static readonly SETTINGS_PINCH_TO_ZOOM = 'CoreSettingsPinchToZoom'; // WS constants. static readonly WS_TIMEOUT = 30000; // Timeout when not in WiFi. diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index d58f14a1d04..bbc01b2c13a 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -58,6 +58,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { protected pageDidEnterListener?: EventListener; protected keyUpListener?: EventListener; protected page?: HTMLElement; + protected moduleNav: HTMLElement | null = null; constructor(el: ElementRef, protected ionContent: IonContent) { this.element = el.nativeElement; @@ -104,10 +105,11 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { // Set a minimum height value. this.initialHeight = this.element.getBoundingClientRect().height || this.initialHeight; - const moduleNav = this.element.querySelector('core-course-module-navigation'); - if (moduleNav) { + this.moduleNav = this.element.tagName === 'CORE-COURSE-MODULE-NAVIGATION' ? + this.element : this.element.querySelector('core-course-module-navigation'); + if (this.moduleNav && this.moduleNav !== this.element) { this.element.classList.add('has-module-nav'); - this.finalHeight = this.initialHeight - (moduleNav.getBoundingClientRect().height); + this.finalHeight = this.initialHeight - this.moduleNav.getBoundingClientRect().height; } this.previousHeight = this.initialHeight; @@ -196,6 +198,11 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { document.activeElement.scrollIntoView({ block: 'center' }); } }); + + // Show footer when it is focused, + this.moduleNav?.addEventListener('focusin', () => { + this.setBarHeight(this.initialHeight); + }); } /** @@ -213,7 +220,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { */ protected onScroll(scrollDetail: ScrollDetail, scrollElement: HTMLElement): void { const maxScroll = scrollElement.scrollHeight - scrollElement.offsetHeight; - if (scrollDetail.scrollTop <= 0 || (this.appearOnBottom && scrollDetail.scrollTop >= maxScroll)) { + const footerHasFocus = this.moduleNav?.contains(document.activeElement); + if (scrollDetail.scrollTop <= 0 || (this.appearOnBottom && scrollDetail.scrollTop >= maxScroll) || footerHasFocus) { // Reset. this.setBarHeight(this.initialHeight); } else { diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 6638b96501a..c08b17b94b5 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -483,6 +483,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { component: CoreCourseCourseIndexComponent, initialBreakpoint: 1, breakpoints: [0, 1], + handle: false, componentProps: { course: this.course, sections: this.sections, diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html index 14b611e81ab..a9ef8818b2f 100644 --- a/src/core/features/course/components/course-index/course-index.html +++ b/src/core/features/course/components/course-index/course-index.html @@ -33,27 +33,33 @@

- - - +
+ + + + +
diff --git a/src/core/features/course/components/course-index/course-index.scss b/src/core/features/course/components/course-index/course-index.scss index 0ee1282631d..2d8d934d878 100644 --- a/src/core/features/course/components/course-index/course-index.scss +++ b/src/core/features/course/components/course-index/course-index.scss @@ -20,6 +20,9 @@ ion-item.item { --background: var(--primary-tint); --color: var(--gray-900); border: 0px; + ion-label h2 { + font-weight: bold; + } } &.item-hightlighted { @@ -84,10 +87,22 @@ ion-item.item { &[role=button] { min-height: auto; min-width: auto; + @include core-focus-outline(); } } + + &::part(native) { + @include core-focus-inset-outline(); + } } div.core-course-index-subsection { @include padding-horizontal(16px, null); } + +.ripple-parent { + position: relative; + ion-ripple-effect { + z-index: 1; + } +} diff --git a/src/core/features/course/components/module-completion/module-completion.scss b/src/core/features/course/components/module-completion/module-completion.scss index 27c7e8302a1..2ebc5b6e001 100644 --- a/src/core/features/course/components/module-completion/module-completion.scss +++ b/src/core/features/course/components/module-completion/module-completion.scss @@ -11,6 +11,11 @@ color: var(--ion-color-shade); } + ion-button.button-solid.ion-color-success.ion-focused::part(native) { + background: var(--ion-color); + color: var(--ion-color-contrast); + } + ion-button.button-outline::part(native){ border-color: var(--gray-400); } diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 52a8bdf074d..51da2cb313d 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -1,7 +1,8 @@ @@ -12,8 +13,18 @@ [isBranded]="module.branded" />

- + @if (module.handlerData.action && module.uservisible) { + + + + } @else { + + } + + +

+ + @if (module.handlerData.action && module.uservisible) { + + }
diff --git a/src/core/features/course/pages/course-summary/course-summary.html b/src/core/features/course/pages/course-summary/course-summary.html index 089c9328172..fdd1b7bbacc 100644 --- a/src/core/features/course/pages/course-summary/course-summary.html +++ b/src/core/features/course/pages/course-summary/course-summary.html @@ -1,6 +1,6 @@ - + @@ -84,7 +84,7 @@

+ [courseId]="isEnrolled ? course.id : null" [detail]="true"> diff --git a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_47.png b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_47.png index 769b0bb7a86..26c54b22e96 100644 Binary files a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_47.png and b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_47.png differ diff --git a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_51.png b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_51.png index 65791fe88a5..1cae765db73 100644 Binary files a/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_51.png and b/src/core/features/course/tests/behat/snapshots/test-basic-usage-of-one-course-in-app-view-course-contents_51.png differ diff --git a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html index 26103bc0ecd..30cd813e6a3 100644 --- a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html +++ b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html @@ -1,31 +1,12 @@ - + tappable [attr.aria-label]="course.displayname || course.fullname">
- -
- -
- -
- - - - - - -
-
- @@ -47,8 +28,10 @@ {{ 'core.courses.aria:favourite' | translate }} - {{ 'core.courses.aria:coursename' | translate }} - + + {{ 'core.courses.aria:coursename' | translate }} + + + +
+ +
+ +
+ + + + + + +
+
+ +
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.scss b/src/core/features/courses/components/course-list-item/course-list-item.scss index 0f8ab669eab..ad59c55b6a1 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.scss +++ b/src/core/features/courses/components/course-list-item/course-list-item.scss @@ -45,6 +45,10 @@ button { ion-card { --border-radius: var(--card-radius); + + ion-ripple-effect { + z-index: 1; + } } .core-button-spinner { diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html index d8478dd39a1..1e6f429fd37 100644 --- a/src/core/features/courses/pages/list/list.html +++ b/src/core/features/courses/pages/list/list.html @@ -32,7 +32,7 @@

{{ 'core.courses.mycourses' | translate }}

-

{{ 'core.courses.totalcoursesearchresults' | translate:{$a: searchTotal} }}

+

{{ 'core.courses.totalcoursesearchresults' | translate:{$a: searchTotal} }}

diff --git a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 33f446b6d18..85e722582a0 100644 --- a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -8,7 +8,7 @@ [placeholder]="placeholder" [aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="focusRTE($event)" (ionBlur)="blurRTE($event)" /> -
+
@@ -99,8 +99,8 @@ - diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss index 5930d15d259..59f85d1fefc 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.scss @@ -22,12 +22,11 @@ display: flex; flex-direction: column; background: var(--background); - border: 1px solid var(--stroke); + border: 2px solid var(--stroke); border-radius: var(--mdl-shape-borderRadius-md); &.has-focus { - border-color: var(--a11y-focus-color); - border-width: 2px; + border-color: var(--dark); outline: none !important; } @@ -58,7 +57,7 @@ } .core-rte-editor, .core-textarea { - padding: 2px; + padding: 8px; margin: 2px; width: calc(100% - 4px); resize: none; diff --git a/src/core/features/login/components/site-onboarding/site-onboarding.html b/src/core/features/login/components/site-onboarding/site-onboarding.html index 794b87d5637..fbcc6821c4e 100644 --- a/src/core/features/login/components/site-onboarding/site-onboarding.html +++ b/src/core/features/login/components/site-onboarding/site-onboarding.html @@ -16,7 +16,8 @@

diff --git a/src/core/features/login/components/site-onboarding/site-onboarding.ts b/src/core/features/login/components/site-onboarding/site-onboarding.ts index 3bf0f10c3af..4ddb2fad29d 100644 --- a/src/core/features/login/components/site-onboarding/site-onboarding.ts +++ b/src/core/features/login/components/site-onboarding/site-onboarding.ts @@ -19,6 +19,7 @@ import { CoreOpener } from '@singletons/opener'; import { GET_STARTED_URL, ONBOARDING_DONE } from '@features/login/constants'; import { ModalController } from '@singletons'; import { CoreSharedModule } from '@/core/shared.module'; +import { CoreConstants } from '@/core/constants'; /** * Component that displays onboarding help regarding the CoreLoginSitePage. @@ -35,6 +36,7 @@ import { CoreSharedModule } from '@/core/shared.module'; export class CoreLoginSiteOnboardingComponent { step = 0; + appName = CoreConstants.CONFIG.appname; /** * Go to next step. diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index b84f1bd5322..4b4f8627c56 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -67,6 +67,7 @@ "login": "Log in", "loginbutton": "Log in", "loginsteps": "For full access to this site, you first need to create an account.", + "logoof": "Logo of {{$a}}", "missingemail": "Missing email address", "missingfirstname": "Missing given name", "missinglastname": "Missing last name", diff --git a/src/core/features/login/pages/site/site.html b/src/core/features/login/pages/site/site.html index 725d059e3ae..b5d70f22032 100644 --- a/src/core/features/login/pages/site/site.html +++ b/src/core/features/login/pages/site/site.html @@ -18,7 +18,8 @@

{{ 'core.login.connecttomoodle' | translate }}

diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index c2f813b2631..4e10b49e678 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -77,6 +77,7 @@ export class CoreLoginSitePage implements OnInit { showScanQR!: boolean; enteredSiteUrl?: CoreLoginSiteInfoExtended; siteFinderSettings!: CoreLoginSiteFinderSettings; + appName = CoreConstants.CONFIG.appname; constructor(protected formBuilder: FormBuilder) {} diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png index 5fa063d00e8..0da83659835 100644 Binary files a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png and b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png differ diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png index 6281d2e667b..06f34ae104e 100644 Binary files a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png and b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png differ diff --git a/src/core/features/mainmenu/components/user-menu/user-menu.html b/src/core/features/mainmenu/components/user-menu/user-menu.html index 327c83baca0..0dd36cd49f2 100644 --- a/src/core/features/mainmenu/components/user-menu/user-menu.html +++ b/src/core/features/mainmenu/components/user-menu/user-menu.html @@ -33,7 +33,7 @@

+ [detail]="true">

{{ siteInfo.fullname }}

diff --git a/src/core/features/mainmenu/pages/menu/menu.scss b/src/core/features/mainmenu/pages/menu/menu.scss index 60328562aeb..786fd93a1ed 100644 --- a/src/core/features/mainmenu/pages/menu/menu.scss +++ b/src/core/features/mainmenu/pages/menu/menu.scss @@ -56,7 +56,10 @@ core-user-menu-button { } ion-tab-button { + color: var(--ion-text-color-step-400); + &.tab-selected { + color: var(--dark); background: var(--background-selected); } @@ -93,6 +96,13 @@ ion-tab-button { ion-tabs.placement-bottom { ion-tab-button { + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + + &.tab-selected { + border-top-color: var(--core-tab-border-color-active); + } + ion-icon.core-tab-icon { margin-bottom: var(--network-margin-bottom); transition: margin 500ms ease-in-out, transform 300ms ease-in-out; @@ -138,6 +148,13 @@ ion-tabs.placement-side { ion-tab-button { --padding-start: 0; --padding-end: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + + &.tab-selected { + border-left-color: var(--core-tab-border-color-active); + } + ion-badge.core-tab-badge { @include position(null, 1px, null, auto); } @@ -146,7 +163,7 @@ ion-tabs.placement-side { ion-tab-button, core-user-menu-button { width: 100%; - min-height: var(--menutabbar-size); + min-height: calc(var(--menutabbar-size) - 8px); flex: 0; ion-icon.core-tab-badge, diff --git a/src/core/features/search/components/search-box/core-search-box.html b/src/core/features/search/components/search-box/core-search-box.html index c61f3235956..60c215d1bb3 100644 --- a/src/core/features/search/components/search-box/core-search-box.html +++ b/src/core/features/search/components/search-box/core-search-box.html @@ -1,7 +1,7 @@ - + + (ionFocus)="focus()"> @@ -10,13 +10,13 @@ - + {{ 'core.search.err_minlength' | translate : {'$a': {'format': lengthCheck} } }} - - + + + +

{{ 'core.settings.enablepinchtozoom' | translate }}

+
+
{ + return Boolean(await CoreConfig.get(CoreConstants.SETTINGS_PINCH_TO_ZOOM, 0)); + } + /** * Init Settings related to DOM. */ async initDomSettings(): Promise { // Set the font size based on user preference. const zoomLevel = await this.getZoomLevel(); + const pinchToZoom = await this.getPinchToZoom(); this.applyZoomLevel(zoomLevel); + this.applyPinchToZoom(pinchToZoom); this.initColorScheme(); } @@ -377,6 +388,30 @@ export class CoreSettingsHelperProvider { document.documentElement.style.setProperty('--zoom-level', zoom + '%'); } + /** + * Enable or disable pinch-to-zoom. + * + * @param pinchToZoom True if pinch-to-zoom should be enabled. + */ + applyPinchToZoom(pinchToZoom: boolean): void { + const element = document.head.querySelector('meta[name=viewport]'); + if (!element) { + return; + } + const content = element.getAttribute('content'); + if (!content) { + return; + } + + element.setAttribute('content', content.replace(/maximum-scale=\d\.\d/, `maximum-scale=${pinchToZoom ? '4.0' : '1.0'}`)); + + // Force layout reflow. + document.body.style.width = '99.9999%'; + setTimeout(() => { + document.body.style.width = ''; + }); + } + /** * Get system allowed color schemes. * diff --git a/src/core/lang.json b/src/core/lang.json index c0857841d86..883d0015fef 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -77,6 +77,7 @@ "decsep": ".", "defaultvalue": "Default ({{$a}})", "delete": "Delete", + "deleted": "Deleted", "deletedoffline": "Deleted offline", "deleteduser": "Deleted user", "deleting": "Deleting", @@ -297,6 +298,7 @@ "selectall": "Select all", "send": "Send", "sending": "Sending", + "sent": "Sent", "serverconnection": "Error connecting to the server: {{details}}", "show": "Show", "showadvanced": "Show advanced", diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index f7cad246761..5f59d43017c 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -580,7 +580,16 @@ export class CoreDom { ): void { const enabled = () => !CoreUtils.isTrueOrOne(element.dataset.disabledA11yClicks ?? 'false'); - element.addEventListener('click', (event) => enabled() && callback(event)); + element.addEventListener('click', (event) => { + if (!enabled()) { + return; + } + + callback(event); + + event.preventDefault(); + event.stopPropagation(); + }); element.addEventListener('keydown', (event) => { if (!enabled()) { diff --git a/src/core/tests/behat/snapshots/it-navigates-properly-in-pages-with-a-split-view-component-navigate-in-grades-tab-on-tablet_15.png b/src/core/tests/behat/snapshots/it-navigates-properly-in-pages-with-a-split-view-component-navigate-in-grades-tab-on-tablet_15.png index ecb044ce1cc..358a0c3f1cd 100644 Binary files a/src/core/tests/behat/snapshots/it-navigates-properly-in-pages-with-a-split-view-component-navigate-in-grades-tab-on-tablet_15.png and b/src/core/tests/behat/snapshots/it-navigates-properly-in-pages-with-a-split-view-component-navigate-in-grades-tab-on-tablet_15.png differ diff --git a/src/index.html b/src/index.html index 864f4b89acd..2d7cee8ec70 100644 --- a/src/index.html +++ b/src/index.html @@ -9,7 +9,7 @@ + content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes, interactive-widget=resizes-content" /> diff --git a/src/theme/components/ion-action-sheet.scss b/src/theme/components/ion-action-sheet.scss index 7dfa2c2710d..e69268e578b 100644 --- a/src/theme/components/ion-action-sheet.scss +++ b/src/theme/components/ion-action-sheet.scss @@ -2,6 +2,8 @@ ion-action-sheet { --button-color: var(--ion-text-color); --button-color-selected: var(--ion-text-color); + @include core-focus-inset-outline(); + .action-sheet-title { --color: var(--ion-text-color); font-weight: bold; @@ -47,3 +49,10 @@ ion-action-sheet { z-index: 100; cursor: pointer; } + +// We need [class] to increase the specificity. See: https://ionicframework.com/docs/api/action-sheet#theming +.action-sheet-button[class] { + &, &.ion-focused { + color: var(--text-color); + } +} diff --git a/src/theme/components/ion-alert.scss b/src/theme/components/ion-alert.scss index 031fdce597a..4576b119c25 100644 --- a/src/theme/components/ion-alert.scss +++ b/src/theme/components/ion-alert.scss @@ -20,13 +20,21 @@ ion-alert { overflow: auto; border-radius: var(--border-radius) !important; + button.alert-button { + // By default, Ionic uses the primary color, which does not meet AA accessibility. + color: var(--text-color); + } + button.alert-button.alert-button-role-destructive { color: var(--danger); } } - .alert-message { + // We need [class] to increase the specificity. See: https://ionicframework.com/docs/api/alert#customization + .alert-message[class] { + // By default, Ionic uses a lighter text color for Android, which does not meet AA accessibility. + color: var(--color); user-select: text; flex-shrink: 0; diff --git a/src/theme/components/ion-button.scss b/src/theme/components/ion-button.scss index 0a22be6cbbc..4cb9af093c0 100644 --- a/src/theme/components/ion-button.scss +++ b/src/theme/components/ion-button.scss @@ -102,6 +102,18 @@ ion-button { border-left: 5px solid transparent; } } + + &.ion-focused { + overflow: visible; + } + + &.ion-focused::part(native) { + --background-focused-opacity: 0; + border-color: var(--ion-background-color); + box-shadow: inset 0 0 0 calc(2px - var(--border-width, 0px)) var(--ion-background-color); + outline: var(--a11y-shadow-focus-outline); + z-index: 1; + } } ion-button, diff --git a/src/theme/components/ion-fab.scss b/src/theme/components/ion-fab.scss index 9adfb69c37d..07c20800caa 100644 --- a/src/theme/components/ion-fab.scss +++ b/src/theme/components/ion-fab.scss @@ -13,4 +13,12 @@ ion-content.has-collapsible-footer ion-fab { ion-fab-button { --box-shadow: 0 3px 5px -1px rgb(0 0 0 / 20%), 0 6px 10px 0 rgb(0 0 0 / 14%), 0 1px 18px 0 rgb(0 0 0 / 12%); + + &.ion-focused::part(native) { + --background-focused-opacity: 0; + border-color: var(--ion-background-color); + box-shadow: inset 0 0 0 1px var(--ion-background-color); + outline: var(--a11y-shadow-focus-outline); + z-index: 1; + } } diff --git a/src/theme/components/ion-input.scss b/src/theme/components/ion-input.scss index 3db67517891..f67d72f31f7 100644 --- a/src/theme/components/ion-input.scss +++ b/src/theme/components/ion-input.scss @@ -8,6 +8,15 @@ ion-input { } } +// We need [class] to increase the specificity, See: https://ionicframework.com/docs/api/input#css-custom-properties +ion-input[class] { + --highlight-color: var(--dark); + + &.has-focus .label-text { + font-weight: bold; + } +} + input[disabled], input[readonly] { opacity: var(--mdl-input-disabled-opacity); diff --git a/src/theme/components/ion-item.scss b/src/theme/components/ion-item.scss index b4fca69e709..a04ff694422 100644 --- a/src/theme/components/ion-item.scss +++ b/src/theme/components/ion-item.scss @@ -24,19 +24,6 @@ ion-item.item { &.item-lines-default:not(.item-has-interactive-control) { --inner-border-width: 0 0 1px 0; } - - &.ion-touched { - &.ion-invalid { - --ion-item-border-color: var(--highlight-color-invalid); - --highlight-background: var(--ion-item-border-color); - --border-color: var(--ion-item-border-color); - } - &.ion-valid { - --ion-item-border-color: var(--highlight-color-valid); - --highlight-background: var(--ion-item-border-color); - --border-color: var(--ion-item-border-color); - } - } } // Hide details on items to align badges. @@ -44,13 +31,28 @@ ion-item.item { --detail-icon-opacity: 0; } - &.item-has-interactive-control:focus-within { - @include core-focus-outline(); + &.item-has-interactive-control:focus-within, + &[button]::part(native) { + @include core-focus-inset-outline(); + } + + &:has(ion-radio:focus-visible)::part(native)::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + @include core-focus-inset-outline-internal(); } &.item-has-interactive-control.item-interactive-disabled { pointer-events: none; } + + &.item-radio-checked { + font-weight: bold; + } } // Fake item. diff --git a/src/theme/components/ion-label.scss b/src/theme/components/ion-label.scss new file mode 100644 index 00000000000..049eb0d52ed --- /dev/null +++ b/src/theme/components/ion-label.scss @@ -0,0 +1,5 @@ +@import "../globals.scss"; + +ion-item:has(core-rich-text-editor.has-focus) > ion-label { + font-weight: bold; +} diff --git a/src/theme/components/ion-tabs.scss b/src/theme/components/ion-tabs.scss new file mode 100644 index 00000000000..a2ec3003f86 --- /dev/null +++ b/src/theme/components/ion-tabs.scss @@ -0,0 +1,9 @@ +@import "../globals.scss"; + +ion-tab-button.ion-focused::part(native) { + @include core-focus-inset-outline-internal(); +} + +ion-tab-bar > ion-button.ion-focused { + @include core-focus-inset-outline-internal(); +} diff --git a/src/theme/components/ionic.scss b/src/theme/components/ionic.scss index 656c91d85fd..488ac41cd03 100644 --- a/src/theme/components/ionic.scss +++ b/src/theme/components/ionic.scss @@ -16,14 +16,16 @@ @import "ion-input.scss"; @import "ion-item.scss"; @import "ion-item-divider.scss"; -@import "ion-modal.scss"; +@import "ion-label.scss"; @import "ion-loading.scss"; +@import "ion-modal.scss"; @import "ion-note.scss"; @import "ion-popover.scss"; @import "ion-radio.scss"; @import "ion-searchbar.scss"; @import "ion-select.scss"; @import "ion-spinner.scss"; +@import "ion-tabs.scss"; @import "ion-toast.scss"; @import "swiper.scss"; diff --git a/src/theme/helpers/custom.mixins.scss b/src/theme/helpers/custom.mixins.scss index d641faaf91d..600dc798e93 100644 --- a/src/theme/helpers/custom.mixins.scss +++ b/src/theme/helpers/custom.mixins.scss @@ -89,6 +89,17 @@ } } +@mixin core-focus-inset-outline() { + &:focus-visible { + @include core-focus-inset-outline-internal(); + } + @supports not selector(:focus-visible) { + @at-root:focus { + @include core-focus-inset-outline-internal(); + } + } +} + @mixin core-focus-background() { &:focus-visible { @include core-focus-background-internal(); @@ -117,11 +128,30 @@ } @mixin core-focus-outline-internal() { - // box-shadow: var(--a11y-shadow-focus-boxShadow); - // border-radius: var(--border-radius); outline: var(--a11y-shadow-focus-outline); + // Use primary for text input controls. + &:is( + input:not([type]), + input[type^=date], + input[type=email], + input[type=month], + input[type=number], + input[type=password], + input[type=search], + input[type=tel], + input[type=text], + input[type=time], + input[type=url], + input[type=week] + ) { + outline-color: var(--primary); + } } +@mixin core-focus-inset-outline-internal() { + @include core-focus-outline-internal(); + outline-offset: calc(var(--a11y-shadow-focus-borderWidth) * -1); +} @mixin core-focus-background-internal() { --background-focused: var(--background-focused, var(--a11y-background-focus-background)); diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index bbf97167fdb..d97b6a34589 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -347,9 +347,10 @@ td { @include core-focus-outline(); } -ion-input, -ion-input input, -ion-textarea, +// We need [class] to increase the specificity, See: https://ionicframework.com/docs/api/input#css-custom-properties +ion-input[class], +ion-textarea[class], +ion-searchbar[class], core-rich-text-editor { --placeholder-color: var(--ion-placeholder-color); --placeholder-opacity: var(--mdl-placeholder-opacity); diff --git a/src/theme/theme.design-system.scss b/src/theme/theme.design-system.scss index f9d37c0157e..652ccd17414 100644 --- a/src/theme/theme.design-system.scss +++ b/src/theme/theme.design-system.scss @@ -146,7 +146,7 @@ --mdl-button-disabled-opacity: 0.6; --mdl-input-disabled-opacity: 0.6; --mdl-item-disabled-opacity: 0.4; - --mdl-placeholder-opacity: 0.6; + --mdl-placeholder-opacity: 0.8; // ***** ACCESSIBILITY ***** // --a11y-sizing-minTargetSize: 44px; @@ -182,8 +182,8 @@ --ion-card-radius: var(--mdl-shape-borderRadius-lg); --ion-card-border-width: 1px; - --bottom-tabs-size: 48px; - --side-tabs-size: 56px; + --bottom-tabs-size: 56px; + --side-tabs-size: 64px; --core-header-toolbar-button-image-size: var(--a11y-sizing-minTargetSize); --core-header-toolbar-border-width: 0px; @@ -199,7 +199,7 @@ --core-combobox-border-width: var(--core-input-border-width); --core-combobox-box-shadow: none; - --core-tab-font-weight-active: var(--mdl-typography-label-fontWeight); + --core-tab-font-weight-active: bold; --core-tabs-height: 48px; --core-progressbar-height: 8px; diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index ed1eb2abf52..199ebc275a1 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -70,7 +70,7 @@ } // Accessibility vars. - --a11y-shadow-focus-boxShadowColor: var(--primary); + --a11y-shadow-focus-boxShadowColor: #{$blue}; --text-color: #{$text-color}; --background-color: #{$background-color}; @@ -138,7 +138,7 @@ --core-tab-color-active: var(--dark); --core-tab-border-color-active: var(--primary); - --core-loading-spinner: var(--primary); + --core-loading-spinner: var(--dark); --core-progressbar-color: var(--primary); --core-progressbar-text-color: var(--medium); @@ -168,13 +168,13 @@ --core-login-input-background: var(--white); --core-login-input-color: var(--gray-900); - --core-star-color: var(--primary); + --core-star-color: var(--text-color); --core-navigation-background: var(--contrast-background); --core-collapsible-footer-background: var(--contrast-background); - --addon-calendar-today-border-color: var(--primary); + --addon-calendar-today-border-color: var(--dark); --addon-calendar-border-color: var(--stroke); --core-messages-message-bg: var(--white);