diff --git a/package-lock.json b/package-lock.json
index 35b71dec..48c4b97e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8104,7 +8104,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8320,11 +8319,23 @@
"utila": "~0.4"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -8344,6 +8355,33 @@
"node": ">=12"
}
},
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@@ -8543,7 +8581,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
"engines": {
"node": ">=0.12"
},
@@ -11244,6 +11281,24 @@
"webpack": "^5.20.0"
}
},
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
@@ -16515,6 +16570,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
+ },
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -16615,8 +16675,7 @@
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -18364,6 +18423,82 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "node_modules/sanitize-html": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
+ "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
+ "dependencies": {
+ "deepmerge": "^4.2.2",
+ "escape-string-regexp": "^4.0.0",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/postcss": {
+ "version": "8.4.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
+ "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -19138,7 +19273,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -20166,9 +20300,9 @@
}
},
"node_modules/tough-cookie": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
- "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+ "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"dependencies": {
"psl": "^1.1.33",
@@ -22318,7 +22452,8 @@
"dependencies": {
"@patternfly/react-core": "^5.0.0",
"@patternfly/react-icons": "^5.0.0",
- "react-jss": "^10.9.2"
+ "react-jss": "^10.9.2",
+ "sanitize-html": "^2.3.3"
},
"devDependencies": {
"@patternfly/documentation-framework": "^2.0.0-alpha.57",
diff --git a/package.json b/package.json
index 32a9295b..af1c3f79 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,9 @@
"lint:md:fix": "eslint packages --ext md --no-eslintrc --config .eslintrc-md.json --cache --fix",
"lint": "npm run lint:js && npm run lint:md",
"lint:fix": "npm run lint:js:fix && npm run lint:md:fix",
+ "serve:a11y": "npm run serve:a11y -w @patternfly/react-component-groups",
"test": "TZ=EST jest packages",
- "test:a11y": "npm run test:a11y -w @patternfly/react-component-groups",
- "serve:a11y": "npm run serve:a11y -w @patternfly/react-component-groups"
+ "test:a11y": "npm run test:a11y -w @patternfly/react-component-groups"
},
"devDependencies": {
"@babel/core": "^7.19.6",
diff --git a/packages/module/package.json b/packages/module/package.json
index f84a942e..c165ac16 100644
--- a/packages/module/package.json
+++ b/packages/module/package.json
@@ -16,8 +16,8 @@
"docs:build": "pf-docs-framework build all --output public",
"docs:serve": "pf-docs-framework serve public --port 5001",
"docs:screenshots": "pf-docs-framework screenshots --urlPrefix http://localhost:5001",
- "test:a11y": "patternfly-a11y --config patternfly-a11y.config",
- "serve:a11y": "serve coverage"
+ "serve:a11y": "serve coverage",
+ "test:a11y": "patternfly-a11y --config patternfly-a11y.config"
},
"repository": "git+https://github.com/patternfly/react-component-groups.git",
"author": "Red Hat",
@@ -33,7 +33,8 @@
"dependencies": {
"@patternfly/react-core": "^5.0.0",
"@patternfly/react-icons": "^5.0.0",
- "react-jss": "^10.9.2"
+ "react-jss": "^10.9.2",
+ "sanitize-html": "^2.3.3"
},
"peerDependencies": {
"react": "^17 || ^18",
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableText.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableText.md
new file mode 100644
index 00000000..6d6721c3
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableText.md
@@ -0,0 +1,53 @@
+---
+section: extensions
+subsection: Component groups
+id: Expandable Text
+source: react
+propComponents: ['ExpandableText', 'ExpandableTextCustomButtonProps']
+---
+
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+## Component usage
+
+The **expandable text** component enables you to display long text to users via a tooltip. It builds off of the [tooltip component](https://www.patternfly.org/components/tooltip) to truncate UI text in an element and display the truncated text in a tooltip.
+
+### Basic ExpandableText component
+
+This is an example of a basic ExpandableText component. It will truncate the text and display the full text in a tooltip. The button will be display under the text and will toggle the text.
+
+```js file="./ExpandableTextExample.tsx"
+
+```
+
+### ExpandableText component with inline text
+
+This is an example of a ExpandableText component with inline text. It will truncate the text and display the full text in a tooltip. The button will be display inline with the text and will toggle the text.
+
+```js file="./ExpandableTextInlineExample.tsx"
+
+```
+
+### ExpandableText component with custom button text
+
+This is an example of a ExpandableText component with custom button text. It will truncate the text and display the full text in a tooltip.
+
+```js file="./ExpandableTextCustomButtonExample.tsx"
+
+```
+
+### ExpandableText component with custom button
+
+This is an example of a ExpandableText component with a custom button. It will truncate the text and display the full text in a tooltip.
+
+```js file="./ExpandableTextCustomButtonExample.tsx"
+
+```
+
+### ExpandableText component with no button
+
+This is an example of a ExpandableText component with no button. It will truncate the text and display the full text in a tooltip. The text will be displayed on hover.
+
+```js file="ExpandableTextNoButtonExample.tsx"
+
+```
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomButtonExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomButtonExample.tsx
new file mode 100644
index 00000000..a2a4bf6b
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomButtonExample.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Button } from '@patternfly/react-core';
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+export const ExpandableTextCustomButtonExample: React.FunctionComponent = () => {
+ const expandButton =
+ const collapseButton =
+ return
+}
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomLinkButtonExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomLinkButtonExample.tsx
new file mode 100644
index 00000000..a3e76edb
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextCustomLinkButtonExample.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+export const ExpandableTextLinkButtonExample: React.FunctionComponent = () => ;
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextExample.tsx
new file mode 100644
index 00000000..b8fa021c
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextExample.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+export const BasicExample: React.FunctionComponent = () => ;
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextInlineExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextInlineExample.tsx
new file mode 100644
index 00000000..5b4c5928
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextInlineExample.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+export const InlineExample: React.FunctionComponent = () => ;
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextNoButtonExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextNoButtonExample.tsx
new file mode 100644
index 00000000..0d8a113f
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExpandableText/ExpandableTextNoButtonExample.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ExpandableText from "@patternfly/react-component-groups/dist/dynamic/ExpandableText";
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+export const ExpandableTextNoButtonExample: React.FunctionComponent = () => ;
diff --git a/packages/module/src/ExpandableText/ExpandableText.test.tsx b/packages/module/src/ExpandableText/ExpandableText.test.tsx
new file mode 100644
index 00000000..22e5a4c1
--- /dev/null
+++ b/packages/module/src/ExpandableText/ExpandableText.test.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import ExpandableText from './ExpandableText';
+
+const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.`;
+
+describe('ExpandableText component', () => {
+ describe('should render correctly', () => {
+ [ true, false ].forEach((isInline) => {
+ describe(isInline ? 'inline' : 'block', () => {
+ it('without length specified', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('with short length', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('clicking on expand toggles to collapse', () => {
+ render();
+ userEvent.click(screen.getByRole('button'));
+
+ expect(screen.getByRole('button')).toMatchSnapshot();
+ });
+
+ it('custom expanded button', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('custom button titles', () => {
+ render();
+ userEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('button')).toMatchSnapshot();
+ });
+
+ it('clicking on expand toggles to collapse', () => {
+ render();
+ userEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('button')).toMatchSnapshot();
+ });
+
+ it('custom expanded button', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('custom button titles', () => {
+ render();
+ userEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('button')).toMatchSnapshot();
+ });
+
+ it('when text length is less than user specified length', () => {
+ expect(render()).toMatchSnapshot();
+ });
+
+ it('hovering over toggles to collapse', () => {
+ const wrapper = render();
+ expect(wrapper).toMatchSnapshot();
+
+ const expandedText = screen.getByTestId('expandable-text-hover');
+
+ userEvent.hover(expandedText);
+ expect(screen.getByTestId('expandable-text-hover')).toMatchSnapshot();
+
+ userEvent.unhover(expandedText);
+ expect(screen.getByTestId('expandable-text-hover')).toMatchSnapshot();
+ });
+ });
+ });
+ });
+});
diff --git a/packages/module/src/ExpandableText/ExpandableText.tsx b/packages/module/src/ExpandableText/ExpandableText.tsx
new file mode 100644
index 00000000..fc03685c
--- /dev/null
+++ b/packages/module/src/ExpandableText/ExpandableText.tsx
@@ -0,0 +1,100 @@
+import React, { useState } from 'react';
+import clsx from 'clsx';
+import { Button, Stack, StackItem } from '@patternfly/react-core';
+import sanitizeHtml from 'sanitize-html';
+import { createUseStyles } from 'react-jss'
+
+const useStyles = createUseStyles({
+ expandableText: {
+ "& button": {
+ padding: "0"
+ },
+ "& is-inline": { marginRight: [ "10px", "0.625rem" ] }
+ } });
+
+const dangerousHtml = (html: string) => ({ __html: sanitizeHtml(html) });
+
+export interface ExpandableTextCustomButtonProps {
+ /** Allows a user to add a custom expand button */
+ expand: React.ReactNode;
+ /** Allows a user to add a custom collapse button */
+ collapse: React.ReactNode;
+}
+
+export interface ExpandableTextProps {
+ /** Additional classes added to the ExpandableText */
+ className?: string;
+ /** Text to display in the ExpandableText component */
+ text?: string;
+ /** A set length for the ExpandableText component. */
+ length?: number;
+ /** Indicates if the text should be kept inline */
+ inline?: boolean;
+ /** If set to true the expand text will be hidden */
+ hideExpandText?: boolean;
+ /** If set to true the text will be expanded when moused over */
+ expandOnMouseOver?: boolean;
+ /** Allows users to create custom expandable buttons */
+ customButton?: ExpandableTextCustomButtonProps;
+}
+
+const ExpandableText: React.FunctionComponent = ({
+ text = '',
+ length = 150,
+ hideExpandText = false,
+ expandOnMouseOver = false,
+ className,
+ inline,
+ customButton,
+ ...props
+}: ExpandableTextProps) => {
+ const classes = useStyles();
+ const expandableTextClasses = clsx(classes.expandableText, className, { [`is-inline`]: inline }, { [`is-block`]: !inline });
+ const trimmedText = text.substring(0, length);
+ const textOverflow = text.length > length;
+ const [ showText, setShowText ] = useState(false);
+ const toggleText = (event: React.MouseEvent) => {
+ event && event.preventDefault();
+ setShowText(!showText);
+ };
+
+ const cloneButton = (button: React.ReactNode) => React.isValidElement(button) ? React.cloneElement(button as React.ReactElement, { onClick: toggleText }) :
+ ;
+
+ const expandButton =
+ (customButton ? cloneButton(customButton.expand) : );
+ const collapseButton =
+ (customButton ? cloneButton(customButton.collapse) : );
+ const textWithOverflow = showText === false ? `${trimmedText}${textOverflow ? '...' : ''}` : text;
+ const html = dangerousHtml(textWithOverflow);
+ const mouseOverHandler = expandOnMouseOver && {
+ onMouseEnter: () => setShowText(true),
+ onMouseLeave: () => setShowText(false),
+ };
+
+ return inline ? (
+
+
+ {!hideExpandText && textOverflow && (showText === false ? expandButton : collapseButton)}
+
+ ) : (
+
+
+
+
+ {!hideExpandText && textOverflow &&
+
+ {showText === false ? expandButton : collapseButton}
+
+ }
+
+ );
+};
+
+export default ExpandableText;
diff --git a/packages/module/src/ExpandableText/__snapshots__/ExpandableText.test.tsx.snap b/packages/module/src/ExpandableText/__snapshots__/ExpandableText.test.tsx.snap
new file mode 100644
index 00000000..705356d0
--- /dev/null
+++ b/packages/module/src/ExpandableText/__snapshots__/ExpandableText.test.tsx.snap
@@ -0,0 +1,1345 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ExpandableText component should render correctly block clicking on expand toggles to collapse 1`] = `
+
+`;
+
+exports[`ExpandableText component should render correctly block clicking on expand toggles to collapse 2`] = `
+
+`;
+
+exports[`ExpandableText component should render correctly block custom button titles 1`] = `
+
+`;
+
+exports[`ExpandableText component should render correctly block custom button titles 2`] = `
+
+`;
+
+exports[`ExpandableText component should render correctly block custom expanded button 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+
+
+
+
+ ,
+ "container":
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+
+
+
+
+ ,
+ "container":
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing...
+
+
+
+`;
+
+exports[`ExpandableText component should render correctly block hovering over toggles to collapse 3`] = `
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing...
+
+
+
+`;
+
+exports[`ExpandableText component should render correctly block when text length is less than user specified length 1`] = `
+{
+ "asFragment": [Function],
+ "baseElement":
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.
+
+
+
+
+ ,
+ "container":
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+
+
+
+
+ ,
+ "container":
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ ,
+ "container":
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ ,
+ "container":
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.
+
+
+ ,
+ "container":
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+laborum.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+
+ ,
+ "container":
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
+et dolore magna aliqua. Ut enim ad minim veniam, q...
+
+
+