Summary: _typescript_

Reviewed By: priteshrnandgaonkar

Differential Revision: D16807180

fbshipit-source-id: dcba794351eee69c0574dc224cf7bd2732bea447
This commit is contained in:
Daniel Büchele
2019-08-20 05:40:31 -07:00
committed by Facebook Github Bot
parent 62a204bdbe
commit 4c4169063d
22 changed files with 501 additions and 473 deletions

View File

@@ -0,0 +1,318 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {
TableColumnOrder,
TableColumnSizes,
TableColumns,
TableOnColumnResize,
TableOnSort,
TableRowSortOrder,
} from './types';
import {normaliseColumnWidth, isPercentage} from './utils';
import {PureComponent} from 'react';
import ContextMenu from '../ContextMenu';
import Interactive from '../Interactive';
import styled from 'react-emotion';
import {colors} from '../colors';
import FlexRow from '../FlexRow';
import invariant from 'invariant';
import {MenuItemConstructorOptions} from 'electron';
import React from 'react';
const TableHeaderArrow = styled('span')({
float: 'right',
});
const TableHeaderColumnInteractive = styled(Interactive)({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: '100%',
});
const TableHeaderColumnContainer = styled('div')({
padding: '0 8px',
});
const TableHeadContainer = styled(FlexRow)(
(props: {horizontallyScrollable?: boolean}) => ({
borderBottom: `1px solid ${colors.sectionHeaderBorder}`,
color: colors.light50,
flexShrink: 0,
left: 0,
overflow: 'hidden',
right: 0,
textAlign: 'left',
top: 0,
zIndex: 2,
minWidth: props.horizontallyScrollable ? 'min-content' : 0,
}),
);
const TableHeadColumnContainer = styled('div')(
(props: {width: string | number}) => ({
position: 'relative',
backgroundColor: colors.white,
flexShrink: props.width === 'flex' ? 1 : 0,
height: 23,
lineHeight: '23px',
fontSize: '0.85em',
fontWeight: 500,
width: props.width === 'flex' ? '100%' : props.width,
'&::after': {
position: 'absolute',
content: '""',
right: 0,
top: 5,
height: 13,
width: 1,
background: colors.light15,
},
'&:last-child::after': {
display: 'none',
},
}),
);
const RIGHT_RESIZABLE = {right: true};
function calculatePercentage(parentWidth: number, selfWidth: number): string {
return `${(100 / parentWidth) * selfWidth}%`;
}
class TableHeadColumn extends PureComponent<{
id: string;
width: string | number;
sortable?: boolean;
isResizable: boolean;
leftHasResizer: boolean;
hasFlex: boolean;
sortOrder?: TableRowSortOrder;
onSort?: TableOnSort;
columnSizes: TableColumnSizes;
onColumnResize?: TableOnColumnResize;
children?: React.ReactNode;
title?: string;
horizontallyScrollable?: boolean;
}> {
ref: HTMLElement;
componentDidMount() {
if (this.props.horizontallyScrollable) {
// measure initial width
this.onResize(this.ref.offsetWidth);
}
}
onClick = () => {
const {id, onSort, sortOrder} = this.props;
const direction =
sortOrder && sortOrder.key === id && sortOrder.direction === 'down'
? 'up'
: 'down';
if (onSort) {
onSort({
direction,
key: id,
});
}
};
onResize = (newWidth: number) => {
const {id, onColumnResize, width} = this.props;
if (!onColumnResize) {
return;
}
let normalizedWidth: number | string = newWidth;
// normalise number to a percentage if we were originally passed a percentage
if (isPercentage(width)) {
const {parentElement} = this.ref;
invariant(parentElement, 'expected there to be parentElement');
const parentWidth = parentElement.clientWidth;
const {childNodes} = parentElement;
const lastElem = childNodes[childNodes.length - 1];
const right =
lastElem instanceof HTMLElement
? lastElem.offsetLeft + lastElem.clientWidth + 1
: 0;
if (right < parentWidth) {
normalizedWidth = calculatePercentage(parentWidth, newWidth);
}
}
onColumnResize(id, normalizedWidth);
};
setRef = (ref: HTMLElement) => {
this.ref = ref;
};
render() {
const {isResizable, sortable, width, title} = this.props;
let {children} = this.props;
children = (
<TableHeaderColumnContainer>{children}</TableHeaderColumnContainer>
);
if (isResizable) {
children = (
<TableHeaderColumnInteractive
grow={true}
resizable={RIGHT_RESIZABLE}
onResize={this.onResize}
minWidth={20}>
{children}
</TableHeaderColumnInteractive>
);
}
return (
<TableHeadColumnContainer
width={width}
title={title}
onClick={sortable === true ? this.onClick : undefined}
innerRef={this.setRef}>
{children}
</TableHeadColumnContainer>
);
}
}
export default class TableHead extends PureComponent<{
columnOrder: TableColumnOrder;
onColumnOrder?: (order: TableColumnOrder) => void;
columns: TableColumns;
sortOrder?: TableRowSortOrder;
onSort?: TableOnSort;
columnSizes: TableColumnSizes;
onColumnResize?: TableOnColumnResize;
horizontallyScrollable?: boolean;
}> {
buildContextMenu = (): MenuItemConstructorOptions[] => {
const visibles = this.props.columnOrder
.map(c => (c.visible ? c.key : null))
.filter(Boolean)
.reduce((acc, cv) => {
acc.add(cv);
return acc;
}, new Set());
return Object.keys(this.props.columns).map(key => {
const visible = visibles.has(key);
return {
label: this.props.columns[key].value,
click: () => {
const {onColumnOrder, columnOrder} = this.props;
if (onColumnOrder) {
const newOrder = columnOrder.slice();
let hasVisibleItem = false;
for (let i = 0; i < newOrder.length; i++) {
const info = newOrder[i];
if (info.key === key) {
newOrder[i] = {key, visible: !visible};
}
hasVisibleItem = hasVisibleItem || newOrder[i].visible;
}
// Dont allow hiding all columns
if (hasVisibleItem) {
onColumnOrder(newOrder);
}
}
},
type: 'checkbox' as 'checkbox',
checked: visible,
};
});
};
render() {
const {
columnOrder,
columns,
columnSizes,
onColumnResize,
onSort,
sortOrder,
horizontallyScrollable,
} = this.props;
const elems = [];
let hasFlex = false;
for (const column of columnOrder) {
if (column.visible && columnSizes[column.key] === 'flex') {
hasFlex = true;
break;
}
}
let lastResizable = true;
const colElems = {};
for (const column of columnOrder) {
if (!column.visible) {
continue;
}
const key = column.key;
const col = columns[key];
let arrow;
if (col.sortable === true && sortOrder && sortOrder.key === key) {
arrow = (
<TableHeaderArrow>
{sortOrder.direction === 'up' ? '▲' : '▼'}
</TableHeaderArrow>
);
}
const width = normaliseColumnWidth(columnSizes[key]);
const isResizable = col.resizable !== false;
const elem = (
<TableHeadColumn
key={key}
id={key}
hasFlex={hasFlex}
isResizable={isResizable}
leftHasResizer={lastResizable}
width={width}
sortable={col.sortable}
sortOrder={sortOrder}
onSort={onSort}
columnSizes={columnSizes}
onColumnResize={onColumnResize}
title={key}
horizontallyScrollable={horizontallyScrollable}>
{col.value}
{arrow}
</TableHeadColumn>
);
elems.push(elem);
colElems[key] = elem;
lastResizable = isResizable;
}
return (
<ContextMenu buildItems={this.buildContextMenu}>
<TableHeadContainer horizontallyScrollable={horizontallyScrollable}>
{elems}
</TableHeadContainer>
</ContextMenu>
);
}
}