Skip to content

Commit

Permalink
✨(website) add language picker
Browse files Browse the repository at this point in the history
Add a language picker to the header of the website.
This will allow users to switch between languages.
  • Loading branch information
AntoLC committed Aug 8, 2023
1 parent a1ad3c0 commit a9f30a2
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 98 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- Add a shared media widget directly in the video player
- Add a transcript plugin to the video player
- Add a link to LTI resources to retrieve them in the standalone website
- Add Language Picker in the standalone website (#2366)

### Changed

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 22 additions & 7 deletions src/frontend/apps/standalone_site/src/features/App/App.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { Grommet, ResponsiveContext } from 'grommet';
import {
playlistMockFactory,
useCurrentUser,
Expand All @@ -18,6 +20,13 @@ const consoleWarn = jest
.spyOn(console, 'warn')
.mockImplementation(() => jest.fn());

jest.mock('grommet', () => ({
...jest.requireActual('grommet'),
Grommet: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

window.scrollTo = jest.fn();
window.isCDNLoaded = true;

Expand Down Expand Up @@ -81,7 +90,13 @@ describe('<App />', () => {
full_name: 'John Doe',
}),
});
render(<App />);
render(
<Grommet>
<ResponsiveContext.Provider value="large">
<App />
</ResponsiveContext.Provider>
</Grommet>,
);

expect(await screen.findByText(/John Doe/i)).toBeInTheDocument();
expect(
Expand Down Expand Up @@ -111,18 +126,18 @@ describe('<App />', () => {
}),
{ virtual: true },
);
const languageGetter = jest.spyOn(window.navigator, 'language', 'get');
languageGetter.mockReturnValue('fr');

render(<App />);

expect(await screen.findByText(/John Doe/i)).toBeInTheDocument();
await userEvent.click(
await screen.findByLabelText(/Language Picker; Selected: en/i),
);

await userEvent.click(screen.getByText(/Français/i));

expect(
await screen.findByRole('menuitem', { name: /Mon Tableau de bord/i }),
).toBeInTheDocument();
expect(
await screen.findByText(/some welcome classroom/i),
).toBeInTheDocument();
});

test('the content features are correcty loaded', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';

import { ConfigResponse } from 'api/useConfig';
import { useContentFeatures } from 'features/Contents';
import { useLanguageStore } from 'features/Language/store/languageStore';

import AppConfig from './AppConfig';

Expand Down Expand Up @@ -244,8 +245,6 @@ describe('AppConfig', () => {
}),
{ virtual: true },
);
const languageGetter = jest.spyOn(window.navigator, 'language', 'get');
languageGetter.mockReturnValue('fr');

deferredConfig.resolve(config);

Expand All @@ -255,6 +254,9 @@ describe('AppConfig', () => {
</AppConfig>,
);

expect(await screen.findByText(/My test/i)).toBeInTheDocument();

useLanguageStore.getState().setLanguage('fr');
expect(await screen.findByText(/Mon test/i)).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('<Header />', () => {
render(<Header />);
expect(screen.getByRole('menubar')).toBeInTheDocument();
expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
expect(screen.getByText(/language/i)).toBeInTheDocument();
expect(
screen.getByLabelText(/Language Picker; Selected: en/i),
).toBeInTheDocument();
});

test('scroll and update background', () => {
Expand Down
167 changes: 99 additions & 68 deletions src/frontend/apps/standalone_site/src/features/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, DropButton, Text } from 'grommet';
import { normalizeColor } from 'grommet/utils';
import { Nullable, theme } from 'lib-common';
import { Breakpoints, Nullable, theme } from 'lib-common';
import {
AnonymousUser,
useCurrentUser,
Expand All @@ -17,6 +17,7 @@ import { ReactComponent as MarshaLogoIcon } from 'assets/svg/logo_marsha.svg';
import { ReactComponent as LogoutIcon } from 'assets/svg/logout.svg';
import { ReactComponent as SettingsIcon } from 'assets/svg/settings.svg';
import { logout } from 'features/Authentication';
import { LanguagePicker } from 'features/Language/';
import { Burger } from 'features/Menu';
import { routes } from 'routes/routes';

Expand Down Expand Up @@ -97,7 +98,7 @@ const messages = defineMessages({
const Header = forwardRef<Nullable<HTMLDivElement>>((_props, ref) => {
const intl = useIntl();
const [isScrollTop, setIsScrollTop] = useState(true);
const { isDesktop } = useResponsive();
const { isDesktop, breakpoint, isSmallerBreakpoint } = useResponsive();
const { currentUser } = useCurrentUser((state) => ({
currentUser: state.currentUser,
}));
Expand Down Expand Up @@ -138,12 +139,25 @@ const Header = forwardRef<Nullable<HTMLDivElement>>((_props, ref) => {
justify="between"
margin={{ bottom: 'small' }}
pad={{ right: 'medium' }}
gap="medium"
gap={
isSmallerBreakpoint(breakpoint, Breakpoints.small) ? 'none' : 'medium'
}
>
<Burger width={60} height={60} aria-controls="menu" />
<Burger
width={60}
height={60}
aria-controls="menu"
style={{ flex: 'none' }}
/>
<Link to={routes.HOMEPAGE.path} style={{ color: 'currentColor' }}>
{siteConfig.is_default_site || !siteConfig.logo_url ? (
<MarshaLogoIcon width={117} height={80} />
<MarshaLogoIcon
height={
isSmallerBreakpoint(breakpoint, Breakpoints.small)
? '100%'
: '80px'
}
/>
) : (
<Box margin={{ top: 'small' }}>
<img src={siteConfig.logo_url} alt="Home" />
Expand All @@ -152,69 +166,86 @@ const Header = forwardRef<Nullable<HTMLDivElement>>((_props, ref) => {
</Link>
</Box>

<DropButton
open={isDropOpen}
onOpen={() => {
setIsDropOpen(true);
}}
onClose={() => {
setIsDropOpen(false);
}}
plain
label={
<Box direction="row" align="center" gap="small" justify="end">
<Text>{fullName}</Text>
<AvatarIcon
title={intl.formatMessage(messages.iconTitle)}
width={42}
height={42}
/>
</Box>
}
dropAlign={{ top: 'bottom', right: 'right' }}
dropContent={
<Box direction="column" margin="small" gap="small">
<NavLinkStyled
to={routes.PROFILE.path}
onClick={() => {
setIsDropOpen(false);
}}
>
<AvatarIcon />
{intl.formatMessage(messages.profile)}
</NavLinkStyled>

<NavLinkStyled
to={routes.PROFILE.subRoutes.PROFILE_SETTINGS.path}
onClick={() => {
setIsDropOpen(false);
}}
>
<SettingsIcon /> {intl.formatMessage(messages.settings)}
</NavLinkStyled>

<ButtonStyled
plain
onClick={() => {
logout();
}}
>
<LogoutIcon />
{intl.formatMessage(messages.logout)}
</ButtonStyled>
</Box>
}
dropProps={{
round: 'xsmall',
border: {
color: 'blue-active',
size: '2px',
},
style: {
zIndex: 991,
},
}}
/>
<Box direction="row" align="center" gap="small" justify="end">
<LanguagePicker />
<DropButton
open={isDropOpen}
onOpen={() => {
setIsDropOpen(true);
}}
onClose={() => {
setIsDropOpen(false);
}}
plain
label={
<Box direction="row" align="center" gap="small" justify="end">
{!isSmallerBreakpoint(breakpoint, Breakpoints.xsmall) && (
<Text
truncate
style={{
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 2,
display: '-webkit-box',
maxWidth: '90px',
whiteSpace: 'normal',
}}
>
{fullName}
</Text>
)}
<AvatarIcon
style={{ flex: 'none' }}
title={intl.formatMessage(messages.iconTitle)}
width={42}
height={42}
/>
</Box>
}
dropAlign={{ top: 'bottom', right: 'right' }}
dropContent={
<Box direction="column" margin="small" gap="small">
<NavLinkStyled
to={routes.PROFILE.path}
onClick={() => {
setIsDropOpen(false);
}}
>
<AvatarIcon />
{intl.formatMessage(messages.profile)}
</NavLinkStyled>

<NavLinkStyled
to={routes.PROFILE.subRoutes.PROFILE_SETTINGS.path}
onClick={() => {
setIsDropOpen(false);
}}
>
<SettingsIcon /> {intl.formatMessage(messages.settings)}
</NavLinkStyled>

<ButtonStyled
plain
onClick={() => {
logout();
}}
>
<LogoutIcon />
{intl.formatMessage(messages.logout)}
</ButtonStyled>
</Box>
}
dropProps={{
round: 'xsmall',
border: {
color: 'blue-active',
size: '2px',
},
style: {
zIndex: 991,
},
}}
/>
</Box>
</HeaderBox>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib-tests';

import { useLanguageStore } from '../store/languageStore';

import LanguagePicker from './LanguagePicker';

describe('<LanguagePicker />', () => {
beforeEach(() => {});

it('renders LanguagePicker', async () => {
render(<LanguagePicker />);
expect(screen.getByText(/language/i)).toBeInTheDocument();

const select = screen.getByLabelText(/Language Picker; Selected: en/i);
expect(select).toBeInTheDocument();
await userEvent.click(select);
expect(screen.getByText(/Français/i)).toBeInTheDocument();
expect(screen.getByText(/English/i)).toBeInTheDocument();
});

it('changes to another language', async () => {
render(<LanguagePicker />);

await userEvent.click(
screen.getByLabelText(/Language Picker; Selected: en/i),
);

await userEvent.click(screen.getByText(/Français/i));
expect(
screen.getByLabelText(/Language Picker; Selected: fr/i),
).toBeInTheDocument();

expect(useLanguageStore.getState().language).toEqual('fr_FR');
});
});
Loading

0 comments on commit a9f30a2

Please sign in to comment.