Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dropdown append to body #1012

Closed
marcalj opened this issue Nov 7, 2016 · 67 comments · Fixed by #2823
Closed

Dropdown append to body #1012

marcalj opened this issue Nov 7, 2016 · 67 comments · Fixed by #2823

Comments

@marcalj
Copy link

marcalj commented Nov 7, 2016

Bug description:

Support for dropdown append-to-body.

Use case

In a list inside an overflowed element, the dropdown is shown "inside" the main element, and you cannot seen it completely.

image

Working with ng1 version:
image

Thanks!

@marcalj
Copy link
Author

marcalj commented Nov 7, 2016

Coming from this issue: #584 (comment)

@scttcper

@PEsteves8
Copy link

I also need something like this. Any progress on it?

@bharding777
Copy link

would really love this feature - constantly getting caught with dropdowns under other elements....

@phillip-haydon
Copy link

Really needing this feature :(

@codeMonkeysBe
Copy link

I need this feature as well. Can I help with the implementation of this?

@clarkj
Copy link

clarkj commented May 1, 2017

My most common use case for dropdown-append-to-body is for building dropdown links in navbars. Does anyone know a workaround for that?

@ryanohearn
Copy link

I really need this feature. Any word on when this will be addressed?

@rkhramiankou
Copy link

+1

1 similar comment
@nponna
Copy link

nponna commented May 16, 2017

+1

@alexcouret
Copy link

Does anyone have a workaround for that?

@kotmatpockuh
Copy link

+1

2 similar comments
@RonNewcomb
Copy link

+1

@alesce
Copy link

alesce commented Jul 31, 2017

+1

@pkozlowski-opensource pkozlowski-opensource added this to the 1.0.0-alpha.31 milestone Aug 2, 2017
pkozlowski-opensource added a commit that referenced this issue Aug 22, 2017
Fixes #1747
Part of #1171
Part of #1012

BREAKING CHANGE:

The `up` input is no longer supported by you can use more flexible
`placement` setting now.

Before:

```html
<div ngbDropdown [up]="true">
```

After:

```html
<div ngbDropdown placement="top-right">
```

Closes #1752
@iwebd
Copy link

iwebd commented Aug 29, 2017

+1

@nsmithdev
Copy link

+1

3 similar comments
@joedjc
Copy link

joedjc commented Oct 3, 2017

+1

@bennyadam
Copy link

+1

@maarten-vandevelde
Copy link

+1

@Asharma86
Copy link

Asharma86 commented Oct 17, 2017

This is a very important feature for dropdown component. Stacking context is big concern and a problem very hard to solve when working with complex data-grids and such. I loved it how I was able to use the popover component with such ease. If you could use the same utility here it will be awesome!

@pkozlowski-opensource +1

@jingzhang
Copy link

@adamk33n3r Sorry for the late reply. I was away on vacation. For me the import works just fine. I don't remember doing anything special.

Does you installation contain this file?
/node_modules/@ng-bootstrap/ng-bootstrap/dropdown/dropdown.d.ts

If not, have you tried reinstalling ng-bootstrap?

@adamk33n3r
Copy link

@jingzhang Yes I have that file. It "imports" correctly. It's just that it's not compiling it to es5. It gets put into my bundle as es6 with imports and in Chrome it errors.

@jingzhang
Copy link

@adamk33n3r without seeing the actual code it's hard to guess the issue. But I think my project is also complied into es6 (not sure if the correct way to call it. I'm not really an expert in typescript).

BTW, my project was created with angular CLI, if yours wasn't, maybe it has something to do with the error.

Sorry I couldn't be of much help.

@lostip
Copy link

lostip commented Aug 8, 2018

@adamk33n3r RE:

"The content child of NgbDropdownMenu is undefined"

check if you have properly added NgbDropdownModule to your module imports

@certainlyakey
Copy link

I am having trouble with the @jingzhang solution on ng-bootstrap 3.0.0. I've copied the code as a new directive, imported this directive in the app.module.ts and put it there in the declarations section. l also have tried to fix some path issues arising on build in terminal. However the browser (Chrome) displays a No provider for NgbDropdown error. What could be the problem?

import {
  Directive, ContentChild, AfterContentInit, ElementRef, OnDestroy,
  Inject, forwardRef
} from '@angular/core';
import { NgbDropdownMenu, NgbDropdown } from '@ng-bootstrap/ng-bootstrap/esm5/dropdown/dropdown';
import { positionElements } from '@ng-bootstrap/ng-bootstrap/esm5/util/positioning';
import { Subscription } from 'rxjs/Subscription';

@Directive({
  selector: '[ngbDropdown][ngbDropdownReposition]',
})
export class HcNgbDropdownRepositionDirective implements AfterContentInit, OnDestroy {

  @ContentChild(NgbDropdownMenu) private menu: NgbDropdownMenu;
  @ContentChild(NgbDropdownMenu, {read: ElementRef}) private menuRef: ElementRef;


  private oldParent: HTMLElement | null;
  private menuWrapper: HTMLElement;
  private readonly onChangeSubscription: Subscription;

  constructor(
    @Inject(forwardRef(() => NgbDropdown)) private dropdown: NgbDropdown,
    private elementRef: ElementRef
  ) {
    this.onChangeSubscription = this.dropdown.openChange.subscribe((open: boolean) => {
      if (!open) {
        setTimeout(() => this.removeMenuFromBody(), 0);
      }
    });
  }

  ngAfterContentInit() {
    this.oldParent = (<HTMLElement> this.menuRef.nativeElement).parentElement;
    this.createWrapper();
    this.menu.position = (triggerEl: HTMLElement, placement: string) => {
      this.setWrapperWidth();

      if (!this.isInBody()) {
        this.appendMenuToBody();
      }

      positionElements(triggerEl, this.menuWrapper, placement, true);
      this.menu.applyPlacement(positionElements(triggerEl, this.menuRef.nativeElement, placement));
    };
  }

  ngOnDestroy() {
    this.removeMenuFromBody();
    if (this.onChangeSubscription) {
      this.onChangeSubscription.unsubscribe();
    }
  }

  private isInBody() {
    return this.menuWrapper.parentNode === document.body;
  }

  private removeMenuFromBody() {
    if (this.isInBody()) {
      if (this.oldParent) {
        this.oldParent.appendChild(this.menuWrapper);
      }
    }
  }

  private appendMenuToBody() {
    window.document.body.appendChild(this.menuWrapper);
  }

  private createWrapper() {
    this.menuWrapper = document.createElement('div');
    this.menuWrapper.style.position = 'absolute';
    this.menuWrapper.style.zIndex = '1030';

    this.menuWrapper.addEventListener('keyup', (event: KeyboardEvent) => {
      if (event.keyCode === 27) {
        this.dropdown.closeFromOutsideEsc();
      }
    });
    this.menuWrapper.appendChild(this.menuRef.nativeElement);
  }

  private setWrapperWidth() {
    const parentEl = <HTMLElement> this.elementRef.nativeElement;
    this.menuWrapper.style.width = parentEl.clientWidth + 'px';
  }
}

@jingzhang
Copy link

@certainlyakey are you sure you upgraded to 3.0.0? Because dropdown.closeFromOutsideEsc() method no longer exists in this version (you can just remove the event listener on menuWrapper)

@certainlyakey
Copy link

Yes, npm list @ng-bootstrap/ng-bootstrap confirms that the version installed is 3.0.0. I have removed the listener just in case, it didn't help much.

@jingzhang
Copy link

OK, that's strange. I asked because with closeFromOutsideEsc in the code it shouldn't even compile. Anyway, I suspect it's due to the way you worked around the path issues. I see this at the top of your code:

import { NgbDropdownMenu, NgbDropdown } from '@ng-bootstrap/ng-bootstrap/esm5/dropdown/dropdown';
import { positionElements } from '@ng-bootstrap/ng-bootstrap/esm5/util/positioning';

I had similar import issues after upgrading to 3.0.0. What I did was:

import { NgbDropdown, ɵl as NgbDropdownMenu } from '@ng-bootstrap/ng-bootstrap';
import { positionElements } from '../ngb/positioning';

Notice that you need to make a local copy of the positioning service, because it's no longer exported in 3.0.0. You can grab it from here: https://github.com/ng-bootstrap/ng-bootstrap/blob/master/src/util/positioning.ts I saved it at /src/app/ngb/positioning.ts, hence the path above

@certainlyakey
Copy link

certainlyakey commented Aug 24, 2018

Thanks @jingzhang! It works now. Apparently there were some problems with import paths indeed which strangely weren't reported by Typescript on build.

I've made an only change of modifying a single line in positionElements method in the positioning service that comes from ng-bootstrap (see the changed file). There was a problem with dropdowns incorrectly offset by the height of the host element. The top position for the target element inside its wrapper should not be the same as for the wrapper itself (since the target is positioned relative to the wrapper, and the wrapper is positioned relative to body).

Thanks again for the help!

@jingzhang
Copy link

NP @certainlyakey And thanks for sharing the fix 👍 I didn't notice that

@Nness
Copy link

Nness commented Aug 28, 2018

In release 3.1, NgbDropdownMenu is export as ep.

// ng-bootstrap.d.ts
export { NgbDropdownAnchor as ɵq, NgbDropdownMenu as ɵp, NgbDropdownToggle as ɵr } from './dropdown/dropdown';

Code should change to
import { NgbDropdown, ɵp as NgbDropdownMenu } from '@ng-bootstrap/ng-bootstrap';

@bluecaret
Copy link

This workaround breaks tab navigation and accessibility. We still very much need an official solution. Can't believe this is still not a thing.

@olgabelous
Copy link

We use bootstrap 3.2 and use custom directive. Maybe it'll help someone.

import { Directive, forwardRef, Inject, OnDestroy } from '@angular/core';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { Subscription } from 'rxjs';
import { positionElements } from '../positioning';

@Directive({
    selector: '[ngbDropdown][appDropdownAppendToBody]'
})
export class DropdownAppendToBodyDirective implements OnDestroy {

    private onChangeSubscription: Subscription;

    constructor(@Inject(forwardRef(() => NgbDropdown)) private dropdown: NgbDropdown) {

        this.onChangeSubscription = this.dropdown.openChange.subscribe((open: boolean) => {
            this.dropdown['_menu'].position = (triggerEl: HTMLElement, placement: string) => {
                if (!this.isInBody()) {
                    this.appendMenuToBody();
                }
                positionElements(triggerEl, this.dropdown['_menu']['_elementRef'].nativeElement, placement, true);
            };

            if (open) {
                if (!this.isInBody()) {
                    this.appendMenuToBody();
                }
            } else {
                setTimeout(() => this.removeMenuFromBody());
            }
        });
    }

    ngOnDestroy() {
        this.removeMenuFromBody();
        if (this.onChangeSubscription) {
            this.onChangeSubscription.unsubscribe();
        }
    }

    private isInBody() {
        return this.dropdown['_menu']['_elementRef'].nativeElement.parentNode === document.body;
    }

    private removeMenuFromBody() {
        if (this.isInBody()) {
            window.document.body.removeChild(this.dropdown['_menu']['_elementRef'].nativeElement);
        }
    }

    private appendMenuToBody() {
        window.document.body.appendChild(this.dropdown['_menu']['_elementRef'].nativeElement);
    }
}
<div ngbDropdown appDropdownAppendToBody  [placement]="['bottom-right', 'top-right']">
    <button ngbDropdownToggle></button>
    <div ngbDropdownMenu>
        ..
    </div>
</div>

We had to move positioning.ts to out app because it wasn't compiled.

@theCrius
Copy link

theCrius commented Oct 19, 2018

Using bootstrap 4 and related ngBootstrap, still no support for this. Maybe make this an higher priority after 2 years of this issue being open?

We have come up with a custom directive to reposition the ngbDropdown menu under the body element instead.

Here is the directive code:

[...]
export class DropdownPositionDirective implements AfterContentInit, OnDestroy {
[...]

And use it like this:

<div ngbDropdown ngbDropdownReposition>
 <!-- Rest of the dropdown code -->
</div>

Hope it helps :)

This solution works in putting the div under body but make attributes of the original directive not work anymore. Specifically placement don't work.

Also the div don't follow the button anymore if you scroll the page, it stay in the place it has opened (which is a problem shared by the original NgbTooltip directive so, not really an issue with the code that the good @mcorcuera provided.

@jrmsamson
Copy link

jrmsamson commented Oct 23, 2018

We use bootstrap 3.2 and use custom directive. Maybe it'll help someone.

import { Directive, forwardRef, Inject, OnDestroy } from '@angular/core';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { Subscription } from 'rxjs';
import { positionElements } from '../positioning';

@Directive({
    selector: '[ngbDropdown][appDropdownAppendToBody]'
})
export class DropdownAppendToBodyDirective implements OnDestroy {

    private onChangeSubscription: Subscription;

    constructor(@Inject(forwardRef(() => NgbDropdown)) private dropdown: NgbDropdown) {

        this.onChangeSubscription = this.dropdown.openChange.subscribe((open: boolean) => {
            this.dropdown['_menu'].position = (triggerEl: HTMLElement, placement: string) => {
                if (!this.isInBody()) {
                    this.appendMenuToBody();
                }
                positionElements(triggerEl, this.dropdown['_menu']['_elementRef'].nativeElement, placement, true);
            };

            if (open) {
                if (!this.isInBody()) {
                    this.appendMenuToBody();
                }
            } else {
                setTimeout(() => this.removeMenuFromBody());
            }
        });
    }

    ngOnDestroy() {
        this.removeMenuFromBody();
        if (this.onChangeSubscription) {
            this.onChangeSubscription.unsubscribe();
        }
    }

    private isInBody() {
        return this.dropdown['_menu']['_elementRef'].nativeElement.parentNode === document.body;
    }

    private removeMenuFromBody() {
        if (this.isInBody()) {
            window.document.body.removeChild(this.dropdown['_menu']['_elementRef'].nativeElement);
        }
    }

    private appendMenuToBody() {
        window.document.body.appendChild(this.dropdown['_menu']['_elementRef'].nativeElement);
    }
}
<div ngbDropdown appDropdownAppendToBody  [placement]="['bottom-right', 'top-right']">
    <button ngbDropdownToggle></button>
    <div ngbDropdownMenu>
        ..
    </div>
</div>

We had to move positioning.ts to out app because it wasn't compiled.

I just copied your code and everything works fine.

Good job

Thanks :)

@lasantha57
Copy link

I guess same exists for the ngb-datepicker as well
untitled

@umdstu
Copy link

umdstu commented Nov 5, 2018

Most angular 2+ components I've seen/used have an appendToBody=true/false option that appends the dropdown to the body and fixes all of this. The old ui-bootstrap has this. Why isn't this an option?

@Maximaximum
Copy link

Quite a pity that this issue hasn't yet received an official fix :(

@fbasso
Copy link
Member

fbasso commented Nov 16, 2018

Regarding the ngb-datepicker, the attribute container="body" can be used on the input.
See https://ng-bootstrap.github.io/#/components/datepicker/api#NgbInputDatepicker

@skorunka
Copy link

Facing some issue when parent div overflow is set to auto(or similar).

@lexeek
Copy link

lexeek commented Dec 24, 2018

Any progress with this issue?

@Apee
Copy link

Apee commented Jan 14, 2019

Any progress... ?

@arajay
Copy link

arajay commented Jan 14, 2019

@EvaSky workaround is working for me. if you don't update from version 2 the positioning util is not private and you can import it. otherwise you have to make a copy in your app (which sucks)

@Maximaximum
Copy link

Very pity that this issue does not seem to be of any priority to the team :(

@mikhailbartashevich
Copy link

Agree it's very pitty, please add this input

@maxokorokov maxokorokov added this to the 4.1 milestone Feb 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.