Skip to content

Commit

Permalink
Refactor and clean up interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruben Carvalho committed Aug 8, 2023
1 parent 827c961 commit 994518d
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 155 deletions.
20 changes: 12 additions & 8 deletions pages/anchor-navigation/basic.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import AnchorNavigation from '~components/anchor-navigation';
import { TextSample } from './utils';
import SpaceBetween from '~components/space-between';
import Header from '~components/header';
import ScreenshotArea from '../utils/screenshot-area';
import styles from './styles.scss';

Expand Down Expand Up @@ -40,17 +41,20 @@ export default function SimpleAnchorNavigation() {
<TextContent />
<div>
<div className={styles['anchor-navigation']}>
<Header id="anchor-nav-heading" variant="h3">

Check warning on line 44 in pages/anchor-navigation/basic.page.tsx

View workflow job for this annotation

GitHub Actions / build / build

Prop "id" is forbidden on Components
On this page
</Header>
<AnchorNavigation
ariaLabelledby="anchor-nav-heading"
anchors={[
{ id: 'section-1', text: 'Section 1', level: 1 },
{ id: 'section-1-1', text: 'Section 1.1', level: 2 },
{ id: 'section-1-1-1', text: 'Section 1.1.1', level: 3 },
{ id: 'section-1-1-2', text: 'Section 1.1.2', level: 3 },
{ id: 'section-1-2', text: 'Section 1.2', level: 2 },
{ id: 'section-1-2-1', text: 'Section 1.2.1', level: 3 },
{ id: 'section-1-2-1-1', text: 'Section 1.2.1.1', level: 4 },
{ href: '#section-1', text: 'Section 1', level: 1 },
{ href: '#section-1-1', text: 'Section 1.1', level: 2 },
{ href: '#section-1-1-1', text: 'Section 1.1.1', level: 3 },
{ href: '#section-1-1-2', text: 'Section 1.1.2', level: 3 },
{ href: '#section-1-2', text: 'Section 1.2', level: 2 },
{ href: '#section-1-2-1', text: 'Section 1.2.1', level: 3 },
{ href: '#section-1-2-1-1', text: 'Section 1.2.1.1', level: 4 },
]}
title="On this page"
/>
</div>
</div>
Expand Down
28 changes: 15 additions & 13 deletions pages/anchor-navigation/expandable.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TextSample } from './utils';
import SpaceBetween from '~components/space-between';
import ScreenshotArea from '../utils/screenshot-area';
import styles from './styles.scss';
import { ExpandableSection } from '~components';

const TextContent = () => {
return (
Expand Down Expand Up @@ -40,19 +41,20 @@ export default function SimpleToc() {
<TextContent />
<div>
<div className={styles['anchor-navigation']}>
<AnchorNavigation
variant="expandable"
anchors={[
{ id: 'section-1', text: 'Section 1', level: 1 },
{ id: 'section-1-1', text: 'Section 1.1', level: 2 },
{ id: 'section-1-1-1', text: 'Section 1.1.1', level: 3 },
{ id: 'section-1-1-2', text: 'Section 1.1.2', level: 3 },
{ id: 'section-1-2', text: 'Section 1.2', level: 2 },
{ id: 'section-1-2-1', text: 'Section 1.2.1', level: 3 },
{ id: 'section-1-2-1-1', text: 'Section 1.2.1.1', level: 4 },
]}
title="On this page"
/>
<ExpandableSection variant="navigation" headerText="On this page">
<AnchorNavigation
ariaLabelledby="anchor-nav-heading"
anchors={[
{ href: '#section-1', text: 'Section 1', level: 1 },
{ href: '#section-1-1', text: 'Section 1.1', level: 2 },
{ href: '#section-1-1-1', text: 'Section 1.1.1', level: 3 },
{ href: '#section-1-1-2', text: 'Section 1.1.2', level: 3 },
{ href: '#section-1-2', text: 'Section 1.2', level: 2 },
{ href: '#section-1-2-1', text: 'Section 1.2.1', level: 3 },
{ href: '#section-1-2-1-1', text: 'Section 1.2.1.1', level: 4 },
]}
/>
</ExpandableSection>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/anchor-navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import InternalAnchorNavigation from './internal';

export { AnchorNavigationProps };

export default function AnchorNavigation({ variant = 'default', ...props }: AnchorNavigationProps) {
return <InternalAnchorNavigation variant={variant} {...props} />;
export default function AnchorNavigation({ ...props }: AnchorNavigationProps) {
return <InternalAnchorNavigation {...props} />;
}
applyDisplayName(AnchorNavigation, 'Anchor Navigation');
34 changes: 13 additions & 21 deletions src/anchor-navigation/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { CancelableEventHandler } from '../internal/events';

export interface AnchorNavigationProps {
/**
* The title of the table of contents, displayed above the anchor items.
* Adds `aria-labelledby` to the component. If you're using this component within a form field,
* don't set this property because the form field component automatically sets it.
*
* Use this property for identifying the header or title that labels the anchor navigation.
*
* To use it correctly, define an ID for the element you want to use as label and set the property to that ID.
*/
title?: string;
ariaLabelledby?: string;

/**
* List of anchors.
Expand All @@ -16,36 +21,22 @@ export interface AnchorNavigationProps {

/**
* Specifies the active anchor.
* For using the component in a controlled pattern, use together with 'disableTracking'.
* For using the component in a controlled manner, use together with 'disableTracking'.
*/
activeAnchor?: AnchorNavigationProps.Anchor;

/**
* The variant of the component:
* 'default' - Use in any context.
* 'expandable' - Allows users to expand or collapse the component to save vertical space.
*/
variant?: 'default' | 'expandable';

/**
* Determines whether the component initially displays in expanded state.
* The component operates in an uncontrolled manner even if you provide a value for this property.
* Only applies when the `variant` is set to `expandable`.
*/
defaultExpanded?: boolean;

/**
* Option to disable scroll spy.
*/
disableTracking?: boolean;

/**
* Triggered when an anchor link is clicked without any modifier keys.
* Fired when an anchor link is clicked without any modifier keys.
*/
onFollow?: CancelableEventHandler<AnchorNavigationProps.Anchor>;

/**
* Triggered when an active anchor link changes.
* Fired when an active anchor link changes.
*/
onActiveAnchorChange?: CancelableEventHandler<AnchorNavigationProps.Anchor>;
}
Expand All @@ -58,9 +49,10 @@ export namespace AnchorNavigationProps {
text: string;

/**
* The `id` attribute used to specify a unique HTML element.
* The `id` attribute of the target HTML element to which this anchor refers.
* For example: `"#section1.1"`
*/
id: string;
href: string;

/**
* The level of nesting.
Expand Down
57 changes: 18 additions & 39 deletions src/anchor-navigation/internal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { useMemo } from 'react';
import clsx from 'clsx';
import styles from './styles.css.js';
import InternalBox from '../box/internal.js';
import InternalSpaceBetween from '../space-between/internal.js';
import { AnchorNavigationProps } from './interfaces.js';
import { checkSafeUrl } from '../internal/utils/check-safe-url.js';
import useScrollSpy from './scroll-spy.js';
import InternalExpandableSection from '../expandable-section/internal.js';
const navigateToItem = (event: React.MouseEvent<HTMLAnchorElement>, id: string) => {
event.preventDefault();
const href = event.currentTarget.getAttribute('href');

Check warning on line 11 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L10-L11

Added lines #L10 - L11 were not covered by tests
Expand All @@ -17,56 +14,38 @@ const navigateToItem = (event: React.MouseEvent<HTMLAnchorElement>, id: string)
el?.scrollIntoView();
}
};
const Anchor = ({ id, text, level, isActive }: AnchorNavigationProps.Anchor & { isActive: boolean }) => {
checkSafeUrl('SideNavigation', id);
const Anchor = ({ href, text, level, isActive }: AnchorNavigationProps.Anchor & { isActive: boolean }) => {
checkSafeUrl('SideNavigation', href);

Check warning on line 18 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L18

Added line #L18 was not covered by tests

return (

Check warning on line 20 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L20

Added line #L20 was not covered by tests
<li className={clsx(styles['anchor-item'], { [styles['anchor-item-active']]: isActive })} key={id}>
<li className={clsx(styles['anchor-item'], { [styles['anchor-item-active']]: isActive })} key={href}>
<a
style={{
// 2px for compensate for -2 negative margin, so active item borders overlap
// 2px for compensate for -2 negative margin, so the active item borders overlaps the border
paddingLeft: `${level * 16}px`,
}}
onClick={e => navigateToItem(e, id)}
onClick={e => navigateToItem(e, href)}

Check warning on line 27 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L27

Added line #L27 was not covered by tests
className={clsx(styles['anchor-link'], { [styles['anchor-link-active']]: isActive })}
href={`#${id}`}
{...(isActive ? { 'aria-current': true } : {})}
href={href}
>
{text}
</a>
</li>
);
};

const AnchorList = ({ anchors, activeId }: { anchors: AnchorNavigationProps.Anchor[]; activeId?: string }) => {
return (
<ul className={styles['anchor-list']}>
{anchors.map((props, index) => (
<Anchor isActive={props.id === activeId} key={index} {...props} />
))}
</ul>
);
};
export default function InternalAnchorNavigation({ anchors, ariaLabelledby, ...props }: AnchorNavigationProps) {
const hrefs = useMemo(() => anchors.map(anchor => anchor.href), [anchors]);

export default function InternalAnchorNavigation({ anchors, variant, ...props }: AnchorNavigationProps) {
const [activeId] = useScrollSpy({ hrefs: anchors.map(anchor => anchor.id) });
// console.log(activeHref);
// const activeHref = 'section-1';
const [activeId] = useScrollSpy({ hrefs });
return (

Check warning on line 42 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L41-L42

Added lines #L41 - L42 were not covered by tests
<div className={styles.root}>
<InternalSpaceBetween direction="vertical" size="s">
{variant === 'expandable' ? (
<InternalExpandableSection variant="footer" headerText={props.title}>
<AnchorList activeId={activeId} anchors={anchors} />
</InternalExpandableSection>
) : (
<>
<InternalBox color="text-body-secondary" variant="h4">
{props.title}
</InternalBox>
<AnchorList activeId={activeId} anchors={anchors} />
</>
)}
</InternalSpaceBetween>
</div>
<nav aria-labelledby={ariaLabelledby} className={styles.root} {...props}>
<ol className={styles['anchor-list']}>
{anchors.map((props, index) => {
return <Anchor isActive={props.href === `#${activeId}`} key={index} {...props} />;

Check warning on line 46 in src/anchor-navigation/internal.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/internal.tsx#L45-L46

Added lines #L45 - L46 were not covered by tests
})}
</ol>
</nav>
);
}
116 changes: 44 additions & 72 deletions src/anchor-navigation/scroll-spy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,99 +5,71 @@ import { useState, useEffect, useRef } from 'react';
export default function useScrollSpy({ hrefs }: { hrefs: string[] }): [string | undefined] {
const [activeSlug, setActiveSlug] = useState<string | undefined>(undefined);
const observerRef = useRef<IntersectionObserver | null>(null);

Check warning on line 7 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L5-L7

Added lines #L5 - L7 were not covered by tests
const isLastItem = useRef<boolean>(false);

// Only run on mount and unmount
useEffect(() => {

Check warning on line 9 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L9

Added line #L9 was not covered by tests
if (!observerRef.current) {
observerRef.current = new IntersectionObserver(
entries => {
console.log({ isLastItem });
if (observerRef.current) {
observerRef.current.disconnect();

Check warning on line 11 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L11

Added line #L11 was not covered by tests
}

observerRef.current = new IntersectionObserver(
entries => {

Check warning on line 15 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L14-L15

Added lines #L14 - L15 were not covered by tests
let activeSlugTemp;
let smallestIndexInViewport = Infinity;
let largestIndexAboveViewport = -1;

Check warning on line 18 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L17-L18

Added lines #L17 - L18 were not covered by tests

let activeSlugTemp = '';
if (isLastItem.current) {
activeSlugTemp = hrefs[hrefs.length - 1];
} else {
let smallestIndexInViewport = Infinity;
let largestIndexAboveViewport = -1;
for (const entry of entries) {
if (entry?.rootBounds) {
const slug = entry.target.id;
const index = hrefs.indexOf(slug);
const aboveHalfViewport =
entry.boundingClientRect.y + entry.boundingClientRect.height <=
entry.rootBounds.y + entry.rootBounds.height;
const insideHalfViewport = entry.intersectionRatio > 0;
for (const entry of entries) {

Check warning on line 20 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L20

Added line #L20 was not covered by tests
if (entry?.rootBounds && entry?.boundingClientRect && typeof entry.intersectionRatio === 'number') {
const slug = entry.target.id;
const index = hrefs.indexOf(`#${slug}`);

Check warning on line 23 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L22-L23

Added lines #L22 - L23 were not covered by tests
const aboveHalfViewport =
entry.boundingClientRect.y + entry.boundingClientRect.height <=

Check warning on line 25 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L25

Added line #L25 was not covered by tests
entry.rootBounds.y + entry.rootBounds.height;
const insideHalfViewport = entry.intersectionRatio > 0;

Check warning on line 27 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L27

Added line #L27 was not covered by tests

// Check if the element is within the viewport and update the active slug if it's the smallest index seen
if (insideHalfViewport && index < smallestIndexInViewport) {
smallestIndexInViewport = index;
activeSlugTemp = slug;

Check warning on line 32 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L31-L32

Added lines #L31 - L32 were not covered by tests
}

if (insideHalfViewport && index < smallestIndexInViewport) {
smallestIndexInViewport = index;
activeSlugTemp = slug;
}
if (smallestIndexInViewport === Infinity && aboveHalfViewport && index > largestIndexAboveViewport) {
largestIndexAboveViewport = index;
activeSlugTemp = slug;
}
}
// If no entry is found within the viewport, find the entry with the largest index above the viewport
if (smallestIndexInViewport === Infinity && aboveHalfViewport && index > largestIndexAboveViewport) {
largestIndexAboveViewport = index;
activeSlugTemp = slug;

Check warning on line 38 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L37-L38

Added lines #L37 - L38 were not covered by tests
}
}
}
if (activeSlugTemp !== undefined) {
setActiveSlug(activeSlugTemp);

Check warning on line 43 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L43

Added line #L43 was not covered by tests
},
{
rootMargin: '0px 0px -50%',
threshold: [0, 1],
}
);
}

// const checkScrollBottom = () => {
// const scrollPos = window.innerHeight + window.scrollY;
// if (scrollPos >= document.documentElement.scrollHeight) {
// isLastItem.current = true;
// setActiveSlug(hrefs[hrefs.length - 1]);
// } else if (activeSlug === hrefs[hrefs.length - 1]) {
// const lastElement = document.getElementById(hrefs[hrefs.length - 1]);
// if (lastElement) {
// const rect = lastElement.getBoundingClientRect();
// if (
// rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
// activeSlug === hrefs[hrefs.length - 1]
// ) {
// setActiveSlug(hrefs[hrefs.length - 2]);
// isLastItem.current = false;
// }
// }
// } else {
// setActiveSlug(hrefs[hrefs.length - 2]);
// isLastItem.current = false;
// }
// };

// window.addEventListener('scroll', checkScrollBottom);

return () => {
observerRef.current?.disconnect();
observerRef.current = null;
// window.removeEventListener('scroll', checkScrollBottom);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
},
{
rootMargin: '0px 0px -50%',
threshold: [0, 1],
}
);

// Run whenever hrefs changes
useEffect(() => {
hrefs.forEach(id => {
hrefs.forEach(href => {
const id = href.slice(1);
const element = document.getElementById(id);

Check warning on line 54 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L52-L54

Added lines #L52 - L54 were not covered by tests
if (element) {
observerRef.current?.observe(element);
}
});

return () => {
hrefs.forEach(id => {
hrefs.forEach(href => {
const id = href.slice(1);
const element = document.getElementById(id);

Check warning on line 63 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L60-L63

Added lines #L60 - L63 were not covered by tests
if (element) {
observerRef.current?.unobserve(element);
}
});

if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;

Check warning on line 71 in src/anchor-navigation/scroll-spy.tsx

View check run for this annotation

Codecov / codecov/patch

src/anchor-navigation/scroll-spy.tsx#L70-L71

Added lines #L70 - L71 were not covered by tests
}
};
}, [hrefs]);

Expand Down

0 comments on commit 994518d

Please sign in to comment.