Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions addon/components/o-s-s/scrollable-bar.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="scrollable-bar-container" id={{this.scrollableBarId}} ...attributes>
{{#if this.isLeftScrollable}}
{{#if (eq this.buttonStyle "icon")}}
<button
class="scroll-to-left-icon"
data-control-name="scrollable-bar-container-scroll-to-left-icon"
{{on "click" this.scrollToLeft}}
>
<OSS::Icon @style={{fa-icon-style "fa-chevron-left"}} @icon={{fa-icon-value "fa-chevron-left"}} />
</button>
{{else}}
<OSS::Button
class="scroll-to-left-btn"
@icon="fa-arrow-left"
@size="sm"
@square={{true}}
data-control-name="scrollable-bar-container-scroll-to-left-btn"
{{on "click" this.scrollToLeft}}
/>
{{/if}}
{{/if}}

<div class={{this.innerContainerStyle}}>
{{yield (hash setupFn=this.observeIntersection teardownFn=this.unobserveIntersection) to="content"}}
</div>

{{#if this.isRightScrollable}}
{{#if (eq this.buttonStyle "icon")}}
<button
class="scroll-to-right-icon"
data-control-name="scrollable-bar-container-scroll-to-right-icon"
{{on "click" this.scrollToRight}}
>
<OSS::Icon @style={{fa-icon-style "fa-chevron-right"}} @icon={{fa-icon-value "fa-chevron-right"}} />
</button>
{{else}}
<OSS::Button
class="scroll-to-right-btn"
@icon="fa-arrow-right"
@size="sm"
@square={{true}}
data-control-name="scrollable-bar-container-scroll-to-right-btn"
{{on "click" this.scrollToRight}}
/>
{{/if}}
{{/if}}
</div>
53 changes: 53 additions & 0 deletions addon/components/o-s-s/scrollable-bar.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { hbs } from 'ember-cli-htmlbars';

export default {
title: 'Components/OSS::ScrollableBar',
component: 'scrollable-bar',
argTypes: {
buttonStyle: {
description: 'Updates the style of scroll buttons. Defaults to "button"',
table: {
type: { summary: 'button | icon' },
defaultValue: { summary: 'undefined' }
},
control: {
type: 'text'
}
}
},
parameters: {
docs: {
description: {
component: 'Container which automatically handle horizontal scroll behavior with left and right buttons'
}
}
}
};

const defaultArgs = {
buttonStyle: undefined
};

const Template = (args) => ({
template: hbs`
<div style="width: 300px; background-color: white; " >
<OSS::ScrollableBar @buttonStyle={{this.buttonStyle}}>
<:content as |callbacks|>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>All</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Replies</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments2</div>
</:content>
</OSS::ScrollableBar>
</div>
`,

context: args
});

export const BasicUsage = Template.bind({});
BasicUsage.args = defaultArgs;
102 changes: 102 additions & 0 deletions addon/components/o-s-s/scrollable-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

interface OSSScrollableBarComponentSignature {
buttonStyle?: 'button' | 'icon';
}

export default class extends Component<OSSScrollableBarComponentSignature> {
@tracked filterEntries: IntersectionObserverEntry[] = [];

scrollableBarId: string = guidFor(this);
declare intersectionObserver: IntersectionObserver;

constructor(owner: unknown, args: OSSScrollableBarComponentSignature) {
super(owner, args);

this.intersectionObserver = new IntersectionObserver(this.intersectionObserverCallback.bind(this), {
root: null,
rootMargin: '0px',
threshold: [0, 1]
});
}

get containerElement(): HTMLElement {
return document.querySelector(`#${this.scrollableBarId} .inner-container`) as HTMLElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If for any reason this.containerElement is undefined, this.scrollWith will break, I'm not sure about as HTMLElement

}

get scrollWidth(): number {
return this.containerElement.clientWidth / 3;
}

get isLeftScrollable(): boolean {
if (this.filterEntries.length === 0 || this.getFilterNodes().length === 0) return false;
const entry = this.filterEntries.find((f) => f.target === this.getFilterNodes()[0]);
return entry !== undefined && entry.intersectionRatio < 1;
}

get isRightScrollable(): boolean {
if (this.filterEntries.length === 0 || this.getFilterNodes().length === 0) return false;
const entry = this.filterEntries.find((f) => f.target === this.getFilterNodes()[this.getFilterNodes().length - 1]);
return entry !== undefined && entry.intersectionRatio < 1;
}

get buttonStyle(): string {
return this.args.buttonStyle === 'icon' ? 'icon' : 'button';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return this.args.buttonStyle === 'icon' ? 'icon' : 'button';
return this.args.buttonStyle ?? 'button';

}

get innerContainerStyle(): string {
const baseStyle = this.args.buttonStyle === 'icon' ? 'inner-container-icon' : 'inner-container-btn';
let innerContainerStyle = 'inner-container';
if (this.isLeftScrollable) {
innerContainerStyle += ` ${baseStyle}--scrollable-left`;
}
if (this.isRightScrollable) {
innerContainerStyle += ` ${baseStyle}--scrollable-right`;
}
return innerContainerStyle;
}

@action
observeIntersection(element: HTMLElement): void {
this.intersectionObserver.observe(element);
if (this.containerElement) {
this.containerElement.scrollLeft = this.containerElement.scrollWidth;
}
}

@action
unobserveIntersection(element: HTMLElement): void {
this.filterEntries = this.filterEntries.filter((e) => e.target !== element);
this.intersectionObserver.unobserve(element);
}

@action
scrollToRight(): void {
this.containerElement.scrollBy({ left: this.scrollWidth, behavior: 'smooth' });
}

@action
scrollToLeft(): void {
this.containerElement.scrollBy({ left: -this.scrollWidth, behavior: 'smooth' });
}

private getFilterNodes(): HTMLElement[] {
return Array.from(this.containerElement?.childNodes || []).filter(
(node): node is HTMLElement => node instanceof HTMLElement
);
}

private intersectionObserverCallback(entries: IntersectionObserverEntry[]): void {
entries.forEach((entry) => {
if (entry.rootBounds) {
this.filterEntries = [...this.filterEntries.filter((e) => e.target !== entry.target), entry];
} else {
this.filterEntries = [...this.filterEntries.filter((e) => e.target !== entry.target)];
}
});
}
}
1 change: 1 addition & 0 deletions app/components/o-s-s/scrollable-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@upfluence/oss-components/components/o-s-s/scrollable-bar';
120 changes: 120 additions & 0 deletions app/styles/organisms/scrollable-bar.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
.scrollable-bar-container {
display: flex;
overflow: hidden;
position: relative;
flex: 1;

.scroll-to-left-btn,
.scroll-to-left-icon {
position: absolute;
left: 0;
z-index: 1;
}

.scroll-to-left-icon,
.scroll-to-right-icon {
display: flex;
justify-content: center;
align-items: center;
background-color: white;
height: 100%;
width: 12px;
border: none;

&:hover {
color: var(--color-gray-500);
}
}

.scroll-to-right-btn,
.scroll-to-right-icon {
position: absolute;
right: 0;
z-index: 1;
}

.scroll-to-left-btn,
.scroll-to-right-btn {
top: var(--spacing-px-9);
visibility: hidden;
opacity: 0;
transition: all 300ms ease-in-out;
}

.scroll-to-left-icon,
.scroll-to-right-icon {
visibility: visible;
opacity: 1;
}

&:hover {
.scroll-to-left-btn,
.scroll-to-right-btn {
visibility: visible;
opacity: 1;
}
}

.inner-container {
display: inline-flex;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-px-9);
overflow: auto;
padding: var(--spacing-px-9) var(--spacing-px-3) var(--spacing-px-9) var(--spacing-px-3);

-ms-overflow-style: none;
scrollbar-width: none;

&::-webkit-scrollbar {
display: none;
}
}

.inner-container-btn {
&--scrollable-right::after {
position: absolute;
content: ' ';
right: 0;
height: 100%;
width: 36px;
background: linear-gradient(180deg, rgba(249, 250, 251, 0.11323) 0%, var(--color-gray-50) 100%);
transform: rotate(-90deg);
}

&--scrollable-left::before {
position: absolute;
content: ' ';
left: 0;
height: 100%;
width: 36px;
background: linear-gradient(180deg, rgba(249, 250, 251, 0.11323) 0%, var(--color-gray-50) 100%);
transform: rotate(90deg);
}
}

.inner-container-icon {
&--scrollable-right::after {
position: absolute;
content: ' ';
right: 12px;
height: 100%;
width: 36px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--color-white) 100%);

transform: rotate(-90deg);
}

&--scrollable-left::before {
position: absolute;
content: ' ';
left: 12px;
height: 100%;
width: 36px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--color-white) 100%);

transform: rotate(90deg);
}
}
}
1 change: 1 addition & 0 deletions app/styles/oss-components.less
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
@import 'organisms/modal-dialog';
@import 'organisms/split-modal';
@import 'organisms/content-panel';
@import 'organisms/scrollable-bar';
@import 'organisms/scrollable-panel';
@import 'organisms/sidebar';
@import 'organisms/access-panel';
Expand Down
44 changes: 44 additions & 0 deletions tests/dummy/app/templates/visual.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -903,4 +903,48 @@
<OSS::Pill @label="Pill" @selected={{this.selectedPill}} @disabled={{true}} />
</div>
</div>

<div
class="fx-col fx-1 background-color-white border border-color-default border-radius-md padding-px-12 fx-gap-px-12"
>
<div class="font-size-md font-weight-semibold">
Scrollable bar
</div>
<div class="fx-row fx-gap-px-24 fx-xalign-start">
<div class="width-pc-20">
With default "button" style:
<div class="fx-row fx-gap-px-24 fx-xalign-center width-pc-100">
<OSS::ScrollableBar>
<:content as |callbacks|>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>All</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Replies</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments2</div>
</:content>
</OSS::ScrollableBar>
</div>
</div>
<div class="width-pc-20">
With "icon" style:
<div class="fx-row fx-gap-px-24 fx-xalign-center width-pc-100">
<OSS::ScrollableBar @buttonStyle="icon">
<:content as |callbacks|>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>All</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Replies</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Applications2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Emails2</div>
<div {{did-insert callbacks.setupFn}} {{will-destroy callbacks.teardownFn}}>Payments2</div>
</:content>
</OSS::ScrollableBar>
</div>
</div>
</div>
</div>
</div>
Loading
Loading