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:
Severiano Badajoz
2020-06-05 10:14:42 -07:00
committed by GitHub
parent 99d004d1f0
commit 4e96847032
13 changed files with 215 additions and 142 deletions
@@ -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>"`;
+4 -3
View File
@@ -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;
+6 -4
View File
@@ -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(
+11 -7
View File
@@ -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) {
-6
View File
@@ -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 }}>
</span> {metadataField}
: {schema.annotations.obsByName[metadataField].categories[0]} </span>
</Truncate>
<Truncate>
<span style={{ maxWidth: 150 }}>
{`: ${schema.annotations.obsByName[metadataField].categories[0]}`}
</span>
</Truncate>
</div> </div>
); );
} }
@@ -246,46 +238,42 @@ class Category extends React.Component {
/> />
<span className="bp3-control-indicator" /> <span className="bp3-control-indicator" />
</label> </label>
<Tooltip <span
content={metadataField} role="menuitem"
disabled={truncatedString === null} tabIndex="0"
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick} data-testid={`${metadataField}:category-expand`}
position={Position.LEFT} onKeyPress={(e) => {
usePortal if (e.key === "Enter") {
modifiers={{ this.handleCategoryClick();
preventOverflow: { enabled: false }, }
hide: { enabled: false },
}} }}
style={{
cursor: "pointer",
}}
onClick={this.handleCategoryClick}
> >
<span <Truncate>
role="menuitem" <span
tabIndex="0" style={{
data-testid={`${metadataField}:category-expand`} maxWidth: isUserAnno ? LABEL_WIDTH_ANNO : LABEL_WIDTH,
onKeyPress={(e) => { }}
if (e.key === "Enter") { data-testid={`${metadataField}:category-label`}
this.handleCategoryClick(); >
} {metadataField}
}} </span>
style={{ </Truncate>
cursor: "pointer", {isExpanded ? (
display: "inline-block", <FaChevronDown
}} data-testclass="category-expand-is-expanded"
onClick={this.handleCategoryClick} style={{ fontSize: 10, marginLeft: 5 }}
> />
{truncatedString || metadataField} ) : (
{isExpanded ? ( <FaChevronRight
<FaChevronDown data-testclass="category-expand-is-not-expanded"
data-testclass="category-expand-is-expanded" style={{ fontSize: 10, marginLeft: 5 }}
style={{ fontSize: 10, marginLeft: 5 }} />
/> )}
) : ( </span>
<FaChevronRight
data-testclass="category-expand-is-not-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
)}
</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
</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>
</div> </div>
+85
View File
@@ -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>
);
};
-12
View File
@@ -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;