bootstrap icon indicating copy to clipboard operation
bootstrap copied to clipboard

Add smart/fixed positioning option for components with dropdowns/popups and for tooltips/popovers

Open icfantv opened this issue 9 years ago • 32 comments

In an effort to give users more flexibility to dictate where popups and tooltips/popovers we need to add the ability let them specify where and how they want the item to be displayed.

Supported Locations - Top, Right, Bottom, Left, Top Left, Top Right, Bottom Left, and Bottom Right.

Smart Positioning - when used, implies that the library will determine the best location for the item.

Fixed Positioning - will display the item at the specified location.

Hybrid Positioning - exact behavior TBD, but maybe something like letting the user specify a fixed position and if it doesn't fit, use smart positioning. Or perhaps, letting the user specify an array of positions to try before using smart positioning.

Supported Components:

  • [x] Datepicker
  • [ ] Dropdown (smart, top and bottom positioning only)
  • [x] Popover
  • [x] Tooltip
  • [ ] Typeahead (smart, top and bottom positioning only)

icfantv avatar Oct 28 '15 15:10 icfantv

#3900 is a good start and should be used for reference.

RobJacobs avatar Oct 28 '15 15:10 RobJacobs

Looks like #4293 may also be a good point of reference.

icfantv avatar Oct 28 '15 16:10 icfantv

This comment relates specifically to how TWBS positions the dropdown component, I plan to follow up in another comment on pixel positioning (tooltip).

TWBS uses 2 approaches to positioning an element relative to another element with the major differences being the markup used and the top/left style values used. This approach relies more on css to keep the linked element positioned and is used with the dropdown component. The following is the basic markup used to create a button with a dropdown:

<div class="dropdown">
  <button> Dropdown </button> 
  <ul class="dropdown-menu"> 
    <li><a href="#">Action</a></li>
  </ul>
</div> 

The relevant positioning css applied by the dropdown and dropdown-menu classes is:

<div style="position: relative;">
  <button> Dropdown </button> 
  <ul style="position: absolute; top: 100%; left: 0"> 
    <li><a href="#">Action</a></li>
  </ul>
</div> 

What this does is let the outer element’s size control where the position of the dropdown menu will appear by setting up a relative position boundary and using absolute positioning on the dropdown-menu element. The outer element’s height will grow if the button size changes thus automatically altering where the dropdown gets positioned. If the outer element has display: inline-block the width will work in the same manner. The top: 100% puts the dropdown-menu elements top edge at 100% the height of its nearest positioned ancestor (the outer dropdown div). The dropup class simply sets bottom: 100% instead and the dropdown-menu element will appear on top of the outer element instead of underneath. Extending this approach to position the dropdown-menu element in any quadrant is as simple as applying 100% to the correct css attribute.

Pros:

  • Puts the burden of positioning the linked element on css, no need to worry about scrolling, element resize, etc...

Cons:

  • Requires strict markup.
  • Requires css not included in TWBS.
  • Will not work with append-to-body.

The following lists those values as well as suggested reset values:

top-left        { top: auto; bottom: 100%; left: 0; right: auto; }
top-right       { top: auto; bottom: 100%; left: auto; right 0; }
bottom-left     { top: 100%; bottom: auto; left: 0; right: auto; }
bottom-right    { top: 100%; bottom: auto; left: auto; right: 0; }
left-top        { top: 0; bottom: auto; left: auto; right: 100%; }
left-bottom     { top: auto; bottom: 0; left: auto; right: 100%; }
right-top       { top: 0; bottom: auto; left: 100%; right: auto; }
right-bottom    { top: auto; bottom: 0; left: 100%; right: auto; }

To center, a transform can be used:

vertical-center   { top: 50%; bottom: auto; transform: translate(0, -50%); }
horizontal-center { left: 50%; right: auto; transform: translate(-50%, 0); }

And with the appropriate primary position:

top-center    { top: auto; bottom: 100%; left: 50%; right: auto; transform: translate(-50%, 0); }
bottom-center { top: 100%; bottom: auto; left: 50%; right: auto; transform: translate(-50%, 0); }
left-center   { top: 50%; bottom: auto; transform: translate(0, -50%); left: auto; right: 100%; }
right-center  { top: 50%; bottom: auto; transform: translate(0, -50%); left: 1005; right: auto; }

This Plunk demos this approach and has the styles refactored into mixins in the placements.less file.

TWBS only offers 2 different positions on the dropdown - top or bottom accomplished by using the dropdown or dropup class on the parent element.

The Dropdown directive is the only directive that uses the dropdown class with a nested dropdown-menu class in it's template. Datepicker and typeahead use the dropdown-menu class in their templates but are not nested in an element with the dropdown class. We need to be careful when using the dropdown-menu class as it adds the following styles that will affect positioning:

position: absolute;
top: 100%;
left: 0;
display: none; // none by default, but block on "open" of the menu
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0; // override default ul

RobJacobs avatar Oct 28 '15 20:10 RobJacobs

This comment relates to how TWBS positions the tooltip and popover component. TWBS accepts the following placement options;

  • top
  • bottom
  • left
  • right

with an 'auto' flag, (eg: 'auto top') to place the tooltip/popover element where it will fit.

This plunk demonstrates the TWBS implementation. This plunk demonstrates a proposal for adding this feature to the angular-ui-bootstrap library for a side by side comparison. A description of the changes:

In TWBS, to calculate tooltip element position, first the host element's (element that the tooltip is applied on) position is found using the following:

Tooltip.prototype.getPosition = function ($element){ 
  $element   = $element || this.$element;

  var el     = $element[0];
  var isBody = el.tagName == 'BODY';

  var elRect    = el.getBoundingClientRect();
  var elOffset  = isBody ? { top: 0, left: 0 } : $element.offset()
  var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
  var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null

  return $.extend({}, elRect, scroll, outerDims, elOffset)
}

So it starts with the host element boundingClientRectangle (BCR) which is the following object:

{
  top: number,
  left: number,
  bottom: number,
  right: number,
  width: number,
  height: number
}

In the return $.extend, the top and left values are overwritten by the elOffset variable (jQuery.offset()), then the width and height values are overwritten by the outerDims variable if the host element is the body element. This logic is already well accounted for in the position service using the offset and position functions. Which the positionElements function uses:

hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);

Props to @pkozlowski-opensource for writing the original positioning service code which has stood the test of time and is still solid.

Now that the host element position has been calculated, the available space around the element needs to be calculated to figure out where the tooltip element can fit. I made the following assumptions:

  • The viewport (host element container) will be the closest scrollable ancestor.
  • The viewport padding will be accounted for.
  • The viewport scrollbar width will be accounted for.
  • The tooltip element margins will be accounted for.
  • The tooltip element placement will only be moved if it fits.

The following is what TWBS uses to get the viewport dimensions:

Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
  var delta = { top: 0, left: 0 }
  if (!this.$viewport) return delta

  var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
  var viewportDimensions = this.getPosition(this.$viewport)

  if (/right|left/.test(placement)) {
    var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
    var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
    if (topEdgeOffset < viewportDimensions.top) { // top overflow
      delta.top = viewportDimensions.top - topEdgeOffset
    } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height { // bottom overflow
      delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
    }
  } else {
    var leftEdgeOffset  = pos.left - viewportPadding
    var rightEdgeOffset = pos.left + viewportPadding + actualWidth
    if (leftEdgeOffset < viewportDimensions.left) { // left overflow
      delta.left = viewportDimensions.left - leftEdgeOffset
    } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
      delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
    }
  }

return delta }

To help with this I created 2 functions in the position service - viewportOffset and scrollParent. The viewportOffset function calls the scrollParent function to get the host elements scrollable parent, then calculates the offsets from the viewport edge with the values representing the amount of space. The function returns the following object:

{
  top: number,
  bottom: number,
  left: number,
  right: number
}

Then in the positionElements function, it's just a matter of adding the margin values to the tooltip width/height and comparing that to the viewportOffset values.

There are some adjustments that need to be made to the tooltip and popover arrows which I added a new positionArrow function to the position service to handle. The positionElements will accept the following placements:

  • top
  • top-left
  • top-right
  • bottom
  • bottom-left
  • bottom-right
  • left
  • left-top
  • left-bottom
  • right
  • right-top
  • right-bottom

and if preceded by a space separated 'auto' will reposition the tooltip element if it does not fit in the desired position. The object returned will be:

{
  top: number,
  left: number,
  placement: string
}

Where placement is the value where the tooltip element will fit without the 'auto' key word. I will follow up with a PR for this shortly when I have finished a few tests.

RobJacobs avatar Nov 12 '15 19:11 RobJacobs

@RobJacobs, did you mean to close this? Were all the components w/ checkboxes addressed? Thanks.

icfantv avatar Nov 23 '15 21:11 icfantv

Any update on this?

Alexintosh avatar Jan 13 '16 12:01 Alexintosh

@Alexintosh, positioning was always targeted for the 1.2 release - @RobJacobs generously and judiciously worked on this for several weeks to get it in for the 1.0 release. We ask that you please be patient. It is coming. Please remember that we are all volunteers doing this in our spare time.

icfantv avatar Jan 13 '16 17:01 icfantv

@icfantv, I'm now looking for this feature too. Is there anything we can do to help out? I see some boxes check off up at the top, are all the unchecked ones un started?

Is the work for this happening on a specific branch? I can look into it for the datepicker, especially if there is already code done for other directives I can peak at.

deeg avatar Jan 15 '16 18:01 deeg

@deeg, you've been pretty active and have some quality commits. we are more than happy to have you work on stuff. ping me on the angular slack or gitter and i can walk you through setting up your own fork and having it linked with our main repo.

icfantv avatar Jan 15 '16 19:01 icfantv

@icfantv, that is already done, just wanted to make sure the other directives hadn't been started by someone else yet. I will look into datepicker in the next few days, possibly next week.

deeg avatar Jan 15 '16 19:01 deeg

Oh, ok. you'll need to check with @RobJacobs as he did the initial work for tooltips and popovers. I strongly suspect that the rest of this has not yet been started.

Oh, and by the way, you rock.

For typeahead and dropdown - I would only support smart, top, and bottom positioning. For the datepicker, probably the same, but I'd like to hear an argument for supporting E, W, or corners before ruling it out. Personally, I don't think it makes sense, but I'm just one man.

You can use what @RobJacobs did for tooltips/popovers to get you started - most of the work may already be done.

icfantv avatar Jan 15 '16 19:01 icfantv

Thanks for the info @icfantv.

I agree with you that E, W, and corners does not make sense for typeahead or datepicker.

deeg avatar Jan 15 '16 19:01 deeg

Hey just wanted to make sure that I could align typeahead right because I'm using it for an Arabic list

anasqadrei avatar Jan 16 '16 01:01 anasqadrei

@anasqadrei, please do not hijack threads by changing the subject. Questions like yours are best served by the community by asking on StackOverflow. That said, I believe it's the browser's responsibility to take the font you're using and render it correctly.

icfantv avatar Jan 16 '16 04:01 icfantv

@icfantv, I've created issue #2753 which was closed in favour of this issue and is referenced here.

anasqadrei avatar Jan 16 '16 22:01 anasqadrei

@anasqadrei, and have you tied an RTL font yet with the latest code base?

icfantv avatar Jan 17 '16 15:01 icfantv

@icfantv, yes. Here is a plunker with the latest codebase(ui-bootstrap 1.0.3, angular 1.4.8, bootstrap 3.3.6) http://plnkr.co/fs79igc1fJWTqY4xrWTt

Copy the RTL text وا and paste it in the textbox. You will see the text in the textbox is aligned to the right but the dropdown is aligned to the left. They both should be aligned right.

anasqadrei avatar Jan 18 '16 00:01 anasqadrei

@anasqadrei, cool, thanks for the plunker. Please create a new issue for this so we can track it. Thanks.

icfantv avatar Jan 18 '16 04:01 icfantv

@anasqadrei. so, shocker...i don't have an arabic keyboard so if you could maybe just paste a translation of the characters so we can try to reproduce that would be helpful. I suppose I could also try with Hebrew as that should suffer from the same problem. Thanks.

icfantv avatar Jan 18 '16 04:01 icfantv

@icfantv, I've updated the plunker so you could type in English characters. Type "o" or "on" for example. Hebrew would also work. I've aligned everything in the html body to RTL anyway.

Issue #2753 was already created with similar plunkers. It was closed in favour of this issue (#4762)

anasqadrei avatar Jan 18 '16 06:01 anasqadrei

@anasqadrei, ok, I see it now. you're just wanting the dropdown typeahead box to be right justified? Please do open a new issue for this. I think we may be able to fix this without waiting for a new position service. Thanks.

icfantv avatar Jan 18 '16 06:01 icfantv

Just add .dropdown-menu-right http://getbootstrap.com/components/#dropdowns-alignment

ie.

      <ul uib-dropdown-menu class="dropdown-menu-right" aria-labelledby="Select Your Language">
          <li ng-repeat="lang in languages">
              <a ng-click="changeLanguage(lang.key)" href="#">{{ lang.text }}</a></li>
          </li>
      </ul>

gercheq avatar Apr 02 '16 18:04 gercheq

i would suggest that for all related rtl stuff you should take a look at this commit https://github.com/MohammadYounes/RTL-bootstrap/commit/3be91ac69ddd92a47e1e53bcebe2b80b3d04b30e in particular to the code used by @MohammadYounes in order to easily handle the positioning of tooltips.

The solution there implemented makes use of the standard https://www.w3.org/International/questions/qa-html-dir

https://mohammadyounes.github.io/RTL-bootstrap/inline/

evilaliv3 avatar Apr 03 '16 10:04 evilaliv3

is someone aware of any workaround for this?

@wesleycho ?

evilaliv3 avatar Aug 31 '16 20:08 evilaliv3

Why is this issue still open? It appears that "auto" handles the smart positioning, by swapping left/right and top/bottom when there's not enough room. What is left to implement for position?

byronigoe avatar Feb 11 '17 16:02 byronigoe

The auto positioning is great however similar to the initial Hybrid mode, being able to specify a preferred position (as it seems to default to top?) would be great.

Js41637 avatar Mar 01 '17 08:03 Js41637

@Js41637 You can specify a preferred position, e.g. "auto bottom-right". Again, I believe everything in this request has been implemented.

byronigoe avatar Mar 01 '17 13:03 byronigoe

I tried putting auto bottom and it only added it to the bottom?

Js41637 avatar Mar 01 '17 22:03 Js41637

@Js41637 I created a Plunker to test "auto bottom", and when there isn't space on the bottom, it moves the popup to the top. Add more BR tags to line 27 if your monitor is a higher resolution: https://plnkr.co/edit/oJykYhAHaTBJF6GQaMo3?p=preview

byronigoe avatar Mar 06 '17 20:03 byronigoe

As I can see typeahead now supports smart positioning. Can anyone tell me how I can use it? I'd like it to open up automatically when there's no space at the bottom.

xReeQz avatar May 22 '17 10:05 xReeQz