* 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:
parent
4163cbf92a
commit
a6a462cf68
65
app/client/cypress/fixtures/textNewDsl.json
Normal file
65
app/client/cypress/fixtures/textNewDsl.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() => {
|
||||
//
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user