diff --git a/change/@ni-nimble-components-e476eedb-989a-4246-811b-039ca8a8352e.json b/change/@ni-nimble-components-e476eedb-989a-4246-811b-039ca8a8352e.json new file mode 100644 index 0000000000..8cb5af85fd --- /dev/null +++ b/change/@ni-nimble-components-e476eedb-989a-4246-811b-039ca8a8352e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Initial component bring up by integrating tiptap editor along with the footer formatting options", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/package-lock.json b/package-lock.json index 2c80b1b597..a38423354e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5876,6 +5876,54 @@ "node": ">=10.0.0" } }, + "node_modules/@remirror/core-constants": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz", + "integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==", + "peer": true + }, + "node_modules/@remirror/core-helpers": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-helpers/-/core-helpers-3.0.0.tgz", + "integrity": "sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==", + "peer": true, + "dependencies": { + "@remirror/core-constants": "^2.0.2", + "@remirror/types": "^1.0.1", + "@types/object.omit": "^3.0.0", + "@types/object.pick": "^1.3.2", + "@types/throttle-debounce": "^2.1.0", + "case-anything": "^2.1.13", + "dash-get": "^1.0.2", + "deepmerge": "^4.3.1", + "fast-deep-equal": "^3.1.3", + "make-error": "^1.3.6", + "object.omit": "^3.0.0", + "object.pick": "^1.3.0", + "throttle-debounce": "^3.0.1" + } + }, + "node_modules/@remirror/types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@remirror/types/-/types-1.0.1.tgz", + "integrity": "sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==", + "peer": true, + "dependencies": { + "type-fest": "^2.19.0" + } + }, + "node_modules/@remirror/types/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "peer": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", @@ -9428,6 +9476,160 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tiptap/core": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.4.tgz", + "integrity": "sha512-2YOMjRqoBGEP4YGgYpuPuBBJHMeqKOhLnS0WVwjVP84zOmMgZ7A8M6ILC9Xr7Q/qHZCvyBGWOSsI7+3HsEzzYQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.4.tgz", + "integrity": "sha512-CWSQy1uWkVsen8HUsqhm+oEIxJrCiCENABUbhaVcJL/MqhnP4Trrh1B6O00Yfoc0XToPRRibDaHMFs4A3MSO0g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.4.tgz", + "integrity": "sha512-JSZKBVTaKSuLl5fR4EKE4dOINOrgeRHYA25Vj6cWjgdvpTw5ef7vcUdn9yP4JwTmLRI+VnnMlYL3rqigU3iZNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.4.tgz", + "integrity": "sha512-mCj2fAhnNhIHttPSqfTPSSTGwClGaPYvhT56Ij/Pi4iCrWjPXzC4XnIkIHSS34qS2tJN4XJzr/z7lm3NeLkF1w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.4.tgz", + "integrity": "sha512-3GAUszn1xZx3vniHMiX9BSKmfvb5QOb0oSLXInN+hx80CgJDIHqIFuhx2dyV9I/HWpa0cTxaLWj64kfDzb1JVg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.4.tgz", + "integrity": "sha512-C/6+qs4Jh8xERRP0wcOopA1+emK8MOkBE4RQx5NbPnT2iCpERP0GlmHBFQIjaYPctZgKFHxsCfRnneS5Xe76+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.4.tgz", + "integrity": "sha512-tSkbLgRo1QMNDJttWs9FeRywkuy5T2HdLKKfUcUNzT3s0q5AqIJl7VyimsBL4A6MUfN1qQMZCMHB4pM9Mkluww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.4.tgz", + "integrity": "sha512-Kfg+8k9p4iJCUKP/yIa18LfUpl9trURSMP/HX3/yQTz9Ul1vDrjxeFjSE5uWNvupcXRAM24js+aYrCmV7zpU+Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.4.tgz", + "integrity": "sha512-nDxpopi9WigVqpfi8nU3B0fWYB14EMvKIkutNZo8wJvKGTZufNI8hw66wupIx/jZH1gFxEa5dHerw6aSYuWjgQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.4.tgz", + "integrity": "sha512-i8/VFlVZh7TkAI49KKX5JmC0tM8RGwyg5zUpozxYbLdCOv07AkJt+E1fLJty9mqH4Y5HJMNnyNxsuZ9Ol/ySRA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.0.4.tgz", + "integrity": "sha512-DNgxntpEaiW7ciW0BTNTL0TFqAreZTrAROWakI4XaYRAyi5H9NfZW8jmwGwMBkoZ1KB3pfy+jT/Bisy4okEQGQ==", + "peer": true, + "dependencies": { + "prosemirror-changeset": "^2.2.0", + "prosemirror-collab": "^1.3.0", + "prosemirror-commands": "^1.3.1", + "prosemirror-dropcursor": "^1.5.0", + "prosemirror-gapcursor": "^1.3.1", + "prosemirror-history": "^1.3.0", + "prosemirror-inputrules": "^1.2.0", + "prosemirror-keymap": "^1.2.0", + "prosemirror-markdown": "^1.10.1", + "prosemirror-menu": "^1.2.1", + "prosemirror-model": "^1.18.1", + "prosemirror-schema-basic": "^1.2.0", + "prosemirror-schema-list": "^1.2.2", + "prosemirror-state": "^1.4.1", + "prosemirror-tables": "^1.3.0", + "prosemirror-trailing-node": "^2.0.2", + "prosemirror-transform": "^1.7.0", + "prosemirror-view": "^1.28.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -9847,6 +10049,18 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==", + "peer": true + }, + "node_modules/@types/object.pick": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", + "integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==", + "peer": true + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -9957,6 +10171,12 @@ "@types/node": "*" } }, + "node_modules/@types/throttle-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", + "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", + "peer": true + }, "node_modules/@types/unist": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", @@ -12647,6 +12867,18 @@ "upper-case-first": "^2.0.2" } }, + "node_modules/case-anything": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz", + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==", + "peer": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -13641,6 +13873,12 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "peer": true + }, "node_modules/critters": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", @@ -14141,6 +14379,12 @@ "node": ">=12" } }, + "node_modules/dash-get": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dash-get/-/dash-get-1.0.2.tgz", + "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==", + "peer": true + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -14273,7 +14517,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" } @@ -16645,8 +16888,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.0", @@ -18925,7 +19167,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -19166,7 +19407,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -21722,6 +21962,12 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "peer": true + }, "node_modules/make-fetch-happen": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", @@ -24037,6 +24283,42 @@ "node": ">= 0.4" } }, + "node_modules/object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", + "peer": true, + "dependencies": { + "is-extendable": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.omit/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -26399,6 +26681,90 @@ "react-is": "^16.13.1" } }, + "node_modules/prosemirror-changeset": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz", + "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==", + "peer": true, + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz", + "integrity": "sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz", + "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "peer": true, + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.2.tgz", + "integrity": "sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz", + "integrity": "sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", + "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, "node_modules/prosemirror-markdown": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.0.tgz", @@ -26447,6 +26813,18 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/prosemirror-menu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.2.tgz", + "integrity": "sha512-437HIWTq4F9cTX+kPfqZWWm+luJm95Aut/mLUy+9OMrOml0bmWDS26ceC6SNfb2/S94et1sZ186vLO7pDHzxSw==", + "peer": true, + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, "node_modules/prosemirror-model": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", @@ -26455,6 +26833,98 @@ "orderedmap": "^2.0.0" } }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz", + "integrity": "sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.19.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz", + "integrity": "sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.4.tgz", + "integrity": "sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==", + "peer": true, + "dependencies": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.7.tgz", + "integrity": "sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==", + "peer": true, + "dependencies": { + "@remirror/core-constants": "^2.0.2", + "@remirror/core-helpers": "^3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.19.0", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.31.2" + } + }, + "node_modules/prosemirror-trailing-node/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==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.4.tgz", + "integrity": "sha512-GO38mvqJ2yeI0BbL5E1CdHcly032Dlfn9nHqlnCHqlNf9e9jZwJixxp6VRtOeDZ1uTDpDIziezMKbA41LpAx3A==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.31.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.7.tgz", + "integrity": "sha512-Pr7w93yOYmxQwzGIRSaNLZ/1uM6YjnenASzN2H6fO6kGekuzRbgZ/4bHbBTd1u4sIQmL33/TcGmzxxidyPwCjg==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -28048,6 +28518,12 @@ "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "dev": true }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "peer": true + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -30340,6 +30816,15 @@ "integrity": "sha512-9d/OfjEOjyeOpnm4F9o0KSK6BI6ytvi9DINSB5h1+jdlCvQlhKpViMSxWpBN9WstdfDQ61BS6NxWqcPCuQCAJg==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -31624,6 +32109,12 @@ "eslint": ">=6.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "peer": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -32834,6 +33325,16 @@ "@ni/nimble-tokens": "^6.3.0", "@tanstack/table-core": "^8.9.3", "@tanstack/virtual-core": "^3.0.0-beta.44", + "@tiptap/core": "^2.0.4", + "@tiptap/extension-bold": "^2.0.4", + "@tiptap/extension-bullet-list": "^2.0.4", + "@tiptap/extension-document": "^2.0.4", + "@tiptap/extension-history": "^2.0.4", + "@tiptap/extension-italic": "^2.0.4", + "@tiptap/extension-list-item": "^2.0.4", + "@tiptap/extension-ordered-list": "^2.0.4", + "@tiptap/extension-paragraph": "^2.0.4", + "@tiptap/extension-text": "^2.0.4", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index 548935eb6c..dda043247a 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -64,6 +64,16 @@ "@ni/nimble-tokens": "^6.3.0", "@tanstack/table-core": "^8.9.3", "@tanstack/virtual-core": "^3.0.0-beta.44", + "@tiptap/core": "^2.0.4", + "@tiptap/extension-bold": "^2.0.4", + "@tiptap/extension-bullet-list": "^2.0.4", + "@tiptap/extension-document": "^2.0.4", + "@tiptap/extension-history": "^2.0.4", + "@tiptap/extension-italic": "^2.0.4", + "@tiptap/extension-list-item": "^2.0.4", + "@tiptap/extension-ordered-list": "^2.0.4", + "@tiptap/extension-paragraph": "^2.0.4", + "@tiptap/extension-text": "^2.0.4", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", diff --git a/packages/nimble-components/src/rich-text-editor/index.ts b/packages/nimble-components/src/rich-text-editor/index.ts index 863af54329..3c6687ae68 100644 --- a/packages/nimble-components/src/rich-text-editor/index.ts +++ b/packages/nimble-components/src/rich-text-editor/index.ts @@ -1,6 +1,19 @@ +import { observable } from '@microsoft/fast-element'; import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; +import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; +import { Editor } from '@tiptap/core'; +import Bold from '@tiptap/extension-bold'; +import BulletList from '@tiptap/extension-bullet-list'; +import Document from '@tiptap/extension-document'; +import History from '@tiptap/extension-history'; +import Italic from '@tiptap/extension-italic'; +import ListItem from '@tiptap/extension-list-item'; +import OrderedList from '@tiptap/extension-ordered-list'; +import Paragraph from '@tiptap/extension-paragraph'; +import Text from '@tiptap/extension-text'; import { template } from './template'; import { styles } from './styles'; +import type { ToggleButton } from '../toggle-button'; declare global { interface HTMLElementTagNameMap { @@ -11,7 +24,199 @@ declare global { /** * A nimble styled rich text editor */ -export class RichTextEditor extends FoundationElement {} +export class RichTextEditor extends FoundationElement { + /** + * @internal + */ + @observable + public boldButton!: ToggleButton; + + /** + * @internal + */ + @observable + public italicsButton!: ToggleButton; + + /** + * @internal + */ + @observable + public bulletListButton!: ToggleButton; + + /** + * @internal + */ + @observable + public numberedListButton!: ToggleButton; + + /** + * @internal + */ + public editor!: HTMLDivElement; + + private tiptapEditor!: Editor; + + /** + * @internal + */ + public override connectedCallback(): void { + super.connectedCallback(); + this.initializeEditor(); + this.bindEditorTransactionEvent(); + } + + /** + * @internal + */ + public override disconnectedCallback(): void { + super.disconnectedCallback(); + this.unbindEditorTransactionEvent(); + } + + /** + * Toggle the bold mark and focus back to the editor + * @internal + */ + public boldButtonClick(): void { + this.tiptapEditor.chain().focus().toggleBold().run(); + } + + /** + * Toggle the bold mark and focus back to the editor + * @internal + */ + public boldButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleBold().run(); + return false; + } + return true; + } + + /** + * Toggle the italics mark and focus back to the editor + * @internal + */ + public italicsButtonClick(): void { + this.tiptapEditor.chain().focus().toggleItalic().run(); + } + + /** + * Toggle the italics mark and focus back to the editor + * @internal + */ + public italicsButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleItalic().run(); + return false; + } + return true; + } + + /** + * Toggle the unordered list node and focus back to the editor + * @internal + */ + public bulletListButtonClick(): void { + this.tiptapEditor.chain().focus().toggleBulletList().run(); + } + + /** + * Toggle the unordered list node and focus back to the editor + * @internal + */ + public bulletListButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleBulletList().run(); + return false; + } + return true; + } + + /** + * Toggle the ordered list node and focus back to the editor + * @internal + */ + public numberedListButtonClick(): void { + this.tiptapEditor.chain().focus().toggleOrderedList().run(); + } + + /** + * Toggle the ordered list node and focus back to the editor + * @internal + */ + public numberedListButtonKeyDown(event: KeyboardEvent): boolean { + if (this.keyActivatesButton(event)) { + this.tiptapEditor.chain().focus().toggleOrderedList().run(); + return false; + } + return true; + } + + /** + * @internal + */ + public stopEventPropagation(event: Event): boolean { + // Don't bubble the 'change' event from the toggle button because + // all the formatting button has its own 'toggle' event through 'click' and 'keydown'. + event.stopPropagation(); + return false; + } + + private initializeEditor(): void { + /** + * For more information on the extensions for the supported formatting options, refer to the links below. + * Tiptap marks: https://tiptap.dev/api/marks + * Tiptap nodes: https://tiptap.dev/api/nodes + */ + this.tiptapEditor = new Editor({ + element: this.editor, + extensions: [ + Document, + Paragraph, + Text, + BulletList, + OrderedList, + ListItem, + Bold, + Italic, + History + ] + }); + } + + /** + * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to + * various actions such as mouse events, keyboard events, changes in the editor content etc,. + * https://tiptap.dev/api/events#transaction + */ + private bindEditorTransactionEvent(): void { + this.tiptapEditor.on('transaction', () => { + this.updateEditorButtonsState(); + }); + } + + private unbindEditorTransactionEvent(): void { + this.tiptapEditor.off('transaction'); + } + + private updateEditorButtonsState(): void { + this.boldButton.checked = this.tiptapEditor.isActive('bold'); + this.italicsButton.checked = this.tiptapEditor.isActive('italic'); + this.bulletListButton.checked = this.tiptapEditor.isActive('bulletList'); + this.numberedListButton.checked = this.tiptapEditor.isActive('orderedList'); + } + + private keyActivatesButton(event: KeyboardEvent): boolean { + switch (event.key) { + case keySpace: + case keyEnter: + return true; + default: + return false; + } + } +} const nimbleRichTextEditor = RichTextEditor.compose({ baseName: 'rich-text-editor', diff --git a/packages/nimble-components/src/rich-text-editor/specs/README.md b/packages/nimble-components/src/rich-text-editor/specs/README.md index fa1b311188..d5928ee243 100644 --- a/packages/nimble-components/src/rich-text-editor/specs/README.md +++ b/packages/nimble-components/src/rich-text-editor/specs/README.md @@ -422,7 +422,15 @@ This component is dependent on the [`tiptap`](https://tiptap.dev/) third party l library. For the currently supported features, we will include the following libraries that will be added to the package.json - [@tiptap/core](https://www.npmjs.com/package/@tiptap/core) -- [@tiptap/starter-kit](https://www.npmjs.com/package/@tiptap/starter-kit) +- [@tiptap/extension-bold](https://www.npmjs.com/package/@tiptap/extension-bold) +- [@tiptap/extension-bullet-list](https://www.npmjs.com/package/@tiptap/extension-bullet-list) +- [@tiptap/extension-document](https://www.npmjs.com/package/@tiptap/extension-document) +- [@tiptap/extension-history](https://www.npmjs.com/package/@tiptap/extension-history) +- [@tiptap/extension-italic](https://www.npmjs.com/package/@tiptap/extension-italic) +- [@tiptap/extension-list-item](https://www.npmjs.com/package/@tiptap/extension-list-item) +- [@tiptap/extension-ordered-list](https://www.npmjs.com/package/@tiptap/extension-ordered-list) +- [@tiptap/extension-paragraph](https://www.npmjs.com/package/@tiptap/extension-paragraph) +- [@tiptap/extension-text](https://www.npmjs.com/package/@tiptap/extension-text) - [@tiptap/extension-placeholder](https://www.npmjs.com/package/@tiptap/extension-placeholder) - [prosemirror-markdown](https://www.npmjs.com/package/prosemirror-markdown) - [prosemirror-model](https://www.npmjs.com/package/prosemirror-model) diff --git a/packages/nimble-components/src/rich-text-editor/styles.ts b/packages/nimble-components/src/rich-text-editor/styles.ts index a47a813379..2a14c19c84 100644 --- a/packages/nimble-components/src/rich-text-editor/styles.ts +++ b/packages/nimble-components/src/rich-text-editor/styles.ts @@ -1,12 +1,163 @@ import { css } from '@microsoft/fast-element'; import { display } from '@microsoft/fast-foundation'; -import { bodyFont, bodyFontColor } from '../theme-provider/design-tokens'; +import { + bodyFont, + bodyFontColor, + borderHoverColor, + borderRgbPartialColor, + borderWidth, + smallDelay, + standardPadding +} from '../theme-provider/design-tokens'; export const styles = css` - ${display('flex')} + ${display('inline-flex')} :host { font: ${bodyFont}; + outline: none; color: ${bodyFontColor}; + flex-direction: column; + --ni-private-rich-text-editor-hover-indicator-width: calc( + ${borderWidth} + 1px + ); + --ni-private-rich-text-editor-footer-section-height: 40px; + ${ + /** Minimum width is added to accommodate all the possible buttons in the toolbar and to support the mobile width. */ '' + } + min-width: 360px; + } + + .container { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + border: ${borderWidth} solid rgba(${borderRgbPartialColor}, 0.3); + } + + .container::after { + display: block; + content: ' '; + position: absolute; + bottom: calc(-1 * ${borderWidth}); + width: 0px; + height: 0px; + left: 50%; + transform: translate(-50%, 50%); + border-bottom: ${borderHoverColor} + var(--ni-private-rich-text-editor-hover-indicator-width) solid; + transition: width ${smallDelay} ease-in; + } + + .container:focus-within { + border-bottom-color: ${borderHoverColor}; + } + + @media (prefers-reduced-motion) { + .container::after { + transition-duration: 0s; + } + } + + :host(:hover) .container::after { + width: 100%; + } + + .editor { + border: ${borderWidth} solid transparent; + border-radius: 0px; + height: calc( + 100% - var(--ni-private-rich-text-editor-footer-section-height) + ); + overflow: auto; + } + + .ProseMirror { + ${ + /** + * Min height represents the one line space for the initial view and max height is referred from the visual design. + * However, max height will be `fit-content` when the `fit-to-content` attribute for the editor component is implemented. + */ '' + } + min-height: 32px; + max-height: 132px; + height: 100%; + border: ${borderWidth} solid transparent; + border-radius: 0px; + background-color: transparent; + font: inherit; + padding: 8px; + box-sizing: border-box; + position: relative; + color: inherit; + } + + ${ + /** + * Below are the styles from prosemirror-view as the Prose mirror expects the "white-space" to be set. The recommendation is to load the style from the below package. + * However, the other classes used in the below file like ".ProseMirror-selectednode", ".ProseMirror-hideselection" were not used anywhere in the ".Prosemirror" content editable div in the DOM. So added only the necessary styles below. + * + * https://github.com/ProseMirror/prosemirror-view/blob/db2223a88b540a8f381fc2720198342e29a60566/style/prosemirror.css#L5C1-L12C2 + */ '' + } + .ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: 'liga' 0; + } + + .ProseMirror pre { + white-space: pre-wrap; + } + + .ProseMirror li { + position: relative; + } + + ${/** End of Prose Mirror defined styles from prosemirror-view */ ''} + + .ProseMirror-focused { + outline: none; + } + + .ProseMirror > :first-child { + margin-block-start: 0; + } + + .ProseMirror > :last-child { + margin-block-end: 0; + } + + li > p { + margin-block: 0; + } + + .footer-section { + display: flex; + justify-content: space-between; + border: ${borderWidth} solid transparent; + border-top-color: rgba(${borderRgbPartialColor}, 0.1); + height: var(--ni-private-rich-text-editor-footer-section-height); + overflow: hidden; + } + + nimble-toolbar::part(positioning-region) { + background: transparent; + padding-right: 8px; + } + + nimble-toolbar::part(start) { + gap: 8px; + } + + .footer-actions { + display: flex; + justify-content: flex-end; + margin-inline-end: ${standardPadding}; + gap: ${standardPadding}; + place-items: center; } `; diff --git a/packages/nimble-components/src/rich-text-editor/template.ts b/packages/nimble-components/src/rich-text-editor/template.ts index 5538fe9226..4cf48755a7 100644 --- a/packages/nimble-components/src/rich-text-editor/template.ts +++ b/packages/nimble-components/src/rich-text-editor/template.ts @@ -1,6 +1,81 @@ -import { html } from '@microsoft/fast-element'; +import { html, ref } from '@microsoft/fast-element'; import type { RichTextEditor } from '.'; +import { toolbarTag } from '../toolbar'; +import { toggleButtonTag } from '../toggle-button'; +import { iconBoldBTag } from '../icons/bold-b'; +import { iconItalicITag } from '../icons/italic-i'; +import { iconListTag } from '../icons/list'; +import { iconNumberListTag } from '../icons/number-list'; +// prettier-ignore export const template = html` - + `; diff --git a/packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts new file mode 100644 index 0000000000..131ae19cba --- /dev/null +++ b/packages/nimble-components/src/rich-text-editor/testing/rich-text-editor.pageobject.ts @@ -0,0 +1,176 @@ +import { keySpace, keyEnter, keyTab } from '@microsoft/fast-web-utilities'; +import type { RichTextEditor } from '..'; +import { waitForUpdatesAsync } from '../../testing/async-helpers'; +import type { ToggleButton } from '../../toggle-button'; + +/** + * Page object for the `nimble-rich-text-editor` component. + */ +export class RichTextEditorPageObject { + public constructor( + private readonly richTextEditorElement: RichTextEditor + ) {} + + public editorSectionHasChildNodes(): boolean { + const editorSection = this.getEditorSection(); + return editorSection!.hasChildNodes(); + } + + public getEditorSectionFirstElementChildClassName(): string { + const editorSection = this.getEditorSection(); + return editorSection!.firstElementChild!.className; + } + + public async clickEditorShortcutKeys( + shortcutKey: string, + isShiftKey: boolean + ): Promise { + const editor = this.getTiptapEditor(); + const event = new KeyboardEvent('keydown', { + key: shortcutKey, + ctrlKey: true, + shiftKey: isShiftKey, + bubbles: true, + cancelable: true + }); + editor!.dispatchEvent(event); + await waitForUpdatesAsync(); + } + + public async pressEnterKeyInEditor(): Promise { + const editor = this.getTiptapEditor(); + const event = new KeyboardEvent('keydown', { + key: keyEnter, + bubbles: true, + cancelable: true + }); + editor!.dispatchEvent(event); + await waitForUpdatesAsync(); + } + + public async pressTabKeyInEditor(): Promise { + const editor = this.getTiptapEditor(); + const event = new KeyboardEvent('keydown', { + key: keyTab, + bubbles: true, + cancelable: true + }); + editor!.dispatchEvent(event); + await waitForUpdatesAsync(); + } + + public async pressShiftTabKeysInEditor(): Promise { + const editor = this.getTiptapEditor(); + const shiftTabEvent = new KeyboardEvent('keydown', { + key: keyTab, + shiftKey: true, + bubbles: true, + cancelable: true + }); + editor!.dispatchEvent(shiftTabEvent); + await waitForUpdatesAsync(); + } + + /** + * To click a formatting button in the footer section, pass its position value as an index (starting from '0') + * @param buttonIndex can be imported from an enum for each button using the `ButtonIndex`. + */ + public async clickFooterButton(buttonIndex: number): Promise { + const button = this.getFormattingButton(buttonIndex); + button!.click(); + await waitForUpdatesAsync(); + } + + /** + * To retrieve the checked state of the button, provide its position value as an index (starting from '0') + * @param buttonIndex can be imported from an enum for each button using the `ButtonIndex`. + */ + public getButtonCheckedState(buttonIndex: number): boolean { + const button = this.getFormattingButton(buttonIndex); + return button!.checked; + } + + /** + * To retrieve the tab index of the button, provide its position value as an index (starting from '0') + * @param buttonIndex can be imported from an enum for each button using the `ButtonIndex`. + */ + public getButtonTabIndex(buttonIndex: number): number { + const button = this.getFormattingButton(buttonIndex); + return button!.tabIndex; + } + + /** + * To trigger a space key press for the button, provide its position value as an index (starting from '0') + * @param buttonIndex can be imported from an enum for each button using the `ButtonIndex`. + */ + public spaceKeyActivatesButton(buttonIndex: number): void { + const button = this.getFormattingButton(buttonIndex)!; + const event = new KeyboardEvent('keypress', { + key: keySpace + } as KeyboardEventInit); + button.control.dispatchEvent(event); + } + + /** + * To trigger a enter key press for the button, provide its position value as an index (starting from '0') + * @param buttonIndex can be imported from an enum for each button using the `ButtonIndex`. + */ + public enterKeyActivatesButton(buttonIndex: number): void { + const button = this.getFormattingButton(buttonIndex)!; + const event = new KeyboardEvent('keypress', { + key: keyEnter + } as KeyboardEventInit); + button.control.dispatchEvent(event); + } + + public async setEditorTextContent(value: string): Promise { + let lastElement = this.getTiptapEditor()?.lastElementChild; + + while (lastElement?.lastElementChild) { + lastElement = lastElement?.lastElementChild; + } + lastElement!.parentElement!.textContent = value; + await waitForUpdatesAsync(); + } + + public getEditorFirstChildTagName(): string { + return this.getTiptapEditor()?.firstElementChild?.tagName ?? ''; + } + + public getEditorFirstChildTextContent(): string { + return this.getTiptapEditor()?.firstElementChild?.textContent ?? ''; + } + + public getEditorTagNames(): string[] { + return Array.from(this.getTiptapEditor()!.querySelectorAll('*')).map( + el => el.tagName + ); + } + + public getEditorLeafContents(): string[] { + return Array.from(this.getTiptapEditor()!.querySelectorAll('*')) + .filter((el, _) => { + return el.children.length === 0; + }) + .map(el => el.textContent || ''); + } + + private getEditorSection(): Element | null | undefined { + return this.richTextEditorElement.shadowRoot?.querySelector('.editor'); + } + + private getTiptapEditor(): Element | null | undefined { + return this.richTextEditorElement.shadowRoot?.querySelector( + '.ProseMirror' + ); + } + + private getFormattingButton( + index: number + ): ToggleButton | null | undefined { + const buttons: NodeListOf = this.richTextEditorElement.shadowRoot!.querySelectorAll( + 'nimble-toggle-button' + ); + return buttons[index]; + } +} diff --git a/packages/nimble-components/src/rich-text-editor/testing/types.ts b/packages/nimble-components/src/rich-text-editor/testing/types.ts new file mode 100644 index 0000000000..4060a4ed4b --- /dev/null +++ b/packages/nimble-components/src/rich-text-editor/testing/types.ts @@ -0,0 +1,7 @@ +export const ToolbarButton = { + bold: 0, + italics: 1, + bulletList: 2, + numberedList: 3 +} as const; +export type ToolbarButton = (typeof ToolbarButton)[keyof typeof ToolbarButton]; diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts index c71444c76d..799b31fba4 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor-matrix.stories.ts @@ -10,6 +10,11 @@ import { } from '../../utilities/tests/matrix'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { richTextEditorTag } from '..'; +import { + cssPropertyFromTokenName, + tokenNames +} from '../../theme-provider/design-token-names'; +import { buttonTag } from '../../button'; const metadata: Meta = { title: 'Tests/Rich Text Editor', @@ -25,10 +30,40 @@ const component = (): ViewTemplate => html` <${richTextEditorTag}> `; +const editorSizingTestCase = ( + [widthLabel, widthStyle]: [string, string], + [heightLabel, heightStyle]: [string, string] +): ViewTemplate => html` +

${widthLabel}; ${heightLabel}

+
+ <${richTextEditorTag} style="${widthStyle}; ${heightStyle};"> + <${buttonTag} slot="footer-actions" appearance="ghost">Cancel + <${buttonTag} slot="footer-actions" appearance="outline">Ok + +
+`; + export const richTextEditorThemeMatrix: StoryFn = createMatrixThemeStory( createMatrix(component) ); +export const richTextEditorSizing: StoryFn = createStory(html` + ${createMatrix(editorSizingTestCase, [ + [ + ['No width', ''], + ['Width 450px', 'width: 450px'], + ['Width 100%', 'width: 100%'] + ], + [ + ['No height', ''], + ['Height 100px', 'height: 100px'], + ['Height 100%', 'height: 100%'] + ] + ])} +`); + export const hiddenRichTextEditor: StoryFn = createStory( hiddenWrapper(html`<${richTextEditorTag} hidden>`) ); diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts index 353d235bec..2a4742e16e 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.spec.ts @@ -1,6 +1,39 @@ -import { RichTextEditor, richTextEditorTag } from '..'; +import { html } from '@microsoft/fast-element'; +import { richTextEditorTag, RichTextEditor } from '..'; +import { type Fixture, fixture } from '../../utilities/tests/fixture'; +import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; +import { RichTextEditorPageObject } from '../testing/rich-text-editor.pageobject'; +import { wackyStrings } from '../../utilities/tests/wacky-strings'; +import type { Button } from '../../button'; +import type { ToggleButton } from '../../toggle-button'; +import { ToolbarButton } from '../testing/types'; + +async function setup(): Promise> { + return fixture( + // prettier-ignore + html` + Cancel + OK +` + ); +} describe('RichTextEditor', () => { + let element: RichTextEditor; + let connect: () => Promise; + let disconnect: () => Promise; + let pageObject: RichTextEditorPageObject; + + beforeEach(async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + pageObject = new RichTextEditorPageObject(element); + }); + + afterEach(async () => { + await disconnect(); + }); + it('can construct an element instance', () => { expect( document.createElement('nimble-rich-text-editor') @@ -10,4 +43,585 @@ describe('RichTextEditor', () => { it('should export its tag', () => { expect(richTextEditorTag).toBe('nimble-rich-text-editor'); }); + + it('should initialize Tiptap editor', () => { + expect(pageObject.editorSectionHasChildNodes()).toBeTrue(); + expect(pageObject.getEditorSectionFirstElementChildClassName()).toBe( + 'ProseMirror' + ); + }); + + it('should set aria role as "textbox"', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + + expect(editor!.getAttribute('role')).toBe('textbox'); + }); + + it('should set aria-multiline to true', () => { + const editor = element.shadowRoot?.querySelector('.editor'); + + expect(editor!.getAttribute('aria-multiline')).toBe('true'); + }); + + it('should have either one of the list buttons checked at the same time on click', async () => { + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeFalse(); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeFalse(); + + await pageObject.clickFooterButton(ToolbarButton.bulletList); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeTrue(); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeFalse(); + + await pageObject.clickFooterButton(ToolbarButton.numberedList); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeFalse(); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeTrue(); + }); + + it('clicking buttons in the slot element should call the click event once', () => { + const cancelButton: Button = element.querySelector('#cancel')!; + const okButton: Button = element.querySelector('#ok')!; + const cancelButtonSpy = jasmine.createSpy(); + const okButtonSpy = jasmine.createSpy(); + cancelButton?.addEventListener('click', cancelButtonSpy); + okButton?.addEventListener('click', okButtonSpy); + + cancelButton.click(); + okButton.click(); + + expect(cancelButtonSpy).toHaveBeenCalledTimes(1); + expect(okButtonSpy).toHaveBeenCalledTimes(1); + }); + + const formattingButtons: { + name: string, + toolbarButtonIndex: number, + iconName: string, + shortcutKey: string, + shiftKey: boolean + }[] = [ + { + name: 'bold', + toolbarButtonIndex: ToolbarButton.bold, + iconName: 'NIMBLE-ICON-BOLD-B', + shortcutKey: 'b', + shiftKey: false + }, + { + name: 'italics', + toolbarButtonIndex: ToolbarButton.italics, + iconName: 'NIMBLE-ICON-ITALIC-I', + shortcutKey: 'i', + shiftKey: false + }, + { + name: 'bullet-list', + toolbarButtonIndex: ToolbarButton.bulletList, + iconName: 'NIMBLE-ICON-LIST', + shortcutKey: '8', + shiftKey: true + }, + { + name: 'numbered-list', + toolbarButtonIndex: ToolbarButton.numberedList, + iconName: 'NIMBLE-ICON-NUMBER-LIST', + shortcutKey: '7', + shiftKey: true + } + ]; + + describe('clicking buttons should update the checked state of the toggle button with focus', () => { + const focused: string[] = []; + const disabled: string[] = []; + + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `"${value.name}" button click check`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeFalse(); + + await pageObject.clickFooterButton( + value.toolbarButtonIndex + ); + + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeTrue(); + expect( + pageObject.getButtonTabIndex(value.toolbarButtonIndex) + ).toBe(0); + } + ); + } + }); + + describe('space key press should update the checked state of the buttons', () => { + const focused: string[] = []; + const disabled: string[] = []; + + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `"${value.name}" button key press check`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeFalse(); + + pageObject.spaceKeyActivatesButton( + value.toolbarButtonIndex + ); + + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeTrue(); + } + ); + } + }); + + describe('enter key press should update the checked state of the buttons', () => { + const focused: string[] = []; + const disabled: string[] = []; + + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `"${value.name}" button key press check`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeFalse(); + + pageObject.enterKeyActivatesButton( + value.toolbarButtonIndex + ); + + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeTrue(); + } + ); + } + }); + + describe('keyboard shortcuts should update the checked state of the buttons', () => { + const focused: string[] = []; + const disabled: string[] = []; + + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `"${value.name}" button keyboard shortcut check`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeFalse(); + + await pageObject.clickEditorShortcutKeys( + value.shortcutKey, + value.shiftKey + ); + + expect( + pageObject.getButtonCheckedState( + value.toolbarButtonIndex + ) + ).toBeTrue(); + } + ); + } + }); + + describe('should not leak change event through shadow DOM for buttons', () => { + const focused: string[] = []; + const disabled: string[] = []; + + for (const value of formattingButtons) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `"${value.name}" button not propagate change event to parent element`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const buttons: NodeListOf = element.shadowRoot!.querySelectorAll( + 'nimble-toggle-button' + ); + const button = buttons[value.toolbarButtonIndex]; + const buttonParent = button!.parentElement; + const spy = jasmine.createSpy(); + const event = new Event('change', { bubbles: true }); + + buttonParent?.addEventListener('change', spy); + button!.dispatchEvent(event); + + expect(spy).toHaveBeenCalledTimes(0); + } + ); + } + }); + + describe('rich text formatting options to its respective HTML elements', () => { + it('should have "strong" tag name for bold button click', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.setEditorTextContent('bold'); + + expect(pageObject.getEditorTagNames()).toEqual(['P', 'STRONG']); + expect(pageObject.getEditorLeafContents()).toEqual(['bold']); + }); + + it('should have "em" tag name for italics button click', async () => { + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent('italics'); + + expect(pageObject.getEditorTagNames()).toEqual(['P', 'EM']); + expect(pageObject.getEditorLeafContents()).toEqual(['italics']); + }); + + it('should have "ol" tag name for numbered list button click', async () => { + await pageObject.setEditorTextContent('numbered list'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + + expect(pageObject.getEditorTagNames()).toEqual(['OL', 'LI', 'P']); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'numbered list' + ]); + }); + + it('should have multiple "ol" tag names for numbered list button click', async () => { + await pageObject.setEditorTextContent('numbered list 1'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.setEditorTextContent('numbered list 2'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'numbered list 1', + 'numbered list 2' + ]); + }); + + it('should have "ol" tag names for nested numbered lists when clicking "tab"', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.setEditorTextContent('Nested List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'OL', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'List', + 'Nested List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeTrue(); + }); + + it('should have "ol" tag names for numbered lists when clicking "tab" to make it nested and "shift+Tab" to make it usual list', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.setEditorTextContent('Nested List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'OL', + 'LI', + 'P' + ]); + + await pageObject.pressShiftTabKeysInEditor(); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'List', + 'Nested List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeTrue(); + }); + + it('should have "ol" tag name for numbered list and "ul" tag name for nested bullet list', async () => { + await pageObject.setEditorTextContent('Numbered List'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.setEditorTextContent('Nested Bullet List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'UL', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'Numbered List', + 'Nested Bullet List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeTrue(); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeTrue(); + }); + + it('should have "ul" tag name for bullet list button click', async () => { + await pageObject.setEditorTextContent('Bullet List'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + + expect(pageObject.getEditorTagNames()).toEqual(['UL', 'LI', 'P']); + expect(pageObject.getEditorLeafContents()).toEqual(['Bullet List']); + }); + + it('should have multiple "ul" tag names for bullet list button click', async () => { + await pageObject.setEditorTextContent('Bullet List 1'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.setEditorTextContent('Bullet List 2'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'Bullet List 1', + 'Bullet List 2' + ]); + }); + + it('should have "ul" tag names for nested bullet lists when clicking "tab"', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.setEditorTextContent('Nested List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'UL', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'List', + 'Nested List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeTrue(); + }); + + it('should have "ul" tag name for bullet list and "ol" tag name for nested numbered list', async () => { + await pageObject.setEditorTextContent('Bullet List'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.setEditorTextContent('Nested Numbered List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'OL', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'Bullet List', + 'Nested Numbered List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.numberedList) + ).toBeTrue(); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeTrue(); + }); + + it('should have "ul" tag names for bullet lists when clicking "tab" to make it nested and "shift+Tab" to make it usual list', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.setEditorTextContent('Nested List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'UL', + 'LI', + 'P' + ]); + + await pageObject.pressShiftTabKeysInEditor(); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'LI', + 'P' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'List', + 'Nested List' + ]); + expect( + pageObject.getButtonCheckedState(ToolbarButton.bulletList) + ).toBeTrue(); + }); + + it('should have "strong" and "em" tag name for both bold and italics button clicks', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent('bold and italics'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'STRONG', + 'EM' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'bold and italics' + ]); + }); + + it('should have "strong", "em" and "ol" tag name for all bold, italics and numbered list button clicks', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent( + 'bold, italics and numbered list' + ); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'STRONG', + 'EM' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'bold, italics and numbered list' + ]); + }); + + it('should have "strong", "em" and "ul" tag name for all bold, italics and bullet list button clicks', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent( + 'bold, italics and bullet list' + ); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'STRONG', + 'EM' + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + 'bold, italics and bullet list' + ]); + }); + }); + + describe('various wacky string values input into the editor', () => { + const focused: string[] = []; + const disabled: string[] = []; + + wackyStrings.forEach(value => { + const specType = getSpecTypeByNamedList(value, focused, disabled); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType( + `wacky string "${value.name}" that are unmodified when rendered the same value within paragraph tag`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + await pageObject.setEditorTextContent(value.name); + + await connect(); + + expect(pageObject.getEditorFirstChildTagName()).toEqual( + 'P' + ); + expect(pageObject.getEditorFirstChildTextContent()).toBe( + value.name + ); + + await disconnect(); + } + ); + }); + }); }); diff --git a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts index 77defb69d3..4f1295dd89 100644 --- a/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts +++ b/packages/nimble-components/src/rich-text-editor/tests/rich-text-editor.stories.ts @@ -1,15 +1,24 @@ -import { html } from '@microsoft/fast-element'; +import { html, when } from '@microsoft/fast-element'; import type { Meta, StoryObj } from '@storybook/html'; import { createUserSelectedThemeStory, incubatingWarning } from '../../utilities/tests/storybook'; import { richTextEditorTag } from '..'; +import { buttonTag } from '../../button'; // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RichTextEditorArgs {} +interface RichTextEditorArgs { + footerActionButtons: boolean; +} const richTextEditorDescription = 'The rich text editor component allows users to add/edit text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The editor generates markdown output and takes markdown as input. The markdown flavor used is [CommonMark](https://spec.commonmark.org/0.30/).\n\n See the [rich text viewer](?path=/docs/incubating-rich-text-viewer--docs) component to render markdown without allowing editing.'; +const footerActionButtonDescription = `To place content such as a button at the far-right of the footer section, set \`slot="footer-actions"\`. + +Note: The content in the \`footer-actions\` slot will not adjust based on the state of the rich-text-editor (e.g. disabled). It is the responsibility of the +client application to make any necessary adjustments. For example, if the buttons should be disabled when the rich-text-editor is disabled, the +client application must implement that functionality. +`; const metadata: Meta = { title: 'Incubating/Rich Text Editor', @@ -27,8 +36,20 @@ const metadata: Meta = { componentName: 'rich text editor', statusLink: 'https://github.com/ni/nimble/issues/1288' })} - <${richTextEditorTag}> - `) + <${richTextEditorTag}> + ${when(x => x.footerActionButtons, html` + <${buttonTag} appearance="ghost" slot="footer-actions">Cancel + <${buttonTag} slot="footer-actions">OK`)} + + `), + argTypes: { + footerActionButtons: { + description: footerActionButtonDescription + } + }, + args: { + footerActionButtons: false + } }; export default metadata; diff --git a/packages/nimble-components/src/rich-text-editor/tests/types.spec.ts b/packages/nimble-components/src/rich-text-editor/tests/types.spec.ts new file mode 100644 index 0000000000..4f52aecc3b --- /dev/null +++ b/packages/nimble-components/src/rich-text-editor/tests/types.spec.ts @@ -0,0 +1,10 @@ +import type { ToolbarButton } from '../testing/types'; + +describe('Editor Toolbar button page object types', () => { + it('ToolbarButton fails compile if assigning arbitrary string values', () => { + // @ts-expect-error This expect will fail if the enum-like type is missing "as const" + const value: ToolbarButton = 'hello'; + // @ts-expect-error This expect will fail if the enum-like type is missing "as const" + expect(value).toEqual('hello'); + }); +}); diff --git a/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts b/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts index 4495234666..63aca4be78 100644 --- a/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts +++ b/packages/nimble-components/src/rich-text-viewer/testing/rich-text-viewer.pageobject.ts @@ -30,6 +30,31 @@ export class RichTextViewerPageObject { ).map(el => el.tagName); } + /** + * @returns An array of tag names with the closing tags (eg: '/P') in a document order + */ + public getRenderedMarkdownTagNamesWithClosingTags(): string[] { + const tagNames: string[] = []; + const renderedElement = this.getMarkdownRenderedElement(); + + const processNode = (node: Node): void => { + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element; + tagNames.push(el.tagName); + + el.childNodes.forEach(processNode); + + tagNames.push(`/${el.tagName}`); + } + }; + + if (renderedElement) { + processNode(renderedElement); + } + + return tagNames.slice(1, -1); + } + /** * Retrieves text contents for the rendered markdown content in document order * @returns An array of text contents of last elements in a tree diff --git a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts b/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts index 40853806d1..1235e90ba1 100644 --- a/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts +++ b/packages/nimble-components/src/rich-text-viewer/tests/rich-text-viewer.spec.ts @@ -193,6 +193,87 @@ describe('RichTextViewer', () => { ]); }); + it('nested numbered lists markdown string to "ol" and "li" HTML tags', async () => { + element.markdown = '1. Option 1\n\n 1. Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'OL', + 'LI', + 'P', + '/P', + 'OL', + 'LI', + 'P', + '/P', + '/LI', + '/OL', + '/LI', + '/OL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('numbered lists markdown string to "ol" tag and nested bullet list markdown string to "ul" tag', async () => { + element.markdown = '1. Option 1\n\n * Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'OL', + 'LI', + 'P', + '/P', + 'UL', + 'LI', + 'P', + '/P', + '/LI', + '/UL', + '/LI', + '/OL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('sequential numbered and bulleted lists should to "ol" and once "ol" tags are closed, should have "ul" tags', async () => { + element.markdown = '1. Option 1\n\n* Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'OL', + 'LI', + 'P', + '/P', + '/LI', + '/OL', + 'UL', + 'LI', + 'P', + '/P', + '/LI', + '/UL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + it('multiple empty numbered lists markdown string("1.\n2.") to "ol" and "li" HTML tags', async () => { element.markdown = '1. \n 2. '; @@ -295,6 +376,87 @@ describe('RichTextViewer', () => { ); }); + it('nested bullet lists markdown string to "ul" and "li" HTML tags', async () => { + element.markdown = '* Option 1\n\n * Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'UL', + 'LI', + 'P', + '/P', + 'UL', + 'LI', + 'P', + '/P', + '/LI', + '/UL', + '/LI', + '/UL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('bullet lists markdown string to "ul" tag and nested numbered list markdown string to "ol" tag', async () => { + element.markdown = '* Option 1\n\n 1. Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'UL', + 'LI', + 'P', + '/P', + 'OL', + 'LI', + 'P', + '/P', + '/LI', + '/OL', + '/LI', + '/UL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + + it('sequential bullet and numbered lists should to "ul" and once "ul" tags are closed, should have "ol" tags', async () => { + element.markdown = '* Option 1\n\n1. Option 2'; + + await connect(); + + expect( + pageObject.getRenderedMarkdownTagNamesWithClosingTags() + ).toEqual([ + 'UL', + 'LI', + 'P', + '/P', + '/LI', + '/UL', + 'OL', + 'LI', + 'P', + '/P', + '/LI', + '/OL' + ]); + expect(pageObject.getRenderedMarkdownLeafContents()).toEqual([ + 'Option 1', + 'Option 2' + ]); + }); + it('multiple bulleted lists markdown string("* \n* \n*") to "ul" and "li" HTML tags', async () => { element.markdown = '* Option 1\n * Option 2\n * Option 3';