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 clientLocalepull/43843/head
parent
e61bc3ba5d
commit
3dbe40410c
|
@ -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;
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue