Split `hamburger-menu` out into `menu-panel`

This commit is contained in:
Robin Ward 2015-08-26 15:51:56 -04:00
parent 05adcda1fc
commit 5457684975
14 changed files with 316 additions and 216 deletions

View File

@ -1,67 +1,6 @@
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNameBindings: ['visible::hidden', 'viewMode'], classNames: ['hamburger-panel'],
attributeBindings: ['style'],
elementId: 'hamburger-menu',
viewMode: 'dropDown',
showClose: Ember.computed.equal('viewMode', 'slide-in'),
@computed('viewMode')
style(viewMode) {
if (viewMode === 'drop-down') {
const $buttonPanel = $('header ul.icons');
const buttonPanelPos = $buttonPanel.offset();
const myWidth = this.$().width();
const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height());
const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - myWidth);
return `left: ${posLeft}px; top: ${posTop}px`.htmlSafe();
} else {
const headerHeight = parseInt($('header.d-header').height() + 3);
return `top: ${headerHeight}px`.htmlSafe();
}
},
@computed('viewMode')
bodyStyle(viewMode) {
if (viewMode === 'drop-down') {
const height = parseInt($(window).height() * 0.8)
return `height: ${height}px`.htmlSafe();
}
},
@observes('visible')
_visibleChanged() {
const isDropdown = (this.get('viewMode') === 'drop-down');
if (this.get('visible')) {
if (isDropdown) {
$('.hamburger-dropdown').addClass('active');
}
if ($(window).width() < 1024) {
this.set('viewMode', 'slide-in');
} else {
this.set('viewMode', 'drop-down');
}
$('html').on('click.close-hamburger', (e) => {
const $target = $(e.target);
if ($target.closest('.hamburger-dropdown').length > 0) { return; }
if ($target.closest('#hamburger-menu').length > 0) { return; }
this.hide();
});
} else {
$('.hamburger-dropdown').removeClass('active');
$('html').off('click.close-hamburger');
}
},
@computed() @computed()
showKeyboardShortcuts() { showKeyboardShortcuts() {
@ -83,29 +22,6 @@ export default Ember.Component.extend({
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
}, },
@on('didInsertElement')
_bindEvents() {
this.$().on('click.discourse-hamburger', 'a', () => {
this.hide();
});
this.appEvents.on('dropdowns:closeAll', this, this.hide);
$('body').on('keydown.discourse-hambuger', (e) => {
if (e.which === 27) {
this.hide();
}
});
},
@on('willDestroyElement')
_removeEvents() {
this.appEvents.off('dropdowns:closeAll', this, this.hide);
this.$().off('click.discourse-hamburger');
$('body').off('keydown.discourse-hambuger');
$('html').off('click.close-hamburger');
},
@computed() @computed()
categories() { categories() {
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
@ -119,14 +35,7 @@ export default Ember.Component.extend({
}); });
}, },
hide() {
this.set('visible', false);
},
actions: { actions: {
close() {
this.hide();
},
keyboardShortcuts() { keyboardShortcuts() {
this.sendAction('showKeyboardAction'); this.sendAction('showKeyboardAction');
}, },

View File

@ -0,0 +1,124 @@
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'],
attributeBindings: ['style'],
viewMode: 'dropDown',
showClose: Ember.computed.equal('viewMode', 'slide-in'),
@computed('viewMode')
style(viewMode) {
if (viewMode === 'drop-down') {
const $buttonPanel = $('header ul.icons');
if ($buttonPanel.length === 0) { return; }
const buttonPanelPos = $buttonPanel.offset();
const myWidth = this.$().width();
const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height()) - $(window).scrollTop();
const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - myWidth);
return `left: ${posLeft}px; top: ${posTop}px`.htmlSafe();
} else {
const headerHeight = parseInt($('header.d-header').height() + 3);
return `top: ${headerHeight}px`.htmlSafe();
}
},
@computed('viewMode')
bodyStyle(viewMode) {
if (viewMode === 'drop-down') {
const height = parseInt($(window).height() * 0.8)
return `height: ${height}px`.htmlSafe();
}
},
@observes('visible')
_visibleChanged() {
const force = this.get('force');
if (force) {
this.set('viewMode', force);
} else if ($(window).width() < 1024) {
this.set('viewMode', 'slide-in');
} else {
this.set('viewMode', 'drop-down');
}
const isDropdown = (this.get('viewMode') === 'drop-down');
const markActive = this.get('markActive');
if (this.get('visible')) {
if (isDropdown && markActive) {
$(markActive).addClass('active');
}
$('html').on('click.close-menu-panel', (e) => {
const $target = $(e.target);
if ($target.closest(markActive).length > 0) { return; }
if ($target.closest('.menu-panel').length > 0) { return; }
this.hide();
});
} else {
if (markActive) {
$(markActive).removeClass('active');
}
$('html').off('click.close-menu-panel');
}
},
@computed()
showKeyboardShortcuts() {
return !Discourse.Mobile.mobileView && !this.capabilities.touch;
},
@computed()
showMobileToggle() {
return Discourse.Mobile.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
},
@computed()
mobileViewLinkTextKey() {
return Discourse.Mobile.mobileView ? "desktop_view" : "mobile_view";
},
@computed()
faqUrl() {
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
},
@on('didInsertElement')
_bindEvents() {
this.$().on('click.discourse-menu-panel', 'a', () => {
this.hide();
});
this.appEvents.on('dropdowns:closeAll', this, this.hide);
$('body').on('keydown.discourse-menu-panel', (e) => {
if (e.which === 27) {
this.hide();
}
});
},
@on('willDestroyElement')
_removeEvents() {
this.appEvents.off('dropdowns:closeAll', this, this.hide);
this.$().off('click.discourse-menu-panel');
$('body').off('keydown.discourse-menu-panel');
$('html').off('click.close-menu-panel');
},
hide() {
this.set('visible', false);
},
actions: {
close() {
this.hide();
}
}
});

View File

@ -1,5 +1,5 @@
{{render "header"}} {{render "header"}}
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} {{hamburger-menu hamburgerVisible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
<div id="main-outlet" class="wrap"> <div id="main-outlet" class="wrap">
<div class="container"> <div class="container">

View File

@ -1,82 +1,73 @@
{{#if visible}} {{#menu-panel visible=hamburgerVisible markActive=".hamburger-dropdown"}}
{{#if showClose}} <ul class="location-links">
<div class='hamburger-header clearfix'> {{#if currentUser.staff}}
<a href {{action "close"}} class='close-hamburger'>{{fa-icon 'times'}}</a>
</div>
{{/if}}
<div class='hamburger-body' style={{bodyStyle}}>
<ul class="location-links">
{{#if currentUser.staff}}
<li>
{{#link-to "admin" class="admin-link"}}
{{fa-icon "wrench"}} {{i18n 'admin_title'}}
{{/link-to}}
</li>
<li>
{{#link-to "adminFlags" class="flagged-posts-link"}}
{{fa-icon "flag"}} {{i18n 'flags_title'}}
{{#if currentUser.site_flagged_posts_count}}
<span title={{i18n 'notifications.total_flagged'}} class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span>
{{/if}}
{{/link-to}}
</li>
{{/if}}
<li> <li>
{{#link-to "discovery.latest" class="latest-topics-link"}} {{#link-to "admin" class="admin-link"}}
{{i18n 'filters.latest.title.zero'}} {{fa-icon "wrench"}} {{i18n 'admin_title'}}
{{/link-to}} {{/link-to}}
</li> </li>
{{#if siteSettings.enable_badges}}
<li>
{{#link-to 'badges' class="badge-link"}}{{i18n 'badges.title'}}{{/link-to}}
</li>
{{/if}}
{{#if siteSettings.enable_user_directory}}
<li>{{#link-to 'users' class="user-directory-link"}}{{i18n "directory.title"}}{{/link-to}}</li>
{{/if}}
{{#if currentUser.show_queued_posts}}
<li>
{{#link-to 'queued-posts'}}
{{i18n "queue.title"}}
{{#if currentUser.post_queue_new_count}}
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
{{/if}}
{{/link-to}}
</li>
{{/if}}
{{plugin-outlet "site-map-links"}}
{{#if showKeyboardShortcuts}}
<li><a href {{action "keyboardShortcuts"}} class="keyboard-shortcuts-link">{{i18n 'keyboard_shortcuts_help.title'}}</a></li>
{{/if}}
<li> <li>
<a href={{faqUrl}} class="faq-link">{{i18n 'faq'}}</a> {{#link-to "adminFlags" class="flagged-posts-link"}}
{{fa-icon "flag"}} {{i18n 'flags_title'}}
{{#if currentUser.site_flagged_posts_count}}
<span title={{i18n 'notifications.total_flagged'}} class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span>
{{/if}}
{{/link-to}}
</li>
{{/if}}
<li>
{{#link-to "discovery.latest" class="latest-topics-link"}}
{{i18n 'filters.latest.title.zero'}}
{{/link-to}}
</li>
{{#if siteSettings.enable_badges}}
<li>
{{#link-to 'badges' class="badge-link"}}{{i18n 'badges.title'}}{{/link-to}}
</li> </li>
<li>
{{#link-to 'about' class="about-link"}}{{i18n 'about.simple_title'}}{{/link-to}}
</li>
{{#if showMobileToggle}}
<li><a href class="mobile-toggle-link" {{action "toggleMobileView"}}>{{boundI18n mobileViewLinkTextKey}}</a></li>
{{/if}}
{{plugin-outlet "site-map-links-last"}}
</ul>
{{#if categories}}
<ul class="category-links clearfix">
<li class="heading" title={{i18n 'filters.categories.help'}}>
{{#link-to "discovery.categories" class="categories-link"}}{{i18n 'filters.categories.title'}}{{/link-to}}
</li>
{{#each categories as |c|}}
{{hamburger-category category=c}}
{{/each}}
</ul>
{{/if}} {{/if}}
</div>
{{/if}} {{#if siteSettings.enable_user_directory}}
<li>{{#link-to 'users' class="user-directory-link"}}{{i18n "directory.title"}}{{/link-to}}</li>
{{/if}}
{{#if currentUser.show_queued_posts}}
<li>
{{#link-to 'queued-posts'}}
{{i18n "queue.title"}}
{{#if currentUser.post_queue_new_count}}
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
{{/if}}
{{/link-to}}
</li>
{{/if}}
{{plugin-outlet "site-map-links"}}
{{#if showKeyboardShortcuts}}
<li><a href {{action "keyboardShortcuts"}} class="keyboard-shortcuts-link">{{i18n 'keyboard_shortcuts_help.title'}}</a></li>
{{/if}}
<li>
<a href={{faqUrl}} class="faq-link">{{i18n 'faq'}}</a>
</li>
<li>
{{#link-to 'about' class="about-link"}}{{i18n 'about.simple_title'}}{{/link-to}}
</li>
{{#if showMobileToggle}}
<li><a href class="mobile-toggle-link" {{action "toggleMobileView"}}>{{boundI18n mobileViewLinkTextKey}}</a></li>
{{/if}}
{{plugin-outlet "site-map-links-last"}}
</ul>
{{#if categories}}
<ul class="category-links clearfix">
<li class="heading" title={{i18n 'filters.categories.help'}}>
{{#link-to "discovery.categories" class="categories-link"}}{{i18n 'filters.categories.title'}}{{/link-to}}
</li>
{{#each categories as |c|}}
{{hamburger-category category=c}}
{{/each}}
</ul>
{{/if}}
{{/menu-panel}}

View File

@ -0,0 +1,10 @@
{{#if visible}}
{{#if showClose}}
<div class='panel-header clearfix'>
<a href {{action "close"}} class='close-panel'>{{fa-icon 'times'}}</a>
</div>
{{/if}}
<div class='panel-body' style={{bodyStyle}}>
{{yield}}
</div>
{{/if}}

View File

@ -1,6 +1,5 @@
<div class='wrap'> <div class='wrap'>
<div class='contents clearfix'> <div class='contents clearfix'>
{{home-logo minimized=showExtraInfo}} {{home-logo minimized=showExtraInfo}}
<div class='panel clearfix'> <div class='panel clearfix'>
@ -52,7 +51,7 @@
{{fa-icon "bars"}} {{fa-icon "bars"}}
</a> </a>
{{else}} {{else}}
<a {{action "toggleHamburgerMenu"}} class='icon' href <a {{action "toggleHamburgerMenu"}} class='icon' href
title={{i18n 'hamburger_menu'}} title={{i18n 'hamburger_menu'}}
aria-label={{i18n 'hamburger_menu'}} aria-label={{i18n 'hamburger_menu'}}
id="toggle-hamburger-menu"> id="toggle-hamburger-menu">

View File

@ -1,20 +1,20 @@
#hamburger-menu.slide-in { .menu-panel.slide-in {
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
height: 100%; height: 100%;
.hamburger-body { .panel-body {
position: absolute; position: absolute;
top: 40px; top: 40px;
bottom: 37px; bottom: 37px;
} }
} }
#hamburger-menu.drop-down { .menu-panel.drop-down {
position: absolute; position: fixed;
} }
#hamburger-menu { .menu-panel {
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
box-shadow: 0 2px 2px rgba(0,0,0, .25); box-shadow: 0 2px 2px rgba(0,0,0, .25);
background-color: $secondary; background-color: $secondary;
@ -23,7 +23,7 @@
padding: 0.5em 0.5em 0.5em 0.5em; padding: 0.5em 0.5em 0.5em 0.5em;
width: 300px; width: 300px;
.close-hamburger { .close-panel {
float: right; float: right;
color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary);
font-size: 1.5em; font-size: 1.5em;
@ -33,19 +33,24 @@
z-index: 9999; z-index: 9999;
} }
.hamburger-header { .panel-header {
position: absolute; position: absolute;
right: 20px; right: 20px;
} }
ul { ul {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.panel-body {
overflow-y: auto;
overflow-x: hidden;
}
}
.hamburger-panel {
ul.location-links li, li.heading { ul.location-links li, li.heading {
a { a {
padding: 0.5em; padding: 0.5em;
@ -76,10 +81,5 @@
font-weight: normal; font-weight: normal;
font-size: 11px; font-size: 11px;
} }
.hamburger-body {
overflow-y: auto;
overflow-x: hidden;
}
} }

View File

@ -17,7 +17,7 @@
@import "desktop/user"; @import "desktop/user";
@import "desktop/history"; @import "desktop/history";
@import "desktop/queued-posts"; @import "desktop/queued-posts";
@import "desktop/hamburger"; @import "desktop/menu-panel";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -6,8 +6,8 @@ test("Menu Items", (assert) => {
visit("/"); visit("/");
click("#toggle-hamburger-menu"); click("#toggle-hamburger-menu");
andThen(() => { andThen(() => {
assert.ok(exists("#hamburger-menu .admin-link")); assert.ok(exists(".hamburger-panel .admin-link"));
assert.ok(exists("#hamburger-menu .flagged-posts-link")); assert.ok(exists(".hamburger-panel .flagged-posts-link"));
assert.ok(exists("#hamburger-menu .flagged-posts.badge-notification"), "it displays flag notifications"); assert.ok(exists(".hamburger-panel .flagged-posts.badge-notification"), "it displays flag notifications");
}); });
}); });

View File

@ -2,38 +2,20 @@ import { acceptance } from "helpers/qunit-helpers";
acceptance("Hamburger Menu"); acceptance("Hamburger Menu");
test("Toggle Menu", (assert) => {
visit("/");
andThen(() => {
assert.ok(exists("#hamburger-menu.hidden"), "hidden by default");
});
click("#toggle-hamburger-menu");
andThen(() => {
assert.ok(!exists("#hamburger-menu.hidden"), "a click makes it appear");
});
click('#main-outlet')
andThen(() => {
assert.ok(exists("#hamburger-menu.hidden"), "clicking the body hides the menu");
});
});
test("Menu Items", (assert) => { test("Menu Items", (assert) => {
visit("/"); visit("/");
click("#toggle-hamburger-menu"); click("#toggle-hamburger-menu");
andThen(() => { andThen(() => {
assert.ok(!exists("#hamburger-menu .admin-link"), 'does not have admin link'); assert.ok(!exists(".hamburger-panel .admin-link"), 'does not have admin link');
assert.ok(!exists("#hamburger-menu .flagged-posts-link"), 'does not have flagged posts link'); assert.ok(!exists(".hamburger-panel .flagged-posts-link"), 'does not have flagged posts link');
assert.ok(exists("#hamburger-menu .latest-topics-link"), 'last link to latest'); assert.ok(exists(".hamburger-panel .latest-topics-link"), 'last link to latest');
assert.ok(exists("#hamburger-menu .badge-link"), 'has link to badges'); assert.ok(exists(".hamburger-panel .badge-link"), 'has link to badges');
assert.ok(exists("#hamburger-menu .user-directory-link"), 'has user directory link'); assert.ok(exists(".hamburger-panel .user-directory-link"), 'has user directory link');
assert.ok(exists("#hamburger-menu .faq-link"), 'has faq link'); assert.ok(exists(".hamburger-panel .faq-link"), 'has faq link');
assert.ok(exists("#hamburger-menu .about-link"), 'has about link'); assert.ok(exists(".hamburger-panel .about-link"), 'has about link');
assert.ok(exists("#hamburger-menu .categories-link"), 'has categories link'); assert.ok(exists(".hamburger-panel .categories-link"), 'has categories link');
assert.ok(exists('#hamburger-menu .category-link'), 'has at least one category'); assert.ok(exists('.hamburger-panel .category-link'), 'has at least one category');
}); });
}); });

View File

@ -0,0 +1,80 @@
import componentTest from 'helpers/component-test';
moduleForComponent('menu-panel', {integration: true});
componentTest('as a dropdown', {
template: `
<div id='outside-area'>click me</div>
<div class='menu-selected'></div>
{{#menu-panel visible=panelVisible markActive=".menu-selected" force="drop-down"}}
Some content
{{/menu-panel}}
`,
setup() {
this.set('panelVisible', false);
},
test(assert) {
assert.ok(exists(".menu-panel.hidden"), "hidden by default");
assert.ok(!exists(".menu-selected.active"), "does not mark anything as active");
this.set('panelVisible', true);
andThen(() => {
assert.ok(!exists('.menu-panel .close-panel'), "the close X is not shown");
assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear");
assert.ok(exists(".menu-selected.active"), "marks the panel as active");
});
click('#outside-area')
andThen(() => {
assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu");
assert.ok(!exists(".menu-selected.active"), "removes the active class");
assert.equal(this.get('panelVisible'), false, 'it updates the bound variable');
});
}
});
componentTest('as a slide-in', {
template: `
<div id='outside-area'>click me</div>
<div class='menu-selected'></div>
{{#menu-panel visible=panelVisible markActive=".menu-selected" force="slide-in"}}
Some content
{{/menu-panel}}
`,
setup() {
this.set('panelVisible', false);
},
test(assert) {
assert.ok(exists(".menu-panel.hidden"), "hidden by default");
assert.ok(!exists(".menu-selected.active"), "does not mark anything as active");
this.set('panelVisible', true);
andThen(() => {
assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear");
assert.ok(!exists(".menu-selected.active"), "slide ins don't mark as active");
});
click('#outside-area')
andThen(() => {
assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu");
assert.equal(this.get('panelVisible'), false, 'it updates the bound variable');
this.set('panelVisible', true);
});
andThen(() => {
assert.ok(exists('.menu-panel .close-panel'), "the close X is shown");
});
click('.close-panel');
andThen(() => {
assert.ok(exists(".menu-panel.hidden"), "clicking the close button closes it");
assert.equal(this.get('panelVisible'), false, 'it updates the bound variable');
});
}
});

View File

@ -1,3 +1,4 @@
import AppEvents from 'discourse/lib/app-events';
import createStore from 'helpers/create-store'; import createStore from 'helpers/create-store';
export default function(name, opts) { export default function(name, opts) {
@ -8,8 +9,12 @@ export default function(name, opts) {
const store = createStore(); const store = createStore();
opts.setup.call(this, store); opts.setup.call(this, store);
} }
const appEvents = AppEvents.create();
this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false });
this.container.register('app-events:main', appEvents, { instantiate: false });
this.container.injection('component', 'siteSettings', 'site-settings:main'); this.container.injection('component', 'siteSettings', 'site-settings:main');
this.container.injection('component', 'appEvents', 'app-events:main');
andThen(() => { andThen(() => {
this.render(opts.template); this.render(opts.template);