Files
flipper/src/ui/components/table/TableHead.tsx
Michel Weststrate 0a8222410c Fix React devtools experience
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
2019-11-15 02:09:32 -08:00

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>
);
}
}