Skip to content

⚠️ — Just trying to learn by re-creating reach/ui's Menu-Button https://reach.tech/menu-button

Notifications You must be signed in to change notification settings

adebiyial/menu-button

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A Contrived Menu Button

See demo on CodeSandbox

I tried to use my knowledge from Epic React to create a Menu Button like https://reach.tech/menu-button

The most important thing I learned is that building a library isn't the most simplest thing. I suppose the first thing to be done is to sketch out how you want the API to be used. In this case:

function Example() {
  return (
    <div className="example" style={{ position: "relative" }}>
      <Menu>
        <MenuButton>click me</MenuButton>
        <MenuList>
          <MenuItem>
            <p>menu item 1</p>
          </MenuItem>
          <MenuItem as="h1">
            <p>menu item 2</p>
          </MenuItem>
          <MenuItem>
            <input type="text" placeholder="type"/>
          </MenuItem>
          <MenuLink href="./">link</MenuLink>
        </MenuList>
      </Menu>
    </div>
  )
}

I enjoy the use of compound components and how they make the API composable. Now I can do something like:

function MenuButton({ children }) {
  const context = useMenuButtonContext();
  return <button type="button" onClick={context.toggle}>{children}</button>
}

with the useMenuButtonContext():

function useMenuButtonContext() {
  const context = useContext(MenuContext);

  if (!context) {
    throw new Error("useMenuButtonContext must be used within a <Menu/>")
  }

  return context;
}

and the MenuContext and Menu

const MenuContext = createContext(null);
function Menu({children}) {
  const { on, elRef , toggle} = useClickOutside(false)

  return <MenuContext.Provider value={{ on, toggle, elRef }}>
      {children}
  </MenuContext.Provider>
}

The useClickOutside allows us to click outside the menu to dismiss it. It looks like:

function useClickOutside(initialState) {
  const [on, setOn] = useState(initialState)
  const elRef = useRef();
  const toggle = () => setOn(!on)

  const onDocumentClick = useCallback(
    (el) => {
      const controlledElement = elRef.current;

      if (controlledElement) {
        const isKeydownEvent = el.type === 'keydown';
        const isClickEvent = el.type === 'click';

        if (isClickEvent) {
          const isInside = controlledElement.contains(el.target);
          if (!isInside) {
            return setOn(false);
          }
        }

        if (isKeydownEvent) {
          const isEscapeKey = isKeydownEvent && el.keyCode === 27;
          if (isEscapeKey) {
            return setOn(false);
          }
        }
      }
    },
    [setOn]
  );


  useEffect(() => {
    // 1. Only attach the event handler to document when `on` is true
    if (on) {
      document.addEventListener('click', onDocumentClick);
      document.addEventListener('keydown', onDocumentClick);
    }

    // 2. Remove the document event handler when the component unmounts
    return () => {
      document.removeEventListener('click', onDocumentClick);
      document.removeEventListener('keydown', onDocumentClick);
    };
  }, [on, onDocumentClick])

  return {on, elRef, toggle}
}

The MenuList and MenuItem, and MenuLink also take the shape:

function MenuList({ children }) {
  const context = useMenuButtonContext();

  return context.on ? <div className="menu-items" ref={context.elRef}>
    {Children.map(children, child => {
      return cloneElement(child, {
        className: "menu-item"
      })
    })}
  </div> : null
}

function MenuItem({as = "div", children, ...props}) {
  return createElement(as, { ...props }, children);
}

function MenuLink({href, children, ...props}) {
  return createElement("a", { href, ...props }, children);
}

We listen for the click and keydown event on the document when the menu is open, then close it if the conditions in onDocumentClick are met.

See how we are gradually composing logic yet separating them in a reusable way? React is love.

Final Note

This is my first time building something like this. Thanks to Epic React from Kent C. Dodds, Ryan Florence, Michael Jackson, Wes Bos, Dan Abramov, Andrew Clark, and everyone on the React team and in the React Community.

About

⚠️ — Just trying to learn by re-creating reach/ui's Menu-Button https://reach.tech/menu-button

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published