mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-09 07:28: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:
@@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"color: transparent; position: relative; overflow: hidden; white-space: nowrap;\\">c3k<span style=\\"position: absolute; right: 0px; color: initial;\\">c3k</span></span></span>"`;
|
||||||
|
|
||||||
|
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"color: transparent; position: relative; overflow: hidden; white-space: nowrap;\\">vain<span style=\\"position: absolute; right: 0px; color: initial;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"><div></div></div><div></div>"`;
|
||||||
@@ -44,9 +44,10 @@ export const cellxgeneActions = (page, utils) => ({
|
|||||||
(rows) =>
|
(rows) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
rows.map((row) => {
|
rows.map((row) => {
|
||||||
const cat = row.querySelector(
|
const cat = row
|
||||||
"[data-testclass='categorical-value']"
|
.querySelector("[data-testclass='categorical-value']")
|
||||||
).innerText;
|
.getAttribute("aria-label");
|
||||||
|
|
||||||
const count = row.querySelector(
|
const count = row.querySelector(
|
||||||
"[data-testclass='categorical-value-count']"
|
"[data-testclass='categorical-value-count']"
|
||||||
).innerText;
|
).innerText;
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ afterAll(() => {
|
|||||||
|
|
||||||
describe("did launch", () => {
|
describe("did launch", () => {
|
||||||
test("page launched", async () => {
|
test("page launched", async () => {
|
||||||
const element = await utils.getOneElementInnerHTML("[data-testid='header']");
|
const element = await utils.getOneElementInnerHTML(
|
||||||
expect(element).toBe(data.title);
|
"[data-testid='header']"
|
||||||
|
);
|
||||||
|
expect(element).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("terms of service, if they are there", async () => {
|
test("terms of service, if they are there", async () => {
|
||||||
@@ -46,10 +48,10 @@ describe("did launch", () => {
|
|||||||
describe("metadata loads", () => {
|
describe("metadata loads", () => {
|
||||||
test("categories and values from dataset appear", async () => {
|
test("categories and values from dataset appear", async () => {
|
||||||
for (const label in data.categorical) {
|
for (const label in data.categorical) {
|
||||||
const categoryName = await utils.getOneElementInnerText(
|
const elem = await utils.getOneElementInnerHTML(
|
||||||
`[data-testid="category-${label}"]`
|
`[data-testid="category-${label}"]`
|
||||||
);
|
);
|
||||||
expect(categoryName).toMatch(label);
|
expect(elem).toMatchSnapshot();
|
||||||
await utils.clickOn(`${label}:category-expand`);
|
await utils.clickOn(`${label}:category-expand`);
|
||||||
const categories = await cxgActions.getAllCategoriesAndCounts(label);
|
const categories = await cxgActions.getAllCategoriesAndCounts(label);
|
||||||
expect(Object.keys(categories)).toMatchObject(
|
expect(Object.keys(categories)).toMatchObject(
|
||||||
|
|||||||
@@ -201,16 +201,18 @@ describe.each([
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function assertCategoryExists(categoryName) {
|
async function assertCategoryExists(categoryName) {
|
||||||
const handle = await utils.waitByID(`${categoryName}:category-expand`);
|
const handle = await utils.waitByID(`${categoryName}:category-label`);
|
||||||
const result = await handle.evaluate((node) => node.innerText);
|
|
||||||
// slice beginning and end of category name result to account for truncation of long names
|
const result = await handle.evaluate((node) =>
|
||||||
expect(result.slice(0, 10)).toBe(categoryName.slice(0, 10));
|
node.getAttribute("aria-label")
|
||||||
expect(result.slice(-10)).toBe(categoryName.slice(-10));
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertCategoryDoesNotExist(categoryName) {
|
async function assertCategoryDoesNotExist(categoryName) {
|
||||||
const result = await page.$(
|
const result = await page.$(
|
||||||
`[data-testid='${categoryName}:category-expand']`
|
`[data-testid='${categoryName}:category-label']`
|
||||||
);
|
);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
}
|
}
|
||||||
@@ -222,7 +224,9 @@ describe.each([
|
|||||||
const previous = await utils.waitByID(
|
const previous = await utils.waitByID(
|
||||||
`categorical-value-${categoryName}-${labelName}`
|
`categorical-value-${categoryName}-${labelName}`
|
||||||
);
|
);
|
||||||
expect(await previous.evaluate((node) => node.innerText)).toBe(labelName);
|
expect(
|
||||||
|
await previous.evaluate((node) => node.getAttribute("aria-label"))
|
||||||
|
).toBe(labelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertLabelDoesNotExist(categoryName, labelName) {
|
async function assertLabelDoesNotExist(categoryName, labelName) {
|
||||||
|
|||||||
@@ -21,13 +21,7 @@ module.exports = {
|
|||||||
"react/jsx-filename-extension": "off",
|
"react/jsx-filename-extension": "off",
|
||||||
"comma-dangle": "off",
|
"comma-dangle": "off",
|
||||||
"no-underscore-dangle": "off",
|
"no-underscore-dangle": "off",
|
||||||
quotes: ["error", "double"],
|
|
||||||
"implicit-arrow-linebreak": "off",
|
"implicit-arrow-linebreak": "off",
|
||||||
"operator-linebreak": [
|
|
||||||
"error",
|
|
||||||
"after",
|
|
||||||
{ overrides: { "?": "before", ":": "before" } },
|
|
||||||
],
|
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
||||||
"no-param-reassign": "off",
|
"no-param-reassign": "off",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"./src/**/*.js": "eslint --fix ./src/",
|
"./src/**/*.js": "eslint --fix",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import React from "react";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
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 CategoryFlipperLayout from "./categoryFlipperLayout";
|
||||||
import AnnoMenu from "./annoMenuCategory";
|
import AnnoMenu from "./annoMenuCategory";
|
||||||
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
|
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
|
||||||
import AnnoDialogAddLabel from "./annoDialogAddLabel";
|
import AnnoDialogAddLabel from "./annoDialogAddLabel";
|
||||||
|
import Truncate from "../../util/truncate";
|
||||||
|
|
||||||
import * as globals from "../../../globals";
|
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) => {
|
@connect((state, ownProps) => {
|
||||||
const { metadataField } = ownProps;
|
const { metadataField } = ownProps;
|
||||||
@@ -114,10 +118,6 @@ class Category extends React.Component {
|
|||||||
We are still loading this category, so render a "busy" signal.
|
We are still loading this category, so render a "busy" signal.
|
||||||
*/
|
*/
|
||||||
const { metadataField } = this.props;
|
const { metadataField } = this.props;
|
||||||
const truncatedString = maybeTruncateString(
|
|
||||||
metadataField,
|
|
||||||
globals.categoryDisplayStringMaxLength
|
|
||||||
);
|
|
||||||
|
|
||||||
const checkboxID = `category-select-${metadataField}`;
|
const checkboxID = `category-select-${metadataField}`;
|
||||||
|
|
||||||
@@ -145,26 +145,17 @@ class Category extends React.Component {
|
|||||||
<input disabled id={checkboxID} checked type="checkbox" />
|
<input disabled id={checkboxID} checked type="checkbox" />
|
||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
</label>
|
</label>
|
||||||
<Tooltip
|
<Truncate>
|
||||||
content={metadataField}
|
|
||||||
disabled={truncatedString === null}
|
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
|
||||||
position={Position.LEFT}
|
|
||||||
usePortal
|
|
||||||
modifiers={{
|
|
||||||
preventOverflow: { enabled: false },
|
|
||||||
hide: { enabled: false },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
|
width: LABEL_WIDTH,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{truncatedString || metadataField}
|
{metadataField}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Truncate>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button minimal loading intent="primary" />
|
<Button minimal loading intent="primary" />
|
||||||
@@ -199,21 +190,22 @@ class Category extends React.Component {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
const truncatedString = maybeTruncateString(
|
|
||||||
metadataField,
|
|
||||||
globals.categoryDisplayStringMaxLength
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isUserAnno &&
|
!isUserAnno &&
|
||||||
schema?.annotations?.obsByName[metadataField]?.categories?.length === 1
|
schema?.annotations?.obsByName[metadataField]?.categories?.length === 1
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 10, marginTop: 4 }}>
|
<div style={{ marginBottom: 10, marginTop: 4 }}>
|
||||||
<span style={{ fontWeight: 700 }}>
|
<Truncate>
|
||||||
{truncatedString || metadataField}
|
<span style={{ maxWidth: 150, fontWeight: 700 }}>
|
||||||
|
{metadataField}
|
||||||
</span>
|
</span>
|
||||||
: {schema.annotations.obsByName[metadataField].categories[0]}
|
</Truncate>
|
||||||
|
<Truncate>
|
||||||
|
<span style={{ maxWidth: 150 }}>
|
||||||
|
{`: ${schema.annotations.obsByName[metadataField].categories[0]}`}
|
||||||
|
</span>
|
||||||
|
</Truncate>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,17 +238,6 @@ class Category extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
</label>
|
</label>
|
||||||
<Tooltip
|
|
||||||
content={metadataField}
|
|
||||||
disabled={truncatedString === null}
|
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
|
||||||
position={Position.LEFT}
|
|
||||||
usePortal
|
|
||||||
modifiers={{
|
|
||||||
preventOverflow: { enabled: false },
|
|
||||||
hide: { enabled: false },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
@@ -268,11 +249,19 @@ class Category extends React.Component {
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "inline-block",
|
|
||||||
}}
|
}}
|
||||||
onClick={this.handleCategoryClick}
|
onClick={this.handleCategoryClick}
|
||||||
>
|
>
|
||||||
{truncatedString || metadataField}
|
<Truncate>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
maxWidth: isUserAnno ? LABEL_WIDTH_ANNO : LABEL_WIDTH,
|
||||||
|
}}
|
||||||
|
data-testid={`${metadataField}:category-label`}
|
||||||
|
>
|
||||||
|
{metadataField}
|
||||||
|
</span>
|
||||||
|
</Truncate>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<FaChevronDown
|
<FaChevronDown
|
||||||
data-testclass="category-expand-is-expanded"
|
data-testclass="category-expand-is-expanded"
|
||||||
@@ -285,7 +274,6 @@ class Category extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
{<AnnoDialogEditCategoryName metadataField={metadataField} />}
|
{<AnnoDialogEditCategoryName metadataField={metadataField} />}
|
||||||
{<AnnoDialogAddLabel metadataField={metadataField} />}
|
{<AnnoDialogAddLabel metadataField={metadataField} />}
|
||||||
|
|||||||
@@ -9,16 +9,15 @@ import {
|
|||||||
Position,
|
Position,
|
||||||
Icon,
|
Icon,
|
||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Tooltip,
|
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import Occupancy from "./occupancy";
|
import Occupancy from "./occupancy";
|
||||||
import * as globals from "../../../globals";
|
import * as globals from "../../../globals";
|
||||||
import styles from "../categorical.css";
|
import styles from "../categorical.css";
|
||||||
import AnnoDialog from "../annoDialog";
|
import AnnoDialog from "../annoDialog";
|
||||||
import LabelInput from "../labelInput";
|
import LabelInput from "../labelInput";
|
||||||
|
import Truncate from "../../util/truncate";
|
||||||
|
|
||||||
import { AnnotationsHelpers } from "../../../util/stateManager";
|
import { AnnotationsHelpers } from "../../../util/stateManager";
|
||||||
import maybeTruncateString from "../../../util/maybeTruncateString";
|
|
||||||
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
||||||
|
|
||||||
/* this is defined outside of the class so we can use it in connect() */
|
/* 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;
|
categories = schema.annotations.obsByName[colorAccessor]?.categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncatedString = maybeTruncateString(
|
|
||||||
displayString,
|
|
||||||
colorAccessor && !isColorBy
|
|
||||||
? globals.categoryLabelDisplayStringShortLength
|
|
||||||
: globals.categoryLabelDisplayStringLongLength
|
|
||||||
);
|
|
||||||
|
|
||||||
const editModeActive =
|
const editModeActive =
|
||||||
isUserAnno &&
|
isUserAnno &&
|
||||||
annotations.labelEditable.category === metadataField &&
|
annotations.labelEditable.category === metadataField &&
|
||||||
@@ -332,6 +324,26 @@ class CategoryValue extends React.Component {
|
|||||||
|
|
||||||
const valueToggleLabel = `value-toggle-checkbox-${displayString}`;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -384,21 +396,12 @@ class CategoryValue extends React.Component {
|
|||||||
onMouseLeave={this.handleMouseEnter}
|
onMouseLeave={this.handleMouseEnter}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<Tooltip
|
<Truncate>
|
||||||
content={displayString}
|
|
||||||
disabled={truncatedString === null}
|
|
||||||
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
|
|
||||||
position={Position.LEFT}
|
|
||||||
usePortal
|
|
||||||
modifiers={{
|
|
||||||
preventOverflow: { enabled: false },
|
|
||||||
hide: { enabled: false },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
data-testid={`categorical-value-${metadataField}-${displayString}`}
|
data-testid={`categorical-value-${metadataField}-${displayString}`}
|
||||||
data-testclass="categorical-value"
|
data-testclass="categorical-value"
|
||||||
style={{
|
style={{
|
||||||
|
width: labelWidth,
|
||||||
color:
|
color:
|
||||||
displayString === globals.unassignedCategoryLabel
|
displayString === globals.unassignedCategoryLabel
|
||||||
? "#ababab"
|
? "#ababab"
|
||||||
@@ -411,13 +414,13 @@ class CategoryValue extends React.Component {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
lineHeight: "1.1em",
|
lineHeight: "1.1em",
|
||||||
height: "1.1em",
|
height: "1.1em",
|
||||||
wordBreak: "break-all",
|
|
||||||
verticalAlign: "middle",
|
verticalAlign: "middle",
|
||||||
|
marginRight: LABEL_MARGIN,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{truncatedString || displayString}
|
{displayString}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Truncate>
|
||||||
{editModeActive ? (
|
{editModeActive ? (
|
||||||
<div>
|
<div>
|
||||||
<AnnoDialog
|
<AnnoDialog
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class Occupancy extends React.PureComponent {
|
|||||||
else this.createHistogram();
|
else this.createHistogram();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div key="text" style={{ fontFamily: "Roboto", fontSize: "14px" }}>
|
<div key="text" style={{ fontSize: "14px" }}>
|
||||||
<p style={{ margin: "0" }}>
|
<p style={{ margin: "0" }}>
|
||||||
This histograms shows the distribution of{" "}
|
This histograms shows the distribution of{" "}
|
||||||
<strong>{colorAccessor}</strong> within{" "}
|
<strong>{colorAccessor}</strong> within{" "}
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class CentroidLabels extends PureComponent {
|
|||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
data-label={label}
|
data-label={label}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "Roboto Condensed",
|
|
||||||
fontSize,
|
fontSize,
|
||||||
fontWeight,
|
fontWeight,
|
||||||
fill: "black",
|
fill: "black",
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Logo from "../framework/logo";
|
import Logo from "../framework/logo";
|
||||||
|
import Truncate from "../util/truncate";
|
||||||
|
|
||||||
|
const DATASET_TITLE_WIDTH = 190;
|
||||||
|
const DATASET_TITLE_FONT_SIZE = 14;
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||||
@@ -14,16 +18,6 @@ class LeftSideBar extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { datasetTitle, aboutURL } = this.props;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -60,26 +54,36 @@ class LeftSideBar extends React.Component {
|
|||||||
gene
|
gene
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
data-testid="header"
|
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
top: -6,
|
top: -6,
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
width: "190px",
|
width: DATASET_TITLE_WIDTH,
|
||||||
marginLeft: "7px",
|
marginLeft: "7px",
|
||||||
height: "1.2em",
|
height: "1.2em",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
wordBreak: "break-all",
|
wordBreak: "break-all",
|
||||||
}}
|
}}
|
||||||
title={datasetTitle}
|
|
||||||
>
|
>
|
||||||
{aboutURL ? (
|
{aboutURL ? (
|
||||||
<a href={aboutURL} target="_blank" rel="noopener noreferrer">
|
<Truncate>
|
||||||
{displayTitle}
|
<a
|
||||||
|
style={{ width: 185 }}
|
||||||
|
href={aboutURL}
|
||||||
|
data-testid="header"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{datasetTitle}
|
||||||
</a>
|
</a>
|
||||||
|
</Truncate>
|
||||||
) : (
|
) : (
|
||||||
displayTitle
|
<Truncate>
|
||||||
|
<span style={{ width: 185 }} data-testid="header">
|
||||||
|
{datasetTitle}
|
||||||
|
</span>
|
||||||
|
</Truncate>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
const maybeTruncateString = (str, maxLength) => {
|
|
||||||
let truncatedString = null;
|
|
||||||
if (str.length > maxLength) {
|
|
||||||
truncatedString = `${str.slice(0, maxLength / 2)}…${str.slice(
|
|
||||||
-maxLength / 2
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return truncatedString;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default maybeTruncateString;
|
|
||||||
Reference in New Issue
Block a user