Files
remark42/frontend/app/components/dropdown/dropdown.tsx
T
Pavel MineevandUmputun 51d2310c1f Nice inputs and buttons (#510)
* Set default font in examle

* prettify file

* add ui elements

* Use UIButton at AuthPanel

* sort deps
* remove unsed getUserTitle method
* move UserId inside AuthPanel
* tests

* use UIButton and UInput in Anonymous login

* Use UIButton and UIInput in email login

* Replace Button to UIButton

* fix context for onTitleClick in dropdown
* rearrage class props in dropdown
* TODO: change finding over DOM to using ref

* Use UIButton in input

* rearrage deps

* Use UIButton in comment

* test

* Focus and Input buttons

* custom focus style for inputs and buttons
* the same focus style for comment input
* align buttons by top line in input

* Token dropdown & hover fix

* disable hover when button disabled
* make token dropdown markup in new style

* Fix trailing comma

* update prettier
* move prettier config to json format because it is more hendy for settings
(for example vscode can suggest rules)
* prettier fixed trailing comman in ejs

* Remove unnecessary conditions

* Files was formatted by Prettier

* fix cursor pointer on collapse button

* Move components and right naming

* ui-button -> button
* ui-input -> input
* input -> comment-form

* Change cursor behavior on disabled state

* Change button style

* font-weight: normal by default
* add small border-radius

* Change another one button to component

* Fix autofocus on username in email login form

* use preact inbuild autoFocus
* fix autofocus on back from token step
* sleep for 0s enough to wait next render before focus
* use more specific name for username input

* fix line-height at auth line on mobile

* fix className
2020-01-05 14:58:10 -06:00

210 lines
5.9 KiB
TypeScript

/** @jsx createElement */
import { createElement, Component, createRef } from 'preact';
import b from 'bem-react-helper';
import { Theme } from '@app/common/types';
import { sleep } from '@app/utils/sleep';
import { Button } from '@app/components/button';
interface Props {
title: string;
titleClass?: string;
heading?: string;
isActive?: boolean;
onTitleClick?: () => void;
mix?: string;
theme: Theme;
onOpen?: (root: HTMLDivElement) => unknown;
onClose?: (root: HTMLDivElement) => unknown;
}
interface State {
isActive: boolean;
contentTranslateX: number;
}
export default class Dropdown extends Component<Props, State> {
rootNode = createRef<HTMLDivElement>();
storedDocumentHeight: string | null = null;
storedDocumentHeightSet: boolean = false;
checkInterval: number | undefined = undefined;
constructor(props: Props) {
super(props);
this.state = {
isActive: props.isActive || false,
contentTranslateX: 0,
};
this.onOutsideClick = this.onOutsideClick.bind(this);
this.receiveMessage = this.receiveMessage.bind(this);
this.__onOpen = this.__onOpen.bind(this);
this.__onClose = this.__onClose.bind(this);
}
onTitleClick = () => {
const isActive = !this.state.isActive;
const contentTranslateX = isActive ? this.state.contentTranslateX : 0;
this.setState(
{
contentTranslateX,
isActive,
},
async () => {
await this.__adjustDropDownContent();
if (isActive) {
this.__onOpen();
this.props.onOpen && this.props.onOpen(this.rootNode.current!);
} else {
this.__onClose();
this.props.onClose && this.props.onClose(this.rootNode.current!);
}
if (this.props.onTitleClick) {
this.props.onTitleClick();
}
}
);
};
__onOpen() {
const isChildOfDropDown = (() => {
if (!this.rootNode.current) return false;
let parent = this.rootNode.current.parentElement!;
while (parent !== document.body) {
if (parent.classList.contains('dropdown')) return true;
parent = parent.parentElement!;
}
return false;
})();
if (isChildOfDropDown) return;
this.storedDocumentHeight = document.body.style.minHeight;
this.storedDocumentHeightSet = true;
let prevDcBottom: number | null = null;
this.checkInterval = window.setInterval(() => {
if (!this.rootNode.current || !this.state.isActive) return;
const windowHeight = window.innerHeight;
const dcBottom = (() => {
// TODO: use ref
const dc = Array.from(this.rootNode.current.children).find(c => c.classList.contains('dropdown__content'));
if (!dc) return 0;
const rect = dc.getBoundingClientRect();
return window.scrollY + Math.abs(rect.top) + dc.scrollHeight + 10;
})();
if (prevDcBottom === null && dcBottom <= windowHeight) return;
if (dcBottom !== prevDcBottom) {
prevDcBottom = dcBottom;
document.body.style.minHeight = dcBottom + 'px';
}
}, 100);
}
__onClose() {
window.clearInterval(this.checkInterval);
if (this.storedDocumentHeightSet) {
document.body.style.minHeight = typeof this.storedDocumentHeight === 'string' ? this.storedDocumentHeight : '';
}
}
async __adjustDropDownContent() {
if (!this.rootNode.current) return;
// TODO: use ref
const dc = this.rootNode.current.querySelector<HTMLDivElement>('.dropdown__content');
if (!dc) return;
await sleep(10);
const rect = dc.getBoundingClientRect();
if (rect.left > 0) {
const wWindow = window.innerWidth;
if (rect.right <= wWindow) return;
const delta = rect.right - wWindow;
const max = Math.min(rect.left, delta);
this.setState({
contentTranslateX: -max,
});
return;
}
this.setState({
contentTranslateX: -rect.left,
});
}
receiveMessage(e: { data: string | object }) {
try {
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
if (!data.clickOutside) return;
if (!this.state.isActive) return;
this.setState(
{
contentTranslateX: 0,
isActive: false,
},
() => {
this.__onClose();
this.props.onClose && this.props.onClose(this.rootNode.current!);
}
);
} catch (e) {}
}
onOutsideClick(e: MouseEvent) {
if (!this.rootNode.current || this.rootNode.current.contains(e.target as Node) || !this.state.isActive) return;
this.setState(
{
contentTranslateX: 0,
isActive: false,
},
() => {
this.__onClose();
this.props.onClose && this.props.onClose(this.rootNode.current!);
}
);
}
componentDidMount() {
document.addEventListener('click', this.onOutsideClick);
window.addEventListener('message', this.receiveMessage);
}
componentWillUnmount() {
document.removeEventListener('click', this.onOutsideClick);
window.removeEventListener('message', this.receiveMessage);
}
render() {
const { title, titleClass, heading, children, mix, theme } = this.props;
const { isActive } = this.state;
return (
<div className={b('dropdown', { mix }, { theme, active: isActive })} ref={this.rootNode}>
<Button
aria-haspopup="listbox"
aria-expanded={isActive && 'true'}
onClick={this.onTitleClick}
theme={theme}
mix={['dropdown__title', titleClass]}
kind="link"
>
{title}
</Button>
<div
className="dropdown__content"
tabIndex={-1}
role="listbox"
style={{ transform: `translateX(${this.state.contentTranslateX}px)` }}
>
{heading && <div className="dropdown__heading">{heading}</div>}
<div className="dropdown__items">{children}</div>
</div>
</div>
);
}
}