diff --git a/commitlint.config.js b/commitlint.config.js
index 3fab66757..dfa513b48 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -1,14 +1,14 @@
module.exports = {
extends: ['@ptsecurity/commitlint-config'],
rules: {
- 'scope-enum': [
+ 'scope-enum': [
2,
'always',
[
'build',
'docs',
'chore',
- 'cdk',
+ 'cdk',
'common',
'typography',
'button',
@@ -28,7 +28,8 @@ module.exports = {
'progress-bar',
'datepicker',
'timepicker',
- 'visual'
+ 'visual',
+ 'navbar'
]
]
}
diff --git a/src/lib-dev/navbar/module.ts b/src/lib-dev/navbar/module.ts
index c09846de4..b0043f1df 100644
--- a/src/lib-dev/navbar/module.ts
+++ b/src/lib-dev/navbar/module.ts
@@ -1,10 +1,11 @@
-import { FormsModule } from '@angular/forms';
import { Component, NgModule, ViewChild, ViewEncapsulation } from '@angular/core';
+import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-import { McNavbarModule, McNavbar } from '../../lib/navbar/';
+import { McButtonModule } from '../../lib/button';
import { McIconModule } from '../../lib/icon';
+import { McNavbarModule, McNavbar, IMcNavbarDropdownItem } from '../../lib/navbar/';
@Component({
@@ -20,6 +21,23 @@ export class NavbarDemoComponent {
readonly minNavbarWidth: number = 940;
+ dropdownItems: IMcNavbarDropdownItem[] = [
+ { link: '#1', text: 'Очень длинный список для проверки ширины' },
+ { link: '#2', text: 'Общие сведения' },
+ { link: '#3', text: 'Еще один пункт' }
+ ];
+
+ buttonDropdownItems: IMcNavbarDropdownItem[] = [
+ { text: 'Пример кастомного компонента 1' },
+ { text: 'Пример кастомного компонента 2' },
+ { text: 'Пример кастомного компонента 3' }
+ ];
+
+ rightDropdownItems: IMcNavbarDropdownItem[] = [
+ { link: '#4', text: 'Пункт в правой части navbar 1' },
+ { link: '#5', text: 'Пункт в правой части navbar 2' }
+ ];
+
private _collapsedNavbarWidth: number = 1280;
get collapsedNavbarWidth(): number {
@@ -50,6 +68,7 @@ export class NavbarDemoComponent {
imports: [
BrowserModule,
McNavbarModule,
+ McButtonModule,
McIconModule,
FormsModule
],
diff --git a/src/lib-dev/navbar/template.html b/src/lib-dev/navbar/template.html
index e0ccba0d0..408eb96c6 100644
--- a/src/lib-dev/navbar/template.html
+++ b/src/lib-dev/navbar/template.html
@@ -28,13 +28,6 @@
Common example
Right Icon
-
-
- mc-dropdown
-
-
-
-
@@ -59,6 +52,33 @@ Common example
+Dropdowns exmaple
+
+
+
+
+ Dropdown items
+
+
+ Custom dropdown items
+
+
+
+
+
+
+
+
+ Right dropdown items
+
+
+
+
+
+
Collapse example
diff --git a/src/lib/navbar/README.md b/src/lib/navbar/README.md
index 5faaa7a9a..0825b4f0f 100644
--- a/src/lib/navbar/README.md
+++ b/src/lib/navbar/README.md
@@ -19,8 +19,9 @@ We strongly recommend adhere to use our child components until it is possible bu
- menu item container **mc-navbar-item**
- icon **[mc-icon]** (another PT Mosaic component, out of the scope of this)
- title **mc-navbar-title**
- - dropdown **[mc-dropdown]** (another PT Mosaic component, out of the scope of this)
- - any markup
+ - menu item dropdown container **mc-navbar-item** with property **[dropdownItems]**
+ - any markup for title dropdown container
+ - **ng-temaplate** with custom component for dropdown item
- any markup
- any markup
@@ -38,4 +39,4 @@ Disable state also could be combined with other states but it is **disabled** at
In the case that absence of space the following elements is collapsed:
- mc-navbar-item is collapsed to [mc-icon] if exists (if not then the item is not collapsed). The title attribute is added for [mc-icon] from mc-navbar-item's [collapsedTitle] input value;
-- mc-navbar-item is collapsed to [mc-icon] if exists (if not then the item is not collapsed).
+- mc-navbar-item is collapsed to [mc-icon] if exists (if not then the item is not collapsed).
\ No newline at end of file
diff --git a/src/lib/navbar/_navbar-base.scss b/src/lib/navbar/_navbar-base.scss
index 87676ed5d..9a3cba2a6 100644
--- a/src/lib/navbar/_navbar-base.scss
+++ b/src/lib/navbar/_navbar-base.scss
@@ -2,6 +2,9 @@
$mc-navbar-h-padding: 0px;
$mc-navbar-height: 48px;
$mc-navbar-item-h-padding: 16px;
+$mc-navbar-dropdown-v-padding: 4px;
+$mc-navbar-dropdown-link-v-padding: 6px;
+$mc-navbar-dropdown-link-h-padding: 16px;
$mc-navbar-brand-h-padding: 12px;
$mc-navbar-brand-margin-right: 24px;
$mc-navbar-title-icon-space: 8px;
@@ -44,6 +47,8 @@ $mc-navbar-icon-min-width: 15px;
align-items: center;
padding-left: $mc-navbar-item-h-padding;
padding-right: $mc-navbar-item-h-padding;
+ background-color: transparent;
+ border: none;
}
%mc-navbar-brand-base {
diff --git a/src/lib/navbar/_navbar-theme.scss b/src/lib/navbar/_navbar-theme.scss
index a2c754fb9..c111c077a 100644
--- a/src/lib/navbar/_navbar-theme.scss
+++ b/src/lib/navbar/_navbar-theme.scss
@@ -63,6 +63,34 @@
opacity: 0.5;
}
}
+
+ .mc-navbar-dropdown {
+ background-color: #fff;
+ border-color: mc-color($mc-grey, 300);
+ box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.2);
+
+ &-link {
+ color: mc-color($mc-grey, 700);
+ outline: none;
+
+ &:hover {
+ background-color: $black-6-opacity;
+ }
+
+ &.cdk-focused {
+ border-color: mc-color($palette, 500);
+ }
+
+ &:active,
+ &.is-active {
+ background-color: mc-color($palette, 100);
+
+ &:hover::before {
+ background-color: $black-6-opacity;
+ }
+ }
+ }
+ }
}
@mixin mc-navbar-theme($theme) {
@@ -72,7 +100,8 @@
}
@mixin mc-navbar-typography($config) {
- .mc-navbar-title {
+ .mc-navbar-title,
+ .mc-navbar-dropdown {
@include mc-typography-level-to-styles($config, body);
}
diff --git a/src/lib/navbar/navbar.component.spec.html b/src/lib/navbar/navbar.component.spec.html
index 1be9045f2..701999b19 100644
--- a/src/lib/navbar/navbar.component.spec.html
+++ b/src/lib/navbar/navbar.component.spec.html
@@ -48,6 +48,10 @@
Left icon
+
+ Dropdown item
+
+
diff --git a/src/lib/navbar/navbar.component.spec.ts b/src/lib/navbar/navbar.component.spec.ts
index e1c4ceab1..ca6af78b5 100644
--- a/src/lib/navbar/navbar.component.spec.ts
+++ b/src/lib/navbar/navbar.component.spec.ts
@@ -2,8 +2,11 @@ import { Component, ViewChild } from '@angular/core';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
-import { McNavbarModule, McNavbar } from './index';
+import { SPACE } from '@ptsecurity/cdk/keycodes';
+import { createKeyboardEvent, createFakeEvent } from '@ptsecurity/cdk/testing';
+
import { McIconModule } from './../icon/icon.module';
+import { McNavbarModule, McNavbar, IMcNavbarDropdownItem } from './index';
const FONT_RENDER_TIMEOUT_MS = 10;
@@ -105,6 +108,81 @@ describe('McNavbar', () => {
expect(testComponent.counter).toBe(0);
});
+
+ it('dropdown item by default should render list links', () => {
+ const fixture = TestBed.createComponent(TestApp);
+
+ fixture.detectChanges();
+
+ const dropdowns = fixture.debugElement.queryAll(By.css('[ng-reflect-dropdown-items]'));
+
+ dropdowns.forEach((dropdown) => {
+ const links = dropdown.queryAll(By.css('.mc-navbar-dropdown-link'));
+ expect(links.length).toBeGreaterThan(0);
+ });
+ });
+
+ it('dropdown content should open by click on navbar-item', () => {
+ const fixture = TestBed.createComponent(TestApp);
+
+ fixture.detectChanges();
+
+ const dropdown = fixture.debugElement.query(By.css('[ng-reflect-dropdown-items]'));
+ const dropdownToggler = dropdown.query(By.css('.mc-navbar-item'));
+ const dropdownContent = dropdown.query(By.css('.mc-navbar-dropdown')).nativeElement as HTMLElement;
+
+ dropdownToggler.nativeElement.click();
+
+ fixture.detectChanges();
+
+ const isOpened = !dropdownContent.classList.contains('is-collapsed');
+
+ expect(isOpened).toBeTruthy();
+ });
+
+ it('dropdown content should open by keydown SPACE on navbar-item', () => {
+ const fixture = TestBed.createComponent(TestApp);
+
+ fixture.detectChanges();
+
+ const dropdown = fixture.debugElement.query(By.css('[ng-reflect-dropdown-items]'));
+ const dropdownToggler = dropdown.query(By.css('.mc-navbar-item')).nativeElement as HTMLElement;
+ const dropdownContent = dropdown.query(By.css('.mc-navbar-dropdown')).nativeElement as HTMLElement;
+
+ const keydownEvent = createKeyboardEvent('keydown', SPACE, dropdownToggler);
+
+ dropdownToggler.dispatchEvent(keydownEvent);
+
+ fixture.detectChanges();
+
+ const isOpened = !dropdownContent.classList.contains('is-collapsed');
+
+ expect(isOpened).toBeTruthy();
+ });
+
+ it('dropdown content should close by blur event from ', () => {
+ const fixture = TestBed.createComponent(TestApp);
+
+ fixture.detectChanges();
+
+ const dropdown = fixture.debugElement.query(By.css('[ng-reflect-dropdown-items]'));
+ const dropdownToggler = dropdown.query(By.css('.mc-navbar-item'));
+ const dropdownContent = dropdown.query(By.css('.mc-navbar-dropdown')).nativeElement as HTMLElement;
+
+ const keydownEvent = createFakeEvent('blur');
+
+ dropdownToggler.nativeElement.click();
+
+ fixture.detectChanges();
+
+ dropdown.nativeElement.dispatchEvent(keydownEvent);
+
+ fixture.detectChanges();
+
+ const isClosed = dropdownContent.classList.contains('is-collapsed');
+
+ expect(isClosed).toBeTruthy();
+ });
});
@Component({
@@ -118,6 +196,12 @@ class TestApp {
counter: number = 0;
navbarContainerWidth: number = 915;
+ dropdownItems: IMcNavbarDropdownItem[] = [
+ { link: '#', text: 'Очень длинный список для проверки ширины' },
+ { link: '#', text: 'Общие сведения' },
+ { link: '#', text: 'Еще один пункт' }
+ ];
+
onItemClick() {
this.counter++;
}
diff --git a/src/lib/navbar/navbar.component.ts b/src/lib/navbar/navbar.component.ts
index 1a81a4ac8..15689b0ec 100644
--- a/src/lib/navbar/navbar.component.ts
+++ b/src/lib/navbar/navbar.component.ts
@@ -1,5 +1,4 @@
-import { fromEvent } from 'rxjs';
-import { Subscription } from 'rxjs/internal/Subscription';
+import { fromEvent, Observable, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import {
@@ -10,10 +9,16 @@ import {
Input,
OnDestroy,
OnInit,
- ViewEncapsulation
+ ViewEncapsulation,
+ ContentChild,
+ TemplateRef,
+ ChangeDetectorRef,
+ ChangeDetectionStrategy,
+ ViewChild
} from '@angular/core';
-import { FocusMonitor } from '@ptsecurity/cdk/a11y';
-
+import { FocusMonitor, FocusOrigin } from '@ptsecurity/cdk/a11y';
+import { SPACE } from '@ptsecurity/cdk/keycodes';
+import { Platform } from '@ptsecurity/cdk/platform';
import { CanDisable, mixinDisabled } from '@ptsecurity/mosaic/core';
@@ -28,6 +33,11 @@ const MC_NAVBAR_LOGO = 'mc-navbar-logo';
export type McNavbarContainerPositionType = 'left' | 'right';
+export interface IMcNavbarDropdownItem {
+ link?: string;
+ text: string;
+}
+
@Directive({
selector: MC_NAVBAR_LOGO,
host: {
@@ -61,30 +71,84 @@ export const _McNavbarMixinBase = mixinDisabled(McNavbarItemBase);
@Component({
selector: MC_NAVBAR_ITEM,
template: `
-
-
-
+
+
+
+
`,
encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
inputs: ['disabled'],
host: {
- '[attr.disabled]': 'disabled || null'
+ '[attr.disabled]': 'disabled || null',
+ '[attr.tabindex]': '-1'
}
})
-export class McNavbarItem extends _McNavbarMixinBase implements OnInit, OnDestroy, CanDisable {
+export class McNavbarItem extends _McNavbarMixinBase implements OnInit, AfterViewInit, OnDestroy, CanDisable {
@Input()
tabIndex: number = 0;
+ @Input()
+ dropdownItems: IMcNavbarDropdownItem[] = [];
+
@Input()
set collapsedTitle(value: string) {
this.elementRef.nativeElement.setAttribute('computedTitle', encodeURI(value));
}
+ @ContentChild('dropdownItemTmpl', { read: TemplateRef })
+ dropdownItemTmpl: TemplateRef;
+
+ @ViewChild('dropdownContent', { read: ElementRef })
+ dropdownContent: ElementRef;
+
+ get hasDropdownContent() {
+ return this.dropdownItems.length > 0;
+ }
+
+ isCollapsed: boolean = true;
+
+ private _subscription: Subscription = new Subscription();
+ private _focusMonitor$: Observable;
+ private _lastFocusedElement: HTMLElement;
+
+ private get _dropdownElements(): HTMLElement[] {
+ return this.dropdownContent ? this.dropdownContent.nativeElement.querySelectorAll('li > *') : [];
+ }
+
constructor(
public elementRef: ElementRef,
- private _focusMonitor: FocusMonitor
+ private _focusMonitor: FocusMonitor,
+ private _platform: Platform,
+ private _cdRef: ChangeDetectorRef
) {
super(elementRef);
}
@@ -92,11 +156,80 @@ export class McNavbarItem extends _McNavbarMixinBase implements OnInit, OnDestro
ngOnInit() {
this.denyClickIfDisabled();
- this._focusMonitor.monitor(this.elementRef.nativeElement, true);
+ this._focusMonitor$ = this._focusMonitor.monitor(this.elementRef.nativeElement, true);
+
+ if (this.hasDropdownContent) {
+ this.listenClickOutside();
+ }
+ }
+
+ ngAfterViewInit() {
+ if (!this.hasDropdownContent) {
+ return;
+ }
+
+ this.startListenFocusDropdownItems();
}
ngOnDestroy() {
+ this._subscription.unsubscribe();
this._focusMonitor.stopMonitoring(this.elementRef.nativeElement);
+ this.stopListenFocusDropdownItems();
+ }
+
+ isActiveDropdownLink(link: string): boolean {
+ if (!this._platform.isBrowser) {
+ return false;
+ }
+
+ return window.location.href.indexOf(link) >= 0;
+ }
+
+ handleClickByItem() {
+ this.toggleDropdown();
+ }
+
+ handleKeydown($event: KeyboardEvent) {
+ const isNavbarItem = ($event.target as HTMLElement).classList.contains(MC_NAVBAR_ITEM);
+
+ if (this.hasDropdownContent && $event.keyCode === SPACE && isNavbarItem) {
+ this.toggleDropdown();
+ }
+ }
+
+ handleClickByDropdownItem() {
+ this.forceCloseDropdown();
+ }
+
+ private listenClickOutside() {
+ this._subscription.add(
+ this._focusMonitor$.subscribe((origin) => {
+ if (origin === null) {
+ this.forceCloseDropdown();
+ }
+ })
+ );
+ }
+
+ private toggleDropdown() {
+ this.isCollapsed = !this.isCollapsed;
+ }
+
+ private forceCloseDropdown() {
+ this.isCollapsed = true;
+ this._cdRef.detectChanges();
+ }
+
+ private startListenFocusDropdownItems() {
+ this._dropdownElements.forEach((el) => {
+ this._focusMonitor.monitor(el, true);
+ });
+ }
+
+ private stopListenFocusDropdownItems() {
+ this._dropdownElements.forEach((el) => {
+ this._focusMonitor.stopMonitoring(el);
+ });
}
// This method is required due to angular 2 issue https://github.com/angular/angular/issues/11200
@@ -209,6 +342,7 @@ class CachedItemWidth {
@Component({
selector: MC_NAVBAR,
+ changeDetection: ChangeDetectionStrategy.OnPush,
template: `