From c8fd9bef601b136fe8e8a3638c83ac60ac53ac4c Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Tue, 9 Jan 2024 12:33:36 +0100 Subject: [PATCH 1/7] feat: add clickable menu sections with smooth scrolling --- .../getting-started/menuSection.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/landingpage/components/getting-started/menuSection.tsx b/packages/landingpage/components/getting-started/menuSection.tsx index c44d98c016..a3d89a6863 100644 --- a/packages/landingpage/components/getting-started/menuSection.tsx +++ b/packages/landingpage/components/getting-started/menuSection.tsx @@ -4,6 +4,7 @@ import 'utils/stringExtensions'; type MenuSectionProps = { title: string; children: ReactNode; + includeInNavigationMenu?: boolean; }; /** @@ -13,9 +14,33 @@ type MenuSectionProps = { * @param children The content to render within the section. */ function MenuSection({ title, children }: MenuSectionProps) { + const id = title.toCamelCase(); + + const handleMenuSectionClick = (event: React.MouseEvent) => { + event.preventDefault(); + const targetElement = document.getElementById(id); + if (!targetElement) return; + + const headerOffset = 80; + const elementPosition = targetElement.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - headerOffset; + + // Change the URL (because we're preventing the default anchor click behavior) + const newUrl = `${window.location.origin}${window.location.pathname}#${id}`; + window.history.pushState(null, '', newUrl); + + // Using window.scrollTo() instead of element.scrollIntoView() because the latter doesn't support offsets + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + }; + return ( -
-

{title}

+
+

+ {title} +

{children}
); From c5e6d176946ef8ddf352ae95cd33bb9e4e0f3499 Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Tue, 9 Jan 2024 12:34:51 +0100 Subject: [PATCH 2/7] chore: format fix --- .../landingpage/components/getting-started/menuSection.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/landingpage/components/getting-started/menuSection.tsx b/packages/landingpage/components/getting-started/menuSection.tsx index a3d89a6863..bb5030d532 100644 --- a/packages/landingpage/components/getting-started/menuSection.tsx +++ b/packages/landingpage/components/getting-started/menuSection.tsx @@ -16,7 +16,9 @@ type MenuSectionProps = { function MenuSection({ title, children }: MenuSectionProps) { const id = title.toCamelCase(); - const handleMenuSectionClick = (event: React.MouseEvent) => { + const handleMenuSectionClick = ( + event: React.MouseEvent, + ) => { event.preventDefault(); const targetElement = document.getElementById(id); if (!targetElement) return; From d398f5c96f57c9f057987157194a6e78ee6570aa Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Fri, 12 Jan 2024 13:00:29 +0100 Subject: [PATCH 3/7] feat: add automatic scrolling to subheader urls on load --- .../components/getting-started/layout.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/landingpage/components/getting-started/layout.tsx b/packages/landingpage/components/getting-started/layout.tsx index 6fb99a2d16..8f52b09b4c 100644 --- a/packages/landingpage/components/getting-started/layout.tsx +++ b/packages/landingpage/components/getting-started/layout.tsx @@ -7,6 +7,7 @@ import { Framework, NameByFramework } from 'utils/frameworks'; import { MainRoutes } from 'utils/routes'; import styles from './layout.module.scss'; import NavigationMenu from './navigationMenu'; +import { useEffect, useState } from 'react'; interface Props { children: React.ReactNode; @@ -21,9 +22,34 @@ const SANDBOX_MAP: { [key in Framework]?: string } = { }; const Layout = ({ children, framework, sandboxUrl }: Props) => { - const { push } = useRouter(); + const router = useRouter(); const frameworkName = NameByFramework[framework]; + useEffect(() => { + const handlePageLoad = () => { + if (router.asPath.includes('#')) { + const hash = window.location.hash.substring(1); + const targetElement = document.getElementById(hash); + if (targetElement) { + const headerOffset = 80; + const elementPosition = targetElement.getBoundingClientRect().top; + const offsetPosition = + elementPosition + window.pageYOffset - headerOffset; + + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + } + } + }; + + // Scroll to the correct section on page load + window.addEventListener('load', handlePageLoad); + + return () => window.removeEventListener('load', handlePageLoad); + }, [router.asPath]); + return (
@@ -31,7 +57,7 @@ const Layout = ({ children, framework, sandboxUrl }: Props) => { id="segment-grp" value={framework} onValueChange={(value) => - push( + router.push( `/${Supported_Locales.EN}${MainRoutes.GETTING_STARTED}/${value.detail}`, ) } From 832e73d287e06f30b8e3c58406908c49f18a1f19 Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Fri, 12 Jan 2024 13:01:42 +0100 Subject: [PATCH 4/7] refactor: restructure menuSection to include subheaders from mdx guides --- .../getting-started/menuSection.tsx | 28 +++++++++++++++---- .../header/desktop/header.desktop.module.scss | 2 +- .../mdx/getting-started/javascript-guide.mdx | 3 +- .../mdx/getting-started/react-guide.mdx | 15 +++++----- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/landingpage/components/getting-started/menuSection.tsx b/packages/landingpage/components/getting-started/menuSection.tsx index bb5030d532..5f0d0cb903 100644 --- a/packages/landingpage/components/getting-started/menuSection.tsx +++ b/packages/landingpage/components/getting-started/menuSection.tsx @@ -1,10 +1,11 @@ import { ReactNode } from 'react'; import 'utils/stringExtensions'; +import { InoTooltip } from '@elements'; type MenuSectionProps = { title: string; children: ReactNode; - includeInNavigationMenu?: boolean; + level?: 'main' | 'sub'; // 'main' for main sections, 'sub' for subsections (subsections will not be listed in the navigationMenu.tsx) }; /** @@ -13,8 +14,10 @@ type MenuSectionProps = { * @param title The title of the section (that also functions as the id). * @param children The content to render within the section. */ -function MenuSection({ title, children }: MenuSectionProps) { - const id = title.toCamelCase(); +function MenuSection({ title, children, level = 'main' }: MenuSectionProps) { + // Sanitize the ID by replacing all non-word characters (this prevents querySelector errors when the id could contain special characters like "Install @inovex.de/elements" in the vue-guide.mdx) + const id = title.toCamelCase().replace(/[^\w-]/g, '-'); + const isMainSection = level === 'main'; const handleMenuSectionClick = ( event: React.MouseEvent, @@ -38,11 +41,24 @@ function MenuSection({ title, children }: MenuSectionProps) { }); }; + const HeadingTag = isMainSection ? 'h2' : 'h3'; + return ( -
-

+
+ {title} -

+ + + # + {children}
); diff --git a/packages/landingpage/components/layout/header/desktop/header.desktop.module.scss b/packages/landingpage/components/layout/header/desktop/header.desktop.module.scss index c0db6e5530..3538e0a2dc 100644 --- a/packages/landingpage/components/layout/header/desktop/header.desktop.module.scss +++ b/packages/landingpage/components/layout/header/desktop/header.desktop.module.scss @@ -6,7 +6,7 @@ $header-height--shrunk: 80px; top: $header-height--shrunk - $header-height; position: sticky; background-color: var(--inovex-elements-white); - z-index: 999; + z-index: 99999; height: $header-height; display: flex; align-items: center; diff --git a/packages/landingpage/mdx/getting-started/javascript-guide.mdx b/packages/landingpage/mdx/getting-started/javascript-guide.mdx index 68b6d009c6..7afd04175a 100644 --- a/packages/landingpage/mdx/getting-started/javascript-guide.mdx +++ b/packages/landingpage/mdx/getting-started/javascript-guide.mdx @@ -22,8 +22,9 @@ Use the npm package manager if... - ... you already started building a single page application. - ... you plan to migrate a static website to a single page application. - ... you plan to start a new project. + -### Install via package manager + Add the package `@inovex.de/elements` to your project using **npm** or **yarn**: diff --git a/packages/landingpage/mdx/getting-started/react-guide.mdx b/packages/landingpage/mdx/getting-started/react-guide.mdx index 4f2a385194..f576a60fe5 100644 --- a/packages/landingpage/mdx/getting-started/react-guide.mdx +++ b/packages/landingpage/mdx/getting-started/react-guide.mdx @@ -109,7 +109,7 @@ import MenuSection from "components/getting-started/menuSection"; ``` - ### Playground + Every component has a variety of powerful and unique properties. They are listed and explained in each component **Playground**: @@ -130,7 +130,7 @@ import MenuSection from "components/getting-started/menuSection"; Hint: These are some of the properties of the `` component. The Playground allows you to modify these properties and see the result in real time. Additionally, we provide examples about the usage of each component with its given properties. - + Following up on our login-form, we add a type and value property, setting it to a state object named _email_. Do the same for the password ``. ```tsx @@ -155,7 +155,7 @@ import MenuSection from "components/getting-started/menuSection"; ``` - ## Events + With the exact same approach, we use **Events**. They emit a certain value when the user does something that triggers our Event to fire. In other terms: Our event gives us a **reaction** to an **action** made by our User. In case of our `` the action is the user typing and our events reaction contains the typed input. @@ -191,8 +191,9 @@ import MenuSection from "components/getting-started/menuSection"; ``` Hint: while in our Playground the Event is called `valueChange`, we name it `onValueChange` inside of our ``. The reason for this is that we want to accomodate our naming similar to the react-way of naming events (e.g. onClick) + - ## Slots + Many components of the Elements Library have **slots**, that can be filled by other components or your own content to further customize its functionality or appearance. @@ -220,8 +221,8 @@ import MenuSection from "components/getting-started/menuSection"; ``` Note that we indicated the position of the slot inside our `` with `slot="icon-trailing"`. - - ## CSS-Variables + + In order to provide a CSS way of styling the inovex-elements, we sometimes provide custom properties, a.k.a. CSS-Variables. In the case of the ``, there are two variables we can use to change the appearance of our component: `--ino-input-line-color` to change the color of the underline and the `--ino-input-label-color` which changes the color of the floating label. They can be used like this: @@ -232,7 +233,7 @@ import MenuSection from "components/getting-started/menuSection"; --ino-input-label-color: blue; } ``` - + From 84b6fd03cd7ed3e1fb8d321ae813be06e7b50593 Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Fri, 12 Jan 2024 13:10:04 +0100 Subject: [PATCH 5/7] refactor: add clearer namings and use sanitizedIds for section handling --- .../getting-started/navigationMenu.tsx | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/landingpage/components/getting-started/navigationMenu.tsx b/packages/landingpage/components/getting-started/navigationMenu.tsx index b4a0137ed3..da6e268e6a 100644 --- a/packages/landingpage/components/getting-started/navigationMenu.tsx +++ b/packages/landingpage/components/getting-started/navigationMenu.tsx @@ -27,31 +27,30 @@ export default function NavigationMenu({ title }: NavigationMenuProps) { 'section[data-menu-section]', ); - const sectionsTemp: Sections = {}; + const sectionMap: Sections = {}; - domSectionElements.forEach((section) => { - const headingElement = section.querySelector('h2'); - if (headingElement) { - const headingText = headingElement.innerHTML.trim(); - const key = headingText; - const value = headingText.toCamelCase(); - sectionsTemp[key] = value; + domSectionElements.forEach((sectionElement) => { + if (sectionElement.id) { + const sectionTitle = + sectionElement.querySelector('h2')?.textContent?.trim() || + sectionElement.id; + sectionMap[sectionTitle] = sectionElement.id; } - observer.observe(section); + observer.observe(sectionElement); }); - setActiveSection(Object.values(sectionsTemp)[0]); // set the first section as active - setSections(sectionsTemp); + setActiveSection(Object.values(sectionMap)[0]); // set the first section as active + setSections(sectionMap); return () => observer.disconnect(); }, []); function handleAnchorClick( event: React.MouseEvent, - section: string, + sectionId: string, ) { event.preventDefault(); - const targetElement = document.querySelector(`#${section}`); + const targetElement = document.querySelector(`#${sectionId}`); if (!targetElement) return; const headerOffset = 80; @@ -59,7 +58,7 @@ export default function NavigationMenu({ title }: NavigationMenuProps) { const offsetPosition = elementPosition + window.pageYOffset - headerOffset; // Change the URL (because we're preventing the default anchor click behavior) - const newUrl = `${window.location.origin}${window.location.pathname}#${section}`; + const newUrl = `${window.location.origin}${window.location.pathname}#${sectionId}`; window.history.pushState(null, '', newUrl); // Using window.scrollTo() instead of element.scrollIntoView() because the latter doesn't support offsets @@ -77,20 +76,19 @@ export default function NavigationMenu({ title }: NavigationMenuProps) { From 705de5b950016090e0dc7b3c2dca8d67d99f1e65 Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Fri, 12 Jan 2024 13:10:43 +0100 Subject: [PATCH 6/7] chore: format fix --- .../landingpage/components/getting-started/navigationMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/landingpage/components/getting-started/navigationMenu.tsx b/packages/landingpage/components/getting-started/navigationMenu.tsx index da6e268e6a..5ff4c891a5 100644 --- a/packages/landingpage/components/getting-started/navigationMenu.tsx +++ b/packages/landingpage/components/getting-started/navigationMenu.tsx @@ -34,7 +34,7 @@ export default function NavigationMenu({ title }: NavigationMenuProps) { const sectionTitle = sectionElement.querySelector('h2')?.textContent?.trim() || sectionElement.id; - sectionMap[sectionTitle] = sectionElement.id; + sectionMap[sectionTitle] = sectionElement.id; } observer.observe(sectionElement); }); From 9037c2bd6c94c459f40f3cc15572e94f2ee22e01 Mon Sep 17 00:00:00 2001 From: Tobias Heim Galindo Date: Wed, 24 Jan 2024 11:17:00 +0100 Subject: [PATCH 7/7] refactor: add shared scroll function to utils --- .../components/getting-started/layout.tsx | 16 ++------------- .../getting-started/menuSection.tsx | 18 ++--------------- .../getting-started/navigationMenu.tsx | 18 ++--------------- .../layout/header/desktop/header.desktop.tsx | 2 +- packages/landingpage/utils/scrollToElement.ts | 20 +++++++++++++++++++ 5 files changed, 27 insertions(+), 47 deletions(-) create mode 100644 packages/landingpage/utils/scrollToElement.ts diff --git a/packages/landingpage/components/getting-started/layout.tsx b/packages/landingpage/components/getting-started/layout.tsx index 8f52b09b4c..7bdf18179f 100644 --- a/packages/landingpage/components/getting-started/layout.tsx +++ b/packages/landingpage/components/getting-started/layout.tsx @@ -8,6 +8,7 @@ import { MainRoutes } from 'utils/routes'; import styles from './layout.module.scss'; import NavigationMenu from './navigationMenu'; import { useEffect, useState } from 'react'; +import { scrollToElement } from 'utils/scrollToElement'; interface Props { children: React.ReactNode; @@ -29,24 +30,11 @@ const Layout = ({ children, framework, sandboxUrl }: Props) => { const handlePageLoad = () => { if (router.asPath.includes('#')) { const hash = window.location.hash.substring(1); - const targetElement = document.getElementById(hash); - if (targetElement) { - const headerOffset = 80; - const elementPosition = targetElement.getBoundingClientRect().top; - const offsetPosition = - elementPosition + window.pageYOffset - headerOffset; - - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth', - }); - } + scrollToElement(hash); } }; - // Scroll to the correct section on page load window.addEventListener('load', handlePageLoad); - return () => window.removeEventListener('load', handlePageLoad); }, [router.asPath]); diff --git a/packages/landingpage/components/getting-started/menuSection.tsx b/packages/landingpage/components/getting-started/menuSection.tsx index 5f0d0cb903..3aff8d17cf 100644 --- a/packages/landingpage/components/getting-started/menuSection.tsx +++ b/packages/landingpage/components/getting-started/menuSection.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import 'utils/stringExtensions'; import { InoTooltip } from '@elements'; +import { scrollToElement } from 'utils/scrollToElement'; type MenuSectionProps = { title: string; @@ -23,22 +24,7 @@ function MenuSection({ title, children, level = 'main' }: MenuSectionProps) { event: React.MouseEvent, ) => { event.preventDefault(); - const targetElement = document.getElementById(id); - if (!targetElement) return; - - const headerOffset = 80; - const elementPosition = targetElement.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.pageYOffset - headerOffset; - - // Change the URL (because we're preventing the default anchor click behavior) - const newUrl = `${window.location.origin}${window.location.pathname}#${id}`; - window.history.pushState(null, '', newUrl); - - // Using window.scrollTo() instead of element.scrollIntoView() because the latter doesn't support offsets - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth', - }); + scrollToElement(id); }; const HeadingTag = isMainSection ? 'h2' : 'h3'; diff --git a/packages/landingpage/components/getting-started/navigationMenu.tsx b/packages/landingpage/components/getting-started/navigationMenu.tsx index 5ff4c891a5..3f001ab75e 100644 --- a/packages/landingpage/components/getting-started/navigationMenu.tsx +++ b/packages/landingpage/components/getting-started/navigationMenu.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import styles from './navigationMenu.module.scss'; +import { scrollToElement } from 'utils/scrollToElement'; // We use Record instead of enum because we can iterate over it AND use it as a type export type Sections = Record; @@ -50,22 +51,7 @@ export default function NavigationMenu({ title }: NavigationMenuProps) { sectionId: string, ) { event.preventDefault(); - const targetElement = document.querySelector(`#${sectionId}`); - if (!targetElement) return; - - const headerOffset = 80; - const elementPosition = targetElement.getBoundingClientRect().top; - const offsetPosition = elementPosition + window.pageYOffset - headerOffset; - - // Change the URL (because we're preventing the default anchor click behavior) - const newUrl = `${window.location.origin}${window.location.pathname}#${sectionId}`; - window.history.pushState(null, '', newUrl); - - // Using window.scrollTo() instead of element.scrollIntoView() because the latter doesn't support offsets - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth', - }); + scrollToElement(sectionId); } // Return null if there are no sections diff --git a/packages/landingpage/components/layout/header/desktop/header.desktop.tsx b/packages/landingpage/components/layout/header/desktop/header.desktop.tsx index 731d58323e..bd4a834ab5 100644 --- a/packages/landingpage/components/layout/header/desktop/header.desktop.tsx +++ b/packages/landingpage/components/layout/header/desktop/header.desktop.tsx @@ -15,7 +15,7 @@ export default function HeaderDesktop() { return (
-
+
{ + const targetElement = document.getElementById(elementId); + if (!targetElement) return; + + const headerInnerElement = document.getElementById('desktopHeaderInner'); + const headerOffset = headerInnerElement + ? headerInnerElement.offsetHeight + : offset; // uses inputted offset if desktop header is not present (e.g. on mobile it defaults to 80px due to 26px high burger menu icon) + + const elementPosition = targetElement.getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - headerOffset; + + // Using window.scrollTo() instead of element.scrollIntoView() because the latter doesn't support offsets + window.scrollTo({ + top: offsetPosition, + behavior: 'smooth', + }); + const newUrl = `${window.location.origin}${window.location.pathname}#${elementId}`; + window.history.pushState(null, '', newUrl); +};