Summary: Currently most components are shown anonymously in the component tree, because using `styled` creates unnamed components, shown as the HTML elements they result in. This has two downsides: 1. React errors / warnings are really vague and it is hard to locate where they are coming from 2. The React Devtools don't show which components are rendering. 3. The effect of the latter it is hard to copy-from-example when developing plugins. This leads to a lot of inconsitency and duplication in the layouts of components Reviewed By: jknoxville Differential Revision: D18503675 fbshipit-source-id: 5a9ea1765346fb4c6a49e37ffa4d0b4bbcd86587
329 lines
8.3 KiB
TypeScript
329 lines
8.3 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* 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',
|
|
});
|
|
TableHeaderArrow.displayName = 'TableHead:TableHeaderArrow';
|
|
|
|
const TableHeaderColumnInteractive = styled(Interactive)({
|
|
display: 'inline-block',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
width: '100%',
|
|
});
|
|
TableHeaderColumnInteractive.displayName =
|
|
'TableHead:TableHeaderColumnInteractive';
|
|
|
|
const TableHeaderColumnContainer = styled('div')({
|
|
padding: '0 8px',
|
|
});
|
|
TableHeaderColumnContainer.displayName = 'TableHead:TableHeaderColumnContainer';
|
|
|
|
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,
|
|
}),
|
|
);
|
|
TableHeadContainer.displayName = 'TableHead:TableHeadContainer';
|
|
|
|
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',
|
|
},
|
|
}),
|
|
);
|
|
TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer';
|
|
|
|
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 | undefined;
|
|
|
|
componentDidMount() {
|
|
if (this.props.horizontallyScrollable && this.ref) {
|
|
// 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) && this.ref) {
|
|
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: {
|
|
[key: string]: JSX.Element;
|
|
} = {};
|
|
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>
|
|
);
|
|
}
|
|
}
|