Skip to content

[WIP] refactor: change lang selector logic #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,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.

Plugin
======
The footer can be replaced or modified using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
Expand All @@ -108,13 +101,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
8 changes: 1 addition & 7 deletions example/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@ subscribe(APP_READY, () => {
authenticatedUser: null,
config: getConfig(),
}}>
<Footer
onLanguageSelected={() => {}}
supportedLanguages={[
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
]}
/>
<Footer />
</AppContext.Provider>
</AppProvider>,
document.getElementById('root'),
Expand Down
23 changes: 5 additions & 18 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';

import messages from './Footer.messages';
import LanguageSelector from './LanguageSelector';

import LanguageSelectorSlot from '../plugin-slots/LanguageSelectorSlot';

ensureConfig([
'LMS_BASE_URL',
Expand Down Expand Up @@ -35,14 +36,10 @@ class SiteFooter extends React.Component {

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

return (
<footer
role="contentinfo"
Expand All @@ -61,12 +58,9 @@ class SiteFooter extends React.Component {
/>
</a>
<div className="flex-grow-1" />
{showLanguageSelector && (
<LanguageSelector
options={supportedLanguages}
onSubmit={onLanguageSelected}
/>
)}
<div className="mb-2">
<LanguageSelectorSlot />
</div>
</div>
</footer>
);
Expand All @@ -78,17 +72,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
78 changes: 21 additions & 57 deletions src/components/Footer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import React, { useMemo } from 'react';
import renderer from 'react-test-renderer';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider, changeUserSessionLanguage } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform/testing';

import Footer from './Footer';
import FooterSlot from '../plugin-slots/FooterSlot';
import StudioFooterHelpSectionSlot from '../plugin-slots/StudioFooterHelpSectionSlot';

const FooterWithContext = ({ locale = 'es' }) => {
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
changeUserSessionLanguage: jest.fn(),
}));

const FooterWithContext = ({ locale = 'en' }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config: {
Expand All @@ -30,65 +35,24 @@ const FooterWithContext = ({ locale = 'es' }) => {
);
};

const FooterWithLanguageSelector = ({ languageSelected = () => {} }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config: {
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
},
}), []);

return (
<IntlProvider locale="en">
<AppContext.Provider
value={contextValue}
>
<Footer
onLanguageSelected={languageSelected}
supportedLanguages={[
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
]}
/>
</AppContext.Provider>
</IntlProvider>
);
};

describe('<Footer />', () => {
describe('renders correctly', () => {
it('renders without a language selector', () => {
const tree = renderer
.create(<FooterWithContext locale="en" />)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders without a language selector in es', () => {
const tree = renderer
.create(<FooterWithContext locale="es" />)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders with a language selector', () => {
const tree = renderer
.create(<FooterWithLanguageSelector />)
.toJSON();
expect(tree).toMatchSnapshot();
});
beforeEach(() => { initializeMockApp(); });

it('renders correctly', () => {
const tree = renderer
.create(<FooterWithContext />)
.toJSON();
expect(tree).toMatchSnapshot();
});

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

await user.selectOptions(screen.getByRole('combobox'), 'es');
await user.click(screen.getByTestId('site-footer-submit-btn'));
await user.click(screen.getByRole('button'), 'English');
await user.click(screen.getByRole('button', { name: 'Español (Latinoamérica)' }));

expect(mockHandleLanguageSelected).toHaveBeenCalledWith('es');
});
expect(changeUserSessionLanguage).toHaveBeenCalledWith('es-419');
});
});

Expand Down
97 changes: 52 additions & 45 deletions src/components/LanguageSelector.jsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
changeUserSessionLanguage, getPrimaryLanguageSubtag, getSupportedLocaleList, getLocale,
} from '@edx/frontend-platform/i18n';
import { Dropdown, Scrollable, useWindowSize } from '@openedx/paragon';
import { Language } from '@openedx/paragon/icons';

const getLocaleName = (locale) => {
const langName = new Intl.DisplayNames([locale], { type: 'language', languageDisplay: 'standard' }).of(locale);
return langName.replace(/^\w/, (c) => c.toUpperCase());
};

const LanguageSelector = ({
intl, options, onSubmit, ...props
supportedLanguages = [],
}) => {
const handleSubmit = (e) => {
e.preventDefault();
const languageCode = e.target.elements['site-footer-language-select'].value;
onSubmit(languageCode);
const [currentLocale, setLocale] = useState(getLocale());
const { width } = useWindowSize();
const options = supportedLanguages.length > 0 ? supportedLanguages : getSupportedLocaleList();

const handleSelect = async (selectedLocale) => {
if (currentLocale !== selectedLocale) {
await changeUserSessionLanguage(selectedLocale);
}
setLocale(selectedLocale);
};

const currentLocaleLabel = useMemo(() => {
if (width < 576) {
return '';
}
if (width < 768) {
return getPrimaryLanguageSubtag(currentLocale).toUpperCase();
}
return getLocaleName(currentLocale);
}, [currentLocale, width]);

return (
<form
className="form-inline"
onSubmit={handleSubmit}
{...props}
>
<div className="form-group">
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label htmlFor="site-footer-language-select" className="d-inline-block m-0">
<FormattedMessage
id="footer.languageForm.select.label"
defaultMessage="Choose Language"
description="The label for the laguage select part of the language selection form."
/>
</label>
<select
id="site-footer-language-select"
className="form-control-sm mx-2"
name="site-footer-language-select"
defaultValue={intl.locale}
>
{options.map(({ value, label }) => <option key={value} value={value}>{label}</option>)}
</select>
<button data-testid="site-footer-submit-btn" className="btn btn-outline-primary btn-sm" type="submit">
<FormattedMessage
id="footer.languageForm.submit.label"
defaultMessage="Apply"
description="The label for button to submit the language selection form."
/>
</button>
</div>
</form>
<Dropdown onSelect={handleSelect}>
<Dropdown.Toggle
id="lang-selector-dropdown"
iconBefore={Language}
variant="outline-primary"
size="sm"
>
{currentLocaleLabel}
</Dropdown.Toggle>
<Dropdown.Menu>
<Scrollable style={{ maxHeight: '40vh' }}>
{options.map((locale) => (
<Dropdown.Item key={`lang-selector-${locale}`} eventKey={locale}>
{getLocaleName(locale)}
</Dropdown.Item>
))}
</Scrollable>
</Dropdown.Menu>
</Dropdown>
);
};

LanguageSelector.propTypes = {
intl: intlShape.isRequired,
onSubmit: PropTypes.func.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
})).isRequired,
supportedLanguages: PropTypes.arrayOf(PropTypes.string),
};

export default injectIntl(LanguageSelector);
export default LanguageSelector;
Loading
Loading