Skip to content

Commit 0e6fb83

Browse files
committed
About modal directive and unit tests
1 parent f83713e commit 0e6fb83

File tree

6 files changed

+361
-0
lines changed

6 files changed

+361
-0
lines changed

Gruntfile.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ module.exports = function (grunt) {
191191
src: ['filters/**/*.html'],
192192
dest: 'templates/filters.js'
193193
},
194+
'patternfly.modals': {
195+
cwd: 'src/',
196+
src: ['modals/**/*.html'],
197+
dest: 'templates/modals.js'
198+
},
194199
'patternfly.sort': {
195200
cwd: 'src/',
196201
src: ['sort/**/*.html'],
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @ngdoc directive
3+
* @name patternfly.modals.directive:pfAboutModal
4+
*
5+
* @description
6+
* Directive for rendering modal windows.
7+
*
8+
* @param {string=} additionalInfo Text explaining the version or copyright
9+
* @param {string=} copyright Product copyright information
10+
* @param {string=} imgAlt The alt text for the corner grahpic
11+
* @param {string=} imgSrc The source for the corner grahpic
12+
* @param {boolean=} isOpen Flag indicating that the modal should be opened
13+
* @param {function=} onClose Function to call when modal is closed
14+
* @param {object=} productInfo data for the modal:<br/>
15+
* <ul style='list-style-type: none'>
16+
* <li>.product - the product label
17+
* <li>.version - the product version
18+
* </ul>
19+
* @param {string=} title The product title for the modal
20+
*
21+
* @example
22+
<example module="patternfly.modals">
23+
<file name="index.html">
24+
<div ng-controller="ModalCtrl">
25+
<button ng-click="open()" class="btn btn-default">Launch About Modal</button>
26+
<div pf-about-modal is-open="isOpen" on-close="onClose()" additional-info="additionalInfo"
27+
product-info="productInfo" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc"></div>
28+
</div>
29+
</file>
30+
<file name="script.js">
31+
angular.module('patternfly.modals').controller('ModalCtrl', function ($scope) {
32+
$scope.additionalInfo = "Donec consequat dignissim neque, sed suscipit quam egestas in. Fusce bibendum " +
33+
"laoreet lectus commodo interdum. Vestibulum odio ipsum, tristique et ante vel, iaculis placerat nulla. " +
34+
"Suspendisse iaculis urna feugiat lorem semper, ut iaculis risus tempus.";
35+
$scope.copyright = "Trademark and Copyright Information";
36+
$scope.imgAlt = "Patternfly Symbol";
37+
$scope.imgSrc = "img/logo-alt.svg";
38+
$scope.title = "Product Title";
39+
$scope.productInfo = [
40+
{ name: 'Version', value: '1.0.0.0.20160819142038_51be77c' },
41+
{ name: 'Server Name', value: 'Localhost' },
42+
{ name: 'User Name', value: 'admin' },
43+
{ name: 'User Role', value: 'Administrator' }];
44+
$scope.open = function () {
45+
$scope.isOpen = true;
46+
}
47+
$scope.onClose = function() {
48+
$scope.isOpen = false;
49+
}
50+
});
51+
</file>
52+
</example>
53+
*/
54+
angular.module('patternfly.modals')
55+
56+
.directive("pfAboutModalTransclude", function ($parse) {
57+
'use strict';
58+
return {
59+
link: function (scope, element, attrs) {
60+
element.append($parse(attrs.pfAboutModalTransclude)(scope));
61+
}
62+
};
63+
})
64+
65+
.directive('pfAboutModal', function () {
66+
'use strict';
67+
return {
68+
restrict: 'A',
69+
scope: {
70+
additionalInfo: '=?',
71+
copyright: '=?',
72+
close: "&onClose",
73+
imgAlt: '=?',
74+
imgSrc: '=?',
75+
isOpen: '=?',
76+
productInfo: '=',
77+
title: '=?'
78+
},
79+
templateUrl: 'modals/about-modal.html',
80+
transclude: true,
81+
controller: ['$scope', '$modal', '$transclude', function ($scope, $modal, $transclude) {
82+
if ($scope.isOpen === undefined) {
83+
$scope.isOpen = false;
84+
}
85+
86+
// The ui-bootstrap modal only supports either template or templateUrl as a way to specify the content.
87+
// When the content is retrieved, it is compiled and linked against the provided scope by the $modal service.
88+
// Unfortunately, there is no way to provide transclusion there.
89+
//
90+
// The solution below embeds a placeholder directive (i.e., pfAboutModalTransclude) to append the transcluded DOM.
91+
// The transcluded DOM is from a different location than the modal, so it needs to be handed over to the
92+
// placeholder directive. Thus, we're passing the actual DOM, not the parsed HTML.
93+
$scope.openModal = function () {
94+
$modal.open({
95+
controller: ['$scope', '$modalInstance', 'content', function ($scope, $modalInstance, content) {
96+
$scope.template = content;
97+
$scope.close = function () {
98+
$modalInstance.close();
99+
};
100+
$scope.$watch(
101+
function () {
102+
return $scope.isOpen;
103+
},
104+
function (newValue) {
105+
if (newValue === false) {
106+
$modalInstance.close();
107+
}
108+
}
109+
);
110+
}],
111+
resolve: {
112+
content: function () {
113+
var transcludedContent;
114+
$transclude(function (clone) {
115+
transcludedContent = clone;
116+
});
117+
return transcludedContent;
118+
}
119+
},
120+
scope: $scope,
121+
templateUrl: "about-modal-template.html"
122+
})
123+
.result.then(
124+
function () {
125+
$scope.close(); // closed
126+
},
127+
function () {
128+
$scope.close(); // dismissed
129+
}
130+
);
131+
};
132+
}],
133+
link: function (scope, element, attrs) {
134+
// watching isOpen attribute to dispay modal when needed
135+
var isOpenListener = scope.$watch('isOpen', function (newVal, oldVal) {
136+
if (newVal === true) {
137+
scope.openModal();
138+
}
139+
});
140+
scope.$on('$destroy', isOpenListener);
141+
}
142+
};
143+
});

src/modals/about-modal.html

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script type="text/ng-template" id="about-modal-template.html">
2+
<div class="about-modal-pf">
3+
<div class="modal-header">
4+
<button type="button" class="close" ng-click="close()" aria-hidden="true">
5+
<span class="pficon pficon-close"></span>
6+
</button>
7+
</div>
8+
<div class="modal-body">
9+
<h1 ng-if="title">{{title}}</h1>
10+
<div ng-if="productInfo && productInfo.length > 0" class="product-versions-pf">
11+
<ul class="list-unstyled">
12+
<li ng-repeat="info in productInfo"><strong>{{info.name}}</strong> {{info.value}}</li>
13+
</ul>
14+
</div>
15+
<div pf-about-modal-transclude="template" class="product-versions-pf"></div>
16+
<div ng-if="additionalInfo" class="product-versions-pf">{{additionalInfo}}</div>
17+
<div ng-if="copyright" class="trademark-pf">{{copyright}}</div>
18+
</div>
19+
<div class="modal-footer">
20+
<img ng-if="imgSrc" ng-src="{{imgSrc}}" alt="{{imgAlt}}"/>
21+
</div>
22+
</div>
23+
</script>

src/modals/modals.module.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @name patternfly
3+
*
4+
* @description
5+
* Modal module for patternfly.
6+
*
7+
*/
8+
angular.module('patternfly.modals', ['ui.bootstrap.modal', 'ui.bootstrap.tpls']);

src/patternfly.module.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ angular.module('patternfly', [
99
'patternfly.card',
1010
'patternfly.filters',
1111
'patternfly.form',
12+
'patternfly.modals',
1213
'patternfly.navigation',
1314
'patternfly.notification',
1415
'patternfly.select',

test/modals/about-modal.spec.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
describe('Directive: pfABoutModal', function () {
2+
var $scope;
3+
var $compile;
4+
5+
// load the controller's module
6+
beforeEach(module(
7+
'patternfly.modals',
8+
'modals/about-modal.html'
9+
));
10+
11+
beforeEach(inject(function (_$compile_, _$rootScope_) {
12+
$compile = _$compile_;
13+
$scope = _$rootScope_;
14+
}));
15+
16+
var compileHtml = function (markup, scope) {
17+
var element = angular.element(markup);
18+
$compile(element)(scope);
19+
scope.$digest();
20+
return element;
21+
};
22+
23+
var closeModal = function(scope) {
24+
scope.isOpen = false;
25+
scope.$digest();
26+
27+
// Although callbacks are executed properly, the modal is not removed in this
28+
// environment -- must remove it manually to mimic UI Bootstrap.
29+
var modal = getModal();
30+
if (modal) {
31+
modal.remove();
32+
}
33+
var modalBackdrop = angular.element(document.querySelector('.modal-backdrop'));
34+
if (modalBackdrop) {
35+
modalBackdrop.remove();
36+
}
37+
};
38+
39+
// Modal elements are located in a template, so wait until modal is shown.
40+
var getModal = function () {
41+
return angular.element(document.querySelector('.modal'));
42+
};
43+
44+
var openModal = function(scope) {
45+
scope.isOpen = true;
46+
scope.$digest();
47+
};
48+
49+
beforeEach(function () {
50+
closeModal($scope);
51+
$scope.copyright = "Copyright Information";
52+
$scope.imgAlt = "Patternfly Symbol";
53+
$scope.imgSrc = "img/logo-alt.svg";
54+
$scope.title = "Product Title";
55+
$scope.isOpen = true;
56+
$scope.productInfo = [
57+
{ product: 'Label', version: 'Version' },
58+
{ product: 'Label', version: 'Version' },
59+
{ product: 'Label', version: 'Version' },
60+
{ product: 'Label', version: 'Version' },
61+
{ product: 'Label', version: 'Version' },
62+
{ product: 'Label', version: 'Version' },
63+
{ product: 'Label', version: 'Version' }];
64+
$scope.open = function () {
65+
$scope.isOpen = true;
66+
}
67+
$scope.onClose = function() {
68+
$scope.isOpen = false;
69+
}
70+
});
71+
72+
it('should invoke the onClose callback when close button is clicked', function () {
73+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
74+
compileHtml(modalHtml, $scope);
75+
var closeButton = angular.element(getModal()).find('button');
76+
eventFire(closeButton[0], 'click');
77+
$scope.$digest();
78+
expect($scope.isOpen).toBe(false);
79+
});
80+
81+
it('should open the about modal via an external button click', function () {
82+
$scope.isOpen = false;
83+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
84+
compileHtml(modalHtml, $scope);
85+
var buttonHtml = '<button ng-click="open()" class="btn btn-default">Launch about modal</button>';
86+
var closeButton = compileHtml(buttonHtml, $scope);
87+
eventFire(closeButton[0], 'click');
88+
$scope.$digest();
89+
expect($scope.isOpen).toBe(true);
90+
expect(angular.element(getModal()).find('h1').length).toBe(1);
91+
});
92+
93+
it('should open the about modal programmatically', function () {
94+
$scope.isOpen = false;
95+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
96+
compileHtml(modalHtml, $scope);
97+
expect(angular.element(getModal()).find('h1').length).toBe(0);
98+
openModal($scope);
99+
expect(angular.element(getModal()).find('h1').length).toBe(1);
100+
});
101+
102+
it('should not open the about modal', function () {
103+
var modalHtml = '<div pf-about-modal is-open="false" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
104+
compileHtml(modalHtml, $scope);
105+
expect(angular.element(getModal()).find('h1').length).toBe(0);
106+
expect(angular.element(getModal()).find('.trademark-pf').length).toBe(0);
107+
});
108+
109+
it('should set the product title', function () {
110+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
111+
compileHtml(modalHtml, $scope);
112+
expect(angular.element(getModal()).find('h1').html()).toBe('Product Title');
113+
});
114+
115+
it('should not show product title when a title is not supplied', function () {
116+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
117+
compileHtml(modalHtml, $scope);
118+
expect(angular.element(getModal()).find('h1').length).toBe(0);
119+
});
120+
121+
it('should set the product copyright', function () {
122+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
123+
compileHtml(modalHtml, $scope);
124+
expect(angular.element(getModal()).find('.trademark-pf').html()).toBe('Copyright Information');
125+
});
126+
127+
it('should not show product copyright when a copyright is not supplied', function () {
128+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
129+
compileHtml(modalHtml, $scope);
130+
expect(angular.element(getModal()).find('.trademark-pf').length).toBe(0);
131+
});
132+
133+
it('should set the corner graphic alt text', function () {
134+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
135+
compileHtml(modalHtml, $scope);
136+
var footer = angular.element(getModal()).find('.modal-footer');
137+
expect(angular.element(footer).find('img').attr('alt')).toBe('Patternfly Symbol');
138+
});
139+
140+
it('should not show alt text for corner graphic when imgAlt is not supplied', function () {
141+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-src="imgSrc" product-info="productInfo"></div>';
142+
compileHtml(modalHtml, $scope);
143+
var footer = angular.element(getModal()).find('.modal-footer');
144+
expect(angular.element(footer).find('img').attr('alt').length).toBe(0);
145+
});
146+
147+
it('should set the corner graphic src', function () {
148+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
149+
compileHtml(modalHtml, $scope);
150+
var footer = angular.element(getModal()).find('.modal-footer');
151+
expect(angular.element(footer).find('img').attr('src')).toBe('img/logo-alt.svg');
152+
});
153+
154+
it('should not show corner graphic when imgSrc is not supplied', function () {
155+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" product-info="productInfo"></div>';
156+
compileHtml(modalHtml, $scope);
157+
var footer = angular.element(getModal()).find('.modal-footer');
158+
expect(angular.element(footer).find('img').length).toBe(0);
159+
});
160+
161+
it('should show simple content', function () {
162+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc" product-info="productInfo"></div>';
163+
compileHtml(modalHtml, $scope);
164+
var transclude = angular.element(getModal()).find('.product-versions-pf');
165+
expect(angular.element(transclude).find('ul').length).toBe(1);
166+
});
167+
168+
it('should show custom content', function () {
169+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc"><ul class="list-unstyled"><li><strong>Label</strong> Version</li></ul></div>';
170+
compileHtml(modalHtml, $scope);
171+
var transclude = angular.element(getModal()).find('.product-versions-pf');
172+
expect(angular.element(transclude).find('ul').length).toBe(1);
173+
});
174+
175+
it('should not show content', function () {
176+
var modalHtml = '<div pf-about-modal is-open="isOpen" on-close="onClose()" title="title" copyright="copyright" img-alt="imgAlt" img-src="imgSrc"></div>';
177+
compileHtml(modalHtml, $scope);
178+
var transclude = angular.element(getModal()).find('.product-versions-pf');
179+
expect(angular.element(transclude).find('ul').length).toBe(0);
180+
});
181+
});

0 commit comments

Comments
 (0)