diff --git a/src/components/Footer.test.jsx b/src/components/Footer.test.jsx
index e605ca9ea8..0039b1481a 100644
--- a/src/components/Footer.test.jsx
+++ b/src/components/Footer.test.jsx
@@ -8,6 +8,7 @@ import { AppContext } from '@edx/frontend-platform/react';
import Footer from './Footer';
import FooterSlot from '../plugin-slots/FooterSlot';
+import StudioFooterHelpSectionSlot from '../plugin-slots/StudioFooterHelpSectionSlot';
const FooterWithContext = ({ locale = 'es' }) => {
const contextValue = useMemo(() => ({
@@ -90,3 +91,32 @@ describe('', () => {
});
});
});
+
+describe('', () => {
+ const SectionWithContext = ({ locale = 'es' }) => {
+ const contextValue = useMemo(() => ({
+ authenticatedUser: null,
+ config: {
+ LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
+ LMS_BASE_URL: process.env.LMS_BASE_URL,
+ },
+ }), []);
+
+ return (
+
+
+
+
+
+ );
+ };
+
+ it('renders correctly', () => {
+ const tree = renderer
+ .create()
+ .toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/components/__snapshots__/Footer.test.jsx.snap b/src/components/__snapshots__/Footer.test.jsx.snap
index ab6c2f34c7..7e93dcad72 100644
--- a/src/components/__snapshots__/Footer.test.jsx.snap
+++ b/src/components/__snapshots__/Footer.test.jsx.snap
@@ -128,3 +128,68 @@ exports[` renders correctly renders without a language selector in es
`;
+
+exports[` renders correctly 1`] = `
+[
+
+
+
+
+
,
+ ,
+]
+`;
diff --git a/src/components/studio-footer/StudioFooter.jsx b/src/components/studio-footer/StudioFooter.jsx
index a41bc0a132..adadd8c3e9 100644
--- a/src/components/studio-footer/StudioFooter.jsx
+++ b/src/components/studio-footer/StudioFooter.jsx
@@ -1,21 +1,19 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext } from 'react';
import isEmpty from 'lodash/isEmpty';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import {
ActionRow,
- Button,
Container,
Hyperlink,
- TransitionReplace,
} from '@openedx/paragon';
-import { ExpandLess, ExpandMore, Help } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import messages from './messages';
import StudioFooterLogoSlot from '../../plugin-slots/StudioFooterLogoSlot';
+import StudioFooterHelpSectionSlot from '../../plugin-slots/StudioFooterHelpSectionSlot';
ensureConfig([
'LMS_BASE_URL',
@@ -32,76 +30,18 @@ const StudioFooter = ({
containerProps,
}) => {
const intl = useIntl();
- const [isOpen, setIsOpen] = useState(false);
const { config } = useContext(AppContext);
const { containerClassName, ...restContainerProps } = containerProps || {};
return (
<>
-
-
-
-
-
+
-
- {isOpen ? (
-
-
-
-
-
-
- {!isEmpty(config.SUPPORT_EMAIL) && (
-
- )}
-
-
- ) : null}
-
© {new Date().getFullYear()} {config.SITE_NAME}
diff --git a/src/components/studio-footer/StudioFooter.test.jsx b/src/components/studio-footer/StudioFooter.test.jsx
index c1eaf062d5..ecb818f14a 100644
--- a/src/components/studio-footer/StudioFooter.test.jsx
+++ b/src/components/studio-footer/StudioFooter.test.jsx
@@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import StudioFooterSlot from '../../plugin-slots/StudioFooterSlot';
@@ -77,7 +78,7 @@ describe('Footer', () => {
render();
const helpToggleButton = screen.getByText(messages.openHelpButtonLabel.defaultMessage);
await user.click(helpToggleButton);
- expect(screen.getByTestId('openEdXPortalButton')).toBeVisible();
+ expect(screen.getByTestId('openEdXDemoCourseButton')).toBeVisible();
});
it('should not show contact us button', async () => {
const user = userEvent.setup();
diff --git a/src/components/studio-footer/help-components/HelpButton.jsx b/src/components/studio-footer/help-components/HelpButton.jsx
new file mode 100644
index 0000000000..587e8f725f
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpButton.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Button,
+} from '@openedx/paragon';
+import { ExpandLess, ExpandMore, Help } from '@openedx/paragon/icons';
+import PropTypes from 'prop-types';
+import messages from '../messages';
+
+const HelpButton = ({ isOpen, setIsOpen }) => {
+ const intl = useIntl();
+ return (
+
+ );
+};
+
+HelpButton.propTypes = {
+ setIsOpen: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+};
+
+export default HelpButton;
diff --git a/src/components/studio-footer/help-components/HelpButton.test.jsx b/src/components/studio-footer/help-components/HelpButton.test.jsx
new file mode 100644
index 0000000000..e1a5bd8ee9
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpButton.test.jsx
@@ -0,0 +1,55 @@
+import React, { useMemo } from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppContext } from '@edx/frontend-platform/react';
+import HelpButton from './HelpButton';
+import '@testing-library/jest-dom';
+
+// eslint-disable-next-line react/prop-types
+const ButtonWithContext = ({ locale = 'en', isOpen, setIsOpen }) => {
+ const contextValue = useMemo(() => ({
+ authenticatedUser: null,
+ config: { },
+ }), []);
+
+ return (
+
+
+
+
+
+ );
+};
+
+describe('HelpButton', () => {
+ const mockSetIsOpen = jest.fn();
+
+ beforeEach(() => {
+ mockSetIsOpen.mockClear();
+ });
+
+ it('renders with "open" label when isOpen is false', () => {
+ render();
+ expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
+ expect(screen.getByRole('button')).toHaveTextContent(/help|open/i);
+ });
+
+ it('renders with "close" label when isOpen is true', () => {
+ render();
+ expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
+ expect(screen.getByRole('button')).toHaveTextContent(/close|help/i);
+ });
+
+ it('calls setIsOpen with the toggled value when clicked', async () => {
+ const user = userEvent.setup();
+ const { rerender } = render();
+ await user.click(screen.getByTestId('helpToggleButton'));
+ expect(mockSetIsOpen).toHaveBeenCalledWith(true);
+ rerender();
+ await user.click(screen.getByTestId('helpToggleButton'));
+ expect(mockSetIsOpen).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/src/components/studio-footer/help-components/HelpContent.jsx b/src/components/studio-footer/help-components/HelpContent.jsx
new file mode 100644
index 0000000000..a89ec282f3
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpContent.jsx
@@ -0,0 +1,76 @@
+import React, { useContext } from 'react';
+import { AppContext } from '@edx/frontend-platform/react';
+import isEmpty from 'lodash/isEmpty';
+import { ensureConfig } from '@edx/frontend-platform';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow,
+ Button,
+} from '@openedx/paragon';
+import messages from '../messages';
+
+ensureConfig([
+ 'SUPPORT_EMAIL',
+], 'Studio Footer Help Content component');
+
+const BUTTONS = [
+ {
+ as: 'a',
+ href: 'https://docs.openedx.org/',
+ size: 'sm',
+ message: messages.edxDocumentationButtonLabel,
+ dataTestid: null,
+ },
+ {
+ as: 'a',
+ href: 'https://openedx.org/',
+ size: 'sm',
+ message: messages.openEdxPortalButtonLabel,
+ dataTestid: 'openEdXPortalButton',
+ },
+ {
+ as: 'a',
+ href: 'https://www.edx.org/course/edx101-overview-of-creating-an-edx-course#.VO4eaLPF-n1',
+ size: 'sm',
+ message: messages.edx101ButtonLabel,
+ dataTestid: 'openEdXDemoCourseButton',
+ },
+ {
+ as: 'a',
+ href: 'https://www.edx.org/course/studiox-creating-a-course-with-edx-studio',
+ size: 'sm',
+ message: messages.studioXButtonLabel,
+ dataTestid: null,
+ },
+];
+
+const HelpContent = () => {
+ const { config } = useContext(AppContext);
+ return (
+
+
+
+ {BUTTONS.map(({
+ as, href, size, message, dataTestid,
+ }) => (
+
+ ))}
+
+ {!isEmpty(config.SUPPORT_EMAIL) && (
+
+ )}
+
+
+ );
+};
+
+export default HelpContent;
diff --git a/src/components/studio-footer/help-components/HelpContent.test.jsx b/src/components/studio-footer/help-components/HelpContent.test.jsx
new file mode 100644
index 0000000000..9e2a0538bb
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpContent.test.jsx
@@ -0,0 +1,44 @@
+import React, { useMemo } from 'react';
+import { render, screen } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppContext } from '@edx/frontend-platform/react';
+import HelpContent from './HelpContent';
+import '@testing-library/jest-dom';
+import messages from '../messages';
+
+// eslint-disable-next-line react/prop-types
+const ContentWithContext = ({ locale = 'en', config = {} }) => {
+ const contextValue = useMemo(() => ({
+ authenticatedUser: null,
+ config,
+ }), [config]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+describe('HelpContent', () => {
+ it('renders all help buttons', () => {
+ const config = { SUPPORT_EMAIL: 'support@example.com' };
+ render();
+ expect(screen.getByText(messages.edxDocumentationButtonLabel.defaultMessage)).toBeInTheDocument();
+ expect(screen.getByText(messages.edx101ButtonLabel.defaultMessage)).toBeInTheDocument();
+ });
+
+ it('does not render contact button if SUPPORT_EMAIL is empty', () => {
+ render();
+ expect(screen.queryByTestId('contactUsButton')).not.toBeInTheDocument();
+ });
+
+ it('renders ActionRow with correct test id', () => {
+ render();
+ expect(screen.getByTestId('helpButtonRow')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/studio-footer/help-components/HelpSection.jsx b/src/components/studio-footer/help-components/HelpSection.jsx
new file mode 100644
index 0000000000..5169a4025a
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpSection.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import {
+ Container,
+ TransitionReplace,
+} from '@openedx/paragon';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import StudioFooterHelpButtonSlot from '../../../plugin-slots/StudioFooterHelpButtonSlot';
+import StudioFooterHelpContentlot from '../../../plugin-slots/StudioFooterHelpContentSlot';
+
+const HelpSection = ({ containerProps }) => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const { containerClassName, ...restContainerProps } = containerProps || {};
+ return (
+ <>
+
+
+ {isOpen && }
+
+ >
+ );
+};
+
+HelpSection.propTypes = {
+ containerProps: PropTypes.shape(Container.propTypes),
+};
+
+HelpSection.defaultProps = {
+ containerProps: {},
+};
+
+export default HelpSection;
diff --git a/src/components/studio-footer/help-components/HelpSection.test.jsx b/src/components/studio-footer/help-components/HelpSection.test.jsx
new file mode 100644
index 0000000000..de57df6abe
--- /dev/null
+++ b/src/components/studio-footer/help-components/HelpSection.test.jsx
@@ -0,0 +1,52 @@
+import React, { useMemo } from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppContext } from '@edx/frontend-platform/react';
+import HelpSection from './HelpSection';
+import '@testing-library/jest-dom';
+
+// eslint-disable-next-line react/prop-types
+const HelpSectionWithContext = ({ locale = 'en', config = {}, containerProps = null }) => {
+ const contextValue = useMemo(() => ({
+ authenticatedUser: null,
+ config,
+ }), [config]);
+
+ return (
+
+
+
+
+
+ );
+};
+
+describe('HelpSection', () => {
+ it('renders the HelpButton', () => {
+ render();
+ expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
+ });
+
+ it('does not show HelpContent by default', () => {
+ render();
+ expect(screen.queryByTestId('helpButtonRow')).not.toBeInTheDocument();
+ });
+
+ it('shows HelpContent when HelpButton is clicked', () => {
+ render();
+ const button = screen.getByTestId('helpToggleButton');
+ fireEvent.click(button);
+ expect(screen.getByTestId('helpButtonRow')).toBeInTheDocument();
+ });
+
+ it('hides HelpContent when HelpButton is clicked twice', () => {
+ render();
+ const button = screen.getByTestId('helpToggleButton');
+ fireEvent.click(button);
+ expect(screen.queryByTestId('helpButtonRow')).toBeInTheDocument();
+ fireEvent.click(button);
+ expect(screen.queryByTestId('helpButtonRow')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/plugin-slots/StudioFooterHelpButtonSlot/README.md b/src/plugin-slots/StudioFooterHelpButtonSlot/README.md
new file mode 100644
index 0000000000..4bd08908ba
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpButtonSlot/README.md
@@ -0,0 +1,47 @@
+# StudioFooterLogoSlot
+
+### Slot ID: `org.openedx.frontend.layout.studio_footer_help_button.v1`
+
+## Description
+
+This slot is used to repace/modify/hide the help button to the studio footer.
+
+## Examples
+
+### Add your custom button.
+
+The following `env.config.jsx` will add a custom help button to the studio footer.
+
+
+
+```jsx
+import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
+import {
+ Button,
+} from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.layout.studio_footer_help_button.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'studio_footer_helpbutton_addition',
+ type: DIRECT_PLUGIN,
+ priority: 40,
+ RenderWidget: ({isOpen, setIsOpen}) => {
+ return (
+
+ )
+ }
+ }
+ },
+ ],
+ }
+ },
+};
+
+export default config;
+```
diff --git a/src/plugin-slots/StudioFooterHelpButtonSlot/images/custom_help_button.png b/src/plugin-slots/StudioFooterHelpButtonSlot/images/custom_help_button.png
new file mode 100644
index 0000000000..a29c8298e7
Binary files /dev/null and b/src/plugin-slots/StudioFooterHelpButtonSlot/images/custom_help_button.png differ
diff --git a/src/plugin-slots/StudioFooterHelpButtonSlot/index.jsx b/src/plugin-slots/StudioFooterHelpButtonSlot/index.jsx
new file mode 100644
index 0000000000..463070b3b9
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpButtonSlot/index.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import PropTypes from 'prop-types';
+import HelpButton from '../../components/studio-footer/help-components/HelpButton';
+
+const StudioFooterHelpButtonSlot = ({ isOpen, setIsOpen }) => (
+
+);
+
+StudioFooterHelpButtonSlot.propTypes = {
+ setIsOpen: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool.isRequired,
+};
+
+export default StudioFooterHelpButtonSlot;
diff --git a/src/plugin-slots/StudioFooterHelpContentSlot/README.md b/src/plugin-slots/StudioFooterHelpContentSlot/README.md
new file mode 100644
index 0000000000..679435a7b4
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpContentSlot/README.md
@@ -0,0 +1,50 @@
+# StudioFooterLogoSlot
+
+### Slot ID: `org.openedx.frontend.layout.studio_footer_help-content.v1`
+
+## Description
+
+This slot is used to repace/modify/hide the help content to the studio footer.
+
+## Examples
+
+### Add a custom help content.
+
+The following `env.config.jsx` will add a custom help content to the studio footer.
+
+
+
+```jsx
+import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
+import {
+ Hyperlink,
+} from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.layout.studio_footer_help-content.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'studio_footer_helpcontent_addition',
+ type: DIRECT_PLUGIN,
+ priority: 40,
+ RenderWidget: () => {
+ return (
+
+
Custom Help content
+
more help
+
+ )
+ }
+ }
+ },
+ ],
+ }
+ },
+};
+
+export default config;
+```
diff --git a/src/plugin-slots/StudioFooterHelpContentSlot/images/custom_help_content.png b/src/plugin-slots/StudioFooterHelpContentSlot/images/custom_help_content.png
new file mode 100644
index 0000000000..1fe530d96a
Binary files /dev/null and b/src/plugin-slots/StudioFooterHelpContentSlot/images/custom_help_content.png differ
diff --git a/src/plugin-slots/StudioFooterHelpContentSlot/index.jsx b/src/plugin-slots/StudioFooterHelpContentSlot/index.jsx
new file mode 100644
index 0000000000..bd62b9767d
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpContentSlot/index.jsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import HelpContent from '../../components/studio-footer/help-components/HelpContent';
+
+const StudioFooterHelpContentSlot = () => (
+
+);
+
+export default StudioFooterHelpContentSlot;
diff --git a/src/plugin-slots/StudioFooterHelpSectionSlot/README.md b/src/plugin-slots/StudioFooterHelpSectionSlot/README.md
new file mode 100644
index 0000000000..6f4aceaf40
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpSectionSlot/README.md
@@ -0,0 +1,53 @@
+# StudioFooterLogoSlot
+
+### Slot ID: `org.openedx.frontend.layout.studio_footer_help_section.v1`
+
+## Description
+
+This slot is used to repace/modify/hide the help section to the studio footer.
+
+
+
+## Examples
+
+### Customize help section.
+
+The following `env.config.jsx` will add a custom help section to the studio footer.
+
+```jsx
+import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
+import {
+ Hyperlink,
+ Button,
+} from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.layout.studio_footer_help_section.v1': {
+ keepDefault: false,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'studio_footer_helpsection_addition',
+ type: DIRECT_PLUGIN,
+ priority: 40,
+ RenderWidget: () => {
+ return (
+
+
Custom Help Section custizable as needed
+
+
About
+
Info
+
+ )
+ }
+ }
+ },
+ ],
+ }
+ },
+};
+
+export default config;
+```
diff --git a/src/plugin-slots/StudioFooterHelpSectionSlot/images/custom_help_section.png b/src/plugin-slots/StudioFooterHelpSectionSlot/images/custom_help_section.png
new file mode 100644
index 0000000000..e410f33667
Binary files /dev/null and b/src/plugin-slots/StudioFooterHelpSectionSlot/images/custom_help_section.png differ
diff --git a/src/plugin-slots/StudioFooterHelpSectionSlot/index.jsx b/src/plugin-slots/StudioFooterHelpSectionSlot/index.jsx
new file mode 100644
index 0000000000..4c374bc129
--- /dev/null
+++ b/src/plugin-slots/StudioFooterHelpSectionSlot/index.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import PropTypes from 'prop-types';
+import {
+ Container,
+} from '@openedx/paragon';
+import HelpSection from '../../components/studio-footer/help-components/HelpSection';
+
+const StudioFooterHelpSectionSlot = ({ containerProps }) => (
+
+);
+
+StudioFooterHelpSectionSlot.propTypes = {
+ containerProps: PropTypes.shape(Container.propTypes),
+};
+
+export default StudioFooterHelpSectionSlot;