feat(client): language dropdown in the side menu is now a drop down (#43729)

* feat(UI): language in the side menu is now a drop down. navigation items are now text wrapped

* fix: use redux navigation to redirect links instead

* fix: fix to use clientLocale as curent language instead

* fix: tests to use clientLocale
pull/43843/head
Lim Shang Yi 2021-10-15 21:08:35 +08:00 committed by GitHub
parent e61bc3ba5d
commit 3dbe40410c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 38 deletions

View File

@ -8,11 +8,15 @@ import { create, ReactTestRendererJSON } from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';
import envData from '../../../../config/env.json';
import {
availableLangs,
langDisplayNames
} from '../../../../config/i18n/all-langs';
import AuthOrProfile from './components/auth-or-profile';
import { NavLinks } from './components/nav-links';
import { UniversalNav } from './components/universal-nav';
const { apiLocation } = envData;
const { apiLocation, clientLocale } = envData;
jest.mock('../../analytics');
@ -63,7 +67,9 @@ describe('<NavLinks />', () => {
hasCurriculumNavItem(view) &&
hasForumNavItem(view) &&
hasNewsNavItem(view) &&
hasRadioNavItem(view)
hasRadioNavItem(view) &&
hasLanguageHeader(view) &&
hasLanguageDropdown(view)
).toBeTruthy();
});
@ -123,7 +129,62 @@ describe('<NavLinks />', () => {
hasForumNavItem(view) &&
hasNewsNavItem(view) &&
hasRadioNavItem(view) &&
hasSignOutNavItem(view)
hasSignOutNavItem(view) &&
hasLanguageHeader(view) &&
hasLanguageDropdown(view)
).toBeTruthy();
});
it('has expected available languages in the language dropdown', () => {
const landingPageProps = {
fetchState: {
pending: false
},
user: {
isDonating: true,
username: 'moT01',
theme: 'default'
},
i18n: {
language: 'en'
},
t: t,
toggleNightMode: (theme: string) => theme
};
const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />);
const view = utils.getRenderOutput();
expect(
hasLanguageHeader(view) &&
hasLanguageDropdown(view) &&
hasAllAvailableLanguagesInDropdown(view)
).toBeTruthy();
});
it('has default language selected in language dropdown based on client config', () => {
const landingPageProps = {
fetchState: {
pending: false
},
user: {
isDonating: true,
username: 'moT01',
theme: 'default'
},
i18n: {
language: 'en'
},
t: t,
toggleNightMode: (theme: string) => theme
};
const utils = ShallowRenderer.createRenderer();
utils.render(<NavLinks {...landingPageProps} />);
const view = utils.getRenderOutput();
expect(
hasLanguageHeader(view) &&
hasLanguageDropdown(view) &&
hasAllAvailableLanguagesInDropdown(view) &&
hasDefaultLanguageInLanguageDropdown(view, clientLocale)
).toBeTruthy();
});
});
@ -205,6 +266,7 @@ const navigationLinks = (component: JSX.Element, key: string) => {
);
return target.props;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const profileNavItem = (component: any) => component.children[0];
@ -268,6 +330,38 @@ const hasRadioNavItem = (component: JSX.Element) => {
);
};
const hasLanguageHeader = (component: JSX.Element) => {
const { children } = navigationLinks(component, 'lang-header');
return children === 'footer.language';
};
const hasLanguageDropdown = (component: JSX.Element) => {
const { children } = navigationLinks(component, 'language-dropdown');
return children.type === 'select';
};
const hasDefaultLanguageInLanguageDropdown = (
component: JSX.Element,
defaultLanguage: string
) => {
const { children } = navigationLinks(component, 'language-dropdown');
return children.props.value === defaultLanguage;
};
const hasAllAvailableLanguagesInDropdown = (component: JSX.Element) => {
const { children }: { children: JSX.Element } = navigationLinks(
component,
'language-dropdown'
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return children.props.children.every(
({ props }: { props: { value: string; children: string } }) =>
availableLangs.client.includes(props.value) &&
(langDisplayNames as Record<string, string>)[props.value] ===
props.children
);
};
const hasSignOutNavItem = (component: JSX.Element) => {
const { children } = navigationLinks(component, 'signout-frag');
const signOutProps = children[1].props;

View File

@ -1,3 +1,4 @@
/* eslint-disable jsx-a11y/no-onchange */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -9,7 +10,6 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
// @ts-nocheck
import {
faCheck,
faCheckSquare,
faHeart,
faSquare,
@ -22,9 +22,9 @@ import { connect } from 'react-redux';
import envData from '../../../../../config/env.json';
import {
availableLangs,
i18nextCodes,
langDisplayNames
} from '../../../../../config/i18n/all-langs';
import { hardGoTo as navigate } from '../../../redux';
import { updateUserFlag } from '../../../redux/settings';
import createLanguageRedirect from '../../create-language-redirect';
import { Link } from '../../helpers';
@ -41,30 +41,51 @@ export interface NavLinksProps {
toggleDisplayMenu?: React.MouseEventHandler<HTMLButtonElement>;
toggleNightMode: (x: any) => any;
user?: Record<string, unknown>;
navigate?: (location: string) => void;
}
const mapDispatchToProps = {
navigate,
toggleNightMode: (theme: unknown) => updateUserFlag({ theme })
};
export class NavLinks extends Component<NavLinksProps, {}> {
static displayName: string;
constructor(props: NavLinksProps) {
super(props);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
}
toggleTheme(currentTheme = 'default', toggleNightMode: any) {
toggleNightMode(currentTheme === 'night' ? 'default' : 'night');
}
handleLanguageChange = (
event: React.ChangeEvent<HTMLSelectElement>
): void => {
const { toggleDisplayMenu, navigate } = this.props;
toggleDisplayMenu();
const path = createLanguageRedirect({
clientLocale,
lang: event.target.value
});
return navigate(path);
};
render() {
const {
displayMenu,
i18n,
fetchState,
t,
toggleDisplayMenu,
toggleNightMode,
user: { isDonating = false, username, theme }
}: NavLinksProps = this.props;
const { pending } = fetchState;
return pending ? (
<div className='nav-skeleton' />
) : (
@ -167,32 +188,20 @@ export class NavLinks extends Component<NavLinksProps, {}> {
<div className='nav-link nav-link-header' key='lang-header'>
{t('footer.language')}
</div>
{locales.map(lang =>
// current lang is a button that closes the menu
i18n.language === i18nextCodes[lang] ? (
<button
className='nav-link nav-link-lang nav-link-flex'
key={'lang-' + lang}
onClick={toggleDisplayMenu}
>
<span>{langDisplayNames[lang]}</span>
<FontAwesomeIcon icon={faCheck} />
</button>
) : (
<Link
className='nav-link nav-link-lang nav-link-flex'
external={true}
// Todo: should treat other lang client application links as external??
key={'lang-' + lang}
to={createLanguageRedirect({
clientLocale,
lang
})}
>
{langDisplayNames[lang]}
</Link>
)
)}
<div className='nav-link' key='language-dropdown'>
<select
className='nav-link-lang-dropdown'
onChange={this.handleLanguageChange}
value={clientLocale}
>
{locales.map(lang => (
<option key={'lang-' + lang} value={lang}>
{langDisplayNames[lang]}
</option>
))}
</select>
</div>
{username && (
<Fragment key='signout-frag'>
<hr className='nav-line-2' />

View File

@ -102,8 +102,8 @@
color: var(--gray-00);
background-color: var(--gray-90);
opacity: 1;
white-space: nowrap;
height: var(--header-height);
white-space: normal;
min-height: var(--header-height);
width: 100%;
align-items: center;
border: none;
@ -135,8 +135,21 @@
height: auto !important;
}
.nav-link-lang {
padding-left: 30px;
.nav-link:hover .nav-link-lang-dropdown,
.nav-link:active .nav-link-lang-dropdown {
background-color: var(--gray-00);
color: var(--gray-90);
cursor: pointer;
}
.nav-link-lang-dropdown {
color: var(--gray-00);
background-color: var(--gray-90);
width: 100%;
border: none;
}
.nav-link-lang-dropdown:focus {
outline: none;
}
.nav-link-flex {

View File

@ -47,7 +47,8 @@ export class Header extends React.Component<
// since the search bar is part of the menu on small screens, clicks on
// the search bar should not toggle the menu
this.searchBarRef.current &&
!this.searchBarRef.current.contains(event.target)
!this.searchBarRef.current.contains(event.target) &&
!(event.target instanceof HTMLSelectElement)
) {
this.toggleDisplayMenu();
}