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

feat(landingpage): add navigation to subheaders in guides #1159

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
18 changes: 16 additions & 2 deletions packages/landingpage/components/getting-started/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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';
import { scrollToElement } from 'utils/scrollToElement';

interface Props {
children: React.ReactNode;
Expand All @@ -21,17 +23,29 @@ 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);
scrollToElement(hash);
}
};

window.addEventListener('load', handlePageLoad);
return () => window.removeEventListener('load', handlePageLoad);
}, [router.asPath]);

return (
<Page title={['Getting Started', frameworkName]}>
<div className={styles.segmentGroup}>
<InoSegmentGroup
id="segment-grp"
value={framework}
onValueChange={(value) =>
push(
router.push(
`/${Supported_Locales.EN}${MainRoutes.GETTING_STARTED}/${value.detail}`,
)
}
Expand Down
35 changes: 32 additions & 3 deletions packages/landingpage/components/getting-started/menuSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ReactNode } from 'react';
import 'utils/stringExtensions';
import { InoTooltip } from '@elements';
import { scrollToElement } from 'utils/scrollToElement';

type MenuSectionProps = {
title: string;
children: ReactNode;
level?: 'main' | 'sub'; // 'main' for main sections, 'sub' for subsections (subsections will not be listed in the navigationMenu.tsx)
};

/**
Expand All @@ -12,10 +15,36 @@ 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) {
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<HTMLHeadingElement>,
) => {
event.preventDefault();
scrollToElement(id);
};

const HeadingTag = isMainSection ? 'h2' : 'h3';

return (
<section data-menu-section id={title.toCamelCase()}>
<h2>{title}</h2>
<section data-menu-section={isMainSection ? 'true' : undefined} id={id}>
<HeadingTag
id={`popover-heading-${id}`}
onClick={handleMenuSectionClick}
style={{ cursor: 'pointer' }}
>
{title}
</HeadingTag>
<InoTooltip
for={`popover-heading-${id}`}
placement="left"
trigger="mouseenter focus"
>
<b>#</b>
</InoTooltip>
{children}
</section>
);
Expand Down
66 changes: 25 additions & 41 deletions packages/landingpage/components/getting-started/navigationMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import styles from './navigationMenu.module.scss';
import { scrollToElement } from 'utils/scrollToElement';

// We use Record<string, string> instead of enum because we can iterate over it AND use it as a type
export type Sections = Record<string, string>;
Expand Down Expand Up @@ -27,46 +28,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<HTMLAnchorElement>,
section: string,
sectionId: string,
) {
event.preventDefault();
const targetElement = document.querySelector(`#${section}`);
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}#${section}`;
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
Expand All @@ -77,20 +62,19 @@ export default function NavigationMenu({ title }: NavigationMenuProps) {
<nav className={styles.navigationMenu}>
<h5>{title}</h5>
<ul className={styles.sections}>
{sections &&
Object.entries(sections).map(([key, value]) => (
<li
key={key}
className={activeSection === value ? styles.active : ''}
{Object.entries(sections).map(([key, sectionId]) => (
<li
key={key}
className={activeSection === sectionId ? styles.active : ''}
>
<a
href={`#${sectionId}`}
onClick={(event) => handleAnchorClick(event, sectionId)}
>
<a
href={`#${value}`}
onClick={(event) => handleAnchorClick(event, value)}
>
{key}
</a>
</li>
))}
{key}
</a>
</li>
))}
</ul>
</nav>
</aside>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function HeaderDesktop() {

return (
<header className={styles.header}>
<div className={styles.headerInner}>
<div id="desktopHeaderInner" className={styles.headerInner}>
<div className={styles.logo}>
<Link
href={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</MenuSection>

### Install via package manager
<MenuSection title='Install via package manager'>

<StepIndicator step='1'>Add the package `@inovex.de/elements` to your project using **npm** or **yarn**:</StepIndicator>

Expand Down
15 changes: 8 additions & 7 deletions packages/landingpage/mdx/getting-started/react-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import MenuSection from "components/getting-started/menuSection";
```
</CH.Section>

### Playground
<MenuSection title='Playground' level='sub'>

Every component has a variety of powerful and unique properties. They are listed and explained in each component **Playground**:

Expand All @@ -130,7 +130,7 @@ import MenuSection from "components/getting-started/menuSection";
<Hint>Hint: These are some of the properties of the `<InoInput>` component.</Hint>

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.

</MenuSection>
<StepIndicator step='5'>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 `<InoInput>`.</StepIndicator>

```tsx
Expand All @@ -155,7 +155,7 @@ import MenuSection from "components/getting-started/menuSection";
</InoInput>
```

## Events
<MenuSection title='Events' level='sub'>

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 `<InoInput>` the action is the user typing and our events reaction contains the typed input.
Expand Down Expand Up @@ -191,8 +191,9 @@ import MenuSection from "components/getting-started/menuSection";
```

<Hint>Hint: while in our Playground the Event is called `valueChange`, we name it `onValueChange` inside of our `<InoInput>`. The reason for this is that we want to accomodate our naming similar to the react-way of naming events (e.g. onClick)</Hint>
</MenuSection>

## Slots
<MenuSection title='Slots' level='sub'>

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.

Expand Down Expand Up @@ -220,8 +221,8 @@ import MenuSection from "components/getting-started/menuSection";
```

Note that we indicated the position of the slot inside our `<InoIcon>` with `slot="icon-trailing"`.

## CSS-Variables
</MenuSection>
<MenuSection title='CSS-Variables' level='sub'>

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 `<ino-input>`, 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:
Expand All @@ -232,7 +233,7 @@ import MenuSection from "components/getting-started/menuSection";
--ino-input-label-color: blue;
}
```

</MenuSection>
</MenuSection>
<MenuSection title="Finishing Touches">

Expand Down
20 changes: 20 additions & 0 deletions packages/landingpage/utils/scrollToElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const scrollToElement = (elementId: string, offset = 80) => {
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);
};
Loading