Skip to content

Commit

Permalink
feat: carousel overflow
Browse files Browse the repository at this point in the history
  • Loading branch information
Jakub Freisler committed Sep 5, 2021
1 parent 35bb527 commit 96e06af
Show file tree
Hide file tree
Showing 12 changed files with 453 additions and 71 deletions.
18 changes: 16 additions & 2 deletions packages/core/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
* To be changed together with `className` core variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
$class-name: 'frs-tc' !default;
$frs-tc-class-name: 'frs-tc' !default;
/**
* To be changed together with `classNameOverflow` core variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
$frs-tc-class-name-overflow: 'frs-tc--o' !default;
/**
* To be changed together with `itemClassName` core variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
$item-class-name: 'frs-tc-item' !default;
$frs-tc-item-class-name: 'frs-tc-item' !default;

.#{$class-name} {
display: flex;
Expand All @@ -34,6 +39,15 @@ $item-class-name: 'frs-tc-item' !default;
}
}

.#{$class-name-overflow} {
&::before,
&::after {
content: '';
}
}

.#{$class-name-overflow}::before,
.#{$class-name-overflow}::after,
.#{$item-class-name} {
flex: 0 0 100%;
overflow: hidden;
Expand Down
128 changes: 123 additions & 5 deletions packages/core/src/index.spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ afterEach(() => {
const defaultConfiguration = {
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
};

describe('::defaultConfig::get', () => {
Expand Down Expand Up @@ -69,9 +71,11 @@ describe('constructor', () => {
expect(carousel.config).toEqual({
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
});
});
});
Expand All @@ -84,9 +88,11 @@ describe('constructor', () => {
expect(carousel.config).toEqual({
active: 1,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: [],
overflow: true,
});
config = undefined;
});
Expand Down Expand Up @@ -146,7 +152,7 @@ describe('init', () => {

it('should add classnames from the config', () => {
carousel.init();
expect(element.className).toBe('something frs-hide-scroll');
expect(element.className).toBe('something frs-hide-scroll frs-tc--o');
});

it('should add item classnames from the config to the items', () => {
Expand Down Expand Up @@ -174,13 +180,25 @@ describe('init', () => {
expect(carousel.config).toEqual({
active: 2,
className: 'something',
classNameOverflow: 'frs-tc--o',
hideScrollClassName: 'frs-hide-scroll',
itemClassName: 'frs-tc-item',
items: carousel.config.items,
overflow: true,
});
config = undefined;
});
});

describe('when `config.overflow` = false', () => {
beforeEach(() => carousel.config.overflow = false);

it('shouldn`t add overflow css class to the carouselElement', () => {
carousel.init();

expect(carousel.carouselElement.className).toBe('frs-tc frs-hide-scroll');
});
});
});

describe('destroy', () => {
Expand Down Expand Up @@ -210,24 +228,56 @@ describe('goTo', () => {
carousel.goTo(1);
expect(element.scrollLeft).toBe(150);
});

describe('when config.overflow = false', () => {
it('should move carousel to the correct position', () => {
carousel.config.overflow = false;
carousel.goTo(2);
expect(element.scrollLeft).toBe(300);
});
});

describe('when value is bigger than items count', () => {
it('should handle the overflow gracefully and move carousel to the correct position', () => {
carousel.goTo(4);
expect(element.scrollLeft).toBe(150);
});

describe('when config.overflow = false', () => {
it('should stay at the last item', () => {
carousel.config.overflow = false;
carousel.goTo(4);
expect(element.scrollLeft).toBe(300);
});
});
});

describe('when value is < 0', () => {
it('should count items starting from the end', () => {
carousel.goTo(-2);
expect(element.scrollLeft).toBe(150);
});

describe('when config.overflow = false', () => {
it('should stay at the first item', () => {
carousel.config.overflow = false;
carousel.goTo(-2);
expect(element.scrollLeft).toBe(0);
});
});
});

describe('when value is smaller than -items.length', () => {
it('should repeat the counting with value + items.length until it gets a valid item number', () => {
carousel.goTo(-20); // 20 % 3 == 2
expect(element.scrollLeft).toBe(150);
describe('when value is smaller than -items.length', () => {
it('should repeat the counting with value + items.length until it gets a valid item number', () => {
carousel.goTo(-20); // 20 % 3 == 2
expect(element.scrollLeft).toBe(150);
});

describe('when config.overflow = false', () => {
it('should stay at the first item', () => {
carousel.config.overflow = false;
carousel.goTo(-20);
expect(element.scrollLeft).toBe(0);
});
});
});
Expand Down Expand Up @@ -274,6 +324,74 @@ describe('resetActive', () => {
});
});

describe('handleOverflow', () => {
let goToSpy: jest.SpyInstance;
beforeEach(() => {
initializeCarousel();
carousel.init();
goToSpy = jest.spyOn(carousel, 'goTo').mockReturnValue(carousel);
});
afterEach(() => goToSpy.mockRestore());

describe('when config.overflow = false', () => {
beforeEach(() => carousel.config.overflow = false);

it('shouldn\'t call goTo', () => {
carousel.handleOverflow();
expect(goToSpy).not.toHaveBeenCalled();
});
});

describe('when config.overflow = true', () => {
describe('when config.items.length === 0', () => {
it('shouldn\'t call goTo', () => {
carousel.config.items = [];
carousel.handleOverflow();
expect(goToSpy).not.toHaveBeenCalled();
});
});

describe('when active === -1', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(-1));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
carousel.handleOverflow();
});

it('should call goTo with this.active', () => {
expect(goToSpy).toHaveBeenCalledWith(-1);
});
});

describe('when active === config.items.length', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(3));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
carousel.handleOverflow();
});

it('should call goTo with this.active', () => {
expect(goToSpy).toHaveBeenCalledWith(carousel.config.items.length);
});
});

describe('when active !== -1 or config.items.length', () => {
beforeAll(() => findXSnapIndexMock.mockReturnValue(2));
afterAll(() => findXSnapIndexMock.mockImplementation(findXSnapIndexActual));
beforeEach(() => {
carousel.resetActive();
carousel.handleOverflow();
});

it('shouldn\'t call goTo', () => {
expect(goToSpy).not.toHaveBeenCalled();
});
});
});
});

describe('findPossibleItems', () => {
describe('when none of the children has itemClassName', () => {
beforeEach(initializeCarousel);
Expand Down
57 changes: 50 additions & 7 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import './index.scss';
import { on, findXSnapIndex, off } from '@frsource/tiny-carousel-utils';
import { on, findXSnapIndex, off, addScrollEndListener } from '@frsource/tiny-carousel-utils';
import type { DeepPartial, OmitFirstItem } from './helpers';

export type PluginDefinition<C extends unknown[] | undefined = undefined> = C extends unknown[]
Expand All @@ -10,12 +10,21 @@ export type PluginDefinition<C extends unknown[] | undefined = undefined> = C ex
install(instance: TinyCarousel): void;
};
export interface Config {
/**
* Index of the item which should be activated during carousel initialization
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
active: number;
/**
* To be changed together with `$class-name` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
className: string;
/**
* To be changed together with `$class-name-overflow` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
classNameOverflow: string;
/**
* To be changed together with `$item-class-name` Sass variable
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
Expand All @@ -26,6 +35,12 @@ export interface Config {
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#styling
*/
hideScrollClassName: string;
/**
* Allows toggling the overflow behavior on or off. Default: true
* More info: https://www.frsource.org/tiny-carousel/api-reference/core/#config
*/
overflow: boolean;

items: HTMLElement[];
}

Expand All @@ -37,15 +52,18 @@ const TINYCAROUSEL_ADDED_CLASS = 'tcACls';
const _defaultConfig: Config = {
active: 0,
className: 'frs-tc',
classNameOverflow: 'frs-tc--o',
itemClassName: 'frs-tc-item',
hideScrollClassName: 'frs-hide-scroll', // default frs-hide-scrollbar classname, for private use only
overflow: true,
items: [],
};

export class TinyCarousel {
public config: Config;
private _active?: number;
private _resetActive: TinyCarousel['resetActive'];
private _handleOverflow: TinyCarousel['handleOverflow'];

static get defaultConfig () {
return Object.assign({}, _defaultConfig);
Expand All @@ -58,6 +76,20 @@ export class TinyCarousel {
constructor(public carouselElement: HTMLElement, config: DeepPartial<Config> = {}) {
this.config = Object.assign({}, _defaultConfig, config);
this._resetActive = this.resetActive.bind(this);
this._handleOverflow = this.handleOverflow.bind(this);
}

// make it public if needed (and document it)
private handleOverflow () {
if (
!this.config.overflow ||
!this.config.items.length
) return;

if (
this.active === -1 ||
this.active === this.config.items.length
) this.goTo(this.active);
}

use<PD extends PluginDefinition>(pluginDefinition: PD, ...args: OmitFirstItem<Parameters<PD['install']>>) {
Expand All @@ -67,12 +99,15 @@ export class TinyCarousel {

init() {
on(this.carouselElement, 'scroll', this._resetActive, { passive: true });
this._handleOverflow = addScrollEndListener(this.carouselElement, this._handleOverflow);

if (!this.config.items.length) this.config.items = this.findPossibleItems();

this.carouselElement.classList.add(this.config.className);
this.carouselElement.classList.add(this.config.hideScrollClassName);

if (this.config.overflow) this.carouselElement.classList.add(this.config.classNameOverflow);

this.config.items.forEach(({ classList, dataset }) => {
if (!classList.contains(this.config.itemClassName)) {
dataset[TINYCAROUSEL_ADDED_CLASS] = '';
Expand All @@ -87,6 +122,7 @@ export class TinyCarousel {

destroy() {
off(this.carouselElement, 'scroll', this._resetActive);
off(this.carouselElement, 'scroll', this._handleOverflow);
return this;
}

Expand All @@ -95,20 +131,27 @@ export class TinyCarousel {
*/
get active () {
if (this._active !== void 0) return this._active;
return this._active = findXSnapIndex(

this._active = findXSnapIndex(
this.carouselElement,
this.config.items,
this.config.overflow
);
return this._active;
}

goTo (n: number) {
const len = this.config.items.length;
if (len) {
// treat negative numbers as counter starting at the end of items array
while (n < 0) n += len;
// treat numbers >= items length as overflow - start counting the rest from the beginning
while (n >= len) n -= len;
if (this.config.overflow) {
// treat negative numbers as counter starting at the end of items array
while (n < 0) n += len;
// treat numbers >= items length as overflow - start counting the rest from the beginning
while (n >= len) n -= len;
} else {
if (n < 0) n = 0;
else if (n >= len) n = len - 1;
}

this.carouselElement.scrollLeft = this.config.items[n].offsetLeft;
}
Expand Down
Loading

0 comments on commit 96e06af

Please sign in to comment.