MOBILE-3745 a11y: Add new combobox component

main
Pau Ferrer Ocaña 2021-05-06 11:06:17 +02:00
parent e7b61672f1
commit fa22325fb4
26 changed files with 373 additions and 171 deletions

View File

@ -34,10 +34,9 @@
</core-context-menu>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div class="ion-padding safe-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
<div class="safe-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
<!-- "Time" selector. -->
<ion-select class="core-button-select ion-text-start" [title]="'core.show' | translate" [(ngModel)]="selectedFilter"
(ngModelChange)="selectedChanged()" interface="popover">
<core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged($event)">
<ion-select-option value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
</ion-select-option>
@ -66,7 +65,7 @@
<ion-select-option value="hidden" *ngIf="showFilters.hidden != 'hidden'" [disabled]="showFilters.hidden == 'disabled'">
{{ 'addon.block_myoverview.hiddencourses' | translate }}
</ion-select-option>
</ion-select>
</core-combobox>
</div>
<!-- Filter courses. -->

View File

@ -414,8 +414,11 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
/**
* The selected courses filter have changed.
*
* @param filter New filter
*/
selectedChanged(): void {
selectedChanged(filter: string): void {
this.selectedFilter = filter;
this.setCourseFilter(this.selectedFilter);
}

View File

@ -10,9 +10,8 @@
</core-context-menu>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<div class="ion-padding safe-padding-horizontal">
<ion-select class="ion-text-start core-button-select" [(ngModel)]="filter" (ngModelChange)="switchFilter()"
interface="popover">
<div class="safe-padding-horizontal">
<core-combobox [selection]="filter" (onChange)="switchFilter($event)">
<ion-select-option value="all">{{ 'core.all' | translate }}</ion-select-option>
<ion-select-option value="overdue">{{ 'addon.block_timeline.overdue' | translate }}</ion-select-option>
<ion-select-option disabled value="disabled">{{ 'addon.block_timeline.duedate' | translate }}</ion-select-option>
@ -20,7 +19,7 @@
<ion-select-option value="next30days">{{ 'addon.block_timeline.next30days' | translate }}</ion-select-option>
<ion-select-option value="next3months">{{ 'addon.block_timeline.next3months' | translate }}</ion-select-option>
<ion-select-option value="next6months">{{ 'addon.block_timeline.next6months' | translate }}</ion-select-option>
</ion-select>
</core-combobox>
</div>
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center">
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"

View File

@ -72,7 +72,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
this.currentSite = CoreSites.getCurrentSite();
this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
this.switchFilter();
this.switchFilter(this.filter);
this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
@ -183,8 +183,11 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
/**
* Change timeline filter being viewed.
*
* @param filter New filter.
*/
switchFilter(): void {
switchFilter(filter: string): void {
this.filter = filter;
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
switch (this.filter) {

View File

@ -67,15 +67,15 @@
<core-empty-box *ngIf="discussions.empty" icon="chatbubbles" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
</core-empty-box>
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap addon-forum-sorting-select">
<ion-button *ngIf="sortingAvailable" id="addon-mod-forum-sort-order-button"
class="core-button-select button-no-uppercase"
aria-haspopup="true" aria-controls="addon-mod-forum-sort-order-selector"
[attr.aria-label]="('core.sort' | translate)"
(click)="showSortOrderSelector()">
<span class="core-button-select-text">{{ selectedSortOrder.label | translate }}</span>
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
</ion-button>
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
<core-combobox
[modalOptions]="sortOrderSelectorModalOptions"
listboxId="addon-mod-forum-sort-selector"
[label]="('core.sort' | translate)"
(onChange)="setSortOrder($event)"
[selection]="selectedSortOrder.label | translate"
interface="modal">
</core-combobox>
</div>
<ion-item *ngFor="let discussion of discussions.items"

View File

@ -2,20 +2,6 @@
:host {
.addon-forum-sorting-select {
display: flex;
.core-button-select {
flex: 1;
}
.core-button-select-text {
overflow: hidden;
text-overflow: ellipsis;
}
}
.addon-forum-star {
color: var(--core-color);
}

View File

@ -15,6 +15,8 @@
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { IonContent } from '@ionic/angular';
import { ModalOptions } from '@ionic/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import {
AddonModForum,
@ -26,7 +28,7 @@ import {
AddonModForumReplyDiscussionData,
} from '@addons/mod/forum/services/forum';
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/forum-offline';
import { ModalController, PopoverController, Translate } from '@singletons';
import { PopoverController, Translate } from '@singletons';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
import { CoreGroups, CoreGroupsProvider } from '@services/groups';
@ -80,18 +82,20 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
sortOrders: AddonModForumSortOrder[] = [];
selectedSortOrder: AddonModForumSortOrder | null = null;
canPin = false;
trackPosts = false;
hasOfflineRatings = false;
sortOrderSelectorModalOptions: ModalOptions = {
component: AddonModForumSortOrderSelectorComponent,
};
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
protected page = 0;
trackPosts = false;
protected usesGroups = false;
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
protected replyObserver?: CoreEventObserver;
protected newDiscObserver?: CoreEventObserver;
protected viewDiscObserver?: CoreEventObserver;
protected changeDiscObserver?: CoreEventObserver;
hasOfflineRatings = false;
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
@ -117,6 +121,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
this.sortingAvailable = AddonModForum.isDiscussionListSortingAvailable();
this.sortOrders = AddonModForum.getAvailableSortOrders();
this.sortOrderSelectorModalOptions.componentProps = {
sortOrders: this.sortOrders,
};
await super.ngOnInit();
// Refresh data if this forum discussion is synchronized from discussions list.
@ -516,6 +524,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
const value = await getSortOrder();
this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
}
/**
@ -621,31 +630,18 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
}
/**
* Display the sort order selector modal.
* Changes the sort order.
*
* @param sortOrder Sort order new data.
*/
async showSortOrderSelector(): Promise<void> {
if (!this.sortingAvailable) {
return;
}
const modal = await ModalController.create({
component: AddonModForumSortOrderSelectorComponent,
componentProps: {
sortOrders: this.sortOrders,
selected: this.selectedSortOrder!.value,
},
});
await modal.present();
const result = await modal.onDidDismiss<AddonModForumSortOrder>();
if (result.data && result.data.value != this.selectedSortOrder?.value) {
this.selectedSortOrder = result.data;
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
if (sortOrder.value != this.selectedSortOrder?.value) {
this.selectedSortOrder = sortOrder;
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
this.page = 0;
try {
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, result.data.value.toFixed(0));
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
await this.showLoadingAndFetch();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error updating preference.');
@ -653,6 +649,17 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
}
}
/**
* Display the sort order selector modal.
*/
async showSortOrderSelector(): Promise<void> {
const modalData = await CoreDomUtils.openModal<AddonModForumSortOrder>(this.sortOrderSelectorModalOptions);
if (modalData) {
this.setSortOrder(modalData);
}
}
/**
* Show the context menu.
*

View File

@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ 'core.sort' | translate }}</ion-title>
<ion-title id="addon-mod-forum-sort-order-label">{{ 'core.sort' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only" aria-hidden="true"></ion-icon>
@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list id="addon-mod-forum-sort-selector" role="menu" aria-labelledby="addon-mod-forum-sort-order-button">
<ion-list id="addon-mod-forum-sort-selector" role="listbox" aria-labelledby="addon-mod-forum-sort-order-label">
<ng-container *ngFor="let sortOrder of sortOrders">
<ion-item class="ion-text-wrap" detail="false" role="combobox"
[attr.aria-current]="selected == sortOrder.value ? 'page' : 'false'" [attr.aria-label]="sortOrder.label | translate"

View File

@ -35,13 +35,11 @@
<ion-label><h2>{{user!.fullname}}</h2></ion-label>
</ion-item>
<div class="ion-padding">
<ion-select [(ngModel)]="type" (ngModelChange)="typeChanged()" interface="popover" class="core-button-select">
<ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option>
<ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option>
<ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
</ion-select>
</div>
<core-combobox [selection]="type" (onChange)="typeChanged($event)">
<ion-select-option value="site">{{ 'addon.notes.sitenotes' | translate }}</ion-select-option>
<ion-select-option value="course">{{ 'addon.notes.coursenotes' | translate }}</ion-select-option>
<ion-select-option value="personal">{{ 'addon.notes.personalnotes' | translate }}</ion-select-option>
</core-combobox>
<ion-card class="core-warning-card" *ngIf="hasOffline">
<ion-item>

View File

@ -157,8 +157,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
/**
* Function called when the type has changed.
*
* @param type New type.
*/
async typeChanged(): Promise<void> {
async typeChanged(type: string): Promise<void> {
this.type = type;
this.notesLoaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING;
@ -199,8 +202,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
this.refreshNotes(false);
} else if (result.data.type && result.data.type != this.type) {
this.type = result.data.type;
this.typeChanged();
this.typeChanged(result.data.type);
}
}
}

View File

@ -41,13 +41,12 @@
</ion-card>
<!-- Show processor selector. -->
<ion-select *ngIf="preferences && preferences.processors && preferences.processors.length > 0"
[ngModel]="currentProcessor!.name" (ngModelChange)="changeProcessor($event)" interface="action-sheet"
class="core-button-select">
<core-combobox *ngIf="preferences && preferences.processors && preferences.processors.length > 0"
[selection]="currentProcessor!.name" (onChange)="changeProcessor($event)">
<ion-select-option *ngFor="let processor of preferences.processors" [value]="processor.name">
{{ processor.displayname }}
</ion-select-option>
</ion-select>
</core-combobox>
<ion-card list *ngFor="let component of components" class="ion-margin-top">
<ion-item-divider class="ion-text-wrap">

View File

@ -14,12 +14,10 @@
<core-loading [hideUntil]="filesLoaded" *ngIf="showPrivateFiles || showSiteFiles">
<!-- Allow selecting the files to see: private or site. -->
<div class="ion-padding" *ngIf="showPrivateFiles && showSiteFiles && !path">
<ion-select [(ngModel)]="root" (ngModelChange)="rootChanged()" interface="popover" class="core-button-select">
<ion-select-option value="my">{{ 'addon.privatefiles.privatefiles' | translate }}</ion-select-option>
<ion-select-option value="site">{{ 'addon.privatefiles.sitefiles' | translate }}</ion-select-option>
</ion-select>
</div>
<core-combobox [selection]="root" (onChange)="rootChanged($event)" *ngIf="showPrivateFiles && showSiteFiles && !path">
<ion-select-option value="my">{{ 'addon.privatefiles.privatefiles' | translate }}</ion-select-option>
<ion-select-option value="site">{{ 'addon.privatefiles.sitefiles' | translate }}</ion-select-option>
</core-combobox>
<!-- Display info about space used and space left. -->
<ion-card class="core-info-card" *ngIf="userQuota && filesInfo && filesInfo.filecount > 0">

View File

@ -99,7 +99,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
}
if (this.root) {
this.rootChanged();
this.rootChanged(this.root);
} else {
this.filesLoaded = true;
}
@ -127,8 +127,12 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
/**
* Function called when the root has changed.
*
* @param root New root.
*/
rootChanged(): void {
rootChanged(root: 'my' | 'site'): void {
this.root = root;
this.filesLoaded = false;
this.component = this.root == 'my' ? AddonPrivateFilesProvider.PRIVATE_FILES_COMPONENT :
AddonPrivateFilesProvider.SITE_FILES_COMPONENT;

View File

@ -0,0 +1,129 @@
@import "~theme/globals";
:host {
ion-select,
ion-button {
--icon-margin: 0 8px;
--background: var(--core-combobox-background);
--background-hover: #000000;
--background-activated: #000000;
--background-focused: #000000;
--background-hover-opacity: .04;
--color: var(--core-combobox-color);
--color-activated: var(--core-combobox-color);
--color-focused: currentcolor;
--color-hover: currentcolor;
--padding-top: 10px;
--padding-end: 10px;
--padding-bottom: 10px;
--padding-start: 16px;
}
}
:host-context(.md) {
--background-activated-opacity: 0;
--background-focused-opacity: .12;
}
:host-context(.ios) {
--background-activated-opacity: .12;
--background-focused-opacity: .15;
}
ion-select,
ion-button {
background: var(--background);
color: var(--color);
text-overflow: ellipsis;
white-space: nowrap;
min-height: 25px;
overflow: hidden;
margin: 8px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
}
ion-select {
&::part(icon) {
margin: var(--icon-margin);
}
&::after {
@include button-state();
transition: var(--transition);
z-index: -1;
}
&:hover::after {
color: var(--color-hover);
background: var(--background-hover);
opacity: var(--background-hover-opacity);
}
&:focus::after,
&:focus-within::after {
color: var(--color-focused);
background: var(--background-focused);
opacity: var(--background-focused-opacity);
}
}
ion-button {
--border-radius: 0;
flex: 1;
min-height: 45px;
&::part(native) {
text-transform: none;
font-weight: 400;
font-size: 16px;
}
.select-text {
margin-inline-end: auto;
overflow: hidden;
text-overflow: ellipsis;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
&.ion-activated {
--color: var(--color-activated);
}
ion-icon {
margin: var(--icon-margin);
}
.select-icon {
margin: var(--icon-margin);
width: 19px;
height: 19px;
position: relative;
opacity: 0.33;
.select-icon-inner {
left: 5px;
top: 50%;
margin-top: -2px;
position: absolute;
width: 0px;
height: 0px;
color: currentcolor;
pointer-events: none;
border-top: 5px solid;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
}
}

View File

@ -0,0 +1,77 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { Translate } from '@singletons';
import { ModalOptions } from '@ionic/core';
import { CoreDomUtils } from '@services/utils/dom';
/**
* Component that show a combo select button (combobox).
*
* @description
*
* Example using modal:
*
* <core-combobox interface="modal" (onChange)="selectedChanged($event)" [modalOptions]="modalOptions"
* icon="fas-folder" [label]="'core.course.section' | translate">
* <span slot="text">selection</span>
* </core-combobox>
*
* Example using popover:
*
* <core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged()">
* <ion-select-option value="1">1</ion-select-option>
* </core-combobox>
*/
@Component({
selector: 'core-combobox',
templateUrl: 'core-combobox.html',
styleUrls: ['combobox.scss'],
encapsulation: ViewEncapsulation.ShadowDom,
})
export class CoreComboboxComponent {
@Input() interface: 'popover' | 'modal' = 'popover';
@Input() label = Translate.instant('core.show'); // Aria label.
@Input() disabled = false;
@Input() selection = '';
@Output() onChange = new EventEmitter<unknown>(); // Will emit an event the value changed.
// Additional options when interface modal is selected.
@Input() icon?: string; // Icon for modal interface.
@Input() protected modalOptions?: ModalOptions; // Will emit an event the value changed.
@Input() listboxId = '';
expanded = false;
async showModal(): Promise<void> {
if (this.expanded || !this.modalOptions) {
return;
}
this.expanded = true;
if (this.listboxId) {
this.modalOptions.id = this.listboxId;
}
const data = await CoreDomUtils.openModal(this.modalOptions);
this.expanded = false;
if (data) {
this.onChange.emit(data);
}
}
}

View File

@ -0,0 +1,33 @@
<ion-select
*ngIf="interface != 'modal'"
class="ion-text-start"
[(ngModel)]="selection"
(ngModelChange)="onChange.emit(selection)"
[interface]="interface"
[attr.aria-label]="label + ': ' + selection"
[disabled]="disabled"
>
<ng-content></ng-content>
</ion-select>
<ion-button
*ngIf="interface == 'modal'"
id="addon-mod-forum-sort-order-button"
aria-haspopup="listbox"
aria-controls="addon-mod-forum-sort-order-selector"
[attr.aria-owns]="listboxId"
[attr.aria-expanded]="expanded"
(click)="showModal()"
[disabled]="disabled"
expand="block"
role="combobox"
>
<ion-icon *ngIf="icon" [name]="icon" slot="start" aria-hidden="true"></ion-icon>
<span class="sr-only" *ngIf="label">{{ label }}:</span>
<div class="select-text">
<slot name="text">{{selection}}</slot>
</div>
<div class="select-icon" role="presentation" aria-hidden="true">
<div class="select-icon-inner"></div>
</div>
</ion-button>

View File

@ -55,6 +55,7 @@ import { CoreTabsComponent } from './tabs/tabs';
import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet';
import { CoreTimerComponent } from './timer/timer';
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
import { CoreComboboxComponent } from './combobox/combobox';
@NgModule({
declarations: [
@ -92,6 +93,7 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
CoreTabsOutletComponent,
CoreTimerComponent,
CoreUserAvatarComponent,
CoreComboboxComponent,
],
imports: [
CommonModule,
@ -136,6 +138,7 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
CoreTabsOutletComponent,
CoreTimerComponent,
CoreUserAvatarComponent,
CoreComboboxComponent,
],
})
export class CoreComponentsModule {}

View File

@ -19,18 +19,15 @@
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
class="ion-text-wrap clearfix ion-justify-content-between core-button-selector-row"
[class.core-section-download]="downloadEnabled">
<ion-button class="core-button-select button-no-uppercase" (click)="showSectionSelector()"
aria-haspopup="listbox" id="core-course-section-button" expand="block">
<ion-icon name="fas-folder" slot="start" aria-hidden="true"></ion-icon>
<span class="sr-only" *ngIf="selectedSection">{{ 'core.course.sections' | translate }}:</span>
<span class="core-button-select-text">
<core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
icon="fas-folder" [label]="'core.course.section' | translate" (onChange)="sectionChanged($event)">
<span slot="text">
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
</core-format-text>
<span *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</span>
<ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
</span>
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
</ion-button>
</core-combobox>
<!-- Section download. -->
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
</div>

View File

@ -27,7 +27,7 @@ import {
ViewChild,
ElementRef,
} from '@angular/core';
import { ModalOptions } from '@ionic/core';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
@ -49,7 +49,6 @@ import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { ModalController } from '@singletons';
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
/**
@ -104,6 +103,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
hasSeveralSections?: boolean;
imageThumb?: string;
progress?: number;
sectionSelectorModalOptions: ModalOptions = {
component: CoreCourseSectionSelectorComponent,
};
protected sectionStatusObserver?: CoreEventObserver;
protected selectTabObserver?: CoreEventObserver;
@ -122,6 +124,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* Component being initialized.
*/
ngOnInit(): void {
this.sectionSelectorModalOptions.componentProps = {
course: this.course,
sections: this.sections,
};
// Listen for section status changes.
this.sectionStatusObserver = CoreEvents.on(
CoreEvents.SECTION_STATUS_CHANGED,
@ -194,6 +202,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
if (changes.sections && this.sections) {
this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
this.treatSections(this.sections);
}
@ -342,21 +351,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.sectionSelectorExpanded = true;
const modal = await ModalController.create({
component: CoreCourseSectionSelectorComponent,
componentProps: {
course: this.course,
sections: this.sections,
selected: this.selectedSection,
},
});
await modal.present();
const result = await modal.onWillDismiss();
const data = await CoreDomUtils.openModal<CoreCourseSection>(this.sectionSelectorModalOptions);
this.sectionSelectorExpanded = false;
if (result?.data) {
this.sectionChanged(result?.data);
if (data) {
this.sectionChanged(data);
}
}
@ -368,6 +367,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
sectionChanged(newSection: CoreCourseSection): void {
const previousValue = this.selectedSection;
this.selectedSection = newSection;
this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection;
this.data.section = this.selectedSection;
if (newSection.id != this.allSectionsId) {

View File

@ -1,6 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ 'core.course.sections' | translate }}</ion-title>
<ion-title id="core-course-section-selector-label">{{ 'core.course.sections' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list id="core-course-section-selector">
<ion-list id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
<ng-container *ngFor="let section of sections">
<ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap"
(click)="selectSection(section)" [attr.aria-current]="selected?.id == section.id ? 'page' : 'false'"

View File

@ -29,8 +29,9 @@
"nocontentavailable": "No content available at the moment.",
"overriddennotice": "Your final grade from this activity was manually adjusted.",
"refreshcourse": "Refresh course",
"section": "Section",
"sections": "Sections",
"useactivityonbrowser": "You can still use it using your device's web browser.",
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
}
}

View File

@ -18,12 +18,11 @@
searchArea="CoreTag"></core-search-box>
</ion-col>
<ion-col size="12" size-sm="6" *ngIf="collections && collections.length > 1">
<ion-select class="core-button-select ion-text-start" [(ngModel)]="collectionId" (ngModelChange)="searchTags(query)"
[disabled]="searching" interface="popover">
<core-combobox [selection]="collectionId" (onChange)="searchTags($event)" [disabled]="searching">
<ion-select-option [value]="0">{{ 'core.tag.inalltagcoll' | translate }}</ion-select-option>
<ion-select-option *ngFor="let collection of collections" [value]="collection.id">
{{ collection.name }}</ion-select-option>
</ion-select>
</core-combobox>
</ion-col>
</ion-row>
</ion-grid>

View File

@ -15,7 +15,7 @@
import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { IonContent } from '@ionic/angular';
import { AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core';
import { ModalOptions, AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core';
import { Md5 } from 'ts-md5';
import { CoreApp } from '@services/app';
@ -1681,6 +1681,26 @@ export class CoreDomUtilsProvider {
});
}
/**
* Opens a Modal.
*
* @param opts Modal Options.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async openModal<T = any>(
opts: ModalOptions,
): Promise<T | undefined> {
const modal = await ModalController.create(opts);
await modal.present();
const result = await modal.onWillDismiss<T>();
if (result?.data) {
return result?.data;
}
}
/**
* View an image in a modal.
*

View File

@ -382,62 +382,6 @@ ion-select::part(text) {
white-space: normal;
}
ion-select.core-button-select,
.core-button-select {
--background: var(--core-button-select-background);
background: var(--background);
--color: var(--ion-color-primary);
color: var(--color);
text-overflow: ellipsis;
white-space: nowrap;
min-height: 45px;
overflow: hidden;
margin: 8px;
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
&::part(icon) {
margin: 0 8px;
}
.core-button-select-text {
margin-inline-end: auto;
}
&.ion-activated {
--color: var(--ion-color-primary-contrast);
}
}
ion-button.core-button-select {
--border-radius: 0;
&::part(native) {
text-transform: none;
font-weight: 400;
font-size: 16px;
}
ion-icon {
margin: 0 8px;
}
.select-icon {
width: 19px;
height: 19px;
position: relative;
opacity: 0.33;
.select-icon-inner {
left: 5px;
top: 50%;
margin-top: -2px;
position: absolute;
width: 0px;
height: 0px;
color: currentcolor;
pointer-events: none;
border-top: 5px solid;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
}
}
// File uploader.
.action-sheet-button input.core-fileuploader-file-handler-input {
position: absolute;

View File

@ -67,7 +67,7 @@
--color: var(--white);
}
--core-button-select-background: var(--custom-button-select-background, #3a3a3a);
--core-combobox-background: var(--custom-combobox-background, #3a3a3a);
--core-login-background: var(--custom-login-background, #3a3a3a);
--core-login-text-color: var(--custom-login-text-color, white);

View File

@ -158,7 +158,8 @@
--color: inherit;
}
--core-button-select-background: var(--custom-button-select-background, var(--ion-color-primary-contrast));
--core-combobox-background: var(--custom-combobox-background, var(--ion-item-background));
--core-combobox-color: var(--custom-combobox-color, var(--ion-color-primary));
--selected-item-color: var(--custom-selected-item-color, var(--core-color));
--selected-item-border-width: var(--custom-selected-item-border-width, 5px);