From 255aec5897e4027d68bdddec6b82b5cc883f505e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 13 May 2021 11:30:01 +0200 Subject: [PATCH] MOBILE-3750 a11y: Add horizontal scroll buttons --- scripts/langindex.json | 2 + .../addon-block-recentlyaccessedcourses.html | 33 ++++-- .../recentlyaccessedcourses.ts | 5 + .../addon-block-recentlyaccesseditems.html | 11 +- .../recentlyaccesseditems.ts | 14 +++ .../addon-block-starredcourses.html | 34 ++++-- .../starredcourses/starredcourses.ts | 6 + src/core/components/components.module.ts | 3 + .../core-horizontal-scroll-controls.html | 23 ++++ .../horizontal-scroll-controls.scss | 10 ++ .../horizontal-scroll-controls.ts | 106 ++++++++++++++++++ src/core/lang.json | 2 + src/theme/theme.base.scss | 4 + 13 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 src/core/components/horizontal-scroll-controls/core-horizontal-scroll-controls.html create mode 100644 src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.scss create mode 100644 src/core/components/horizontal-scroll-controls/horizontal-scroll-controls.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 28c9b6d7f..d010f23c4 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2036,6 +2036,8 @@ "core.save": "moodle", "core.savechanges": "assign", "core.scanqr": "local_moodlemobileapp", + "core.scrollbackward": "local_moodlemobileapp", + "core.scrollforward": "local_moodlemobileapp", "core.search": "moodle", "core.searching": "local_moodlemobileapp", "core.searchresults": "moodle", diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html index 0f445b464..fe13ea87b 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html @@ -2,24 +2,33 @@

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

-
- - - - - {{prefetchCoursesData.badge}} - - +
+
+ + + + + {{prefetchCoursesData.badge}} + + +
+ + +
-
+
diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index 3b6d98336..bea1cc104 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -46,6 +46,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom downloadCourseEnabled = false; downloadCoursesEnabled = false; + scrollElementId!: string; protected prefetchIconsInitialized = false; protected isDestroyed = false; @@ -62,6 +63,10 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom * Component being initialized. */ async ngOnInit(): Promise { + // Generate unique id for scroll element. + const scrollId = CoreUtils.getUniqueId('AddonBlockRecentlyAccessedCoursesComponent-Scroll'); + + this.scrollElementId = `addon-block-recentlyaccessedcourses-scroll-${scrollId}`; // Refresh the enabled flags if enabled. this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html index bc9ffadb7..a2e1fde5e 100644 --- a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html @@ -1,8 +1,17 @@

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

+
+ + +
-
+
{ + // Generate unique id for scroll element. + const scrollId = CoreUtils.getUniqueId('AddonBlockRecentlyAccessedItemsComponent-Scroll'); + + this.scrollElementId = `addon-block-recentlyaccesseditems-scroll-${scrollId}`; + + super.ngOnInit(); + } + /** * Perform the invalidate content function. * diff --git a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html index dec6f7412..bd010b101 100644 --- a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html +++ b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html @@ -2,24 +2,34 @@

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

-
- - - - - {{prefetchCoursesData.badge}} - - +
+
+ + + + + {{prefetchCoursesData.badge}} + + +
+ + +
-
+
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;