From 3dbe40410cd79fe6bb17f7d85b96b40318afcee9 Mon Sep 17 00:00:00 2001 From: Lim Shang Yi Date: Fri, 15 Oct 2021 21:08:35 +0800 Subject: [PATCH] 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 --- client/src/components/Header/Header.test.tsx | 100 +++++++++++++++++- .../Header/components/nav-links.tsx | 69 ++++++------ .../Header/components/universal-nav.css | 21 +++- client/src/components/Header/index.tsx | 3 +- 4 files changed, 155 insertions(+), 38 deletions(-) diff --git a/client/src/components/Header/Header.test.tsx b/client/src/components/Header/Header.test.tsx index 2bfbed6af4d..7954b7b501e 100644 --- a/client/src/components/Header/Header.test.tsx +++ b/client/src/components/Header/Header.test.tsx @@ -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('', () => { hasCurriculumNavItem(view) && hasForumNavItem(view) && hasNewsNavItem(view) && - hasRadioNavItem(view) + hasRadioNavItem(view) && + hasLanguageHeader(view) && + hasLanguageDropdown(view) ).toBeTruthy(); }); @@ -123,7 +129,62 @@ describe('', () => { 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(); + 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(); + 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)[props.value] === + props.children + ); +}; + const hasSignOutNavItem = (component: JSX.Element) => { const { children } = navigationLinks(component, 'signout-frag'); const signOutProps = children[1].props; diff --git a/client/src/components/Header/components/nav-links.tsx b/client/src/components/Header/components/nav-links.tsx index c7281d41b3f..453df3ecf5b 100644 --- a/client/src/components/Header/components/nav-links.tsx +++ b/client/src/components/Header/components/nav-links.tsx @@ -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; toggleNightMode: (x: any) => any; user?: Record; + navigate?: (location: string) => void; } const mapDispatchToProps = { + navigate, toggleNightMode: (theme: unknown) => updateUserFlag({ theme }) }; export class NavLinks extends Component { 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 + ): 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 ? (
) : ( @@ -167,32 +188,20 @@ export class NavLinks extends Component {
{t('footer.language')}
- {locales.map(lang => - // current lang is a button that closes the menu - i18n.language === i18nextCodes[lang] ? ( - - ) : ( - - {langDisplayNames[lang]} - - ) - )} + +
+ +
{username && (
diff --git a/client/src/components/Header/components/universal-nav.css b/client/src/components/Header/components/universal-nav.css index ea21580874c..1d63450e52b 100644 --- a/client/src/components/Header/components/universal-nav.css +++ b/client/src/components/Header/components/universal-nav.css @@ -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 { diff --git a/client/src/components/Header/index.tsx b/client/src/components/Header/index.tsx index 40d890f09d4..0c904b6b33c 100644 --- a/client/src/components/Header/index.tsx +++ b/client/src/components/Header/index.tsx @@ -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(); }