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:
@@ -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;
|
||||||
|
@@ -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' />
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user