Skip to content

Commit 75f7105

Browse files
svetoldo4444kavalorkin
authored andcommitted
fix(dropdown): fix dropdown inside click (#4609)
fixes #1933
1 parent 0e806e1 commit 75f7105

10 files changed

+323
-297
lines changed

demo/src/app/components/+dropdown/demos/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DemoDropdownAutoCloseComponent } from './autoclose/autoclose';
1717
import { DemoDropdownCustomHtmlComponent } from './custom-html/custom-html';
1818
import { DemoAccessibilityComponent } from './accessibility/accessibility';
1919
import { DemoDropdownByIsOpenPropComponent } from './trigger-by-isopen-property/trigger-by-isopen-property';
20+
import { DemoDropdownInsideClickComponent } from './inside-click/inside-click';
2021

2122
export const DEMO_COMPONENTS = [
2223
DemoDropdownBasicComponent,
@@ -37,5 +38,6 @@ export const DEMO_COMPONENTS = [
3738
DemoDropdownStateChangeEventComponent,
3839
DemoDropdownAutoCloseComponent,
3940
DemoDropdownCustomHtmlComponent,
40-
DemoAccessibilityComponent
41+
DemoAccessibilityComponent,
42+
DemoDropdownInsideClickComponent
4143
];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="btn-group" dropdown [insideClick]="true">
2+
<button dropdownToggle type="button" class="btn btn-primary dropdown-toggle">
3+
Button dropdown <span class="caret"></span>
4+
</button>
5+
<ul *dropdownMenu class="dropdown-menu" role="menu">
6+
<li role="menuitem"><a class="dropdown-item" href="#">Action</a></li>
7+
<li role="menuitem"><a class="dropdown-item" href="#">Another action</a></li>
8+
<li role="menuitem"><a class="dropdown-item" href="#">Something else here</a></li>
9+
<li class="divider dropdown-divider"></li>
10+
<li role="menuitem"><a class="dropdown-item" href="#">Separated link</a>
11+
</li>
12+
</ul>
13+
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Component } from '@angular/core';
2+
3+
@Component({
4+
selector: 'demo-dropdown-inside-click',
5+
templateUrl: './inside-click.html'
6+
})
7+
export class DemoDropdownInsideClickComponent {}

demo/src/app/components/+dropdown/dropdown-section.list.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DemoDropdownStateChangeEventComponent } from './demos/state-change-even
1717
import { DemoDropdownAutoCloseComponent } from './demos/autoclose/autoclose';
1818
import { DemoDropdownCustomHtmlComponent } from './demos/custom-html/custom-html';
1919
import { DemoAccessibilityComponent } from './demos/accessibility/accessibility';
20+
import { DemoDropdownInsideClickComponent } from './demos/inside-click/inside-click';
2021

2122
import { ContentSection } from '../../docs/models/content-section.model';
2223
import { DemoTopSectionComponent } from '../../docs/demo-section-components/demo-top-section/index';
@@ -120,6 +121,15 @@ export const demoComponentContent: ContentSection[] = [
120121
to right align the dropdown menu.</p>`,
121122
outlet: DemoDropdownAlignmentComponent
122123
},
124+
{
125+
title: 'Inside click',
126+
anchor: 'inside-click',
127+
component: require('!!raw-loader?lang=typescript!./demos/inside-click/inside-click.ts'),
128+
html: require('!!raw-loader?lang=markup!./demos/inside-click/inside-click.html'),
129+
description: `<p>By default, a dropdown menu closes on document click, even if you clicked on an element inside the dropdown.
130+
Use <code>[insideClick]="true"</code> to allow click inside the dropdown</p>`,
131+
outlet: DemoDropdownInsideClickComponent
132+
},
123133
{
124134
title: 'Nested dropdowns (experimental)',
125135
anchor: 'nested-dropdowns',

src/dropdown/bs-dropdown-container.component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ export class BsDropdownContainerComponent implements OnDestroy {
3636
private _state: BsDropdownState,
3737
private cd: ChangeDetectorRef,
3838
private _renderer: Renderer2,
39-
_element: ElementRef
39+
private _element: ElementRef
4040
) {
4141
this._subscription = _state.isOpenChange.subscribe((value: boolean) => {
4242
this.isOpen = value;
43-
const dropdown = _element.nativeElement.querySelector('.dropdown-menu');
43+
const dropdown = this._element.nativeElement.querySelector('.dropdown-menu');
4444
if (dropdown && !isBs3()) {
4545
this._renderer.addClass(dropdown, 'show');
4646
if (dropdown.classList.contains('dropdown-menu-right')) {
@@ -61,6 +61,11 @@ export class BsDropdownContainerComponent implements OnDestroy {
6161
});
6262
}
6363

64+
/** @internal */
65+
_contains(el: Element): boolean {
66+
return this._element.nativeElement.contains(el);
67+
}
68+
6469
ngOnDestroy(): void {
6570
this._subscription.unsubscribe();
6671
}

src/dropdown/bs-dropdown-toggle.directive.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { Subscription } from 'rxjs';
99

1010
import { BsDropdownState } from './bs-dropdown.state';
11+
import { BsDropdownDirective } from './bs-dropdown.directive';
1112

1213
@Directive({
1314
selector: '[bsDropdownToggle],[dropdownToggle]',
@@ -24,7 +25,7 @@ export class BsDropdownToggleDirective implements OnDestroy {
2425

2526
private _subscriptions: Subscription[] = [];
2627

27-
constructor(private _state: BsDropdownState, private _element: ElementRef) {
28+
constructor(private _state: BsDropdownState, private _element: ElementRef, private dropdown: BsDropdownDirective) {
2829
// sync is open value with state
2930
this._subscriptions.push(
3031
this._state.isOpenChange.subscribe(
@@ -52,7 +53,8 @@ export class BsDropdownToggleDirective implements OnDestroy {
5253
if (
5354
this._state.autoClose &&
5455
event.button !== 2 &&
55-
!this._element.nativeElement.contains(event.target)
56+
!this._element.nativeElement.contains(event.target) &&
57+
!(this.dropdown.insideClick && this.dropdown._contains(event))
5658
) {
5759
this._state.toggleClick.emit(false);
5860
}

src/dropdown/bs-dropdown.directive.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export class BsDropdownDirective implements OnInit, OnDestroy {
6565
return this._state.autoClose;
6666
}
6767

68+
/**
69+
* This attribute indicates that the dropdown shouldn't close on inside click when autoClose is set to true
70+
*/
71+
@Input() insideClick: boolean;
72+
6873
/**
6974
* Disables dropdown toggle and hides dropdown menu if opened
7075
*/
@@ -120,17 +125,17 @@ export class BsDropdownDirective implements OnInit, OnDestroy {
120125
return !isBs3();
121126
}
122127

123-
// todo: move to component loader
124-
private _isInlineOpen = false;
128+
private _dropdown: ComponentLoader<BsDropdownContainerComponent>;
125129

126130
private get _showInline(): boolean {
127131
return !this.container;
128132
}
129133

130-
private _inlinedMenu: EmbeddedViewRef<BsDropdownMenuDirective>;
134+
// todo: move to component loader
135+
private _isInlineOpen = false;
131136

137+
private _inlinedMenu: EmbeddedViewRef<BsDropdownMenuDirective>;
132138
private _isDisabled: boolean;
133-
private _dropdown: ComponentLoader<BsDropdownContainerComponent>;
134139
private _subscriptions: Subscription[] = [];
135140
private _isInited = false;
136141

@@ -280,6 +285,12 @@ export class BsDropdownDirective implements OnInit, OnDestroy {
280285
return this.show();
281286
}
282287

288+
/** @internal */
289+
_contains(event: any): boolean {
290+
return this._elementRef.nativeElement.contains(event.target) ||
291+
(this._dropdown.instance && this._dropdown.instance._contains(event.target));
292+
}
293+
283294
ngOnDestroy(): void {
284295
// clean up subscriptions and destroy dropdown
285296
for (const sub of this._subscriptions) {

src/spec/accordion.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe('Component: Accordion', () => {
126126

127127
it('should have the appropriate heading', () => {
128128
const titles = Array.from(
129-
element.querySelectorAll('.panel-heading .accordion-toggle div')
129+
element.querySelectorAll('.panel-heading .accordion-toggle button')
130130
);
131131
titles.forEach((title: HTMLElement, idx: number) => {
132132
const expectedTitle = `Panel ${idx + 1}`;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { BsDropdownContainerComponent, BsDropdownModule, BsDropdownState } from '../dropdown';
4+
import { Subject } from 'rxjs';
5+
import { window } from '../utils';
6+
7+
describe('BsDropdownContainerComponent tests', () => {
8+
let fixture: ComponentFixture<BsDropdownContainerComponent>;
9+
let component: BsDropdownContainerComponent;
10+
/* tslint:disable-next-line:no-inferred-empty-object-type */
11+
const stateSubject = new Subject();
12+
let fakeService;
13+
14+
beforeEach(() => {
15+
fakeService = {
16+
isOpenChange: stateSubject.asObservable()
17+
};
18+
TestBed.configureTestingModule({
19+
imports: [BsDropdownModule.forRoot()],
20+
providers: [{ provide: BsDropdownState, useValue: fakeService }]
21+
});
22+
});
23+
24+
beforeEach(() => {
25+
fixture = TestBed.createComponent(BsDropdownContainerComponent);
26+
component = fixture.componentInstance;
27+
fixture.detectChanges();
28+
});
29+
30+
it('should not be null', () => {
31+
expect(component).not.toBeNull();
32+
});
33+
34+
it('should be call isOpenChange method', () => {
35+
const tempVal = window.__theme;
36+
window.__theme = 'bs4';
37+
const spy = spyOn((component as any).cd, 'detectChanges');
38+
39+
stateSubject.next(true);
40+
fixture.detectChanges();
41+
42+
expect(spy).toHaveBeenCalled();
43+
window.__theme = tempVal;
44+
});
45+
});

0 commit comments

Comments
 (0)