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

Tooltip preventOverflow option #4678

Open
Ste35 opened this issue Feb 23, 2024 · 2 comments
Open

Tooltip preventOverflow option #4678

Ste35 opened this issue Feb 23, 2024 · 2 comments

Comments

@Ste35
Copy link

Ste35 commented Feb 23, 2024

Hi,
I'd like to report a possible bug. When a tooltip is near the edge of the document (or near the edge of it's parent element with overflow:hidden style) and there isn't the auto value in placement option, the tooltip overflow the visible part of the viewport resulting in being partially "cropped". According to popper docs this can be avoided using the preventOverflow option that (if I'm not wrong) should be enabled by default, but it doesn't seem to work.

I also tried to manually add the modifiers via the popperOptions property without any success...

tp

It seems to work fine with only bootstrap:
tp1

<button
  type="button"
  class="btn btn-outline-secondary m-2"
  data-bs-toggle="tooltip"
  data-bs-placement="bottom"
  data-bs-title="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin porttitor semper sapien ac posuere. Ut mattis hendrerit massa in laoreet"
  data-bs-trigger="click"
>
  Tooltip on top
</button>
<script>const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))</script>

Is there a way to keep the placement (e.g. bottom) and avoid the overflow?

Link to minimally-working StackBlitz that reproduces the issue:

https://stackblitz.com/edit/angular-jimum7?file=src%2Fapp%2Ftooltip-basic.html,src%2Fapp%2Ftooltip-basic.ts

Versions of Angular, ng-bootstrap and Bootstrap:

Angular: 17

ng-bootstrap: 16

@Nness
Copy link

Nness commented Mar 26, 2024

The report issue is also happen to dropdown and popover. From the code I can see preventOverflow has been added as default.

ng-bootstrap utilize popper lite, which popper lite doesn't include preventOverflow, where you have to import in to make it work. Which ng-bootstrap did.

export function getPopperOptions({ placement, baseClass }: PositioningOptions, rtl: NgbRTL): Partial<Options> {
let placementVals: Array<Placement> = Array.isArray(placement)
? placement
: (placement.split(placementSeparator) as Array<Placement>);
// No need to consider left and right here, as start and end are enough, and it is used for 'auto' placement only
const allowedPlacements = [
'top',
'bottom',
'start',
'end',
'top-start',
'top-end',
'bottom-start',
'bottom-end',
'start-top',
'start-bottom',
'end-top',
'end-bottom',
];
// replace auto placement with other placements
let hasAuto = placementVals.findIndex((val) => val === 'auto');
if (hasAuto >= 0) {
allowedPlacements.forEach(function (obj) {
if (placementVals.find((val) => val.search('^' + obj) !== -1) == null) {
placementVals.splice(hasAuto++, 1, obj as Placement);
}
});
}
const popperPlacements = placementVals.map((_placement) => {
return getPopperClassPlacement(_placement, rtl.isRTL());
});
let mainPlacement = popperPlacements.shift();
const bsModifier: Partial<Modifier<any, any>> = {
name: 'bootstrapClasses',
enabled: !!baseClass,
phase: 'write',
fn({ state }) {
const bsClassRegExp = new RegExp(baseClass + '(-[a-z]+)*', 'gi');
const popperElement: HTMLElement = state.elements.popper as HTMLElement;
const popperPlacement = state.placement;
let className = popperElement.className;
// Remove old bootstrap classes
className = className.replace(bsClassRegExp, '');
// Add current placements
className += ` ${getBootstrapBaseClassPlacement(baseClass!, popperPlacement)}`;
// Remove multiple spaces
className = className.trim().replace(spacesRegExp, ' ');
// Reassign
popperElement.className = className;
},
};
return {
placement: mainPlacement,
modifiers: [
bsModifier,
flip,
preventOverflow,
arrow,
{
enabled: true,
name: 'flip',
options: {
fallbackPlacements: popperPlacements,
},
},
{
enabled: true,
name: 'preventOverflow',
phase: 'main',
fn: function () {},
},
],
};
}

bootstrap also has preventOverflow, but they use popper instead of popper lite.

https://github.com/twbs/bootstrap/blob/24cc552343c817b0d13201639c635a778d55da09/js/src/tooltip.js#L399-L437

Personally, this has been struggling me for a while as well.

@Nness
Copy link

Nness commented Mar 29, 2024

I have found out the issue. The following code cause preventOverflow to not functional. The goal of the code was suppose to provide additional option and to be merged with popper's preventOverflow modifier. But it actually setup fn.

{
enabled: true,
name: 'preventOverflow',
phase: 'main',
fn: function () {},
},

If we compare the option bootstrap has added and ng-bootstrap's code.

// bootstrap
{
   name: 'preventOverflow',
   options: {
      boundary: this._config.boundary
   }
}

// ng-bootstrap
{
  enabled: true,
  name: 'preventOverflow',
  phase: 'main',
  fn: function () {},
}

If we either remove ng-bootstrap's modifier or swap with bootstrap's modifier. In both case works.

Following is popperOptions function I use to debug ng-bootstrap. The logic is to check how many preventOverflow has been added. If more than one, remove last one. @Ste35 you can give it a try.

  
  popperOptions(options: Partial<Options>): Partial<Options> {
    const target = options.modifiers?.filter(m => m.name === 'preventOverflow');
    if (!target || target.length <= 1) {
      return options;
    }

    const last = target.at(-1);
    if (options.modifiers && last) {
      options.modifiers = options.modifiers.filter(m => m !== last);
    }

    console.log('popper options', options);

    return options;
  }

@maxokorokov The second preventOverflow that ng-bootstrap added, I feels not necessary. Bootstrap use it to setup boundary, where ng-bootstrap setup nothing. We can potentially remove it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants