Skip to content

Commit

Permalink
feat: add hierarchical menus & universal bar menu support to Navigation
Browse files Browse the repository at this point in the history
Navigation:
 - add support for hierarchical menus
 - add support for universal bar menu
   - the universal bar menu is shown on the top of the header at least
     when using a wide screen
 - remove deprecated variant property

queries:
 - add parentId to menu query to fetch hierarchy of menus

mocks:
 - make navigation mock menu hierarchical
   - this hierarchical menu can be seen in storybook's ArchiveSearchPage
 - add mock for universal bar menu

packages:
 - add crypto-browserify as development dependency to fix running
   storybook
   - crypto is being used by hds-react since HDS v3
 - set version to 1.0.0-alpha230

refs LIIKUNTA-582
  • Loading branch information
karisal-anders committed Nov 18, 2023
1 parent 98a672e commit aa71ea3
Show file tree
Hide file tree
Showing 11 changed files with 561 additions and 41 deletions.
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
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
116 changes: 97 additions & 19 deletions src/core/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { groupBy } from 'lodash-es';
import {
Header,
LanguageOption,
Expand All @@ -10,19 +11,25 @@ import {
// 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 { 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,17 +40,15 @@ 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 = {
const CITY_OF_HELSINKI_TRANSLATIONS = {
EN: 'City of Helsinki',
FI: 'Helsingin kaupunki',
SV: 'Helsingfors stad',
} as const satisfies Record<LanguageCodeEnum, string>;

const LOGO_LABELS = {
const HELSINKI_TRANSLATIONS = {
EN: 'Helsinki',
FI: 'Helsinki',
SV: 'Helsingfors',
Expand Down Expand Up @@ -71,15 +76,64 @@ 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,
Expand Down Expand Up @@ -120,7 +174,24 @@ export function Navigation({
const logoProps: LogoProps = {
size: 'large',
src: LOGO_SOURCES[currentLanguageCode],
alt: LOGO_LABELS[currentLanguageCode],
alt: HELSINKI_TRANSLATIONS[currentLanguageCode],
};

const localizedCityOfHelsinki =
CITY_OF_HELSINKI_TRANSLATIONS[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 +205,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 +224,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

0 comments on commit aa71ea3

Please sign in to comment.