Skip to content

Instantly share code, notes, and snippets.

@chenyong
Created September 3, 2019 06:49
Show Gist options
  • Save chenyong/9e27f82b187ddfe11fe6f7aa2770a8aa to your computer and use it in GitHub Desktop.
Save chenyong/9e27f82b187ddfe11fe6f7aa2770a8aa to your computer and use it in GitHub Desktop.
Dropdown area component with mostly DOM operations. It works but has quite some drawbacks.
import { h, FunctionComponent, VNode, render } from "preact";
import { css } from "emotion";
import { useState, useRef, useEffect, useLayoutEffect } from "preact/hooks";
import { timeout } from "../util/time";
interface IPosition {
left?: number;
right?: number;
top?: number;
bottom?: number;
}
let DropdownArea: FunctionComponent<{
className?: string;
style?: any;
cardClassName?: string;
alignToRight?: boolean;
duration?: number;
renderContent: (onClose: FuncVoid) => VNode;
/** 允许组件强行定义菜单所在位置 */
position?: IPosition;
}> = props => {
let menuVisibility = useRef(false);
let transitionDuration = props.duration || 300;
let containerElement = useRef<HTMLDivElement>(null);
let menuElement = useRef<HTMLDivElement>(null);
/** Methods */
let onClose = () => {
domHideMenu();
};
let domShowMenu = async () => {
menuVisibility.current = true;
let rect = containerElement.current.getBoundingClientRect();
let position: IPosition = {};
if (props.position != null) {
position = props.position;
} else {
position.top = 54 + 4;
if (props.alignToRight) {
position.right = rect.right + 2;
} else {
position.left = rect.left - 2;
}
}
let el = document.createElement("div");
menuElement.current = el;
el.classList.add(styleMenu);
el.style.top = `${position.top}px`;
el.style.left = position.left != null ? `${position.left}px` : undefined;
el.style.right =
position.right != null
? `${window.innerWidth - position.right}px`
: undefined;
el.style.transitionDuration = `${transitionDuration}ms`;
document.body.appendChild(el);
render(props.renderContent(onClose), menuElement.current);
await timeout(1);
el.classList.add(styleMenuVisible);
};
let domHideMenu = async () => {
if (menuVisibility.current === false) {
return;
}
let el = menuElement.current;
el.classList.remove(styleMenuVisible);
await timeout(transitionDuration);
render(null, menuElement.current);
document.body.removeChild(el);
menuVisibility.current = false;
};
let toggleMenu = (event: MouseEvent) => {
event.stopPropagation();
if (menuVisibility.current === false) {
domShowMenu();
} else {
domHideMenu();
}
};
/** Effects */
useEffect(() => {
let onWindowClick = (event: MouseEvent) => {
domHideMenu();
};
window.addEventListener("click", onWindowClick);
return () => {
window.removeEventListener("click", onWindowClick);
};
}, []);
useLayoutEffect(() => {
if (menuVisibility.current) {
render(props.renderContent(onClose), menuElement.current);
}
});
useEffect(() => {
// clear DOM Nodes when unmounting
return () => {
if (menuVisibility.current) {
render(null, menuElement.current);
document.body.removeChild(menuElement.current);
}
};
});
/** Renderers */
return (
<div className={styleContainer} ref={containerElement} onClick={toggleMenu}>
{props.children}
</div>
);
};
export default DropdownArea;
let styleContainer = css`
cursor: pointer;
`;
let styleMenu = css`
position: fixed;
opacity: 0;
z-index: 1000; /* same as antd popups */
background-color: white;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.16);
min-width: 80px;
min-height: 20px;
`;
let styleMenuVisible = css`
opacity: 1;
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment