feat: #4141 added truncate support into text widget (#9820)

* feat: #4141 added truncate support into text widget

* fix: updated truncate show logic, cypress added

* fix: updated ellipsis position

Co-authored-by: bhavin <techbhavin@gmail.com>
This commit is contained in:
Yash Vibhandik 2022-01-04 11:51:02 +05:30 committed by GitHub
parent 4163cbf92a
commit a6a462cf68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 360 additions and 25 deletions

View File

@ -0,0 +1,65 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 966,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 240,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 16,
"minHeight": 280,
"parentColumnSpace": 1,
"dynamicTriggerPathList": [],
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"isVisible": true,
"text": "Label",
"fontSize": "PARAGRAPH",
"fontStyle": "BOLD",
"textAlign": "LEFT",
"textColor": "#231F20",
"widgetName": "Text1",
"version": 1,
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 57.875,
"parentRowSpace": 40,
"leftColumn": 4,
"rightColumn": 8,
"topRow": 1,
"bottomRow": 2,
"parentId": "0",
"widgetId": "266vj9u1mr"
},
{
"isVisible": true,
"text": "Label",
"fontSize": "PARAGRAPH",
"fontStyle": "BOLD",
"textAlign": "LEFT",
"textColor": "#231F20",
"widgetName": "Text2",
"version": 1,
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 57.875,
"parentRowSpace": 40,
"leftColumn": 4,
"rightColumn": 12,
"topRow": 3,
"bottomRow": 9,
"parentId": "0",
"widgetId": "hfbdx6i5g9"
}
]
}
}

View File

@ -0,0 +1,67 @@
const dsl = require("../../../../fixtures/textNewDsl.json");
describe("Text Widget Truncate Functionality", function() {
before(() => {
cy.addDsl(dsl);
});
it("Validate long text is not truncating in default", function() {
cy.get(
`.appsmith_widget_${dsl.dsl.children[0].widgetId} .t--draggable-textwidget`,
).click({
force: true,
});
cy.testJsontext(
"text",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
);
cy.get(
`.appsmith_widget_${dsl.dsl.children[0].widgetId} .t--widget-textwidget-truncate`,
).should("not.exist");
});
it("Enable Truncate Text option and Validate", function() {
cy.get(".t--property-control-truncatetext > .bp3-switch").click();
cy.wait("@updateLayout");
cy.get(
`.appsmith_widget_${dsl.dsl.children[0].widgetId} .t--widget-textwidget-truncate`,
).should("exist");
cy.closePropertyPane();
});
it("Open modal on click and Validate", function() {
cy.get(
`.appsmith_widget_${dsl.dsl.children[0].widgetId} .t--widget-textwidget-truncate`,
).click();
cy.get(".t--widget-textwidget-truncate-modal").should("exist");
// close modal
cy.get(".t--widget-textwidget-truncate-modal span[name='cross']").click({
force: true,
});
});
it("Add Long Text to large text box and validate", function() {
cy.get(
`.appsmith_widget_${dsl.dsl.children[1].widgetId} .t--draggable-textwidget`,
).click({
force: true,
});
cy.wait(200);
cy.testJsontext(
"text",
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
);
cy.get(
`.appsmith_widget_${dsl.dsl.children[1].widgetId} .t--widget-textwidget-truncate`,
).should("not.exist");
});
afterEach(() => {
//
});
});

View File

@ -9,13 +9,20 @@ import {
TextSize,
TEXT_SIZES,
} from "constants/WidgetConstants";
import Icon, { IconSize } from "components/ads/Icon";
import { isEqual, get } from "lodash";
import ModalComponent from "components/designSystems/appsmith/ModalComponent";
import { Colors } from "constants/Colors";
export type TextAlign = "LEFT" | "CENTER" | "RIGHT" | "JUSTIFY";
const ELLIPSIS_HEIGHT = 15;
export const TextContainer = styled.div`
& {
height: 100%;
width: 100%;
position: relative;
}
ul {
@ -40,22 +47,38 @@ export const TextContainer = styled.div`
}
`;
const StyledIcon = styled(Icon)<{ backgroundColor?: string }>`
cursor: pointer;
bottom: 0;
left: 0;
right: 0;
height: ${ELLIPSIS_HEIGHT}px;
background: ${(props) =>
props.backgroundColor ? props.backgroundColor : "transparent"};
`;
export const StyledText = styled(Text)<{
scroll: boolean;
truncate: boolean;
isTruncated: boolean;
textAlign: string;
backgroundColor?: string;
textColor?: string;
fontStyle?: string;
fontSize?: TextSize;
}>`
height: 100%;
overflow-y: ${(props) => (props.scroll ? "auto" : "hidden")};
height: ${(props) =>
props.isTruncated ? `calc(100% - ${ELLIPSIS_HEIGHT}px)` : "100%"};
overflow-y: ${(props) =>
props.scroll ? (props.isTruncated ? "hidden" : "auto") : "hidden"};
text-overflow: ellipsis;
text-align: ${(props) => props.textAlign.toLowerCase()};
display: flex;
width: 100%;
justify-content: flex-start;
align-items: ${(props) => (props.scroll ? "flex-start" : "center")};
flex-direction: ${(props) => (props.isTruncated ? "column" : "unset")};
align-items: ${(props) =>
props.scroll || props.truncate ? "flex-start" : "center"};
background: ${(props) => props?.backgroundColor};
color: ${(props) => props?.textColor};
font-style: ${(props) =>
@ -72,6 +95,36 @@ export const StyledText = styled(Text)<{
}
`;
const ModalContent = styled.div`
background: ${Colors.WHITE};
padding: 24px;
padding-top: 16px;
`;
const Heading = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-weight: 500;
font-size: 20px;
line-height: 24px;
letter-spacing: -0.24px;
color: ${Colors.GREY_10};
}
.icon > svg > path {
stroke: ${Colors.GREY_9};
}
`;
const Content = styled.div`
padding-top: 16px;
color: ${Colors.GREY_9};
max-height: 70vh;
overflow: auto;
`;
export interface TextComponentProps extends ComponentProps {
text?: string;
textAlign: TextAlign;
@ -83,9 +136,69 @@ export interface TextComponentProps extends ComponentProps {
textColor?: string;
fontStyle?: string;
disableLink: boolean;
shouldTruncate: boolean;
truncateButtonColor?: string;
// helpers to detect and re-calculate content width
bottomRow?: number;
leftColumn?: number;
rightColumn?: number;
topRow?: number;
}
class TextComponent extends React.Component<TextComponentProps> {
type State = {
isTruncated: boolean;
showModal: boolean;
};
type TextRef = React.Ref<Text> | undefined;
class TextComponent extends React.Component<TextComponentProps, State> {
state = {
isTruncated: false,
showModal: false,
};
textRef = React.createRef() as TextRef;
getTruncate = (element: any) => {
const { isTruncated } = this.state;
// add ELLIPSIS_HEIGHT and check content content is overflowing or not
return (
element.scrollHeight >
element.offsetHeight + (isTruncated ? ELLIPSIS_HEIGHT : 0)
);
};
componentDidMount = () => {
const textRef = get(this.textRef, "current.textRef");
if (textRef && this.props.shouldTruncate) {
const isTruncated = this.getTruncate(textRef);
this.setState({ isTruncated });
}
};
componentDidUpdate = (prevProps: TextComponentProps) => {
if (!isEqual(prevProps, this.props)) {
if (this.props.shouldTruncate) {
const textRef = get(this.textRef, "current.textRef");
if (textRef) {
const isTruncated = this.getTruncate(textRef);
this.setState({ isTruncated });
}
} else if (prevProps.shouldTruncate && !this.props.shouldTruncate) {
this.setState({ isTruncated: false });
}
}
};
handleModelOpen = () => {
this.setState({ showModal: true });
};
handleModelClose = () => {
this.setState({ showModal: false });
};
render() {
const {
backgroundColor,
@ -93,33 +206,86 @@ class TextComponent extends React.Component<TextComponentProps> {
ellipsize,
fontSize,
fontStyle,
shouldScroll,
shouldTruncate,
text,
textAlign,
textColor,
truncateButtonColor,
} = this.props;
return (
<TextContainer>
<StyledText
backgroundColor={backgroundColor}
className={this.props.isLoading ? "bp3-skeleton" : "bp3-ui-text"}
ellipsize={ellipsize}
fontSize={fontSize}
fontStyle={fontStyle}
scroll={!!this.props.shouldScroll}
textAlign={textAlign}
textColor={textColor}
<>
<TextContainer>
<StyledText
backgroundColor={backgroundColor}
className={this.props.isLoading ? "bp3-skeleton" : "bp3-ui-text"}
ellipsize={ellipsize}
fontSize={fontSize}
fontStyle={fontStyle}
isTruncated={this.state.isTruncated}
ref={this.textRef}
scroll={!!shouldScroll}
textAlign={textAlign}
textColor={textColor}
truncate={!!shouldTruncate}
>
<Interweave
content={text}
matchers={
disableLink
? []
: [new EmailMatcher("email"), new UrlMatcher("url")]
}
newWindow
/>
</StyledText>
{this.state.isTruncated && (
<StyledIcon
backgroundColor={backgroundColor}
className="t--widget-textwidget-truncate"
fillColor={truncateButtonColor}
name="context-menu"
onClick={this.handleModelOpen}
size={IconSize.XXXL}
/>
)}
</TextContainer>
<ModalComponent
canEscapeKeyClose
canOutsideClickClose
className="t--widget-textwidget-truncate-modal"
hasBackDrop
isOpen={this.state.showModal}
onClose={this.handleModelClose}
overlayClassName="text-widget-truncate"
scrollContents
width={500}
>
<Interweave
content={text}
matchers={
disableLink
? []
: [new EmailMatcher("email"), new UrlMatcher("url")]
}
newWindow
/>
</StyledText>
</TextContainer>
<ModalContent>
<Heading>
<div className="title">Show More</div>
<Icon
className="icon"
name="cross"
onClick={this.handleModelClose}
size={IconSize.MEDIUM}
/>
</Heading>
<Content>
<Interweave
content={text}
matchers={
disableLink
? []
: [new EmailMatcher("email"), new UrlMatcher("url")]
}
newWindow
/>
</Content>
</ModalContent>
</ModalComponent>
</>
);
}
}

View File

@ -12,9 +12,12 @@ export const CONFIG = {
fontStyle: "BOLD",
textAlign: "LEFT",
textColor: "#231F20",
truncateButtonColor: "#FFC13D",
rows: 1 * GRID_DENSITY_MIGRATION_V1,
columns: 4 * GRID_DENSITY_MIGRATION_V1,
widgetName: "Text",
shouldScroll: false,
shouldTruncate: false,
version: 1,
animateLoading: true,
},

View File

@ -37,6 +37,14 @@ class TextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "shouldTruncate",
label: "Truncate Text",
helpText: "Set truncate text",
controlType: "SWITCH",
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "isVisible",
helpText: "Controls the visibility of the widget",
@ -106,6 +114,24 @@ class TextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
},
},
},
{
propertyName: "truncateButtonColor",
label: "Truncate Button Color",
controlType: "COLOR_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
regex: /^(?![<|{{]).+/,
},
},
dependencies: ["shouldTruncate"],
hidden: (props: TextWidgetProps) => {
return !props.shouldTruncate;
},
},
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
@ -239,15 +265,21 @@ class TextWidget extends BaseWidget<TextWidgetProps, WidgetState> {
>
<TextComponent
backgroundColor={this.props.backgroundColor}
bottomRow={this.props.bottomRow}
disableLink={this.props.disableLink || false}
fontSize={this.props.fontSize}
fontStyle={this.props.fontStyle}
isLoading={this.props.isLoading}
key={this.props.widgetId}
leftColumn={this.props.leftColumn}
rightColumn={this.props.rightColumn}
shouldScroll={this.props.shouldScroll}
shouldTruncate={this.props.shouldTruncate}
text={this.props.text}
textAlign={this.props.textAlign ? this.props.textAlign : "LEFT"}
textColor={this.props.textColor}
topRow={this.props.topRow}
truncateButtonColor={this.props.truncateButtonColor}
widgetId={this.props.widgetId}
/>
</WidgetStyleContainer>
@ -271,6 +303,7 @@ export interface TextStyles {
fontStyle?: string;
fontSize?: TextSize;
textAlign?: TextAlign;
truncateButtonColor?: string;
}
export interface TextWidgetProps
@ -280,6 +313,7 @@ export interface TextWidgetProps
text?: string;
isLoading: boolean;
shouldScroll: boolean;
shouldTruncate: boolean;
disableLink: boolean;
}