Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get the containing block of a "fixed" positioned element with javascript?

Let's say we have the following setup:

#header {
    background-color: #ddd;
    padding: 2rem;
}
#containing-block {
    background-color: #eef;
    padding: 2rem;
    height: 70px;
    transform: translate(0, 0);
}
#button {
    position: fixed;
    top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
    containing-block
    <div>
      <div>
        <div>
          <button id="button" onclick="console.log('offsetParent', this.offsetParent)">click me</button>
        </div>
      </div>
    </div>
</div>

where the button has fixed position and the containing-block has a transform property in place.

This might come as a surprise, but the button is positioned relative to the #containing-block, not the viewport (as one would expect when using fixed). That's because the #containing-block element has the transform property set. See https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed for clarification.

Is there an easy way to find out which is the containing block of the button? Which is the element top: 50px is calculated in respect to? Assume you don't have a reference to the containing block and you don't know how many levels up it is. It may even be the documentElement if there are no ancestors with transform, perspective or filter properties set.

For absolute or relative positioned elements, we have elem.offsetParent which gives us this reference. However, it is set to null for fixed elements.

Of course, I could look up the dom and find the first element that has a style property of transform, perspective or filter set, but this seems hacky and not future proof.

Thanks!

like image 924
cipak Avatar asked Mar 20 '20 17:03

cipak


People also ask

How do you designate the containing block for an absolutely positioned element?

Identifying the containing block If the position property is absolute , the containing block is formed by the edge of the padding box of the nearest ancestor element that has a position value other than static ( fixed , absolute , relative , or sticky ).

When using position fixed What will the element always be positioned relative to?

An element with position: fixed; is positioned relative to the viewport, which means it always stays in the same place even if the page is scrolled. The top, right, bottom, and left properties are used to position the element.

Which element do you want to Centre a block of content or position a content blocked on the page?

The <center> HTML element is a block-level element that displays its block-level or inline contents centered horizontally within its containing element.

Which style places an element at a fixed location within its container?

Fixed positioning This can be used to create a "floating" element that stays in the same position regardless of scrolling. In the example below, box "One" is fixed at 80 pixels from the top of the page and 10 pixels from the left.


1 Answers

Known behavior and spec compliant. spec should probably be changed though.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent

I've included a few workarounds from various libraries.

Workaround taken from dom-helpers (seems to be most consistent and use of offsetParent to traverse means it should only ever really traverse once or twice.):
https://github.com/react-bootstrap/dom-helpers/blob/master/src/offsetParent.ts

// taken from popper.js
function getStyleComputedProperty(element, property) {
  if (element.nodeType !== 1) {
    return [];
  }
  // NOTE: 1 DOM access here
  const window = element.ownerDocument.defaultView;
  const css = window.getComputedStyle(element, null);
  return property ? css[property] : css;
}

getOffsetParent = function(node) {
  const doc = (node && node.ownerDocument) || document
  const isHTMLElement = e => !!e && 'offsetParent' in e
  let parent = node && node.offsetParent

  while (
    isHTMLElement(parent) &&
    parent.nodeName !== 'HTML' &&
    getComputedStyle(parent, 'position') === 'static'
  ) {
    parent = parent.offsetParent
  }

  return (parent || doc.documentElement)
}
#header {
    background-color: #ddd;
    padding: 2rem;
}
#containing-block {
    background-color: #eef;
    padding: 2rem;
    height: 70px;
    transform: translate(0, 0);
}
#button {
    position: fixed;
    top: 50px;
}
<div id="header">header</div>
    <div id="containing-block">
        containing-block
        <div>
          <div>
            <div>
              <button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
            </div>
          </div>
        </div>
    </div>

Workaround code taken from jQuery source. Doesn't deal with non-element, nor TABLE TH TD, but it's jQuery. https://github.com/jquery/jquery/blob/master/src/offset.js

// taken from popper.js
function getStyleComputedProperty(element, property) {
  if (element.nodeType !== 1) {
    return [];
  }
  // NOTE: 1 DOM access here
  const window = element.ownerDocument.defaultView;
  const css = window.getComputedStyle(element, null);
  return property ? css[property] : css;
}

getOffsetParent = function(elem) {
  var doc = elem.ownerDocument;
  var offsetParent = elem.offsetParent || doc.documentElement;
  while (offsetParent &&
    (offsetParent !== doc.body || offsetParent !== doc.documentElement) &&
    getComputedStyle(offsetParent, "position") === "static") {

    offsetParent = offsetParent.parentNode;
  }
  return offsetParent;
}
#header {
    background-color: #ddd;
    padding: 2rem;
}
#containing-block {
    background-color: #eef;
    padding: 2rem;
    height: 70px;
    transform: translate(0, 0);
}
#button {
    position: fixed;
    top: 50px;
}
<div id="header">header</div>
    <div id="containing-block">
        containing-block
        <div>
          <div>
            <div>
              <button id="button" onclick="console.log('offsetParent', getOffsetParent(this),this.offsetParent)">click me</button>
            </div>
          </div>
        </div>
    </div>

Workaround code taken from popper.js. Doesn't seem to get doc.body right. The only one that specifically deals with TH TD TABLE. dom-helpers should work just because it uses offsetParent to traverse. https://github.com/popperjs/popper-core/blob/master/src/dom-utils/getOffsetParent.js

var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined';

const isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);
const isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);
function isIE(version) {
  if (version === 11) {
    return isIE11;
  }
  if (version === 10) {
    return isIE10;
  }
  return isIE11 || isIE10;
}

function getStyleComputedProperty(element, property) {
  if (element.nodeType !== 1) {
    return [];
  }
  // NOTE: 1 DOM access here
  const window = element.ownerDocument.defaultView;
  const css = window.getComputedStyle(element, null);
  return property ? css[property] : css;
}

function getOffsetParent(element) {
  if (!element) {
    return document.documentElement;
  }

  const noOffsetParent = isIE(10) ? document.body : null;

  // NOTE: 1 DOM access here
  let offsetParent = element.offsetParent || null;
  // Skip hidden elements which don't have an offsetParent
  while (offsetParent === noOffsetParent && element.nextElementSibling) {
    offsetParent = (element = element.nextElementSibling).offsetParent;
  }

  const nodeName = offsetParent && offsetParent.nodeName;

  if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {
    return element ? element.ownerDocument.documentElement : document.documentElement;
  }

  // .offsetParent will return the closest TH, TD or TABLE in case
  // no offsetParent is present, I hate this job...
  if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {
    return getOffsetParent(offsetParent);
  }

  return offsetParent;
}
#header {
    background-color: #ddd;
    padding: 2rem;
}
#containing-block {
    background-color: #eef;
    padding: 2rem;
    height: 70px;
    transform: translate(0, 0);
}
#button {
    position: fixed;
    top: 50px;
}
<div id="header">header</div>
<div id="containing-block">
    containing-block
    <div>
      <div>
        <div>
          <button id="button" onclick="console.log('offsetParent', getOffsetParent(this))">click me</button>
        </div>
      </div>
    </div>
</div>
like image 163
user120242 Avatar answered Oct 11 '22 19:10

user120242