Commit 58cad116 authored by Paul Slaughter's avatar Paul Slaughter

Setup HAML, CSS, and Vue for top nav responsive

- Add hide-when-top-nav-open classes
- Set up eventHub for app component to use
- Puts placeholder content which will be
  filled in a follow up since this is behind
  a feature flag
parent b2d828d5
......@@ -36,6 +36,7 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
import navEventHub, { EVENT_RESPONSIVE_TOGGLE } from './nav/event_hub';
import 'ee_else_ce/main_ee';
......@@ -203,6 +204,7 @@ document.addEventListener('DOMContentLoaded', () => {
$('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
navEventHub.$emit(EVENT_RESPONSIVE_TOGGLE);
});
/**
......
<script>
import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '../event_hub';
const TEMPORARY_PLACEHOLDER = 'Placeholder for responsive top nav';
export default {
props: {
navData: {
type: Object,
required: true,
},
},
created() {
eventHub.$on(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
},
beforeDestroy() {
eventHub.$off(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
},
methods: {
onToggle() {
document.body.classList.toggle('top-nav-responsive-open');
},
},
TEMPORARY_PLACEHOLDER,
};
</script>
<template>
<p>{{ $options.TEMPORARY_PLACEHOLDER }}</p>
</template>
import eventHubFactory from '~/helpers/event_hub_factory';
export const EVENT_RESPONSIVE_TOGGLE = 'top-nav-responsive-toggle';
export default eventHubFactory();
export const initTopNav = async () => {
// With combined_menu feature flag, there's a benefit to splitting up the import
const importModule = () => import(/* webpackChunkName: 'top_nav' */ './mount');
const tryMountTopNav = async () => {
const el = document.getElementById('js-top-nav');
if (!el) {
return;
}
// With combined_menu feature flag, there's a benefit to splitting up the import
const { mountTopNav } = await import(/* webpackChunkName: 'top_nav' */ './mount');
const { mountTopNav } = await importModule();
mountTopNav(el);
};
const tryMountTopNavResponsive = async () => {
const el = document.getElementById('js-top-nav-responsive');
if (!el) {
return;
}
const { mountTopNavResponsive } = await importModule();
mountTopNavResponsive(el);
};
export const initTopNav = async () => Promise.all([tryMountTopNav(), tryMountTopNavResponsive()]);
import Vue from 'vue';
import Vuex from 'vuex';
import ResponsiveApp from './components/responsive_app.vue';
import App from './components/top_nav_app.vue';
import { createStore } from './stores';
Vue.use(Vuex);
export const mountTopNav = (el) => {
const mount = (el, Component) => {
const viewModel = JSON.parse(el.dataset.viewModel);
const store = createStore();
......@@ -13,7 +14,7 @@ export const mountTopNav = (el) => {
el,
store,
render(h) {
return h(App, {
return h(Component, {
props: {
navData: viewModel,
},
......@@ -21,3 +22,7 @@ export const mountTopNav = (el) => {
},
});
};
export const mountTopNav = (el) => mount(el, App);
export const mountTopNavResponsive = (el) => mount(el, ResponsiveApp);
......@@ -106,7 +106,7 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
&.menu-expanded {
@include media-breakpoint-down(xs) {
.title-container {
.hide-when-menu-expanded {
display: none;
}
......@@ -665,3 +665,26 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
color: inherit !important;
}
}
.top-nav-responsive {
@include gl-display-none;
color: var(--indigo-900, $theme-indigo-900);
}
.top-nav-responsive-open {
.hide-when-top-nav-responsive-open {
@include media-breakpoint-down(xs) {
display: none !important;
}
}
.top-nav-responsive {
@include media-breakpoint-down(xs) {
@include gl-display-block;
}
}
.navbar-gitlab .header-content .title-container {
flex: 0;
}
}
......@@ -337,9 +337,6 @@ h1 {
.d-none {
display: none !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-block {
display: block !important;
}
......@@ -354,9 +351,6 @@ h1 {
}
}
@media (min-width: 992px) {
.d-lg-none {
display: none !important;
}
.d-lg-block {
display: block !important;
}
......@@ -1957,9 +1951,7 @@ body.gl-dark .navbar-gitlab .navbar-collapse {
}
body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler {
border-left: 1px solid #b3b3b3;
}
body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler svg {
fill: #fafafa;
color: #fafafa;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
......@@ -2146,9 +2138,40 @@ body.gl-dark {
white-space: nowrap;
width: 1px;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-display-none {
display: none;
}
@media (min-width: 62rem) {
.gl-lg-display-none {
display: none;
}
}
@media (min-width: 36rem) {
.gl-sm-display-block {
display: block;
}
}
.gl-display-inline-block {
display: inline-block;
}
@media (min-width: 36rem) {
.gl-sm-display-inline-block {
display: inline-block;
}
}
.gl-absolute {
position: absolute;
}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;
}
......
......@@ -322,9 +322,6 @@ h1 {
.d-none {
display: none !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-block {
display: block !important;
}
......@@ -339,9 +336,6 @@ h1 {
}
}
@media (min-width: 992px) {
.d-lg-none {
display: none !important;
}
.d-lg-block {
display: block !important;
}
......@@ -1927,9 +1921,40 @@ body.sidebar-refactoring
white-space: nowrap;
width: 1px;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-display-none {
display: none;
}
@media (min-width: 62rem) {
.gl-lg-display-none {
display: none;
}
}
@media (min-width: 36rem) {
.gl-sm-display-block {
display: block;
}
}
.gl-display-inline-block {
display: inline-block;
}
@media (min-width: 36rem) {
.gl-sm-display-inline-block {
display: inline-block;
}
}
.gl-absolute {
position: absolute;
}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;
}
......
......@@ -22,10 +22,7 @@
.container-fluid {
.navbar-toggler {
border-left: 1px solid lighten($border-and-box-shadow, 10%);
svg {
fill: $search-and-nav-links;
}
color: $search-and-nav-links;
}
}
......
.layout-page{ class: page_with_sidebar_class }
.layout-page.hide-when-top-nav-responsive-open{ class: page_with_sidebar_class }
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper.content-wrapper-margin{ class: "#{@content_wrapper_class}" }
......@@ -27,3 +27,5 @@
= render "layouts/flash", extra_flash_class: 'limit-container-width'
= yield :before_content
= yield
= render "layouts/nav/top_nav_responsive", class: 'layout-page content-wrapper-margin'
......@@ -6,11 +6,12 @@
= header_message
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
.mobile-overlay
.alert-wrapper
.alert-wrapper.hide-when-top-nav-responsive-open
= render 'shared/outdated_browser'
= render "layouts/broadcast"
= yield :flash_message
= render "layouts/flash"
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" }
.content-wrapper.hide-when-top-nav-responsive-open{ id: "content-body", class: "d-flex flex-column align-items-stretch" }
= yield
= render "layouts/nav/top_nav_responsive", class: "gl-flex-fill-1 gl-overflow-y-auto"
= footer_message
- has_impersonation_link = header_link?(:admin_impersonation)
- user_status_data = user_status_properties(current_user)
- use_top_nav_redesign = Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
.header-content
.title-container
.title-container{ class: ('hide-when-menu-expanded' if !use_top_nav_redesign) }
%h1.title
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
......@@ -19,8 +20,9 @@
%span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1
= _('Next')
- if Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
= render "layouts/nav/top_nav"
- if use_top_nav_redesign
.gl-display-none.gl-sm-display-block
= render "layouts/nav/top_nav"
- else
- if current_user
= render "layouts/nav/dashboard"
......@@ -30,11 +32,11 @@
.navbar-collapse.collapse
%ul.nav.navbar-nav
- if current_user
= render 'layouts/header/new_dropdown'
= render 'layouts/header/new_dropdown', class: ('gl-display-none gl-sm-display-block' if use_top_nav_redesign)
- if header_link?(:search)
%li.nav-item.d-none.d-lg-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-lg-none
%li.nav-item{ class: use_top_nav_redesign ? "gl-display-none gl-sm-display-inline-block gl-lg-display-none" : "gl-display-inline-block gl-lg-display-none" }
= link_to search_context.search_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search')
- if header_link?(:issues)
......@@ -115,10 +117,15 @@
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-default btn-sign-in'
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: ('gl-border-none!' if use_top_nav_redesign) }
%span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if use_top_nav_redesign
%span.more-icon.gl-px-3
%span.gl-pr-2= _('Menu')
= sprite_icon('dot-grid', size: 16)
- else
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon')
= sprite_icon('close', size: 12, css_class: 'close-icon')
- if display_whats_new?
#whats-new-app{ data: { version_digest: whats_new_version_digest } }
......
......@@ -2,10 +2,11 @@
- menu_sections = view_model.fetch(:menu_sections)
- title = view_model.fetch(:title)
- show_headers = menu_sections.length > 1
- top_class = local_assigns.fetch(:class, nil)
- return if menu_sections.empty?
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
%li.header-new.dropdown{ class: top_class, data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", id: "js-onboarding-new-project-link", title: title, ref: 'tooltip', aria: { label: title }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static', qa_selector: 'new_menu_toggle' } do
= sprite_icon('plus-square')
= sprite_icon('chevron-down', css_class: 'caret-down')
......
- return unless Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
- top_class = local_assigns.fetch(:class, nil)
- view_model = top_nav_view_model(project: @project, group: @group)
.top-nav-responsive{ class: top_class }
#js-top-nav-responsive{ data: { view_model: view_model.to_json } }
......@@ -337,9 +337,6 @@ h1 {
.d-none {
display: none !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-block {
display: block !important;
}
......@@ -354,9 +351,6 @@ h1 {
}
}
@media (min-width: 992px) {
.d-lg-none {
display: none !important;
}
.d-lg-block {
display: block !important;
}
......@@ -1957,9 +1951,7 @@ body.gl-dark .navbar-gitlab .navbar-collapse {
}
body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler {
border-left: 1px solid #b3b3b3;
}
body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler svg {
fill: #fafafa;
color: #fafafa;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
......@@ -2146,9 +2138,40 @@ body.gl-dark {
white-space: nowrap;
width: 1px;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-display-none {
display: none;
}
@media (min-width: 62rem) {
.gl-lg-display-none {
display: none;
}
}
@media (min-width: 36rem) {
.gl-sm-display-block {
display: block;
}
}
.gl-display-inline-block {
display: inline-block;
}
@media (min-width: 36rem) {
.gl-sm-display-inline-block {
display: inline-block;
}
}
.gl-absolute {
position: absolute;
}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;
}
......
......@@ -322,9 +322,6 @@ h1 {
.d-none {
display: none !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-block {
display: block !important;
}
......@@ -339,9 +336,6 @@ h1 {
}
}
@media (min-width: 992px) {
.d-lg-none {
display: none !important;
}
.d-lg-block {
display: block !important;
}
......@@ -1927,9 +1921,40 @@ body.sidebar-refactoring
white-space: nowrap;
width: 1px;
}
.gl-border-none\! {
border-style: none !important;
}
.gl-display-none {
display: none;
}
@media (min-width: 62rem) {
.gl-lg-display-none {
display: none;
}
}
@media (min-width: 36rem) {
.gl-sm-display-block {
display: block;
}
}
.gl-display-inline-block {
display: inline-block;
}
@media (min-width: 36rem) {
.gl-sm-display-inline-block {
display: inline-block;
}
}
.gl-absolute {
position: absolute;
}
.gl-px-3 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.gl-pr-2 {
padding-right: 0.25rem;
}
.gl-ml-3 {
margin-left: 0.5rem;
}
......
......@@ -10,6 +10,8 @@ const HTML_TO_REMOVE = [
'#js-peek',
'.modal',
'.feature-highlight',
// The user has to open up the responsive nav, so we don't need it on load
'.top-nav-responsive',
// We don't want to capture all the children of a dropdown-menu
'.dropdown-menu',
];
......
......@@ -97,6 +97,8 @@ RSpec.describe 'Admin mode' do
end
it 'can leave admin mode using dropdown menu on smaller screens', :js do
skip('pending responsive development under :combined_menu feature flag') if Feature.enabled?(:combined_menu)
resize_screen_xs
visit root_dashboard_path
......@@ -131,7 +133,7 @@ RSpec.describe 'Admin mode' do
end
it 'relocates admin dashboard links to dropdown list on smaller screen', :js do
skip('not applicable with :combined_menu feature flag enabled') if Feature.enabled?(:combined_menu)
skip('pending responsive development under :combined_menu feature flag') if Feature.enabled?(:combined_menu)
resize_screen_xs
visit root_dashboard_path
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'top nav responsive', :js do
include MobileHelpers
let_it_be(:user) { create(:user) }
let_it_be(:responsive_menu_text) { 'Placeholder for responsive top nav' }
before do
stub_feature_flags(combined_menu: true)
sign_in(user)
visit explore_projects_path
resize_screen_xs
end
context 'before opened' do
it 'has page content and hides responsive menu', :aggregate_failures do
expect(page).to have_css('.page-title', text: 'Projects')
expect(page).to have_no_text(responsive_menu_text)
end
end
context 'when opened' do
before do
click_button('Menu')
end
it 'hides everything and shows responsive menu', :aggregate_failures do
expect(page).to have_no_css('.page-title', text: 'Projects')
expect(page).to have_link('Dashboard', id: 'logo')
expect(page).to have_text(responsive_menu_text)
end
end
end
import { shallowMount } from '@vue/test-utils';
import { range } from 'lodash';
import ResponsiveApp from '~/nav/components/responsive_app.vue';
import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '~/nav/event_hub';
import { TEST_NAV_DATA } from '../mock_data';
describe('~/nav/components/responsive_app.vue', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(ResponsiveApp, {
propsData: {
navData: TEST_NAV_DATA,
},
});
};
const triggerResponsiveToggle = () => eventHub.$emit(EVENT_RESPONSIVE_TOGGLE);
const hasBodyResponsiveOpen = () => document.body.classList.contains('top-nav-responsive-open');
beforeEach(() => {
// Add test class to reset state + assert that we're adding classes correctly
document.body.className = 'test-class';
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it.each`
times | expectation
${0} | ${false}
${1} | ${true}
${2} | ${false}
`(
'with responsive toggle event triggered $times, body responsive open = $expectation',
({ times, expectation }) => {
range(times).forEach(triggerResponsiveToggle);
expect(hasBodyResponsiveOpen()).toBe(expectation);
},
);
});
describe('when destroyed', () => {
beforeEach(() => {
createComponent();
wrapper.destroy();
});
it('responsive toggle event does nothing', () => {
triggerResponsiveToggle();
expect(hasBodyResponsiveOpen()).toBe(false);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment