Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LIIKUNTA-582, LIIKUNTA-585 | feat: add hierarchical menus & universal bar menu support to Navigation #138

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],

webpackFinal: async (config) => {
config.resolve.fallback.crypto = require.resolve('crypto-browserify');
return config;
},

addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-helsinki-headless-cms",
"version": "1.0.0-alpha229",
"version": "1.0.0-alpha230",
"description": "React components for displaying Headless CMS content according to guidelines set by HDS",
"main": "cjs/index.js",
"module": "index.js",
Expand Down Expand Up @@ -93,6 +93,7 @@
"babel-loader": "^9.1.3",
"classnames": "^2.3.1",
"cross-env": "^7.0.3",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"edit-json-file": "^1.7.0",
"eslint": "^8.47.0",
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const CITY_OF_HELSINKI_WEBSITE_URL = 'https://www.hel.fi/';
export const MAIN_CONTENT_ID = 'main-content';
export const TOP_LEVEL_MENU_ITEM_PARENT_ID = 'null_parent_id' as const;
5 changes: 4 additions & 1 deletion src/common/headlessService/__generated__.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12340,6 +12340,7 @@ export type LanguagesQuery = {
export type MenuItemFragment = {
__typename?: 'MenuItem';
id: string;
parentId?: string | null;
order?: number | null;
target?: string | null;
title?: string | null;
Expand Down Expand Up @@ -14667,6 +14668,7 @@ export type MenuQuery = {
nodes: Array<{
__typename?: 'MenuItem';
id: string;
parentId?: string | null;
order?: number | null;
target?: string | null;
title?: string | null;
Expand Down Expand Up @@ -19567,6 +19569,7 @@ export const MenuPageFieldsFragmentDoc = gql`
export const MenuItemFragmentDoc = gql`
fragment MenuItem on MenuItem {
id
parentId
order
target
title
Expand Down Expand Up @@ -20009,7 +20012,7 @@ export const MenuDocument = gql`
query menu($id: ID!) {
menu(idType: NAME, id: $id) {
id
menuItems {
menuItems(first: 100) {
nodes {
...MenuItem
}
Expand Down
6 changes: 5 additions & 1 deletion src/common/headlessService/graphql/menu.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
fragment MenuItem on MenuItem {
id
parentId
order
target
title
Expand Down Expand Up @@ -29,7 +30,10 @@ fragment menuPageFields on Page {
query menu($id: ID!) {
menu(idType: NAME, id: $id) {
id
menuItems {
# Using "first" to try to get all menu items without pagination, 100 was the maximum
# that could be received without pagination from production Headless CMS environment
# using "pages" query so hopefully the limit is not lower for menuItems
menuItems(first: 100) {
nodes {
...MenuItem
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/archiveSearchPage/ArchiveSearchPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { defaultConfig } from '../configProvider/defaultConfig';
import { SearchPageContent as ArchiveSearchPageContent } from '../archiveSearchPageContent/ArchiveSearchPageContent';
import navigationLanguages from '../navigation/__mocks__/navigationLanguages.mock';
import navigationMenu from '../navigation/__mocks__/navigationMenu.mock';
import navigationUniversalBarMenu from '../navigation/__mocks__/navigationUniversalBarMenu.mock';
import { Navigation } from '../navigation/Navigation';
import { ArchivePage as ArchiveSearchPage } from './ArchiveSearchPage';
import articles from '../archiveSearchPageContent/__mocks__/articles.mock';
Expand Down Expand Up @@ -46,6 +47,7 @@ const navigation = (
<Navigation
languages={navigationLanguages}
menu={navigationMenu}
universalBarMenu={navigationUniversalBarMenu}
onTitleClick={() => {
// eslint-disable-next-line no-console
console.log('I should navigate');
Expand Down
13 changes: 13 additions & 0 deletions src/core/configProvider/configContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,26 @@ import { VenueType } from '../../common/venuesService/types';
import type { CardProps } from '../card/Card';
import { HtmlToReactProps } from '../../common/components/htmlToReact/HtmlToReact';

export const FALLBACK_TRANSLATION_KEYS = [
'cityOfHelsinki',
'helsinki',
'helsinkiLogo',
] as const;
export type FallbackTranslationKey = (typeof FALLBACK_TRANSLATION_KEYS)[number];
export type FallbackTranslation = Record<LanguageCodeEnum, string>;
export type FallbackTranslations = Record<
FallbackTranslationKey,
FallbackTranslation
>;

export type Config = {
siteName: string;
mainContentId?: string;
internalHrefOrigins: string[];
organisationPrefixes: string[];
currentLanguageCode: LanguageCodeEnum;
fallbackImageUrls: string[];
fallbackTranslations: FallbackTranslations;
copy: {
breadcrumbNavigationLabel: string;
breadcrumbListLabel: string;
Expand Down
21 changes: 21 additions & 0 deletions src/core/configProvider/defaultConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { logoFi, logoSv } from 'hds-react';

import { ModuleItemTypeEnum } from '../../common/headlessService/constants';
import { LanguageCodeEnum } from '../../common/headlessService/types';
Expand All @@ -22,6 +23,26 @@ export const defaultConfig: Config = {
'./images/event_placeholder_D.jpg',
],
organisationPrefixes: [],
// Here are the fallback translations for English/Finnish/Swedish:
fallbackTranslations: {
cityOfHelsinki: {
EN: 'City of Helsinki',
FI: 'Helsingin kaupunki',
SV: 'Helsingfors stad',
},
helsinki: {
EN: 'Helsinki',
FI: 'Helsinki',
SV: 'Helsingfors',
},
helsinkiLogo: {
EN: logoFi,
FI: logoFi,
SV: logoSv,
},
},
// Here are the current language's translations:
// FIXME: "copy" should be renamed for clarity
copy: {
breadcrumbNavigationLabel: 'Breadcrumb navigation',
breadcrumbListLabel: 'breadcrumbs',
Expand Down
2 changes: 2 additions & 0 deletions src/core/navigation/Navigation.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
.maxWidthXl {
div[class^='HeaderActionBar-module_headerActionBar_'],
div[class*=' HeaderActionBar-module_headerActionBar_'],
div[class^='HeaderUniversalBar-module_headerUniversalBar_'],
div[class*=' HeaderUniversalBar-module_headerUniversalBar_'],
nav[class^='HeaderNavigationMenu-module_headerNavigationMenu_'],
nav[class*=' HeaderNavigationMenu-module_headerNavigationMenu_'] {
margin: 0 auto;
Expand Down
148 changes: 99 additions & 49 deletions src/core/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import React from 'react';
import {
Header,
LanguageOption,
Logo,
logoFi,
LogoProps,
logoSv,
} from 'hds-react';
import { groupBy } from 'lodash-es';
import { Header, LanguageOption, Logo, LogoProps } from 'hds-react';
// eslint-disable-next-line import/no-extraneous-dependencies
import classNames from 'classnames';

import { Config } from '../configProvider/configContext';
import styles from './Navigation.module.scss';
import {
Language,
LanguageCodeEnum,
Menu,
} from '../../common/headlessService/types';
import { Language, Menu } from '../../common/headlessService/types';
import { useConfig } from '../configProvider/useConfig';
import { MAIN_CONTENT_ID } from '../../common/constants';
import {
CITY_OF_HELSINKI_WEBSITE_URL,
MAIN_CONTENT_ID,
TOP_LEVEL_MENU_ITEM_PARENT_ID,
} from '../../common/constants';

type MenuItem = Omit<Menu['menuItems']['nodes'][0], '__typename'>;

export type NavigationProps = {
menu?: Menu;
universalBarMenu?: Menu;
languages?: Language[];
className?: string;
userNavigation?: React.ReactNode;
Expand All @@ -33,28 +29,8 @@ export type NavigationProps = {
allLanguages: Language[],
) => string;
getIsItemActive?: (menuItem: MenuItem) => boolean;
/** @deprecated Not used anymore i.e. does nothing after HDS 3 was taken into use. */
variant?: 'default' | 'inline';
};

const LOGO_ARIA_LABELS = {
EN: 'City of Helsinki',
FI: 'Helsingin kaupunki',
SV: 'Helsingfors stad',
} as const satisfies Record<LanguageCodeEnum, string>;

const LOGO_LABELS = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can add those though hcrc config, in copy section we have translations

Copy link
Contributor Author

@karisal-anders karisal-anders Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to config:

  • LOGO_LABELS -> config.translations.helsinki

EN: 'Helsinki',
FI: 'Helsinki',
SV: 'Helsingfors',
} as const satisfies Record<LanguageCodeEnum, string>;

const LOGO_SOURCES = {
EN: logoFi,
FI: logoFi,
SV: logoSv,
} as const satisfies Record<LanguageCodeEnum, string>;

/**
* Find language from language list by language code.
* @param {Language[]} languages - List of languages
Expand All @@ -71,19 +47,69 @@ function findLanguage(
);
}

interface MenuItemChildren {
[menuItemId: string]: MenuItem[];
}

type MenuLinkProps = {
menuItemChildren?: MenuItemChildren;
navigationItem: MenuItem;
getRoutedInternalHref: Config['utils']['getRoutedInternalHref'];
getIsItemActive: NavigationProps['getIsItemActive'];
A: Config['components']['A'];
};

/**
* Create a menu link element
* @param {MenuLinkProps} props - Properties, if menuItemChildren is undefined then the
* created element will not have dropdown links
* @return {React.JSX.Element} Header.Link element
*/
function createMenuLinkElement(props: MenuLinkProps) {
const {
menuItemChildren,
navigationItem,
getRoutedInternalHref,
getIsItemActive,
A,
} = props;
return (
<Header.Link
as={A}
id={navigationItem.id}
key={navigationItem.id}
title={navigationItem.title ?? undefined}
label={navigationItem.label ?? ''}
href={getRoutedInternalHref(navigationItem.path)}
active={getIsItemActive?.(navigationItem) ?? false}
dropdownLinks={
(menuItemChildren?.[navigationItem.id]?.length &&
menuItemChildren[navigationItem.id]?.map((childNavigationItem) =>
createMenuLinkElement({
...props,
navigationItem: childNavigationItem,
}),
)) ||
undefined
}
/>
);
}

export function Navigation({
menu,
universalBarMenu,
languages,
className,
userNavigation,
onTitleClick,
getPathnameForLanguage,
getIsItemActive,
variant, // eslint-disable-line @typescript-eslint/no-unused-vars
}: NavigationProps) {
const {
siteName,
currentLanguageCode,
fallbackTranslations,
copy: { menuToggleAriaLabel, skipToContentLabel },
components: { A },
utils: { getRoutedInternalHref },
Expand Down Expand Up @@ -119,8 +145,25 @@ export function Navigation({

const logoProps: LogoProps = {
size: 'large',
src: LOGO_SOURCES[currentLanguageCode],
alt: LOGO_LABELS[currentLanguageCode],
src: fallbackTranslations.helsinkiLogo[currentLanguageCode],
alt: fallbackTranslations.helsinki[currentLanguageCode],
};

const localizedCityOfHelsinki =
fallbackTranslations.cityOfHelsinki[currentLanguageCode];

const menuItemChildren: MenuItemChildren = groupBy(
menu?.menuItems?.nodes ?? [],
(menuItem) => menuItem.parentId ?? TOP_LEVEL_MENU_ITEM_PARENT_ID,
);

const sharedMenuLinkProps: Omit<
MenuLinkProps,
'navigationItem' | 'menuItemChildren'
> = {
getRoutedInternalHref,
getIsItemActive,
A,
};

return (
Expand All @@ -134,6 +177,16 @@ export function Navigation({
skipTo={`#${MAIN_CONTENT_ID}`}
label={skipToContentLabel}
/>
{universalBarMenu && (
<Header.UniversalBar
primaryLinkText={localizedCityOfHelsinki}
primaryLinkHref={CITY_OF_HELSINKI_WEBSITE_URL}
>
{universalBarMenu?.menuItems?.nodes?.map((navigationItem) =>
createMenuLinkElement({ ...sharedMenuLinkProps, navigationItem }),
)}
</Header.UniversalBar>
)}
<Header.ActionBar
titleHref="#"
logoHref="#"
Expand All @@ -143,23 +196,20 @@ export function Navigation({
onLogoClick={onTitleClick}
frontPageLabel={siteName}
logo={<Logo {...logoProps} />}
logoAriaLabel={LOGO_ARIA_LABELS[currentLanguageCode]}
logoAriaLabel={localizedCityOfHelsinki}
>
<Header.LanguageSelector />
{userNavigation && userNavigation}
</Header.ActionBar>
<Header.NavigationMenu>
{menu?.menuItems?.nodes?.map((navigationItem) => (
<Header.Link
as={A}
id={navigationItem.id}
key={navigationItem.id}
title={navigationItem.title}
label={navigationItem.label}
href={getRoutedInternalHref(navigationItem.path)}
active={getIsItemActive?.(navigationItem) ?? false}
/>
))}
{menuItemChildren[TOP_LEVEL_MENU_ITEM_PARENT_ID]?.map(
(navigationItem) =>
createMenuLinkElement({
...sharedMenuLinkProps,
menuItemChildren,
navigationItem,
}),
)}
</Header.NavigationMenu>
</Header>
);
Expand Down
Loading
Loading