From 72834f83068f3104a1932af0f87dfdcb8620dcf1 Mon Sep 17 00:00:00 2001 From: Jerzy Czopek Date: Mon, 11 Jan 2016 22:34:52 +0100 Subject: [PATCH] feat(callout): add new directive Add new directive `uif-callout`. Closes #15. Closes #119. --- src/components/callout/.gitignore | 1 + src/components/callout/calloutArrowEnum.ts | 20 + .../callout/calloutDirective.spec.ts | 499 ++++++++++++++++++ src/components/callout/calloutDirective.ts | 308 +++++++++++ src/components/callout/calloutTypeEnum.ts | 19 + src/components/callout/demo/index.html | 237 +++++++++ src/components/callout/demo/index.js | 26 + src/core/components.ts | 2 + 8 files changed, 1112 insertions(+) create mode 100644 src/components/callout/.gitignore create mode 100644 src/components/callout/calloutArrowEnum.ts create mode 100644 src/components/callout/calloutDirective.spec.ts create mode 100644 src/components/callout/calloutDirective.ts create mode 100644 src/components/callout/calloutTypeEnum.ts create mode 100644 src/components/callout/demo/index.html create mode 100644 src/components/callout/demo/index.js diff --git a/src/components/callout/.gitignore b/src/components/callout/.gitignore new file mode 100644 index 0000000..efc798a --- /dev/null +++ b/src/components/callout/.gitignore @@ -0,0 +1 @@ +!demo/index.js \ No newline at end of file diff --git a/src/components/callout/calloutArrowEnum.ts b/src/components/callout/calloutArrowEnum.ts new file mode 100644 index 0000000..82520ce --- /dev/null +++ b/src/components/callout/calloutArrowEnum.ts @@ -0,0 +1,20 @@ +'use strict'; + +/** + * Enum for callout arrow direction. It is intended to use as string as `uif-arrow` attribute value on `uif-callout` directive. + * + * @readonly + * @enum {string} + * @usage + * + * This is used to generate the string that you pass into the directive. Specifically, the string is passed + * to the `uif-arrow` attribute. To evaluate the enum value as a string: + * + * let arrow: string = CalloutArrow[CalloutArrow.left]; + */ +export enum CalloutArrow { + left, + right, + top, + bottom +} diff --git a/src/components/callout/calloutDirective.spec.ts b/src/components/callout/calloutDirective.spec.ts new file mode 100644 index 0000000..b7ebae8 --- /dev/null +++ b/src/components/callout/calloutDirective.spec.ts @@ -0,0 +1,499 @@ +'use strict'; + +import * as ng from 'angular'; + +describe('calloutDirectives:', () => { + beforeEach(() => { + angular.mock.module('officeuifabric.core'); + angular.mock.module('officeuifabric.components.callout'); + }); + + describe('HTML rendering', () => { + let element: JQuery; + let scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + })); + + it('main element should be DIV', () => { + let calloutElement: JQuery = element.children().first(); + + expect(calloutElement[0].tagName === 'DIV').toBeTruthy(); + }); + + it('main element should have callout CSS class', () => { + let calloutElement: JQuery = element.find('div').first(); + + expect(calloutElement[0]).toHaveClass('ms-Callout'); + }); + + it('should have div with proper CSS as first child', () => { + let mainElement: JQuery = element.find('div.ms-Callout > div').first(); + + expect(mainElement[0]).toHaveClass('ms-Callout-main'); + }); + + it('should have inner div rendered with proper CSS', () => { + let innerElement: JQuery = element.find('div.ms-Callout-inner'); + + expect(innerElement[0]).toBeDefined(); + expect(innerElement[0]).toHaveClass('ms-Callout-inner'); + + let parentElement: JQuery = innerElement.parent(); + expect(parentElement[0]).toBeDefined(); + expect(parentElement[0]).toHaveClass('ms-Callout-main'); + }); + + it('plain callout should have left arrow', () => { + let calloutElement: JQuery = element.find('div').first(); + + expect(calloutElement[0]).toHaveClass('ms-Callout--arrowLeft'); + }); + + it('main element should have proper arrow CSS class', inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element('' + + '' + + '' + + ''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let topArrowElement: JQuery = element.find('div.ms-Callout').eq(0); + let bottomArrowElement: JQuery = element.find('div.ms-Callout').eq(1); + let leftArrowElement: JQuery = element.find('div.ms-Callout').eq(2); + let rightArrowElement: JQuery = element.find('div.ms-Callout').eq(3); + + expect(topArrowElement[0]).toHaveClass('ms-Callout--arrowTop'); + expect(bottomArrowElement[0]).toHaveClass('ms-Callout--arrowBottom'); + expect(leftArrowElement[0]).toHaveClass('ms-Callout--arrowLeft'); + expect(rightArrowElement[0]).toHaveClass('ms-Callout--arrowRight'); + })); + + it( + 'empty arrow attribute logs error', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService, $log: ng.ILogService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + expect($log.error.logs[0]).toContain('Error [ngOfficeUiFabric] officeuifabric.components.callout - "' + + '" is not a valid value for uifArrow. It should be left, right, top, bottom.'); + // expect(calloutElement[0]).toHaveClass('ms-Callout--arrowLeft'); + })); + + it( + 'main element has CSS class when uif-separator is present', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).toHaveClass('ms-Callout--actionText'); + })); + + it( + 'main element has CSS class when uif-action-text is present', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).toHaveClass('ms-Callout--actionText'); + })); + + it('main element has no separator when uif-action-text and uif-separator not present', () => { + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).not.toHaveClass('ms-Callout--actionText'); + }); + + it( + 'main element has proper CSS class uif-type=oobe', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).toHaveClass('ms-Callout--OOBE'); + })); + + it( + 'main element has proper CSS class uif-type=peek', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).toHaveClass('ms-Callout--Peek'); + })); + + it( + 'should log error when uif-type empty', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService, $log: ng.ILogService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + expect($log.error.logs[0]).toContain('Error [ngOfficeUiFabric] officeuifabric.components.callout - ' + + '"" is not a valid value for uifType. It should be oobe or peek'); + })); + + it('main element has no type CSS and no error if uif-type not applied', inject(($log: ng.ILogService) => { + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).not.toHaveClass('ms-Callout--Peek'); + expect(calloutElement[0]).not.toHaveClass('ms-Callout--OOBE'); + expect($log.error.length === 0).toBeTruthy(); + })); + + it( + 'main element has proper CSS and HTML when uif-close attribute present', + inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).toHaveClass('ms-Callout--close'); + + let closeButton: JQuery = calloutElement.find('button.ms-Callout-close').eq(0); + expect(closeButton).toBeDefined(); + expect(closeButton).not.toBeNull(); + + let buttonIcon: JQuery = closeButton.find('i').eq(0); + expect(buttonIcon[0]).toHaveClass('ms-Icon'); + expect(buttonIcon[0]).toHaveClass('ms-Icon--x'); + })); + + }); + + describe('callout visibility', () => { + let element: JQuery; + let scope: any; + + beforeEach(inject(($rootScope: ng.IRootScopeService) => { + element = ng.element(''); + scope = $rootScope.$new(); + + scope.vm = { + isOpen: false + }; + })); + + it('intially callout is closed', inject(($compile: ng.ICompileService) => { + $compile(element)(scope); + scope.$digest(); + + let calloutElement: JQuery = element.eq(0); + expect(calloutElement[0]).toHaveClass('ng-hide'); + })); + + it('intially callout is open', inject(($compile: ng.ICompileService) => { + $compile(element)(scope); + scope.vm.isOpen = true; + + scope.$digest(); + + let calloutElement: JQuery = element.eq(0); + expect(calloutElement[0]).not.toHaveClass('ng-hide'); + })); + + it('callout opens/closes on uif-is-open change', inject(($compile: ng.ICompileService) => { + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + // close it + scope.vm.isOpen = false; + scope.$digest(); + + let calloutElement: JQuery = element.eq(0); + expect(calloutElement[0]).toHaveClass('ng-hide'); + + // reopen + scope.vm.isOpen = true; + scope.$digest(); + + calloutElement = element.find('div.ms-Callout').eq(0); + expect(calloutElement[0]).not.toHaveClass('ng-hide'); + })); + + it('clicking close button closes callout', inject(($compile: ng.ICompileService) => { + element = ng.element(''); + + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + + let closeButton: JQuery = element.find('button.ms-Callout-close').eq(0); + + closeButton.click(); + scope.$digest(); + + expect(element[0]).toHaveClass('ng-hide'); + expect(scope.vm.isOpen).toBeFalsy(); + })); + + it('should not close by itself when mouse is over callout', inject(($compile: ng.ICompileService) => { + element = ng.element(''); + + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + let callout: JQuery = element.eq(0); + callout.mouseenter(); + scope.$digest(); + + scope.vm.isOpen = false; + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + + })); + + it('should close by itself when mouse outside callout', inject(($compile: ng.ICompileService) => { + element = ng.element(''); + + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + let callout: JQuery = element.eq(0); + callout.mouseenter(); + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + + scope.vm.isOpen = false; + callout.mouseleave(); + scope.$digest(); + expect(element[0]).toHaveClass('ng-hide'); + })); + + it('should close itself when mouse over callout and close button clicked', inject(($compile: ng.ICompileService) => { + element = ng.element(''); + + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + let callout: JQuery = element.eq(0); + callout.mouseenter(); + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + + let closeButton: JQuery = element.find('button.ms-Callout-close').eq(0); + closeButton.click(); + scope.$digest(); + + expect(element[0]).toHaveClass('ng-hide'); + })); + + it('should not close when mouse moves outside callout but there is close button', inject(($compile: ng.ICompileService) => { + element = ng.element(''); + + scope.vm.isOpen = true; + + $compile(element)(scope); + scope.$digest(); + + let callout: JQuery = element.eq(0); + callout.mouseenter(); + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + + callout.mouseleave(); + scope.$digest(); + + expect(element[0]).not.toHaveClass('ng-hide'); + })); + }); + + describe('callout header directive tests', () => { + let element: JQuery; + let scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + })); + + it('should be rendered as DIV', () => { + let headerDiv: JQuery = element.children('div').first(); + + expect(headerDiv.length === 1).toBeTruthy(); + expect(headerDiv[0].tagName === 'DIV').toBeTruthy(); + }); + + it('should have proper CSS', () => { + let headerDiv: JQuery = element.find('div').first(); + + expect(headerDiv[0]).toHaveClass('ms-Callout-header'); + }); + + it('main DIV should have child P', () => { + let headerDiv: JQuery = element.find('div').first(); + let headerParagrah: JQuery = headerDiv.children().first(); + + expect(headerParagrah[0].tagName === 'P').toBeTruthy(); + + }); + + it('should have proper CSS on title P', () => { + let headerParagrah: JQuery = element.find('p').first(); + + expect(headerParagrah[0]).toHaveClass('ms-Callout-title'); + }); + + it('should trasnsclude inside P', inject(($compile: ng.ICompileService) => { + element = ng.element('Callout header test!'); + $compile(element)(scope); + + let headerParagrah: JQuery = element.find('p.ms-Callout-title').eq(0); + + expect(headerParagrah[0].innerHTML.indexOf('Callout header test!')).toBeGreaterThan(-1); + })); + + }); + + describe('callout content directive tests', () => { + let element: JQuery; + let scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + })); + + it('should be rendered as DIV', () => { + let headerDiv: JQuery = element.children('div').first(); + + expect(headerDiv.length === 1).toBeTruthy(); + expect(headerDiv[0].tagName === 'DIV').toBeTruthy(); + }); + + it('should have proper CSS', () => { + let headerDiv: JQuery = element.find('div').first(); + + expect(headerDiv[0]).toHaveClass('ms-Callout-content'); + }); + + it('main DIV should have child P', () => { + let headerDiv: JQuery = element.find('div').first(); + let headerParagrah: JQuery = headerDiv.children().first(); + + expect(headerParagrah[0].tagName === 'P').toBeTruthy(); + + }); + + it('should have proper CSS on content P', () => { + let headerParagrah: JQuery = element.find('p').first(); + + expect(headerParagrah[0]).toHaveClass('ms-Callout-subText'); + }); + + it('should trasnsclude inside P', inject(($compile: ng.ICompileService) => { + element = ng.element('Callout content test!'); + $compile(element)(scope); + + let headerParagrah: JQuery = element.find('p.ms-Callout-subText').eq(0); + + expect(headerParagrah[0].innerHTML.indexOf('Callout content test!')).toBeGreaterThan(-1); + })); + + }); + + describe('callout actions directive tests', () => { + let element: JQuery; + let scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element(''); + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + })); + + it('should be rendered as DIV', () => { + let headerDiv: JQuery = element.children('div').first(); + + expect(headerDiv.length === 1).toBeTruthy(); + expect(headerDiv[0].tagName === 'DIV').toBeTruthy(); + }); + + it('should have proper CSS', () => { + let headerDiv: JQuery = element.find('div').first(); + + expect(headerDiv[0]).toHaveClass('ms-Callout-actions'); + }); + + it('should trasnsclude inside main element', inject(($compile: ng.ICompileService) => { + element = ng.element('Callout actions test!'); + $compile(element)(scope); + + expect(element[0].innerHTML.indexOf('Callout actions test!')).toBeGreaterThan(-1); + })); + + }); + + describe('callout directives rendering together', () => { + let element: JQuery; + let scope: ng.IScope; + + beforeEach(inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => { + element = ng.element('' + + 'All of your favorite people' + + 'Message body is optional. If help documentation is available' + + 'Learn more' + + ''); + + scope = $rootScope; + $compile(element)(scope); + scope.$digest(); + })); + + it('should render ms-Callout-inner & header should be appended there', () => { + let mainWrapper: JQuery = element.find('div.ms-Callout-main'); + let innerWrapper: JQuery = element.find('div.ms-Callout-inner'); + + expect(mainWrapper.length === 1).toBeTruthy(); + expect(innerWrapper.length === 1).toBeTruthy(); + + let header: JQuery = mainWrapper.children().eq(0); + + expect(header[0].tagName === 'UIF-CALLOUT-HEADER').toBeTruthy(); + + }); + + }); + +}); + diff --git a/src/components/callout/calloutDirective.ts b/src/components/callout/calloutDirective.ts new file mode 100644 index 0000000..2e58f98 --- /dev/null +++ b/src/components/callout/calloutDirective.ts @@ -0,0 +1,308 @@ +'use strict'; + +import * as ng from 'angular'; +import {CalloutType} from './calloutTypeEnum'; +import {CalloutArrow} from './calloutArrowEnum'; + + +/** + * @ngdoc class + * @name CalloutController + * @module officeuifabric.components.callout + * + * @restrict E + * + * @description + * CalloutController is the controller for the directive + * + */ +export class CalloutController { + public static $inject: string[] = ['$log']; + constructor(public $log: ng.ILogService) { } +} + +/** + * @ngdoc directive + * @name uifCalloutHeader + * @module officeuifabric.components.callout + * + * @restrict E + * + * @description + * `` is an directive for rendering callout header + * + * @usage + * + * + * All of your favorite people + * + */ +export class CalloutHeaderDirective implements ng.IDirective { + public restrict: string = 'E'; + public transclude: boolean = true; + public replace: boolean = false; + public scope: boolean = false; + public template: string = '

'; + + public static factory(): ng.IDirectiveFactory { + const directive: ng.IDirectiveFactory = () => new CalloutHeaderDirective(); + return directive; + } + + public link(scope: any, instanceElement: ng.IAugmentedJQuery, attrs: any, ctrls: any[]): void { + + let mainWrapper: JQuery = instanceElement.parent().parent(); + + if (!ng.isUndefined(mainWrapper) && mainWrapper.hasClass('ms-Callout-main')) { + let detachedHeader: JQuery = instanceElement.detach(); + mainWrapper.prepend(detachedHeader); + } + } +} + +/** + * @ngdoc directive + * @name uifCalloutContent + * @module officeuifabric.components.callout + * + * @restrict E + * + * @description + * `` is an directive for rendering callout content + * + * @usage + * + * + * Consider adding a link to learn more at the bottom. + * + */ +export class CalloutContentDirective implements ng.IDirective { + public restrict: string = 'E'; + public transclude: boolean = true; + public replace: boolean = false; + public scope: boolean = false; + public template: string = '

'; + + public static factory(): ng.IDirectiveFactory { + const directive: ng.IDirectiveFactory = () => new CalloutContentDirective(); + return directive; + } + +} + + +/** + * @ngdoc directive + * @name uifCalloutActions + * @module officeuifabric.components.callout + * + * @restrict E + * + * @description + * `` is an directive for rendering callout actions + * + * @usage + * + * + * + * Learn more + * + * + */ +export class CalloutActionsDirective implements ng.IDirective { + public restrict: string = 'E'; + public transclude: boolean = true; + public replace: boolean = false; + public scope: boolean = false; + public template: string = '
'; + + public static factory(): ng.IDirectiveFactory { + const directive: ng.IDirectiveFactory = () => new CalloutActionsDirective(); + return directive; + } + +} + +/** + * @ngdoc interface + * @name ICalloutAttributes + * @module officeuifabric.components.callout + * + * @description + * Those are the attributes that can be used on callout directive. + * + * + * @property {string} uifSeparator - Renders a separating line between content and actions + * @property {string} uifActiontext - Render a separating line between content and actions. Same as `uifSeparator` + * @property {CalloutArrow} uifArrow - Direction of the arrow + * @property {boolean} uifClose - Renders close button + */ +interface ICalloutAttributes extends ng.IAttributes { + uifSeparator: string; + uifActionText: string; + uifArrow: string; + uifClose: boolean; +} + +/** + * @ngdoc interface + * @name ICalloutScope + * @module officeuifabric.components.callout + * + * @description + * This is the scope used by the directive. + * + * @property {CalloutType} uifType - Type of callout to render + * @property {boolean} ngShow - Callout visible or not. + */ +interface ICalloutScope extends ng.IScope { + arrowDirection: string; + hasSeparator: boolean; + uifType: string; + ngShow: boolean; + closeButton: boolean; + isMouseOver: boolean; + closeButtonClicked: boolean; +} + +/** + * @ngdoc directive + * @name uifCallout + * @module officeuifabric.components.callout + * + * @restrict E + * + * @description + * `` is an directive for rendering callout component. + * @see /~https://github.com/OfficeDev/Office-UI-Fabric/tree/master/src/components/Callout + * + * @usage + * + * + * Consider adding a link to learn more at the bottom. + * + */ +export class CalloutDirective implements ng.IDirective { + public restrict: string = 'E'; + public transclude: boolean = true; + public replace: boolean = false; + public template: string = + '
' + + '
'; + + public require: string[] = ['uifCallout']; + + public scope: any = { + ngShow: '=', + uifType: '@' + }; + + public controller: typeof CalloutController = CalloutController; + + public static factory(): ng.IDirectiveFactory { + const directive: ng.IDirectiveFactory = () => new CalloutDirective(); + return directive; + } + + public link(scope: ICalloutScope, instanceElement: ng.IAugmentedJQuery, attrs: ICalloutAttributes, ctrls: any[]): void { + + let calloutController: CalloutController = ctrls[0]; + + attrs.$observe('uifType', (calloutType: string) => { + if (ng.isUndefined(CalloutType[calloutType])) { + calloutController.$log.error('Error [ngOfficeUiFabric] officeuifabric.components.callout - "' + + calloutType + '" is not a valid value for uifType. It should be oobe or peek'); + } + }); + + if (!attrs.uifArrow) { + scope.arrowDirection = 'Left'; + } + + attrs.$observe('uifArrow', (attrArrowDirection: string) => { + + if (ng.isUndefined(CalloutArrow[attrArrowDirection])) { + calloutController.$log.error('Error [ngOfficeUiFabric] officeuifabric.components.callout - "' + + attrArrowDirection + '" is not a valid value for uifArrow. It should be left, right, top, bottom.'); + return; + } + + let capitalizedDirection: string = (attrArrowDirection.charAt(0)).toUpperCase(); + capitalizedDirection += (attrArrowDirection.slice(1)).toLowerCase(); + + scope.arrowDirection = capitalizedDirection; + + }); + + scope.hasSeparator = (!ng.isUndefined(attrs.uifActionText) || !ng.isUndefined(attrs.uifSeparator)); + + if (!ng.isUndefined(attrs.uifClose)) { + scope.closeButton = true; + + let closeButtonElement: ng.IAugmentedJQuery = ng.element( + ''); + + let calloutDiv: JQuery = instanceElement.find('div').eq(0); + calloutDiv.append(closeButtonElement); + + // register close button click + closeButtonElement.bind('click', (eventObject: JQueryEventObject) => { + scope.ngShow = false; + scope.closeButtonClicked = true; + scope.$apply(); + }); + } + + instanceElement.bind('mouseenter', (eventObject: JQueryEventObject) => { + scope.isMouseOver = true; + scope.$apply(); + }); + + instanceElement.bind('mouseleave', (eventObject: JQueryEventObject) => { + scope.isMouseOver = false; + scope.$apply(); + }); + + + scope.$watch('ngShow', (newValue: boolean, oldValue: boolean) => { + // close button closes everytime + let isClosingByButtonClick: boolean = !newValue && scope.closeButtonClicked; + if (isClosingByButtonClick) { + scope.ngShow = scope.closeButtonClicked = false; + return; + } + + if (!newValue) { + scope.ngShow = scope.isMouseOver; + } + }); + + scope.$watch('isMouseOver', (newVal: boolean, oldVal: boolean) => { + // mouse was over element and now it's out + if (!newVal && oldVal) { + // when there's button, only button can close. + if (!scope.closeButton) { + scope.ngShow = false; + } + } + }); + } +} + +/** + * @ngdoc module + * @name officeuifabric.components.callout + * + * @description + * Callout + * + */ +export var module: ng.IModule = ng.module('officeuifabric.components.callout', ['officeuifabric.components']) + .directive('uifCallout', CalloutDirective.factory()) + .directive('uifCalloutHeader', CalloutHeaderDirective.factory()) + .directive('uifCalloutContent', CalloutContentDirective.factory()) + .directive('uifCalloutActions', CalloutActionsDirective.factory()); diff --git a/src/components/callout/calloutTypeEnum.ts b/src/components/callout/calloutTypeEnum.ts new file mode 100644 index 0000000..db981dd --- /dev/null +++ b/src/components/callout/calloutTypeEnum.ts @@ -0,0 +1,19 @@ +'use strict'; + +/** + * + * Enum for callout type. It is intended to use as string as `uif-type` attribute value on `uif-callout` directive. + * + * @readonly + * @enum {string} + * @usage + * + * This is used to generate the string that you pass into the directive. Specifically, the string is passed + * to the `uif-type` attribute. To evaluate the enum value as a string: + * + * let type: string = CalloutType[CalloutType.peek]; + */ +export enum CalloutType { + oobe, + peek +} diff --git a/src/components/callout/demo/index.html b/src/components/callout/demo/index.html new file mode 100644 index 0000000..82a36a4 --- /dev/null +++ b/src/components/callout/demo/index.html @@ -0,0 +1,237 @@ + + + + + ngOfficeUiFabric | uif-Callout demo + + + + + + + + + + + + + +

Callout Demo | <uif-callout>

+ In order for this demo to work you must first build the library in debug mode. + + +

To render callout, use the following markup: +
+

+  <uif-callout uif-arrow="left">
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom.</uif-callout-content>
+    <uif-callout-actions>
+      <a href="#" class="ms-Callout-link ms-Link ms-Link--hero">Learn more</a>
+    </uif-callout-actions>
+  </uif-callout>
+      
+

+ +

+ + All of your favorite people + Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom. + + Learn more + + +

+ + +

+ This markup will render OOBE callout: +
+

+  <uif-callout uif-arrow="top" uif-type="oobe">
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>People automatically puts together all of the people you care most about in one place.</uif-callout-content>
+    <uif-callout-actions>
+      <button class="ms-Callout-button ms-Button ms-Button--primary">
+        <span class="ms-Button-label">More</span>
+        <span class="ms-Button-description">Description of the action this button takes</span>
+      </button>
+      <button class="ms-Callout-button ms-Button">
+        <span class="ms-Button-label">Got it</span>
+        <span class="ms-Button-description">Description of the action this button takes</span>
+      </button>
+    </uif-callout-actions>
+  </uif-callout>
+      
+

+ +

+ + All of your favorite people + People automatically puts together all of the people you care most about in one place. + + + + + +

+ +

+ This markup will render Peek callout: +
+

+  <uif-callout uif-arrow="right" uif-type="peek">
+    <uif-callout-header>Uploaded 2 items to <span class="ms-Link">Alton's OneDrive</span></uif-callout-header>
+    <uif-callout-actions>
+      <button class="ms-Callout-button ms-Button">
+        <span class="ms-Button-label">More</span>
+        <span class="ms-Button-description">Description of the action this button takes</span>
+      </button>
+    </uif-callout-actions>
+  </uif-callout>
+      
+

+ +

+ + Uploaded 2 items to Alton's OneDrive + + + + +

+ + + +

+ Separator can be rendered between content and actions containers. uif-actiontext and uif-separator server the same purpose. +
+

+  <uif-callout uif-action-text>
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>People automatically puts together all of the people you care most about in one place.</uif-callout-header>
+    <uif-callout-actions>
+      <button class="ms-Callout-action ms-Button ms-Button--command">
+        <span class="ms-Callout-actionText ms-Button-icon"><i class="ms-Icon ms-Icon--check"></i></span>
+        <span class="ms-Button-label">Save</span>
+      </button>
+      <button class="ms-Callout-action ms-Button ms-Button--command">
+        <span class="ms-Button-icon"><i class="ms-Icon ms-Icon--x"></i></span>
+        <span class="ms-Callout-actionText ms-Button-label">Cancel</span>
+      </button>
+    </uif-callout-actions>
+  </uif-callout>
+      
+

+ +

+ + All of your favorite people + People automatically puts together all of the people you care most about in one place. + + + + + +

+ + + +
+

Callout supports ngShow directive. Hover over the link to test. +
+

+  <uif-callout ng-show="vm.firstVisible" uif-arrow="left">
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom.</uif-callout-content>
+    <uif-callout-actions>
+      <a href="#" class="ms-Callout-link ms-Link ms-Link--hero">Learn more</a>
+    </uif-callout-actions>
+  </uif-callout>
+        
+

+ +
+ + All of your favorite people + Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom. + + Learn more + + +
+ + + +

Callout can have close button, with help of uif-close attribute. +
+

+  <uif-callout ng-show="vm.secondVisible" uif-arrow="left" uif-close>
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom.</uif-callout-content>
+    <uif-callout-actions>
+      <a href="#" class="ms-Callout-link ms-Link ms-Link--hero">Learn more</a>
+    </uif-callout-actions>
+  </uif-callout>
+        
+

+
+ +
+
+ + All of your favorite people + Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom. + + Learn more + + +
+ + + +

When ngShow is used without uif-close, callout is closed when mouse leaves from callout: +
+

+  <uif-callout ng-show="vm.thirdVisible" uif-arrow="left">
+    <uif-callout-header>All of your favorite people</uif-callout-header>
+    <uif-callout-content>Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom.</uif-callout-content>
+    <uif-callout-actions>
+      <a href="#" class="ms-Callout-link ms-Link ms-Link--hero">Learn more</a>
+    </uif-callout-actions>
+  </uif-callout>
+        
+

+
+ +
+
+ + All of your favorite people + Message body is optional. If help documentation is available, consider adding a link to learn more at the bottom. + + Learn more + + +
+
+ + + \ No newline at end of file diff --git a/src/components/callout/demo/index.js b/src/components/callout/demo/index.js new file mode 100644 index 0000000..0e9046f --- /dev/null +++ b/src/components/callout/demo/index.js @@ -0,0 +1,26 @@ +'use strict'; + +var demoApp = angular.module('demoApp', [ + 'officeuifabric.core', + 'officeuifabric.components.callout' +]); + +demoApp.controller('calloutDemoController',['$scope', '$timeout', calloutDemoController]); + +function calloutDemoController($scope, $timeout){ + $scope.vm = { + firstVisible : false, + secondVisible: false, + thirdVisible: false + }; + + $scope.toggleClick = function(){ + $scope.vm.secondVisible = !$scope.vm.secondVisible; + } + + $scope.thirdToggleClick = function(){ + $timeout(function(){ + $scope.vm.thirdVisible = !($scope.vm.thirdVisible); + }, 1000); + }; +} \ No newline at end of file diff --git a/src/core/components.ts b/src/core/components.ts index d69f595..701d8ee 100644 --- a/src/core/components.ts +++ b/src/core/components.ts @@ -1,6 +1,7 @@ 'use strict'; import * as ng from 'angular'; +import * as calloutModule from '../components/callout/calloutDirective'; import * as choicefieldModule from '../components/choicefield/choicefieldDirective'; import * as contextualMenuModule from '../components/contextualmenu/contextualMenu'; import * as dropdownModule from '../components/dropdown/dropdownDirective'; @@ -24,6 +25,7 @@ import * as toggleModule from '../components/toggle/toggleDirective'; * */ export var module: ng.IModule = ng.module('officeuifabric.components', [ + calloutModule.module.name, choicefieldModule.module.name, contextualMenuModule.module.name, dropdownModule.module.name,