Skip to content

Commit

Permalink
feat(collapse): add horizontal collapse
Browse files Browse the repository at this point in the history
  • Loading branch information
ValentinNelu authored and maxokorokov committed Aug 16, 2022
1 parent f96ec1a commit 98f0527
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 53 deletions.
53 changes: 31 additions & 22 deletions demo/src/app/components/collapse/collapse.module.ts
@@ -1,16 +1,22 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { NgModule } from '@angular/core';
import {NgModule} from '@angular/core';

import { NgbdSharedModule } from '../../shared';
import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component';
import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared';
import { NgbdApiPage } from '../shared/api-page/api.component';
import { NgbdExamplesPage } from '../shared/examples-page/examples.component';
import { NgbdCollapseBasic } from './demos/basic/collapse-basic';
import { NgbdCollapseBasicModule } from './demos/basic/collapse-basic.module';
import { NgbdCollapseNavbar } from './demos/navbar/collapse-navbar';
import { NgbdCollapseNavbarModule } from './demos/navbar/collapse-navbar.module';
import { Routes } from '@angular/router';
import {NgbdSharedModule} from '../../shared';
import {
ComponentWrapper
} from '../../shared/component-wrapper/component-wrapper.component';
import {NgbdComponentsSharedModule, NgbdDemoList} from '../shared';
import {NgbdApiPage} from '../shared/api-page/api.component';
import {NgbdExamplesPage} from '../shared/examples-page/examples.component';
import {NgbdCollapseBasic} from './demos/basic/collapse-basic';
import {NgbdCollapseBasicModule} from './demos/basic/collapse-basic.module';
import {NgbdCollapseHorizontal} from './demos/horizontal/collapse-horizontal';
import {
NgbdCollapseHorizontalModule
} from './demos/horizontal/collapse-horizontal.module';
import {NgbdCollapseNavbar} from './demos/navbar/collapse-navbar';
import {NgbdCollapseNavbarModule} from './demos/navbar/collapse-navbar.module';
import {Routes} from '@angular/router';

const DEMOS = {
basic: {
Expand All @@ -19,6 +25,14 @@ const DEMOS = {
code: require('!!raw-loader!./demos/basic/collapse-basic').default,
markup: require('!!raw-loader!./demos/basic/collapse-basic.html').default
},
horizontal: {
title: 'Horizontal collapse',
type: NgbdCollapseHorizontal,
code:
require('!!raw-loader!./demos/horizontal/collapse-horizontal').default,
markup: require('!!raw-loader!./demos/horizontal/collapse-horizontal.html')
.default
},
navbar: {
title: 'Responsive Navbar',
type: NgbdCollapseNavbar,
Expand All @@ -28,30 +42,25 @@ const DEMOS = {
};

export const ROUTES: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'examples' },
{
{path: '', pathMatch: 'full', redirectTo: 'examples'}, {
path: '',
component: ComponentWrapper,
data: {
bootstrap: 'https://getbootstrap.com/docs/%version%/components/collapse/'
},
children: [
{ path: 'examples', component: NgbdExamplesPage },
{ path: 'api', component: NgbdApiPage }
{path: 'examples', component: NgbdExamplesPage},
{path: 'api', component: NgbdApiPage}
]
}
];

@NgModule({
imports: [
NgbdSharedModule,
NgbdComponentsSharedModule,
NgbdCollapseBasicModule,
NgbdCollapseNavbarModule
NgbdSharedModule, NgbdComponentsSharedModule, NgbdCollapseBasicModule,
NgbdCollapseHorizontalModule, NgbdCollapseNavbarModule
]
})
export class NgbdCollapseModule {
constructor(demoList: NgbdDemoList) {
demoList.register('collapse', DEMOS);
}
constructor(demoList: NgbdDemoList) { demoList.register('collapse', DEMOS); }
}
@@ -0,0 +1,15 @@
<p>You need a width on the immediate child element for the animation to work correctly.<p>
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()" [attr.aria-expanded]="!isCollapsed"
aria-controls="collapseExample">
Toggle
</button>
</p>
<div style="min-height: 100px;">
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" [horizontal]="true">
<div class="card" style="width: 300px;">
<div class="card-body">
You can collapse horizontally this card by clicking Toggle
</div>
</div>
</div>
</div>
@@ -0,0 +1,14 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

import {NgbdCollapseHorizontal} from './collapse-horizontal';

@NgModule({
imports: [BrowserModule, NgbModule],
declarations: [NgbdCollapseHorizontal],
exports: [NgbdCollapseHorizontal],
bootstrap: [NgbdCollapseHorizontal]
})
export class NgbdCollapseHorizontalModule {
}
@@ -0,0 +1,10 @@
import {Component} from '@angular/core';

@Component({
selector: 'ngbd-collapse-horizontal',
templateUrl: './collapse-horizontal.html'
})

export class NgbdCollapseHorizontal {
public isCollapsed = false;
}
4 changes: 2 additions & 2 deletions src/accordion/accordion.ts
Expand Up @@ -362,7 +362,7 @@ export class NgbAccordion implements AfterContentChecked {
ngbRunTransition(this._ngZone, panelElement, ngbCollapsingTransition, {
animation: false,
runningTransition: 'continue',
context: {direction: panel.isOpen ? 'show' : 'hide'}
context: {direction: panel.isOpen ? 'show' : 'hide', dimension: 'height'}
});
}
} else {
Expand Down Expand Up @@ -421,7 +421,7 @@ export class NgbAccordion implements AfterContentChecked {
ngbRunTransition(this._ngZone, panelElement !, ngbCollapsingTransition, {
animation,
runningTransition: 'stop',
context: {direction: panel.isOpen ? 'show' : 'hide'}
context: {direction: panel.isOpen ? 'show' : 'hide', dimension: 'height'}
}).subscribe(() => {
panel.transitionRunning = false;
const {id} = panel;
Expand Down
1 change: 1 addition & 0 deletions src/collapse/collapse-config.ts
Expand Up @@ -10,6 +10,7 @@ import {NgbConfig} from '../ngb-config';
@Injectable({providedIn: 'root'})
export class NgbCollapseConfig {
private _animation: boolean;
horizontal = false;

constructor(private _ngbConfig: NgbConfig) {}

Expand Down
25 changes: 18 additions & 7 deletions src/collapse/collapse.ts
Expand Up @@ -14,9 +14,10 @@ import {ngbCollapsingTransition} from '../util/transition/ngbCollapseTransition'
import {NgbCollapseConfig} from './collapse-config';

/**
* A directive to provide a simple way of hiding and showing elements on the page.
* A directive to provide a simple way of hiding and showing elements on the
* page.
*/
@Directive({selector: '[ngbCollapse]', exportAs: 'ngbCollapse'})
@Directive({selector: '[ngbCollapse]', exportAs: 'ngbCollapse', host: {'[class.collapse-horizontal]': 'horizontal'}})
export class NgbCollapse implements OnInit, OnChanges {
/**
* If `true`, collapse will be animated.
Expand All @@ -36,14 +37,21 @@ export class NgbCollapse implements OnInit, OnChanges {
@Output() ngbCollapseChange = new EventEmitter<boolean>();

/**
* An event emitted when the collapse element is shown, after the transition. It has no payload.
* If `true`, will collapse horizontally.
*/
@Input() horizontal: boolean;

/**
* An event emitted when the collapse element is shown, after the transition.
* It has no payload.
*
* @since 8.0.0
*/
@Output() shown = new EventEmitter<void>();

/**
* An event emitted when the collapse element is hidden, after the transition. It has no payload.
* An event emitted when the collapse element is hidden, after the transition.
* It has no payload.
*
* @since 8.0.0
*/
Expand All @@ -52,6 +60,7 @@ export class NgbCollapse implements OnInit, OnChanges {

constructor(private _element: ElementRef, config: NgbCollapseConfig, private _zone: NgZone) {
this.animation = config.animation;
this.horizontal = config.horizontal;
}

ngOnInit() { this._runTransition(this.collapsed, false); }
Expand All @@ -77,9 +86,11 @@ export class NgbCollapse implements OnInit, OnChanges {
}

private _runTransition(collapsed: boolean, animation: boolean) {
return ngbRunTransition(
this._zone, this._element.nativeElement, ngbCollapsingTransition,
{animation, runningTransition: 'stop', context: {direction: collapsed ? 'hide' : 'show'}});
return ngbRunTransition(this._zone, this._element.nativeElement, ngbCollapsingTransition, {
animation,
runningTransition: 'stop',
context: {direction: collapsed ? 'hide' : 'show', dimension: this.horizontal ? 'width' : 'height'}
});
}

private _runTransitionWithEvents(collapsed: boolean, animation: boolean) {
Expand Down
55 changes: 33 additions & 22 deletions src/util/transition/ngbCollapseTransition.ts
Expand Up @@ -3,10 +3,11 @@ import {reflow} from '../util';

export interface NgbCollapseCtx {
direction: 'show' | 'hide';
maxHeight?: string;
dimension: 'width' | 'height';
maxSize?: string;
}

function measureCollapsingElementHeightPx(element: HTMLElement): string {
function measureCollapsingElementDimensionPx(element: HTMLElement, dimension: 'width' | 'height'): string {
// SSR fix for without injecting the PlatformId
if (typeof navigator === 'undefined') {
return '0px';
Expand All @@ -18,19 +19,19 @@ function measureCollapsingElementHeightPx(element: HTMLElement): string {
classList.add('show');
}

element.style.height = '';
const height = element.getBoundingClientRect().height + 'px';
element.style[dimension] = '';
const dimensionSize = element.getBoundingClientRect()[dimension] + 'px';

if (!hasShownClass) {
classList.remove('show');
}

return height;
return dimensionSize;
}

export const ngbCollapsingTransition: NgbTransitionStartFn<NgbCollapseCtx> =
(element: HTMLElement, animation: boolean, context: NgbCollapseCtx) => {
let {direction, maxHeight} = context;
let {direction, maxSize, dimension} = context;
const {classList} = element;

function setInitialClasses() {
Expand All @@ -48,30 +49,40 @@ export const ngbCollapsingTransition: NgbTransitionStartFn<NgbCollapseCtx> =
return;
}

// No maxHeight -> running the transition for the first time
if (!maxHeight) {
maxHeight = measureCollapsingElementHeightPx(element);
context.maxHeight = maxHeight;

// Fix the height before starting the animation
element.style.height = direction !== 'show' ? maxHeight : '0px';

if (direction === 'show') {
// Fix the dimension before starting the animation
element.style[dimension] = '0px';
classList.remove('collapse');
classList.remove('collapsing');
classList.remove('show');

reflow(element);

// Start the animation
classList.add('collapsing');
}

// Start or revert the animation
element.style.height = direction === 'show' ? maxHeight : '0px';
const scrollDimension = `scroll${dimension[0].toUpperCase()}${dimension.slice(1)}`;
element.style[dimension] = element[scrollDimension] + 'px';
} else {
// No maxSize -> running the transition for the first time
if (!maxSize) {
maxSize = measureCollapsingElementDimensionPx(element, dimension);
context.maxSize = maxSize;

// Fix the height before starting the animation
element.style[dimension] = maxSize;

classList.remove('collapse');
classList.remove('collapsing');
classList.remove('show');

reflow(element);

// Start the animation
classList.add('collapsing');
}
element.style[dimension] = '0px';
}

return () => {
setInitialClasses();
classList.remove('collapsing');
element.style.height = '';
element.style[dimension] = '';
};
};

0 comments on commit 98f0527

Please sign in to comment.