Skip to content

Commit

Permalink
feat: create a footer lang selector
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Dec 11, 2024
1 parent ab74daa commit f321602
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 49 deletions.
17 changes: 3 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ Optionally, use the following variables to configure the Terms of Service Modal
* ``PRIVACY_POLICY_URL`` - The URL for the privacy policy.
* ``TERMS_OF_SERVICE_URL`` - The URL for the terms of service.
* ``TOS_AND_HONOR_CODE`` - The URL for the honor code.

* ``ENABLE_FOOTER_LANG_SELECTOR`` - A boolean to enable the lnaguage selector in the footer component.
* ``SITE_SUPPORTED_LENGUAGES`` - A list with all the languages to display in the selector.

Installation
============
Expand Down Expand Up @@ -94,12 +95,6 @@ This library has the following exports:
* ``messages``: Internationalization messages suitable for use with `@edx/frontend-platform/i18n <https://edx.github.io/frontend-platform/module-Internationalization.html>`_
* ``dist/footer.scss``: A SASS file which contains style information for the component. It should be imported into the micro-frontend's own SCSS file.

<Footer /> component props
==========================

* onLanguageSelected: Provides the footer with an event handler for when the user selects a
language from its dropdown.
* supportedLanguages: An array of objects representing available languages. See example below for object shape.

Terms of Service Modal
=======================
Expand Down Expand Up @@ -156,13 +151,7 @@ Component Usage Example::

...

<Footer
onLanguageSelected={(languageCode) => {/* set language */}}
supportedLanguages={[
{ label: 'English', value: 'en'},
{ label: 'Español', value: 'es' },
]}
/>
<Footer />

* `An example of minimal component and messages usage. <https://github.com/openedx/frontend-template-application/blob/3355bb3a96232390e9056f35b06ffa8f105ed7ca/src/index.jsx#L23>`_
* `An example of SCSS file usage. <https://github.com/openedx/frontend-template-application/blob/3cd5485bf387b8c479baf6b02bf59e3061dc3465/src/index.scss#L9>`_
Expand Down
22 changes: 6 additions & 16 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,10 @@ class SiteFooter extends React.Component {

render() {
const {
supportedLanguages,
onLanguageSelected,
logo,
intl,
} = this.props;
const showLanguageSelector = supportedLanguages.length > 0 && onLanguageSelected;
const { config } = this.context;
const { config, authenticatedUser } = this.context;

return (
<footer
Expand All @@ -145,18 +142,18 @@ class SiteFooter extends React.Component {
</div>
<FooterLinks intl={intl} />

{showLanguageSelector && (
{config.ENABLE_FOOTER_LANG_SELECTOR && (
<LanguageSelector
options={supportedLanguages}
onSubmit={onLanguageSelected}
options={config.SITE_SUPPORTED_LENGUAGES}
authenticatedUser={authenticatedUser}
/>
)}
</div>
</section>
<AdditionalLogosSection />
{
config.MODAL_UPDATE_TERMS_OF_SERVICE && <ModalToS />
}
config.MODAL_UPDATE_TERMS_OF_SERVICE && <ModalToS />
}
</footer>
);
}
Expand All @@ -167,17 +164,10 @@ SiteFooter.contextType = AppContext;
SiteFooter.propTypes = {
intl: intlShape.isRequired,
logo: PropTypes.string,
onLanguageSelected: PropTypes.func,
supportedLanguages: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
})),
};

SiteFooter.defaultProps = {
logo: undefined,
onLanguageSelected: undefined,
supportedLanguages: [],
};

export default injectIntl(SiteFooter);
Expand Down
39 changes: 24 additions & 15 deletions src/components/Footer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@
import React, { useMemo } from 'react';
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from '@testing-library/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';

import Footer from './Footer';

import { patchPreferences, postSetLang } from './data/api';

jest.mock('./data/api', () => ({
patchPreferences: jest.fn(),
postSetLang: jest.fn(),
}));

const FooterWithContext = ({ locale = 'pt-pt' }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config: {
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
},
}), []);

Expand All @@ -27,12 +36,18 @@ const FooterWithContext = ({ locale = 'pt-pt' }) => {
);
};

const FooterWithLanguageSelector = ({ languageSelected = () => {} }) => {
const FooterWithLanguageSelector = () => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
authenticatedUser: { username: 'user123' },
config: {
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
SITE_NAME: process.env.SITE_NAME,
ENABLE_FOOTER_LANG_SELECTOR: true,
SITE_SUPPORTED_LENGUAGES: [
{ label: 'English', value: 'en' },
{ label: 'Português', value: 'pt-pt' },
],
},
}), []);

Expand All @@ -41,13 +56,7 @@ const FooterWithLanguageSelector = ({ languageSelected = () => {} }) => {
<AppContext.Provider
value={contextValue}
>
<Footer
onLanguageSelected={languageSelected}
supportedLanguages={[
{ label: 'English', value: 'en' },
{ label: 'Português', value: 'pt-pt' },
]}
/>
<Footer />
</AppContext.Provider>
</IntlProvider>
);
Expand Down Expand Up @@ -76,11 +85,11 @@ describe('<Footer />', () => {
});

describe('handles language switching', () => {
it('calls onLanguageSelected prop when a language is changed', () => {
const mockHandleLanguageSelected = jest.fn();
render(<FooterWithLanguageSelector languageSelected={mockHandleLanguageSelected} />);
it('calls patchPreferences and postSetLang when a language is changed', async () => {
initializeMockApp();
render(<FooterWithLanguageSelector />);

fireEvent.submit(screen.getByTestId('site-footer-submit-btn'), {
await fireEvent.submit(screen.getByTestId('site-footer-submit-btn'), {
target: {
elements: {
'site-footer-language-select': {
Expand All @@ -89,8 +98,8 @@ describe('<Footer />', () => {
},
},
});

expect(mockHandleLanguageSelected).toHaveBeenCalledWith('pt-pt');
expect(patchPreferences).toHaveBeenCalledWith('user123', { prefLang: 'pt-pt' });
expect(postSetLang).toHaveBeenCalledWith('pt-pt');
});
});
});
32 changes: 28 additions & 4 deletions src/components/LanguageSelector.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { publish } from '@edx/frontend-platform';
import {
getLocale, injectIntl, intlShape, FormattedMessage, LOCALE_CHANGED, handleRtl,
} from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';

import { patchPreferences, postSetLang } from './data/api';

const onLanguageSelected = async (username, selectedLanguageCode) => {
try {
if (username) {
await patchPreferences(username, { prefLang: selectedLanguageCode });
await postSetLang(selectedLanguageCode);
}
publish(LOCALE_CHANGED, getLocale());
handleRtl();
} catch (error) {
logError(error);
}
};

const LanguageSelector = ({
intl, options, onSubmit, ...props
intl, options, authenticatedUser, ...props
}) => {
const handleSubmit = (e) => {
e.preventDefault();
const previousSiteLanguage = getLocale();
const languageCode = e.target.elements['site-footer-language-select'].value;
onSubmit(languageCode);
if (previousSiteLanguage !== languageCode) {
onLanguageSelected(authenticatedUser?.username, languageCode);
}
};

return (
Expand Down Expand Up @@ -47,8 +69,10 @@ const LanguageSelector = ({
};

LanguageSelector.propTypes = {
authenticatedUser: PropTypes.shape({
username: PropTypes.string,
}).isRequired,
intl: intlShape.isRequired,
onSubmit: PropTypes.func.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
Expand Down
32 changes: 32 additions & 0 deletions src/components/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';

export async function patchPreferences(username, params) {
let processedParams = snakeCaseObject(params);
processedParams = convertKeyNames(processedParams, {
pref_lang: 'pref-lang',
});

await getAuthenticatedHttpClient()
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});

return params;
}

export async function postSetLang(code) {
const formData = new FormData();
const requestConfig = {
headers: {
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
};
const url = `${getConfig().LMS_BASE_URL}/i18n/setlang/`;
formData.append('language', code);

await getAuthenticatedHttpClient()
.post(url, formData, requestConfig);
}

0 comments on commit f321602

Please sign in to comment.