diff --git a/README.rst b/README.rst index 7b6208bc..ca392666 100644 --- a/README.rst +++ b/README.rst @@ -251,15 +251,16 @@ Wrap '''' Unlike Modify, the Wrap operation adds a React component around the widget, and a single widget can receive more than -one wrap operation. Each wrapper function takes in a ``component`` and ``id`` prop. +one wrap operation. Each wrapper function takes in a ``component``, ``id`` and ``pluginProps`` prop. .. code-block:: - const wrapWidget = ({ component, idx }) => ( + const wrapWidget = ({ component, idx, pluginProps }) => (

This is a wrapper component that is placed around the widget.

{component}

With this wrapper, you can add anything before or after the widget.

+

You can use the pluginProps to pass in any additional props to the wrapper: {pluginProps.prop1}

); @@ -272,6 +273,9 @@ one wrap operation. Each wrapper function takes in a ``component`` and ``id`` pr { op: PLUGIN_OPERATIONS.Wrap, widgetId: 'default_contents', + pluginProps: { + prop1: 'prop1', + }, wrapper: wrapWidget, } diff --git a/src/plugins/PluginSlot.jsx b/src/plugins/PluginSlot.jsx index 99e53d8e..44bf6120 100644 --- a/src/plugins/PluginSlot.jsx +++ b/src/plugins/PluginSlot.jsx @@ -93,6 +93,7 @@ function BasePluginSlot({ wrapComponent( () => container, pluginConfig.wrappers, + pluginProps, ), ); } else { diff --git a/src/plugins/PluginSlot.test.jsx b/src/plugins/PluginSlot.test.jsx index e18244ac..567b9800 100644 --- a/src/plugins/PluginSlot.test.jsx +++ b/src/plugins/PluginSlot.test.jsx @@ -3,7 +3,7 @@ import React from 'react'; import '@testing-library/jest-dom'; import classNames from 'classnames'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { logError } from '@edx/frontend-platform/logging'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -78,7 +78,7 @@ function DefaultContents({ className, onClick, ...rest }) { ); } -function PluginSlotWrapper({ slotOptions, children }) { +function PluginSlotWrapper({ slotOptions, children, pluginProps }) { return ( {children} @@ -182,9 +183,39 @@ describe('PluginSlot', () => { { op: PLUGIN_OPERATIONS.Wrap, widgetId: 'default_contents', - wrapper: ({ component }) => ( + wrapper: ({ component, pluginProps }) => (
{component} +
+ {pluginProps?.prop1 && `This is a wrapper with ${pluginProps?.prop1}.`} +
+
+ ), + }, + ], + keepDefault: true, + }); + + const { getByTestId } = render(); + const customWrapper = getByTestId('custom-wrapper'); + const defaultContent = getByTestId('default_contents'); + expect(customWrapper).toContainElement(defaultContent); + const pluginProps = within(customWrapper).getByTestId('custom-wrapper-props'); + expect(pluginProps).toHaveTextContent('This is a wrapper with prop1.'); + }); + + it('should wrap a Plugin when using the "wrap" operation without passing props', () => { + usePluginSlot.mockReturnValueOnce({ + plugins: [ + { + op: PLUGIN_OPERATIONS.Wrap, + widgetId: 'default_contents', + wrapper: ({ component, pluginProps }) => ( +
+ {component} +
+ {`This is a wrapper without props: ${JSON.stringify(pluginProps)}`} +
), }, @@ -196,6 +227,8 @@ describe('PluginSlot', () => { const customWrapper = getByTestId('custom-wrapper'); const defaultContent = getByTestId('default_contents'); expect(customWrapper).toContainElement(defaultContent); + const pluginProps = within(customWrapper).getByTestId('custom-wrapper-no-props'); + expect(pluginProps).toHaveTextContent('This is a wrapper without props: {}'); }); it('should not render a widget if the Hide operation is applied to it', () => { diff --git a/src/plugins/data/utils.jsx b/src/plugins/data/utils.jsx index 68e4e3be..c79d0d71 100644 --- a/src/plugins/data/utils.jsx +++ b/src/plugins/data/utils.jsx @@ -82,13 +82,14 @@ export const organizePlugins = (defaultContents, plugins) => { * * @param {Function} renderComponent - Function that returns JSX (i.e. React Component) * @param {Array} wrappers - Array of components that each use a "component" prop to render the wrapped contents + * @params {object} pluginProps - Props defined in the PluginSlot * @returns {React.ReactElement} - The plugin component wrapped by any number of wrappers provided. */ -export const wrapComponent = (renderComponent, wrappers) => wrappers.reduce( +export const wrapComponent = (renderComponent, wrappers, pluginProps) => wrappers.reduce( // Disabled lint because currently we don't have a unique identifier for this // The "component" and "wrapper" are both functions // eslint-disable-next-line react/no-array-index-key - (component, wrapper, idx) => React.createElement(wrapper, { component, key: idx }), + (component, wrapper, idx) => React.createElement(wrapper, { component, key: idx, pluginProps }), renderComponent(), ); diff --git a/src/plugins/data/utils.test.jsx b/src/plugins/data/utils.test.jsx index b93c4163..602cebd5 100644 --- a/src/plugins/data/utils.test.jsx +++ b/src/plugins/data/utils.test.jsx @@ -22,10 +22,10 @@ const mockIsAdminWrapper = ({ widget }) => { return isAdmin ? widget : null; }; -const makeMockElementWrapper = (testId = 0) => function MockElementWrapper({ component }) { +const makeMockElementWrapper = (testId = 0) => function MockElementWrapper({ component, pluginProps }) { return (
- This is a wrapper. + {pluginProps?.prop1 && `This is a wrapper with ${pluginProps?.prop1}.`} {component}
); @@ -181,6 +181,18 @@ describe('organizePlugins', () => { describe('wrapComponent', () => { describe('when provided with a single wrapper in an array', () => { it('should wrap the provided component', () => { + const wrappedComponent = wrapComponent(mockRenderWidget, [makeMockElementWrapper()], { prop1: 'prop1' }); + + const { getByTestId } = render(wrappedComponent); + + const wrapper = getByTestId('wrapper0'); + const widget = getByTestId('widget'); + + expect(wrapper).toContainElement(widget); + expect(wrapper).toHaveTextContent('This is a wrapper with prop1.'); + }); + + it('should wrap the provided component without passing props', () => { const wrappedComponent = wrapComponent(mockRenderWidget, [makeMockElementWrapper()]); const { getByTestId } = render(wrappedComponent); @@ -189,6 +201,7 @@ describe('wrapComponent', () => { const widget = getByTestId('widget'); expect(wrapper).toContainElement(widget); + expect(wrapper).not.toHaveTextContent('This is a wrapper with prop1.'); }); }); describe('when provided with multiple wrappers in an array', () => {