Skip to content
Closed
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
10 changes: 8 additions & 2 deletions components/blocks/spacer/spacer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function Spacer({ data }: { data: Consultingv2BlocksSpacer }) {
);

const spacer = isDecorated ? (
<V2ComponentWrapper data={data}>
<V2ComponentWrapper data={data} ariaHidden>
<div
style={{
height: data?.spacerHeight,
Expand All @@ -38,5 +38,11 @@ export function Spacer({ data }: { data: Consultingv2BlocksSpacer }) {
/>
);

return hideClasses ? <div className={hideClasses}>{spacer}</div> : spacer;
return hideClasses ? (
<div aria-hidden="true" className={hideClasses}>
{spacer}
</div>
) : (
spacer
);
}
10 changes: 3 additions & 7 deletions components/blocks/v3/heroBox/heroBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ButtonRow from "@/components/blocksSubtemplates/buttonRow";
import { backgroundOptions } from "@/components/blocksSubtemplates/tinaFormElements/colourOptions/blockBackgroundOptions";
import V2ComponentWrapper from "@/components/layout/v2ComponentWrapper";
import { Container } from "@/components/util/container";
import { findNextBlock, scrollToBlockContent } from "@/lib/scrollToBlock";
import { cn } from "@/lib/utils";
import Image from "next/image";
import { useState, type CSSProperties } from "react";
Expand Down Expand Up @@ -59,13 +60,8 @@ export const V3HeroBox = ({ data, priority = false }) => {
type="button"
aria-label="Scroll to content"
onClick={(e) => {
// Scroll so the arrow ends up 10% of the viewport height down from
// the top of the screen, pulling the hero up out of the way.
const { top } = e.currentTarget.getBoundingClientRect();
window.scrollTo({
top: window.scrollY + top - window.innerHeight * 0.1,
behavior: "smooth",
});
const nextBlock = findNextBlock(e.currentTarget);
if (nextBlock) scrollToBlockContent(nextBlock);
}}
className="pointer-events-auto flex size-11 items-center justify-center rounded-full border border-black/70 text-black transition-colors hover:bg-black hover:text-white dark:border-white/80 dark:text-white dark:hover:bg-white dark:hover:text-black"
>
Expand Down
2 changes: 1 addition & 1 deletion components/layout/v2ComponentWrapper.schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const anchorIdSchema: TinaField = {
},
},
description:
"Optional id for in-page links. Set this on a section, then point a button's link at #<id> to jump here (the offset for the sticky header is handled automatically).",
"Optional id for in-page links. Set this on a section, then point a button's link at #<id> to jump here (a little breathing room above the section is added automatically).",
};

// Background + anchor fields every V2 block shares β€” spread once via `...wrapperBaseFields`.
Expand Down
9 changes: 8 additions & 1 deletion components/layout/v2ComponentWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ const V2ComponentWrapper = ({
children,
fadeInMargin = "-100px",
className,
ariaHidden,
}: {
data: BackgroundData;
children: React.ReactNode;
fadeInMargin?: UseInViewOptions["margin"];
className?: string;
ariaHidden?: boolean;
}) => {
//Bleed effect setup
const bleed = useRef(null);
Expand Down Expand Up @@ -58,12 +60,13 @@ const V2ComponentWrapper = ({
return (
<section
id={data.anchorId || undefined}
aria-hidden={ariaHidden || undefined}
className={classNames(
backgroundOptions.find((value) => {
return value.reference === data.background?.backgroundColour;
})?.classes,
"relative w-full overflow-visible",
// Offset in-page anchor scrolling so the target clears the sticky header
// Breathing room above a section jumped to by an in-page anchor link.
data.anchorId && "scroll-mt-24",
className
)}
Expand Down Expand Up @@ -110,8 +113,12 @@ const V2ComponentWrapper = ({
className="pointer-events-none absolute inset-0 z-25 bg-dot-grid bg-dots"
/>
)}
{/* data-block-content marks where this block's content starts, past the
wrapper's padding and the decorative layers above. lib/scrollToBlock
targets it. */}
<section
ref={ref}
data-block-content=""
className={classNames(
"relative z-30 transition-opacity duration-300",
isInInitialViewport === false && "opacity-0",
Expand Down
52 changes: 52 additions & 0 deletions lib/scrollToBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Blocks are wrapped in `<div className="contents" data-tinafield>` by
// blocks-renderer.tsx; that wrapper is the block boundary here.
const BLOCK_WRAPPER = "[data-tinafield]";

export function prefersReducedMotion(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

/** The next block after `fromEl`'s own that renders a visible box. */
export function findNextBlock(fromEl: Element): Element | null {
let sibling = fromEl.closest(BLOCK_WRAPPER)?.nextElementSibling ?? null;
while (sibling) {
const root = sibling.firstElementChild;
if (
root &&
root.getClientRects().length > 0 &&
root.getAttribute("aria-hidden") !== "true"
) {
return root;
}
sibling = sibling.nextElementSibling;
}
return null;
}

function contentSection(blockRoot: Element): Element {
return blockRoot.querySelector(":scope > [data-block-content]") ?? blockRoot;
}

/** Document offset where a block's content starts, past its container padding. */
export function getBlockContentTop(blockRoot: Element): number {
const content = contentSection(blockRoot);
const container = content.firstElementChild;
const laidOut = !!container && container.getClientRects().length > 0;
const el = laidOut ? container : content;
const padding = laidOut
? parseFloat(getComputedStyle(container).paddingTop) || 0
: 0;
return window.scrollY + el.getBoundingClientRect().top + padding;
}

export function scrollToBlockContent(blockRoot: Element): void {
window.scrollTo({
top: getBlockContentTop(blockRoot),
// "instant" not "auto" β€” auto defers to the global scroll-smooth CSS.
behavior: prefersReducedMotion() ? "instant" : "smooth",
});
// Move the reading position too, so screen readers and the keyboard follow.
const target = contentSection(blockRoot) as HTMLElement;
if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
target.focus({ preventScroll: true });
}
Loading