redux-devtools/packages/devui/src/Tabs/TabsHeader.tsx
Nathan Bierema 727d753081
feature(devui): convert to TypeScript (#629)
* stash

* and those

* stash

* stash

* stash

* stash

* tests

* fix errors

* revert

* stash

* fix lint

* prettier
2020-09-09 10:35:22 -04:00

249 lines
6.9 KiB
TypeScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import observeResize from 'simple-element-resize-detector';
import { FaAngleDoubleRight } from 'react-icons/fa';
import ContextMenu from '../ContextMenu';
import createStyledComponent from '../utils/createStyledComponent';
import * as styles from './styles';
const TabsWrapper = createStyledComponent(styles);
export type ReactButtonElement = React.ReactElement<
JSX.IntrinsicElements['button'],
'button'
>;
export interface Tab<P> {
name: string;
value?: string;
component?: React.ComponentType<P>;
selector?: (tab: this) => P;
}
interface Props<P> {
tabs: ReactButtonElement[];
items: Tab<P>[];
main: boolean | undefined;
onClick: (value: string) => void;
position: 'left' | 'right' | 'center';
collapsible: boolean | undefined;
selected: string | undefined;
}
interface State {
visibleTabs: ReactButtonElement[];
hiddenTabs: ReactButtonElement[];
subMenuOpened: boolean;
contextMenu: { top: number; left: number } | undefined;
}
export default class TabsHeader<P> extends Component<Props<P>, State> {
state: State = {
visibleTabs: this.props.tabs.slice(),
hiddenTabs: [],
subMenuOpened: false,
contextMenu: undefined,
};
iconWidth = 0;
hiddenTabsWidth: number[] = [];
tabsWrapperRef?: HTMLDivElement | null;
tabsRef?: HTMLDivElement | null;
resizeDetector?: HTMLIFrameElement;
UNSAFE_componentWillReceiveProps(nextProps: Props<P>) {
if (
nextProps.tabs !== this.props.tabs ||
nextProps.selected !== this.props.selected ||
nextProps.collapsible !== this.props.collapsible
) {
this.setState({ hiddenTabs: [], visibleTabs: nextProps.tabs.slice() });
}
}
componentDidMount() {
if (this.props.collapsible) {
this.collapse();
this.enableResizeEvents();
}
}
componentDidUpdate(prevProps: Props<P>) {
const { collapsible } = this.props;
if (!collapsible) {
if (prevProps.collapsible !== collapsible) this.disableResizeEvents();
return;
}
let shouldCollapse = false;
if (this.iconWidth === 0) {
const tabButtons = this.tabsRef!.children;
if (
(this.tabsRef!.children[tabButtons.length - 1] as HTMLButtonElement)
.value === 'expandIcon'
) {
this.iconWidth = tabButtons[
tabButtons.length - 1
].getBoundingClientRect().width;
shouldCollapse = true;
}
} else if (this.state.hiddenTabs.length === 0) {
this.iconWidth = 0;
}
if (prevProps.collapsible !== collapsible) {
this.enableResizeEvents();
shouldCollapse = true;
}
if (shouldCollapse || this.props.selected !== prevProps.selected) {
this.collapse();
}
}
componentWillUnmount() {
if (this.props.collapsible) {
this.disableResizeEvents();
}
}
enableResizeEvents() {
this.resizeDetector = observeResize(this.tabsWrapperRef!, this.collapse);
window.addEventListener('mousedown', this.hideSubmenu);
}
disableResizeEvents() {
this.resizeDetector!.remove();
window.removeEventListener('mousedown', this.hideSubmenu);
}
collapse = () => {
if (this.state.subMenuOpened) this.hideSubmenu();
const { selected, tabs } = this.props;
const tabsWrapperRef = this.tabsWrapperRef;
const tabsRef = this.tabsRef;
const tabButtons = this.tabsRef!.children;
const visibleTabs = this.state.visibleTabs;
const hiddenTabs = this.state.hiddenTabs;
let tabsWrapperRight = tabsWrapperRef!.getBoundingClientRect().right;
if (!tabsWrapperRight) return; // tabs are hidden
const tabsRefRight = tabsRef!.getBoundingClientRect().right;
let i = visibleTabs.length - 1;
let hiddenTab;
if (tabsRefRight >= tabsWrapperRight - this.iconWidth) {
if (
this.props.position === 'right' &&
hiddenTabs.length > 0 &&
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef!.getBoundingClientRect().width
) {
while (
i < tabs.length - 1 &&
tabsRef!.getBoundingClientRect().width + this.hiddenTabsWidth[0] <
tabsWrapperRef!.getBoundingClientRect().width
) {
hiddenTab = hiddenTabs.shift();
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
i++;
}
} else {
while (
i > 0 &&
tabButtons[i] &&
tabButtons[i].getBoundingClientRect().right >=
tabsWrapperRight - this.iconWidth
) {
if ((tabButtons[i] as HTMLButtonElement).value !== selected) {
hiddenTabs.unshift(...visibleTabs.splice(i, 1));
this.hiddenTabsWidth.unshift(
tabButtons[i].getBoundingClientRect().width
);
} else {
tabsWrapperRight -= tabButtons[i].getBoundingClientRect().width;
}
i--;
}
}
} else {
while (
i < tabs.length - 1 &&
tabButtons[i] &&
tabButtons[i].getBoundingClientRect().right + this.hiddenTabsWidth[0] <
tabsWrapperRight - this.iconWidth
) {
hiddenTab = hiddenTabs.shift();
visibleTabs.splice(Number(hiddenTab!.key), 0, hiddenTab!);
this.hiddenTabsWidth.shift();
i++;
}
}
this.setState({ visibleTabs, hiddenTabs });
};
hideSubmenu = () => {
this.setState({ subMenuOpened: false, contextMenu: undefined });
};
getTabsWrapperRef: React.RefCallback<HTMLDivElement> = (node) => {
this.tabsWrapperRef = node;
};
getTabsRef: React.RefCallback<HTMLDivElement> = (node) => {
this.tabsRef = node;
};
expandMenu: React.MouseEventHandler = (e) => {
const rect = e.currentTarget.children[0].getBoundingClientRect();
this.setState({
contextMenu: {
top: rect.top + 10,
left: rect.left,
},
subMenuOpened: true,
});
};
render() {
const { visibleTabs, hiddenTabs, contextMenu } = this.state;
return (
<TabsWrapper
ref={this.getTabsWrapperRef}
main={this.props.main}
position={this.props.position}
>
<div ref={this.getTabsRef}>
{visibleTabs}
{this.props.collapsible &&
visibleTabs.length < this.props.items.length && (
<button onClick={this.expandMenu} value="expandIcon">
<FaAngleDoubleRight />
</button>
)}
</div>
{this.props.collapsible && contextMenu && (
<ContextMenu
items={hiddenTabs}
onClick={this.props.onClick}
x={contextMenu.left}
y={contextMenu.top}
visible={this.state.subMenuOpened}
/>
)}
</TabsWrapper>
);
}
static propTypes = {
tabs: PropTypes.array.isRequired,
items: PropTypes.array.isRequired,
main: PropTypes.bool,
onClick: PropTypes.func,
position: PropTypes.string,
collapsible: PropTypes.bool,
selected: PropTypes.string,
};
}