Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Responsive dropdown toggle #99

Merged
merged 5 commits into from
Aug 27, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/dropdownToggle/dropdownToggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@
</li>
</ul>
*/
angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position' ])
angular.module('mm.foundation.dropdownToggle', [ 'mm.foundation.position', 'mm.foundation.mediaQueries' ])

.directive('dropdownToggle', ['$document', '$location', '$position', function ($document, $location, $position) {
.controller('DropdownToggleController', ['$scope', '$attrs', 'mediaQueries', function($scope, $attrs, mediaQueries) {
this.small = function() {
return mediaQueries.small() && !mediaQueries.medium();
};
}])

.directive('dropdownToggle', ['$document', '$window', '$location', '$position', function ($document, $window, $location, $position) {
var openElement = null,
closeMenu = angular.noop;
return {
restrict: 'CA',
scope: {
dropdownToggle: '@'
},
link: function(scope, element, attrs) {
controller: 'DropdownToggleController',
link: function(scope, element, attrs, controller) {
var dropdown = angular.element($document[0].querySelector(scope.dropdownToggle));

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

dropdown.css({
left: offset.left - parentOffset.left + 'px',
top: offset.top - parentOffset.top + offset.height + 'px'
});
var dropdownWidth = dropdown.prop('offsetWidth');

var css = {
top: offset.top - parentOffset.top + offset.height + 'px'
};

if (controller.small()) {
css.left = Math.max((parentOffset.width - dropdownWidth) / 2, 8) + 'px';
css.position = 'absolute';
css.width = '95%';
css['max-width'] = 'none';
}
else {
var left = Math.round(offset.left - parentOffset.left);
var rightThreshold = $window.innerWidth - dropdownWidth - 8;
if (left > rightThreshold) {
left = rightThreshold;
dropdown.removeClass('left').addClass('right');
}
css.left = left + 'px';
css.position = null;
css['max-width'] = null;
}

dropdown.css(css);

openElement = element;
closeMenu = function (event) {
Expand Down
50 changes: 37 additions & 13 deletions src/dropdownToggle/test/dropdownToggle.spec.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
describe('dropdownToggle', function() {
var $compile, $rootScope, $document, $location, elm, toggleElm, targetElm;

beforeEach(module('mm.foundation.dropdownToggle'));

beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$location_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$document = _$document_;
$location = _$location_;
$scope = $rootScope.$new();

}));
var $compile, $rootScope, $document, $location, $window, elm, toggleElm, targetElm;

function dropdown(id) {
if (!id) {
Expand All @@ -29,6 +18,17 @@ describe('dropdownToggle', function() {
}
});

beforeEach(module('mm.foundation.dropdownToggle'));

beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$location_, _$window_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$document = _$document_;
$window = _$window_;
$location = _$location_;
$scope = $rootScope.$new();
}));

describe('with a single dropdown', function() {
beforeEach(function() {
elm = dropdown();
Expand Down Expand Up @@ -81,5 +81,29 @@ describe('dropdownToggle', function() {
elm2.remove();
});
});

describe('on a mobile device', function() {
var trueFn = Boolean.bind(null, true);
var falseFn = Boolean.bind(null, false);

angular.module('mm.foundation.dropdownToggle')
.factory('mediaQueries', function() {
return {small: trueFn, medium: falseFn, large: falseFn };
});

it('should be full-width', function() {
elm = dropdown('responsive');
toggleElm = elm.find('a');
targetElm = elm.find('ul');

toggleElm.click();

expect(targetElm.css('position')).toBe('absolute');
expect(targetElm.css('max-width')).toBe('none');

var expectedWidth = Math.round($window.innerWidth * 0.95);
expect(targetElm.css('width')).toBe(expectedWidth + 'px');
});
});
});

5 changes: 5 additions & 0 deletions src/mediaQueries/docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
A module providing factories to aid in building responsive components.

The `matchMedia` factory provides either a shim for browsers without `window.matchMedia`, or `windows.matchMedia` itself.

The `mediaQueries` factory provides methods to detect if screen falls under the **small**, **medium** or **large** breakpoint.
59 changes: 59 additions & 0 deletions src/mediaQueries/mediaQueries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
angular.module("mm.foundation.mediaQueries", [])
.factory('matchMedia', ['$document', '$window', function($document, $window) {
// MatchMedia for IE <= 9
return $window.matchMedia || (function matchMedia(doc, undefined){
var bool,
docElem = doc.documentElement,
refNode = docElem.firstElementChild || docElem.firstChild,
// fakeBody required for <FF4 when executed in <head>
fakeBody = doc.createElement("body"),
div = doc.createElement("div");

div.id = "mq-test-1";
div.style.cssText = "position:absolute;top:-100em";
fakeBody.style.background = "none";
fakeBody.appendChild(div);

return function (q) {
div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
docElem.insertBefore(fakeBody, refNode);
bool = div.offsetWidth === 42;
docElem.removeChild(fakeBody);
return {
matches: bool,
media: q
};
};

}($document[0]));
}])
.factory('mediaQueries', ['$document', 'matchMedia', function($document, matchMedia) {
var head = angular.element($document[0].querySelector('head'));
head.append('<meta class="foundation-mq-topbar" />');
head.append('<meta class="foundation-mq-small" />');
head.append('<meta class="foundation-mq-medium" />');
head.append('<meta class="foundation-mq-large" />');

var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g;
var queries = {
topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''),
small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''),
medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''),
large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '')
};

return {
topbarBreakpoint: function () {
return !matchMedia(queries.topbar).matches;
},
small: function () {
return matchMedia(queries.small).matches;
},
medium: function () {
return matchMedia(queries.medium).matches;
},
large: function () {
return matchMedia(queries.large).matches;
}
};
}]);
60 changes: 1 addition & 59 deletions src/topbar/topbar.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,5 @@

angular.module("mm.foundation.topbar", [])
.factory('mediaQueries', ['$document', '$window', function($document, $window){
var head = angular.element($document[0].querySelector('head'));
head.append('<meta class="foundation-mq-topbar" />');
head.append('<meta class="foundation-mq-small" />');
head.append('<meta class="foundation-mq-medium" />');
head.append('<meta class="foundation-mq-large" />');

// MatchMedia for IE <= 9
var matchMedia = $window.matchMedia || (function(doc, undefined){
var bool,
docElem = doc.documentElement,
refNode = docElem.firstElementChild || docElem.firstChild,
// fakeBody required for <FF4 when executed in <head>
fakeBody = doc.createElement("body"),
div = doc.createElement("div");

div.id = "mq-test-1";
div.style.cssText = "position:absolute;top:-100em";
fakeBody.style.background = "none";
fakeBody.appendChild(div);

return function (q) {
div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
docElem.insertBefore(fakeBody, refNode);
bool = div.offsetWidth === 42;
docElem.removeChild(fakeBody);
return {
matches: bool,
media: q
};
};

}($document[0]));

var regex = /^[\/\\'"]+|(;\s?})+|[\/\\'"]+$/g;
var queries = {
topbar: getComputedStyle(head[0].querySelector('meta.foundation-mq-topbar')).fontFamily.replace(regex, ''),
small : getComputedStyle(head[0].querySelector('meta.foundation-mq-small')).fontFamily.replace(regex, ''),
medium : getComputedStyle(head[0].querySelector('meta.foundation-mq-medium')).fontFamily.replace(regex, ''),
large : getComputedStyle(head[0].querySelector('meta.foundation-mq-large')).fontFamily.replace(regex, '')
};

return {
topbarBreakpoint: function () {
return !matchMedia(queries.topbar).matches;
},
small: function () {
return matchMedia(queries.small).matches;
},
medium: function () {
return matchMedia(queries.medium).matches;
},
large: function () {
return matchMedia(queries.large).matches;
}
};

}])
angular.module("mm.foundation.topbar", ['mm.foundation.mediaQueries'])
.factory('closest', [function(){
return function(el, selector) {
var matchesSelector = function (node, selector) {
Expand Down