diff --git a/.eslintrc.js b/.eslintrc.js
index 237b4781c..ced39f8d4 100755
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/no-extraneous-dependencies
const { getBaseConfig } = require('@openedx/frontend-build');
const config = getBaseConfig('eslint');
diff --git a/docs/template/edx/publish.js b/docs/template/edx/publish.js
index 4c390f56c..3c684cfbf 100644
--- a/docs/template/edx/publish.js
+++ b/docs/template/edx/publish.js
@@ -629,7 +629,7 @@ exports.publish = (memberData, opts, tutorials) => {
generateSourceFiles(sourceFiles, opts.encoding);
}
- // if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
+ if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
// index page displays information from package.json and lists files
files = find({kind: 'file'});
diff --git a/env.config.js b/env.config.js
index f4585f66d..28aa85c0b 100644
--- a/env.config.js
+++ b/env.config.js
@@ -1,8 +1,41 @@
-// NOTE: This file is used by the example app. frontend-build expects the file
+/* eslint-disable no-console */
+
+// NOTE: This file is used by the example app. frontend-build expects the file
// to be in the root of the repository. This is not used by the actual frontend-platform library.
-// Also note that in an actual application this file would be added to .gitignore.
+// Also note that in an actual application, this file would be added to .gitignore.
const config = {
JS_FILE_VAR: 'JS_FILE_VAR_VALUE_FOR_EXAMPLE_APP',
+ componentPropOverrides: {
+ targets: {
+ example: {
+ 'data-dd-privacy': 'mask', // Custom `data-*` attribute (e.g., Datadog)
+ 'data-hj-suppress': '', // Custom `data-*` attribute (e.g., Hotjar)
+ className: 'fs-mask', // Custom `className` attribute (e.g., Fullstory)
+ onClick: (e) => { // Custom `onClick` attribute
+ console.log('[env.config] onClick event for example', e);
+ },
+ style: { // Custom `style` attribute
+ background: 'blue',
+ color: 'white',
+ },
+ },
+ example2: {
+ 'data-dd-privacy': 'mask', // Custom `data-*` attribute (e.g., Datadog)
+ 'data-hj-suppress': '', // Custom `data-*` attribute (e.g., Hotjar)
+ className: 'fs-mask', // Custom `className` attribute (e.g., Fullstory)
+ onClick: (e) => { // Custom `onClick` attribute
+ console.log('[env.config] onClick event for example2', e);
+ },
+ style: { // Custom `style` attribute
+ background: 'blue',
+ color: 'white',
+ },
+ },
+ example3: {
+ 'data-dd-action-name': 'example name', // Custom `data-*` attribute (e.g., Datadog)
+ },
+ },
+ },
};
export default config;
diff --git a/example/ComponentPropOverridesPage.jsx b/example/ComponentPropOverridesPage.jsx
new file mode 100644
index 000000000..db12299a4
--- /dev/null
+++ b/example/ComponentPropOverridesPage.jsx
@@ -0,0 +1,152 @@
+import {
+ forwardRef, useContext, useEffect, useRef, useState,
+} from 'react';
+import PropTypes from 'prop-types';
+
+import { AppContext, useComponentPropOverrides, withComponentPropOverrides } from '@edx/frontend-platform/react';
+
+// Example via `useComponentPropOverrides` (hook)
+const ExampleComponentWithDefaultPropOverrides = forwardRef(({ children, ...rest }, ref) => {
+ const propOverrides = useComponentPropOverrides('example', rest);
+ return {children} ;
+});
+ExampleComponentWithDefaultPropOverrides.displayName = 'ExampleComponentWithDefaultPropOverrides';
+ExampleComponentWithDefaultPropOverrides.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+const ExampleComponentWithAllowedPropOverrides = forwardRef(({ children, ...rest }, ref) => {
+ const propOverrides = useComponentPropOverrides('example2', rest, {
+ allowedPropNames: ['className', 'style', 'onClick'],
+ });
+ return {children} ;
+});
+ExampleComponentWithAllowedPropOverrides.displayName = 'ExampleComponentWithAllowedPropOverrides';
+ExampleComponentWithAllowedPropOverrides.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+// Example via `withComponentPropOverrides` (HOC)
+const ExampleComponent = forwardRef(({ children, ...rest }, ref) => (
+ {children}
+));
+ExampleComponent.displayName = 'ExampleComponent';
+ExampleComponent.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+const ExampleComponentWithPropOverrides3 = withComponentPropOverrides('example3')(ExampleComponent);
+
+function jsonStringify(obj) {
+ const replacer = (key, value) => {
+ if (typeof value === 'function') {
+ return '[Function]';
+ }
+ return value;
+ };
+ return JSON.stringify(obj, replacer, 2);
+}
+
+function useExample() {
+ const ref = useRef(null);
+ const [node, setNode] = useState(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ setNode(ref.current.outerHTML);
+ }
+ }, []);
+
+ return {
+ ref,
+ node,
+ };
+}
+
+export default function ComponentPropOverridesPage() {
+ const { config } = useContext(AppContext);
+ const firstExample = useExample();
+ const secondExample = useExample();
+ const thirdExample = useExample();
+
+ const { componentPropOverrides } = config;
+
+ return (
+
+
Example usage of componentPropOverrides
from configuration
+
+
Current configuration
+ {componentPropOverrides ? (
+
+ {jsonStringify({ componentPropOverrides })}
+
+ ) : (
+
+ No componentPropOverrides
configuration found. Consider updating this
+ application's env.config.js
to configure any custom props.
+
+ )}
+
+
Examples
+
+ The following examples below demonstrate
+ using useComponentPropOverrides
and withComponentPropOverrides
to
+ extend any component's base props based on the application's configuration. Inspect the DOM
+ elements for the rendered example components below to observe the configured attributes/values.
+
+
+ {/* Example 1 (useComponentPropOverrides) */}
+
useComponentPropOverrides
(hook)
+
Default support prop overrides
+
+ By default, only data-*
attributes and className
props are
+ supported; other props will be ignored. You may opt-in to non-default prop
+ overrides by extending the allowedPropNames
option.
+
+
+ console.log('ExampleComponentWithPropOverrides clicked', e)}
+ style={{ borderBottom: '4px solid red' }}
+ className="example-class"
+ >
+ Example 1
+
+
+
Result: {' '}
+
+ {firstExample.node}
+
+
+ {/* Example 2 (useComponentPropOverrides) */}
+
Opt-in to specific prop overrides with allowedPropNames
+
+ console.log('ExampleComponentWithPropOverrides clicked', e)}
+ style={{ borderBottom: '4px solid red' }}
+ className="example-class"
+ >
+ Example 2
+
+
+
Result: {' '}
+
+ {secondExample.node}
+
+
+ {/* Example 3 (withComponentPropOverrides) */}
+
withComponentPropOverrides
(HOC)
+
+
+ Example 3
+
+
+
Result: {' '}
+
+ {thirdExample.node}
+
+
+ );
+}
diff --git a/example/ExamplePage.jsx b/example/ExamplePage.jsx
index 1b513ded3..0071911aa 100644
--- a/example/ExamplePage.jsx
+++ b/example/ExamplePage.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import { Component } from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -48,6 +48,7 @@ class ExamplePage extends Component {
EXAMPLE_VAR env var came through: {getConfig().EXAMPLE_VAR}
JS_FILE_VAR var came through: {getConfig().JS_FILE_VAR}
Visit authenticated page.
+ Visit component prop overrides page.
Visit error page.
);
diff --git a/example/index.jsx b/example/index.jsx
index 52b37aec5..a82870015 100644
--- a/example/index.jsx
+++ b/example/index.jsx
@@ -1,7 +1,6 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
-import React from 'react';
import ReactDOM from 'react-dom';
import {
AppProvider,
@@ -16,6 +15,7 @@ import { Routes, Route } from 'react-router-dom';
import './index.scss';
import ExamplePage from './ExamplePage';
import AuthenticatedPage from './AuthenticatedPage';
+import ComponentPropOverridesPage from './ComponentPropOverridesPage';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -27,6 +27,7 @@ subscribe(APP_READY, () => {
element={ }
/>
} />
+ } />
,
document.getElementById('root'),
diff --git a/package-lock.json b/package-lock.json
index 0ea97a193..7d7579e2f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,11 +36,12 @@
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.2.0",
- "@openedx/frontend-build": "14.0.10",
+ "@openedx/frontend-build": "14.0.15",
"@openedx/paragon": "22.6.1",
"@testing-library/jest-dom": "6.4.6",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
+ "@testing-library/user-event": "14.5.2",
"axios-mock-adapter": "^1.22.0",
"core-js": "3.37.1",
"husky": "8.0.3",
@@ -96,11 +97,10 @@
}
},
"node_modules/@babel/cli": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.7.tgz",
- "integrity": "sha512-8dfPprJgV4O14WTx+AQyEA+opgUKPrsIXX/MdL50J1n06EQJ6m1T+CdsJe0qEC0B/Xl85i+Un5KVAxd/PACX9A==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.8.tgz",
+ "integrity": "sha512-isdp+G6DpRyKc+3Gqxy2rjzgF7Zj9K0mzLNnxz+E/fgeag8qT3vVulX4gY9dGO1q0y+0lUv6V3a+uhUzMzrwXg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"commander": "^6.2.0",
@@ -217,31 +217,30 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz",
- "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
+ "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
- "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
+ "version": "7.24.9",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
+ "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
- "@babel/generator": "^7.24.7",
- "@babel/helper-compilation-targets": "^7.24.7",
- "@babel/helper-module-transforms": "^7.24.7",
- "@babel/helpers": "^7.24.7",
- "@babel/parser": "^7.24.7",
+ "@babel/generator": "^7.24.9",
+ "@babel/helper-compilation-targets": "^7.24.8",
+ "@babel/helper-module-transforms": "^7.24.9",
+ "@babel/helpers": "^7.24.8",
+ "@babel/parser": "^7.24.8",
"@babel/template": "^7.24.7",
- "@babel/traverse": "^7.24.7",
- "@babel/types": "^7.24.7",
+ "@babel/traverse": "^7.24.8",
+ "@babel/types": "^7.24.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -282,12 +281,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
- "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
+ "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.24.7",
+ "@babel/types": "^7.25.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -324,14 +323,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz",
- "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
+ "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.24.7",
- "@babel/helper-validator-option": "^7.24.7",
- "browserslist": "^4.22.2",
+ "@babel/compat-data": "^7.25.2",
+ "@babel/helper-validator-option": "^7.24.8",
+ "browserslist": "^4.23.1",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -437,14 +436,13 @@
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz",
- "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz",
+ "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.24.7",
- "@babel/types": "^7.24.7"
+ "@babel/traverse": "^7.24.8",
+ "@babel/types": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -465,16 +463,15 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz",
- "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
+ "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
"dev": true,
"dependencies": {
- "@babel/helper-environment-visitor": "^7.24.7",
"@babel/helper-module-imports": "^7.24.7",
"@babel/helper-simple-access": "^7.24.7",
- "@babel/helper-split-export-declaration": "^7.24.7",
- "@babel/helper-validator-identifier": "^7.24.7"
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "@babel/traverse": "^7.25.2"
},
"engines": {
"node": ">=6.9.0"
@@ -497,11 +494,10 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz",
- "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
+ "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -525,15 +521,14 @@
}
},
"node_modules/@babel/helper-replace-supers": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz",
- "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz",
+ "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@babel/helper-environment-visitor": "^7.24.7",
- "@babel/helper-member-expression-to-functions": "^7.24.7",
- "@babel/helper-optimise-call-expression": "^7.24.7"
+ "@babel/helper-member-expression-to-functions": "^7.24.8",
+ "@babel/helper-optimise-call-expression": "^7.24.7",
+ "@babel/traverse": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -584,9 +579,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
- "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+ "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -603,9 +598,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz",
- "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
+ "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -628,13 +623,13 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz",
- "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
+ "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.24.7",
- "@babel/types": "^7.24.7"
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
@@ -735,10 +730,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
- "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+ "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
"dev": true,
+ "dependencies": {
+ "@babel/types": "^7.25.2"
+ },
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1255,18 +1253,16 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz",
- "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz",
+ "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.24.7",
- "@babel/helper-compilation-targets": "^7.24.7",
- "@babel/helper-environment-visitor": "^7.24.7",
- "@babel/helper-function-name": "^7.24.7",
- "@babel/helper-plugin-utils": "^7.24.7",
- "@babel/helper-replace-supers": "^7.24.7",
- "@babel/helper-split-export-declaration": "^7.24.7",
+ "@babel/helper-compilation-targets": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-replace-supers": "^7.25.0",
+ "@babel/traverse": "^7.25.0",
"globals": "^11.1.0"
},
"engines": {
@@ -1293,12 +1289,12 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz",
- "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz",
+ "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==",
"dev": true,
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.7"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -1498,13 +1494,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz",
- "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz",
+ "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==",
"dev": true,
"dependencies": {
- "@babel/helper-module-transforms": "^7.24.7",
- "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-module-transforms": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
"@babel/helper-simple-access": "^7.24.7"
},
"engines": {
@@ -1663,12 +1659,12 @@
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz",
- "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz",
+ "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==",
"dev": true,
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.7",
+ "@babel/helper-plugin-utils": "^7.24.8",
"@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
},
@@ -1917,12 +1913,12 @@
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz",
- "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
+ "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
"dev": true,
"dependencies": {
- "@babel/helper-plugin-utils": "^7.24.7"
+ "@babel/helper-plugin-utils": "^7.24.8"
},
"engines": {
"node": ">=6.9.0"
@@ -2013,16 +2009,15 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz",
- "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==",
+ "version": "7.24.8",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz",
+ "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.24.7",
- "@babel/helper-compilation-targets": "^7.24.7",
- "@babel/helper-plugin-utils": "^7.24.7",
- "@babel/helper-validator-option": "^7.24.7",
+ "@babel/compat-data": "^7.24.8",
+ "@babel/helper-compilation-targets": "^7.24.8",
+ "@babel/helper-plugin-utils": "^7.24.8",
+ "@babel/helper-validator-option": "^7.24.8",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7",
@@ -2053,9 +2048,9 @@
"@babel/plugin-transform-block-scoping": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-transform-class-static-block": "^7.24.7",
- "@babel/plugin-transform-classes": "^7.24.7",
+ "@babel/plugin-transform-classes": "^7.24.8",
"@babel/plugin-transform-computed-properties": "^7.24.7",
- "@babel/plugin-transform-destructuring": "^7.24.7",
+ "@babel/plugin-transform-destructuring": "^7.24.8",
"@babel/plugin-transform-dotall-regex": "^7.24.7",
"@babel/plugin-transform-duplicate-keys": "^7.24.7",
"@babel/plugin-transform-dynamic-import": "^7.24.7",
@@ -2068,7 +2063,7 @@
"@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
"@babel/plugin-transform-member-expression-literals": "^7.24.7",
"@babel/plugin-transform-modules-amd": "^7.24.7",
- "@babel/plugin-transform-modules-commonjs": "^7.24.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.24.8",
"@babel/plugin-transform-modules-systemjs": "^7.24.7",
"@babel/plugin-transform-modules-umd": "^7.24.7",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
@@ -2078,7 +2073,7 @@
"@babel/plugin-transform-object-rest-spread": "^7.24.7",
"@babel/plugin-transform-object-super": "^7.24.7",
"@babel/plugin-transform-optional-catch-binding": "^7.24.7",
- "@babel/plugin-transform-optional-chaining": "^7.24.7",
+ "@babel/plugin-transform-optional-chaining": "^7.24.8",
"@babel/plugin-transform-parameters": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
@@ -2089,7 +2084,7 @@
"@babel/plugin-transform-spread": "^7.24.7",
"@babel/plugin-transform-sticky-regex": "^7.24.7",
"@babel/plugin-transform-template-literals": "^7.24.7",
- "@babel/plugin-transform-typeof-symbol": "^7.24.7",
+ "@babel/plugin-transform-typeof-symbol": "^7.24.8",
"@babel/plugin-transform-unicode-escapes": "^7.24.7",
"@babel/plugin-transform-unicode-property-regex": "^7.24.7",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
@@ -2098,7 +2093,7 @@
"babel-plugin-polyfill-corejs2": "^0.4.10",
"babel-plugin-polyfill-corejs3": "^0.10.4",
"babel-plugin-polyfill-regenerator": "^0.6.1",
- "core-js-compat": "^3.31.0",
+ "core-js-compat": "^3.37.1",
"semver": "^6.3.1"
},
"engines": {
@@ -2194,34 +2189,30 @@
}
},
"node_modules/@babel/template": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
- "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
+ "version": "7.25.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
+ "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.24.7",
- "@babel/parser": "^7.24.7",
- "@babel/types": "^7.24.7"
+ "@babel/parser": "^7.25.0",
+ "@babel/types": "^7.25.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
- "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
+ "version": "7.25.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
+ "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.24.7",
- "@babel/generator": "^7.24.7",
- "@babel/helper-environment-visitor": "^7.24.7",
- "@babel/helper-function-name": "^7.24.7",
- "@babel/helper-hoist-variables": "^7.24.7",
- "@babel/helper-split-export-declaration": "^7.24.7",
- "@babel/parser": "^7.24.7",
- "@babel/types": "^7.24.7",
+ "@babel/generator": "^7.25.0",
+ "@babel/parser": "^7.25.3",
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.2",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -2230,12 +2221,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.24.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
- "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
+ "version": "7.25.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+ "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
"dev": true,
"dependencies": {
- "@babel/helper-string-parser": "^7.24.7",
+ "@babel/helper-string-parser": "^7.24.8",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
@@ -2258,9 +2249,9 @@
}
},
"node_modules/@csstools/cascade-layer-name-parser": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.12.tgz",
- "integrity": "sha512-iNCCOnaoycAfcIot3v/orjkTol+j8+Z5xgpqxUpZSdqeaxCADQZtldHhlvzDipmi7OoWdcJUO6DRZcnkMSBEIg==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz",
+ "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==",
"dev": true,
"funding": [
{
@@ -2276,14 +2267,14 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^2.7.0",
- "@csstools/css-tokenizer": "^2.3.2"
+ "@csstools/css-parser-algorithms": "^2.7.1",
+ "@csstools/css-tokenizer": "^2.4.1"
}
},
"node_modules/@csstools/css-parser-algorithms": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.0.tgz",
- "integrity": "sha512-qvBMcOU/uWFCH/VO0MYe0AMs0BGMWAt6FTryMbFIKYtZtVnqTZtT8ktv5o718llkaGZWomJezJZjq3vJDHeJNQ==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz",
+ "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==",
"dev": true,
"funding": [
{
@@ -2299,13 +2290,13 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-tokenizer": "^2.3.2"
+ "@csstools/css-tokenizer": "^2.4.1"
}
},
"node_modules/@csstools/css-tokenizer": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.2.tgz",
- "integrity": "sha512-0xYOf4pQpAaE6Sm2Q0x3p25oRukzWQ/O8hWVvhIt9Iv98/uu053u2CGm/g3kJ+P0vOYTAYzoU8Evq2pg9ZPXtw==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz",
+ "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==",
"dev": true,
"funding": [
{
@@ -2322,9 +2313,9 @@
}
},
"node_modules/@csstools/media-query-list-parser": {
- "version": "2.1.12",
- "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.12.tgz",
- "integrity": "sha512-t1/CdyVJzOQUiGUcIBXRzTAkWTFPxiPnoKwowKW2z9Uj78c2bBWI/X94BeVfUwVq1xtCjD7dnO8kS6WONgp8Jw==",
+ "version": "2.1.13",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz",
+ "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==",
"dev": true,
"funding": [
{
@@ -2340,8 +2331,8 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^2.7.0",
- "@csstools/css-tokenizer": "^2.3.2"
+ "@csstools/css-parser-algorithms": "^2.7.1",
+ "@csstools/css-tokenizer": "^2.4.1"
}
},
"node_modules/@discoveryjs/json-ext": {
@@ -2392,6 +2383,15 @@
"@newrelic/publish-sourcemap": "^5.0.1"
}
},
+ "node_modules/@edx/typescript-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.1.0.tgz",
+ "integrity": "sha512-HF+7dsSgA2YQ6f/qV4HnrEYBoIhIdxVQZgDyYk/YGvaVGqT6IFuaHnYUP7ImpCUMOUmx/Jl7EyuVeaMe2LrMcA==",
+ "dev": true,
+ "peerDependencies": {
+ "typescript": "^4.9.4"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2820,6 +2820,19 @@
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"dev": true
},
+ "node_modules/@formatjs/ts-transformer/node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
@@ -3772,22 +3785,22 @@
}
},
"node_modules/@openedx/frontend-build": {
- "version": "14.0.10",
- "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.0.10.tgz",
- "integrity": "sha512-yTn8C+WV7tsDBQWz4PYKHQPNLtSj3KqT4qjuo13CUFk5hqUMZRNBZqw2ihT2cI9jBMPufra34TqYRtdK/xNqlQ==",
+ "version": "14.0.15",
+ "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.0.15.tgz",
+ "integrity": "sha512-D4j2IGAKkwVUpJGGZ5k926A6vDVuLSnpWYW1KLTmLTeh4JtBtMJlJ0D1TGXbImkiNje+GPjIxXUE9sjCLQGJUg==",
"dev": true,
"dependencies": {
- "@babel/cli": "7.24.7",
- "@babel/core": "7.24.7",
+ "@babel/cli": "7.24.8",
+ "@babel/core": "7.24.9",
"@babel/eslint-parser": "7.22.9",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
- "@babel/preset-env": "7.24.7",
+ "@babel/preset-env": "7.24.8",
"@babel/preset-react": "7.24.7",
"@edx/eslint-config": "4.1.0",
"@edx/new-relic-source-map-webpack-plugin": "2.1.0",
- "@edx/typescript-config": "1.0.1",
+ "@edx/typescript-config": "1.1.0",
"@formatjs/cli": "^6.0.3",
"@fullhuman/postcss-purgecss": "5.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
@@ -3823,8 +3836,8 @@
"jest": "29.6.1",
"jest-environment-jsdom": "29.6.1",
"mini-css-extract-plugin": "1.6.2",
- "postcss": "8.4.38",
- "postcss-custom-media": "10.0.6",
+ "postcss": "8.4.39",
+ "postcss-custom-media": "10.0.8",
"postcss-loader": "7.3.4",
"postcss-rtlcss": "5.1.2",
"react-dev-utils": "12.0.1",
@@ -3851,16 +3864,6 @@
"react": "^16.9.0 || ^17.0.0"
}
},
- "node_modules/@openedx/frontend-build/node_modules/@edx/typescript-config": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.0.1.tgz",
- "integrity": "sha512-w0g3nIX9oEch8Rip8q8sb/nrurGEHA1BEjK/I1LAQwA44K4FPMWvyvabmZErrdTJ9sXcZL10aWD3bat1obV8Bg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "typescript": "^4.9.4"
- }
- },
"node_modules/@openedx/frontend-build/node_modules/jest-environment-jsdom": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.6.1.tgz",
@@ -3888,20 +3891,6 @@
}
}
},
- "node_modules/@openedx/frontend-build/node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">= 10.14.2"
- }
- },
"node_modules/@openedx/paragon": {
"version": "22.6.1",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.6.1.tgz",
@@ -4575,6 +4564,19 @@
}
}
},
+ "node_modules/@testing-library/user-event": {
+ "version": "14.5.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz",
+ "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -6807,6 +6809,19 @@
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"dev": true
},
+ "node_modules/babel-plugin-formatjs/node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/babel-plugin-istanbul": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
@@ -7175,9 +7190,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.23.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
- "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+ "version": "4.23.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+ "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"dev": true,
"funding": [
{
@@ -7194,10 +7209,10 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001587",
- "electron-to-chromium": "^1.4.668",
- "node-releases": "^2.0.14",
- "update-browserslist-db": "^1.0.13"
+ "caniuse-lite": "^1.0.30001646",
+ "electron-to-chromium": "^1.5.4",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -7334,9 +7349,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001612",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz",
- "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==",
+ "version": "1.0.30001651",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
+ "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"dev": true,
"funding": [
{
@@ -7700,7 +7715,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 6"
}
@@ -8788,9 +8802,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
- "version": "1.4.745",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz",
- "integrity": "sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA==",
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
+ "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
"dev": true
},
"node_modules/email-prop-type": {
@@ -9353,6 +9367,19 @@
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
"dev": true
},
+ "node_modules/eslint-plugin-formatjs/node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/eslint-plugin-import": {
"version": "2.27.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
@@ -14615,9 +14642,9 @@
"dev": true
},
"node_modules/node-releases": {
- "version": "2.0.14",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
- "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true
},
"node_modules/nodemon": {
@@ -15196,9 +15223,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true
},
"node_modules/picomatch": {
@@ -15444,9 +15471,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.38",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
- "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+ "version": "8.4.39",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
+ "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true,
"funding": [
{
@@ -15464,7 +15491,7 @@
],
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.0",
+ "picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@@ -15522,9 +15549,9 @@
}
},
"node_modules/postcss-custom-media": {
- "version": "10.0.6",
- "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.6.tgz",
- "integrity": "sha512-BjihQoIO4Wjqv9fQNExSJIim8UAmkhLxuJnhJsLTRFSba1y1MhxkJK5awsM//6JJ+/Tu5QUxf624RQAvKHv6SA==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.8.tgz",
+ "integrity": "sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==",
"dev": true,
"funding": [
{
@@ -15536,12 +15563,11 @@
"url": "https://opencollective.com/csstools"
}
],
- "license": "MIT",
"dependencies": {
- "@csstools/cascade-layer-name-parser": "^1.0.11",
- "@csstools/css-parser-algorithms": "^2.6.3",
- "@csstools/css-tokenizer": "^2.3.1",
- "@csstools/media-query-list-parser": "^2.1.11"
+ "@csstools/cascade-layer-name-parser": "^1.0.13",
+ "@csstools/css-parser-algorithms": "^2.7.1",
+ "@csstools/css-tokenizer": "^2.4.1",
+ "@csstools/media-query-list-parser": "^2.1.13"
},
"engines": {
"node": "^14 || ^16 || >=18"
@@ -19415,16 +19441,16 @@
}
},
"node_modules/typescript": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
- "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=4.2.0"
}
},
"node_modules/uc.micro": {
@@ -19572,9 +19598,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.13",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
- "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
+ "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"dev": true,
"funding": [
{
@@ -19591,8 +19617,8 @@
}
],
"dependencies": {
- "escalade": "^3.1.1",
- "picocolors": "^1.0.0"
+ "escalade": "^3.1.2",
+ "picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
diff --git a/package.json b/package.json
index 71cde363b..880d5730b 100644
--- a/package.json
+++ b/package.json
@@ -35,11 +35,12 @@
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.2.0",
- "@openedx/frontend-build": "14.0.10",
+ "@openedx/frontend-build": "14.0.15",
"@openedx/paragon": "22.6.1",
"@testing-library/jest-dom": "6.4.6",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
+ "@testing-library/user-event": "14.5.2",
"axios-mock-adapter": "^1.22.0",
"core-js": "3.37.1",
"husky": "8.0.3",
diff --git a/src/react/hooks.js b/src/react/hooks.js
index b1d4e219e..06d50e332 100644
--- a/src/react/hooks.js
+++ b/src/react/hooks.js
@@ -1,7 +1,7 @@
-/* eslint-disable import/prefer-default-export */
import { useEffect } from 'react';
import { subscribe, unsubscribe } from '../pubSub';
import { sendTrackEvent } from '../analytics';
+import { getConfig } from '../config';
/**
* A React hook that allows functional components to subscribe to application events. This should
@@ -9,9 +9,10 @@ import { sendTrackEvent } from '../analytics';
* provide necessary data to a given component, rather than utilizing a non-React-like Pub/Sub
* mechanism.
*
- * @memberof module:React
* @param {string} type
* @param {function} callback
+ *
+ * @memberof module:React
*/
export const useAppEvent = (type, callback) => {
useEffect(() => {
@@ -48,3 +49,110 @@ export const useTrackColorSchemeChoice = () => {
};
}, []);
};
+
+/**
+ * @typedef {object} ComponentPropOverride
+ * @property {string|boolean|Record|Function} [key] - The custom prop value.
+ */
+
+/**
+ * @typedef {object} ComponentPropOverrides
+ * @property {Record} targets - A mapping of component targets to custom props.
+ */
+
+/**
+ * @typedef {object} AppConfigWithComponentPropOverrides
+ * @property {ComponentPropOverrides} componentPropOverrides - The component prop overrides configuration.
+ */
+
+/**
+ * @typedef {object} ComponentPropOverridesOptions
+ * @property {string[]} [allowedPropNames=["className"]] - The list of prop names allowed to be overridden.
+ * @property {boolean} [allowsDataAttributes=true] - Whether to allow `data-*` attributes to be applied.
+ */
+
+/**
+ * A React hook that processes the given `target` to extend/merge component props
+ * with any corresponding attributes/values based on configuration.
+ *
+ * This hook looks up the specified `target` in the `componentPropOverrides` configuration,
+ * and if a match is found, it merges the component's props with the mapped attributes/values
+ * per the configured component targets.
+ *
+ * @param {string} target - The component target used to identify custom props per configuration.
+ * @param {Record} props - The original props object passed to the component.
+ * @param {ComponentPropOverridesOptions} [options] - Optional configuration for the hook.
+ * @returns {Record} An updated props object with custom props merged in.
+ *
+ * @example
+ * // Given a configuration like:
+ * {
+ * componentPropOverrides: {
+ * targets: {
+ * example: {
+ * 'data-dd-privacy': 'mask',
+ * 'data-hj-suppress': '',
+ * className: 'fs-mask',
+ * },
+ * },
+ * },
+ * }
+ *
+ * // and calling the hook as follows:
+ * const propOverrides = useComponentPropOverrides('example', { otherProp: 'value' });
+ *
+ * // The resulting `propOverrides` will be:
+ * { otherProp: 'value', 'data-dd-privacy': 'mask', data-hj-suppress: '', className: 'fs-mask' }
+ *
+ * @memberof module:React
+ * @see module:React.withComponentPropOverrides
+ */
+export function useComponentPropOverrides(target, props, options = {}) {
+ /** @type {AppConfigWithComponentPropOverrides} */
+ const { componentPropOverrides } = getConfig();
+ const propOverridesForTarget = componentPropOverrides?.targets?.[target];
+ if (!target || !propOverridesForTarget) {
+ return props;
+ }
+ const overrideOptions = {
+ // Allow for custom prop overrides to be applied to the component. These are
+ // separate from any `data-*` attributes, which are always supported.
+ allowedPropNames: ['className'],
+ // Allow for any `data-*` attributes to be applied to the component.
+ allowsDataAttributes: true,
+ ...options,
+ };
+
+ const updatedProps = { ...props };
+ // Apply the configured attributes/values/classes for the matched target
+ Object.entries(propOverridesForTarget).forEach(([attributeName, attributeValue]) => {
+ const isAllowedPropName = !!overrideOptions.allowedPropNames?.includes(attributeName);
+ const isDataAttribute = !!overrideOptions.allowsDataAttributes && attributeName.startsWith('data-');
+ const isAllowedPropOverride = isAllowedPropName || isDataAttribute;
+ if (!isAllowedPropOverride) {
+ // Skip applying the override prop if it's not allowed.
+ return;
+ }
+
+ // Parse attributeValue as empty string if not provided, or falsey value is given (e.g., `undefined`).
+ let transformedAttributeValue = !attributeValue ? '' : attributeValue;
+ if (attributeName === 'className') {
+ // Append the `className` to the existing `className` prop value (if any)
+ transformedAttributeValue = [updatedProps.className, attributeValue].join(' ').trim();
+ } else if (attributeName === 'style' && typeof attributeValue === 'object') {
+ // Merge the `style` object with the existing `style` prop object (if any)
+ transformedAttributeValue = { ...updatedProps.style, ...attributeValue };
+ } else if (typeof attributeValue === 'function') {
+ // Merge the function with the existing prop's function
+ const oldFn = updatedProps[attributeName];
+ transformedAttributeValue = oldFn ? (...args) => {
+ oldFn(...args);
+ attributeValue(...args);
+ } : attributeValue;
+ }
+
+ // Update the props with the transformed attribute value
+ updatedProps[attributeName] = transformedAttributeValue;
+ });
+ return updatedProps;
+}
diff --git a/src/react/index.js b/src/react/index.js
index 0985d9271..171ce2301 100644
--- a/src/react/index.js
+++ b/src/react/index.js
@@ -13,4 +13,5 @@ export { default as ErrorBoundary } from './ErrorBoundary';
export { default as ErrorPage } from './ErrorPage';
export { default as LoginRedirect } from './LoginRedirect';
export { default as PageWrap } from './PageWrap';
-export { useAppEvent } from './hooks';
+export { default as withComponentPropOverrides } from './withComponentPropOverrides';
+export { useAppEvent, useComponentPropOverrides } from './hooks';
diff --git a/src/react/withComponentPropOverrides.jsx b/src/react/withComponentPropOverrides.jsx
new file mode 100644
index 000000000..ee9552e7d
--- /dev/null
+++ b/src/react/withComponentPropOverrides.jsx
@@ -0,0 +1,45 @@
+import React, { forwardRef } from 'react';
+import { useComponentPropOverrides } from './hooks';
+
+/**
+ * A Higher-Order Component (HOC) that enhances the wrapped component with custom props via
+ * the `useComponentPropOverrides` hook to merge any custom props from configuration with the
+ * actual component props.
+ *
+ * @param {string} [target] - The target used to identify any custom props for a given element.
+ * @param {ComponentPropOverridesOptions} [options] - Optional configuration for the HOC.
+ *
+ * @example
+ * // Given a configuration like:
+ * {
+ * componentPropOverrides: {
+ * targets: {
+ * example: {
+ * 'data-dd-privacy': 'mask',
+ * 'data-hj-suppress': '',
+ * className: 'fs-mask',
+ * },
+ * },
+ * },
+ * }
+ *
+ * // and calling the HOC as follows:
+ * const ComponentWithPropOverrides = withComponentPropOverrides('example')(MyComponent);
+ *
+ *
+ * // The resulting `ComponentWithPropOverrides` will render `MyComponent` with the following props:
+ * { otherProp: 'value', 'data-dd-privacy': 'mask', data-hj-suppress: '', className: 'fs-mask' }
+ *
+ * @see module:React.useComponentPropOverrides
+ * @memberof module:React
+ */
+const withComponentPropOverrides = (target, options = {}) => (WrappedComponent) => {
+ const WithComponentPropOverrides = forwardRef((props, ref) => {
+ const propOverrides = useComponentPropOverrides(target, props, options);
+ return ;
+ });
+ WithComponentPropOverrides.displayName = `withComponentPropOverrides(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
+ return WithComponentPropOverrides;
+};
+
+export default withComponentPropOverrides;
diff --git a/src/react/withComponentPropOverrides.test.jsx b/src/react/withComponentPropOverrides.test.jsx
new file mode 100644
index 000000000..98db40f42
--- /dev/null
+++ b/src/react/withComponentPropOverrides.test.jsx
@@ -0,0 +1,174 @@
+/* eslint-disable react/prop-types */
+import React from 'react';
+import { Route, Routes, MemoryRouter } from 'react-router-dom';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import withComponentPropOverrides from './withComponentPropOverrides';
+import AppContext from './AppContext';
+import { getConfig } from '../config';
+
+jest.mock('../auth');
+jest.mock('../config');
+
+const mockCustomComponentOnClick = jest.fn();
+const mockcomponentPropOverridesConfig = {
+ targets: {
+ example: {
+ 'data-dd-privacy': 'mask', // `data-*` (Datadog example)
+ 'data-hj-suppress': '', // `data-*` (Hotjar example)
+ className: 'fs-mask', // `className` (Fullstory example)
+ style: {
+ background: 'blue',
+ color: 'white',
+ },
+ onClick: mockCustomComponentOnClick,
+ },
+ example2: {
+ 'data-dd-action-name': 'example name', // `data-*` (Datadog example)
+ },
+ },
+};
+
+function ExampleComponent(props) {
+ return (
+ hello world
+ );
+}
+
+describe('withComponentPropOverrides', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getConfig.mockReturnValue({});
+ });
+
+ it.each([
+ {
+ MockExampleComponent: withComponentPropOverrides('example')(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: true,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('example', { allowedPropNames: ['className', 'style', 'onClick'] })(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: true,
+ hasSpecialComponentPropOverridesApplied: true,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('example')(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: true,
+ hasDefaultComponentPropOverridesApplied: true,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('example', { allowedPropNames: ['className', 'style', 'onClick'] })(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: true,
+ hasDefaultComponentPropOverridesApplied: true,
+ hasSpecialComponentPropOverridesApplied: true,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('invalid')(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: false,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides(undefined)(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: false,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('')(ExampleComponent),
+ hasComponentPropOverridesConfigured: true,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: false,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('example')(ExampleComponent),
+ hasComponentPropOverridesConfigured: false,
+ exampleComponentHasOnClick: false,
+ hasDefaultComponentPropOverridesApplied: false,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ {
+ MockExampleComponent: withComponentPropOverrides('example')(ExampleComponent),
+ hasComponentPropOverridesConfigured: false,
+ exampleComponentHasOnClick: true,
+ hasDefaultComponentPropOverridesApplied: false,
+ hasSpecialComponentPropOverridesApplied: false,
+ },
+ ])('should return a component with the expected configured attributes/values, if any (%s)', async ({
+ MockExampleComponent,
+ hasComponentPropOverridesConfigured,
+ exampleComponentHasOnClick,
+ hasDefaultComponentPropOverridesApplied,
+ hasSpecialComponentPropOverridesApplied,
+ }) => {
+ if (hasComponentPropOverridesConfigured) {
+ getConfig.mockReturnValue({ componentPropOverrides: mockcomponentPropOverridesConfig });
+ }
+ const mockComponentOnClick = jest.fn();
+ const baseProps = {
+ className: 'existing',
+ onClick: exampleComponentHasOnClick ? mockComponentOnClick : undefined,
+ style: { borderBottom: '4px solid red' },
+ };
+ const App = (
+
+
+
+ } />
+
+
+
+ );
+ render(App);
+ const element = screen.getByTestId('component-prop-overrides-element');
+ expect(element).toBeInTheDocument();
+
+ // verify base props
+ if (hasDefaultComponentPropOverridesApplied) {
+ expect(element).toHaveAttribute('data-dd-privacy', 'mask');
+ expect(element).toHaveAttribute('data-hj-suppress', '');
+ expect(element).toHaveClass('fs-mask');
+
+ // verify opt-in props
+ if (hasSpecialComponentPropOverridesApplied) {
+ expect(element).toHaveClass('existing'); // should still have base className prop
+ expect(element).toHaveStyle({ background: 'blue', color: 'white', borderBottom: '4px solid red' });
+ }
+ } else {
+ expect(element).not.toHaveAttribute('data-dd-privacy');
+ expect(element).not.toHaveAttribute('data-hj-suppress', '');
+ expect(element).not.toHaveClass('fs-mask');
+ expect(element).toHaveClass('existing'); // should still have base className prop
+ expect(element).toHaveStyle({ borderBottom: '4px solid red' });
+ }
+
+ // simulate click event
+ await userEvent.click(element);
+
+ // verify onClick event
+ if (exampleComponentHasOnClick) {
+ await waitFor(() => {
+ expect(mockComponentOnClick).toHaveBeenCalledTimes(1);
+ expect(mockComponentOnClick).toHaveBeenCalledWith(expect.any(Object));
+ });
+ }
+ if (hasSpecialComponentPropOverridesApplied) {
+ expect(mockCustomComponentOnClick).toHaveBeenCalledTimes(1);
+ expect(mockCustomComponentOnClick).toHaveBeenCalledWith(expect.any(Object));
+ } else {
+ expect(mockCustomComponentOnClick).not.toHaveBeenCalled();
+ }
+ });
+});