fix(a11y): main menu a11y updates (#45137)
* fix: main menu a11y updates * update font menu * fix: sign in button text alignment * fix: disabled button font * fix: language menu font * remove sign-in from main menu * fix: add section dividers to menu * fix: height on language selector menu * fix: alignment of Menu button text * fix: language globe icon * refactor: remove dup selector * update language menu colors * refactor: clearer name for language menu display state * fix: don't close onBlur if Menu button is clicked * refactor: move globe icon styling to CSS * refactor: get rid of switch statements * refactor: remove try catch block * fix: translate Change language button * fix: move search into nav menu for mobile layout * fix: forgot a merge * refactor: updates for changes in i18n/all-langs * fix: prevent menu from collapsing when focus is on change language button and user clicks into search * fix: translate cancel change option in language picker * feat: add cypress tests * feat: display the complete language list * fix: fix TS typing * fix: force scrollbar on lang menu * fix: remove scroll bar from lang menu * fix: close menu when user tabs away from last menu item * add list role to navigation list to appease Safari * chore: capitalize Change Language/remove CSS comment * fix: right side search box alignment in narrow view * remove extraneous list role * fix: cypress Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com> Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>pull/46267/head
parent
6d89576b6c
commit
d2332093f6
|
@ -70,7 +70,9 @@
|
|||
"start-coding": "Start coding!",
|
||||
"go-to-settings": "Go to settings to claim your certification",
|
||||
"click-start-course": "Start the course",
|
||||
"click-start-project": "Start the project"
|
||||
"click-start-project": "Start the project",
|
||||
"change-language": "Change Language",
|
||||
"cancel-change": "Cancel Change"
|
||||
},
|
||||
"landing": {
|
||||
"big-heading-1": "Learn to code — for free.",
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
|
||||
function LanguageGlobe(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
height={22}
|
||||
viewBox='0 0 24 24'
|
||||
width={24}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M13 2.04932C13 2.04932 16 5.99994 16 11.9999C16 17.9999 13 21.9506 13 21.9506'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M11 21.9506C11 21.9506 8 17.9999 8 11.9999C8 5.99994 11 2.04932 11 2.04932'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M2.62964 15.5H21.3704'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M2.62964 8.5H21.3704'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LanguageGlobe.displayName = 'LanguageGlobe';
|
||||
|
||||
export default LanguageGlobe;
|
|
@ -1,39 +1,54 @@
|
|||
import React, { RefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AuthOrProfile from './auth-or-profile';
|
||||
|
||||
export interface MenuButtonProps {
|
||||
className?: string;
|
||||
displayMenu?: boolean;
|
||||
innerRef?: RefObject<HTMLButtonElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined;
|
||||
user?: Record<string, unknown>;
|
||||
showMenu: () => void;
|
||||
hideMenu: () => void;
|
||||
}
|
||||
|
||||
const MenuButton = ({
|
||||
displayMenu,
|
||||
innerRef,
|
||||
onClick,
|
||||
user
|
||||
showMenu,
|
||||
hideMenu
|
||||
}: MenuButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Will close the menu if the user Shift+Tabs from the menu button.
|
||||
const handleBlur = (event: React.FocusEvent<HTMLButtonElement>): void => {
|
||||
if (
|
||||
event.relatedTarget &&
|
||||
!event.relatedTarget.closest('.universal-nav-right') &&
|
||||
displayMenu
|
||||
) {
|
||||
hideMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (): void => {
|
||||
if (displayMenu) {
|
||||
hideMenu();
|
||||
return;
|
||||
}
|
||||
showMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
aria-expanded={displayMenu}
|
||||
className={
|
||||
'toggle-button-nav' + (displayMenu ? ' reverse-toggle-color' : '')
|
||||
}
|
||||
onClick={onClick}
|
||||
ref={innerRef}
|
||||
>
|
||||
{t('buttons.menu')}
|
||||
</button>
|
||||
<span className='navatar'>
|
||||
<AuthOrProfile user={user} />
|
||||
</span>
|
||||
</>
|
||||
<button
|
||||
aria-expanded={displayMenu}
|
||||
className={`toggle-button-nav${
|
||||
displayMenu ? ' reverse-toggle-color' : ''
|
||||
}`}
|
||||
id='toggle-button-nav'
|
||||
onBlur={handleBlur}
|
||||
onClick={handleClick}
|
||||
ref={innerRef}
|
||||
>
|
||||
{t('buttons.menu')}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -16,19 +16,21 @@ import {
|
|||
faExternalLinkAlt
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component, Fragment, createRef, Ref } from 'react';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import envData from '../../../../../config/env.json';
|
||||
import {
|
||||
availableLangs,
|
||||
getLangName
|
||||
LangNames,
|
||||
LangCodes
|
||||
} 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';
|
||||
import { Themes } from '../../settings/theme';
|
||||
import LanguageGlobe from '../../../assets/icons/language-globe';
|
||||
|
||||
const { clientLocale, radioLocation, apiLocation } = envData;
|
||||
|
||||
|
@ -36,13 +38,17 @@ const locales = availableLangs.client;
|
|||
|
||||
export interface NavLinksProps {
|
||||
displayMenu?: boolean;
|
||||
isLanguageMenuDisplayed?: boolean;
|
||||
fetchState?: { pending: boolean };
|
||||
i18n: Object;
|
||||
t: TFunction;
|
||||
toggleDisplayMenu?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
hideMenu: () => void;
|
||||
toggleNightMode: (x: any) => any;
|
||||
user?: Record<string, unknown>;
|
||||
navigate?: (location: string) => void;
|
||||
showLanguageMenu?: (elementToFocus: HTMLButtonElement) => void;
|
||||
hideLanguageMenu?: () => void;
|
||||
menuButtonRef: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
@ -52,10 +58,22 @@ const mapDispatchToProps = {
|
|||
|
||||
export class NavLinks extends Component<NavLinksProps, {}> {
|
||||
static displayName: string;
|
||||
langButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
firstLangOptionRef: React.RefObject<HTMLElement>;
|
||||
lastLangOptionRef: React.RefObject<HTMLElement>;
|
||||
|
||||
constructor(props: NavLinksProps) {
|
||||
super(props);
|
||||
this.langButtonRef = createRef();
|
||||
this.firstLangOptionRef = createRef();
|
||||
this.lastLangOptionRef = createRef();
|
||||
this.handleLanguageChange = this.handleLanguageChange.bind(this);
|
||||
this.handleLanguageMenuKeyDown = this.handleLanguageMenuKeyDown.bind(this);
|
||||
this.handleLanguageButtonClick = this.handleLanguageButtonClick.bind(this);
|
||||
this.handleLanguageButtonKeyDown =
|
||||
this.handleLanguageButtonKeyDown.bind(this);
|
||||
this.handleMenuKeyDown = this.handleMenuKeyDown.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
}
|
||||
|
||||
toggleTheme(currentTheme = Themes.Default, toggleNightMode: any) {
|
||||
|
@ -64,23 +82,172 @@ export class NavLinks extends Component<NavLinksProps, {}> {
|
|||
);
|
||||
}
|
||||
|
||||
handleLanguageChange = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
): void => {
|
||||
const { toggleDisplayMenu, navigate } = this.props;
|
||||
toggleDisplayMenu();
|
||||
getPreviousMenuItem(target: HTMLElement): HTMLElement {
|
||||
const { menuButtonRef } = this.props;
|
||||
const previousSibling =
|
||||
target.closest('.nav-list > li')?.previousElementSibling;
|
||||
return previousSibling?.querySelector('a, button') ?? menuButtonRef.current;
|
||||
}
|
||||
|
||||
handleLanguageChange = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.preventDefault();
|
||||
const { hideMenu, hideLanguageMenu, menuButtonRef, navigate } = this.props;
|
||||
const newLanguage = event.target.dataset.value as string;
|
||||
// If user selected cancel then close menu and put focus on button
|
||||
if (newLanguage === 'exit-lang-menu') {
|
||||
// Set focus to language button first so we don't lose focus
|
||||
// for screen readers.
|
||||
this.langButtonRef.current.focus();
|
||||
hideLanguageMenu();
|
||||
return;
|
||||
}
|
||||
// Put focus on menu button first so we don't lose focus
|
||||
// for screen readers.
|
||||
menuButtonRef.current.focus();
|
||||
hideMenu();
|
||||
// If user selected the current language then we just close the menu
|
||||
if (newLanguage === clientLocale) {
|
||||
return;
|
||||
}
|
||||
const path = createLanguageRedirect({
|
||||
clientLocale,
|
||||
lang: event.target.value
|
||||
lang: newLanguage
|
||||
});
|
||||
|
||||
return navigate(path);
|
||||
};
|
||||
|
||||
handleMenuKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
||||
const { menuButtonRef, hideMenu } = this.props;
|
||||
if (event.key === 'Escape') {
|
||||
menuButtonRef.current.focus();
|
||||
hideMenu();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
handleLanguageButtonClick = (): void => {
|
||||
const { isLanguageMenuDisplayed, hideLanguageMenu, showLanguageMenu } =
|
||||
this.props;
|
||||
if (isLanguageMenuDisplayed) {
|
||||
hideLanguageMenu();
|
||||
} else {
|
||||
showLanguageMenu(this.firstLangOptionRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
handleLanguageButtonKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>
|
||||
): void => {
|
||||
const { menuButtonRef, showLanguageMenu, hideMenu } = this.props;
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const doKeyPress = {
|
||||
Escape: () => {
|
||||
menuButtonRef.current.focus();
|
||||
hideMenu();
|
||||
event.preventDefault();
|
||||
},
|
||||
ArrowDown: () => {
|
||||
showLanguageMenu(this.firstLangOptionRef.current);
|
||||
event.preventDefault();
|
||||
},
|
||||
ArrowUp: () => {
|
||||
showLanguageMenu(this.lastLangOptionRef.current);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
doKeyPress[event.key]?.();
|
||||
};
|
||||
|
||||
handleLanguageMenuKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>
|
||||
): void => {
|
||||
const { hideLanguageMenu, hideMenu } = this.props;
|
||||
const focusFirstLanguageMenuItem = () => {
|
||||
this.firstLangOptionRef.current.focus();
|
||||
event.preventDefault();
|
||||
};
|
||||
const focusLastLanguageMenuItem = () => {
|
||||
this.lastLangOptionRef.current.focus();
|
||||
event.preventDefault();
|
||||
};
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const doKeyPress = {
|
||||
Tab: () => {
|
||||
if (!event.shiftKey) {
|
||||
// Let the Tab work as normal.
|
||||
hideLanguageMenu();
|
||||
// Close the menu if focus is now outside of the menu. This will
|
||||
// happen when there is no Sign Out menu item.
|
||||
setTimeout(() => {
|
||||
const currentlyFocusedElement = document.activeElement;
|
||||
if (
|
||||
currentlyFocusedElement &&
|
||||
!currentlyFocusedElement.closest('.nav-list')
|
||||
) {
|
||||
hideMenu();
|
||||
}
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
// Because FF adds an extra tab stop to the lang menu (because it
|
||||
// is scrollable) we need to manually focus the previous menu item.
|
||||
this.getPreviousMenuItem(this.langButtonRef.current).focus();
|
||||
hideLanguageMenu();
|
||||
event.preventDefault();
|
||||
},
|
||||
ArrowUp: () => {
|
||||
const arrowUpItemToFocus =
|
||||
event.target === this.firstLangOptionRef.current
|
||||
? this.lastLangOptionRef.current
|
||||
: (event.target.parentNode.previousSibling
|
||||
.firstChild as HTMLElement);
|
||||
arrowUpItemToFocus.focus();
|
||||
event.preventDefault();
|
||||
},
|
||||
ArrowDown: () => {
|
||||
const arrowDownItemToFocus =
|
||||
event.target === this.lastLangOptionRef.current
|
||||
? this.firstLangOptionRef.current
|
||||
: (event.target.parentNode.nextSibling.firstChild as HTMLElement);
|
||||
arrowDownItemToFocus.focus();
|
||||
event.preventDefault();
|
||||
},
|
||||
Escape: () => {
|
||||
// Set focus to language button first so we don't lose focus
|
||||
// for screen readers.
|
||||
this.langButtonRef.current.focus();
|
||||
hideLanguageMenu();
|
||||
event.preventDefault();
|
||||
},
|
||||
Home: focusFirstLanguageMenuItem,
|
||||
PageUp: focusFirstLanguageMenuItem,
|
||||
End: focusLastLanguageMenuItem,
|
||||
PageDown: focusLastLanguageMenuItem
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
doKeyPress[event.key]?.();
|
||||
};
|
||||
|
||||
// Added to the last item in the nav menu. Will close the menu if
|
||||
// the user Tabs out of the menu.
|
||||
handleBlur = (event: React.FocusEvent<HTMLButtonElement>): void => {
|
||||
const { hideMenu, menuButtonRef } = this.props;
|
||||
console.log('handleBlur: relatedTarget = ', event.relatedTarget);
|
||||
if (
|
||||
event.relatedTarget &&
|
||||
!event.relatedTarget.closest('.nav-list') &&
|
||||
event.relatedTarget !== menuButtonRef.current
|
||||
) {
|
||||
hideMenu();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayMenu,
|
||||
isLanguageMenuDisplayed,
|
||||
fetchState,
|
||||
t,
|
||||
toggleNightMode,
|
||||
|
@ -92,132 +259,214 @@ export class NavLinks extends Component<NavLinksProps, {}> {
|
|||
return pending ? (
|
||||
<div className='nav-skeleton' />
|
||||
) : (
|
||||
<div className={'nav-list' + (displayMenu ? ' display-menu' : '')}>
|
||||
<ul
|
||||
aria-labelledby='toggle-button-nav'
|
||||
className={`nav-list${displayMenu ? ' display-menu' : ''}${
|
||||
isLanguageMenuDisplayed ? ' display-lang-menu' : ''
|
||||
}`}
|
||||
>
|
||||
{isDonating ? (
|
||||
<div className='nav-link nav-link-flex nav-link-header' key='donate'>
|
||||
<span>{t('donate.thanks')}</span>
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</div>
|
||||
<li key='donate'>
|
||||
<div className='nav-link nav-link-flex nav-link-header'>
|
||||
<span>{t('donate.thanks')}</span>
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</div>
|
||||
</li>
|
||||
) : (
|
||||
<Link className='nav-link' key='donate' sameTab={false} to='/donate'>
|
||||
{t('buttons.donate')}
|
||||
</Link>
|
||||
<li key='donate'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to='/donate'
|
||||
>
|
||||
{t('buttons.donate')}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{!username && (
|
||||
<a
|
||||
className='nav-link nav-link-sign-in'
|
||||
href={`${apiLocation}/signin`}
|
||||
key='signin'
|
||||
<li key='learn'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
to='/learn'
|
||||
>
|
||||
{t('buttons.sign-in')}
|
||||
</a>
|
||||
)}
|
||||
<Link className='nav-link' key='learn' to='/learn'>
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
{t('buttons.curriculum')}
|
||||
</Link>
|
||||
</li>
|
||||
{username && (
|
||||
<Fragment key='profile-settings'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
key='profile'
|
||||
sameTab={false}
|
||||
to={`/${username}`}
|
||||
>
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
<Link
|
||||
className='nav-link'
|
||||
key='settings'
|
||||
sameTab={false}
|
||||
to={`/settings`}
|
||||
>
|
||||
{t('buttons.settings')}
|
||||
</Link>
|
||||
<li key='profile'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to={`/${username}`}
|
||||
>
|
||||
{t('buttons.profile')}
|
||||
</Link>
|
||||
</li>
|
||||
<li key='settings'>
|
||||
<Link
|
||||
className='nav-link'
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to={`/settings`}
|
||||
>
|
||||
{t('buttons.settings')}
|
||||
</Link>
|
||||
</li>
|
||||
</Fragment>
|
||||
)}
|
||||
<hr className='nav-line' />
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
key='forum'
|
||||
sameTab={false}
|
||||
to={t('links:nav.forum')}
|
||||
>
|
||||
<span>{t('buttons.forum')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
key='news'
|
||||
sameTab={false}
|
||||
to={t('links:nav.news')}
|
||||
>
|
||||
<span>{t('buttons.news')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
key='radio'
|
||||
sameTab={false}
|
||||
to={radioLocation}
|
||||
>
|
||||
<span>{t('buttons.radio')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
<hr className='nav-line' />
|
||||
<button
|
||||
className={
|
||||
'nav-link nav-link-flex' + (!username ? ' nav-link-header' : '')
|
||||
}
|
||||
disabled={!username}
|
||||
key='theme'
|
||||
onClick={() => this.toggleTheme(String(theme), toggleNightMode)}
|
||||
>
|
||||
{username ? (
|
||||
<>
|
||||
<span>{t('settings.labels.night-mode')}</span>
|
||||
{theme === Themes.Night ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSquare} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className='nav-link-dull'>{t('misc.change-theme')}</span>
|
||||
)}
|
||||
</button>
|
||||
<div className='nav-link nav-link-header' key='lang-header'>
|
||||
{t('footer.language')}
|
||||
</div>
|
||||
|
||||
<div className='nav-link dropdown-nav-link' key='language-dropdown'>
|
||||
<select
|
||||
className='nav-link-lang-dropdown'
|
||||
onChange={this.handleLanguageChange}
|
||||
value={clientLocale}
|
||||
<li key='forum' className='nav-line'>
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to={t('links:nav.forum')}
|
||||
>
|
||||
{locales.map(lang => (
|
||||
<option key={'lang-' + lang} value={lang}>
|
||||
{getLangName(lang)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<span>{t('buttons.forum')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
</li>
|
||||
<li key='news'>
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to={t('links:nav.news')}
|
||||
>
|
||||
<span>{t('buttons.news')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
</li>
|
||||
<li key='radio'>
|
||||
<Link
|
||||
className='nav-link nav-link-flex'
|
||||
external={true}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
sameTab={false}
|
||||
to={radioLocation}
|
||||
>
|
||||
<span>{t('buttons.radio')}</span>
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</Link>
|
||||
</li>
|
||||
<li className='nav-line' key='theme'>
|
||||
<button
|
||||
{...(!username && { 'aria-describedby': 'theme-sign-in' })}
|
||||
aria-disabled={!username}
|
||||
aria-pressed={theme === Themes.Night ? 'true' : 'false'}
|
||||
className={
|
||||
'nav-link nav-link-flex' + (!username ? ' nav-link-header' : '')
|
||||
}
|
||||
onClick={() => {
|
||||
if (username) {
|
||||
this.toggleTheme(String(theme), toggleNightMode);
|
||||
}
|
||||
}}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
>
|
||||
{username ? (
|
||||
<>
|
||||
<span>{t('settings.labels.night-mode')}</span>
|
||||
{theme === Themes.Night ? (
|
||||
<FontAwesomeIcon icon={faCheckSquare} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSquare} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Fragment key='night-mode'>
|
||||
<span className='sr-only'>
|
||||
{t('settings.labels.night-mode')}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='nav-link-dull'
|
||||
id='theme-sign-in'
|
||||
>
|
||||
{t('misc.change-theme')}
|
||||
</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
<li key='lang-menu'>
|
||||
<div className='nav-lang' key='language-dropdown'>
|
||||
<button
|
||||
aria-controls='nav-lang-menu'
|
||||
{...(isLanguageMenuDisplayed && { 'aria-expanded': true })}
|
||||
aria-haspopup='true'
|
||||
className='nav-link nav-lang-button'
|
||||
id='nav-lang-button'
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleLanguageButtonClick}
|
||||
onKeyDown={this.handleLanguageButtonKeyDown}
|
||||
ref={this.langButtonRef}
|
||||
>
|
||||
<span>{t('buttons.change-language')}</span>
|
||||
<LanguageGlobe />
|
||||
</button>
|
||||
<ul
|
||||
aria-labelledby='nav-lang-button'
|
||||
className={'nav-lang-menu' + (username ? ' logged-in' : '')}
|
||||
id='nav-lang-menu'
|
||||
role='menu'
|
||||
>
|
||||
<li key='lang-menu-exit' role='none'>
|
||||
<button
|
||||
className='nav-link nav-lang-menu-option'
|
||||
data-value='exit-lang-menu'
|
||||
onClick={this.handleLanguageChange}
|
||||
onKeyDown={this.handleLanguageMenuKeyDown}
|
||||
ref={this.firstLangOptionRef}
|
||||
role='menuitem'
|
||||
tabIndex='-1'
|
||||
>
|
||||
{t('buttons.cancel-change')}
|
||||
</button>
|
||||
</li>
|
||||
{locales.map((lang, index) => (
|
||||
<li key={'lang-' + lang} role='none'>
|
||||
<button
|
||||
{...(clientLocale === lang && { 'aria-current': true })}
|
||||
className='nav-link nav-lang-menu-option'
|
||||
data-value={lang}
|
||||
{...(LangCodes[lang] && {
|
||||
lang: LangCodes[lang] as string
|
||||
})}
|
||||
onClick={this.handleLanguageChange}
|
||||
onKeyDown={this.handleLanguageMenuKeyDown}
|
||||
{...(index === locales.length - 1 && {
|
||||
ref: this.lastLangOptionRef
|
||||
})}
|
||||
role='menuitem'
|
||||
tabIndex='-1'
|
||||
>
|
||||
{LangNames[lang]}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{username && (
|
||||
<Fragment key='signout-frag'>
|
||||
<hr className='nav-line-2' />
|
||||
<a
|
||||
className='nav-link'
|
||||
href={`${apiLocation}/signout`}
|
||||
key='sign-out'
|
||||
>
|
||||
{t('buttons.sign-out')}
|
||||
</a>
|
||||
<li className='nav-line' key='sign-out'>
|
||||
<a
|
||||
className='nav-link nav-link-signout'
|
||||
href={`${apiLocation}/signout`}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleMenuKeyDown}
|
||||
>
|
||||
{t('buttons.sign-out')}
|
||||
</a>
|
||||
</li>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
font-size: 18px;
|
||||
font-family: 'Roboto-Mono', sans-serif;
|
||||
font-family: 'Lato', sans-serif;
|
||||
height: var(--header-height);
|
||||
background: var(--theme-color);
|
||||
position: fixed;
|
||||
|
@ -18,10 +18,6 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.universal-nav .universal-nav-right a {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.universal-nav-left {
|
||||
display: flex;
|
||||
flex: 1 0 33%;
|
||||
|
@ -89,7 +85,12 @@
|
|||
margin: 0 0 0 -12px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-width: 250px;
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
width: 100%;
|
||||
height: 2.2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
|
@ -104,28 +105,35 @@
|
|||
min-height: var(--header-height);
|
||||
width: 100%;
|
||||
border: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropdown-nav-link {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
.nav-lang {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropdown-nav-link:hover .nav-link-lang-dropdown,
|
||||
.dropdown-nav-link:active .nav-link-lang-dropdown {
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23000000%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
|
||||
.nav-link:focus {
|
||||
outline: 3px solid var(--blue-mid);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link:active {
|
||||
color: var(--theme-color);
|
||||
.nav-link:focus:not(:focus-visible) {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.nav-link:not([aria-disabled='true']):hover {
|
||||
color: var(--theme-color) !important;
|
||||
text-decoration: none;
|
||||
background: white;
|
||||
background-color: var(--gray-10) !important;
|
||||
cursor: pointer;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.nav-link-header,
|
||||
.nav-link-header:hover,
|
||||
.nav-link-header:not([aria-disabled='true']):hover,
|
||||
.nav-link-header:active {
|
||||
color: var(--gray-00);
|
||||
background-color: var(--gray-90);
|
||||
|
@ -142,31 +150,146 @@
|
|||
height: auto !important;
|
||||
}
|
||||
|
||||
.nav-link:hover .nav-link-lang-dropdown,
|
||||
.nav-link:active .nav-link-lang-dropdown {
|
||||
.nav-link:hover .nav-lang-menu,
|
||||
.nav-link:active .nav-lang-menu {
|
||||
background-color: var(--gray-00);
|
||||
color: var(--gray-90);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-link-lang-dropdown {
|
||||
padding: 0 15px;
|
||||
color: var(--gray-00);
|
||||
background-color: var(--gray-90);
|
||||
button.nav-link:focus {
|
||||
color: var(--tertiary-color);
|
||||
background-color: var(--tertiary-background);
|
||||
outline: 3px solid var(--blue-mid);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
button.nav-link:focus:not(:focus-visible) {
|
||||
background-color: inherit;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button.nav-link:not([aria-disabled='true']):hover {
|
||||
background-color: var(--gray-10);
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
button.nav-link[aria-disabled='true'] {
|
||||
cursor: default;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.display-menu .theme-disabled {
|
||||
height: 4.2rem;
|
||||
}
|
||||
|
||||
.nav-link-header label {
|
||||
font-weight: normal;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-lang-menu {
|
||||
display: none;
|
||||
padding: 0;
|
||||
color: var(--gray-90);
|
||||
background-color: var(--gray-80);
|
||||
width: 100%;
|
||||
border: none;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23ffffff%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23ffffff%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
|
||||
background-size: 10px;
|
||||
background-position: calc(100% - 15px) center;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
border: 2px solid var(--gray-90);
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
top: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: auto;
|
||||
/* lang menu should always be full height */
|
||||
height: 19.9rem;
|
||||
}
|
||||
|
||||
/* main menu must be at least as tall as lang menu (when displayed)
|
||||
unless there isn't enough view port height */
|
||||
.nav-list.display-lang-menu {
|
||||
min-height: min(19.9rem, calc(100vh - var(--header-height)));
|
||||
}
|
||||
|
||||
.nav-lang-menu > button:hover {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.nav-lang-menu .nav-link {
|
||||
color: #fff;
|
||||
background-color: inherit;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.nav-lang-menu .nav-link:focus {
|
||||
color: #000;
|
||||
background-color: var(--gray-10);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-lang-menu .nav-link:focus:not(:focus-visible) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.nav-lang-menu .nav-link:hover {
|
||||
background-color: var(--gray-10) !important;
|
||||
}
|
||||
|
||||
.nav-lang-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.nav-lang-button:hover svg,
|
||||
.nav-lang-button:focus {
|
||||
fill: var(--gray-10);
|
||||
}
|
||||
|
||||
.nav-lang-button:focus:not(:focus-visible) {
|
||||
fill: revert;
|
||||
}
|
||||
|
||||
.nav-lang-button[aria-expanded='true'] + .nav-lang-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-lang-button svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.dark-palette .nav-lang-button:focus {
|
||||
fill: revert;
|
||||
}
|
||||
|
||||
.nav-lang-menu-option[aria-current='true'] {
|
||||
/* check mark for current language */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16'%3E%3Cg fill='white'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06a.733.733 0 0 1 1.047 0l3.052 3.093l5.4-6.425a.247.247 0 0 1 .02-.022z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||
background-size: 1.2rem;
|
||||
background-position: calc(100% - 10px) center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.nav-link-lang-dropdown:focus {
|
||||
outline: none;
|
||||
.nav-lang-menu-option[aria-current='true']:focus {
|
||||
/* check mark for current language */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06a.733.733 0 0 1 1.047 0l3.052 3.093l5.4-6.425a.247.247 0 0 1 .02-.022z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-lang-menu-option[aria-current='true']:focus:not(:focus-visible) {
|
||||
/* check mark for current language */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16'%3E%3Cg fill='white'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06a.733.733 0 0 1 1.047 0l3.052 3.093l5.4-6.425a.247.247 0 0 1 .02-.022z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-lang-menu-option[aria-current='true']:hover {
|
||||
/* check mark for current language */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16'%3E%3Cg fill='currentColor'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06a.733.733 0 0 1 1.047 0l3.052 3.093l5.4-6.425a.247.247 0 0 1 .02-.022z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E") !important;
|
||||
}
|
||||
|
||||
.nav-link-flex {
|
||||
|
@ -202,8 +325,10 @@
|
|||
color: var(--gray-00);
|
||||
background-color: var(--theme-color);
|
||||
cursor: pointer;
|
||||
max-height: calc(var(--header-height) - 6px);
|
||||
max-height: calc(var(--header-height) - 8px);
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-button-nav:hover {
|
||||
|
@ -213,8 +338,12 @@
|
|||
}
|
||||
|
||||
.toggle-button-nav:focus {
|
||||
outline: 5px auto -webkit-focus-ring-color !important;
|
||||
outline-offset: -3px;
|
||||
outline: 3px solid var(--blue-mid);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.toggle-button-nav:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.avatar-nav-link {
|
||||
|
@ -269,12 +398,12 @@
|
|||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.display-menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
.expand-lang-menu .display-menu {
|
||||
overflow-y: unset;
|
||||
}
|
||||
|
||||
.toggle-button-nav {
|
||||
display: flex;
|
||||
.display-menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.reverse-toggle-color {
|
||||
|
@ -287,14 +416,14 @@
|
|||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
.nav-line,
|
||||
.nav-line {
|
||||
border-top: 0.1rem solid var(--gray-45);
|
||||
}
|
||||
|
||||
.nav-line-2 {
|
||||
border-color: var(--gray-45);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-line-2 {
|
||||
border-top-width: 2px;
|
||||
}
|
||||
|
||||
|
@ -302,13 +431,71 @@
|
|||
max-height: calc(var(--header-height) - 6px);
|
||||
padding: 0 8px;
|
||||
margin-left: 2px;
|
||||
font-family: 'Roboto Mono', monospace !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-SearchBox-form {
|
||||
width: 100vw;
|
||||
height: 38px;
|
||||
max-width: unset;
|
||||
padding: 0 15px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* In mobile layout, prevent search input from hanging around if the
|
||||
menu is collapsed. */
|
||||
.universal-nav-right
|
||||
#toggle-button-nav[aria-expanded='false']
|
||||
+ .fcc_searchBar
|
||||
.ais-SearchBox-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* In mobile layout, prevent search results from hanging around if the
|
||||
menu is collapsed. */
|
||||
.universal-nav-right
|
||||
#toggle-button-nav[aria-expanded='false']
|
||||
+ .fcc_searchBar
|
||||
.ais-Hits {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.universal-nav-right .ais-SearchBox-input {
|
||||
width: calc(100vw - 17rem);
|
||||
padding-left: 27px;
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-Hits {
|
||||
width: calc(100vw - 17rem);
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-SearchBox-submit {
|
||||
top: unset;
|
||||
margin-top: 19px;
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
.ais-SearchBox-input:focus {
|
||||
outline: 3px solid var(--blue-mid);
|
||||
}
|
||||
|
||||
.ais-SearchBox-submit:focus {
|
||||
outline: 3px solid var(--blue-mid);
|
||||
}
|
||||
|
||||
.ais-SearchBox-submit:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.universal-nav-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.universal-nav-middle {
|
||||
flex: none;
|
||||
}
|
||||
|
@ -350,21 +537,26 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.fcc_searchBar .ais-SearchBox-form {
|
||||
max-width: 100%;
|
||||
.fcc_searchBar,
|
||||
.fcc_searchBar div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ais-SearchBox-input {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.ais-Hits {
|
||||
min-width: calc(100% - 30px);
|
||||
.universal-nav-right .fcc_searchBar .ais-SearchBox-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.display-menu {
|
||||
max-height: calc(100vh - var(--header-height) * 2);
|
||||
}
|
||||
|
||||
.universal-nav-right .ais-SearchBox-input {
|
||||
width: calc(100vw - 30px);
|
||||
}
|
||||
|
||||
.universal-nav-right .fcc_searchBar .ais-Hits {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 455px) {
|
||||
|
|
|
@ -6,29 +6,41 @@
|
|||
import Loadable from '@loadable/component';
|
||||
import React, { Ref } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Media from 'react-responsive';
|
||||
import { isLanding } from '../../../utils/path-parsers';
|
||||
import { Link, SkeletonSprite } from '../../helpers';
|
||||
import MenuButton from './menu-button';
|
||||
import NavLinks from './nav-links';
|
||||
import NavLogo from './nav-logo';
|
||||
import './universal-nav.css';
|
||||
import AuthOrProfile from './auth-or-profile';
|
||||
|
||||
const SearchBar = Loadable(() => import('../../search/searchBar/search-bar'));
|
||||
const SearchBarOptimized = Loadable(
|
||||
() => import('../../search/searchBar/search-bar-optimized')
|
||||
);
|
||||
|
||||
const MAX_MOBILE_WIDTH = 980;
|
||||
|
||||
export interface UniversalNavProps {
|
||||
displayMenu?: boolean;
|
||||
isLanguageMenuDisplayed?: boolean;
|
||||
fetchState?: { pending: boolean };
|
||||
menuButtonRef?: Ref<HTMLButtonElement> | undefined;
|
||||
searchBarRef?: unknown;
|
||||
toggleDisplayMenu?: React.MouseEventHandler<HTMLButtonElement> | undefined;
|
||||
showMenu?: () => void;
|
||||
hideMenu?: () => void;
|
||||
showLanguageMenu?: (elementToFocus: HTMLButtonElement) => void;
|
||||
hideLanguageMenu?: () => void;
|
||||
user?: Record<string, unknown>;
|
||||
}
|
||||
export const UniversalNav = ({
|
||||
displayMenu,
|
||||
toggleDisplayMenu,
|
||||
isLanguageMenuDisplayed,
|
||||
showMenu,
|
||||
hideMenu,
|
||||
showLanguageMenu,
|
||||
hideLanguageMenu,
|
||||
menuButtonRef,
|
||||
searchBarRef,
|
||||
user,
|
||||
|
@ -47,15 +59,13 @@ export const UniversalNav = ({
|
|||
return (
|
||||
<nav
|
||||
aria-label={t('aria.primary-nav')}
|
||||
className={'universal-nav' + (displayMenu ? ' expand-nav' : '')}
|
||||
className={`universal-nav${displayMenu ? ' expand-nav' : ''}`}
|
||||
id='universal-nav'
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'universal-nav-left' + (displayMenu ? ' display-search' : '')
|
||||
}
|
||||
className={`universal-nav-left${displayMenu ? ' display-search' : ''}`}
|
||||
>
|
||||
{search}
|
||||
<Media minWidth={MAX_MOBILE_WIDTH + 1}>{search}</Media>
|
||||
</div>
|
||||
<div className='universal-nav-middle'>
|
||||
<Link id='universal-nav-logo' to='/learn'>
|
||||
|
@ -68,21 +78,32 @@ export const UniversalNav = ({
|
|||
<SkeletonSprite />
|
||||
</div>
|
||||
) : (
|
||||
<MenuButton
|
||||
displayMenu={displayMenu}
|
||||
innerRef={menuButtonRef}
|
||||
onClick={toggleDisplayMenu}
|
||||
user={user}
|
||||
/>
|
||||
<>
|
||||
<MenuButton
|
||||
displayMenu={displayMenu}
|
||||
hideMenu={hideMenu}
|
||||
innerRef={menuButtonRef}
|
||||
showMenu={showMenu}
|
||||
user={user}
|
||||
/>
|
||||
<Media maxWidth={MAX_MOBILE_WIDTH}>{search}</Media>
|
||||
<NavLinks
|
||||
displayMenu={displayMenu}
|
||||
fetchState={fetchState}
|
||||
isLanguageMenuDisplayed={isLanguageMenuDisplayed}
|
||||
hideLanguageMenu={hideLanguageMenu}
|
||||
hideMenu={hideMenu}
|
||||
menuButtonRef={menuButtonRef}
|
||||
showLanguageMenu={showLanguageMenu}
|
||||
showMenu={showMenu}
|
||||
user={user}
|
||||
/>
|
||||
<div className='navatar'>
|
||||
<AuthOrProfile user={user} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<NavLinks
|
||||
displayMenu={displayMenu}
|
||||
fetchState={fetchState}
|
||||
toggleDisplayMenu={toggleDisplayMenu}
|
||||
user={user}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,191 +2,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
|
||||
import React, { Ref } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { create, ReactTestRendererJSON } from 'react-test-renderer';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
|
||||
import envData from '../../../../config/env.json';
|
||||
import { availableLangs, getLangName } from '../../../../config/i18n/all-langs';
|
||||
import { Themes } from '../settings/theme';
|
||||
import AuthOrProfile from './components/auth-or-profile';
|
||||
import { NavLinks } from './components/nav-links';
|
||||
import { UniversalNav } from './components/universal-nav';
|
||||
|
||||
const { apiLocation, clientLocale } = envData;
|
||||
|
||||
jest.mock('../../analytics');
|
||||
|
||||
describe('<UniversalNav />', () => {
|
||||
const UniversalNavProps = {
|
||||
displayMenu: false,
|
||||
menuButtonRef: {} as Ref<HTMLButtonElement> | undefined,
|
||||
searchBarRef: {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
toggleDisplayMenu: function () {},
|
||||
pathName: '/',
|
||||
fetchState: {
|
||||
pending: false
|
||||
}
|
||||
};
|
||||
it('renders to the DOM', () => {
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
utils.render(<UniversalNav {...UniversalNavProps} />);
|
||||
const view = utils.getRenderOutput();
|
||||
expect(view).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<NavLinks />', () => {
|
||||
const { t } = useTranslation();
|
||||
it('has expected navigation links when not signed in', () => {
|
||||
const landingPageProps = {
|
||||
fetchState: {
|
||||
pending: false
|
||||
},
|
||||
user: {
|
||||
isDonating: false,
|
||||
username: null,
|
||||
theme: Themes.Default
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
toggleNightMode: (theme: Themes) => theme,
|
||||
t: t
|
||||
};
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
utils.render(<NavLinks {...landingPageProps} />);
|
||||
const view = utils.getRenderOutput();
|
||||
expect(
|
||||
hasDonateNavItem(view) &&
|
||||
hasSignInNavItem(view) &&
|
||||
hasCurriculumNavItem(view) &&
|
||||
hasForumNavItem(view) &&
|
||||
hasNewsNavItem(view) &&
|
||||
hasRadioNavItem(view) &&
|
||||
hasLanguageHeader(view) &&
|
||||
hasLanguageDropdown(view)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('has expected navigation links when signed in', () => {
|
||||
const landingPageProps = {
|
||||
fetchState: {
|
||||
pending: false
|
||||
},
|
||||
user: {
|
||||
isDonating: false,
|
||||
username: 'nhcarrigan',
|
||||
theme: Themes.Default
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
t: t,
|
||||
toggleNightMode: (theme: Themes) => theme
|
||||
};
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
utils.render(<NavLinks {...landingPageProps} />);
|
||||
const view = utils.getRenderOutput();
|
||||
expect(
|
||||
hasDonateNavItem(view) &&
|
||||
hasCurriculumNavItem(view) &&
|
||||
hasProfileAndSettingsNavItems(view, landingPageProps.user.username) &&
|
||||
hasForumNavItem(view) &&
|
||||
hasNewsNavItem(view) &&
|
||||
hasRadioNavItem(view) &&
|
||||
hasSignOutNavItem(view)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('has expected navigation links when signed in and donating', () => {
|
||||
const landingPageProps = {
|
||||
fetchState: {
|
||||
pending: false
|
||||
},
|
||||
user: {
|
||||
isDonating: true,
|
||||
username: 'moT01',
|
||||
theme: Themes.Default
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
t: t,
|
||||
toggleNightMode: (theme: Themes) => theme
|
||||
};
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
utils.render(<NavLinks {...landingPageProps} />);
|
||||
const view = utils.getRenderOutput();
|
||||
expect(
|
||||
hasThanksForDonating(view) &&
|
||||
hasCurriculumNavItem(view) &&
|
||||
hasProfileAndSettingsNavItems(view, landingPageProps.user.username) &&
|
||||
hasForumNavItem(view) &&
|
||||
hasNewsNavItem(view) &&
|
||||
hasRadioNavItem(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: Themes.Default
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
t: t,
|
||||
toggleNightMode: (theme: Themes) => 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: Themes.Default
|
||||
},
|
||||
i18n: {
|
||||
language: 'en'
|
||||
},
|
||||
t: t,
|
||||
toggleNightMode: (theme: Themes) => theme
|
||||
};
|
||||
const utils = ShallowRenderer.createRenderer();
|
||||
utils.render(<NavLinks {...landingPageProps} />);
|
||||
const view = utils.getRenderOutput();
|
||||
expect(
|
||||
hasLanguageHeader(view) &&
|
||||
hasLanguageDropdown(view) &&
|
||||
hasAllAvailableLanguagesInDropdown(view) &&
|
||||
hasDefaultLanguageInLanguageDropdown(view, clientLocale)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<AuthOrProfile />', () => {
|
||||
it('has avatar with default border for default users', () => {
|
||||
const defaultUserProps = {
|
||||
|
@ -257,123 +78,9 @@ describe('<AuthOrProfile />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const navigationLinks = (component: JSX.Element, key: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const target = component.props.children.find(
|
||||
(child: { key?: string }) => child && child.key === key
|
||||
);
|
||||
return target.props;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const profileNavItem = (component: any) => component.children[0];
|
||||
|
||||
const hasDonateNavItem = (component: JSX.Element) => {
|
||||
const { children, to } = navigationLinks(component, 'donate');
|
||||
return children === 'buttons.donate' && to === '/donate';
|
||||
};
|
||||
|
||||
const hasThanksForDonating = (component: JSX.Element) => {
|
||||
const { children } = navigationLinks(component, 'donate');
|
||||
return children ? children[0].props.children === 'donate.thanks' : null;
|
||||
};
|
||||
|
||||
const hasSignInNavItem = (component: JSX.Element) => {
|
||||
const { children } = navigationLinks(component, 'signin');
|
||||
return children === 'buttons.sign-in';
|
||||
};
|
||||
|
||||
const hasCurriculumNavItem = (component: JSX.Element) => {
|
||||
const { children, to } = navigationLinks(component, 'learn');
|
||||
return children === 'buttons.curriculum' && to === '/learn';
|
||||
};
|
||||
|
||||
const hasProfileAndSettingsNavItems = (
|
||||
component: JSX.Element,
|
||||
username: string
|
||||
) => {
|
||||
const fragment = navigationLinks(component, 'profile-settings');
|
||||
|
||||
const profile = fragment ? fragment.children[0].props : null;
|
||||
const settings = fragment.children[1].props;
|
||||
|
||||
const hasProfile =
|
||||
profile.children === 'buttons.profile' && profile.to === `/${username}`;
|
||||
const hasSettings =
|
||||
settings.children === 'buttons.settings' && settings.to === '/settings';
|
||||
|
||||
return hasProfile && hasSettings;
|
||||
};
|
||||
|
||||
const hasForumNavItem = (component: JSX.Element) => {
|
||||
const { children, to } = navigationLinks(component, 'forum');
|
||||
// TODO: test compiled TFunction value
|
||||
return (
|
||||
children[0].props.children === 'buttons.forum' && to === 'links:nav.forum'
|
||||
);
|
||||
};
|
||||
|
||||
const hasNewsNavItem = (component: JSX.Element) => {
|
||||
const { children, to } = navigationLinks(component, 'news');
|
||||
return (
|
||||
children[0].props.children === 'buttons.news' && to === 'links:nav.news'
|
||||
);
|
||||
};
|
||||
|
||||
const hasRadioNavItem = (component: JSX.Element) => {
|
||||
const { children, to } = navigationLinks(component, 'radio');
|
||||
return (
|
||||
children[0].props.children === 'buttons.radio' &&
|
||||
to === 'https://coderadio.freecodecamp.org'
|
||||
);
|
||||
};
|
||||
|
||||
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) &&
|
||||
getLangName(props.value) === props.children
|
||||
);
|
||||
};
|
||||
|
||||
const hasSignOutNavItem = (component: JSX.Element) => {
|
||||
const { children } = navigationLinks(component, 'signout-frag');
|
||||
const signOutProps = children[1].props;
|
||||
|
||||
return (
|
||||
signOutProps.children === 'buttons.sign-out' &&
|
||||
signOutProps.href === `${apiLocation}/signout`
|
||||
);
|
||||
};
|
||||
|
||||
/* TODO: Apply this to Universalnav component
|
||||
const hasSignInButton = component =>
|
||||
component.props.children[1].props.children === 'buttons.sign-in';
|
||||
*/
|
||||
|
||||
const avatarHasClass = (
|
||||
componentTree: ReactTestRendererJSON | ReactTestRendererJSON[] | null,
|
||||
classes: string
|
||||
|
|
|
@ -15,52 +15,71 @@ export interface HeaderProps {
|
|||
}
|
||||
export class Header extends React.Component<
|
||||
HeaderProps,
|
||||
{ displayMenu: boolean }
|
||||
{ displayMenu: boolean; isLanguageMenuDisplayed: boolean }
|
||||
> {
|
||||
menuButtonRef: React.RefObject<any>;
|
||||
menuButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
searchBarRef: React.RefObject<any>;
|
||||
static displayName: string;
|
||||
constructor(props: HeaderProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
displayMenu: false
|
||||
displayMenu: false,
|
||||
isLanguageMenuDisplayed: false
|
||||
};
|
||||
this.menuButtonRef = React.createRef();
|
||||
this.searchBarRef = React.createRef();
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
this.toggleDisplayMenu = this.toggleDisplayMenu.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
this.showMenu = this.showMenu.bind(this);
|
||||
this.hideMenu = this.hideMenu.bind(this);
|
||||
this.showLanguageMenu = this.showLanguageMenu.bind(this);
|
||||
this.hideLanguageMenu = this.hideLanguageMenu.bind(this);
|
||||
}
|
||||
|
||||
handleClickOutside(event: globalThis.MouseEvent): void {
|
||||
const eventTarget = event.target as HTMLElement;
|
||||
if (
|
||||
this.state.displayMenu &&
|
||||
this.menuButtonRef.current &&
|
||||
!this.menuButtonRef.current.contains(event.target) &&
|
||||
!this.menuButtonRef.current.contains(eventTarget) &&
|
||||
// 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) &&
|
||||
!(event.target instanceof HTMLSelectElement)
|
||||
!this.searchBarRef.current.contains(eventTarget) &&
|
||||
// don't count clicks on language button/menu
|
||||
!eventTarget.closest('.nav-lang') &&
|
||||
// don't count clicks on disabled elements
|
||||
!eventTarget.closest('[aria-disabled="true"]')
|
||||
) {
|
||||
this.toggleDisplayMenu();
|
||||
this.hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
toggleDisplayMenu(): void {
|
||||
this.setState(({ displayMenu }: { displayMenu: boolean }) => ({
|
||||
displayMenu: !displayMenu
|
||||
}));
|
||||
showMenu(): void {
|
||||
this.setState({ displayMenu: true }, () => {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
});
|
||||
}
|
||||
|
||||
hideMenu(): void {
|
||||
this.setState({ displayMenu: false }, () => {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
this.hideLanguageMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// elementToFocus must be a link in the language menu
|
||||
showLanguageMenu(elementToFocus: HTMLButtonElement): void {
|
||||
this.setState({ isLanguageMenuDisplayed: true }, () =>
|
||||
elementToFocus.focus()
|
||||
);
|
||||
}
|
||||
|
||||
hideLanguageMenu(): void {
|
||||
this.setState({ isLanguageMenuDisplayed: false });
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { displayMenu } = this.state;
|
||||
const { displayMenu, isLanguageMenuDisplayed } = this.state;
|
||||
const { fetchState, user } = this.props;
|
||||
return (
|
||||
<>
|
||||
|
@ -71,9 +90,13 @@ export class Header extends React.Component<
|
|||
<UniversalNav
|
||||
displayMenu={displayMenu}
|
||||
fetchState={fetchState}
|
||||
isLanguageMenuDisplayed={isLanguageMenuDisplayed}
|
||||
hideLanguageMenu={this.hideLanguageMenu}
|
||||
hideMenu={this.hideMenu}
|
||||
menuButtonRef={this.menuButtonRef}
|
||||
searchBarRef={this.searchBarRef}
|
||||
toggleDisplayMenu={this.toggleDisplayMenu}
|
||||
showMenu={this.showMenu}
|
||||
showLanguageMenu={this.showLanguageMenu}
|
||||
user={user}
|
||||
/>
|
||||
</header>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
describe('Navigation links', () => {
|
||||
it('should render the expected forum and news links.', () => {
|
||||
cy.visit('/learn');
|
||||
cy.get('.toggle-button-nav').should('be.visible');
|
||||
cy.get('.toggle-button-nav').click();
|
||||
cy.get('.nav-list')
|
||||
.contains('Forum')
|
||||
.should('have.attr', 'href', 'https://forum.freecodecamp.org/');
|
||||
cy.get('.nav-list')
|
||||
.contains('News')
|
||||
.should('have.attr', 'href', 'https://freecodecamp.org/news/');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
import { availableLangs, LangNames } from '../../../../config/i18n/all-langs';
|
||||
import envData from '../../../../config/env.json';
|
||||
|
||||
const { clientLocale } = envData;
|
||||
|
||||
const selectors = {
|
||||
'navigation-list': '.nav-list',
|
||||
'toggle-button': '.toggle-button-nav',
|
||||
'language-menu': '.nav-lang-menu',
|
||||
'sign-in-button': "[data-test-label='landing-small-cta']",
|
||||
'avatar-link': '.avatar-nav-link',
|
||||
'avatar-container': '.avatar-container'
|
||||
};
|
||||
|
||||
const links = {
|
||||
'sign-in': '/signin',
|
||||
'sign-out': '/signout',
|
||||
donate: '/donate',
|
||||
curriculum: '/learn',
|
||||
forum: 'https://forum.freecodecamp.org/',
|
||||
news: 'https://freecodecamp.org/news/',
|
||||
radio: 'https://coderadio.freecodecamp.org',
|
||||
'avatar-link': '/developmentuser',
|
||||
settings: '/settings'
|
||||
};
|
||||
|
||||
describe('Default Navigation Menu', () => {
|
||||
it('should render the expected nav items.', () => {
|
||||
cy.visit('/learn');
|
||||
testLink('Sign in', 'sign-in-button');
|
||||
cy.get(selectors['language-menu']).should('not.be.visible');
|
||||
cy.get(selectors['toggle-button']).should('be.visible').click();
|
||||
cy.get(selectors['navigation-list']).contains('Sign in to change theme.');
|
||||
testLink('Donate');
|
||||
testLink('Curriculum');
|
||||
testLink('Forum');
|
||||
testLink('News');
|
||||
testLink('Radio');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lanuage menu', () => {
|
||||
it('should render all used languages.', () => {
|
||||
cy.get(selectors['navigation-list']).contains('Change Language').click();
|
||||
testAllLanuges();
|
||||
cy.get(selectors['language-menu'])
|
||||
.should('be.visible')
|
||||
.contains('English')
|
||||
.should('have.attr', 'aria-current', 'true');
|
||||
cy.get(selectors['language-menu'])
|
||||
.should('be.visible')
|
||||
.contains(LangNames[clientLocale])
|
||||
.should('have.attr', 'aria-current', 'true');
|
||||
});
|
||||
it('should have default language selected', () => {
|
||||
cy.get(selectors['language-menu'])
|
||||
.should('be.visible')
|
||||
.contains(LangNames[clientLocale])
|
||||
.should('have.attr', 'aria-current', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authenticated Navigation Menu', () => {
|
||||
before(() => {
|
||||
cy.clearCookies();
|
||||
cy.exec('npm run seed');
|
||||
cy.login();
|
||||
cy.get(selectors['toggle-button']).should('be.visible').click();
|
||||
});
|
||||
it('should show default avatar.', () => {
|
||||
testLink('Settings');
|
||||
testLink('Sign out');
|
||||
cy.get(selectors['sign-in-button']).should('not.exist');
|
||||
cy.get(selectors['avatar-link'])
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', links['avatar-link']);
|
||||
cy.get(selectors['avatar-container']).should(
|
||||
'have.class',
|
||||
'default-border'
|
||||
);
|
||||
cy.get(selectors['navigation-list']).contains('Night Mode').click();
|
||||
cy.get('body').should('have.class', 'dark-palette');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Donor Navigation Menu', () => {
|
||||
before(() => {
|
||||
cy.clearCookies();
|
||||
cy.exec('npm run seed -- --donor');
|
||||
cy.login();
|
||||
cy.visit('/donate');
|
||||
});
|
||||
it('should show donor avatar border.', () => {
|
||||
cy.get(selectors['avatar-container']).should('have.class', 'gold-border');
|
||||
});
|
||||
it('should show thank you message.', () => {
|
||||
cy.get(selectors['navigation-list']).contains('Thanks for donating');
|
||||
});
|
||||
});
|
||||
|
||||
const testAllLanuges = () => {
|
||||
const availableLangNames = availableLangs.client.map(lang => LangNames[lang]);
|
||||
availableLangNames.forEach(langName =>
|
||||
cy.get(selectors['language-menu']).contains(langName)
|
||||
);
|
||||
};
|
||||
|
||||
const testLink = (item, selector = 'navigation-list') =>
|
||||
cy
|
||||
.get(selectors[selector])
|
||||
.contains(item)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', links[item.replaceAll(' ', '-').toLowerCase()]);
|
Loading…
Reference in New Issue