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
This commit is contained in:
Lim Shang Yi
2021-10-15 21:08:35 +08:00
committed by GitHub
parent e61bc3ba5d
commit 3dbe40410c
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 ShallowRenderer from 'react-test-renderer/shallow';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import {
availableLangs,
langDisplayNames
} from '../../../../config/i18n/all-langs';
import AuthOrProfile from './components/auth-or-profile'; import AuthOrProfile from './components/auth-or-profile';
import { NavLinks } from './components/nav-links'; import { NavLinks } from './components/nav-links';
import { UniversalNav } from './components/universal-nav'; import { UniversalNav } from './components/universal-nav';
const { apiLocation } = envData; const { apiLocation, clientLocale } = envData;
jest.mock('../../analytics'); jest.mock('../../analytics');
@@ -63,7 +67,9 @@ describe('<NavLinks />', () => {
hasCurriculumNavItem(view) && hasCurriculumNavItem(view) &&
hasForumNavItem(view) && hasForumNavItem(view) &&
hasNewsNavItem(view) && hasNewsNavItem(view) &&
hasRadioNavItem(view) hasRadioNavItem(view) &&
hasLanguageHeader(view) &&
hasLanguageDropdown(view)
).toBeTruthy(); ).toBeTruthy();
}); });
@@ -123,7 +129,62 @@ describe('<NavLinks />', () => {
hasForumNavItem(view) && hasForumNavItem(view) &&
hasNewsNavItem(view) && hasNewsNavItem(view) &&
hasRadioNavItem(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(); ).toBeTruthy();
}); });
}); });
@@ -205,6 +266,7 @@ const navigationLinks = (component: JSX.Element, key: string) => {
); );
return target.props; return target.props;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const profileNavItem = (component: any) => component.children[0]; 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 hasSignOutNavItem = (component: JSX.Element) => {
const { children } = navigationLinks(component, 'signout-frag'); const { children } = navigationLinks(component, 'signout-frag');
const signOutProps = children[1].props; 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/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@@ -9,7 +10,6 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
// @ts-nocheck // @ts-nocheck
import { import {
faCheck,
faCheckSquare, faCheckSquare,
faHeart, faHeart,
faSquare, faSquare,
@@ -22,9 +22,9 @@ import { connect } from 'react-redux';
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
import { import {
availableLangs, availableLangs,
i18nextCodes,
langDisplayNames langDisplayNames
} from '../../../../../config/i18n/all-langs'; } from '../../../../../config/i18n/all-langs';
import { hardGoTo as navigate } from '../../../redux';
import { updateUserFlag } from '../../../redux/settings'; import { updateUserFlag } from '../../../redux/settings';
import createLanguageRedirect from '../../create-language-redirect'; import createLanguageRedirect from '../../create-language-redirect';
import { Link } from '../../helpers'; import { Link } from '../../helpers';
@@ -41,30 +41,51 @@ export interface NavLinksProps {
toggleDisplayMenu?: React.MouseEventHandler<HTMLButtonElement>; toggleDisplayMenu?: React.MouseEventHandler<HTMLButtonElement>;
toggleNightMode: (x: any) => any; toggleNightMode: (x: any) => any;
user?: Record<string, unknown>; user?: Record<string, unknown>;
navigate?: (location: string) => void;
} }
const mapDispatchToProps = { const mapDispatchToProps = {
navigate,
toggleNightMode: (theme: unknown) => updateUserFlag({ theme }) toggleNightMode: (theme: unknown) => updateUserFlag({ theme })
}; };
export class NavLinks extends Component<NavLinksProps, {}> { export class NavLinks extends Component<NavLinksProps, {}> {
static displayName: string; static displayName: string;
constructor(props: NavLinksProps) {
super(props);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
}
toggleTheme(currentTheme = 'default', toggleNightMode: any) { toggleTheme(currentTheme = 'default', toggleNightMode: any) {
toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); 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() { render() {
const { const {
displayMenu, displayMenu,
i18n,
fetchState, fetchState,
t, t,
toggleDisplayMenu,
toggleNightMode, toggleNightMode,
user: { isDonating = false, username, theme } user: { isDonating = false, username, theme }
}: NavLinksProps = this.props; }: NavLinksProps = this.props;
const { pending } = fetchState; const { pending } = fetchState;
return pending ? ( return pending ? (
<div className='nav-skeleton' /> <div className='nav-skeleton' />
) : ( ) : (
@@ -167,32 +188,20 @@ export class NavLinks extends Component<NavLinksProps, {}> {
<div className='nav-link nav-link-header' key='lang-header'> <div className='nav-link nav-link-header' key='lang-header'>
{t('footer.language')} {t('footer.language')}
</div> </div>
{locales.map(lang =>
// current lang is a button that closes the menu <div className='nav-link' key='language-dropdown'>
i18n.language === i18nextCodes[lang] ? ( <select
<button className='nav-link-lang-dropdown'
className='nav-link nav-link-lang nav-link-flex' onChange={this.handleLanguageChange}
key={'lang-' + lang} value={clientLocale}
onClick={toggleDisplayMenu} >
> {locales.map(lang => (
<span>{langDisplayNames[lang]}</span> <option key={'lang-' + lang} value={lang}>
<FontAwesomeIcon icon={faCheck} /> {langDisplayNames[lang]}
</button> </option>
) : ( ))}
<Link </select>
className='nav-link nav-link-lang nav-link-flex' </div>
external={true}
// Todo: should treat other lang client application links as external??
key={'lang-' + lang}
to={createLanguageRedirect({
clientLocale,
lang
})}
>
{langDisplayNames[lang]}
</Link>
)
)}
{username && ( {username && (
<Fragment key='signout-frag'> <Fragment key='signout-frag'>
<hr className='nav-line-2' /> <hr className='nav-line-2' />

View File

@@ -102,8 +102,8 @@
color: var(--gray-00); color: var(--gray-00);
background-color: var(--gray-90); background-color: var(--gray-90);
opacity: 1; opacity: 1;
white-space: nowrap; white-space: normal;
height: var(--header-height); min-height: var(--header-height);
width: 100%; width: 100%;
align-items: center; align-items: center;
border: none; border: none;
@@ -135,8 +135,21 @@
height: auto !important; height: auto !important;
} }
.nav-link-lang { .nav-link:hover .nav-link-lang-dropdown,
padding-left: 30px; .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 { .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 // since the search bar is part of the menu on small screens, clicks on
// the search bar should not toggle the menu // the search bar should not toggle the menu
this.searchBarRef.current && this.searchBarRef.current &&
!this.searchBarRef.current.contains(event.target) !this.searchBarRef.current.contains(event.target) &&
!(event.target instanceof HTMLSelectElement)
) { ) {
this.toggleDisplayMenu(); this.toggleDisplayMenu();
} }