diff --git a/app/client/cypress/fixtures/textNewDsl.json b/app/client/cypress/fixtures/textNewDsl.json new file mode 100644 index 0000000000..4bc0cf6b4c --- /dev/null +++ b/app/client/cypress/fixtures/textNewDsl.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_truncate_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_truncate_spec.js new file mode 100644 index 0000000000..82df5532a9 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Text_truncate_spec.js @@ -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(() => { + // + }); +}); diff --git a/app/client/src/widgets/TextWidget/component/index.tsx b/app/client/src/widgets/TextWidget/component/index.tsx index e9320ed29a..ad477535ad 100644 --- a/app/client/src/widgets/TextWidget/component/index.tsx +++ b/app/client/src/widgets/TextWidget/component/index.tsx @@ -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 { +type State = { + isTruncated: boolean; + showModal: boolean; +}; + +type TextRef = React.Ref | undefined; + +class TextComponent extends React.Component { + 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 { ellipsize, fontSize, fontStyle, + shouldScroll, + shouldTruncate, text, textAlign, textColor, + truncateButtonColor, } = this.props; + return ( - - + + + + + {this.state.isTruncated && ( + + )} + + - - - + + +
Show More
+ +
+ + + +
+ + ); } } diff --git a/app/client/src/widgets/TextWidget/index.ts b/app/client/src/widgets/TextWidget/index.ts index ee580747d5..de2296df45 100644 --- a/app/client/src/widgets/TextWidget/index.ts +++ b/app/client/src/widgets/TextWidget/index.ts @@ -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, }, diff --git a/app/client/src/widgets/TextWidget/widget/index.tsx b/app/client/src/widgets/TextWidget/widget/index.tsx index 72582e0249..f437bb2d81 100644 --- a/app/client/src/widgets/TextWidget/widget/index.tsx +++ b/app/client/src/widgets/TextWidget/widget/index.tsx @@ -37,6 +37,14 @@ class TextWidget extends BaseWidget { 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 { }, }, }, + { + 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 { > @@ -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; }