mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-27 06:48:12 +08:00
Create Truncation Component (#1500)
* remove ESLint rules
operator-linebreak is no different then default
quotes is overwriting prettier config
* add string-pixel-width dep
* don't lint-staged src, only staged
* add widthMap
* create Truncate component
* refactor in truncate component
* add font load checking
* remove font-family styling
* render Truncate's child instead of creating own component to render
* refactor to use Truncate component
* add span back
* support children
* remove maybeTruncateString
* sub in Truncate component
* add bold prop
* accurately compute largest possible string
* remove logs
* tweak truncation method
* memoize comp function
* explain disable
* fix bugs w/ abs/floor
* tweak widths
* tweak widths
* fix font size
* remove border
* move test-id
* attempt css solution
* Revert "attempt css solution"
This reverts commit aac4d8a6f6.
* CSS solution v2
* remove string-pixel-width
* remove widthsMap
* remove dead code
* remove "data-truncated" as it is always true
* tweak label width
* fix e2e tests
* remove testing string
* e2e annotations tweaks
* correct snapshot
* remove resolves
* check for labels
* add test-id
* format fix
* update snapshot
* fix color
* pull constants out where available
This commit is contained in:
@@ -2,14 +2,18 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||
import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core";
|
||||
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
|
||||
import CategoryFlipperLayout from "./categoryFlipperLayout";
|
||||
import AnnoMenu from "./annoMenuCategory";
|
||||
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
|
||||
import AnnoDialogAddLabel from "./annoDialogAddLabel";
|
||||
import Truncate from "../../util/truncate";
|
||||
|
||||
import * as globals from "../../../globals";
|
||||
import maybeTruncateString from "../../../util/maybeTruncateString";
|
||||
|
||||
const LABEL_WIDTH = globals.leftSidebarWidth - 100;
|
||||
const ANNO_BUTTON_WIDTH = 50;
|
||||
const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH;
|
||||
|
||||
@connect((state, ownProps) => {
|
||||
const { metadataField } = ownProps;
|
||||
@@ -114,10 +118,6 @@ class Category extends React.Component {
|
||||
We are still loading this category, so render a "busy" signal.
|
||||
*/
|
||||
const { metadataField } = this.props;
|
||||
const truncatedString = maybeTruncateString(
|
||||
metadataField,
|
||||
globals.categoryDisplayStringMaxLength
|
||||
);
|
||||
|
||||
const checkboxID = `category-select-${metadataField}`;
|
||||
|
||||
@@ -145,26 +145,17 @@ class Category extends React.Component {
|
||||
<input disabled id={checkboxID} checked type="checkbox" />
|
||||
<span className="bp3-control-indicator" />
|
||||
</label>
|
||||
<Tooltip
|
||||
content={metadataField}
|
||||
disabled={truncatedString === null}
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
||||
position={Position.LEFT}
|
||||
usePortal
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<Truncate>
|
||||
<span
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
width: LABEL_WIDTH,
|
||||
}}
|
||||
>
|
||||
{truncatedString || metadataField}
|
||||
{metadataField}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Truncate>
|
||||
</div>
|
||||
<div>
|
||||
<Button minimal loading intent="primary" />
|
||||
@@ -199,21 +190,22 @@ class Category extends React.Component {
|
||||
false
|
||||
);
|
||||
|
||||
const truncatedString = maybeTruncateString(
|
||||
metadataField,
|
||||
globals.categoryDisplayStringMaxLength
|
||||
);
|
||||
|
||||
if (
|
||||
!isUserAnno &&
|
||||
schema?.annotations?.obsByName[metadataField]?.categories?.length === 1
|
||||
) {
|
||||
return (
|
||||
<div style={{ marginBottom: 10, marginTop: 4 }}>
|
||||
<span style={{ fontWeight: 700 }}>
|
||||
{truncatedString || metadataField}
|
||||
</span>
|
||||
: {schema.annotations.obsByName[metadataField].categories[0]}
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 150, fontWeight: 700 }}>
|
||||
{metadataField}
|
||||
</span>
|
||||
</Truncate>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 150 }}>
|
||||
{`: ${schema.annotations.obsByName[metadataField].categories[0]}`}
|
||||
</span>
|
||||
</Truncate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -246,46 +238,42 @@ class Category extends React.Component {
|
||||
/>
|
||||
<span className="bp3-control-indicator" />
|
||||
</label>
|
||||
<Tooltip
|
||||
content={metadataField}
|
||||
disabled={truncatedString === null}
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
||||
position={Position.LEFT}
|
||||
usePortal
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
<span
|
||||
role="menuitem"
|
||||
tabIndex="0"
|
||||
data-testid={`${metadataField}:category-expand`}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.handleCategoryClick();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={this.handleCategoryClick}
|
||||
>
|
||||
<span
|
||||
role="menuitem"
|
||||
tabIndex="0"
|
||||
data-testid={`${metadataField}:category-expand`}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
this.handleCategoryClick();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
}}
|
||||
onClick={this.handleCategoryClick}
|
||||
>
|
||||
{truncatedString || metadataField}
|
||||
{isExpanded ? (
|
||||
<FaChevronDown
|
||||
data-testclass="category-expand-is-expanded"
|
||||
style={{ fontSize: 10, marginLeft: 5 }}
|
||||
/>
|
||||
) : (
|
||||
<FaChevronRight
|
||||
data-testclass="category-expand-is-not-expanded"
|
||||
style={{ fontSize: 10, marginLeft: 5 }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Truncate>
|
||||
<span
|
||||
style={{
|
||||
maxWidth: isUserAnno ? LABEL_WIDTH_ANNO : LABEL_WIDTH,
|
||||
}}
|
||||
data-testid={`${metadataField}:category-label`}
|
||||
>
|
||||
{metadataField}
|
||||
</span>
|
||||
</Truncate>
|
||||
{isExpanded ? (
|
||||
<FaChevronDown
|
||||
data-testclass="category-expand-is-expanded"
|
||||
style={{ fontSize: 10, marginLeft: 5 }}
|
||||
/>
|
||||
) : (
|
||||
<FaChevronRight
|
||||
data-testclass="category-expand-is-not-expanded"
|
||||
style={{ fontSize: 10, marginLeft: 5 }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{<AnnoDialogEditCategoryName metadataField={metadataField} />}
|
||||
{<AnnoDialogAddLabel metadataField={metadataField} />}
|
||||
|
||||
@@ -9,16 +9,15 @@ import {
|
||||
Position,
|
||||
Icon,
|
||||
PopoverInteractionKind,
|
||||
Tooltip,
|
||||
} from "@blueprintjs/core";
|
||||
import Occupancy from "./occupancy";
|
||||
import * as globals from "../../../globals";
|
||||
import styles from "../categorical.css";
|
||||
import AnnoDialog from "../annoDialog";
|
||||
import LabelInput from "../labelInput";
|
||||
import Truncate from "../../util/truncate";
|
||||
|
||||
import { AnnotationsHelpers } from "../../../util/stateManager";
|
||||
import maybeTruncateString from "../../../util/maybeTruncateString";
|
||||
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
||||
|
||||
/* this is defined outside of the class so we can use it in connect() */
|
||||
@@ -317,13 +316,6 @@ class CategoryValue extends React.Component {
|
||||
categories = schema.annotations.obsByName[colorAccessor]?.categories;
|
||||
}
|
||||
|
||||
const truncatedString = maybeTruncateString(
|
||||
displayString,
|
||||
colorAccessor && !isColorBy
|
||||
? globals.categoryLabelDisplayStringShortLength
|
||||
: globals.categoryLabelDisplayStringLongLength
|
||||
);
|
||||
|
||||
const editModeActive =
|
||||
isUserAnno &&
|
||||
annotations.labelEditable.category === metadataField &&
|
||||
@@ -332,6 +324,26 @@ class CategoryValue extends React.Component {
|
||||
|
||||
const valueToggleLabel = `value-toggle-checkbox-${displayString}`;
|
||||
|
||||
const LEFT_MARGIN = 33;
|
||||
const CHECKBOX = 26;
|
||||
const CELL_NUMBER = 61;
|
||||
const ANNO_MENU = 26;
|
||||
const LABEL_MARGIN = 24;
|
||||
|
||||
const otherElementsWidth =
|
||||
LEFT_MARGIN +
|
||||
CHECKBOX +
|
||||
CELL_NUMBER +
|
||||
LABEL_MARGIN +
|
||||
(isUserAnno ? ANNO_MENU : 0);
|
||||
|
||||
const OCCUPANCY_WIDTH = 100;
|
||||
|
||||
const labelWidth =
|
||||
colorAccessor && !isColorBy
|
||||
? globals.leftSidebarWidth - otherElementsWidth - OCCUPANCY_WIDTH
|
||||
: globals.leftSidebarWidth - otherElementsWidth;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -384,21 +396,12 @@ class CategoryValue extends React.Component {
|
||||
onMouseLeave={this.handleMouseEnter}
|
||||
/>
|
||||
</label>
|
||||
<Tooltip
|
||||
content={displayString}
|
||||
disabled={truncatedString === null}
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
||||
position={Position.LEFT}
|
||||
usePortal
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<Truncate>
|
||||
<span
|
||||
data-testid={`categorical-value-${metadataField}-${displayString}`}
|
||||
data-testclass="categorical-value"
|
||||
style={{
|
||||
width: labelWidth,
|
||||
color:
|
||||
displayString === globals.unassignedCategoryLabel
|
||||
? "#ababab"
|
||||
@@ -411,13 +414,13 @@ class CategoryValue extends React.Component {
|
||||
overflow: "hidden",
|
||||
lineHeight: "1.1em",
|
||||
height: "1.1em",
|
||||
wordBreak: "break-all",
|
||||
verticalAlign: "middle",
|
||||
marginRight: LABEL_MARGIN,
|
||||
}}
|
||||
>
|
||||
{truncatedString || displayString}
|
||||
{displayString}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Truncate>
|
||||
{editModeActive ? (
|
||||
<div>
|
||||
<AnnoDialog
|
||||
|
||||
@@ -168,7 +168,7 @@ class Occupancy extends React.PureComponent {
|
||||
else this.createHistogram();
|
||||
}}
|
||||
/>
|
||||
<div key="text" style={{ fontFamily: "Roboto", fontSize: "14px" }}>
|
||||
<div key="text" style={{ fontSize: "14px" }}>
|
||||
<p style={{ margin: "0" }}>
|
||||
This histograms shows the distribution of{" "}
|
||||
<strong>{colorAccessor}</strong> within{" "}
|
||||
|
||||
@@ -85,7 +85,6 @@ class CentroidLabels extends PureComponent {
|
||||
textAnchor="middle"
|
||||
data-label={label}
|
||||
style={{
|
||||
fontFamily: "Roboto Condensed",
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fill: "black",
|
||||
|
||||
@@ -3,6 +3,10 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import Logo from "../framework/logo";
|
||||
import Truncate from "../util/truncate";
|
||||
|
||||
const DATASET_TITLE_WIDTH = 190;
|
||||
const DATASET_TITLE_FONT_SIZE = 14;
|
||||
|
||||
@connect((state) => ({
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
@@ -14,16 +18,6 @@ class LeftSideBar extends React.Component {
|
||||
render() {
|
||||
const { datasetTitle, aboutURL } = this.props;
|
||||
|
||||
const displayTitle =
|
||||
datasetTitle.length > globals.datasetTitleMaxCharacterCount
|
||||
? `${datasetTitle.substring(
|
||||
0,
|
||||
Math.floor(globals.datasetTitleMaxCharacterCount / 2)
|
||||
)}…${datasetTitle.slice(
|
||||
-Math.floor(globals.datasetTitleMaxCharacterCount / 2)
|
||||
)}`
|
||||
: datasetTitle;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -60,26 +54,36 @@ class LeftSideBar extends React.Component {
|
||||
gene
|
||||
</span>
|
||||
<div
|
||||
data-testid="header"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
display: "inline-block",
|
||||
width: "190px",
|
||||
width: DATASET_TITLE_WIDTH,
|
||||
marginLeft: "7px",
|
||||
height: "1.2em",
|
||||
overflow: "hidden",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
title={datasetTitle}
|
||||
>
|
||||
{aboutURL ? (
|
||||
<a href={aboutURL} target="_blank" rel="noopener noreferrer">
|
||||
{displayTitle}
|
||||
</a>
|
||||
<Truncate>
|
||||
<a
|
||||
style={{ width: 185 }}
|
||||
href={aboutURL}
|
||||
data-testid="header"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{datasetTitle}
|
||||
</a>
|
||||
</Truncate>
|
||||
) : (
|
||||
displayTitle
|
||||
<Truncate>
|
||||
<span style={{ width: 185 }} data-testid="header">
|
||||
{datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { cloneElement } from "react";
|
||||
import { Tooltip, Position } from "@blueprintjs/core";
|
||||
|
||||
import { tooltipHoverOpenDelayQuick } from "../../globals";
|
||||
|
||||
const SPLIT_STYLE = {
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
justifyContent: "flex-start",
|
||||
};
|
||||
|
||||
const FIRST_HALF_STYLE = {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 1,
|
||||
minWidth: "5px",
|
||||
};
|
||||
|
||||
const SECOND_HALF_INNER_STYLE = {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
};
|
||||
const SECOND_HALF_STYLE = {
|
||||
color: "transparent",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
export default (props) => {
|
||||
const { children } = props;
|
||||
// Truncate only support a single child with a text child
|
||||
|
||||
if (
|
||||
React.Children.count(children) !== 1 ||
|
||||
React.Children.count(children.props?.children) !== 1
|
||||
) {
|
||||
throw Error("Only pass a single child with text to Truncate");
|
||||
}
|
||||
const originalString = children.props.children;
|
||||
|
||||
const firstString = originalString.substr(0, originalString.length / 2);
|
||||
const secondString = originalString.substr(originalString.length / 2);
|
||||
|
||||
const inheritedColor = children.props.style.color;
|
||||
|
||||
const splitStyle = { ...children.props.style, ...SPLIT_STYLE };
|
||||
const secondHalfInnerStyle = {
|
||||
...SECOND_HALF_INNER_STYLE,
|
||||
color: inheritedColor || "initial",
|
||||
};
|
||||
|
||||
const truncatedJSX = (
|
||||
<span style={splitStyle}>
|
||||
<span style={FIRST_HALF_STYLE}>{firstString}</span>
|
||||
<span style={SECOND_HALF_STYLE}>
|
||||
{secondString}
|
||||
<span style={secondHalfInnerStyle}>{secondString}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
// clone children, changing the children(text) to the truncated string
|
||||
const newChildren = React.Children.map(children, (child) =>
|
||||
cloneElement(child, {
|
||||
children: truncatedJSX,
|
||||
"aria-label": originalString,
|
||||
})
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
content={originalString}
|
||||
hoverOpenDelay={tooltipHoverOpenDelayQuick}
|
||||
position={Position.LEFT}
|
||||
usePortal
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
{newChildren}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user