Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
useLeftRightAutoPosition
import {useLayoutEffect, useState} from 'react';
/**
* React Hook to measure a DOM element and set its `position` depending on
* whether it would overflow left or right.
*
* Uses useLayoutEffect for batched DOM measurements and painting.
*
* @param toggle a boolean on whether to run the effect. Runs cleanup when it changes.
* You might want a toggle if the element is always present in the DOM, and its display toggled.
*/
export function useLeftRightAutoPosition(toggle: boolean) {
// Create a callback ref, so that we get notified about changes to it (React will call it when setting it)
// @see https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
// This is because mutable refs do not notify us of changes; useCardRedundantClickArea does similar things
// Note that unlike the example in the docs, we need access to the node, so we useState for this
const [node, setNode] = useState<HTMLElement | null>(null);
useLayoutEffect(
() => {
if (node !== null && toggle === true) {
// NOTE: These are all expensive calculations, so be careful when touching them
// The good thing about useLayoutEffect is that it will run before React flushes to the DOM,
// so we have time to measure and "batch" paints.
// Want to learn more about DOM measurements?
// @see https://gist.github.com/paulirish/5d52fb081b3570c81e3a
const boundingRect = node.getBoundingClientRect();
const hasLeftOverflow = boundingRect.left < 0;
const hasRightOverflow =
boundingRect.right > (window.innerWidth || document.documentElement.clientWidth);
if (hasLeftOverflow && hasRightOverflow) {
// TODO: What do we do here? Is this even possible?
} else {
// If there's a left overflow, put it flowing to the right
if (hasLeftOverflow) {
node.style.left = '0';
}
// If there's a right overflow, put it flowing to the left
if (hasRightOverflow) {
node.style.right = '0';
} else {
// If there is no left or right overflow, then defer to the browser by setting nothing
// This might seem counter-intuitive, but the browser knows better about where to position things
// Simplifying a bit (not talking about margins), it will be left: 0 on ltr layouts or right: 0 on rtl layouts
// We'd rather not do that math here though :)
// @see https://www.w3.org/TR/css-position-3/#abs-non-replaced-width
}
}
return () => {
node.style.left = null;
node.style.right = null;
};
}
},
[node, toggle],
);
return setNode;
}

In real use, here's what it could look like:

const UserIndicatorMenu = ({user, logout, translations}) => {
  const {on: isMenuOpen, toggle: toggleMenu} = useToggle({});

  // Decide whether the menu should be positioned left or right, depending on overflow
  const menuRef = useLeftRightAutoPosition(isMenuOpen);

  return (
    <Box position="relative">
      <button aria-expanded={isMenuOpen} onClick={toggleMenu} className={`${buttonCls} silver-40`}>
        {/* Use margin+negative margin for pseudo- gap */}
        <Flex mn="1" alignItems="center" flexWrap="wrap-reverse">
          <Box ma="1">
            <BlockText ma="0">{`${user.get('firstName')} ${user.get('lastName')}`}</BlockText>
          </Box>
          <img
            // Hide the alt, because the name is displayed already
            alt=""
            src={getProfileImagePlay(user, 'tiny')}
            className="db w2 ht2 br-100 ma1"
            width="40"
            height="40"
          />
        </Flex>
      </button>
      <Box
        display={isMenuOpen ? 'block' : 'none'}
        bgColor="dark-40"
        position="absolute"
        shadow="1"
        mt="2"
        ref={menuRef}
        extraClassName="top-100 z-nav-popout"
      >
        <ul className="pv2 vs3">
          <li>
            <SimpleLink
              onClickInternal={toggleMenu}
              className={linkCls}
              href={`/players/${user.get('id')}`}
            >
              {translations.get('USER_INDICATOR_PROFILE_LABEL')}
            </SimpleLink>
          </li>
          <li>
            <SimpleLink onClickInternal={toggleMenu} className={linkCls} href="/change-password">
              {translations.get('USER_INDICATOR_CHANGE_PASSWORD_LABEL')}
            </SimpleLink>
          </li>
          <li>
            <button className={`${buttonCls} ph3 silver-10`} onClick={logout}>
              {translations.get('LOGOUT')}
            </button>
          </li>
        </ul>
      </Box>
    </Box>
  );
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment