diff --git a/e2e/flyout.e2e-spec.ts b/e2e/flyout.e2e-spec.ts index 44ceb66..fc1ade1 100644 --- a/e2e/flyout.e2e-spec.ts +++ b/e2e/flyout.e2e-spec.ts @@ -47,4 +47,36 @@ describe('Flyout', () => { expect('body').toMatchBaselineScreenshot(done); element(by.css('.sky-flyout .sky-flyout-btn-close')).click(); }); + + it('should match previous screenshot when row iterators are enabled', (done) => { + SkyHostBrowser.get('visual/flyout'); + SkyHostBrowser.setWindowBreakpoint('lg'); + element(by.css('#open-flyout-with-iterators')).click(); + expect('body').toMatchBaselineScreenshot(done); + element(by.css('.sky-flyout .sky-flyout-btn-close')).click(); + }); + + it('should match previous screenshot when row iterators are enabled (screen: xs)', (done) => { + SkyHostBrowser.get('visual/flyout'); + SkyHostBrowser.setWindowBreakpoint('xs'); + element(by.css('#open-flyout-with-iterators')).click(); + expect('body').toMatchBaselineScreenshot(done); + element(by.css('.sky-flyout .sky-flyout-btn-close')).click(); + }); + + it('should match previous screenshot when row iterators are disabled', (done) => { + SkyHostBrowser.get('visual/flyout'); + SkyHostBrowser.setWindowBreakpoint('lg'); + element(by.css('#open-flyout-with-iterators-disabled')).click(); + expect('body').toMatchBaselineScreenshot(done); + element(by.css('.sky-flyout .sky-flyout-btn-close')).click(); + }); + + it('should match previous screenshot when row iterator are disabled (screen: xs)', (done) => { + SkyHostBrowser.get('visual/flyout'); + SkyHostBrowser.setWindowBreakpoint('xs'); + element(by.css('#open-flyout-with-iterators-disabled')).click(); + expect('body').toMatchBaselineScreenshot(done); + element(by.css('.sky-flyout .sky-flyout-btn-close')).click(); + }); }); diff --git a/package.json b/package.json index ac43bb5..ab40423 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ }, "dependencies": {}, "devDependencies": { - "@blackbaud/skyux": "2.28.1", - "@blackbaud/skyux-builder": "1.28.0", + "@blackbaud/skyux": "2.34.0", + "@blackbaud/skyux-builder": "1.30.0", "@skyux-sdk/builder-plugin-skyux": "1.0.0-rc.5" } } diff --git a/screenshots-baseline/body0-chrome-1044x788-dpr-1.png b/screenshots-baseline/body0-chrome-1044x788-dpr-1.png deleted file mode 100644 index b31c212..0000000 Binary files a/screenshots-baseline/body0-chrome-1044x788-dpr-1.png and /dev/null differ diff --git a/screenshots-baseline/body2-chrome-1044x788-dpr-1.png b/screenshots-baseline/body2-chrome-1044x788-dpr-1.png deleted file mode 100644 index 6ca7be2..0000000 Binary files a/screenshots-baseline/body2-chrome-1044x788-dpr-1.png and /dev/null differ diff --git a/src/app/public/modules/flyout/flyout-instance.spec.ts b/src/app/public/modules/flyout/flyout-instance.spec.ts index 52b9edc..a19042f 100644 --- a/src/app/public/modules/flyout/flyout-instance.spec.ts +++ b/src/app/public/modules/flyout/flyout-instance.spec.ts @@ -29,4 +29,44 @@ describe('Flyout instance', () => { type: SkyFlyoutMessageType.Close }); }); + + it('should expose iterator next button methods to the flyout', () => { + const flyout = new SkyFlyoutInstance(); + const spy = spyOn(flyout.hostController, 'next').and.callThrough(); + + flyout.iteratorNextButtonDisabled = true; + expect(spy).toHaveBeenCalledWith({ + type: SkyFlyoutMessageType.DisableIteratorNextButton + }); + + flyout.iteratorNextButtonDisabled = false; + expect(spy).toHaveBeenCalledWith({ + type: SkyFlyoutMessageType.EnableIteratorNextButton + }); + }); + + it('should expose iterator previous button methods to the flyout', () => { + const flyout = new SkyFlyoutInstance(); + const spy = spyOn(flyout.hostController, 'next').and.callThrough(); + + flyout.iteratorPreviousButtonDisabled = true; + expect(spy).toHaveBeenCalledWith({ + type: SkyFlyoutMessageType.DisableIteratorPreviousButton + }); + + flyout.iteratorPreviousButtonDisabled = false; + expect(spy).toHaveBeenCalledWith({ + type: SkyFlyoutMessageType.EnableIteratorPreviousButton + }); + }); + + it('should complete subscribes on destroy', () => { + const flyout = new SkyFlyoutInstance(); + const previousSpy = spyOn(flyout.iteratorPreviousButtonClick, 'complete').and.callThrough(); + const nextSpy = spyOn(flyout.iteratorNextButtonClick, 'complete').and.callThrough(); + + flyout.ngOnDestroy(); + expect(previousSpy).toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/public/modules/flyout/flyout-instance.ts b/src/app/public/modules/flyout/flyout-instance.ts index bd193da..b32169f 100644 --- a/src/app/public/modules/flyout/flyout-instance.ts +++ b/src/app/public/modules/flyout/flyout-instance.ts @@ -1,5 +1,6 @@ import { - EventEmitter + EventEmitter, + OnDestroy } from '@angular/core'; import { @@ -11,11 +12,61 @@ import { SkyFlyoutMessageType } from './types'; -export class SkyFlyoutInstance { +export class SkyFlyoutInstance implements OnDestroy { public closed = new EventEmitter(); public componentInstance: T; public isOpen = true; + public get iteratorNextButtonClick(): EventEmitter { + return this._iteratorNextButtonClick; + } + + public get iteratorPreviousButtonClick(): EventEmitter { + return this._iteratorPreviousButtonClick; + } + + public set iteratorNextButtonDisabled(newValue: boolean) { + this._iteratorNextButtonDisabled = newValue; + if (newValue) { + this.hostController.next({ + type: SkyFlyoutMessageType.DisableIteratorNextButton + }); + } else { + this.hostController.next({ + type: SkyFlyoutMessageType.EnableIteratorNextButton + }); + } + } + + public get iteratorNextButtonDisabled(): boolean { + return this._iteratorNextButtonDisabled; + } + + public set iteratorPreviousButtonDisabled(newValue: boolean) { + this._iteratorPreviousButtonDisabled = newValue; + if (newValue) { + this.hostController.next({ + type: SkyFlyoutMessageType.DisableIteratorPreviousButton + }); + } else { + this.hostController.next({ + type: SkyFlyoutMessageType.EnableIteratorPreviousButton + }); + } + } + + public get iteratorPreviousButtonDisabled(): boolean { + return this._iteratorPreviousButtonDisabled; + } + + private _iteratorNextButtonClick = new EventEmitter(); + + private _iteratorPreviousButtonClick = new EventEmitter(); + + private _iteratorNextButtonDisabled = false; + + private _iteratorPreviousButtonDisabled = false; + // Used to communicate with the host component. public get hostController(): Subject { return this._hostController; @@ -29,6 +80,11 @@ export class SkyFlyoutInstance { }); } + public ngOnDestroy() { + this._iteratorPreviousButtonClick.complete(); + this._iteratorNextButtonClick.complete(); + } + public close() { this.hostController.next({ type: SkyFlyoutMessageType.Close diff --git a/src/app/public/modules/flyout/flyout-iterator.component.html b/src/app/public/modules/flyout/flyout-iterator.component.html new file mode 100644 index 0000000..b1a7cf7 --- /dev/null +++ b/src/app/public/modules/flyout/flyout-iterator.component.html @@ -0,0 +1,26 @@ +
+ + +
diff --git a/src/app/public/modules/flyout/flyout-iterator.component.ts b/src/app/public/modules/flyout/flyout-iterator.component.ts new file mode 100644 index 0000000..b86c881 --- /dev/null +++ b/src/app/public/modules/flyout/flyout-iterator.component.ts @@ -0,0 +1,53 @@ +import { + Component, + OnDestroy, + EventEmitter, + Input, + Output +} from '@angular/core'; + +@Component({ + selector: 'sky-flyout-iterator', + templateUrl: './flyout-iterator.component.html' +}) +export class SkyFlyoutIteratorComponent implements OnDestroy { + + @Input() + public nextButtonDisabled: boolean; + + @Input() + public previousButtonDisabled: boolean; + + @Output() + public get previousButtonClick(): EventEmitter { + return this._previousButtonClick; + } + + private _previousButtonClick = new EventEmitter(); + + @Output() + public get nextButtonClick(): EventEmitter { + return this._nextButtonClick; + } + + private _nextButtonClick = new EventEmitter(); + + constructor() {} + + public ngOnDestroy() { + this._previousButtonClick.complete(); + this._nextButtonClick.complete(); + } + + public onIteratorPreviousClick() { + if (!this.previousButtonDisabled) { + this._previousButtonClick.emit(); + } + } + + public onIteratorNextClick() { + if (!this.nextButtonDisabled) { + this._nextButtonClick.emit(); + } + } +} diff --git a/src/app/public/modules/flyout/flyout.component.html b/src/app/public/modules/flyout/flyout.component.html index 218ec2b..5062b7d 100644 --- a/src/app/public/modules/flyout/flyout.component.html +++ b/src/app/public/modules/flyout/flyout.component.html @@ -26,8 +26,16 @@ (mousedown)="onMouseDown($event)" />
-
-
+
+ + +
+
diff --git a/src/app/public/modules/flyout/flyout.component.spec.ts b/src/app/public/modules/flyout/flyout.component.spec.ts index fd05792..cb24cd1 100644 --- a/src/app/public/modules/flyout/flyout.component.spec.ts +++ b/src/app/public/modules/flyout/flyout.component.spec.ts @@ -652,4 +652,169 @@ describe('Flyout component', () => { }) ); }); + + describe('iterator', () => { + function getIteratorButtons() { + return document.querySelectorAll('#iterators button') as NodeListOf; + } + + it('should not show iterator buttons if config.showIterator is undefined', fakeAsync(() => { + openFlyout(); + const iteratorButtons = getIteratorButtons(); + expect(iteratorButtons.length).toEqual(0); + })); + + it('should not show iterator buttons if config.showIterator is false', fakeAsync(() => { + openFlyout({ + showIterator: false + }); + const iteratorButtons = getIteratorButtons(); + expect(iteratorButtons.length).toEqual(0); + })); + + it('should show iterator buttons if config.showIterator is true', fakeAsync(() => { + openFlyout({ + showIterator: true + }); + const iteratorButtons = getIteratorButtons(); + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeFalsy(); + expect(iteratorButtons[1].disabled).toBeFalsy(); + })); + + it('should disable iterator buttons if config disabled properties are true', fakeAsync(() => { + openFlyout({ + showIterator: true, + iteratorPreviousButtonDisabled: true, + iteratorNextButtonDisabled: true + }); + const iteratorButtons = getIteratorButtons(); + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeTruthy(); + expect(iteratorButtons[1].disabled).toBeTruthy(); + })); + + it('should enable iterator buttons if disabled properties are false', fakeAsync(() => { + openFlyout({ + showIterator: true, + iteratorPreviousButtonDisabled: false, + iteratorNextButtonDisabled: false + }); + const iteratorButtons = getIteratorButtons(); + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeFalsy(); + expect(iteratorButtons[1].disabled).toBeFalsy(); + })); + + it('should emit if previous button is clicked', fakeAsync(() => { + let previousCalled = false; + let nextCalled = false; + + const flyoutInstance = openFlyout({ + showIterator: true + }); + + flyoutInstance.iteratorPreviousButtonClick.subscribe(() => { + previousCalled = true; + }); + + flyoutInstance.iteratorNextButtonClick.subscribe(() => { + nextCalled = true; + }); + + const iteratorButtons = getIteratorButtons(); + iteratorButtons[0].click(); + fixture.detectChanges(); + tick(); + expect(previousCalled).toEqual(true); + expect(nextCalled).toEqual(false); + })); + + it('should emit if next button is clicked', fakeAsync(() => { + let previousCalled = false; + let nextCalled = false; + + const flyoutInstance = openFlyout({ + showIterator: true + }); + + flyoutInstance.iteratorPreviousButtonClick.subscribe(() => { + previousCalled = true; + }); + + flyoutInstance.iteratorNextButtonClick.subscribe(() => { + nextCalled = true; + }); + + const iteratorButtons = getIteratorButtons(); + iteratorButtons[1].click(); + fixture.detectChanges(); + tick(); + expect(previousCalled).toEqual(false); + expect(nextCalled).toEqual(true); + })); + + it('should not emit if iterator buttons are clicked when config properties are disabled', fakeAsync(() => { + let previousCalled = false; + let nextCalled = false; + + const flyoutInstance = openFlyout({ + showIterator: true, + iteratorPreviousButtonDisabled: true, + iteratorNextButtonDisabled: true + }); + + flyoutInstance.iteratorPreviousButtonClick.subscribe(() => { + previousCalled = true; + }); + + flyoutInstance.iteratorNextButtonClick.subscribe(() => { + nextCalled = true; + }); + + const iteratorButtons = getIteratorButtons(); + iteratorButtons[0].click(); + iteratorButtons[1].click(); + fixture.detectChanges(); + tick(); + expect(previousCalled).toEqual(false); + expect(nextCalled).toEqual(false); + })); + + it('should allow consumer to enable/disable buttons after flyout is opened', fakeAsync(() => { + const flyout = openFlyout({ + showIterator: true + }); + const iteratorButtons = getIteratorButtons(); + + // Expect flyout to have iterator buttons, both enabled. + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeFalsy(); + expect(iteratorButtons[1].disabled).toBeFalsy(); + expect(flyout.iteratorNextButtonDisabled).toEqual(false); + expect(flyout.iteratorPreviousButtonDisabled).toEqual(false); + + flyout.iteratorPreviousButtonDisabled = true; + flyout.iteratorNextButtonDisabled = true; + fixture.detectChanges(); + + // Expect flyout to have iterator buttons, both disabled. + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeTruthy(); + expect(iteratorButtons[1].disabled).toBeTruthy(); + expect(flyout.iteratorNextButtonDisabled).toEqual(true); + expect(flyout.iteratorPreviousButtonDisabled).toEqual(true); + + flyout.iteratorPreviousButtonDisabled = false; + flyout.iteratorNextButtonDisabled = false; + fixture.detectChanges(); + + // Expect flyout to have iterator buttons, both enabled. + expect(iteratorButtons.length).toEqual(2); + expect(iteratorButtons[0].disabled).toBeFalsy(); + expect(iteratorButtons[1].disabled).toBeFalsy(); + expect(flyout.iteratorNextButtonDisabled).toEqual(false); + expect(flyout.iteratorPreviousButtonDisabled).toEqual(false); + })); + }); }); diff --git a/src/app/public/modules/flyout/flyout.component.ts b/src/app/public/modules/flyout/flyout.component.ts index b4743e1..3c53084 100644 --- a/src/app/public/modules/flyout/flyout.component.ts +++ b/src/app/public/modules/flyout/flyout.component.ts @@ -167,6 +167,10 @@ export class SkyFlyoutComponent implements OnDestroy, OnInit { this.config.minWidth = this.config.minWidth || 320; this.config.maxWidth = this.config.maxWidth || this.config.defaultWidth; + this.config.showIterator = this.config.showIterator || false; + this.config.iteratorNextButtonDisabled = this.config.iteratorNextButtonDisabled || false; + this.config.iteratorPreviousButtonDisabled = this.config.iteratorPreviousButtonDisabled || false; + const factory = this.resolver.resolveComponentFactory(component); const providers = ReflectiveInjector.resolve(this.config.providers); const injector = ReflectiveInjector.fromResolvedProviders(providers, this.injector); @@ -251,6 +255,14 @@ export class SkyFlyoutComponent implements OnDestroy, OnInit { this.adapter.toggleIframePointerEvents(true); } + public onIteratorPreviousButtonClick() { + this.flyoutInstance.iteratorPreviousButtonClick.emit(); + } + + public onIteratorNextButtonClick() { + this.flyoutInstance.iteratorNextButtonClick.emit(); + } + private createFlyoutInstance(component: T): SkyFlyoutInstance { const instance = new SkyFlyoutInstance(); @@ -278,6 +290,22 @@ export class SkyFlyoutComponent implements OnDestroy, OnInit { this.isOpen = true; this.isOpening = false; break; + + case SkyFlyoutMessageType.EnableIteratorNextButton: + this.config.iteratorNextButtonDisabled = false; + break; + + case SkyFlyoutMessageType.EnableIteratorPreviousButton: + this.config.iteratorPreviousButtonDisabled = false; + break; + + case SkyFlyoutMessageType.DisableIteratorNextButton: + this.config.iteratorNextButtonDisabled = true; + break; + + case SkyFlyoutMessageType.DisableIteratorPreviousButton: + this.config.iteratorPreviousButtonDisabled = true; + break; } this.changeDetector.markForCheck(); diff --git a/src/app/public/modules/flyout/flyout.module.ts b/src/app/public/modules/flyout/flyout.module.ts index 348ad32..a300d98 100644 --- a/src/app/public/modules/flyout/flyout.module.ts +++ b/src/app/public/modules/flyout/flyout.module.ts @@ -38,13 +38,18 @@ import { SkyFlyoutComponent } from './flyout.component'; +import { + SkyFlyoutIteratorComponent +} from './flyout-iterator.component'; + import { SkyFlyoutService } from './flyout.service'; @NgModule({ declarations: [ - SkyFlyoutComponent + SkyFlyoutComponent, + SkyFlyoutIteratorComponent ], providers: [ SkyFlyoutAdapterService, diff --git a/src/app/public/modules/flyout/index.ts b/src/app/public/modules/flyout/index.ts index ffa57a3..e66e876 100644 --- a/src/app/public/modules/flyout/index.ts +++ b/src/app/public/modules/flyout/index.ts @@ -1,6 +1,7 @@ export * from './flyout-adapter.service'; export * from './flyout.component'; export * from './flyout-instance'; +export * from './flyout-iterator.component'; export * from './flyout.module'; export * from './flyout.service'; export * from './flyout-providers'; diff --git a/src/app/public/modules/flyout/types/flyout-config.ts b/src/app/public/modules/flyout/types/flyout-config.ts index d347e82..75c1b9a 100644 --- a/src/app/public/modules/flyout/types/flyout-config.ts +++ b/src/app/public/modules/flyout/types/flyout-config.ts @@ -16,4 +16,7 @@ export interface SkyFlyoutConfig { permalink?: SkyFlyoutPermalink; primaryAction?: SkyFlyoutAction; providers?: any[]; + showIterator?: boolean; + iteratorPreviousButtonDisabled?: boolean; + iteratorNextButtonDisabled?: boolean; } diff --git a/src/app/public/modules/flyout/types/flyout-message-type.ts b/src/app/public/modules/flyout/types/flyout-message-type.ts index b08573e..1b14c52 100644 --- a/src/app/public/modules/flyout/types/flyout-message-type.ts +++ b/src/app/public/modules/flyout/types/flyout-message-type.ts @@ -1,4 +1,8 @@ export enum SkyFlyoutMessageType { Open = 0, - Close = 1 + Close, + EnableIteratorNextButton, + EnableIteratorPreviousButton, + DisableIteratorNextButton, + DisableIteratorPreviousButton } diff --git a/src/app/visual/flyout/flyout-visual.component.html b/src/app/visual/flyout/flyout-visual.component.html index efa90e2..09dd828 100644 --- a/src/app/visual/flyout/flyout-visual.component.html +++ b/src/app/visual/flyout/flyout-visual.component.html @@ -3,11 +3,32 @@ class="sky-flyout-demo-item" >
+ +
+ + +
diff --git a/src/app/visual/flyout/flyout-visual.component.ts b/src/app/visual/flyout/flyout-visual.component.ts index 356ca89..0f8678a 100644 --- a/src/app/visual/flyout/flyout-visual.component.ts +++ b/src/app/visual/flyout/flyout-visual.component.ts @@ -1,8 +1,14 @@ import { - Component + Component, + OnDestroy } from '@angular/core'; import { + Subject +} from 'rxjs/Subject'; + +import { + SkyFlyoutInstance, SkyFlyoutService } from '../../public'; @@ -19,7 +25,7 @@ import { templateUrl: './flyout-visual.component.html', styleUrls: ['./flyout-visual.component.scss'] }) -export class FlyoutVisualComponent { +export class FlyoutVisualComponent implements OnDestroy { public users: {id: string, name: string}[] = [ { id: '1', name: 'Sally' }, { id: '2', name: 'John' }, @@ -27,10 +33,19 @@ export class FlyoutVisualComponent { { id: '4', name: 'Janet' } ]; + public flyout: SkyFlyoutInstance; + + private ngUnsubscribe = new Subject(); + constructor( private flyoutService: SkyFlyoutService ) { } + public ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + public openFlyout(record: any) { this.flyoutService.open(FlyoutDemoComponent, { providers: [{ @@ -39,4 +54,28 @@ export class FlyoutVisualComponent { }] }); } + + public openFlyoutWithIterators(record: any, previousButtonDisabled: boolean, nextButtonDisabled: boolean) { + this.flyout = this.flyoutService.open(FlyoutDemoComponent, { + providers: [{ + provide: FlyoutDemoContext, + useValue: record + }], + showIterator: true, + iteratorPreviousButtonDisabled: previousButtonDisabled, + iteratorNextButtonDisabled: nextButtonDisabled + }); + + this.flyout.iteratorPreviousButtonClick + .takeUntil(this.ngUnsubscribe) + .subscribe(() => { + console.log('previous clicked'); + }); + + this.flyout.iteratorNextButtonClick + .takeUntil(this.ngUnsubscribe) + .subscribe(() => { + console.log('next clicked'); + }); + } } diff --git a/src/assets/locales/resources_en_US.json b/src/assets/locales/resources_en_US.json index b5b1292..455a2a5 100644 --- a/src/assets/locales/resources_en_US.json +++ b/src/assets/locales/resources_en_US.json @@ -1,12 +1,16 @@ { - "skyux_flyout_resize_handle": { - "_description": "Label for the flyout resizing handle", - "message": "Resize the flyout window. Use left arrow key to expand the flyout window and right arrow key to shrink the flyout window." - }, "skyux_flyout_close": { "_description": "Text for flyout close button", "message": "Close flyout" }, + "skyux_flyout_iterator_previous_button": { + "_description": "Label for the previous iterator button", + "message": "Previous" + }, + "skyux_flyout_iterator_next_button": { + "_description": "Label for the next iterator button", + "message": "Next" + }, "skyux_flyout_permalink_button": { "_description": "Default text for the flyout permalink button", "message": "View record" @@ -14,5 +18,9 @@ "skyux_flyout_primary_action_button": { "_description": "Default text for the flyout primary action button", "message": "Create list" + }, + "skyux_flyout_resize_handle": { + "_description": "Label for the flyout resizing handle", + "message": "Resize the flyout window. Use left arrow key to expand the flyout window and right arrow key to shrink the flyout window." } }