From 2cbf6fb25ef2109cbf7fd9e96ff1d17e93ea16b7 Mon Sep 17 00:00:00 2001 From: Jennifer Blumberg Date: Sun, 8 Sep 2024 15:51:52 -0700 Subject: [PATCH] New desktop header (#4598) ### Description This PR is an initial version of the desktop menu for the homepage revamp. The mobile menu is temporarily removed, so that the desktop menu can be reviewed in isolation. *This PR is not intended for merge into master until the completion of the homepage revamp* ### Notes There is still some refactoring and cleanup to do (particularly in the CSS) that will be done as part of a subsequent larger PR that includes the mobile menu. However, the functionality of the desktop can be previewed. --------- Co-authored-by: Nicholas Blumberg <41446765+nick-next@users.noreply.github.com> Co-authored-by: Nick B Co-authored-by: Pablo Noel --- server/__init__.py | 4 +- server/config/base/header_v2.json | 120 ++++++++++--- server/templates/base.html | 2 +- static/css/core.scss | 162 ++---------------- static/js/apps/base/components/header_bar.tsx | 130 -------------- .../base/components/header_bar/header_bar.tsx | 20 +-- .../components/header_bar/header_logo.tsx | 35 +--- .../components/header_bar/menu_desktop.tsx | 103 +++++------ .../header_bar/menu_desktop_rich_menu.tsx | 27 +-- .../header_bar/menu_desktop_section_group.tsx | 82 +++------ .../components/header_bar/menu_mobile.tsx | 51 +----- .../header_bar/menu_mobile_rich_menu.tsx | 24 +-- .../header_bar/menu_rich_section_group.tsx | 60 +++---- static/js/apps/base/header_app.tsx | 6 +- static/js/apps/base/main.ts | 4 +- static/js/apps/homepage/app_v2.tsx | 26 --- .../nl_search_bar/nl_search_bar_standard.tsx | 14 +- static/js/shared/types/base.ts | 54 ++++++ static/webpack.config.js | 1 + 19 files changed, 309 insertions(+), 616 deletions(-) delete mode 100644 static/js/apps/base/components/header_bar.tsx diff --git a/server/__init__.py b/server/__init__.py index cd4eec6325..11103a9b01 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -445,8 +445,10 @@ def add_language_code(endpoint, values): @app.context_processor def inject_common_parameters(): common_variables = { + #TODO: replace HEADER_MENU with V2 'HEADER_MENU': json.dumps(libutil.get_json("config/base/header.json")), - 'FOOTER_MENU': json.dumps(libutil.get_json("config/base/footer.json")) + 'FOOTER_MENU': json.dumps(libutil.get_json("config/base/footer.json")), + 'HEADER_MENU_V2': json.dumps(libutil.get_json("config/base/header_v2.json")), } locale_variable = dict(locale=get_locale()) return {**common_variables, **locale_variable} diff --git a/server/config/base/header_v2.json b/server/config/base/header_v2.json index f51472dc1e..fa18989590 100644 --- a/server/config/base/header_v2.json +++ b/server/config/base/header_v2.json @@ -1,14 +1,76 @@ [ { - "id": "tools", + "label": "Overview", + "ariaLabel": "Data Commons overview", + "introduction": "Data Commons is an open-source platform aggregating global public data for easy exploration using natural language.", + "primarySectionGroups": [ + { + "title": "Key Features", + "items": [ + { + "title": "Large harmonized public dataset", + "description": "240 billion data points across 260K statistical variables, harmonized from governmental, inter-governmental, academic and non-profit organizations" + }, + { + "title": "Natural language interface", + "description": "Data Commons uses AI for natural language queries, making public data accessible and useful to all" + } + ] + }, + { + "title": "Build your Data Commons", + "items": [ + { + "title": "Tailor your own Data Commons", + "description": "Launch your own Data Commons and customize it with your own data to better engage your specific audience" + }, + { + "title": "Explore your data with natural language", + "description": "Ask questions in your own words and get answers directly from your data" + }, + { + "title": "Actionable Insights", + "description": "Gain actionable insights from your data in connection to global data", + "links": [ + { + "title": "Learn more & build yours today", + "url": "{place.place}" + } + ] + } + ] + } + ], + "secondarySectionGroups": [ + { + "title": "Other Data Commons", + "items": [ + { + "title": "Partners", + "description": "Featured organizations who organizations have tailored their own Data Commons to meet their specific needs and goals", + "links": [ + { + "title": "United Nations", + "url": "https://www.un.org", + "linkType": "external" + }, + { + "title": "One.org", + "url": "https://one.org", + "linkType": "external" + } + ] + } + ] + } + ] + }, + { "label": "Tools", "ariaLabel": "Show exploration tools", - "introduction": { - "description": "Explore a variety of tools to visualize, analyze, and interact with the Data Commons knowledge graph and its extensive datasets" - }, + "introduction": "Explore a variety of tools to visualize, analyze, and interact with the Data Commons knowledge graph and its extensive datasets.", "primarySectionGroups": [ { - "id": "tools-0", "items": [ { "title": "Knowledge Graph", @@ -28,7 +90,6 @@ ] }, { - "id": "tools-1", "items": [ { "title": "Map Explorer", @@ -50,90 +111,93 @@ ] }, { - "id": "docs", "label": "Documentation", "ariaLabel": "Show documentation links", - "introduction": { - "description": "Access in-depth tutorials, guides, and API references to unlock the full potential of Data Commons and integrate it into your projects" - }, + "introduction": "Access in-depth tutorials, guides, and API references to unlock the full potential of Data Commons and integrate it into your projects.", "primarySectionGroups": [ { - "id": "docs-0", "items": [ { "title": "Docs", "url": "https://docs.datacommons.org", - "description": "Learn how to access and visualize Data Commons data: docs for the website, APIs, and more, for all users and needs" + "description": "Learn how to leverage the Data Commons unified database with comprehensive documentation, tutorials, and guides." }, { "title": "API", "url": "https://docs.datacommons.org/api", - "description": "Access Data Commons data programmatically, using REST and Python APIs" + "description": "Access a unified knowledge graph with standardized data from diverse sources using Data Commons APIs." } ] }, { - "id": "docs-1", "items": [ { "title": "Tutorials", "url": "https://docs.datacommons.org/tutorials", - "description": "Get familiar with the Data Commons Knowledge Graph and APIs using analysis examples in Google Colab notebooks written in Python" + "description": "Get familiar with the Data Commons Knowledge Graph and APIs using analysis examples in Google Colab notebooks written in Python." }, { "title": "Contributions", "url": "https://docs.datacommons.org/contributing/", - "description": "Become part of Data Commons by contributing data, tools, educational materials, or sharing your analysis and insights. Collaborate and help expand the Data Commons Knowledge Graph" + "description": "Become part of Data Commons by contributing data, tools, educational materials, or sharing your analysis and insights. Collaborate and help expand the knowledge graph!" } ] } ] }, { - "id": "about", "label": "About", "ariaLabel": "Show about links", - "introduction": { - "description": "Data Commons is an initiative from Google. Explore diverse data, learn to use its tools through Python examples, and stay updated on the latest news and research" - }, + "introduction": "Data Commons is an initiative from Google. Discover how Data Commons is changing data analysis. Explore diverse data, learn to use its tools through Python examples, and stay updated on the latest news and research.", "primarySectionGroups": [ { - "id": "about-0", "items": [ { "title": "Why Data Commons", "url": "{static.about}", - "description": "Discover why Data Commons is revolutionizing data access and analysis. Learn how its unified Knowledge Graph empowers you to explore diverse, standardized data" + "description": "Discover why Data Commons is revolutionizing data access and analysis. Learn how its unified knowledge graph empowers you to explore diverse, standardized data." }, { "title": "Data Sources", "url": "https://docs.datacommons.org/datasets/", - "description": "Get familiar with the data available in Data Commons" + "description": "Get familiar with the Data Commons Knowledge Graph and APIs using analysis examples in Google Colab notebooks written in Python.", + "links": [ + { + "title": "Data Updates", + "url": "https://www.datacommons.org/rss", + "linkType": "rss" + } + ] } ] }, { - "id": "about-1", "items": [ { "title": "FAQ", "url": "{static.faq}", - "description": "Find quick answers to common questions about Data Commons, its usage, data sources, and available resources" + "description": "Find quick answers to common questions about Data Commons, its usage, data sources, and available resources." }, { "title": "Blog", "url": "https://blog.datacommons.org/", - "description": "Stay up-to-date with the latest news, updates, and insights from the Data Commons team. Explore new features, research, and educational content related to the project" + "description": "Stay up-to-date with the latest news, updates, and insights from the Data Commons team. Explore new features, research, and educational content related to the project.", + "links": [ + { + "title": "Blog posts", + "url": "https://blog.datacommons.org/rss", + "linkType": "rss" + } + ] } ] } ] }, { - "id": "feedback", "label": "Feedback", "ariaLabel": "Give feedback", - "url": "https://docs.google.com/forms/d/e/1FAIpQLSc_xIinZPbO5RHDjq2a4eMoElkFgfe79U5DQ8-_kVhCNf2FJQ/viewform", + "url": "{static.feedback}", "exposeInMobileBanner": true } ] \ No newline at end of file diff --git a/server/templates/base.html b/server/templates/base.html index f72be22023..27b30cb015 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -110,7 +110,7 @@
{ - const visibleMenu = useMemo(() => { - return menu.map((menuItem) => ({ - ...menuItem, - subMenu: menuItem.subMenu.filter((subMenuItem) => !subMenuItem.hide), - })); - }, [menu]); - - return ( -
- -
- ); -}; - -export default HeaderBar; diff --git a/static/js/apps/base/components/header_bar/header_bar.tsx b/static/js/apps/base/components/header_bar/header_bar.tsx index a8d1730e0c..5187f5e0e7 100644 --- a/static/js/apps/base/components/header_bar/header_bar.tsx +++ b/static/js/apps/base/components/header_bar/header_bar.tsx @@ -20,7 +20,7 @@ import React, { ReactElement } from "react"; -import { HeaderMenu, Labels, Routes } from "../../../../shared/types/base"; +import { HeaderMenuV2, Labels, Routes } from "../../../../shared/types/base"; import HeaderBarSearch from "./header_bar_search"; import HeaderLogo from "./header_logo"; import MenuDesktop from "./menu_desktop"; @@ -31,12 +31,8 @@ interface HeaderBarProps { name: string; //a path to the logo to be displayed in the header logoPath: string; - //the width of the logo - if provided, this will be used to prevent content bouncing as the logo loads in after the rest of the content. - logoWidth: string; //the data that will populate the header menu. - menu: HeaderMenu[]; - //if set true, the header menu will show - this value is pulled in from the page template and will default to false. - showHeaderSearchBar: boolean; + menu: HeaderMenuV2[]; //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; //the routes dictionary - this is used to convert routes to resolved urls @@ -46,39 +42,35 @@ interface HeaderBarProps { const HeaderBar = ({ name, logoPath, - logoWidth, menu, - showHeaderSearchBar, labels, routes, }: HeaderBarProps): ReactElement => { return ( -
+
-
+ ); }; diff --git a/static/js/apps/base/components/header_bar/header_logo.tsx b/static/js/apps/base/components/header_bar/header_logo.tsx index 36ff0eb619..b262fe3b01 100644 --- a/static/js/apps/base/components/header_bar/header_logo.tsx +++ b/static/js/apps/base/components/header_bar/header_logo.tsx @@ -20,12 +20,6 @@ import React, { ReactElement } from "react"; -import { - GA_EVENT_HEADER_CLICK, - GA_PARAM_ID, - GA_PARAM_URL, - triggerGAEvent, -} from "../../../../shared/ga_events"; import { Labels, Routes } from "../../../../shared/types/base"; interface HeaderLogoProps { @@ -33,8 +27,6 @@ interface HeaderLogoProps { name: string; //a path to the logo to be displayed in the header logoPath: string; - //the width of the logo - if provided, this will be used to prevent content bouncing as the logo loads in after the rest of the content. - logoWidth: string; //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; //the routes dictionary - this is used to convert routes to resolved urls @@ -44,7 +36,6 @@ interface HeaderLogoProps { const HeaderLogo = ({ name, logoPath, - logoWidth, labels, routes, }: HeaderLogoProps): ReactElement => { @@ -55,34 +46,12 @@ const HeaderLogo = ({ { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: "dc-logo", - [GA_PARAM_URL]: "{static.homepage}", - }); - return true; - }} > - {`${name} + {`${name}
)} - { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: "dc-name", - [GA_PARAM_URL]: "{static.homepage}", - }); - return true; - }} - > - {name} - + {name} ); }; diff --git a/static/js/apps/base/components/header_bar/menu_desktop.tsx b/static/js/apps/base/components/header_bar/menu_desktop.tsx index e01c6c253c..55d317fe98 100644 --- a/static/js/apps/base/components/header_bar/menu_desktop.tsx +++ b/static/js/apps/base/components/header_bar/menu_desktop.tsx @@ -14,30 +14,26 @@ * limitations under the License. */ -/** The content of the desktop version of the header */ +/* The content of the desktop version of the header */ import React, { ReactElement, useEffect, useRef, useState } from "react"; -import { - GA_EVENT_HEADER_CLICK, - GA_PARAM_ID, - GA_PARAM_URL, - triggerGAEvent, -} from "../../../../shared/ga_events"; -import useEscapeKeyInputHandler from "../../../../shared/hooks/escape_key_handler"; -import { HeaderMenu, Labels, Routes } from "../../../../shared/types/base"; +import { HeaderMenuV2, Labels, Routes } from "../../../../shared/types/base"; import { resolveHref, slugify } from "../../utilities/utilities"; import MenuDesktopRichMenu from "./menu_desktop_rich_menu"; interface MenuDesktopProps { //the data that will populate the header menu. - menu: HeaderMenu[]; + menu: HeaderMenuV2[]; //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } +//TODO: verify the desired length of the timer +const MENU_CLOSE_TIMER = 250; + const MenuDesktop = ({ menu, labels, @@ -46,7 +42,7 @@ const MenuDesktop = ({ const [openMenu, setOpenMenu] = useState(null); const [panelHeight, setPanelHeight] = useState(0); const submenuRefs = useRef>([]); - const menuContainerRef = useRef(null); + const closeMenuTimer = useRef(null); const resetMenu = (): void => { setOpenMenu(null); @@ -57,15 +53,27 @@ const MenuDesktop = ({ openMenu === index ? resetMenu() : setOpenMenu(index); }; - useEscapeKeyInputHandler(() => { - resetMenu(); - }); - const itemMenuTouch = (e: React.TouchEvent, index: number): void => { e.preventDefault(); toggleMenu(index); }; + const handleOpenMenu = (index: number): void => { + setOpenMenu(index); + }; + + const handleMouseLeave = (): void => { + closeMenuTimer.current = setTimeout(() => { + resetMenu(); + }, MENU_CLOSE_TIMER); + }; + + const handleMouseEnter = (): void => { + if (closeMenuTimer.current) { + clearTimeout(closeMenuTimer.current); + } + }; + useEffect(() => { if (openMenu !== null && submenuRefs.current[openMenu]) { setPanelHeight(submenuRefs.current[openMenu]?.scrollHeight || 0); @@ -73,67 +81,51 @@ const MenuDesktop = ({ }, [openMenu]); useEffect(() => { - const handleClickOutside = (event: MouseEvent): void => { - if ( - menuContainerRef.current && - !menuContainerRef.current.contains(event.target as Node) && - !submenuRefs.current.some((ref) => ref?.contains(event.target as Node)) - ) { + const handleScroll = (): void => { + if (openMenu !== null) { resetMenu(); } }; - - document.addEventListener("mousedown", handleClickOutside); + window.addEventListener("scroll", handleScroll); return () => { - document.removeEventListener("mousedown", handleClickOutside); + window.removeEventListener("scroll", handleScroll); }; }, [openMenu]); return ( -
+
    {menu.map((menuItem, index) => ( -
  • +
  • !menuItem.url && handleOpenMenu(index)} + onClick={(): void => !menuItem.url && toggleMenu(index)} + onTouchEnd={(e): void => { + if (!menuItem.url) itemMenuTouch(e, index); + }} + > {menuItem.url ? ( { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: `desktop main ${menuItem.id}`, - [GA_PARAM_URL]: menuItem.url, - }); - return true; - }} > {labels[menuItem.label]} ) : ( <> - + keyboard_arrow_down +
    { submenuRefs.current[index] = el; @@ -146,7 +138,6 @@ const MenuDesktop = ({ > diff --git a/static/js/apps/base/components/header_bar/menu_desktop_rich_menu.tsx b/static/js/apps/base/components/header_bar/menu_desktop_rich_menu.tsx index c61d3d585d..221e15b08d 100644 --- a/static/js/apps/base/components/header_bar/menu_desktop_rich_menu.tsx +++ b/static/js/apps/base/components/header_bar/menu_desktop_rich_menu.tsx @@ -14,21 +14,18 @@ * limitations under the License. */ -/** A component to render the rich menu drop-down for the desktop menu */ +/* A component to render the rich menu drop-down for the desktop menu */ //TODO: Look into folding this into the same component as the mobile version, pending no changes to the structure. import React, { ReactElement } from "react"; -import { HeaderMenu, Labels, Routes } from "../../../../shared/types/base"; -import MenuRichLinkGroup from "./menu_rich_link_group"; +import { HeaderMenuV2, Routes } from "../../../../shared/types/base"; import MenuRichSectionGroup from "./menu_rich_section_group"; interface MenuDesktopRichMenuProps { //the top level header item that will render in the open rich menu container - menuItem: HeaderMenu; - //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. - labels: Labels; + menuItem: HeaderMenuV2; //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; //a flag to indicate whether the menu is open. @@ -37,24 +34,14 @@ interface MenuDesktopRichMenuProps { const MenuDesktopRichMenu = ({ menuItem, - labels, routes, open, }: MenuDesktopRichMenuProps): ReactElement => { return (
    -

    {labels[menuItem.introduction?.label ?? menuItem.label]}

    - {menuItem.introduction?.description && ( -

    {menuItem.introduction.description}

    - )} - {menuItem.introduction.links?.length > 0 && ( - - )} +

    {menuItem.label}

    + {menuItem.introduction &&

    {menuItem.introduction}

    }
    {menuItem.primarySectionGroups?.length > 0 && (
    @@ -63,8 +50,6 @@ const MenuDesktopRichMenu = ({ key={index} menuGroup={primarySectionGroup} routes={routes} - type="desktop" - open={open} /> ))}
    @@ -77,8 +62,6 @@ const MenuDesktopRichMenu = ({ key={index} menuGroup={secondarySectionGroup} routes={routes} - type="desktop" - open={open} /> ) )} diff --git a/static/js/apps/base/components/header_bar/menu_desktop_section_group.tsx b/static/js/apps/base/components/header_bar/menu_desktop_section_group.tsx index a4030871a2..007bd82fbc 100644 --- a/static/js/apps/base/components/header_bar/menu_desktop_section_group.tsx +++ b/static/js/apps/base/components/header_bar/menu_desktop_section_group.tsx @@ -18,10 +18,6 @@ import React, { ReactElement } from "react"; -import { - GA_EVENT_HEADER_CLICK, - triggerGAEvent, -} from "../../../../shared/ga_events"; import { HeaderMenuGroup, Routes } from "../../../../shared/types/base"; import { resolveHref } from "../../utilities/utilities"; @@ -43,23 +39,11 @@ const MenuDesktopSectionGroup = ({
    {item.title && item.url ? (
    - { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - GA_PARAM_ID: `desktop ${menuGroup.id} ${index}`, - GA_PARAM_URL: item.url, - }); - return true; - }} - > + {item.linkType === "external" && ( arrow_outward )} - {item.linkType === "rss" && ( - rss_feed - )} + {item.linkType === "rss" && rss_feed} {item.title}
    @@ -71,53 +55,29 @@ const MenuDesktopSectionGroup = ({ {item.links?.length > 0 && (
    - {item.links.map((link, linksIndex) => { - const url = resolveHref(link.url, routes); - return ( - + ))}
    )}
    diff --git a/static/js/apps/base/components/header_bar/menu_mobile.tsx b/static/js/apps/base/components/header_bar/menu_mobile.tsx index ff234e9c27..1b2d5dbcc1 100644 --- a/static/js/apps/base/components/header_bar/menu_mobile.tsx +++ b/static/js/apps/base/components/header_bar/menu_mobile.tsx @@ -24,19 +24,13 @@ import React, { useState, } from "react"; -import { - GA_EVENT_HEADER_CLICK, - GA_PARAM_ID, - GA_PARAM_URL, - triggerGAEvent, -} from "../../../../shared/ga_events"; -import { HeaderMenu, Labels, Routes } from "../../../../shared/types/base"; +import { HeaderMenuV2, Labels, Routes } from "../../../../shared/types/base"; import { resolveHref } from "../../utilities/utilities"; import MenuMobileRichMenu from "./menu_mobile_rich_menu"; interface MenuMobileProps { //the data that will populate the header menu. - menu: HeaderMenu[]; + menu: HeaderMenuV2[]; //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; //the routes dictionary - this is used to convert routes to resolved urls @@ -60,13 +54,7 @@ const MenuMobile = ({ setSelectedPrimaryItemIndex(null); }; - const handlePrimaryItemClick = ( - primaryItemIndex: number, - id: string - ): void => { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: `mobile ${id}`, - }); + const handlePrimaryItemClick = (primaryItemIndex: number): void => { setSelectedPrimaryItemIndex(primaryItemIndex); }; @@ -104,8 +92,6 @@ const MenuMobile = ({ [menu] ); - const tabIndex = open ? 0 : -1; - return (
    @@ -114,13 +100,6 @@ const MenuMobile = ({ key={menuItem.label} className="menu-main-link" href={resolveHref(menuItem.url, routes)} - onClick={() => { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: `mobile main ${menuItem.id}`, - [GA_PARAM_URL]: menuItem.url, - }); - return true; - }} > {labels[menuItem.label]} @@ -139,19 +118,11 @@ const MenuMobile = ({ >
    - {selectedPrimaryItemIndex !== null && ( - )} @@ -170,24 +141,14 @@ const MenuMobile = ({ { - triggerGAEvent(GA_EVENT_HEADER_CLICK, { - [GA_PARAM_ID]: `mobile submenu ${item.id}`, - }); - return true; - }} - tabIndex={tabIndex} > {labels[item.label]} ) : ( <>