diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts
index b9f3c1b7c..1b4bcc553 100644
--- a/src/core/components/mod-icon/mod-icon.ts
+++ b/src/core/components/mod-icon/mod-icon.ts
@@ -55,7 +55,7 @@ export class CoreModIconComponent implements OnInit, OnChanges {
@HostBinding('attr.role')
get getRole(): string | null {
- return !this.showAlt ? 'presentation' : null;
+ return this.showAlt ? 'img' : 'presentation';
}
@HostBinding('attr.aria-label')
diff --git a/src/core/components/swipe-slides/swipe-slides.html b/src/core/components/swipe-slides/swipe-slides.html
index bcfc05ac8..aeeff91a2 100644
--- a/src/core/components/swipe-slides/swipe-slides.html
+++ b/src/core/components/swipe-slides/swipe-slides.html
@@ -1,5 +1,6 @@
-
-
+
+
diff --git a/src/core/components/swipe-slides/swipe-slides.ts b/src/core/components/swipe-slides/swipe-slides.ts
index fa3864c38..49690e299 100644
--- a/src/core/components/swipe-slides/swipe-slides.ts
+++ b/src/core/components/swipe-slides/swipe-slides.ts
@@ -72,7 +72,7 @@ export class CoreSwipeSlidesComponent- implements OnChanges, OnDe
protected hostElement: HTMLElement;
protected unsubscribe?: () => void;
protected resizeListener: CoreEventObserver;
- protected activeSlideIndexes: number[] = [];
+ protected activeSlideIndex?: number;
protected onReadyPromise = new CorePromisedValue();
constructor(
@@ -112,7 +112,7 @@ export class CoreSwipeSlidesComponent
- implements OnChanges, OnDe
* @returns Whether the slide is active.
*/
isActive(index: number): boolean {
- return this.activeSlideIndexes.includes(index);
+ return this.activeSlideIndex === index;
}
/**
@@ -153,7 +153,7 @@ export class CoreSwipeSlidesComponent
- implements OnChanges, OnDe
item: items[initialIndex],
};
- this.activeSlideIndexes = [initialIndex];
+ this.activeSlideIndex = initialIndex;
this.manager.setSelectedItem(items[initialIndex]);
this.onWillChange.emit(initialItemData);
@@ -268,7 +268,7 @@ export class CoreSwipeSlidesComponent
- implements OnChanges, OnDe
return;
}
- this.activeSlideIndexes.push(currentItemData.index);
+ this.activeSlideIndex = undefined;
this.manager?.setSelectedItem(currentItemData.item);
this.onWillChange.emit(currentItemData);
@@ -283,12 +283,12 @@ export class CoreSwipeSlidesComponent
- implements OnChanges, OnDe
async slideDidChange(): Promise {
const currentItemData = await this.getCurrentSlideItemData();
if (!currentItemData) {
- this.activeSlideIndexes = [];
+ this.activeSlideIndex = undefined;
return;
}
- this.activeSlideIndexes = [currentItemData.index];
+ this.activeSlideIndex = currentItemData.index;
this.onDidChange.emit(currentItemData);
@@ -348,7 +348,7 @@ export class CoreSwipeSlidesComponent
- implements OnChanges, OnDe
return;
}
- this.swiper?.update();
+ this.swiper.update();
// We need to ensure the slides are updated before continuing.
await CoreUtils.nextTicks(2);
diff --git a/src/core/components/tabs-outlet/core-tabs-outlet.html b/src/core/components/tabs-outlet/core-tabs-outlet.html
index 1819ef577..f1af5a514 100644
--- a/src/core/components/tabs-outlet/core-tabs-outlet.html
+++ b/src/core/components/tabs-outlet/core-tabs-outlet.html
@@ -8,8 +8,8 @@
-
+
this.performAction(event));
+ async ngOnInit(): Promise {
+ let hasNativeButton = false;
+ if ('componentOnReady' in this.element) {
+ await this.element.componentOnReady();
+
+ // Native buttons may be already accessible and does not neet to set TabIndex and role.
+ hasNativeButton = !!this.element.shadowRoot?.querySelector('.button-native');
+ }
+
+ CoreDom.initializeClickableElementA11y(this.element, (event) => this.performAction(event), !hasNativeButton);
}
/**
@@ -79,7 +87,7 @@ export class CoreLinkDirective implements OnInit {
href = href || this.element.getAttribute('href') || this.element.getAttribute('xlink:href');
- if (!href || CoreUrlUtils.getUrlScheme(href) == 'javascript') {
+ if (!href || CoreUrlUtils.getUrlScheme(href) === 'javascript') {
return;
}
diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html
index 8a837aa6a..423cf7031 100644
--- a/src/core/features/course/components/course-format/course-format.html
+++ b/src/core/features/course/components/course-format/course-format.html
@@ -54,12 +54,10 @@
- {{'core.course.communicationroomlink' | translate }}
- {{'core.course.courseindex' | translate }}
diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss
index d46bf5a78..a57402960 100644
--- a/src/core/features/course/components/module/module.scss
+++ b/src/core/features/course/components/module/module.scss
@@ -126,7 +126,7 @@
}
.activity-extrabadges {
- color: var(--gray-700);
+ color: var(--medium);
}
.activity-description-availabilityinfo {
diff --git a/src/core/features/courses/pages/list/list.html b/src/core/features/courses/pages/list/list.html
index 47b406bc2..b896132fb 100644
--- a/src/core/features/courses/pages/list/list.html
+++ b/src/core/features/courses/pages/list/list.html
@@ -26,9 +26,9 @@
+ searchArea="CoreCoursesSearch" [lengthCheck]="1" />
-
+
0">
@@ -45,7 +45,7 @@
+ [message]="'core.courses.nosearchresults' | translate" role="alert" />
diff --git a/src/core/features/courses/pages/list/list.ts b/src/core/features/courses/pages/list/list.ts
index 0a3d460ec..1993496cb 100644
--- a/src/core/features/courses/pages/list/list.ts
+++ b/src/core/features/courses/pages/list/list.ts
@@ -49,6 +49,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
coursesLoaded = 0;
canLoadMore = false;
loadMoreError = false;
+ loadingMessage = Translate.instant('core.loading');
showOnlyEnrolled = false;
@@ -176,6 +177,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
* @returns Promise resolved when done.
*/
protected async loadCourses(clearTheList = false): Promise {
+ this.loadingMessage = Translate.instant('core.loading');
+
this.loadMoreError = false;
try {
@@ -249,9 +252,10 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
this.searchTotal = 0;
this.logSearch = CoreTime.once(() => this.performLogSearch());
- const modal = await CoreDomUtils.showModalLoading('core.searching', true);
+ this.loaded = false;
await this.searchCourses().finally(() => {
- modal.dismiss();
+ this.loaded = true;
+
});
}
@@ -310,6 +314,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
*/
protected async searchCourses(): Promise {
this.loadMoreError = false;
+ this.loadingMessage = Translate.instant('core.searching');
try {
const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled);
diff --git a/src/core/features/courses/pages/my/my.scss b/src/core/features/courses/pages/my/my.scss
index e073b4273..df09390a0 100644
--- a/src/core/features/courses/pages/my/my.scss
+++ b/src/core/features/courses/pages/my/my.scss
@@ -11,6 +11,5 @@
core-block ::ng-deep ion-card.addon-block-myoverview {
--border-width: 0;
- --background: transparent;
margin: 0;
}
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 029b97c3d..6b476e077 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
@@ -1,8 +1,7 @@
+ (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
Promise;
- protected selectionChangeFunction?: () => void;
+ protected selectionChangeFunction = (): void => this.updateToolbarStyles();
protected languageChangedSubscription?: Subscription;
protected resizeListener?: CoreEventObserver;
protected domPromise?: CoreCancellablePromise;
@@ -226,6 +227,15 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
}
ionItem.classList.add('item-rte');
+ if (this.editorElement) {
+ const debounceMutation = CoreUtils.debounce(() => {
+ this.onChange();
+ }, 20);
+
+ this.contentObserver = new MutationObserver(debounceMutation);
+ this.contentObserver.observe(this.editorElement, { childList: true, subtree: true, characterData: true });
+ }
+
const label = ionItem.querySelector('ion-label');
if (!label) {
@@ -253,7 +263,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
protected setListeners(): void {
// Listen for changes on the control to update the editor (if it is updated from outside of this component).
this.valueChangeSubscription = this.control?.valueChanges.subscribe((newValue) => {
- if (this.draftWasRestored && this.originalContent == newValue) {
+ if (this.draftWasRestored && this.originalContent === newValue) {
// A draft was restored and the content hasn't changed in the site. Use the draft value instead of this one.
this.control?.setValue(this.lastDraft, { emitEvent: false });
@@ -282,7 +292,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.windowResized();
}, 50);
- document.addEventListener('selectionchange', this.selectionChangeFunction = () => this.updateToolbarStyles());
+ document.addEventListener('selectionchange', this.selectionChangeFunction);
this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => {
// Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon.
@@ -304,8 +314,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param event Event
*/
onKeyDown(event: KeyboardEvent): void {
- this.onChange();
-
const shortcutId = this.getShortcutId(event);
const commands = this.getShortcutCommands();
const command = commands[shortcutId];
@@ -364,7 +372,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Get first children with content, not fixed.
let scrollContentHeight = 0;
- while (scrollContentHeight == 0 && content?.children) {
+ while (scrollContentHeight === 0 && content?.children) {
const children = Array.from(content.children)
.filter((element) => element.slot !== 'fixed' && !element.classList.contains('core-loading-container'));
@@ -489,7 +497,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param event The event.
*/
async toggleEditor(event: Event): Promise {
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -581,7 +589,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @returns If value is null only a white space.
*/
protected isNullOrWhiteSpace(value: string | null | undefined): boolean {
- if (value == null || value === undefined) {
+ if (value === null || value === undefined) {
return true;
}
@@ -602,10 +610,17 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
}
if (this.isNullOrWhiteSpace(value)) {
- this.editorElement.innerHTML = '';
+ // Avoid loops.
+ if (this.editorElement.innerHTML !== '') {
+ this.editorElement.innerHTML = '';
+ }
this.textarea.value = '';
} else {
- this.editorElement.innerHTML = value || '';
+ value = value || '';
+ // Avoid loops.
+ if (this.editorElement.innerHTML !== value) {
+ this.editorElement.innerHTML = value;
+ }
this.textarea.value = value;
this.treatExternalContent();
}
@@ -637,7 +652,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* toolbar styles button when set.
*/
buttonAction(event: Event, command: string, parameters?: string): void {
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -659,7 +674,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param command.parameters Command parameters.
*/
protected executeCommand({ name: command, parameters }: EditorCommand): void {
- if (parameters == 'block') {
+ if (parameters === 'block') {
// eslint-disable-next-line deprecation/deprecation
document.execCommand('formatBlock', false, '<' + command + '>');
@@ -676,7 +691,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Modern browsers are using non a11y tags, so replace them.
if (command === 'bold') {
this.replaceTags(['b'], ['strong']);
- } else if (command == 'italic') {
+ } else if (command === 'italic') {
this.replaceTags(['i'], ['em']);
}
}
@@ -715,14 +730,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param event Event.
* @param force If true it will not check the target of the event.
*/
- hideToolbar(event: Event, force = false): void {
+ hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, force = false): void {
if (!force && event.target && this.element.contains(event.target as HTMLElement)) {
// Do not hide if clicked inside the editor area, except forced.
return;
}
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -748,7 +763,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
/**
* Show the toolbar.
*/
- showToolbar(event: Event): void {
+ showToolbar(event: FocusEvent): void {
this.updateToolbarButtons();
this.element.classList.add('ion-touched');
@@ -779,14 +794,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param event Event.
*/
downAction(event: Event): void {
- if (event.type == 'keydown' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keydown' && !this.isValidKeyboardKey(event)) {
return;
}
const selection = window.getSelection()?.toString();
// When RTE is focused with a whole paragraph in desktop the stopBubble will not fire click.
- if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection == '') {
+ if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection === '') {
this.stopBubble(event);
}
}
@@ -795,7 +810,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* Method that shows the next toolbar buttons.
*/
async toolbarNext(event: Event): Promise {
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* Method that shows the previous toolbar buttons.
*/
async toolbarPrev(event: Event): Promise {
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* Update the number of toolbar buttons displayed.
*/
async updateToolbarButtons(): Promise {
- if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent == null) {
+ if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) {
// Don't calculate if component isn't in current view, the calculations are wrong.
return;
}
@@ -879,15 +894,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
*/
updateToolbarStyles(): void {
const node = window.getSelection()?.focusNode;
- if (!node) {
+
+ if (!node || !this.element.contains(node)) {
return;
}
- let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement;
+ let element = node.nodeType === 1 ? node as HTMLElement : node.parentElement;
+
const styles = {};
- while (element != null && element !== this.editorElement) {
+ while (element !== null && element !== this.editorElement) {
const tagName = element.tagName.toLowerCase();
+
if (this.toolbarStyles[tagName]) {
styles[tagName] = 'true';
}
@@ -906,7 +924,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
/**
* Check if should auto save drafts.
*
- * @returns {boolean} Whether it should auto save drafts.
+ * @returns Whether it should auto save drafts.
*/
protected shouldAutoSaveDrafts(): boolean {
return !!CoreSites.getCurrentSite() &&
@@ -943,8 +961,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
let draftText = entry.drafttext || '';
// Revert untouched editor contents to an empty string.
- if (draftText == '' || draftText == '
' || draftText == '
' ||
- draftText == '
' || draftText == '
') {
+ if (draftText === '' || draftText === '
' || draftText === '
' ||
+ draftText === '
' || draftText === '
') {
draftText = '';
}
@@ -977,7 +995,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
const newText = this.control.value ?? '';
- if (this.lastDraft == newText) {
+ if (this.lastDraft === newText) {
// Text hasn't changed, nothing to save.
return;
}
@@ -996,7 +1014,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Draft saved, notify the user.
this.lastDraft = newText;
this.showMessage('core.editor.autosavesucceeded', this.SAVE_MESSAGE_CLEAR_TIME);
- } catch (error) {
+ } catch {
// Error saving draft.
}
}, this.DRAFT_AUTOSAVE_FREQUENCY);
@@ -1009,7 +1027,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
this.resetObserver = CoreEvents.on(CoreEvents.FORM_ACTION, async (data: CoreEventFormActionData) => {
const form = this.element.closest('form');
- if (data.form && form && data.form == form) {
+ if (data.form && form && data.form === form) {
try {
await CoreEditorOffline.deleteDraft(
this.contextLevel || ContextLevel.SYSTEM,
@@ -1048,7 +1066,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @returns Promise resolved when done.
*/
async scanQR(event: Event): Promise {
- if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) {
+ if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) {
return;
}
@@ -1097,14 +1115,20 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
ngOnDestroy(): void {
this.valueChangeSubscription?.unsubscribe();
this.languageChangedSubscription?.unsubscribe();
- this.selectionChangeFunction && document.removeEventListener('selectionchange', this.selectionChangeFunction);
+
+ document.removeEventListener('selectionchange', this.selectionChangeFunction);
+
clearInterval(this.initHeightInterval);
clearInterval(this.autoSaveInterval);
clearTimeout(this.hideMessageTimeout);
+
this.resetObserver?.off();
this.keyboardObserver?.off();
- this.labelObserver?.disconnect();
this.resizeListener?.off();
+
+ this.labelObserver?.disconnect();
+ this.contentObserver?.disconnect();
+
this.domPromise?.cancel();
this.buttonsDomPromise?.cancel();
}
diff --git a/src/core/features/enrol/services/enrol-helper.ts b/src/core/features/enrol/services/enrol-helper.ts
index 54d3dd03b..72ef71821 100644
--- a/src/core/features/enrol/services/enrol-helper.ts
+++ b/src/core/features/enrol/services/enrol-helper.ts
@@ -17,6 +17,7 @@ import { makeSingleton } from '@singletons';
import { CoreEnrolAction, CoreEnrolDelegate, CoreEnrolInfoIcon } from './enrol-delegate';
import { CoreUtils } from '@services/utils/utils';
import { CoreEnrol, CoreEnrolEnrolmentMethod } from './enrol';
+import { CoreArray } from '@singletons/array';
/**
* Service that provides helper functions for enrolment plugins.
@@ -32,7 +33,7 @@ export class CoreEnrolHelperService {
* @returns Enrolment icons to show.
*/
async getEnrolmentIcons(methodTypes: string[], courseId: number): Promise {
- methodTypes = CoreUtils.uniqueArray(methodTypes);
+ methodTypes = CoreArray.unique(methodTypes);
let enrolmentIcons: CoreEnrolInfoIcon[] = [];
let addBrowserOption = false;
diff --git a/src/core/features/fileuploader/services/handlers/album.ts b/src/core/features/fileuploader/services/handlers/album.ts
index e61ddf6de..6f457a7a1 100644
--- a/src/core/features/fileuploader/services/handlers/album.ts
+++ b/src/core/features/fileuploader/services/handlers/album.ts
@@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { CorePlatform } from '@services/platform';
-import { CoreUtils } from '@services/utils/utils';
+import { CoreArray } from '@singletons/array';
import { makeSingleton } from '@singletons';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate';
import { CoreFileUploaderHelper } from '../fileuploader-helper';
@@ -41,7 +41,7 @@ export class CoreFileUploaderAlbumHandlerService implements CoreFileUploaderHand
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
// Album allows picking images and videos.
- return CoreUtils.filterByRegexp(mimetypes, /^(image|video)\//);
+ return CoreArray.filterByRegexp(mimetypes, /^(image|video)\//);
}
/**
diff --git a/src/core/features/fileuploader/services/handlers/audio.ts b/src/core/features/fileuploader/services/handlers/audio.ts
index 2a2e93725..a5e5dc573 100644
--- a/src/core/features/fileuploader/services/handlers/audio.ts
+++ b/src/core/features/fileuploader/services/handlers/audio.ts
@@ -16,10 +16,11 @@ import { Injectable } from '@angular/core';
import { CoreApp } from '@services/app';
import { CorePlatform } from '@services/platform';
-import { CoreUtils } from '@services/utils/utils';
+import { CoreArray } from '@singletons/array';
import { makeSingleton } from '@singletons';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate';
import { CoreFileUploaderHelper } from '../fileuploader-helper';
+
/**
* Handler to record an audio to upload it.
*/
@@ -42,10 +43,10 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand
getSupportedMimetypes(mimetypes: string[]): string[] {
if (CorePlatform.isIOS()) {
// In iOS it's recorded as WAV.
- return CoreUtils.filterByRegexp(mimetypes, /^audio\/wav$/);
+ return CoreArray.filterByRegexp(mimetypes, /^audio\/wav$/);
} else if (CorePlatform.isAndroid()) {
// In Android we don't know the format the audio will be recorded, so accept any audio mimetype.
- return CoreUtils.filterByRegexp(mimetypes, /^audio\//);
+ return CoreArray.filterByRegexp(mimetypes, /^audio\//);
} else {
// In browser, support audio formats that are supported by MediaRecorder.
if (MediaRecorder) {
diff --git a/src/core/features/fileuploader/services/handlers/camera.ts b/src/core/features/fileuploader/services/handlers/camera.ts
index 80cae2cdd..373b338fa 100644
--- a/src/core/features/fileuploader/services/handlers/camera.ts
+++ b/src/core/features/fileuploader/services/handlers/camera.ts
@@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreApp } from '@services/app';
import { CorePlatform } from '@services/platform';
-import { CoreUtils } from '@services/utils/utils';
+import { CoreArray } from '@singletons/array';
import { makeSingleton } from '@singletons';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate';
import { CoreFileUploaderHelper } from '../fileuploader-helper';
@@ -42,7 +42,7 @@ export class CoreFileUploaderCameraHandlerService implements CoreFileUploaderHan
*/
getSupportedMimetypes(mimetypes: string[]): string[] {
// Camera only supports JPEG and PNG.
- return CoreUtils.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/);
+ return CoreArray.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/);
}
/**
diff --git a/src/core/features/fileuploader/services/handlers/video.ts b/src/core/features/fileuploader/services/handlers/video.ts
index 7d5b72476..ac655b580 100644
--- a/src/core/features/fileuploader/services/handlers/video.ts
+++ b/src/core/features/fileuploader/services/handlers/video.ts
@@ -16,10 +16,11 @@ import { Injectable } from '@angular/core';
import { CoreApp } from '@services/app';
import { CorePlatform } from '@services/platform';
-import { CoreUtils } from '@services/utils/utils';
+import { CoreArray } from '@singletons/array';
import { makeSingleton } from '@singletons';
import { CoreFileUploaderHandler, CoreFileUploaderHandlerData, CoreFileUploaderHandlerResult } from '../fileuploader-delegate';
import { CoreFileUploaderHelper } from '../fileuploader-helper';
+
/**
* Handler to record a video to upload it.
*/
@@ -42,10 +43,10 @@ export class CoreFileUploaderVideoHandlerService implements CoreFileUploaderHand
getSupportedMimetypes(mimetypes: string[]): string[] {
if (CorePlatform.isIOS()) {
// In iOS it's recorded as MOV.
- return CoreUtils.filterByRegexp(mimetypes, /^video\/quicktime$/);
+ return CoreArray.filterByRegexp(mimetypes, /^video\/quicktime$/);
} else if (CorePlatform.isAndroid()) {
// In Android we don't know the format the video will be recorded, so accept any video mimetype.
- return CoreUtils.filterByRegexp(mimetypes, /^video\//);
+ return CoreArray.filterByRegexp(mimetypes, /^video\//);
} else {
// In browser, support video formats that are supported by MediaRecorder.
if (MediaRecorder) {
diff --git a/src/core/features/grades/pages/courses/courses.html b/src/core/features/grades/pages/courses/courses.html
index 9a0f31c22..1c7eb2f8d 100644
--- a/src/core/features/grades/pages/courses/courses.html
+++ b/src/core/features/grades/pages/courses/courses.html
@@ -21,7 +21,9 @@
[attr.aria-current]="courses.getItemAriaCurrent(course)" class="ion-text-wrap" button [detail]="true"
(click)="courses.select(course)">
-
+
+
+
diff --git a/src/core/features/h5p/classes/content-validator.ts b/src/core/features/h5p/classes/content-validator.ts
index 3dec4b47b..df57b88d1 100644
--- a/src/core/features/h5p/classes/content-validator.ts
+++ b/src/core/features/h5p/classes/content-validator.ts
@@ -17,6 +17,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreH5P } from '@features/h5p/services/h5p';
import { Translate } from '@singletons';
import { CoreH5PCore, CoreH5PLibraryData, CoreH5PLibraryAddonData, CoreH5PContentDepsTreeDependency } from './core';
+import { CoreArray } from '@singletons/array';
const ALLOWED_STYLEABLE_TAGS = ['span', 'p', 'div', 'h1', 'h2', 'h3', 'td'];
@@ -131,7 +132,7 @@ export class CoreH5PContentValidator {
tags.push('s');
}
- tags = CoreUtils.uniqueArray(tags);
+ tags = CoreArray.unique(tags);
// Determine allowed style tags
const stylePatterns: RegExp[] = [];
@@ -372,7 +373,7 @@ export class CoreH5PContentValidator {
if (semantics.extraAttributes) {
validKeys = validKeys.concat(semantics.extraAttributes);
}
- validKeys = CoreUtils.uniqueArray(validKeys);
+ validKeys = CoreArray.unique(validKeys);
this.filterParams(file, validKeys);
@@ -556,7 +557,7 @@ export class CoreH5PContentValidator {
let validKeys = ['library', 'params', 'subContentId', 'metadata'];
if (semantics.extraAttributes) {
- validKeys = CoreUtils.uniqueArray(validKeys.concat(semantics.extraAttributes));
+ validKeys = CoreArray.unique(validKeys.concat(semantics.extraAttributes));
}
this.filterParams(value, validKeys);
diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts
index 64f6eaa9d..3e93bc137 100644
--- a/src/core/features/login/pages/site/site.ts
+++ b/src/core/features/login/pages/site/site.ts
@@ -235,7 +235,7 @@ export class CoreLoginSitePage implements OnInit {
/**
* Validate Url.
*
- * @returns {ValidatorFn} Validation results.
+ * @returns Validation results.
*/
protected moodleUrlValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
diff --git a/src/core/features/mainmenu/pages/menu/menu.scss b/src/core/features/mainmenu/pages/menu/menu.scss
index 768a558a9..22f2007d9 100644
--- a/src/core/features/mainmenu/pages/menu/menu.scss
+++ b/src/core/features/mainmenu/pages/menu/menu.scss
@@ -41,48 +41,53 @@
}
ion-tab-bar {
- height: var(--menutabbar-size);
+ --background: var(--core-bottom-tabs-background);
+ --color: var(--core-bottom-tabs-color);
+ --color-selected: var(--core-bottom-tabs-color-selected);
+ --background-selected: var(--core-bottom-tabs-background-selected);
- core-user-menu-button {
- align-items: center;
- display: flex;
- justify-content: center;
+ height: var(--menutabbar-size);
+}
+
+core-user-menu-button {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+}
+
+ion-tab-button {
+ &.tab-selected {
+ background: var(--background-selected);
}
- ion-tab-button {
- &.tab-selected {
- background: var(--background-selected);
- }
+ ion-icon.core-tab-icon {
+ text-overflow: unset;
+ overflow: visible;
+ text-align: center;
+ font-size: var(--mdl-typography-icon-fontSize-lg);
+ }
- ion-icon.core-tab-icon {
- text-overflow: unset;
- overflow: visible;
- text-align: center;
- font-size: var(--mdl-typography-icon-fontSize-lg);
- }
+ ion-badge.core-tab-badge {
+ font-size: 12px;
+ font-weight: bold;
+ border-radius: 10px;
+ padding-left: 6px;
+ padding-right: 6px;
+ line-height: 14px;
+ --background: var(--core-bottom-tabs-badge-color);
+ --color: var(--core-bottom-tabs-badge-text-color);
+ }
- ion-badge.core-tab-badge {
- font-size: 12px;
- font-weight: bold;
- border-radius: 10px;
- padding-left: 6px;
- padding-right: 6px;
- line-height: 14px;
- --background: var(--core-bottom-tabs-badge-color);
- --color: var(--core-bottom-tabs-badge-text-color);
- }
-
- ion-icon.core-tab-badge {
- color: var(--core-bottom-tabs-badge-color);
- padding: 3px 6px 2px;
- @include position(8px, null, null, calc(50% + 6px));
- min-width: 12px;
- font-size: 8px;
- font-weight: normal;
- box-sizing: border-box;
- position: absolute;
- z-index: 1;
- }
+ ion-icon.core-tab-badge {
+ color: var(--core-bottom-tabs-badge-color);
+ padding: 3px 6px 2px;
+ @include position(8px, null, null, calc(50% + 6px));
+ min-width: 12px;
+ font-size: 8px;
+ font-weight: normal;
+ box-sizing: border-box;
+ position: absolute;
+ z-index: 1;
}
}
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 054ab491a..c10f9d2ea 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
@@ -2,8 +2,7 @@
-
+
+
+
+ {{ 'core.search.err_minlength' | translate : {'$a': {'format': lengthCheck} } }}
+
+
diff --git a/src/core/features/search/components/search-box/search-box.scss b/src/core/features/search/components/search-box/search-box.scss
index 8afdc8c3d..fa22c8742 100644
--- a/src/core/features/search/components/search-box/search-box.scss
+++ b/src/core/features/search/components/search-box/search-box.scss
@@ -35,8 +35,6 @@
}
}
-
-
.core-search-history {
max-height: calc(-120px + 80vh);
overflow-y: auto;
diff --git a/src/core/features/search/components/search-box/search-box.ts b/src/core/features/search/components/search-box/search-box.ts
index c013928d3..1db5989c8 100644
--- a/src/core/features/search/components/search-box/search-box.ts
+++ b/src/core/features/search/components/search-box/search-box.ts
@@ -62,6 +62,7 @@ export class CoreSearchBoxComponent implements OnInit {
searchText = '';
history: CoreSearchHistoryDBRecord[] = [];
historyShown = false;
+ showLengthAlert = false;
constructor() {
this.onSubmit = new EventEmitter();
@@ -86,14 +87,17 @@ export class CoreSearchBoxComponent implements OnInit {
* @param e Event.
*/
submitForm(e?: Event): void {
- e && e.preventDefault();
- e && e.stopPropagation();
+ e?.preventDefault();
+ e?.stopPropagation();
if (this.searchText.length < this.lengthCheck) {
- // The view should handle this case, but we check it here too just in case.
+ this.showLengthAlert = true;
+
return;
}
+ this.showLengthAlert = false;
+
if (this.searchArea) {
this.saveSearchToHistory(this.searchText);
}
@@ -147,6 +151,7 @@ export class CoreSearchBoxComponent implements OnInit {
clearForm(): void {
this.searched = '';
this.searchText = '';
+ this.showLengthAlert = false;
this.onClear.emit();
}
diff --git a/src/core/features/search/lang.json b/src/core/features/search/lang.json
index 5a17626cd..d1fbb00cd 100644
--- a/src/core/features/search/lang.json
+++ b/src/core/features/search/lang.json
@@ -8,5 +8,6 @@
"globalsearch": "Global search",
"noresults": "No results for \"{{$a}}\"",
"noresultshelp": "Check for typos or try using different keywords",
+ "err_minlength": "You must enter at least {{$a.format}} characters here.",
"resultby": "By {{$a}}"
}
diff --git a/src/core/features/search/pages/global-search/global-search.html b/src/core/features/search/pages/global-search/global-search.html
index 61f677773..ebdb674d9 100644
--- a/src/core/features/search/pages/global-search/global-search.html
+++ b/src/core/features/search/pages/global-search/global-search.html
@@ -35,7 +35,7 @@
[error]="loadMoreError" />
- {{ 'core.search.empty' | translate }}
+ {{ 'core.search.empty' | translate }}
{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}
{{ 'core.search.noresultshelp' | translate }}
diff --git a/src/core/features/settings/pages/dev/dev.html b/src/core/features/settings/pages/dev/dev.html
index 37484811f..3de3e2c67 100644
--- a/src/core/features/settings/pages/dev/dev.html
+++ b/src/core/features/settings/pages/dev/dev.html
@@ -17,13 +17,13 @@
-
+
Change text direction
{{ direction }}
-
+
Force safe area margins
@@ -34,13 +34,13 @@
-
+
Enable remote styles {{remoteStylesCount}}
-
+
Enable site plugin styles {{pluginStylesCount}}
diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html
index 98a838f0f..bda6b9467 100644
--- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html
+++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html
@@ -18,7 +18,7 @@
-
+
{{ 'core.settings.developeroptions' | translate }}
diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html
index 1a3fa3331..bd5fc3828 100644
--- a/src/core/features/settings/pages/general/general.html
+++ b/src/core/features/settings/pages/general/general.html
@@ -11,16 +11,16 @@
-
+
- {{ 'core.settings.language' | translate }}
+ {{ 'core.settings.language' | translate }}
{{ entry.name }}
-
+
- {{ 'core.settings.fontsize' | translate }}
+ {{ 'core.settings.fontsize' | translate }}
- 0" lines="none">
+ 0" lines="none">
-
{{ 'core.settings.colorscheme' | translate }}
+
{{ 'core.settings.colorscheme' | translate }}
{{ 'core.settings.forcedsetting' | translate }}
@@ -47,34 +47,34 @@
0 && selectedScheme==='system' && isAndroid" lines="none">
- {{ 'core.settings.colorscheme-system-notice' | translate }}
+ {{ 'core.settings.colorscheme-system-notice' | translate }}
-
+
- {{ 'core.settings.enablerichtexteditor' | translate }}
- {{ 'core.settings.enablerichtexteditordescription' | translate }}
+ {{ 'core.settings.enablerichtexteditor' | translate }}
+ {{ 'core.settings.enablerichtexteditordescription' | translate }}
-
+
- {{ 'core.settings.ioscookies' | translate }}
- {{ 'core.settings.ioscookiesdescription' | translate }}
+ {{ 'core.settings.ioscookies' | translate }}
+ {{ 'core.settings.ioscookiesdescription' | translate }}
{{ 'core.opensettings' | translate }}
-
+
- {{ 'core.settings.debugdisplay' | translate }}
- {{ 'core.settings.debugdisplaydescription' | translate }}
+ {{ 'core.settings.debugdisplay' | translate }}
+ {{ 'core.settings.debugdisplaydescription' | translate }}
-
+
- {{ 'core.settings.enableanalytics' | translate }}
- {{ 'core.settings.enableanalyticsdescription' | translate }}
+ {{ 'core.settings.enableanalytics' | translate }}
+ {{ 'core.settings.enableanalyticsdescription' | translate }}
diff --git a/src/core/features/settings/pages/site/site.html b/src/core/features/settings/pages/site/site.html
index d463d1ba9..100d21a13 100644
--- a/src/core/features/settings/pages/site/site.html
+++ b/src/core/features/settings/pages/site/site.html
@@ -16,7 +16,7 @@
-
diff --git a/src/core/features/settings/pages/synchronization/synchronization.html b/src/core/features/settings/pages/synchronization/synchronization.html
index 44b02e537..30718da96 100644
--- a/src/core/features/settings/pages/synchronization/synchronization.html
+++ b/src/core/features/settings/pages/synchronization/synchronization.html
@@ -23,7 +23,7 @@
{{ 'core.settings.syncsettings' | translate }}
-
+
{{ 'core.settings.syncdatasaver' | translate }}
diff --git a/src/core/features/tag/pages/search/search.html b/src/core/features/tag/pages/search/search.html
index 40a682ac7..8416209e6 100644
--- a/src/core/features/tag/pages/search/search.html
+++ b/src/core/features/tag/pages/search/search.html
@@ -19,7 +19,7 @@
1 ? '' : null">
+ autocorrect="off" [spellcheck]="false" [autoFocus]="false" [lengthCheck]="1" searchArea="CoreTag" />
1">
@@ -34,7 +34,7 @@
+ [message]="'core.tag.notagsfound' | translate: {$a: query}" role="alert" />
0">
diff --git a/src/core/features/user/pages/participants/participants.html b/src/core/features/user/pages/participants/participants.html
index 4775bb79f..96bb9d5d9 100644
--- a/src/core/features/user/pages/participants/participants.html
+++ b/src/core/features/user/pages/participants/participants.html
@@ -12,7 +12,7 @@
[message]="'core.user.noparticipants' | translate" />
+ [message]="'core.noresults' | translate" [attr.role]="searchQuery ? 'alert' : null" />
listener.resolve());
@@ -149,6 +150,8 @@ export class CoreUserToursService {
const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
viewContainer?.removeAttribute('aria-hidden');
+ viewContainer?.removeAttribute('tabindex');
+
}
/**
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index 129f5218f..ff9720ac1 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -299,24 +299,38 @@ export class CoreDomUtilsProvider {
): Promise {
let retries = 10;
- let focusElement = element;
+ let elementToFocus = element;
- if ('getInputElement' in focusElement) {
- // If it's an Ionic element get the right input to use.
- focusElement.componentOnReady && await focusElement.componentOnReady();
- focusElement = await focusElement.getInputElement();
+ /**
+ * See focusElement function on Ionic Framework utils/helpers.ts.
+ */
+ if (elementToFocus.classList.contains('ion-focusable')) {
+ const app = elementToFocus.closest('ion-app');
+ if (app) {
+ app.setFocus([elementToFocus]);
+ }
+
+ if (document.activeElement === elementToFocus) {
+ return;
+ }
}
- if (!focusElement || !focusElement.focus) {
+ if ('getInputElement' in elementToFocus) {
+ // If it's an Ionic element get the right input to use.
+ elementToFocus.componentOnReady && await elementToFocus.componentOnReady();
+ elementToFocus = await elementToFocus.getInputElement();
+ }
+
+ if (!elementToFocus || !elementToFocus.focus) {
throw new CoreError('Element to focus cannot be focused');
}
- while (retries > 0 && focusElement !== document.activeElement) {
- focusElement.focus();
+ while (retries > 0 && elementToFocus !== document.activeElement) {
+ elementToFocus.focus();
- if (focusElement === document.activeElement) {
+ if (elementToFocus === document.activeElement) {
await CoreUtils.nextTick();
- if (CorePlatform.isAndroid() && this.supportsInputKeyboard(focusElement)) {
+ if (CorePlatform.isAndroid() && this.supportsInputKeyboard(elementToFocus)) {
// On some Android versions the keyboard doesn't open automatically.
CoreApp.openKeyboard();
}
diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts
index 00c1e2729..9b68444a5 100644
--- a/src/core/services/utils/utils.ts
+++ b/src/core/services/utils/utils.ts
@@ -40,6 +40,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { CoreUrlUtils } from './url';
import { QRScanner } from '@features/native/plugins';
+import { CoreArray } from '@singletons/array';
export type TreeNode = T & { children: TreeNode[] };
@@ -350,6 +351,7 @@ export class CoreUtilsProvider {
* @param from Object to copy the properties from.
* @param to Object where to store the properties.
* @param clone Whether the properties should be cloned (so they are different instances).
+ * @deprecated since 4.4. Not used anymore.
*/
copyProperties(from: Record, to: Record, clone: boolean = true): void {
for (const name in from) {
@@ -387,6 +389,7 @@ export class CoreUtilsProvider {
* Empties an array without losing its reference.
*
* @param array Array to empty.
+ * @deprecated since 4.4. Not used anymore.
*/
emptyArray(array: unknown[]): void {
array.length = 0; // Empty array without losing its reference.
@@ -396,6 +399,7 @@ export class CoreUtilsProvider {
* Removes all properties from an object without losing its reference.
*
* @param object Object to remove the properties.
+ * @deprecated since 4.4. Not used anymore.
*/
emptyObject(object: Record): void {
for (const key in object) {
@@ -482,17 +486,10 @@ export class CoreUtilsProvider {
* @param array Array to filter.
* @param regex RegExp to apply to each string.
* @returns Filtered array.
+ * @deprecated since 4.4. Use CoreArray.filterByRegexp instead.
*/
filterByRegexp(array: string[], regex: RegExp): string[] {
- if (!array || !array.length) {
- return [];
- }
-
- return array.filter((entry) => {
- const matches = entry.match(regex);
-
- return matches && matches.length;
- });
+ return CoreArray.filterByRegexp(array, regex);
}
/**
@@ -956,7 +953,7 @@ export class CoreUtilsProvider {
* @returns Merged array.
*/
mergeArraysWithoutDuplicates(array1: T[], array2: T[], key?: string): T[] {
- return this.uniqueArray(array1.concat(array2), key) as T[];
+ return CoreArray.unique(array1.concat(array2), key) as T[];
}
/**
@@ -1390,6 +1387,7 @@ export class CoreUtilsProvider {
* @param data Object.
* @param prefix Prefix to add.
* @returns Prefixed object.
+ * @deprecated since 4.4. Not used anymore.
*/
prefixKeys(data: Record, prefix: string): Record {
const newObj = {};
@@ -1611,21 +1609,10 @@ export class CoreUtilsProvider {
* @param array The array to treat.
* @param [key] Key of the property that must be unique. If not specified, the whole entry.
* @returns Array without duplicate values.
+ * @deprecated since 4.4. Use CoreArray.unique instead.
*/
uniqueArray(array: T[], key?: string): T[] {
- const unique = {}; // Use an object to make it faster to check if it's duplicate.
-
- return array.filter(entry => {
- const value = key ? entry[key] : entry;
-
- if (value in unique) {
- return false;
- }
-
- unique[value] = true;
-
- return true;
- });
+ return CoreArray.unique(array, key);
}
/**
diff --git a/src/core/singletons/array.ts b/src/core/singletons/array.ts
index 711313ef6..49f81195f 100644
--- a/src/core/singletons/array.ts
+++ b/src/core/singletons/array.ts
@@ -66,4 +66,46 @@ export class CoreArray {
return newArray;
}
+ /**
+ * Return an array without duplicate values.
+ *
+ * @param array The array to treat.
+ * @param [key] Key of the property that must be unique. If not specified, the whole entry.
+ * @returns Array without duplicate values.
+ */
+ static unique(array: T[], key?: string): T[] {
+ const unique = {}; // Use an object to make it faster to check if it's duplicate.
+
+ return array.filter(entry => {
+ const value = key ? entry[key] : entry;
+
+ if (value in unique) {
+ return false;
+ }
+
+ unique[value] = true;
+
+ return true;
+ });
+ }
+
+ /**
+ * Given an array of strings, return only the ones that match a regular expression.
+ *
+ * @param array Array to filter.
+ * @param regex RegExp to apply to each string.
+ * @returns Filtered array.
+ */
+ static filterByRegexp(array: string[], regex: RegExp): string[] {
+ if (!array || !array.length) {
+ return [];
+ }
+
+ return array.filter((entry) => {
+ const matches = entry.match(regex);
+
+ return matches && matches.length;
+ });
+ }
+
}
diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts
index e67b78ba4..367418436 100644
--- a/src/core/singletons/dom.ts
+++ b/src/core/singletons/dom.ts
@@ -537,10 +537,12 @@ export class CoreDom {
*
* @param element Element to listen to events.
* @param callback Callback to call when clicked or the key is pressed.
+ * @param setTabIndex Whether to set tabindex and role.
*/
static initializeClickableElementA11y(
element: HTMLElement & {disabled?: boolean},
callback: (event: MouseEvent | KeyboardEvent) => void,
+ setTabIndex = true,
): void {
const enabled = () => !CoreUtils.isTrueOrOne(element.dataset.disabledA11yClicks ?? 'false');
@@ -563,14 +565,14 @@ export class CoreDom {
}
if (event.key === ' ' || event.key === 'Enter') {
+ callback(event);
+
event.preventDefault();
event.stopPropagation();
-
- callback(event);
}
});
- if (element.tagName !== 'BUTTON' && element.tagName !== 'A') {
+ if (setTabIndex && element.tagName !== 'BUTTON' && element.tagName !== 'A') {
// Set tabindex if not previously set.
if (element.getAttribute('tabindex') === null) {
element.setAttribute('tabindex', element.disabled ? '-1' : '0');
diff --git a/src/core/singletons/tests/array.test.ts b/src/core/singletons/tests/array.test.ts
index c09225cf1..db34315a3 100644
--- a/src/core/singletons/tests/array.test.ts
+++ b/src/core/singletons/tests/array.test.ts
@@ -23,4 +23,18 @@ describe('CoreArray singleton', () => {
expect(CoreArray.withoutItem(originalArray, 'not found')).toEqual(['foo', 'bar', 'baz']);
});
+ it('gets unique array', () => {
+ const originalArray = ['foo', 'bar', 'foo', 'baz'];
+
+ expect(CoreArray.unique(originalArray)).toEqual(['foo', 'bar', 'baz']);
+ });
+
+ it('filters array by regexp', () => {
+ const originalArray = ['foo', 'bar', 'baz', 'qux'];
+
+ expect(CoreArray.filterByRegexp(originalArray, /ba/)).toEqual(['bar', 'baz']);
+ expect(CoreArray.filterByRegexp(originalArray, /foo/)).toEqual(['foo']);
+ expect(CoreArray.filterByRegexp([], /foo/)).toEqual([]);
+ });
+
});
diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts
index d4a39c5d5..9f98a5f3d 100644
--- a/src/testing/services/behat-runtime.ts
+++ b/src/testing/services/behat-runtime.ts
@@ -496,18 +496,27 @@ export class TestingBehatRuntimeService {
getHeader(): string {
this.log('Action - Get header');
- let titles = Array.from(document.querySelectorAll('.ion-page:not(.ion-page-hidden) > ion-header h1'));
- titles = titles.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body));
+ let titles = Array.from(document.querySelectorAll('.ion-page:not(.ion-page-hidden) > ion-header h1'))
+ .filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body))
+ .map((title) => title.innerText.trim());
+
+ // Collapsed title, get the floating title.
+ if (titles.length < 0 || (titles.length === 1 && titles[0] === '')) {
+ titles = Array.from(document.querySelectorAll(
+ '.ion-page:not(.ion-page-hidden) h1.collapsible-header-floating-title',
+ )).filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body))
+ .map((title) => title.innerText.trim());
+ }
if (titles.length > 1) {
- return 'ERROR: Too many possible titles ('+titles.length+').';
- } else if (!titles.length) {
- return 'ERROR: No title found.';
- } else {
- const title = titles[0].innerText.trim();
-
- return 'OK:' + title;
+ return `ERROR: Too many possible titles (${titles.length}).`;
}
+
+ if (!titles.length) {
+ return 'ERROR: No title found.';
+ }
+
+ return `OK: ${titles[0]}`;
}
/**
diff --git a/src/theme/components/collapsible-header.scss b/src/theme/components/collapsible-header.scss
index f3f1af3f8..c890beafb 100644
--- a/src/theme/components/collapsible-header.scss
+++ b/src/theme/components/collapsible-header.scss
@@ -25,6 +25,12 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
opacity: 0;
}
+ &.collapsible-header-page-is-collapsed .collapsible-header-floating-title {
+ pointer-events: none;
+ user-select: none;
+ visibility: hidden;
+ }
+
&:not(.collapsible-header-page-is-collapsed) .collapsible-header-collapsed {
--core-header-toolbar-border-width: 0;
--core-header-buttons-background: var(--ion-background-color);
@@ -35,6 +41,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
h1 {
opacity: 0;
+ pointer-events: none;
+ user-select: none;
+ visibility: hidden;
}
}
@@ -64,7 +73,9 @@ body:not(.core-iframe-fullscreen) .collapsible-header-page {
.collapsible-header-original-title {
pointer-events: none;
+ user-select: none;
opacity: 0;
+ visibility: hidden;
}
& > *:not(.collapsible-header-floating-title-wrapper),
diff --git a/src/theme/components/error-accordion.scss b/src/theme/components/error-accordion.scss
index e99653bf6..26267c72f 100644
--- a/src/theme/components/error-accordion.scss
+++ b/src/theme/components/error-accordion.scss
@@ -8,7 +8,6 @@
--state-color-hover: rgb(40 40 40, 4%); // --gray-900 4%
--state-color-pressed: rgb(40 40 40, 12%); // --gray-900 12%
- --state-color-focused: rgb(40 40 40, 12%); // --gray-900 12%
background: var(--background-color);
border-radius: var(--mdl-shape-borderRadius-xs);
@@ -78,19 +77,12 @@
transform: rotate(0);
}
+ @include core-focus-background();
+
&:hover {
background: var(--state-color-hover);
}
- &:focus {
- box-shadow: none;
- background: var(--state-color-focused);
- }
-
- &:focus-visible {
- @include core-focus-style();
- }
-
&:active {
background: var(--state-color-pressed);
}
diff --git a/src/theme/components/ion-card.scss b/src/theme/components/ion-card.scss
index af6075e72..451a9530f 100644
--- a/src/theme/components/ion-card.scss
+++ b/src/theme/components/ion-card.scss
@@ -14,6 +14,8 @@ ion-card {
&::part(native) {
--border-width: 0;
+
+ @include core-focus-over();
}
ion-item:only-child {
diff --git a/src/theme/components/ion-item-divider.scss b/src/theme/components/ion-item-divider.scss
index d4bb6f37b..ed63c3d5f 100644
--- a/src/theme/components/ion-item-divider.scss
+++ b/src/theme/components/ion-item-divider.scss
@@ -11,11 +11,9 @@ ion-item.item.divider {
ion-label h2,
ion-label p.item-heading {
- font-size: var(--item-divider-font-size);
- font-weight: 500;
- line-height: 1.5;
+ font: var(--mdl-typography-subtitle-font-md);
}
ion-label h2.big {
- font-size: var(--item-divider-font-size-big);
+ font: var(--mdl-typography-subtitle-font-lg);
}
}
diff --git a/src/theme/components/ion-item.scss b/src/theme/components/ion-item.scss
index 4febd0a8e..069efcb74 100644
--- a/src/theme/components/ion-item.scss
+++ b/src/theme/components/ion-item.scss
@@ -1,8 +1,13 @@
-ion-item {
+ion-item.item {
--detail-icon-color: var(--ion-item-detail-icon-color);
--detail-icon-font-size: var(--ion-item-detail-icon-font-size);
--detail-icon-opacity: var(--ion-item-detail-icon-opacity);
+ ion-input.in-item,
+ &.item-label > ion-label.label-stacked {
+ font-size: var(--mdl-typography-fontSize-lg);
+ }
+
> ion-icon[slot] {
color: var(--ion-item-icon-color);
}
@@ -16,7 +21,7 @@ ion-item {
&.ion-invalid {
--inner-border-width: 0 0 1px 0;
- &.ion-touched {
+ &.ion-touched:not(.ion-no-validation) {
&.ion-invalid {
--ion-item-border-color: var(--highlight-color-invalid);
--highlight-background: var(--ion-item-border-color);
@@ -30,11 +35,25 @@ ion-item {
}
}
+ &.ion-no-validation {
+ --inner-border-width: 0 0 1px 0;
+ }
+
// Hide details on items to align badges.
&.hide-detail {
--detail-icon-opacity: 0;
}
+
+ &:not(.item-input) {
+ --show-full-highlight: 0;
+ --show-inset-highlight: 0;
+ }
+
+ &.item-has-interactive-control:focus-within {
+ @include core-focus-outline();
+ }
}
+
// Fake item.
div.fake-ion-item {
position: relative;
@@ -50,7 +69,7 @@ div.fake-ion-item {
}
.md div.fake-ion-item {
- font-size: 16px;
+ font-size: var(--text-size);
font-weight: normal;
text-transform: none;
@include padding(null, 16px, null, 16px);
@@ -125,3 +144,175 @@ div.fake-ion-item {
}
}
+
+// Item Headings.
+// Some styles taken from ion-label
+ion-item.item.item-label > ion-label,
+.fake-ion-item > ion-label,
+ion-item .in-item {
+ font-size: var(--text-size);
+
+ p {
+ --color: var(--subdued-text-color);
+ color: var(--color);
+ @include margin(2px, 0);
+ }
+
+ .item-heading {
+ @include margin(2px, 0);
+
+ font-size: 1rem;
+ font-weight: normal;
+
+ text-overflow: inherit;
+ overflow: inherit;
+ --color: initial;
+ color: var(--color);
+
+ &.item-heading-secondary {
+ @include margin(2px, 0);
+
+ font-size: var(--text-size);
+ font-weight: normal;
+ line-height: normal;
+
+ --color: var(--subdued-text-color);
+ }
+ }
+}
+
+// Correctly inherit ion-text-wrap onto labels.
+.item > ion-label,
+.fake-ion-item,
+.item.ion-text-wrap > ion-checkbox::part(label),
+ion-checkbox.ion-text-wrap::part(label) {
+ core-format-text,
+ core-format-text > *:not(pre) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.item.ion-text-wrap > ion-label,
+ion-item > .in-item,
+.fake-ion-item.ion-text-wrap,
+.item.ion-text-wrap > ion-checkbox::part(label),
+ion-checkbox.ion-text-wrap::part(label) {
+ core-format-text,
+ core-format-text > *:not(pre) {
+ white-space: normal;
+ overflow: inherit;
+ }
+}
+
+.item.ion-text-wrap > ion-label,
+.item.ion-text-wrap ion-checkbox::part(label),
+ion-checkbox.ion-text-wrap::part(label),
+.item.ion-text-wrap ion-radio::part(label),
+ion-radio.ion-text-wrap::part(label) {
+ white-space: normal !important;
+}
+
+ion-item .core-input-errors-wrapper {
+ width: 100%;
+}
+
+ion-item.item.item-file {
+ ion-thumbnail {
+ --size: 32px;
+ width: var(--size);
+ height: var(--size);
+ }
+
+ p.item-heading {
+ font-size: var(--text-size);
+ }
+
+ p {
+ font-size: var(--mdl-typography-fontSize-sm);
+ }
+
+ ion-label {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+
+ ion-button {
+ --a11y-sizing-minTargetSize: 40px;
+ }
+
+ &.item-directory ion-icon {
+ @include margin-horizontal(0px, 16px);
+ }
+
+ [slot=end] {
+ @include margin-horizontal(16px, null);
+ }
+}
+
+.item-dimmed {
+ opacity: 0.7;
+ --background: var(--light);
+ ion-item {
+ --background: var(--light);
+ }
+}
+
+// Make links clickable when inside radio or checkbox items. Style part.
+@media (hover: hover) {
+ ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
+ color: var(--color-hover);
+
+ &::after {
+ background: var(--background-hover);
+ opacity: var(--background-hover-opacity);
+ }
+ }
+
+ ion-item.ion-color.item-multiple-inputs:hover::part(native) {
+ color: #{current-color(contrast)};
+
+ &::after {
+ background: #{current-color(contrast)};
+ }
+ }
+}
+
+// It fixes the click on links where ion-ripple-effect is present.
+// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
+ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
+ion-item.ion-activatable:not(.only-links) {
+ cursor: pointer;
+ ion-label {
+ z-index: 3;
+ pointer-events: none;
+
+ ion-anchor, a,
+ ion-button, button,
+ ion-item.ion-focusable,
+ audio, video, select, input, iframe {
+ pointer-events: visible;
+ }
+ }
+
+ ion-checkbox, ion-datetime, ion-radio, ion-select{
+ position: static;
+ }
+}
+
+ion-item.item-multiple-inputs.only-links {
+ a {
+ cursor: pointer;
+ }
+}
+
+// Case with ion-input + ion-select inside.
+ion-item.item-input.item-multiple-inputs {
+ .flex-row {
+ width: 100%;
+ ion-select {
+ position: relative;
+ }
+ }
+}
diff --git a/src/theme/components/ion-select.scss b/src/theme/components/ion-select.scss
new file mode 100644
index 000000000..1123bf14f
--- /dev/null
+++ b/src/theme/components/ion-select.scss
@@ -0,0 +1,81 @@
+// Select.
+ion-select {
+ &::part(text) {
+ white-space: normal;
+ }
+ &::part(icon) {
+ opacity: 1;
+ }
+}
+
+ion-select-popover {
+ ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
+ opacity: 1;
+ }
+
+ ion-item {
+ font-size: var(--text-size);
+ }
+ ion-item.core-select-option-border-bottom {
+ border-bottom: 1px solid var(--stroke);
+ }
+
+ ion-item.core-select-option-title {
+ cursor: pointer;
+ ion-radio::part(container) {
+ display: none;
+ }
+ }
+}
+
+// Case with ion-input + ion-select inside.
+ion-item.item-input.item-multiple-inputs {
+ .flex-row {
+ width: 100%;
+ ion-select {
+ position: relative;
+ }
+ }
+}
+
+.select-alert.ios {
+ .alert-checkbox-icon {
+ border-radius: 2px;
+ }
+
+ .alert-radio-icon {
+ height: var(--size);
+ width: var(--size);
+ min-width: var(--size);
+ border-radius: var(--border-radius);
+ border-width: var(--outer-border-width);
+ border-style: var(--border-style);
+ border-color: var(--color);
+ @include margin(10px, 8px, 10px, 8px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .alert-radio-inner {
+ top: auto;
+ left: auto;
+ position: static;
+ border-radius: var(--inner-border-radius);
+ width: calc(50% + var(--outer-border-width));
+ height: calc(50% + var(--outer-border-width));
+ transform: scale3d(0, 0, 0);
+ transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
+ background: var(--contrast-background);
+ border: 0 !important;
+ }
+ }
+
+ button[aria-checked=true] .alert-radio-icon {
+ border-color: var(--color-checked);
+ background: var(--color-checked);
+
+ .alert-radio-inner {
+ transform: scale3d(1, 1, 1);
+ }
+ }
+}
diff --git a/src/theme/components/ion-tab-bar.scss b/src/theme/components/ion-tab-bar.scss
deleted file mode 100644
index 81864b57b..000000000
--- a/src/theme/components/ion-tab-bar.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-ion-tab-bar.mainmenu-tabs {
- --background: var(--core-bottom-tabs-background);
- --color: var(--core-bottom-tabs-color);
- --color-selected: var(--core-bottom-tabs-color-selected);
- --background-selected: var(--core-bottom-tabs-background-selected);
-}
diff --git a/src/theme/components/swiper.scss b/src/theme/components/swiper.scss
new file mode 100644
index 000000000..ceb684684
--- /dev/null
+++ b/src/theme/components/swiper.scss
@@ -0,0 +1,62 @@
+
+swiper-container {
+ --swiper-theme-color: var(--ion-color-primary, #3880ff);
+ --swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
+ --swiper-pagination-color: var(--swiper-theme-color);
+ --swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
+ --swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
+ --swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5);
+ width: 100%;
+ max-width: 100%;
+ max-height: 100vh;
+ // CSS Grid/Flexbox bug size workaround
+ // @see https://github.com/kenwheeler/slick/issues/982
+ // @see https://github.com/nolimits4web/swiper/issues/3599
+ min-height: 0;
+ min-width: 0;
+
+ swiper-slide {
+ display: flex;
+ position: relative;
+
+ flex-direction: column;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: 100%;
+
+ font-size: 18px;
+
+ text-align: center;
+ box-sizing: border-box;
+
+ img {
+ width: auto;
+ max-width: 100%;
+ height: auto;
+ max-height: 100%;
+ }
+ }
+}
+
+
+// To make core-swipe-slides fill the remaining height.
+.core-swipe-slides-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ min-height: 100%;
+
+ core-swipe-slides {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ swiper-container {
+ flex-grow: 1;
+ max-width: 100%;
+ }
+ }
+}
diff --git a/src/theme/helpers/custom.mixins.scss b/src/theme/helpers/custom.mixins.scss
index ae8f6e920..22272230f 100644
--- a/src/theme/helpers/custom.mixins.scss
+++ b/src/theme/helpers/custom.mixins.scss
@@ -66,7 +66,42 @@
}
}
-@mixin core-focus() {
+@mixin core-focus-over() {
+ &:focus-visible {
+ @include core-focus-over-internal();
+ }
+
+ @supports not selector(:focus-visible) {
+ @at-root:focus {
+ @include core-focus-over-internal();
+ }
+ }
+}
+
+@mixin core-focus-outline() {
+ &:focus-visible {
+ @include core-focus-outline-internal();
+ }
+ @supports not selector(:focus-visible) {
+ @at-root:focus {
+ @include core-focus-outline-internal();
+ }
+ }
+}
+
+@mixin core-focus-background() {
+ &:focus-visible {
+ @include core-focus-background-internal();
+ }
+ @supports not selector(:focus-visible) {
+ @at-root:focus {
+ @include core-focus-background-internal();
+ }
+ }
+}
+
+
+@mixin core-focus-over-internal() {
outline: none;
position: relative;
@@ -74,17 +109,26 @@
@include position(0px, 0px, 0px, 0px);
position: absolute;
content: "";
- opacity: 1;
z-index: 1;
- @include core-focus-style();
+ pointer-events: none;
+ user-select: none;
+ @include core-focus-background-internal();
}
}
-@mixin core-focus-style() {
- box-shadow: var(--a11y-shadow-focus-boxShadow);
- border-radius: var(--border-radius);
- // Thicker option:
- // outline: var(--a11y-shadow-focus-outline);
+@mixin core-focus-outline-internal() {
+ // box-shadow: var(--a11y-shadow-focus-boxShadow);
+ // border-radius: var(--border-radius);
+ outline: var(--a11y-shadow-focus-outline);
+}
+
+
+@mixin core-focus-background-internal() {
+ --background-focused: var(--background-focused, var(--a11y-background-focus-background));
+ --background-focused-opacity: var(--a11y-background-focus-opacity);
+ --background: var(--a11y-background-focus-background-rgb);
+ background: var(--background);
+ outline: none;
}
@mixin core-transition($properties: all, $duration: 500ms, $timing-function: ease-in-out) {
diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss
index 6d661464c..2e2b7c87d 100644
--- a/src/theme/theme.base.scss
+++ b/src/theme/theme.base.scss
@@ -72,77 +72,6 @@ html[dir=rtl] {
.font-lg { font-size: 1.7rem; }
.font-sm { font-size: 1.2rem; }
-// Item Headings.
-// Some styles taken from ion-label
-.item > ion-label,
-.fake-ion-item > ion-label,
-ion-item .in-item {
- p {
- --color: var(--subdued-text-color);
- color: var(--color);
- @include margin(2px, 0);
- font-size: 0.875rem;
- }
-
- .item-heading {
- @include margin(2px, 0);
-
- font-size: 1rem;
- font-weight: normal;
-
- text-overflow: inherit;
- overflow: inherit;
- --color: initial;
- color: var(--color);
-
- &.item-heading-secondary {
- @include margin(2px, 0);
-
- font-size: var(--text-size);
- font-weight: normal;
- line-height: normal;
-
- --color: var(--subdued-text-color);
- }
- }
-}
-
-// Correctly inherit ion-text-wrap onto labels.
-.item > ion-label,
-.fake-ion-item,
-.item.ion-text-wrap > ion-checkbox::part(label),
-ion-checkbox.ion-text-wrap::part(label) {
- core-format-text,
- core-format-text > *:not(pre) {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-
-.item.ion-text-wrap > ion-label,
-ion-item > .in-item,
-.fake-ion-item.ion-text-wrap,
-.item.ion-text-wrap > ion-checkbox::part(label),
-ion-checkbox.ion-text-wrap::part(label) {
- core-format-text,
- core-format-text > *:not(pre) {
- white-space: normal;
- overflow: inherit;
- }
-}
-
-.item.ion-text-wrap > ion-label,
-.item.ion-text-wrap ion-checkbox::part(label),
-ion-checkbox.ion-text-wrap::part(label),
-.item.ion-text-wrap ion-radio::part(label),
-ion-radio.ion-text-wrap::part(label) {
- white-space: normal !important;
-}
-
-ion-item .core-input-errors-wrapper {
- width: 100%;
-}
@each $color-name, $unused in $colors {
.text-#{$color-name},
@@ -566,47 +495,6 @@ body.core-iframe-fullscreen ion-router-outlet {
}
}
-.item.item-file {
- ion-thumbnail {
- --size: 32px;
- width: var(--size);
- height: var(--size);
- }
-
- p.item-heading {
- font-size: var(--text-size);
- }
-
- p {
- font-size: 12px;
- }
-
- ion-label {
- margin-top: 8px;
- margin-bottom: 8px;
- }
-
- ion-button {
- --a11y-sizing-minTargetSize: 40px;
- }
-
- &.item-directory ion-icon {
- @include margin-horizontal(0px, 16px);
- }
-
- [slot=end] {
- @include margin-horizontal(16px, null);
- }
-}
-
-.item-dimmed {
- opacity: 0.7;
- --background: var(--light);
- ion-item {
- --background: var(--light);
- }
-}
-
// Extra text colors.
.text-gray {
color: var(--gray-500);
@@ -626,24 +514,39 @@ body.core-iframe-fullscreen ion-router-outlet {
--background: var(--color-tint);
--color: var(--color-shade);
- ion-item {
+ ion-item.item {
--background: var(--color-tint);
--color: var(--color-shade);
--inner-border-width: 0px;
--border-width: 0px;
font-size: var(--text-size);
- ion-label, ion-label > p {
+ &.item-label > ion-label,
+ &.item-label > ion-label > p {
--color: var(--color-shade);
}
+
+ > ion-icon[slot] {
+ color: var(--color-shade);
+ @include margin-horizontal(null, 16px);
+ }
}
ion-label {
white-space: normal !important;
}
- ion-item > ion-icon[slot] {
- color: var(--color-shade);
- @include margin-horizontal(null, 16px);
+ }
+
+ ion-item.item.core-#{$color-name}-item {
+ --color-base: var(--ion-color-#{$color-name});
+ --color-shade: var(--ion-color-#{$color-name}-shade);
+
+ --border-width: 0 0 3px 0;
+ --border-color: var(--color-base);
+ --inner-border-width: 0px;
+
+ > ion-icon[slot] {
+ color: var(--color-base);
}
}
@@ -748,7 +651,7 @@ ion-toolbar h1 .core-bar-button-image img {
// Radio.
ion-radio,
input[type=radio],
-.select-alert.ios .alert-radio-icon {
+.select-alert .alert-radio-icon {
--border-radius: 50%;
--border-width: 2px;
--outer-border-width: 2px;
@@ -761,7 +664,6 @@ input[type=radio],
}
}
-
.ios ion-radio,
.ios input[type=radio],
.select-alert.ios .alert-radio-icon {
@@ -803,43 +705,6 @@ input[type=radio],
}
}
-.select-alert.ios {
- .alert-radio-icon {
- height: var(--size);
- width: var(--size);
- min-width: var(--size);
- border-radius: var(--border-radius);
- border-width: var(--outer-border-width);
- border-style: var(--border-style);
- border-color: var(--color);
- @include margin(10px, 8px, 10px, 8px);
- display: flex;
- align-items: center;
- justify-content: center;
-
- .alert-radio-inner {
- top: auto;
- left: auto;
- position: static;
- border-radius: var(--inner-border-radius);
- width: calc(50% + var(--outer-border-width));
- height: calc(50% + var(--outer-border-width));
- transform: scale3d(0, 0, 0);
- transition: transform 280ms cubic-bezier(.4, 0, .2, 1);
- background: var(--contrast-background);
- border: 0 !important;
- }
- }
-
- button[aria-checked=true] .alert-radio-icon {
- border-color: var(--color-checked);
- background: var(--color-checked);
-
- .alert-radio-inner {
- transform: scale3d(1, 1, 1);
- }
- }
-}
// Checkbox.
ion-checkbox,
@@ -857,48 +722,10 @@ input[type=checkbox] {
}
}
-
.ios input[type=checkbox] {
--outer-border-width: 1px;
}
-.select-alert.ios .alert-checkbox-icon {
- border-radius: 2px;
-}
-
-// Select.
-ion-select {
- &::part(text) {
- white-space: normal;
- }
- &::part(icon) {
- opacity: 1;
- }
- &::part(label) {
- max-width: none;
- }
-}
-
-ion-select-popover {
- ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
- opacity: 1;
- }
-
- ion-item {
- font-size: 14px;
- }
- ion-item.core-select-option-border-bottom {
- border-bottom: 1px solid var(--stroke);
- }
-
- ion-item.core-select-option-title {
- cursor: pointer;
- ion-radio::part(container) {
- display: none;
- }
- }
-}
-
ion-badge {
line-height: 1.1;
padding: 2px 8px;
@@ -1048,65 +875,6 @@ ion-datetime.datetime-disabled {
opacity: .65 !important;
}
-// Make links clickable when inside radio or checkbox items. Style part.
-@media (hover: hover) {
- ion-item.item-multiple-inputs:not(.item-rte):hover::part(native) {
- color: var(--color-hover);
-
- &::after {
- background: var(--background-hover);
- opacity: var(--background-hover-opacity);
- }
- }
-
- ion-item.ion-color.item-multiple-inputs:hover::part(native) {
- color: #{current-color(contrast)};
-
- &::after {
- background: #{current-color(contrast)};
- }
- }
-}
-
-
-// It fixes the click on links where ion-ripple-effect is present.
-// Make links clickable when inside radio or checkbox items. Pointer and cursor part.
-ion-item.item-multiple-inputs:not(.only-links):not(.item-rte),
-ion-item.ion-activatable:not(.only-links) {
- cursor: pointer;
- ion-label {
- z-index: 3;
- pointer-events: none;
-
- ion-anchor, a,
- ion-button, button,
- ion-item.ion-focusable,
- audio, video, select, input, iframe {
- pointer-events: visible;
- }
- }
-
- ion-checkbox, ion-datetime, ion-radio, ion-select{
- position: static;
- }
-}
-
-ion-item.item-multiple-inputs.only-links {
- a {
- cursor: pointer;
- }
-}
-
-// Case with ion-input + ion-select inside.
-ion-item.item-input.item-multiple-inputs {
- .flex-row {
- width: 100%;
- ion-select {
- position: relative;
- }
- }
-}
-
ion-item-divider.item,
ion-item.item,
td {
@@ -1122,68 +890,15 @@ td {
}
// Change default outline.
-:focus-visible {
- @include core-focus-style();
- border-radius: inherit;
- outline: none;
+.ion-activatable,
+.clickable,
+.ion-focusable.ion-focused,
+.ion-selectable {
+ @include core-focus-background();
}
-// Focus highlight for accessibility.
-.ion-focused:not(.item-multiple-inputs):not(:focus),
-ion-input.has-focus,
-ion-card:focus {
- @include core-focus();
-
- :focus-visible,
- .clickable:focus {
- box-shadow: none;
- }
-}
-
-.ion-focused.item-multiple-inputs {
- 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.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 {
- box-shadow: revert;
- opacity: revert;
- z-index: revert;
- }
- .item-highlight, .item-inner-highlight {
- position: unset;
- }
-}
-
-textarea, button, select, input, a, .clickable {
- &:focus {
- @include core-focus-style();
- outline: none;
- }
-}
-
-ion-loading:focus-visible,
-ion-alert:focus-visible,
-ion-popover:focus-visible,
-ion-modal:focus-visible {
- box-shadow: none;
- border-radius: 0;
-}
-
-ion-input .native-input {
- &:focus, &:focus-visible {
- box-shadow: none;
- outline: none;
- }
+:not(.hydrated):not(.native-input):not(.native-textarea) { // Not an ionic component.
+ @include core-focus-outline();
}
ion-input,
@@ -1296,67 +1011,6 @@ ion-grid.core-no-grid > ion-row {
}
}
-// To make core-swipe-slides fill the remaining height.
-.core-swipe-slides-container {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- min-height: 100%;
-
- core-swipe-slides {
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-
- swiper-container {
- flex-grow: 1;
- max-width: 100%;
- }
- }
-}
-
-swiper-container {
- --swiper-theme-color: var(--ion-color-primary, #3880ff);
- --swiper-pagination-bullet-inactive-color: var(--ion-color-step-200, #cccccc);
- --swiper-pagination-color: var(--swiper-theme-color);
- --swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25);
- --swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1);
- --swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5);
- width: 100%;
- max-width: 100%;
- max-height: 100vh;
- // CSS Grid/Flexbox bug size workaround
- // @see https://github.com/kenwheeler/slick/issues/982
- // @see https://github.com/nolimits4web/swiper/issues/3599
- min-height: 0;
- min-width: 0;
-
- swiper-slide {
- display: flex;
- position: relative;
-
- flex-direction: column;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
-
- width: 100%;
- height: 100%;
-
- font-size: 18px;
-
- text-align: center;
- box-sizing: border-box;
-
- img {
- width: auto;
- max-width: 100%;
- height: auto;
- max-height: 100%;
- }
- }
-}
-
.has-spacer,
.core-flex-fill {
display: flex;
diff --git a/src/theme/theme.design-system.scss b/src/theme/theme.design-system.scss
index ef9a41c88..0d10bf1a6 100644
--- a/src/theme/theme.design-system.scss
+++ b/src/theme/theme.design-system.scss
@@ -145,6 +145,9 @@ html {
--a11y-shadow-focus-borderWidth: 2px;
--a11y-shadow-focus-boxShadow: inset 0 0 var(--a11y-shadow-focus-borderWidth) 1px var(--a11y-focus-color);
--a11y-shadow-focus-outline: var(--a11y-shadow-focus-borderWidth) solid var(--a11y-focus-color);
+ --a11y-background-focus-background: var(--ion-text-color);
+ --a11y-background-focus-opacity: 0.08;
+ --a11y-background-focus-background-rgb: rgba(var(--ion-text-color-rgb), var(--a11y-background-focus-opacity));
// @TODO ***** VARIABLES TO BE REVIEWED, RENAMED AND TIDIED ***** //
--text-size: var(--mdl-typography-body-fontSize-md);
@@ -198,8 +201,6 @@ html {
--item-divider-min-height: calc(var(--a11y-sizing-minTargetSize) + 8px);
--item-divider-border-width: 0px;
- --item-divider-font-size: 16px;
- --item-divider-font-size-big: 20px;
--spacer-vertical: 8px;
@@ -247,4 +248,6 @@ html {
--a11y-min-target-size: var(--a11y-sizing-minTargetSize);
--a11y-focus-width: var(--a11y-shadow-focus-boxShadowSpread);
--a11y-focus-color: var(--a11y-shadow-focus-boxShadowColor);
+ --item-divider-font-size: var(--mdl-typography-subtitle-fontSize-md);
+ --item-divider-font-size-big: var(--mdl-typography-subtitle-fontSize-lg);
}
diff --git a/src/theme/theme.scss b/src/theme/theme.scss
index e177f74a0..5e67ed220 100644
--- a/src/theme/theme.scss
+++ b/src/theme/theme.scss
@@ -42,9 +42,10 @@ html {
@import "components/ion-note.scss";
@import "components/ion-popover.scss";
@import "components/ion-searchbar.scss";
+ @import "components/ion-select.scss";
@import "components/ion-spinner.scss";
- @import "components/ion-tab-bar.scss";
@import "components/ion-toast.scss";
+ @import "components/swiper.scss";
}
/* Some styles from 3rd party libraries. */
diff --git a/upgrade.txt b/upgrade.txt
index 21bdec12d..464109bb1 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -16,6 +16,7 @@ For more information about upgrading, read the official documentation: https://m
- FileTransfer service is no longer available, now we recommend use window.FileTransfer instead.
- CSS variable --font-size-normal has been deprecated in favor of --font-size-md.
- Activity modules services that does not admit plugins are not avaible for site plugins anymore.
+ - CoreUserDelegate type available values changed from newpage and communication to listitem and button.
=== 4.3.0 ===