nishant-design-system
Version:
Sense UI components library
162 lines (138 loc) • 3.9 kB
Flow
// @flow strict
import * as React from 'react';
import invariant from 'invariant';
import {pageHeight} from './dom';
export type ChildProps = {
onOpen: () => void,
isOpen: boolean,
height: ?number,
pageBottom: ?number,
cancelNext: () => void,
clickAway: () => void,
anchorRef: (?HTMLElement) => mixed,
};
export type ClickAwayProps = {
closeOnEscapeKeypress?: boolean,
children: (props: ChildProps) => React.Node,
shouldCancel: (event: MouseEvent) => boolean,
onChange?: (isOpen: boolean) => mixed,
};
type ClickAwayState = {
isOpen: boolean,
height: ?number,
pageBottom: ?number,
};
// TODO(Nishant): Make this a functional component
export class ClickAway extends React.Component<ClickAwayProps, ClickAwayState> {
static defaultProps: {
shouldCancel: () => boolean,
closeOnEscapeKeypress?: boolean,
} = {
shouldCancel: () => false,
closeOnEscapeKeypress: true,
};
state: ClickAwayState = {
isOpen: false,
height: null,
pageBottom: null,
};
el: ?HTMLElement = null;
cancelNext: boolean = false;
componentDidMount() {
if (this.el) {
this.setState({
height: this.el.offsetHeight,
pageBottom: this.pageBottom(),
});
}
}
componentDidUpdate(prevProps: ClickAwayProps, prevState: ClickAwayState) {
const {isOpen} = this.state;
if (prevState.isOpen !== isOpen) {
if (this.state.isOpen) {
window.document.addEventListener('click', this.handleCloseClick);
if (this.props.closeOnEscapeKeypress) {
window.document.addEventListener(
'keyup',
this.handleCloseOnEscapeKeypress,
);
}
} else {
window.document.removeEventListener('click', this.handleCloseClick);
window.document.removeEventListener(
'keyup',
this.handleCloseOnEscapeKeypress,
);
}
}
}
componentWillUnmount() {
window.document.removeEventListener('click', this.handleCloseClick);
window.document.removeEventListener(
'keyup',
this.handleCloseOnEscapeKeypress,
);
}
render(): React.Node {
const {height, isOpen, pageBottom} = this.state;
return this.props.children({
onOpen: this.handleOpenClick,
isOpen,
height,
pageBottom,
cancelNext: this.cancelNextClickAway,
clickAway: this.forceClose,
anchorRef: (el) => (this.el = el),
});
}
handleOpenClick: () => void = () => {
// NOTE (kyle): we recalculate the position on click because sibling and niece components
// could have changed.
let {pageBottom} = this.state;
if (this.el) {
pageBottom = this.pageBottom();
}
this.setState(
{
isOpen: true,
pageBottom,
},
this.handleOnChange,
);
};
handleCloseClick: (evt: MouseEvent) => void = (evt: MouseEvent) => {
if (this.props.shouldCancel(evt)) {
return;
}
if (this.cancelNext === true) {
this.cancelNext = false;
} else {
this.setState(
{
isOpen: false,
},
this.handleOnChange,
);
}
};
handleCloseOnEscapeKeypress: (
evt?: SyntheticKeyboardEvent<EventTarget>,
) => void = (evt?: SyntheticKeyboardEvent<EventTarget>) => {
if (evt?.key === 'Escape') {
this.forceClose();
}
};
forceClose: () => void = () => {
this.setState({isOpen: false}, this.handleOnChange);
};
cancelNextClickAway: () => void = () => {
this.cancelNext = true;
};
handleOnChange: () => mixed = () =>
this.props.onChange && this.props.onChange(this.state.isOpen);
pageBottom(): $FlowFixMe {
invariant(this.el, 'pageBottom() requires that this.el not be null');
const bottomBound = this.el ? this.el.getBoundingClientRect().bottom : 0;
return pageHeight() - bottomBound + window.scrollY;
}
}