Skip to content

Commit 6ac63ee

Browse files
authored
Merge pull request #2861 from Brain-up/extract-horizontal-scroll-component
Extract reusable UiHorizontalScroll component
2 parents ee67885 + 2841667 commit 6ac63ee

4 files changed

Lines changed: 398 additions & 132 deletions

File tree

frontend/app/components/group-navigation/index.gts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import '../../styles/horizontal-scroll.css';
21
import Component from '@glimmer/component';
2+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
33
import { array } from '@ember/helper';
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
45
import UiTabButton from 'brn/components/ui/tab-button';
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
import UiHorizontalScroll from 'brn/components/ui/horizontal-scroll';
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
59
import autofitText from 'brn/modifiers/autofit-text';
610

711
interface SeriesItem {
@@ -30,21 +34,19 @@ export default class GroupNavigationComponent extends Component<GroupNavigationS
3034
}
3135

3236
<template>
33-
<div class="hs-container" ...attributes>
34-
<ul class="hs full no-scrollbar">
35-
{{#each this.sortedSeries as |series|}}
36-
<li class="item">
37-
<UiTabButton
38-
data-test-active-link={{series.name}}
39-
@route="group.series"
40-
@models={{array series.id}}
41-
@title={{series.name}}
42-
@tooltip={{series.description}}
43-
{{autofitText series.name}}
44-
/>
45-
</li>
46-
{{/each}}
47-
</ul>
48-
</div>
37+
<UiHorizontalScroll ...attributes>
38+
{{#each this.sortedSeries as |series|}}
39+
<li class="item">
40+
<UiTabButton
41+
data-test-active-link={{series.name}}
42+
@route="group.series"
43+
@models={{array series.id}}
44+
@title={{series.name}}
45+
@tooltip={{series.description}}
46+
{{autofitText series.name}}
47+
/>
48+
</li>
49+
{{/each}}
50+
</UiHorizontalScroll>
4951
</template>
5052
}
Lines changed: 18 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
1-
import '../../styles/horizontal-scroll.css';
21
import Component from '@glimmer/component';
3-
import { trackedRef } from 'ember-ref-bucket';
4-
import { action } from '@ember/object';
5-
import { debounce, cancel } from '@ember/runloop';
6-
import { tracked } from '@glimmer/tracking';
7-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8-
import { on } from '@ember/modifier';
9-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
10-
import { fn } from '@ember/helper';
112
// eslint-disable-next-line @typescript-eslint/no-unused-vars
123
import { array } from '@ember/helper';
134
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14-
import htmlSafe from 'brn/helpers/html-safe';
15-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
16-
import createRef from 'ember-ref-bucket/modifiers/create-ref';
17-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
185
import UiTabButton from 'brn/components/ui/tab-button';
196
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
import UiHorizontalScroll from 'brn/components/ui/horizontal-scroll';
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
209
import autofitText from 'brn/modifiers/autofit-text';
2110

2211
interface SubgroupItem {
@@ -34,10 +23,6 @@ interface SubgroupNavigationSignature {
3423
}
3524

3625
export default class SubgroupNavigation extends Component<SubgroupNavigationSignature> {
37-
@trackedRef('container') container!: HTMLUListElement;
38-
@tracked scrollIteration = 0;
39-
debounceTimer: ReturnType<typeof debounce> | undefined = undefined;
40-
4126
get sortedExercises(): SubgroupItem[] {
4227
const group = this.args.group as SubgroupItem[] | undefined;
4328
if (!group || !Array.isArray(group)) return [];
@@ -48,104 +33,22 @@ export default class SubgroupNavigation extends Component<SubgroupNavigationSign
4833
});
4934
}
5035

51-
get showLeftScrollButton() {
52-
return this.hasScrollAtAll && this.container?.scrollLeft > 0;
53-
}
54-
get showRightScrollButton() {
55-
if (!this.hasScrollAtAll) {
56-
return false;
57-
}
58-
const scrollSize = this.container?.offsetWidth + this.container?.scrollLeft;
59-
const result = scrollSize <= this.container?.scrollWidth;
60-
return result;
61-
}
62-
get hasScrollAtAll() {
63-
this.scrollIteration; // track iteration
64-
if (!this.container) {
65-
return false;
66-
}
67-
return this.container?.scrollWidth > this.container?.offsetWidth;
68-
}
69-
70-
@action scroll(direction: 'right' | 'left') {
71-
const position = this.container.scrollLeft;
72-
const offset = 150;
73-
const newPosition =
74-
direction === 'right' ? position + offset : position - offset;
75-
this.container.scrollTo({
76-
top: 0,
77-
left: newPosition,
78-
behavior: 'smooth',
79-
});
80-
}
81-
82-
@action onScroll() {
83-
cancel(this.debounceTimer);
84-
this.debounceTimer = debounce(this, this.updateScroll, 100);
85-
}
86-
87-
updateScroll() {
88-
this.scrollIteration++;
89-
}
90-
91-
willDestroy() {
92-
super.willDestroy();
93-
cancel(this.debounceTimer);
94-
}
95-
9636
<template>
97-
<div class="hs-container" ...attributes>
98-
<div class="full relative overflow-hidden">
99-
{{#if this.showLeftScrollButton}}
100-
<div class="scroll-fade scroll-fade--left"></div>
101-
<button
102-
type="button"
103-
class="scroll-btn bg-purple-primary hover:opacity-75 focus:outline-hidden absolute left-0 z-20 flex items-center justify-center w-8 h-8 text-white rounded-full shadow-md"
104-
105-
{{on "click" (fn this.scroll "left")}}
106-
aria-label="Scroll left"
107-
>
108-
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
109-
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
110-
</svg>
111-
</button>
112-
{{/if}}
113-
<ul
114-
class="hs no-scrollbar"
115-
style={{htmlSafe "scroll-behavior: smooth;"}}
116-
{{createRef "container"}}
117-
{{on "scroll" this.onScroll}}
118-
>
119-
{{#each this.sortedExercises as |exercise|}}
120-
<li class="item">
121-
<UiTabButton
122-
data-test-active-link={{exercise.name}}
123-
class="pl-3 pr-3"
124-
@small={{true}}
125-
@route="group.series.subgroup"
126-
@models={{array exercise.id}}
127-
@title={{exercise.name}}
128-
@tooltip={{exercise.description}}
129-
{{autofitText exercise.name}}
130-
/>
131-
</li>
132-
{{/each}}
133-
</ul>
134-
{{#if this.showRightScrollButton}}
135-
<div class="scroll-fade scroll-fade--right"></div>
136-
<button
137-
type="button"
138-
class="scroll-btn bg-purple-primary hover:opacity-75 focus:outline-hidden absolute right-0 z-20 flex items-center justify-center w-8 h-8 text-white rounded-full shadow-md"
139-
140-
{{on "click" (fn this.scroll "right")}}
141-
aria-label="Scroll right"
142-
>
143-
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
144-
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
145-
</svg>
146-
</button>
147-
{{/if}}
148-
</div>
149-
</div>
37+
<UiHorizontalScroll ...attributes>
38+
{{#each this.sortedExercises as |exercise|}}
39+
<li class="item">
40+
<UiTabButton
41+
data-test-active-link={{exercise.name}}
42+
class="pl-3 pr-3"
43+
@small={{true}}
44+
@route="group.series.subgroup"
45+
@models={{array exercise.id}}
46+
@title={{exercise.name}}
47+
@tooltip={{exercise.description}}
48+
{{autofitText exercise.name}}
49+
/>
50+
</li>
51+
{{/each}}
52+
</UiHorizontalScroll>
15053
</template>
15154
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import '../../../styles/horizontal-scroll.css';
2+
import Component from '@glimmer/component';
3+
import { action } from '@ember/object';
4+
import { debounce, cancel } from '@ember/runloop';
5+
import { tracked } from '@glimmer/tracking';
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
import { on } from '@ember/modifier';
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9+
import { fn } from '@ember/helper';
10+
import { modifier } from 'ember-modifier';
11+
12+
interface UiHorizontalScrollSignature {
13+
Args: {};
14+
Blocks: {
15+
default: [];
16+
};
17+
Element: HTMLDivElement;
18+
}
19+
20+
export default class UiHorizontalScroll extends Component<UiHorizontalScrollSignature> {
21+
@tracked containerElement: HTMLUListElement | null = null;
22+
@tracked scrollIteration = 0;
23+
debounceTimer: ReturnType<typeof debounce> | undefined = undefined;
24+
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
registerContainer = modifier((element: HTMLUListElement) => {
27+
this.containerElement = element;
28+
return () => {
29+
this.containerElement = null;
30+
};
31+
});
32+
33+
get showLeftScrollButton() {
34+
return this.hasScrollAtAll && (this.containerElement?.scrollLeft ?? 0) > 0;
35+
}
36+
37+
get showRightScrollButton() {
38+
if (!this.hasScrollAtAll) {
39+
return false;
40+
}
41+
const el = this.containerElement;
42+
if (!el) return false;
43+
const scrollSize = el.offsetWidth + el.scrollLeft;
44+
return scrollSize < el.scrollWidth;
45+
}
46+
47+
get hasScrollAtAll() {
48+
this.scrollIteration;
49+
if (!this.containerElement) {
50+
return false;
51+
}
52+
return this.containerElement.scrollWidth > this.containerElement.offsetWidth;
53+
}
54+
55+
@action scroll(direction: 'right' | 'left') {
56+
if (!this.containerElement) return;
57+
const position = this.containerElement.scrollLeft;
58+
const offset = 150;
59+
const newPosition =
60+
direction === 'right' ? position + offset : position - offset;
61+
this.containerElement.scrollTo({
62+
top: 0,
63+
left: newPosition,
64+
behavior: 'smooth',
65+
});
66+
}
67+
68+
@action onScroll() {
69+
cancel(this.debounceTimer);
70+
this.debounceTimer = debounce(this, this.updateScroll, 100);
71+
}
72+
73+
updateScroll() {
74+
this.scrollIteration++;
75+
}
76+
77+
willDestroy() {
78+
super.willDestroy();
79+
cancel(this.debounceTimer);
80+
}
81+
82+
<template>
83+
<div class="hs-container" ...attributes>
84+
<div class="full relative overflow-hidden">
85+
{{#if this.showLeftScrollButton}}
86+
<div class="scroll-fade scroll-fade--left"></div>
87+
<button
88+
type="button"
89+
class="scroll-btn bg-purple-primary hover:opacity-75 focus:outline-hidden absolute left-0 z-20 flex items-center justify-center w-8 h-8 text-white rounded-full shadow-md"
90+
{{on "click" (fn this.scroll "left")}}
91+
aria-label="Scroll left"
92+
>
93+
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
94+
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
95+
</svg>
96+
</button>
97+
{{/if}}
98+
<ul
99+
class="hs no-scrollbar"
100+
{{this.registerContainer}}
101+
{{on "scroll" this.onScroll}}
102+
>
103+
{{yield}}
104+
</ul>
105+
{{#if this.showRightScrollButton}}
106+
<div class="scroll-fade scroll-fade--right"></div>
107+
<button
108+
type="button"
109+
class="scroll-btn bg-purple-primary hover:opacity-75 focus:outline-hidden absolute right-0 z-20 flex items-center justify-center w-8 h-8 text-white rounded-full shadow-md"
110+
{{on "click" (fn this.scroll "right")}}
111+
aria-label="Scroll right"
112+
>
113+
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
114+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
115+
</svg>
116+
</button>
117+
{{/if}}
118+
</div>
119+
</div>
120+
</template>
121+
}

0 commit comments

Comments
 (0)