Skip to content

Latest commit

 

History

History
97 lines (62 loc) · 14.8 KB

anchored-position.md

File metadata and controls

97 lines (62 loc) · 14.8 KB

Anchored Position

The getAnchoredPosition behavior is used to calculate the position of a "floating" element that is anchored to another DOM element. This is useful for implementing overlay UI, such as dialogs, popovers, tooltips, toasts, and dropdown-style menus.

At a high level, the getAnchoredPosition algorithm will attempt to find the most suitable position for the floating element based on the passed-in settings, its containing element, and the size and position of the anchor element. Specifically, the calculated position should try to ensure that the floating element, when positioned at the calculated coordinates, does not overflow or underflow the container's bounding box.

Settings for this behavior allow the user to customize several aspects of this calculation. See PositionSettings below for a detailed description of these settings.

Positioning algorithm

When calculating the position of the floating element, the algorithm relies on different measurements from three separate elements:

  1. The floating element's width and height
  2. The anchor element's x/y position and its width and height
  3. The floating element's clipping container (for x/y position, width and height, and border sizes)

The public API only asks for the first two elements; the floating element's container is discovered via DOM traversal.

Finding the floating element's clipping container

The returned anchored position calculation is relative to the floating element's closest positioned ancestor. To find this ancestor, we try to check parents of the floating element until we find one that has a position set to anything other than static and use that element's bounding box as the container. If we can't find such an element, we will try to use document.body.

Once we have found the appropriate relative ancestor, we attempt to find the floating element's clipping container. The clipping container is an element that: 1) has overflow set to something other than visible, and 2) is either an ancestor of the relative ancestor, or is itself the relative ancestor. Again, if we cannot locate such an element, we will use document.body as the clipping container.

Once we have the clipping container, its bounding box is used as the viewport for the position calculation (see the next section). If the clipping container ends up being document.body, we take one additional step, allowing the clipping rectangle to be at least as tall as the window. This is done because the body element doesn't take up the full window size by default, but we still want to allow the entire space to be used as the viewport for the position calculation. It may be a good idea to ensure that this clipping container element also contains the anchor element and is scrollable. This will ensure that if scrolled, the anchor and floating element will move together.

Positioning and overflow

With the positions and sizes of the above DOM elements, the algorithm calculates the (x, y) coordinate for the floating element. Then, it checks to see if, based on the floating element's size, if it would overflow the bounds of the container. If it would, it does one of two things:

A) If the overflow happens in the same direction as the anchor side (e.g. side is 'outside-bottom' and the overflowing portion of the floating element is the bottom), try to find a different side, recalculate the position, and check for overflow again. If we check all four sides and don't find one that fits, revert to the bottom side, in hopes that a scrollbar might appear. B) If the overflow happens in the same direction as the anchor alignment (e.g. align is 'start' and the overflowing portion of the floating element is the right), try to find a different alignment, recalculate the position, and check for overflow again. If we check all three alignments and don't find one that fits, revert to initial aligment, in hopes that (C) will fix it. C) Otherwise, adjust the alignment offset so that the floating element can stay inside the container's bounds.

For a more in-depth explanation of the positioning settings, see PositionSettings below.

Usage

const settings = {
  side: 'outside-right',
  align: 'center',
  alignmentOffset: 10,
  anchorOffset: -10
} as Partial<PositionSettings>
const float = document.getElementById('floatingElement')
const anchor = document.getElementById('anchorElement')
const {top, left} = getAnchoredPosition(float, anchor, settings)
float.style.top = `${top}px`
float.style.left = `${left}px`

API

The getAnchoredPosition function takes the following arguments.

Name Type Default Description
floatingElement Element This is an Element that is currently rendered on the page. getAnchoredPosition needs to be able to measure this element's width and height.
anchorElement Element This is an Element that the floating element will be "anchored" to. In other words, the calculated position of the floating element will be based on this element's position and size.
settings PositionSettings {} Settings to customize the positioning algorithm. See below for a description of each setting.

PositionSettings interface

PositionSettings is an object with the following interface

Name Type Default Description
side AnchorSide "outside-bottom" Sets the side of the anchor element that the floating element should be pinned to. This side is given by a string starting with either inside or outside, followed by a hyphen, followed by either top, right, bottom, or left. Additionally, "inside-center" is an allowed value.

The first part of this string, inside or outside, determines whether the floating element should be attempted to be placed "inside" the anchor element or "outside" of it. Using inside is useful for making it appear that the anchor contains the floating element, and it can be used for implementing a dialog that is centered on the screen. The outside value is more common and can be used for tooltips, popovers, menus, etc.

The second part of this string determines the edge on the anchor element that the floating element will be anchored to. If side is "inside-center", then the floating element will be centered in the X-direction (while align is used to position it in the Y-direction).
align AnchorAlignment "start" Determines how the floating element should align with the anchor element. If set to "start", the floating element's first edge (top or left) will align with the anchor element's first edge. If set to "center", the floating element will be centered along the axis of the anchor edge. If set to "end", the floating element's last edge will align with the anchor element's last edge.
anchorOffset number 4* The number of pixels between the anchor edge and the floating element. Positive values move the floating element farther from the anchor element (for outside positioning) or further inside the anchor element (for inside positioning). Negative values have the opposite effect.
alignmentOffset number 4** An additional offset, in pixels, to move the floating element from the aligning edge. Positive values move the floating element in the direction of center-alignment. Negative values move the floating element away from center-alignment. When align is "center", positive offsets move the floating element right (top or bottom anchor side) or down (left or right anchor side).
allowOutOfBounds boolean false If false, when the above settings result in rendering the floating element wholly or partially off-screen, attempt to adjust the settings to prevent this. Only applies to outside positioning.

First, attempt to flip to the opposite edge of the anchor if the floating element is getting clipped in that direction. If flipping results in a similar clipping, try moving to the adjacent sides.

Once we find a side that does not clip the overlay in its own dimension, check the rest of the sides to see if we need to adjust the alignment offset to fit in other dimensions.

If we try all four sides and get clipped each time, settle for overflowing and use the bottom side, since the ability to scroll is most likely in this direction.

* If side is set to "inside-center", this defaults to 0 instead of 4.

** If using outside positioning, or if align is set to "center", this defaults to 0 instead of 4.

AnchorSide

AnchorSide can be any of the following strings:

'inside-top', 'inside-bottom', 'inside-left', 'inside-right', 'inside-center', 'outside-top', 'outside-bottom', 'outside-left', 'outside-right'

AnchorAlignment

AnchorAlignment can be any of the following strings:

'start', 'center', 'end'

Best practices

As discussed above, the positioning algorithm needs to first measure the size of three different elements. Therefore, all three of these elements (anchor element, floating element, and the floating element's closest positioned container) must be rendered at the time getAnchoredPosition is called. To avoid a frame where the floating element is rendered at the (0, 0) position, give it a style of visibility: hidden until its position is returned at set. This allows the element to be measured without showing up on the page.

A note on performance

Every time getAnchoredPosition is called, it causes a reflow because it needs to query the rendering engine for the positions of 3 elements: the anchor element, the floating element, and the closest ancestor of the floating element that is positioned. Therefore, this function should not be called until it is needed (e.g. an overlay-style menu is invoked and displayed).