Skip to content

Commit ef2a8d5

Browse files
committed
Merge pull request #99 from nervetattoo/responsive-dropdown-toggle
Responsive dropdown toggle
2 parents e19fceb + 48d2a56 commit ef2a8d5

File tree

5 files changed

+137
-79
lines changed

5 files changed

+137
-79
lines changed

src/dropdownToggle/dropdownToggle.js

+35-7
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,24 @@
1010
</li>
1111
</ul>
1212
*/
13-
angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position' ])
13+
angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position', 'mm.foundation.mediaQueries' ])
1414

15-
.directive('dropdownToggle', ['$document', '$location', '$position', function ($document, $location, $position) {
15+
.controller('DropdownToggleController', ['$scope', '$attrs', 'mediaQueries', function($scope, $attrs, mediaQueries) {
16+
this.small = function() {
17+
return mediaQueries.small() && !mediaQueries.medium();
18+
};
19+
}])
20+
21+
.directive('dropdownToggle', ['$document', '$window', '$location', '$position', function ($document, $window, $location, $position) {
1622
var openElement = null,
1723
closeMenu = angular.noop;
1824
return {
1925
restrict: 'CA',
2026
scope: {
2127
dropdownToggle: '@'
2228
},
23-
link: function(scope, element, attrs) {
29+
controller: 'DropdownToggleController',
30+
link: function(scope, element, attrs, controller) {
2431
var dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));
2532

2633
scope.$watch('$location.path', function() { closeMenu(); });
@@ -41,10 +48,31 @@ angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position' ])
4148
var offset = $position.offset(element);
4249
var parentOffset = $position.offset(angular.element(dropdown[0].offsetParent));
4350

44-
dropdown.css({
45-
left: offset.left - parentOffset.left + 'px',
46-
top: offset.top - parentOffset.top + offset.height + 'px'
47-
});
51+
var dropdownWidth = dropdown.prop('offsetWidth');
52+
53+
var css = {
54+
top: offset.top - parentOffset.top + offset.height + 'px'
55+
};
56+
57+
if (controller.small()) {
58+
css.left = Math.max((parentOffset.width - dropdownWidth) / 2, 8) + 'px';
59+
css.position = 'absolute';
60+
css.width = '95%';
61+
css['max-width'] = 'none';
62+
}
63+
else {
64+
var left = Math.round(offset.left - parentOffset.left);
65+
var rightThreshold = $window.innerWidth - dropdownWidth - 8;
66+
if (left > rightThreshold) {
67+
left = rightThreshold;
68+
dropdown.removeClass('left').addClass('right');
69+
}
70+
css.left = left + 'px';
71+
css.position = null;
72+
css['max-width'] = null;
73+
}
74+
75+
dropdown.css(css);
4876

4977
openElement = element;
5078
closeMenu = function (event) {

src/dropdownToggle/test/dropdownToggle.spec.js

+37-13
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
describe('dropdownToggle', function() {
2-
var $compile, $rootScope, $document, $location, elm, toggleElm, targetElm;
3-
4-
beforeEach(module('mm.foundation.dropdownToggle'));
5-
6-
beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$location_) {
7-
$compile = _$compile_;
8-
$rootScope = _$rootScope_;
9-
$document = _$document_;
10-
$location = _$location_;
11-
$scope = $rootScope.$new();
12-
13-
}));
2+
var $compile, $rootScope, $document, $location, $window, elm, toggleElm, targetElm;
143

154
function dropdown(id) {
165
if (!id) {
@@ -29,6 +18,17 @@ describe('dropdownToggle', function() {
2918
}
3019
});
3120

21+
beforeEach(module('mm.foundation.dropdownToggle'));
22+
23+
beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$location_, _$window_) {
24+
$compile = _$compile_;
25+
$rootScope = _$rootScope_;
26+
$document = _$document_;
27+
$window = _$window_;
28+
$location = _$location_;
29+
$scope = $rootScope.$new();
30+
}));
31+
3232
describe('with a single dropdown', function() {
3333
beforeEach(function() {
3434
elm = dropdown();
@@ -81,5 +81,29 @@ describe('dropdownToggle', function() {
8181
elm2.remove();
8282
});
8383
});
84+
85+
describe('on a mobile device', function() {
86+
var trueFn = Boolean.bind(null, true);
87+
var falseFn = Boolean.bind(null, false);
88+
89+
angular.module('mm.foundation.dropdownToggle')
90+
.factory('mediaQueries', function() {
91+
return {small: trueFn, medium: falseFn, large: falseFn };
92+
});
93+
94+
it('should be full-width', function() {
95+
elm = dropdown('responsive');
96+
toggleElm = elm.find('a');
97+
targetElm = elm.find('ul');
98+
99+
toggleElm.click();
100+
101+
expect(targetElm.css('position')).toBe('absolute');
102+
expect(targetElm.css('max-width')).toBe('none');
103+
104+
var expectedWidth = Math.round($window.innerWidth * 0.95);
105+
expect(targetElm.css('width')).toBe(expectedWidth + 'px');
106+
});
107+
});
84108
});
85-
109+

src/mediaQueries/docs/readme.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
A module providing factories to aid in building responsive components.
2+
3+
The `matchMedia` factory provides either a shim for browsers without `window.matchMedia`, or `windows.matchMedia` itself.
4+
5+
The `mediaQueries` factory provides methods to detect if screen falls under the **small**, **medium** or **large** breakpoint.

src/mediaQueries/mediaQueries.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
angular.module("mm.foundation.mediaQueries", [])
2+
.factory('matchMedia', ['$document', '$window', function($document, $window) {
3+
// MatchMedia for IE <= 9
4+
return $window.matchMedia || (function matchMedia(doc, undefined){
5+
var bool,
6+
docElem = doc.documentElement,
7+
refNode = docElem.firstElementChild || docElem.firstChild,
8+
// fakeBody required for <FF4 when executed in <head>
9+
fakeBody = doc.createElement("body"),
10+
div = doc.createElement("div");
11+
12+
div.id = "mq-test-1";
13+
div.style.cssText = "position:absolute;top:-100em";
14+
fakeBody.style.background = "none";
15+
fakeBody.appendChild(div);
16+
17+
return function (q) {
18+
div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
19+
docElem.insertBefore(fakeBody, refNode);
20+
bool = div.offsetWidth === 42;
21+
docElem.removeChild(fakeBody);
22+
return {
23+
matches: bool,
24+
media: q
25+
};
26+
};
27+
28+
}($document[0]));
29+
}])
30+
.factory('mediaQueries', ['$document', 'matchMedia', function($document, matchMedia) {
31+
var head = angular.element($document[0].querySelector('head'));
32+
head.append('<meta class="foundation-mq-topbar" />');
33+
head.append('<meta class="foundation-mq-small" />');
34+
head.append('<meta class="foundation-mq-medium" />');
35+
head.append('<meta class="foundation-mq-large" />');
36+
37+
var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g;
38+
var queries = {
39+
topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''),
40+
small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''),
41+
medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''),
42+
large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '')
43+
};
44+
45+
return {
46+
topbarBreakpoint: function () {
47+
return !matchMedia(queries.topbar).matches;
48+
},
49+
small: function () {
50+
return matchMedia(queries.small).matches;
51+
},
52+
medium: function () {
53+
return matchMedia(queries.medium).matches;
54+
},
55+
large: function () {
56+
return matchMedia(queries.large).matches;
57+
}
58+
};
59+
}]);

src/topbar/topbar.js

+1-59
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,5 @@
11

2-
angular.module("mm.foundation.topbar", [])
3-
.factory('mediaQueries', ['$document', '$window', function($document, $window){
4-
var head = angular.element($document[0].querySelector('head'));
5-
head.append('<meta class="foundation-mq-topbar" />');
6-
head.append('<meta class="foundation-mq-small" />');
7-
head.append('<meta class="foundation-mq-medium" />');
8-
head.append('<meta class="foundation-mq-large" />');
9-
10-
// MatchMedia for IE <= 9
11-
var matchMedia = $window.matchMedia || (function(doc, undefined){
12-
var bool,
13-
docElem = doc.documentElement,
14-
refNode = docElem.firstElementChild || docElem.firstChild,
15-
// fakeBody required for <FF4 when executed in <head>
16-
fakeBody = doc.createElement("body"),
17-
div = doc.createElement("div");
18-
19-
div.id = "mq-test-1";
20-
div.style.cssText = "position:absolute;top:-100em";
21-
fakeBody.style.background = "none";
22-
fakeBody.appendChild(div);
23-
24-
return function (q) {
25-
div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
26-
docElem.insertBefore(fakeBody, refNode);
27-
bool = div.offsetWidth === 42;
28-
docElem.removeChild(fakeBody);
29-
return {
30-
matches: bool,
31-
media: q
32-
};
33-
};
34-
35-
}($document[0]));
36-
37-
var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g;
38-
var queries = {
39-
topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''),
40-
small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''),
41-
medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''),
42-
large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '')
43-
};
44-
45-
return {
46-
topbarBreakpoint: function () {
47-
return !matchMedia(queries.topbar).matches;
48-
},
49-
small: function () {
50-
return matchMedia(queries.small).matches;
51-
},
52-
medium: function () {
53-
return matchMedia(queries.medium).matches;
54-
},
55-
large: function () {
56-
return matchMedia(queries.large).matches;
57-
}
58-
};
59-
60-
}])
2+
angular.module("mm.foundation.topbar", ['mm.foundation.mediaQueries'])
613
.factory('closest', [function(){
624
return function(el, selector) {
635
var matchesSelector = function (node, selector) {

0 commit comments

Comments
 (0)