- 0">
+
diff --git a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts
index edafb09e6..b95d8084d 100644
--- a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts
+++ b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts
@@ -46,6 +46,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
downloadCourseEnabled = false;
downloadCoursesEnabled = false;
+ scrollElementId!: string;
protected prefetchIconsInitialized = false;
protected isDestroyed = false;
@@ -62,6 +63,11 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
* Component being initialized.
*/
async ngOnInit(): Promise {
+ // Generate unique id for scroll element.
+ const scrollId = CoreUtils.getUniqueId('AddonBlockStarredCoursesComponent-Scroll');
+
+ this.scrollElementId = `addon-block-starredcourses-scroll-${scrollId}`;
+
// Refresh the enabled flags if enabled.
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts
index 0ca148450..f1a876dc6 100644
--- a/src/core/components/components.module.ts
+++ b/src/core/components/components.module.ts
@@ -57,6 +57,7 @@ import { CoreTimerComponent } from './timer/timer';
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
import { CoreComboboxComponent } from './combobox/combobox';
import { CoreSpacerComponent } from './spacer/spacer';
+import { CoreHorizontalScrollControlsComponent } from './horizontal-scroll-controls/horizontal-scroll-controls';
@NgModule({
declarations: [
@@ -96,6 +97,7 @@ import { CoreSpacerComponent } from './spacer/spacer';
CoreUserAvatarComponent,
CoreComboboxComponent,
CoreSpacerComponent,
+ CoreHorizontalScrollControlsComponent,
],
imports: [
CommonModule,
@@ -142,6 +144,7 @@ import { CoreSpacerComponent } from './spacer/spacer';
CoreUserAvatarComponent,
CoreComboboxComponent,
CoreSpacerComponent,
+ CoreHorizontalScrollControlsComponent,
],
})
export class CoreComponentsModule {}
diff --git a/src/core/components/horizontal-scroll-controls/core-horizontal-scroll-controls.html b/src/core/components/horizontal-scroll-controls/core-horizontal-scroll-controls.html
new file mode 100644
index 000000000..7f3091787
--- /dev/null
+++ b/src/core/components/horizontal-scroll-controls/core-horizontal-scroll-controls.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.scss b/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.scss
new file mode 100644
index 000000000..e4c64b946
--- /dev/null
+++ b/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.scss
@@ -0,0 +1,10 @@
+:host {
+ --flex-row-direction: row;
+
+ display: flex;
+ flex-direction: var(--flex-row-direction);
+}
+
+:host-context([dir="rtl"]) {
+ --flex-row-direction: row-reverse;
+}
diff --git a/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.ts b/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.ts
new file mode 100644
index 000000000..61964d49b
--- /dev/null
+++ b/src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.ts
@@ -0,0 +1,106 @@
+// (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, Input } from '@angular/core';
+import { Platform } from '@singletons';
+
+const enum ScrollPosition {
+ Start = 'start',
+ End = 'end',
+ Middle = 'middle',
+ Hidden = 'hidden',
+}
+
+@Component({
+ selector: 'core-horizontal-scroll-controls',
+ templateUrl: 'core-horizontal-scroll-controls.html',
+ styleUrls: ['./horizontal-scroll-controls.scss'],
+})
+export class CoreHorizontalScrollControlsComponent {
+
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('aria-controls') targetId?: string;
+
+ scrollPosition: ScrollPosition = ScrollPosition.Hidden;
+
+ /**
+ * Get target element.
+ */
+ private get target(): HTMLElement | null {
+ return this.targetId && document.getElementById(this.targetId) || null;
+ }
+
+ /**
+ * Scroll the target in the given direction.
+ *
+ * @param direction Scroll direction.
+ */
+ scroll(direction: 'forward' | 'backward'): void {
+ if (!this.target) {
+ return;
+ }
+
+ const leftDelta = direction === 'forward' ? this.target.clientWidth : -this.target.clientWidth;
+ const newScrollLeft = Math.max(
+ Math.min(
+ this.target.scrollLeft + leftDelta,
+ this.target.scrollWidth - this.target.clientWidth,
+ ),
+ 0,
+ );
+
+ this.target.scrollBy({
+ left: leftDelta,
+ behavior: 'smooth',
+ });
+
+ this.updateScrollPosition(newScrollLeft);
+ }
+
+ /**
+ * Update the current scroll position.
+ */
+ updateScrollPosition(scrollLeft?: number): void {
+ this.scrollPosition = this.getScrollPosition(scrollLeft);
+ }
+
+ /**
+ * Get the current scroll position.
+ *
+ * @param scrollLeft Scroll left to use for reference in the calculations.
+ * @returns Scroll position.
+ */
+ private getScrollPosition(scrollLeft?: number): ScrollPosition {
+ scrollLeft = scrollLeft ?? this.target?.scrollLeft ?? 0;
+
+ if (!this.target || this.target.scrollWidth <= this.target.clientWidth) {
+ return ScrollPosition.Hidden;
+ }
+
+ if (scrollLeft === 0) {
+ return Platform.isRTL ? ScrollPosition.End : ScrollPosition.Start;
+ }
+
+ if (!Platform.isRTL && this.target.scrollWidth - scrollLeft === this.target.clientWidth) {
+ return ScrollPosition.End;
+ }
+
+ if (Platform.isRTL && this.target.scrollWidth + scrollLeft === this.target.clientWidth) {
+ return ScrollPosition.Start;
+ }
+
+ return ScrollPosition.Middle;
+ }
+
+}
diff --git a/src/core/lang.json b/src/core/lang.json
index bc1cf8e64..93e269478 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -269,6 +269,8 @@
"sorry": "Sorry...",
"sort": "Sort",
"sortby": "Sort by",
+ "scrollbackward": "Scroll backward",
+ "scrollforward": "Scroll forward",
"start": "Start",
"storingfiles": "Storing files",
"strftimedate": "%d %B %Y",
diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss
index b712d1574..6d1a7d2b9 100644
--- a/src/theme/theme.base.scss
+++ b/src/theme/theme.base.scss
@@ -32,6 +32,10 @@
text-transform: none;
}
+.flex { display: flex; }
+.inline-block { display: inline-block; }
+.block { display: block; }
+
.flex-row {
display: flex;
flex-direction: row;