mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
[DURACOM-195] Header, navbar, and sidebar refactoring
This commit is contained in:
@@ -6,7 +6,7 @@ describe('Collection Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from a Collection page', () => {
|
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||||
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ describe('Community Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from a Community page', () => {
|
it('should load if you click on "Statistics" from a Community page', () => {
|
||||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import '../support/commands';
|
|||||||
describe('Site Statistics Page', () => {
|
describe('Site Statistics Page', () => {
|
||||||
it('should load if you click on "Statistics" from homepage', () => {
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', '/statistics');
|
cy.location('pathname').should('eq', '/statistics');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ describe('Item Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||||
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
const page = {
|
const page = {
|
||||||
openLoginMenu() {
|
openLoginMenu() {
|
||||||
// Click the "Log In" dropdown menu in header
|
// Click the "Log In" dropdown menu in header
|
||||||
cy.get('ds-themed-navbar [data-test="login-menu"]').click();
|
cy.get('ds-themed-header [data-test="login-menu"]').click();
|
||||||
},
|
},
|
||||||
openUserMenu() {
|
openUserMenu() {
|
||||||
// Once logged in, click the User menu in header
|
// Once logged in, click the User menu in header
|
||||||
cy.get('ds-themed-navbar [data-test="user-menu"]').click();
|
cy.get('ds-themed-header [data-test="user-menu"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingButton(email, password) {
|
submitLoginAndPasswordByPressingButton(email, password) {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
cy.get('ds-themed-header [data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
cy.get('ds-themed-header [data-test="password"]').type(password);
|
||||||
// Click login button
|
// Click login button
|
||||||
cy.get('ds-themed-navbar [data-test="login-button"]').click();
|
cy.get('ds-themed-header [data-test="login-button"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingEnter(email, password) {
|
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||||
// In opened Login modal, fill out email & password, then click Enter
|
// In opened Login modal, fill out email & password, then click Enter
|
||||||
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
cy.get('ds-themed-header [data-test="email"]').type(email);
|
||||||
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
cy.get('ds-themed-header [data-test="password"]').type(password);
|
||||||
cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
|
cy.get('ds-themed-header [data-test="password"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitLogoutByPressingButton() {
|
submitLogoutByPressingButton() {
|
||||||
// This is the POST command that will actually log us out
|
// This is the POST command that will actually log us out
|
||||||
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
||||||
// Click logout button
|
// Click logout button
|
||||||
cy.get('ds-themed-navbar [data-test="logout-button"]').click();
|
cy.get('ds-themed-header [data-test="logout-button"]').click();
|
||||||
// Wait until above POST command responds before continuing
|
// Wait until above POST command responds before continuing
|
||||||
// (This ensures next action waits until logout completes)
|
// (This ensures next action waits until logout completes)
|
||||||
cy.wait('@logout');
|
cy.wait('@logout');
|
||||||
@@ -102,10 +102,10 @@ describe('Login Modal', () => {
|
|||||||
page.openLoginMenu();
|
page.openLoginMenu();
|
||||||
|
|
||||||
// Registration link should be visible
|
// Registration link should be visible
|
||||||
cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
|
cy.get('ds-themed-header [data-test="register"]').should('be.visible');
|
||||||
|
|
||||||
// Click registration link & you should go to registration page
|
// Click registration link & you should go to registration page
|
||||||
cy.get('ds-themed-navbar [data-test="register"]').click();
|
cy.get('ds-themed-header [data-test="register"]').click();
|
||||||
cy.location('pathname').should('eq', '/register');
|
cy.location('pathname').should('eq', '/register');
|
||||||
cy.get('ds-register-email').should('exist');
|
cy.get('ds-register-email').should('exist');
|
||||||
|
|
||||||
@@ -119,10 +119,10 @@ describe('Login Modal', () => {
|
|||||||
page.openLoginMenu();
|
page.openLoginMenu();
|
||||||
|
|
||||||
// Forgot password link should be visible
|
// Forgot password link should be visible
|
||||||
cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
|
cy.get('ds-themed-header [data-test="forgot"]').should('be.visible');
|
||||||
|
|
||||||
// Click link & you should go to Forgot Password page
|
// Click link & you should go to Forgot Password page
|
||||||
cy.get('ds-themed-navbar [data-test="forgot"]').click();
|
cy.get('ds-themed-header [data-test="forgot"]').click();
|
||||||
cy.location('pathname').should('eq', '/forgot');
|
cy.location('pathname').should('eq', '/forgot');
|
||||||
cy.get('ds-forgot-email').should('exist');
|
cy.get('ds-forgot-email').should('exist');
|
||||||
|
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
const page = {
|
const page = {
|
||||||
fillOutQueryInNavBar(query) {
|
fillOutQueryInNavBar(query) {
|
||||||
// Click the magnifying glass
|
// Click the magnifying glass
|
||||||
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
|
||||||
// Fill out a query in input that appears
|
// Fill out a query in input that appears
|
||||||
cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
|
cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
|
||||||
},
|
},
|
||||||
submitQueryByPressingEnter() {
|
submitQueryByPressingEnter() {
|
||||||
cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
|
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitQueryByPressingIcon() {
|
submitQueryByPressingIcon() {
|
||||||
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,23 +1,21 @@
|
|||||||
<div class="sidebar-section">
|
<li role="presentation">
|
||||||
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
<a class="sidebar-section-wrapper"
|
||||||
[ngClass]="{ disabled: isDisabled }"
|
[ngClass]="{ disabled: isDisabled }"
|
||||||
[attr.aria-disabled]="isDisabled"
|
role="menuitem"
|
||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
[attr.aria-disabled]="isDisabled"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[attr.aria-labelledby]="adminMenuSectionTitleId(section.id)"
|
||||||
[routerLink]="itemModel.link"
|
[routerLink]="itemModel.link"
|
||||||
(keyup.space)="navigate($event)"
|
(keyup.space)="navigate($event)"
|
||||||
(keyup.enter)="navigate($event)"
|
(keyup.enter)="navigate($event)"
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
>
|
>
|
||||||
<div class="shortcut-icon">
|
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
||||||
|
<span [id]="adminMenuSectionTitleId(section.id)">{{itemModel.text | translate}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-collapsible">
|
</div>
|
||||||
<div class="toggle">
|
</a>
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
</li>
|
||||||
{{itemModel.text | translate}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
@@ -48,7 +48,7 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set the right icon', () => {
|
it('should set the right icon', () => {
|
||||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"]')).query(By.css('i.fas'));
|
||||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
});
|
});
|
||||||
it('should not contain the disabled class', () => {
|
it('should not contain the disabled class', () => {
|
||||||
@@ -88,7 +88,7 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set the right icon', () => {
|
it('should set the right icon', () => {
|
||||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"]')).query(By.css('i.fas'));
|
||||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
});
|
});
|
||||||
it('should contain the disabled class', () => {
|
it('should contain the disabled class', () => {
|
||||||
|
@@ -52,4 +52,12 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
|||||||
this.router.navigate(this.itemModel.link);
|
this.router.navigate(this.itemModel.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminMenuSectionId(sectionId: string) {
|
||||||
|
return `admin-menu-section-${sectionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
adminMenuSectionTitleId(sectionId: string) {
|
||||||
|
return `admin-menu-section-${sectionId}-title`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,54 +1,62 @@
|
|||||||
<nav class="navbar navbar-dark p-0"
|
<nav class="navbar navbar-dark p-0 vh-100"
|
||||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
id="admin-sidebar"
|
||||||
|
[attr.aria-label]="'menu.header.nav.description' | translate"
|
||||||
|
[ngClass]="{'expanded': sidebarOpen, 'collapsed': sidebarClosed, 'transitioning': sidebarTransitioning}"
|
||||||
[@slideSidebar]="{
|
[@slideSidebar]="{
|
||||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||||
params: {sidebarWidth: (sidebarWidth | async)}
|
params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) }
|
||||||
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||||
*ngIf="menuVisible | async"
|
*ngIf="menuVisible | async"
|
||||||
(mouseenter)="handleMouseEnter($event)"
|
(mouseenter)="handleMouseEnter($event)"
|
||||||
(mouseleave)="handleMouseLeave($event)"
|
(mouseleave)="handleMouseLeave($event)">
|
||||||
role="navigation" [attr.aria-label]="'menu.header.admin.description' |translate">
|
|
||||||
<div class="sidebar-top-level-items">
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="admin-menu-header">
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<div href="javascript:void(0);" class="nav-item d-flex flex-row flex-nowrap py-0">
|
|
||||||
<div class="shortcut-icon navbar-brand logo-wrapper">
|
|
||||||
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
|
||||||
[alt]="('menu.header.image.logo') | translate">
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-collapsible navbar-brand">
|
|
||||||
<div class="mr-0">
|
|
||||||
<h4 class="section-header-text mb-0">{{ 'menu.header.admin' | translate }}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li *ngFor="let section of (sections | async)">
|
<!-- HEADER -->
|
||||||
<ng-container
|
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
<div class="sidebar-full-width-container" id="sidebar-header-container" aria-hidden="true">
|
||||||
</li>
|
<div class="sidebar-section-wrapper">
|
||||||
</ul>
|
<div class="sidebar-fixed-element-wrapper">
|
||||||
</div>
|
<img id="admin-sidebar-logo" src="assets/images/dspace-logo-mini.svg" [alt]="('menu.header.image.logo') | translate" aria-hidden="true">
|
||||||
<div class="navbar-nav">
|
</div>
|
||||||
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
<button class="nav-item nav-link sidebar-section d-flex flex-row flex-nowrap border-0" type="button"
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
||||||
(click)="toggle($event)"
|
<h4 class="my-1">{{ 'menu.header.admin' | translate }}</h4>
|
||||||
(keyup.space)="toggle($event)"
|
|
||||||
>
|
|
||||||
<span class="shortcut-icon">
|
|
||||||
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
|
||||||
[title]="'menu.section.icon.pin' | translate"></i>
|
|
||||||
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
|
||||||
[title]="'menu.section.icon.unpin' | translate"></i>
|
|
||||||
</span>
|
|
||||||
<span class="sidebar-collapsible text-left">
|
|
||||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
|
||||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ITEMS -->
|
||||||
|
|
||||||
|
<div class="sidebar-full-width-container" id="sidebar-top-level-items-container">
|
||||||
|
<ul class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar"
|
||||||
|
[attr.aria-label]="'menu.header.admin.description' |translate">
|
||||||
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOGGLER -->
|
||||||
|
|
||||||
|
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container" aria-hidden="true">
|
||||||
|
<a class="sidebar-section-wrapper sidebar-full-width-container"
|
||||||
|
href="javascript:void(0);"
|
||||||
|
(click)="toggle($event)"
|
||||||
|
(keyup.space)="toggle($event)"
|
||||||
|
>
|
||||||
|
<div class="sidebar-fixed-element-wrapper">
|
||||||
|
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||||
|
[title]="'menu.section.icon.pin' | translate"></i>
|
||||||
|
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
||||||
|
[title]="'menu.section.icon.unpin' | translate"></i>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
|
||||||
|
{{ ((menuCollapsed | async) ? 'menu.section.pin' : 'menu.section.unpin' ) | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
@@ -1,123 +1,140 @@
|
|||||||
:host {
|
:host {
|
||||||
--ds-icon-z-index: 10;
|
|
||||||
|
|
||||||
left: 0;
|
/* SIDEBAR SIZE AND POSITION */
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
/* Sidebar hierarchy:
|
||||||
flex: 1 1 auto;
|
§ nav
|
||||||
nav {
|
§ .sidebar-full-width-container (any OPTIONAL full width element with no horizontal margin or padding - it can be nested)
|
||||||
background-color: var(--ds-admin-sidebar-bg);
|
§ .sidebar-section-wrapper
|
||||||
height: 100%;
|
§ .sidebar-fixed-element-wrapper
|
||||||
flex-direction: column;
|
§ .sidebar-collapsible-element-outer-wrapper
|
||||||
> div {
|
§ .sidebar-collapsible-element-inner-wrapper
|
||||||
width: 100%;
|
§ .sidebar-item
|
||||||
&.sidebar-top-level-items {
|
*/
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
// Sidebar position
|
||||||
@include dark-scrollbar;
|
position: fixed;
|
||||||
}
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--ds-sidebar-z-index);
|
||||||
|
|
||||||
|
// Sidebar size and content position
|
||||||
|
nav#admin-sidebar {
|
||||||
|
|
||||||
|
max-width: var(--ds-admin-sidebar-fixed-element-width); // Sidebar collapsed width
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
div#sidebar-top-level-items-container {
|
||||||
|
flex: 1 1 auto; // Fill available vertical space
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
@include dark-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
img#admin-sidebar-logo {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
|
||||||
|
// This class must be applied to any nested wrapper containing a sidebar section
|
||||||
|
.sidebar-full-width-container {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This class must be applied to the innermost block element containing a section or subsection link
|
||||||
|
// (it can be applied together with `sidebar-collapsible-element-inner-wrapper`)
|
||||||
|
.sidebar-item {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These classes handle the collapsing behavior
|
||||||
|
.sidebar-section-wrapper {
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
// These elements have fixed width and determine the width of the collapsed sidebar
|
||||||
|
& > .sidebar-fixed-element-wrapper {
|
||||||
|
min-width: var(--ds-admin-sidebar-fixed-element-width);
|
||||||
|
flex: 1 1 auto; // Fill available space
|
||||||
|
|
||||||
|
// Align the icons
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.inactive ::ng-deep .sidebar-collapsible {
|
& > .sidebar-collapsible-element-outer-wrapper {
|
||||||
margin-left: calc(-1 * var(--ds-sidebar-items-width));
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end; // make inner wrapper slide on the left when collapsing
|
||||||
.navbar-nav {
|
max-width: calc(100% - var(--ds-admin-sidebar-fixed-element-width)); // fill available space
|
||||||
.admin-menu-header {
|
padding-left: var(--ds-dark-scrollbar-width); // leave room for scrollbar
|
||||||
background-color: var(--ds-admin-sidebar-header-bg);
|
overflow-x: hidden; // hide inner wrapper when sidebar is collapsed
|
||||||
|
|
||||||
.sidebar-section {
|
// These elements have fixed width and slide on the left when the sidebar is collapsed
|
||||||
background-color: inherit;
|
// Their content should fill all available space
|
||||||
}
|
& > .sidebar-collapsible-element-inner-wrapper {
|
||||||
|
min-width: calc(var(--ds-admin-sidebar-collapsible-element-width) - var(--ds-dark-scrollbar-width));
|
||||||
.logo-wrapper {
|
height: 100%;
|
||||||
img {
|
padding-right: 16px !important;
|
||||||
height: 20px;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
.section-header-text {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
::ng-deep {
|
|
||||||
.navbar-nav {
|
|
||||||
.sidebar-section {
|
|
||||||
display: flex;
|
|
||||||
align-content: stretch;
|
|
||||||
background-color: var(--ds-admin-sidebar-bg);
|
|
||||||
overflow-x: visible;
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
padding-top: var(--bs-spacer);
|
|
||||||
padding-bottom: var(--bs-spacer);
|
|
||||||
background-color: inherit;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
// since links fill the whole sidebar, we _inset_ the outline
|
|
||||||
outline-offset: -4px;
|
|
||||||
|
|
||||||
// replace padding with margins so it doesn't extend over the :focus-visible outline
|
|
||||||
// → can't remove the padding altogether; the icon needs to fill out
|
|
||||||
// the collapsed width of the sidebar for the slide animation to look decent.
|
|
||||||
.shortcut-icon {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
margin-left: var(--ds-icon-padding);
|
|
||||||
margin-right: var(--ds-icon-padding);
|
|
||||||
}
|
|
||||||
.logo-wrapper {
|
|
||||||
margin-right: var(--bs-navbar-padding-x) !important;
|
|
||||||
}
|
|
||||||
.navbar-brand {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-top: var(--bs-navbar-brand-padding-y);
|
|
||||||
margin-bottom: var(--bs-navbar-brand-padding-y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-icon {
|
|
||||||
background-color: inherit;
|
|
||||||
padding-left: var(--ds-icon-padding);
|
|
||||||
padding-right: var(--ds-icon-padding);
|
|
||||||
z-index: var(--ds-icon-z-index);
|
|
||||||
align-self: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-collapsible {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: var(--bs-spacer);
|
|
||||||
width: var(--ds-sidebar-items-width);
|
|
||||||
position: relative;
|
|
||||||
.toggle {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-top: var(--bs-spacer);
|
|
||||||
|
|
||||||
li a {
|
|
||||||
padding-left: var(--bs-spacer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active > .sidebar-collapsible > .nav-link {
|
|
||||||
color: var(--bs-navbar-dark-active-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set here any style that depends on the sidebar status
|
||||||
|
&.collapsed, &.transitioning, &.expanded { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SIDEBAR STYLE */
|
||||||
|
|
||||||
|
nav#admin-sidebar {
|
||||||
|
|
||||||
|
background-color: var(--ds-admin-sidebar-bg);
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
// Set here the style of the *-menu-item nested components
|
||||||
|
.ds-menu-item {
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--ds-admin-sidebar-link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
color: var(--ds-admin-sidebar-link-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div#sidebar-header-container {
|
||||||
|
background-color: var(--ds-admin-sidebar-header-bg);
|
||||||
|
.sidebar-fixed-element-wrapper {
|
||||||
|
background-color: var(--ds-admin-sidebar-header-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div#sidebar-collapse-toggle-container {
|
||||||
|
.sidebar-collapsible-element-inner-wrapper {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -143,7 +143,7 @@ describe('AdminSidebarComponent', () => {
|
|||||||
describe('when the collapse link is clicked', () => {
|
describe('when the collapse link is clicked', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'toggleMenu');
|
spyOn(menuService, 'toggleMenu');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > button'));
|
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle-container > a'));
|
||||||
sidebarToggler.triggerEventHandler('click', {
|
sidebarToggler.triggerEventHandler('click', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
import { Component, HostListener, Injector, Input, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
@@ -28,9 +28,14 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
menuID = MenuID.ADMIN;
|
menuID = MenuID.ADMIN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that emits the width of the collapsible menu sections
|
* Observable that emits the width of the sidebar when expanded
|
||||||
*/
|
*/
|
||||||
sidebarWidth: Observable<string>;
|
@Input() expandedSidebarWidth$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the width of the sidebar when collapsed
|
||||||
|
*/
|
||||||
|
@Input() collapsedSidebarWidth$: Observable<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is true when the sidebar is open, is false when the sidebar is animating or closed
|
* Is true when the sidebar is open, is false when the sidebar is animating or closed
|
||||||
@@ -44,6 +49,12 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
sidebarClosed = !this.sidebarOpen; // Closed in UI, animation finished
|
sidebarClosed = !this.sidebarOpen; // Closed in UI, animation finished
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is true when the sidebar is opening or closing
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
sidebarTransitioning = !this.sidebarOpen; // Animation in progress
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when either the menu OR the menu's preview is expanded, else emits false
|
* Emits true when either the menu OR the menu's preview is expanded, else emits false
|
||||||
*/
|
*/
|
||||||
@@ -69,7 +80,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
|
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
.subscribe((loggedIn: boolean) => {
|
.subscribe((loggedIn: boolean) => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -134,6 +144,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
* @param event The animation event
|
* @param event The animation event
|
||||||
*/
|
*/
|
||||||
startSlide(event: any): void {
|
startSlide(event: any): void {
|
||||||
|
this.sidebarTransitioning = true;
|
||||||
if (event.toState === 'expanded') {
|
if (event.toState === 'expanded') {
|
||||||
this.sidebarClosed = false;
|
this.sidebarClosed = false;
|
||||||
} else if (event.toState === 'collapsed') {
|
} else if (event.toState === 'collapsed') {
|
||||||
@@ -146,6 +157,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
* @param event The animation event
|
* @param event The animation event
|
||||||
*/
|
*/
|
||||||
finishSlide(event: any): void {
|
finishSlide(event: any): void {
|
||||||
|
this.sidebarTransitioning = false;
|
||||||
if (event.fromState === 'expanded') {
|
if (event.fromState === 'expanded') {
|
||||||
this.sidebarClosed = true;
|
this.sidebarClosed = true;
|
||||||
} else if (event.fromState === 'collapsed') {
|
} else if (event.fromState === 'collapsed') {
|
||||||
|
@@ -1,37 +1,47 @@
|
|||||||
<div class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
<li [ngClass]="{'expanded': (isExpanded$ | async)}" role="presentation"
|
||||||
[@bgColor]="{
|
[@bgColor]="{
|
||||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
value: ((isExpanded$ | async) ? 'endBackground' : 'startBackground'),
|
||||||
params: {endColor: (sidebarActiveBg | async)}}">
|
params: {endColor: (sidebarActiveBg$ | async)}
|
||||||
<div class="nav-item nav-link d-flex flex-row flex-nowrap"
|
}">
|
||||||
role="button" tabindex="0"
|
<a class="sidebar-section-wrapper"
|
||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
role="menuitem" tabindex="0"
|
||||||
[attr.aria-expanded]="expanded | async"
|
aria-haspopup="menu"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[attr.aria-controls]="adminMenuSectionId(section.id)"
|
||||||
[class.disabled]="section.model?.disabled"
|
[attr.aria-expanded]="isExpanded$ | async"
|
||||||
|
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
|
||||||
|
[class.disabled]="section.model?.disabled"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
(keyup.space)="toggleSection($event)"
|
(keyup.space)="toggleSection($event)"
|
||||||
(keyup.enter)="toggleSection($event)"
|
(keyup.enter)="toggleSection($event)"
|
||||||
|
href="javascript:void(0);"
|
||||||
>
|
>
|
||||||
<div class="shortcut-icon h-100">
|
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-collapsible">
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
<div class="toggle">
|
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
<span [id]="adminMenuSectionTitleId(section.id)">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<i class="fas fa-chevron-right fa-pull-right"
|
<i class="fas fa-chevron-right fa-xs" aria-hidden="true"
|
||||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'"
|
[@rotate]="(isExpanded$ | async) ? 'expanded' : 'collapsed'"
|
||||||
[title]="('menu.section.toggle.' + section.id) | translate"
|
|
||||||
></i>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
</div>
|
||||||
<li *ngFor="let subSection of (subSections$ | async)">
|
</a>
|
||||||
<ng-container
|
<div class="sidebar-section-wrapper subsection" @slide *ngIf="(isExpanded$ | async)">
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
<div class="sidebar-fixed-element-wrapper"></div>
|
||||||
</li>
|
<div class="sidebar-collapsible-element-outer-wrapper">
|
||||||
</ul>
|
<div class="sidebar-collapsible-element-inner-wrapper">
|
||||||
|
<ul class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate">
|
||||||
|
<li class="sidebar-item" role="presentation" *ngFor="let subSection of (subSections$ | async)">
|
||||||
|
{{ (sectionMap$ | async).get(subSection.id).component | json}}
|
||||||
|
<ng-container
|
||||||
|
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
|
@@ -1,23 +1,24 @@
|
|||||||
:host ::ng-deep {
|
:host {
|
||||||
.fa-chevron-right {
|
ul.sidebar-sub-level-item-list {
|
||||||
padding-left: calc(var(--bs-spacer) / 2);
|
list-style: none;
|
||||||
font-size: 0.5rem;
|
margin: 0;
|
||||||
line-height: 3;
|
padding: 0;
|
||||||
|
::ng-deep .ds-menu-item {
|
||||||
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-sub-level-items {
|
.toggler-wrapper {
|
||||||
list-style: disc;
|
display: flex;
|
||||||
color: var(--bs-navbar-dark-color);
|
flex-direction: row;
|
||||||
overflow: hidden;
|
flex-wrap: nowrap;
|
||||||
margin-bottom: calc(-1 * var(--bs-spacer)); // the bottom-most nav-item is padded, no need for double spacing
|
align-items: center;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-collapsible {
|
.subsection {
|
||||||
display: flex;
|
.sidebar-collapsible-element-inner-wrapper {
|
||||||
flex-direction: column;
|
overflow-y: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
li.sidebar-section.expanded {
|
|
||||||
background-color: var(--ds-admin-sidebar-active-bg) !important;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -49,14 +49,14 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set the right icon', () => {
|
it('should set the right icon', () => {
|
||||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon > i.fas'));
|
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"] > i.fas'));
|
||||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the header text is clicked', () => {
|
describe('when the header text is clicked', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'toggleActiveSection');
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section > div.nav-item'));
|
const sidebarToggler = fixture.debugElement.query(By.css('a.sidebar-section-wrapper'));
|
||||||
sidebarToggler.triggerEventHandler('click', {
|
sidebarToggler.triggerEventHandler('click', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
|
@@ -31,23 +31,23 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
/**
|
/**
|
||||||
* The background color of the section when it's active
|
* The background color of the section when it's active
|
||||||
*/
|
*/
|
||||||
sidebarActiveBg;
|
sidebarActiveBg$: Observable<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the sidebar is currently collapsed, true when it's expanded
|
* Emits true when the sidebar is currently collapsed, true when it's expanded
|
||||||
*/
|
*/
|
||||||
sidebarCollapsed: Observable<boolean>;
|
isSidebarCollapsed$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the sidebar's preview is currently collapsed, true when it's expanded
|
* Emits true when the sidebar's preview is currently collapsed, true when it's expanded
|
||||||
*/
|
*/
|
||||||
sidebarPreviewCollapsed: Observable<boolean>;
|
isSidebarPreviewCollapsed$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits true when the menu section is expanded, else emits false
|
* Emits true when the menu section is expanded, else emits false
|
||||||
* This is true when the section is active AND either the sidebar or it's preview is open
|
* This is true when the section is active AND either the sidebar or it's preview is open
|
||||||
*/
|
*/
|
||||||
expanded: Observable<boolean>;
|
isExpanded$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('sectionDataProvider') menuSection,
|
@Inject('sectionDataProvider') menuSection,
|
||||||
@@ -64,12 +64,20 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||||
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
this.isExpanded$ = combineLatestObservable([this.active, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
|
||||||
.pipe(
|
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
||||||
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
toggleSection($event: Event) {
|
||||||
|
this.menuService.expandMenuPreview(this.menuID); // fixes accessibility issue
|
||||||
|
super.toggleSection($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
adminMenuSubsectionId(sectionId: string, subsectionId: string) {
|
||||||
|
return `admin-menu-section-${sectionId}-${subsectionId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Themed wrapper for AdminSidebarComponent
|
* Themed wrapper for AdminSidebarComponent
|
||||||
@@ -11,6 +12,19 @@ import { AdminSidebarComponent } from './admin-sidebar.component';
|
|||||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
})
|
})
|
||||||
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
|
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the width of the sidebar when expanded
|
||||||
|
*/
|
||||||
|
@Input() expandedSidebarWidth$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the width of the sidebar when collapsed
|
||||||
|
*/
|
||||||
|
@Input() collapsedSidebarWidth$: Observable<string>;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof AdminSidebarComponent & keyof this)[] = ['collapsedSidebarWidth$', 'expandedSidebarWidth$'];
|
||||||
|
|
||||||
protected getComponentName(): string {
|
protected getComponentName(): string {
|
||||||
return 'AdminSidebarComponent';
|
return 'AdminSidebarComponent';
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,36 @@
|
|||||||
<div class="nav-item dropdown expandable-navbar-section text-md-center"
|
<li class="ds-menu-item-wrapper text-md-center" role="presentation"
|
||||||
*ngVar="(active | async) as isActive"
|
[id]="'expandable-navbar-section-' + section.id"
|
||||||
(keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)"
|
(mouseenter)="onMouseEnter($event, isActive)"
|
||||||
(keyup.space)="isActive ? deactivateSection($event) : activateSection($event)"
|
(mouseleave)="onMouseLeave($event, isActive)"
|
||||||
(keydown.space)="$event.preventDefault()"
|
data-test="navbar-section-wrapper"
|
||||||
(mouseenter)="activateSection($event)"
|
*ngVar="(active | async) as isActive">
|
||||||
(mouseleave)="deactivateSection($event)">
|
<a href="javascript:void(0);" routerLinkActive="active"
|
||||||
<button class="btn btn-link nav-link dropdown-toggle" routerLinkActive="active" type="button"
|
role="menuitem"
|
||||||
[class.disabled]="section.model?.disabled"
|
(keyup.enter)="toggleSection($event)"
|
||||||
|
(keyup.space)="toggleSection($event)"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
data-toggle="dropdown">
|
(keydown.space)="$event.preventDefault()"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
data-test="navbar-section-toggler"
|
||||||
|
[attr.aria-expanded]="isActive"
|
||||||
|
[attr.aria-controls]="expandableNavbarSectionId(section.id)"
|
||||||
|
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
|
||||||
|
[class.disabled]="section.model?.disabled"
|
||||||
|
id="browseDropdown">
|
||||||
|
<span class="flex-fill">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</button>
|
<!-- <span class="sr-only">{{'nav.expandable-navbar-section-suffix' | translate}}</span>-->
|
||||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
</span>
|
||||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
|
||||||
<li *ngFor="let subSection of (subSections$ | async)">
|
</a>
|
||||||
|
<ul @slide *ngIf="isActive" (click)="deactivateSection($event)"
|
||||||
|
[id]="expandableNavbarSectionId(section.id)"
|
||||||
|
role="menu"
|
||||||
|
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
|
||||||
|
<li *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</li>
|
||||||
|
@@ -1,47 +1,24 @@
|
|||||||
.expandable-navbar-section {
|
:host {
|
||||||
display: flex;
|
li.ds-menu-item-wrapper {
|
||||||
height: 100%;
|
position: relative; // align dropdown menu with respect to this element
|
||||||
flex-direction: column;
|
}
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.nav-link {
|
.dropdown-menu {
|
||||||
text-align: left;
|
overflow: hidden;
|
||||||
border: none;
|
@include media-breakpoint-down(sm) {
|
||||||
|
border: 0;
|
||||||
|
background-color: var(--ds-expandable-navbar-bg);
|
||||||
|
}
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
background-color: var(--ds-navbar-dropdown-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:active, &:focus {
|
.toggle-menu-icon {
|
||||||
box-shadow: none !important;
|
&, &:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
background-color: var(--ds-expandable-navbar-bg);
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: 100%;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
::ng-deep a.nav-link {
|
|
||||||
color: var(--ds-expandable-navbar-link-color) !important;
|
|
||||||
padding-right: var(--bs-spacer);
|
|
||||||
padding-left: var(--bs-spacer);
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:hover, &:focus {
|
|
||||||
color: var(--ds-expandable-navbar-link-color-hover) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mobile menu styling **/
|
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
|
||||||
.dropdown-toggle {
|
|
||||||
&:after {
|
|
||||||
float: right;
|
|
||||||
margin-top: calc(var(--bs-spacer) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dropdown-menu {
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -47,120 +47,138 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the mouse enters the section header', () => {
|
describe('when the mouse enters the section header (while inactive)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'onMouseEnter').and.callThrough();
|
||||||
|
spyOn(component, 'activateSection').and.callThrough();
|
||||||
spyOn(menuService, 'activateSection');
|
spyOn(menuService, 'activateSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
||||||
|
component.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]'));
|
||||||
sidebarToggler.triggerEventHandler('mouseenter', {
|
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call activateSection on the menuService', () => {
|
it('should call onMouseEnter', () => {
|
||||||
|
expect(component.onMouseEnter).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should activate the section', () => {
|
||||||
|
expect(component.activateSection).toHaveBeenCalled();
|
||||||
expect(menuService.activateSection).toHaveBeenCalled();
|
expect(menuService.activateSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the mouse leaves the section header', () => {
|
describe('when the mouse leaves the section header (while active)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(component, 'onMouseLeave').and.callThrough();
|
||||||
|
spyOn(component, 'deactivateSection').and.callThrough();
|
||||||
spyOn(menuService, 'deactivateSection');
|
spyOn(menuService, 'deactivateSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
// Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true));
|
||||||
|
component.ngOnInit();
|
||||||
|
component.mouseEntered = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]'));
|
||||||
sidebarToggler.triggerEventHandler('mouseleave', {
|
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call deactivateSection on the menuService', () => {
|
it('should call onMouseLeave', () => {
|
||||||
|
expect(component.onMouseLeave).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deactivate the section', () => {
|
||||||
|
expect(component.deactivateSection).toHaveBeenCalled();
|
||||||
expect(menuService.deactivateSection).toHaveBeenCalled();
|
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when Enter key is pressed on section header (while inactive)', () => {
|
describe('when Enter key is pressed on section header (while inactive)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'activateSection');
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.enter) action used in our component HTML
|
// dispatch the (keyup.enter) action used in our component HTML
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call activateSection on the menuService', () => {
|
it('should call activateSection on the menuService', () => {
|
||||||
expect(menuService.activateSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when Enter key is pressed on section header (while active)', () => {
|
describe('when Enter key is pressed on section header (while active)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'deactivateSection');
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
// Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property.
|
// Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true));
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.enter) action used in our component HTML
|
// dispatch the (keyup.enter) action used in our component HTML
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call deactivateSection on the menuService', () => {
|
it('should call toggleSection on the menuService', () => {
|
||||||
expect(menuService.deactivateSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while inactive)', () => {
|
describe('when spacebar is pressed on section header (while inactive)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'activateSection');
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
// Make sure section is 'inactive'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(false));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.space) action used in our component HTML
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call activateSection on the menuService', () => {
|
it('should call toggleSection on the menuService', () => {
|
||||||
expect(menuService.activateSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when spacebar is pressed on section header (while active)', () => {
|
describe('when spacebar is pressed on section header (while active)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'deactivateSection');
|
spyOn(component, 'toggleSection').and.callThrough();
|
||||||
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
// Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property.
|
// Make sure section is 'active'. Requires calling ngOnInit() to update component 'active' property.
|
||||||
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true));
|
spyOn(menuService, 'isSectionActive').and.returnValue(observableOf(true));
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
// dispatch the (keyup.space) action used in our component HTML
|
// dispatch the (keyup.space) action used in our component HTML
|
||||||
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
sidebarToggler.nativeElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call deactivateSection on the menuService', () => {
|
it('should call toggleSection on the menuService', () => {
|
||||||
expect(menuService.deactivateSection).toHaveBeenCalled();
|
expect(component.toggleSection).toHaveBeenCalled();
|
||||||
});
|
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||||
});
|
|
||||||
|
|
||||||
describe('when a click occurs on the section header', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(menuService, 'toggleActiveSection');
|
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown > button'));
|
|
||||||
sidebarToggler.triggerEventHandler('click', {
|
|
||||||
preventDefault: () => {/**/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call toggleActiveSection on the menuService', () => {
|
|
||||||
expect(menuService.toggleActiveSection).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -195,7 +213,7 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
describe('when the mouse enters the section header', () => {
|
describe('when the mouse enters the section header', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'activateSection');
|
spyOn(menuService, 'activateSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown > button'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]'));
|
||||||
sidebarToggler.triggerEventHandler('mouseenter', {
|
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
@@ -210,7 +228,7 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
describe('when the mouse leaves the section header', () => {
|
describe('when the mouse leaves the section header', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'deactivateSection');
|
spyOn(menuService, 'deactivateSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown > button'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-wrapper"]'));
|
||||||
sidebarToggler.triggerEventHandler('mouseleave', {
|
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
@@ -225,7 +243,7 @@ describe('ExpandableNavbarSectionComponent', () => {
|
|||||||
describe('when a click occurs on the section header link', () => {
|
describe('when a click occurs on the section header link', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(menuService, 'toggleActiveSection');
|
spyOn(menuService, 'toggleActiveSection');
|
||||||
const sidebarToggler = fixture.debugElement.query(By.css('div.nav-item.dropdown > button'));
|
const sidebarToggler = fixture.debugElement.query(By.css('[data-test="navbar-section-toggler"]'));
|
||||||
sidebarToggler.triggerEventHandler('click', {
|
sidebarToggler.triggerEventHandler('click', {
|
||||||
preventDefault: () => {/**/
|
preventDefault: () => {/**/
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
import { Component, HostListener, Inject, Injector, OnInit } from '@angular/core';
|
||||||
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { slide } from '../../shared/animations/slide';
|
import { slide } from '../../shared/animations/slide';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an expandable section in the navbar
|
* Represents an expandable section in the navbar
|
||||||
@@ -21,12 +22,42 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
*/
|
*/
|
||||||
menuID = MenuID.PUBLIC;
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if mouse has entered the menu section toggler
|
||||||
|
*/
|
||||||
|
mouseEntered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if screen size was small before a resize event
|
||||||
|
*/
|
||||||
|
wasMobile = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits true if the screen is small, false otherwise
|
||||||
|
*/
|
||||||
|
isMobile$: Observable<boolean>;
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize() {
|
||||||
|
this.isMobile$.pipe(
|
||||||
|
first()
|
||||||
|
).subscribe((isMobile) => {
|
||||||
|
// When switching between desktop and mobile active sections should be deactivated
|
||||||
|
if (isMobile !== this.wasMobile) {
|
||||||
|
this.wasMobile = isMobile;
|
||||||
|
this.menuService.deactivateSection(this.menuID, this.section.id);
|
||||||
|
this.mouseEntered = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
constructor(@Inject('sectionDataProvider') menuSection,
|
constructor(@Inject('sectionDataProvider') menuSection,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private windowService: HostWindowService
|
private windowService: HostWindowService
|
||||||
) {
|
) {
|
||||||
super(menuSection, menuService, injector);
|
super(menuSection, menuService, injector);
|
||||||
|
this.isMobile$ = this.windowService.isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -34,48 +65,42 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the super function that activates this section (triggered on hover)
|
* When the mouse enters the section toggler activate the menu section
|
||||||
* Has an extra check to make sure the section can only be activated on non-mobile devices
|
* @param $event
|
||||||
* @param {Event} event The user event that triggered this function
|
* @param isActive
|
||||||
*/
|
*/
|
||||||
activateSection(event): void {
|
onMouseEnter($event: Event, isActive: boolean) {
|
||||||
this.windowService.isXsOrSm().pipe(
|
this.isMobile$.pipe(
|
||||||
first()
|
first()
|
||||||
).subscribe((isMobile) => {
|
).subscribe((isMobile) => {
|
||||||
if (!isMobile) {
|
if (!isMobile && !isActive && !this.mouseEntered) {
|
||||||
super.activateSection(event);
|
this.activateSection($event);
|
||||||
}
|
}
|
||||||
|
this.mouseEntered = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the super function that deactivates this section (triggered on hover)
|
* When the mouse leaves the section toggler deactivate the menu section
|
||||||
* Has an extra check to make sure the section can only be deactivated on non-mobile devices
|
* @param $event
|
||||||
* @param {Event} event The user event that triggered this function
|
* @param isActive
|
||||||
*/
|
*/
|
||||||
deactivateSection(event): void {
|
onMouseLeave($event: Event, isActive: boolean) {
|
||||||
this.windowService.isXsOrSm().pipe(
|
this.isMobile$.pipe(
|
||||||
first()
|
first()
|
||||||
).subscribe((isMobile) => {
|
).subscribe((isMobile) => {
|
||||||
if (!isMobile) {
|
if (!isMobile && isActive && this.mouseEntered) {
|
||||||
super.deactivateSection(event);
|
this.deactivateSection($event);
|
||||||
}
|
}
|
||||||
|
this.mouseEntered = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the super function that toggles this section (triggered on click)
|
* returns the ID of the DOM element representing the navbar section
|
||||||
* Has an extra check to make sure the section can only be toggled on mobile devices
|
* @param sectionId
|
||||||
* @param {Event} event The user event that triggered this function
|
|
||||||
*/
|
*/
|
||||||
toggleSection(event): void {
|
expandableNavbarSectionId(sectionId: string) {
|
||||||
event.preventDefault();
|
return `expandable-navbar-section-${sectionId}-dropdown`;
|
||||||
this.windowService.isXsOrSm().pipe(
|
|
||||||
first()
|
|
||||||
).subscribe((isMobile) => {
|
|
||||||
if (isMobile) {
|
|
||||||
super.toggleSection(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
<div class="nav-item navbar-section text-md-center">
|
<li role="presentation"
|
||||||
|
class="ds-menu-item-wrapper text-md-center"
|
||||||
|
[id]="'navbar-section-' + section.id">
|
||||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</div>
|
</li>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
.navbar-section {
|
:host {
|
||||||
display: flex;
|
li.ds-menu-item-wrapper {
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,21 @@
|
|||||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
||||||
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||||
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
class="navbar navbar-light navbar-expand-md px-md-0 pt-md-0 pt-3 navbar-container" role="navigation"
|
||||||
<div class="navbar-inner-container w-100" [class.container]="!(isMobile$ | async)">
|
[attr.aria-label]="'nav.main.description' | translate" id="main-navbar">
|
||||||
<div class="reset-padding-md w-100">
|
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
||||||
<div id="collapsingNav">
|
<div class="navbar-inner-container w-100" [class.container]="!(isMobile$ | async)">
|
||||||
<ul class="navbar-nav navbar-navigation mr-auto shadow-none">
|
<div class="w-100">
|
||||||
<li *ngIf="(isMobile$ | async) && (isAuthenticated$ | async)">
|
<div id="collapsingNav">
|
||||||
<ds-themed-user-menu [inExpandableNavbar]="true"></ds-themed-user-menu>
|
<ng-container *ngIf="(isMobile$ | async) && (isAuthenticated$ | async)">
|
||||||
</li>
|
<ds-themed-user-menu [inExpandableNavbar]="true"></ds-themed-user-menu>
|
||||||
<li *ngFor="let section of (sections | async)">
|
</ng-container>
|
||||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
<ul class="navbar-nav align-items-md-center mr-auto shadow-none gapx-3">
|
||||||
</li>
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
</ul>
|
<ng-container
|
||||||
</div>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||||
</div>
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
:host {
|
:host {
|
||||||
|
--ds-collapsible-navbar-height: auto;
|
||||||
|
|
||||||
nav.navbar {
|
nav.navbar {
|
||||||
background-color: var(--ds-navbar-bg);
|
background-color: var(--ds-navbar-bg);
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mobile menu styling **/
|
/** Mobile menu styling **/
|
||||||
@@ -23,8 +26,8 @@
|
|||||||
|
|
||||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||||
.reset-padding-md {
|
.reset-padding-md {
|
||||||
margin-left: calc(var(--bs-spacer) / -2);
|
margin-left: calc(var(--bs-spacer) / -1);
|
||||||
margin-right: calc(var(--bs-spacer) / -2);
|
margin-right: calc(var(--bs-spacer) / -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,9 +41,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav {
|
#main-navbar ::ng-deep {
|
||||||
::ng-deep a.nav-link, .btn.nav-link {
|
.ds-menu-item, .ds-menu-toggler-wrapper {
|
||||||
color: var(--ds-navbar-link-color);
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-menu-item {
|
||||||
|
display: block;
|
||||||
|
color: var(--ds-navbar-link-color);
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
color: var(--ds-navbar-link-color-hover);
|
color: var(--ds-navbar-link-color-hover);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
<button (click)="skipToMainContent()" class="btn btn-primary" id="skip-to-main-content">
|
<button (click)="skipToMainContent()" class="sr-only" id="skip-to-main-content">
|
||||||
{{ 'root.skip-to-content' | translate }}
|
{{ 'root.skip-to-content' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader" [@slideSidebarPadding]="{
|
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader" [@slideSidebarPadding]="{
|
||||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
value: (!(isSidebarVisible$ | async) ? 'hidden' : (slideSidebarOver$ | async) ? 'unpinned' : 'pinned'),
|
||||||
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
|
params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) }
|
||||||
}">
|
}">
|
||||||
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
|
<ds-themed-admin-sidebar [expandedSidebarWidth$]="expandedSidebarWidth$" [collapsedSidebarWidth$]="collapsedSidebarWidth$"></ds-themed-admin-sidebar>
|
||||||
<div class="inner-wrapper">
|
<div class="inner-wrapper">
|
||||||
<ds-system-wide-alert-banner></ds-system-wide-alert-banner>
|
<ds-system-wide-alert-banner></ds-system-wide-alert-banner>
|
||||||
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
|
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
|
||||||
|
@@ -1,25 +1,17 @@
|
|||||||
import { map, startWith } from 'rxjs/operators';
|
import { first, map, skipWhile, startWith } from 'rxjs/operators';
|
||||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { MetadataService } from '../core/metadata/metadata.service';
|
|
||||||
import { HostWindowState } from '../shared/search/host-window.reducer';
|
|
||||||
import { NativeWindowRef, NativeWindowService } from '../core/services/window.service';
|
|
||||||
import { AuthService } from '../core/auth/auth.service';
|
|
||||||
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { ThemeConfig } from '../../config/theme.config';
|
import { ThemeConfig } from '../../config/theme.config';
|
||||||
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { slideSidebarPadding } from '../shared/animations/slide';
|
import { slideSidebarPadding } from '../shared/animations/slide';
|
||||||
import { MenuID } from '../shared/menu/menu-id.model';
|
import { MenuID } from '../shared/menu/menu-id.model';
|
||||||
import { getPageInternalServerErrorRoute } from '../app-routing-paths';
|
import { getPageInternalServerErrorRoute } from '../app-routing-paths';
|
||||||
import { hasValueOperator } from '../shared/empty.util';
|
import { INotificationBoardOptions } from 'src/config/notifications-config.interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-root',
|
selector: 'ds-root',
|
||||||
@@ -28,13 +20,13 @@ import { hasValueOperator } from '../shared/empty.util';
|
|||||||
animations: [slideSidebarPadding],
|
animations: [slideSidebarPadding],
|
||||||
})
|
})
|
||||||
export class RootComponent implements OnInit {
|
export class RootComponent implements OnInit {
|
||||||
sidebarVisible: Observable<boolean>;
|
|
||||||
slideSidebarOver: Observable<boolean>;
|
|
||||||
collapsedSidebarWidth: Observable<string>;
|
|
||||||
totalSidebarWidth: Observable<string>;
|
|
||||||
theme: Observable<ThemeConfig> = of({} as any);
|
theme: Observable<ThemeConfig> = of({} as any);
|
||||||
notificationOptions;
|
isSidebarVisible$: Observable<boolean>;
|
||||||
models;
|
slideSidebarOver$: Observable<boolean>;
|
||||||
|
collapsedSidebarWidth$: Observable<string>;
|
||||||
|
expandedSidebarWidth$: Observable<string>;
|
||||||
|
notificationOptions: INotificationBoardOptions;
|
||||||
|
models: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to show a full screen loader
|
* Whether or not to show a full screen loader
|
||||||
@@ -47,12 +39,6 @@ export class RootComponent implements OnInit {
|
|||||||
@Input() shouldShowRouteLoader: boolean;
|
@Input() shouldShowRouteLoader: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
|
||||||
private translate: TranslateService,
|
|
||||||
private store: Store<HostWindowState>,
|
|
||||||
private metadata: MetadataService,
|
|
||||||
private angulartics2DSpace: Angulartics2DSpace,
|
|
||||||
private authService: AuthService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
@@ -62,13 +48,19 @@ export class RootComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.sidebarVisible = this.menuService.isMenuVisibleWithVisibleSections(MenuID.ADMIN);
|
this.isSidebarVisible$ = this.menuService.isMenuVisibleWithVisibleSections(MenuID.ADMIN);
|
||||||
|
|
||||||
this.collapsedSidebarWidth = this.cssService.getVariable('--ds-collapsed-sidebar-width').pipe(hasValueOperator());
|
this.expandedSidebarWidth$ = this.cssService.getVariable('--ds-admin-sidebar-total-width').pipe(
|
||||||
this.totalSidebarWidth = this.cssService.getVariable('--ds-total-sidebar-width').pipe(hasValueOperator());
|
skipWhile((val) => !val),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
this.collapsedSidebarWidth$ = this.cssService.getVariable('--ds-admin-sidebar-fixed-element-width').pipe(
|
||||||
|
skipWhile((val) => !val),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
|
||||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||||
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
this.slideSidebarOver$ = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([collapsed, mobile]) => collapsed || mobile),
|
map(([collapsed, mobile]) => collapsed || mobile),
|
||||||
startWith(true),
|
startWith(true),
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { animate, animateChild, group, query, state, style, transition, trigger } from '@angular/animations';
|
import { animate, group, state, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
export const slide = trigger('slide', [
|
export const slide = trigger('slide', [
|
||||||
state('expanded', style({ height: '*' })),
|
state('expanded', style({ height: '*' })),
|
||||||
@@ -12,45 +12,35 @@ export const slide = trigger('slide', [
|
|||||||
|
|
||||||
export const slideMobileNav = trigger('slideMobileNav', [
|
export const slideMobileNav = trigger('slideMobileNav', [
|
||||||
|
|
||||||
state('expanded', style({ height: 'auto', 'min-height': '100vh' })),
|
state('expanded', style({ height: '*', minHeight: '*' })),
|
||||||
|
|
||||||
state('collapsed', style({ height: 0 })),
|
state('collapsed', style({ height: 0, minHeight: 0 })),
|
||||||
|
|
||||||
transition('expanded <=> collapsed', animate('300ms'))
|
transition('expanded <=> collapsed', animate('300ms'))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const collapsedStyle = style({ marginLeft: '-{{ sidebarWidth }}' });
|
const collapsedSidebarStyle = style({ maxWidth: '{{collapsedWidth}}' });
|
||||||
const expandedStyle = style({ marginLeft: '0' });
|
const expandedSidebarStyle = style({ maxWidth: '{{expandedWidth}}' });
|
||||||
const options = { params: { sidebarWidth: '*' } };
|
const unpinnedSidebarPageStyle = style({ paddingLeft: '{{collapsedWidth}}' });
|
||||||
|
const pinnedSidebarPageStyle = style({ paddingLeft: '{{expandedWidth}}' });
|
||||||
|
const hiddenSidebarPageStyle = style({ paddingLeft: '0' });
|
||||||
|
const options = { params: { collapsedWidth: '*', expandedWidth: '*' } };
|
||||||
|
const animation = '300ms ease-in-out';
|
||||||
|
|
||||||
export const slideSidebar = trigger('slideSidebar', [
|
export const slideSidebar = trigger('slideSidebar', [
|
||||||
|
state('collapsed', collapsedSidebarStyle, options),
|
||||||
transition('expanded => collapsed',
|
state('expanded', expandedSidebarStyle, options),
|
||||||
group(
|
transition('expanded => collapsed', animate(animation, collapsedSidebarStyle)),
|
||||||
[
|
transition('collapsed => expanded', animate(animation, expandedSidebarStyle)),
|
||||||
query('@*', animateChild()),
|
|
||||||
query('.sidebar-collapsible', expandedStyle, options),
|
|
||||||
query('.sidebar-collapsible', animate('300ms ease-in-out', collapsedStyle))
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
|
|
||||||
transition('collapsed => expanded',
|
|
||||||
group(
|
|
||||||
[
|
|
||||||
query('@*', animateChild()),
|
|
||||||
query('.sidebar-collapsible', collapsedStyle),
|
|
||||||
query('.sidebar-collapsible', animate('300ms ease-in-out', expandedStyle), options)
|
|
||||||
]
|
|
||||||
))
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const slideSidebarPadding = trigger('slideSidebarPadding', [
|
export const slideSidebarPadding = trigger('slideSidebarPadding', [
|
||||||
state('hidden', style({ paddingLeft: 0 })),
|
state('hidden', hiddenSidebarPageStyle),
|
||||||
state('shown', style({ paddingLeft: '{{ collapsedSidebarWidth }}' }), { params: { collapsedSidebarWidth: '*' } }),
|
state('unpinned', unpinnedSidebarPageStyle, options),
|
||||||
state('expanded', style({ paddingLeft: '{{ totalSidebarWidth }}' }), { params: { totalSidebarWidth: '*' } }),
|
state('pinned', pinnedSidebarPageStyle, options),
|
||||||
transition('hidden <=> shown', [animate('200ms')]),
|
transition('hidden <=> unpinned', animate(animation)),
|
||||||
transition('hidden <=> expanded', [animate('200ms')]),
|
transition('hidden <=> pinned', animate(animation)),
|
||||||
transition('shown <=> expanded', [animate('200ms')]),
|
transition('unpinned <=> pinned', animate(animation)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const expandSearchInput = trigger('toggleAnimation', [
|
export const expandSearchInput = trigger('toggleAnimation', [
|
||||||
|
@@ -1,40 +1,52 @@
|
|||||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
<div class="navbar-nav mr-auto" *ngIf="!(isMobile$ | async); else mobileButtons" data-test="auth-nav">
|
||||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
<div *ngIf="!(isAuthenticated | async) && (showAuth | async)"
|
||||||
|
class="nav-item"
|
||||||
(click)="$event.stopPropagation();">
|
(click)="$event.stopPropagation();">
|
||||||
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
<div ngbDropdown #loginDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
<button class="btn btn-link dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
|
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
|
||||||
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
|
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
|
||||||
ngbDropdownToggle>
|
role="menuitem"
|
||||||
{{ 'nav.login' | translate }}
|
aria-haspopup="menu"
|
||||||
</button>
|
aria-controls="loginDropdownMenu"
|
||||||
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
[attr.aria-expanded]="loginDrop.isOpen()"
|
||||||
|
ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
|
||||||
|
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
|
||||||
|
role="menu"
|
||||||
[attr.aria-label]="'nav.login' | translate">
|
[attr.aria-label]="'nav.login' | translate">
|
||||||
<ds-themed-log-in
|
<ds-themed-log-in
|
||||||
[isStandalonePage]="false"></ds-themed-log-in>
|
[isStandalonePage]="false"></ds-themed-log-in>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
<div *ngIf="(isAuthenticated | async) && (showAuth | async)" class="nav-item">
|
||||||
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5">
|
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||||
|
<a href="javascript:void(0);"
|
||||||
|
role="menuitem"
|
||||||
|
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
|
||||||
|
aria-controls="user-menu-dropdown"
|
||||||
|
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
|
||||||
|
class="dropdownLogout px-1"
|
||||||
|
[attr.data-test]="'user-menu' | dsBrowserOnly"
|
||||||
|
ngbDropdownToggle>
|
||||||
|
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
|
||||||
|
<ul id="logoutDropdownMenu" class="p-3" ngbDropdownMenu role="presentation">
|
||||||
|
<ds-themed-user-menu></ds-themed-user-menu>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #mobileButtons>
|
||||||
|
<div data-test="auth-nav">
|
||||||
|
<a *ngIf="!(isAuthenticated | async)" routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button">
|
||||||
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
<a *ngIf="(isAuthenticated | async)" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1">
|
||||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
|
||||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
|
||||||
<button role="button" [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" class="btn btn-link dropdownLogout px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
|
|
||||||
<i class="fas fa-user-circle fa-lg fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate">
|
|
||||||
<ds-themed-user-menu></ds-themed-user-menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
|
||||||
<a role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1">
|
|
||||||
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
|
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
|
||||||
<span class="sr-only">(current)</span>
|
<span class="sr-only">(current)</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</ng-template>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Do not use ul/li in this menu as it breaks e2e accessibility tests -->
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
.loginDropdownMenu, .logoutDropdownMenu {
|
#loginDropdownMenu, #logoutDropdownMenu {
|
||||||
min-width: 330px;
|
min-width: 330px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loginDropdownMenu {
|
#loginDropdownMenu {
|
||||||
min-height: 75px;
|
min-height: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@ import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
|
|||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
|
||||||
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
describe('AuthNavMenuComponent', () => {
|
describe('AuthNavMenuComponent', () => {
|
||||||
|
|
||||||
@@ -75,7 +76,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
strictActionImmutability: false
|
strictActionImmutability: false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot(),
|
||||||
|
NgbDropdownModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AuthNavMenuComponent,
|
AuthNavMenuComponent,
|
||||||
@@ -83,7 +85,7 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: HostWindowService, useValue: window },
|
{ provide: HostWindowService, useValue: window },
|
||||||
{ provide: AuthService, useValue: authService }
|
{ provide: AuthService, useValue: authService },
|
||||||
],
|
],
|
||||||
schemas: [
|
schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
@@ -117,10 +119,10 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
const navMenuItemSelector = '.nav-item';
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
}));
|
}));
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -155,10 +157,10 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
const navMenuItemSelector = '.nav-item';
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -197,10 +199,10 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
const navMenuItemSelector = '.nav-item';
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -210,7 +212,7 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render login dropdown menu', () => {
|
it('should render login dropdown menu', () => {
|
||||||
const loginDropdownMenu = deNavMenuItem.query(By.css('div.loginDropdownMenu'));
|
const loginDropdownMenu = deNavMenuItem.query(By.css('div#loginDropdownMenu'));
|
||||||
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -236,10 +238,10 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
const navMenuItemSelector = '.nav-item';
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -309,11 +311,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -322,7 +321,7 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render login link', () => {
|
it('should render login link', () => {
|
||||||
const loginDropdownMenu = deNavMenuItem.query(By.css('.loginLink'));
|
const loginDropdownMenu = deNavMenu.query(By.css('.loginLink'));
|
||||||
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
expect(loginDropdownMenu.nativeElement).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -345,11 +344,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const navMenuSelector = '.navbar-nav';
|
const navMenuSelector = '[data-test="auth-nav"]';
|
||||||
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
|
||||||
|
|
||||||
const navMenuItemSelector = 'li';
|
|
||||||
deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -358,7 +354,7 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render logout link', inject([Store], (store: Store<AppState>) => {
|
it('should render logout link', inject([Store], (store: Store<AppState>) => {
|
||||||
const logoutDropdownMenu = deNavMenuItem.query(By.css('a.logoutLink'));
|
const logoutDropdownMenu = deNavMenu.query(By.css('a.logoutLink'));
|
||||||
expect(logoutDropdownMenu.nativeElement).toBeDefined();
|
expect(logoutDropdownMenu.nativeElement).toBeDefined();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@@ -32,7 +32,7 @@ export class AuthNavMenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public loading: Observable<boolean>;
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
public isXsOrSm$: Observable<boolean>;
|
public isMobile$: Observable<boolean>;
|
||||||
|
|
||||||
public showAuth = observableOf(false);
|
public showAuth = observableOf(false);
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export class AuthNavMenuComponent implements OnInit {
|
|||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private authService: AuthService
|
private authService: AuthService
|
||||||
) {
|
) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isMobile$ = this.windowService.isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -1,15 +1,32 @@
|
|||||||
<ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading>
|
<ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading>
|
||||||
<div *ngIf="!(loading$ | async)">
|
<ul *ngIf="!(loading$ | async)" class="user-menu" role="menu"
|
||||||
<span class="dropdown-item-text" [class.pl-0]="inExpandableNavbar">
|
[ngClass]="{ 'pb-2 mb-2 border-bottom' : inExpandableNavbar }"
|
||||||
|
[attr.aria-label]="'nav.user-profile-menu-and-logout' |translate" id="user-menu-dropdown">
|
||||||
|
<li class="ds-menu-item-wrapper username-email-wrapper" role="presentation">
|
||||||
{{dsoNameService.getName(user$ | async)}}<br>
|
{{dsoNameService.getName(user$ | async)}}<br>
|
||||||
<span class="text-muted">{{(user$ | async)?.email}}</span>
|
<span class="text-muted">{{(user$ | async)?.email}}</span>
|
||||||
</span>
|
</li>
|
||||||
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
|
<li class="ds-menu-item-wrapper" role="presentation">
|
||||||
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
<a class="ds-menu-item" role="menuitem"
|
||||||
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[subscriptionsRoute]" routerLinkActive="active">{{'nav.subscriptions' | translate}}</a>
|
[routerLink]="[profileRoute]"
|
||||||
|
routerLinkActive="active">{{'nav.profile' | translate}}</a>
|
||||||
<div class="dropdown-divider"></div>
|
</li>
|
||||||
<ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
|
<li class="ds-menu-item-wrapper" role="presentation">
|
||||||
</div>
|
<a class="ds-menu-item" role="menuitem"
|
||||||
|
[routerLink]="[mydspaceRoute]"
|
||||||
|
routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
<li class="ds-menu-item-wrapper" role="presentation">
|
||||||
|
<a class="ds-menu-item" role="menuitem"
|
||||||
|
[routerLink]="[subscriptionsRoute]"
|
||||||
|
routerLinkActive="active">{{'nav.subscriptions' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
<ng-container *ngIf="!inExpandableNavbar">
|
||||||
|
<li class="dropdown-divider" aria-hidden="true"></li>
|
||||||
|
<li class="ds-menu-item-wrapper" role="presentation">
|
||||||
|
<ds-log-out data-test="log-out-component"></ds-log-out>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
ul.user-menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-menu-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -140,7 +140,7 @@ describe('UserMenuComponent', () => {
|
|||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
deUserMenu = fixture.debugElement.query(By.css('div'));
|
deUserMenu = fixture.debugElement.query(By.css('ul#user-menu-dropdown'));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -164,7 +164,7 @@ describe('UserMenuComponent', () => {
|
|||||||
it('should display user name and email', () => {
|
it('should display user name and email', () => {
|
||||||
const username = 'User Test';
|
const username = 'User Test';
|
||||||
const email = 'test@test.com';
|
const email = 'test@test.com';
|
||||||
const span = deUserMenu.query(By.css('.dropdown-item-text'));
|
const span = deUserMenu.query(By.css('.username-email-wrapper'));
|
||||||
expect(span).toBeDefined();
|
expect(span).toBeDefined();
|
||||||
expect(span.nativeElement.innerHTML).toContain(username);
|
expect(span.nativeElement.innerHTML).toContain(username);
|
||||||
expect(span.nativeElement.innerHTML).toContain(email);
|
expect(span.nativeElement.innerHTML).toContain(email);
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
<div ngbDropdown class="navbar-nav" *ngIf="moreThanOneLanguage" display="dynamic" placement="bottom-right">
|
<div ngbDropdown class="navbar-nav" *ngIf="moreThanOneLanguage" display="dynamic" placement="bottom-right">
|
||||||
<a href="javascript:void(0);" role="button"
|
<a href="javascript:void(0);" role="menuitem"
|
||||||
[attr.aria-label]="'nav.language' |translate"
|
[attr.aria-label]="'nav.language' |translate"
|
||||||
|
aria-controls="language-menu-list"
|
||||||
|
aria-haspopup="menu"
|
||||||
[title]="'nav.language' | translate"
|
[title]="'nav.language' | translate"
|
||||||
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
(click)="$event.preventDefault()" data-toggle="dropdown" ngbDropdownToggle
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
<i class="fas fa-globe-asia fa-lg fa-fw"></i>
|
||||||
</a>
|
</a>
|
||||||
<ul ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate">
|
<ul ngbDropdownMenu class="dropdown-menu" [attr.aria-label]="'nav.language' |translate" id="language-menu-list" role="menu">
|
||||||
<li class="dropdown-item" tabindex="0" #langSelect *ngFor="let lang of translate.getLangs()"
|
<li class="dropdown-item" tabindex="0" #langSelect *ngFor="let lang of translate.getLangs()"
|
||||||
|
role="menuitem"
|
||||||
(keyup.enter)="useLang(lang)"
|
(keyup.enter)="useLang(lang)"
|
||||||
(click)="useLang(lang)"
|
(click)="useLang(lang)"
|
||||||
[class.active]="lang === translate.currentLang">
|
[class.active]="lang === translate.currentLang">
|
||||||
|
@@ -28,10 +28,10 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">
|
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly" role="menuitem">
|
||||||
{{ 'login.form.new-user' | translate }}
|
{{ 'login.form.new-user' | translate }}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">
|
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly" role="menuitem">
|
||||||
{{ 'login.form.forgot-password' | translate }}
|
{{ 'login.form.forgot-password' | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
<a class="nav-item nav-link"
|
<a class="ds-menu-item" role="menuitem"
|
||||||
|
routerLinkActive="active"
|
||||||
[ngClass]="{'disabled': !hasLink }"
|
[ngClass]="{'disabled': !hasLink }"
|
||||||
[attr.aria-disabled]="!hasLink"
|
[attr.aria-disabled]="!hasLink"
|
||||||
[href]="href"
|
[href]="href"
|
||||||
|
@@ -9,7 +9,8 @@ import { MenuItemType } from '../menu-item-type.model';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-external-link-menu-item',
|
selector: 'ds-external-link-menu-item',
|
||||||
templateUrl: './external-link-menu-item.component.html'
|
styleUrls: ['./menu-item.component.scss'],
|
||||||
|
templateUrl: './external-link-menu-item.component.html',
|
||||||
})
|
})
|
||||||
@rendersMenuItemForType(MenuItemType.EXTERNAL)
|
@rendersMenuItemForType(MenuItemType.EXTERNAL)
|
||||||
export class ExternalLinkMenuItemComponent implements OnInit {
|
export class ExternalLinkMenuItemComponent implements OnInit {
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
|
<a class="ds-menu-item" role="menuitem"
|
||||||
<a class="nav-item nav-link"
|
|
||||||
[ngClass]="{ 'disabled': !hasLink || item.disabled }"
|
[ngClass]="{ 'disabled': !hasLink || item.disabled }"
|
||||||
[attr.aria-disabled]="!hasLink || item.disabled"
|
[attr.aria-disabled]="!hasLink || item.disabled"
|
||||||
[title]="item.text | translate"
|
[attr.data-test]="'link-menu-item.'+item.text"
|
||||||
[routerLink]="getRouterLink()"
|
[routerLink]="getRouterLink()"
|
||||||
[queryParams]="item.queryParams"
|
[queryParams]="item.queryParams"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
|
@@ -10,7 +10,8 @@ import { Router } from '@angular/router';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-link-menu-item',
|
selector: 'ds-link-menu-item',
|
||||||
templateUrl: './link-menu-item.component.html'
|
styleUrls: ['./menu-item.component.scss'],
|
||||||
|
templateUrl: './link-menu-item.component.html',
|
||||||
})
|
})
|
||||||
@rendersMenuItemForType(MenuItemType.LINK)
|
@rendersMenuItemForType(MenuItemType.LINK)
|
||||||
export class LinkMenuItemComponent implements OnInit {
|
export class LinkMenuItemComponent implements OnInit {
|
||||||
|
5
src/app/shared/menu/menu-item/menu-item.component.scss
Normal file
5
src/app/shared/menu/menu-item/menu-item.component.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
:host {
|
||||||
|
.ds-menu-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
<a *ngIf="!item.disabled"
|
<a *ngIf="!item.disabled"
|
||||||
|
class="ds-menu-item" role="menuitem"
|
||||||
|
routerLinkActive="active"
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
class="nav-item nav-link"
|
|
||||||
role="button"
|
|
||||||
[title]="item.text | translate"
|
[title]="item.text | translate"
|
||||||
(click)="activate($event)"
|
(click)="activate($event)"
|
||||||
(keyup.space)="activate($event)"
|
(keyup.space)="activate($event)"
|
||||||
(keyup.enter)="activate($event)"
|
(keyup.enter)="activate($event)"
|
||||||
>{{item.text | translate}}</a>
|
>{{item.text | translate}}</a>
|
||||||
<span *ngIf="item.disabled" class="nav-item nav-link disabled">{{item.text | translate}}</span>
|
<span *ngIf="item.disabled" class="nav-item nav-link disabled">{{item.text | translate}}</span>
|
||||||
|
@@ -45,7 +45,7 @@ describe('OnClickMenuItemComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call the function on the item when clicked', () => {
|
it('should call the function on the item when clicked', () => {
|
||||||
debugElement.query(By.css('a.nav-link')).triggerEventHandler('click', new Event(('click')));
|
debugElement.query(By.css('a.ds-menu-item')).triggerEventHandler('click', new Event(('click')));
|
||||||
expect(item.function).toHaveBeenCalled();
|
expect(item.function).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,7 +8,7 @@ import { MenuItemType } from '../menu-item-type.model';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-onclick-menu-item',
|
selector: 'ds-onclick-menu-item',
|
||||||
styleUrls: ['./onclick-menu-item.component.scss'],
|
styleUrls: ['./menu-item.component.scss', './onclick-menu-item.component.scss'],
|
||||||
templateUrl: './onclick-menu-item.component.html'
|
templateUrl: './onclick-menu-item.component.html'
|
||||||
})
|
})
|
||||||
@rendersMenuItemForType(MenuItemType.ONCLICK)
|
@rendersMenuItemForType(MenuItemType.ONCLICK)
|
||||||
|
@@ -1 +1 @@
|
|||||||
<span [class.disabled]="item.disabled">{{item.text | translate}}</span>
|
<span class="ds-menu-item" [class.disabled]="item.disabled">{{item.text | translate}}</span>
|
||||||
|
@@ -8,6 +8,7 @@ import { MenuItemType } from '../menu-item-type.model';
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-text-menu-item',
|
selector: 'ds-text-menu-item',
|
||||||
|
styleUrls: ['./menu-item.component.scss'],
|
||||||
templateUrl: './text-menu-item.component.html',
|
templateUrl: './text-menu-item.component.html',
|
||||||
})
|
})
|
||||||
@rendersMenuItemForType(MenuItemType.TEXT)
|
@rendersMenuItemForType(MenuItemType.TEXT)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import {of as observableOf, Observable } from 'rxjs';
|
import {of as observableOf, Observable } from 'rxjs';
|
||||||
|
import { WidthCategory } from '../host-window.service';
|
||||||
|
|
||||||
// declare a stub service
|
// declare a stub service
|
||||||
export class HostWindowServiceStub {
|
export class HostWindowServiceStub {
|
||||||
@@ -24,4 +25,8 @@ export class HostWindowServiceStub {
|
|||||||
isMobile(): Observable<boolean> {
|
isMobile(): Observable<boolean> {
|
||||||
return this.isXs();
|
return this.isXs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUpTo(maxWidthCat: WidthCategory): Observable<boolean> {
|
||||||
|
return this.isXs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,9 @@ export class MenuServiceStub {
|
|||||||
deactivateSection(): void { /***/
|
deactivateSection(): void { /***/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSection(): void { /***/
|
||||||
|
}
|
||||||
|
|
||||||
addSection(menuID: MenuID, section: MenuSection): void { /***/
|
addSection(menuID: MenuID, section: MenuSection): void { /***/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="(systemWideAlert$ |async)?.active">
|
<div *ngIf="(systemWideAlert$ |async)?.active">
|
||||||
<div class="rounded-0 alert alert-warning w100">
|
<div class="rounded-0 alert alert-warning w100 m-0 px-1">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span class="font-weight-bold">
|
<span class="font-weight-bold">
|
||||||
<span *ngIf="(countDownDays|async) > 0 || (countDownHours| async) > 0 || (countDownMinutes|async) > 0 ">
|
<span *ngIf="(countDownDays|async) > 0 || (countDownHours| async) > 0 || (countDownMinutes|async) > 0 ">
|
||||||
|
@@ -2854,6 +2854,8 @@
|
|||||||
|
|
||||||
"logout.title": "Logout",
|
"logout.title": "Logout",
|
||||||
|
|
||||||
|
"menu.header.nav.description": "Admin navigation bar",
|
||||||
|
|
||||||
"menu.header.admin": "Management",
|
"menu.header.admin": "Management",
|
||||||
|
|
||||||
"menu.header.image.logo": "Repository logo",
|
"menu.header.image.logo": "Repository logo",
|
||||||
@@ -3100,6 +3102,8 @@
|
|||||||
|
|
||||||
"mydspace.view-btn": "View",
|
"mydspace.view-btn": "View",
|
||||||
|
|
||||||
|
"nav.expandable-navbar-section-suffix": "(submenu)",
|
||||||
|
|
||||||
"nav.browse.header": "All of DSpace",
|
"nav.browse.header": "All of DSpace",
|
||||||
|
|
||||||
"nav.community-browse.header": "By Community",
|
"nav.community-browse.header": "By Community",
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
/** Help Variables **/
|
/** Help Variables **/
|
||||||
$fa-fixed-width: 1.25rem !default;
|
$fa-fixed-width: 1.25rem !default;
|
||||||
$icon-padding: 1rem !default;
|
$icon-padding: 1rem !default;
|
||||||
$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)) !default;
|
|
||||||
$sidebar-items-width: 250px !default;
|
|
||||||
$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default;
|
|
||||||
|
|
||||||
/* Fonts */
|
/* Fonts */
|
||||||
// Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path
|
// Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path
|
||||||
|
@@ -40,13 +40,24 @@
|
|||||||
|
|
||||||
$admin-sidebar-bg: darken(#2B4E72, 17%);
|
$admin-sidebar-bg: darken(#2B4E72, 17%);
|
||||||
$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%);
|
$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%);
|
||||||
|
$admin-sidebar-fixed-element-width: 55px !default;
|
||||||
|
$admin-sidebar-collapsible-element-width: 250px !default;
|
||||||
|
$admin-sidebar-total-width: $admin-sidebar-fixed-element-width + $admin-sidebar-collapsible-element-width !default;
|
||||||
|
|
||||||
--ds-admin-sidebar-bg: #{$admin-sidebar-bg};
|
--ds-admin-sidebar-bg: #{$admin-sidebar-bg};
|
||||||
--ds-admin-sidebar-active-bg: #{$admin-sidebar-active-bg};
|
--ds-admin-sidebar-active-bg: #{$admin-sidebar-active-bg};
|
||||||
--ds-admin-sidebar-header-bg: #{darken($admin-sidebar-bg, 7%)};
|
--ds-admin-sidebar-header-bg: #{darken($admin-sidebar-bg, 7%)};
|
||||||
|
--ds-admin-sidebar-fixed-element-width: #{$admin-sidebar-fixed-element-width};
|
||||||
|
--ds-admin-sidebar-fixed-element-z-index: 10;
|
||||||
|
--ds-admin-sidebar-collapsible-element-width: #{$admin-sidebar-collapsible-element-width};
|
||||||
|
--ds-admin-sidebar-total-width: #{$admin-sidebar-total-width};
|
||||||
|
--ds-admin-sidebar-link-color: #{$navbar-dark-color};
|
||||||
|
--ds-admin-sidebar-link-hover-color: #{$navbar-dark-hover-color};
|
||||||
|
|
||||||
--ds-dark-scrollbar-bg: #{$admin-sidebar-active-bg};
|
--ds-dark-scrollbar-bg: #{$admin-sidebar-active-bg};
|
||||||
--ds-dark-scrollbar-alt-bg: #{lighten($admin-sidebar-active-bg, 2%)};
|
--ds-dark-scrollbar-alt-bg: #{lighten($admin-sidebar-active-bg, 2%)};
|
||||||
--ds-dark-scrollbar-fg: #47495d;
|
--ds-dark-scrollbar-fg: #47495d;
|
||||||
|
--ds-dark-scrollbar-width: 8px;
|
||||||
|
|
||||||
--ds-submission-sections-margin-bottom: .5rem;
|
--ds-submission-sections-margin-bottom: .5rem;
|
||||||
|
|
||||||
@@ -69,9 +80,6 @@
|
|||||||
|
|
||||||
--ds-fa-fixed-width: #{$fa-fixed-width};
|
--ds-fa-fixed-width: #{$fa-fixed-width};
|
||||||
--ds-icon-padding: #{$icon-padding};
|
--ds-icon-padding: #{$icon-padding};
|
||||||
--ds-collapsed-sidebar-width: #{$collapsed-sidebar-width};
|
|
||||||
--ds-sidebar-items-width: #{$sidebar-items-width};
|
|
||||||
--ds-total-sidebar-width: #{$total-sidebar-width};
|
|
||||||
|
|
||||||
--ds-top-footer-bg: #{$gray-200};
|
--ds-top-footer-bg: #{$gray-200};
|
||||||
--ds-footer-bg: #{theme-color('primary')};
|
--ds-footer-bg: #{theme-color('primary')};
|
||||||
|
@@ -33,11 +33,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ds-admin-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
z-index: var(--ds-sidebar-z-index);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-top {
|
.sticky-top {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
@mixin dark-scrollbar() {
|
@mixin dark-scrollbar() {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: var(--ds-dark-scrollbar-width);
|
||||||
height: 3px;
|
height: 3px;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-button {
|
&::-webkit-scrollbar-button {
|
||||||
|
@@ -1,3 +1,9 @@
|
|||||||
<div [class.open]="!(isNavBarCollapsed | async)" id="header-navbar-wrapper">
|
<div id="header-navbar-wrapper"><!-- [class.open]="!(isNavBarCollapsed$ | async)" -->
|
||||||
<ds-themed-header></ds-themed-header>
|
<ds-themed-header></ds-themed-header>
|
||||||
|
<!-- Collapsible navbar (mobile only) -->
|
||||||
|
<div *ngIf="(isMobile$ | async)" id="mobile-navbar-wrapper" [@slideMobileNav]="(isNavBarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||||
|
<nav class="px-3" id="collapsible-mobile-navbar" [attr.aria-hidden]="(isNavBarCollapsed$ | async)" [attr.aria-label]="'nav.main.description' | translate">
|
||||||
|
<ds-themed-navbar></ds-themed-navbar>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,91 @@
|
|||||||
:host {
|
:host {
|
||||||
// The header-navbar-wrapper should not have a z-index, otherwise it would cover the media viewer despite its higher z-index
|
--ds-header-navbar-border-bottom-style: solid var(--ds-header-navbar-border-bottom-height) var(--ds-header-navbar-border-bottom-color);
|
||||||
position: relative;
|
--ds-mobile-navbar-border-top-style: solid var(--ds-mobile-navbar-border-top-height) var(--ds-mobile-navbar-border-top-color);
|
||||||
|
--ds-collapsible-navbar-height: calc(100vh - var(--ds-header-height));
|
||||||
|
|
||||||
div#header-navbar-wrapper {
|
div#header-navbar-wrapper {
|
||||||
border-bottom: 5px var(--ds-header-navbar-border-bottom-color) solid;
|
// The header-navbar-wrapper should not have a z-index, otherwise it would cover the media viewer despite its higher z-index
|
||||||
|
position: relative; // required by the mobile collapsible navbar
|
||||||
|
border-bottom: var(--ds-header-navbar-border-bottom-style); // gets covered by mobile navbar wrapper, when open
|
||||||
|
|
||||||
|
div#mobile-navbar-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--ds-expandable-navbar-bg);
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--ds-nav-z-index);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
nav#collapsible-mobile-navbar {
|
||||||
|
// Following parameters are changed by slideMobileNav animation
|
||||||
|
min-height: var(--ds-collapsible-navbar-height);
|
||||||
|
height: auto;
|
||||||
|
border-bottom: var(--ds-header-navbar-border-bottom-style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MENU ITEMS */
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.ds-menu-item, .ds-menu-toggler-wrapper {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav#desktop-navbar ::ng-deep {
|
||||||
|
|
||||||
|
// desktop menu
|
||||||
|
.ds-menu-item-wrapper, .ds-menu-item, .ds-menu-toggler-wrapper {
|
||||||
|
// fill navbar height (required by dropdown menus) and center content
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ds-menu-item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.ds-menu-item, .ds-menu-toggler-wrapper {
|
||||||
|
color: var(--ds-navbar-link-color) !important;
|
||||||
|
&:hover, &:focus {
|
||||||
|
color: var(--ds-navbar-link-color-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// desktop submenu
|
||||||
|
.dropdown-menu {
|
||||||
|
.ds-menu-item {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
nav#collapsible-mobile-navbar {
|
||||||
|
|
||||||
|
border-top: var(--ds-mobile-navbar-border-top-style);
|
||||||
|
padding-top: var(--ds-mobile-navbar-padding-top);
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
|
||||||
|
// mobile menu
|
||||||
|
.ds-menu-item {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-menu-item, .ds-menu-toggler-wrapper {
|
||||||
|
color: var(--ds-expandable-navbar-link-color) !important;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
color: var(--ds-expandable-navbar-link-color-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mobile submenu
|
||||||
|
.dropdown-menu {
|
||||||
|
.ds-menu-item {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component';
|
import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component';
|
||||||
|
import { slideMobileNav } from '../../../../app/shared/animations/slide';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents a wrapper for the horizontal navbar and the header
|
* This component represents a wrapper for the horizontal navbar and the header
|
||||||
@@ -8,6 +9,7 @@ import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/h
|
|||||||
selector: 'ds-header-navbar-wrapper',
|
selector: 'ds-header-navbar-wrapper',
|
||||||
styleUrls: ['header-navbar-wrapper.component.scss'],
|
styleUrls: ['header-navbar-wrapper.component.scss'],
|
||||||
templateUrl: 'header-navbar-wrapper.component.html',
|
templateUrl: 'header-navbar-wrapper.component.html',
|
||||||
|
animations: [slideMobileNav],
|
||||||
})
|
})
|
||||||
export class HeaderNavbarWrapperComponent extends BaseComponent {
|
export class HeaderNavbarWrapperComponent extends BaseComponent {
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,34 @@
|
|||||||
<header class="header">
|
<header id="main-site-header">
|
||||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="container navbar navbar-expand-md px-0">
|
<div class="container h-100 d-flex flex-row flex-nowrap justify-content-between gapx-3">
|
||||||
<div class="d-flex flex-grow-1">
|
<!-- Logo and navbar wrapper -->
|
||||||
<a class="navbar-brand m-2" routerLink="/home">
|
<div id="header-left"
|
||||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
[attr.role]="(isMobile$ | async) ? 'navigation' : 'presentation'"
|
||||||
|
[attr.aria-label]="(isMobile$ | async) ? ('nav.main.description' | translate) : null"
|
||||||
|
class="h-100 flex-fill d-flex flex-row flex-nowrap justify-content-start align-items-center gapx-3">
|
||||||
|
<a class="d-block" routerLink="/home" [attr.aria-label]="'home.title' | translate">
|
||||||
|
<img id="header-logo" src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
|
||||||
</a>
|
</a>
|
||||||
|
<nav *ngIf="!(isMobile$ | async)" class="navbar navbar-expand p-0 align-items-stretch align-self-stretch" id="desktop-navbar" [attr.aria-label]="'nav.main.description' | translate">
|
||||||
|
<ds-themed-navbar></ds-themed-navbar>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-buttons d-flex flex-grow-1 ml-auto justify-content-end align-items-center gapx-1">
|
<!-- Search bar and other menus -->
|
||||||
|
<div id="header-right" class="h-100 d-flex flex-row flex-nowrap justify-content-end align-items-center gapx-1">
|
||||||
<ds-themed-search-navbar></ds-themed-search-navbar>
|
<ds-themed-search-navbar></ds-themed-search-navbar>
|
||||||
<ds-themed-lang-switch></ds-themed-lang-switch>
|
<div role="menubar" class="h-100 d-flex flex-row flex-nowrap align-items-center gapx-1">
|
||||||
<ds-context-help-toggle></ds-context-help-toggle>
|
<ds-themed-lang-switch></ds-themed-lang-switch>
|
||||||
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
<ds-context-help-toggle></ds-context-help-toggle>
|
||||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||||
<div class="pl-2">
|
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
||||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
</div>
|
||||||
aria-controls="collapsingNav"
|
|
||||||
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
<div id="mobile-navbar-toggler" class="d-block d-lg-none ml-3" *ngIf="(isMobile$ | async)">
|
||||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
<button id="navbar-toggler" class="btn" type="button" (click)="toggleNavbar()"
|
||||||
|
[attr.aria-label]="'nav.toggle' | translate" aria-controls="collapsible-mobile-navbar" [attr.aria-expanded]="!(isNavBarCollapsed$ | async)">
|
||||||
|
<span class="fas fa-bars fa-fw fa-xl" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<ds-themed-navbar></ds-themed-navbar>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@@ -1,26 +1,19 @@
|
|||||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
:host {
|
||||||
nav.navbar {
|
#main-site-header {
|
||||||
display: none;
|
height: var(--ds-header-height);
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background-color: var(--ds-header-bg);
|
background-color: var(--ds-header-bg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand img {
|
img#header-logo {
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
height: 40px;
|
||||||
height: var(--ds-header-logo-height-xs);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.navbar-toggler .navbar-toggler-icon {
|
|
||||||
background-image: none !important;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-toggler {
|
button#navbar-toggler {
|
||||||
color: var(--ds-header-icon-color);
|
color: var(--ds-header-icon-color);
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
color: var(--ds-header-icon-color-hover);
|
color: var(--ds-header-icon-color-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { HeaderComponent as BaseComponent } from '../../../../app/header/header.component';
|
import { HeaderComponent as BaseComponent } from '../../../../app/header/header.component';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the header with the logo and simple navigation
|
* Represents the header with the logo and simple navigation
|
||||||
@@ -9,5 +10,11 @@ import { HeaderComponent as BaseComponent } from '../../../../app/header/header.
|
|||||||
styleUrls: ['header.component.scss'],
|
styleUrls: ['header.component.scss'],
|
||||||
templateUrl: 'header.component.html',
|
templateUrl: 'header.component.html',
|
||||||
})
|
})
|
||||||
export class HeaderComponent extends BaseComponent {
|
export class HeaderComponent extends BaseComponent implements OnInit {
|
||||||
|
public isNavBarCollapsed$: Observable<boolean>;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.isNavBarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,9 @@
|
|||||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
<ng-container *ngIf="(isMobile$ | async) && (isAuthenticated$ | async)">
|
||||||
class="navbar navbar-expand-md navbar-light p-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
|
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
|
||||||
<div class="navbar-inner-container w-100 h-100" [class.container]="!(isXsOrSm$ | async)">
|
</ng-container>
|
||||||
<a class="navbar-brand my-2" routerLink="/home">
|
<ul class="navbar-nav h-100 align-items-md-stretch gapx-3" role="menubar">
|
||||||
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate" />
|
<ng-container *ngFor="let section of (sections | async)">
|
||||||
</a>
|
<ng-container
|
||||||
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||||
<div id="collapsingNav" class="w-100 h-100">
|
</ng-container>
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
|
</ul>
|
||||||
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
|
|
||||||
<ds-themed-user-menu [inExpandableNavbar]="true"></ds-themed-user-menu>
|
|
||||||
</li>
|
|
||||||
<li *ngFor="let section of (sections | async)">
|
|
||||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-buttons d-flex align-items-center gapx-1">
|
|
||||||
<ds-themed-search-navbar class="navbar-collapsed"></ds-themed-search-navbar>
|
|
||||||
<ds-themed-lang-switch class="navbar-collapsed"></ds-themed-lang-switch>
|
|
||||||
<ds-context-help-toggle class="navbar-collapsed"></ds-context-help-toggle>
|
|
||||||
<ds-themed-auth-nav-menu class="navbar-collapsed"></ds-themed-auth-nav-menu>
|
|
||||||
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
@@ -1,65 +1,3 @@
|
|||||||
nav.navbar {
|
:host {
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
.navbar-inner-container {
|
|
||||||
border-top: 1px var(--ds-header-navbar-border-top-color) solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav {
|
|
||||||
background-color: var(--ds-navbar-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mobile menu styling **/
|
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
|
|
||||||
.navbar {
|
|
||||||
width: 100vw;
|
|
||||||
background-color: var(--bs-white);
|
|
||||||
position: absolute;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 0;
|
|
||||||
z-index: var(--ds-nav-z-index);
|
|
||||||
&.open {
|
|
||||||
height: 100vh; //doesn't matter because wrapper is sticky
|
|
||||||
border-bottom: 5px var(--ds-header-navbar-border-bottom-color) solid; // open navbar covers header-navbar-wrapper border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
|
||||||
.reset-padding-md {
|
|
||||||
margin-left: calc(var(--bs-spacer) / -2);
|
|
||||||
margin-right: calc(var(--bs-spacer) / -2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */
|
|
||||||
.navbar-expand-md.navbar-container {
|
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
|
|
||||||
> .navbar-inner-container {
|
|
||||||
padding: 0 var(--bs-spacer);
|
|
||||||
a.navbar-brand {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.navbar-collapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.navbar-brand img {
|
|
||||||
max-height: var(--ds-header-logo-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav ::ng-deep {
|
|
||||||
a.nav-link, .btn.nav-link {
|
|
||||||
color: var(--ds-navbar-link-color);
|
|
||||||
|
|
||||||
&:hover, &:focus {
|
|
||||||
color: var(--ds-navbar-link-color-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,29 @@
|
|||||||
// Override or add CSS variables for your theme here
|
// Override or add CSS variables for your theme here
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--ds-header-logo-height: 40px;
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
--ds-header-logo-height: 40px;
|
||||||
|
--ds-header-height: 80px;
|
||||||
|
}
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
--ds-header-logo-height: 50px;
|
||||||
|
--ds-header-height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
--ds-banner-text-background: rgba(0, 0, 0, 0.45);
|
--ds-banner-text-background: rgba(0, 0, 0, 0.45);
|
||||||
--ds-banner-background-gradient-width: 300px;
|
--ds-banner-background-gradient-width: 300px;
|
||||||
|
|
||||||
--ds-home-news-link-color: #{$green};
|
--ds-home-news-link-color: #{$green};
|
||||||
--ds-home-news-link-hover-color: #{darken($green, 15%)};
|
--ds-home-news-link-hover-color: #{darken($green, 15%)};
|
||||||
|
|
||||||
--ds-header-navbar-border-bottom-color: #{$green};
|
--ds-header-navbar-border-bottom-color: #{$green};
|
||||||
|
--ds-header-navbar-border-bottom-height: 5px;
|
||||||
|
|
||||||
|
/* set the next two properties as `--ds-header-navbar-border-bottom-*`
|
||||||
|
in order to keep the bottom border of the header when navbar is expanded */
|
||||||
|
--ds-mobile-navbar-border-top-color: #{$white};
|
||||||
|
--ds-mobile-navbar-border-top-height: 0;
|
||||||
|
--ds-mobile-navbar-padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user