Skip to content
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
4 changes: 2 additions & 2 deletions app/(events)/events/[...filename]/eventsv2.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useMobileHeaderAppearance } from "@/app/components/header-appearance";
import { HeaderAppearanceMarker } from "@/app/components/header-appearance";
import { Blocks } from "@/components/blocks-renderer";
import { Container } from "@/components/util/container";
import { Section } from "@/components/util/section";
Expand All @@ -17,9 +17,9 @@ type EventsV2PageProps<T> = {
const EventsV2Page = memo(
function EventsV2Page({ tinaProps }: EventsV2PageProps<object>) {
const { blocks, appearance } = tinaProps.data.eventsv2;
useMobileHeaderAppearance(appearance ?? null);
return (
<div className="dark flex h-full flex-col">
<HeaderAppearanceMarker appearance={appearance} />
<Section color={"toggleLightMode"}>
<Container
size="custom"
Expand Down
51 changes: 17 additions & 34 deletions app/components/header-appearance.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
"use client";
import { createContext, useContext, useEffect, useMemo, useState } from "react";

export type MobileHeaderAppearance = {
hideFlag?: boolean | null;
hideContactButton?: boolean | null;
};
Comment on lines 1 to 4
type Ctx = {
mobile: MobileHeaderAppearance;
setMobile: (v: MobileHeaderAppearance) => void;
};
const HeaderAppearanceContext = createContext<Ctx>({
mobile: {},
setMobile: () => {},
});

export function HeaderAppearanceProvider({
children,
// The header lives in the root layout and can't see the current page's data, so
// hiding the mobile flag/Contact button used to happen in a client effect AFTER
// hydration β€” reflowing the header and everything below it (CLS). Instead, the page
// server-renders this hidden marker; CSS in styles.css reads it via `:has()` and hides
// the elements on first paint, so SSR and hydration match. Client-side navigation swaps
// the marker in/out with the page, so the header updates without any header-side JS.
// (Middleware `x-pathname` + server resolution β€” the other documented fix β€” was rejected:
// reading headers() in the root layout breaks the `dynamic = "force-static"` routes.)
export function HeaderAppearanceMarker({
appearance,
}: {
children: React.ReactNode;
appearance?: MobileHeaderAppearance | null;
}) {
const [mobile, setMobile] = useState<MobileHeaderAppearance>({});
const value = useMemo(() => ({ mobile, setMobile }), [mobile]);
if (!appearance?.hideFlag && !appearance?.hideContactButton) return null;
return (
<HeaderAppearanceContext.Provider value={value}>
{children}
</HeaderAppearanceContext.Provider>
<div
hidden
data-mm-hide-flag={appearance.hideFlag ? "" : undefined}
data-mm-hide-contact={appearance.hideContactButton ? "" : undefined}
/>
);
}

export const useHeaderAppearance = () => useContext(HeaderAppearanceContext);

// ponytail: client context β†’ SSR shows full header for one frame before hiding flag/
// contact. Upgrade path if the flash matters: set `x-pathname` in middleware.ts and
// resolve appearance server-side in app/layout.tsx so the header renders correctly at SSR.
export function useMobileHeaderAppearance(
value: MobileHeaderAppearance | null
) {
const { setMobile } = useHeaderAppearance();
useEffect(() => {
setMobile(value ?? {});
return () => setMobile({});
}, [value, setMobile]);
}
19 changes: 8 additions & 11 deletions app/components/page-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Footer } from "@/components/layout/footer/footer";
import { HeaderAppearanceProvider } from "./header-appearance";

type PageLayoutProps = {
children: React.ReactNode;
Expand All @@ -12,16 +11,14 @@ const PageLayout = ({
megaMenu,
}: PageLayoutProps) => {
return (
<HeaderAppearanceProvider>
<div className="flex min-h-screen flex-col">
<header className="no-print z-1">
{phishingBanner}
{megaMenu}
</header>
<main className="z-0 grow bg-white">{children}</main>
<Footer />
</div>
</HeaderAppearanceProvider>
<div className="flex min-h-screen flex-col">
<header className="no-print z-1">
{phishingBanner}
{megaMenu}
</header>
<main className="z-0 grow bg-white">{children}</main>
<Footer />
</div>
);
};

Expand Down
12 changes: 6 additions & 6 deletions components/server/MegaMenuWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import classNames from "classnames";
import { usePathname } from "next/navigation";
import { MegaMenuLayout } from "ssw.megamenu";
import { useHeaderAppearance } from "@/app/components/header-appearance";
import { CustomLink } from "../customLink";

export function MegaMenuWrapper(props) {
const pathName = usePathname();
const { mobile } = useHeaderAppearance();

// Per-page hiding of the mobile flag/Contact button is handled by CSS reading a
// server-rendered marker (see header-appearance.tsx), so the header always renders
// both here and stays correct at SSR. Only the contact-us page (known at SSR from the
// pathname) hides the phone via the package itself.
return (
<MegaMenuLayout
hidePhone={
pathName === "/company/contact-us" || Boolean(mobile.hideContactButton)
}
isFlagVisible={!mobile.hideFlag}
hidePhone={pathName === "/company/contact-us"}
isFlagVisible={true}
menuBarItems={props.menu}
tagline="Enterprise Software Development"
linkComponent={(props) => (
Expand Down
17 changes: 17 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,20 @@
}
}
}

/*
* Mobile header appearance (CLS fix β€” see app/components/header-appearance.tsx).
* A page that opts out server-renders a hidden [data-mm-hide-flag] / [data-mm-hide-contact]
* marker; these rules hide the matching ssw.megamenu elements on first paint instead of
* after hydration. Selectors target ssw.megamenu@4.13.8 internals β€” revisit on upgrade.
* Left unlayered so they beat Tailwind's @layer utilities without !important.
* Degrades gracefully where :has() is unsupported (Firefox < 121): the flag/Contact
* button simply stay visible β€” no layout shift, just no opt-out.
*/
body:has([data-mm-hide-flag]) header .xl\:hidden button:has(img[alt$="flag" i]),
body:has([data-mm-hide-flag]) header .xl\:hidden .h-4.w-px {
display: none;
}
body:has([data-mm-hide-contact]) header .gap-2.sm\:flex-grow-0 {
display: none;
}
Loading