diff --git a/www/api-reference/app/api/[area]/layout.tsx b/www/api-reference/app/api/[area]/layout.tsx index e0f4d1b03e..31b226a0be 100644 --- a/www/api-reference/app/api/[area]/layout.tsx +++ b/www/api-reference/app/api/[area]/layout.tsx @@ -10,6 +10,7 @@ import { Roboto_Mono } from "next/font/google" import AnalyticsProvider from "@/providers/analytics" import NavbarProvider from "@/providers/navbar" import ModalProvider from "../../../providers/modal" +import { ScrollControllerProvider } from "../../../hooks/scroll-utils" export const metadata = { title: "Medusa API Reference", @@ -48,15 +49,17 @@ export default function RootLayout({ -
- -
- -
- {children} -
+ +
+ +
+ +
+ {children} +
+
-
+ diff --git a/www/api-reference/app/api/[area]/page.tsx b/www/api-reference/app/api/[area]/page.tsx index 2393269354..65604443d3 100644 --- a/www/api-reference/app/api/[area]/page.tsx +++ b/www/api-reference/app/api/[area]/page.tsx @@ -47,6 +47,7 @@ export function generateMetadata({ params: { area } }: ReferencePageProps) { return { title: `Medusa ${capitalize(area)} API Reference`, description: `REST API reference for the Medusa ${area} API. This reference includes code snippets and examples for Medusa JS Client and cURL.`, + metadataBase: process.env.NEXT_PUBLIC_BASE_URL, } } diff --git a/www/api-reference/components/Card/index.tsx b/www/api-reference/components/Card/index.tsx index d9fc6c3544..1f64589ae7 100644 --- a/www/api-reference/components/Card/index.tsx +++ b/www/api-reference/components/Card/index.tsx @@ -13,7 +13,7 @@ const Card = ({ title, text, href, className }: CardProps) => { return (
{ - const [selectedTab, setSelectedTab] = useState(tabs[0]) +const CodeTabs = ({ tabs, className, group = "client" }: CodeTabsProps) => { + const { selectedTab, changeSelectedTab } = useTabs({ + tabs, + group, + }) const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], []) const codeTabSelectorRef = useRef(null) const codeTabsWrapperRef = useRef(null) + const { blockElementScrollPositionUntilNextRender } = + useScrollPositionBlocker() const changeTabSelectorCoordinates = useCallback( (selectedTabElm: HTMLElement) => { @@ -79,18 +85,21 @@ const CodeTabs = ({ tabs, className }: CodeTabsProps) => {
) diff --git a/www/api-reference/components/Tags/Operation/CodeSection/index.tsx b/www/api-reference/components/Tags/Operation/CodeSection/index.tsx index 030ba7eec3..8fc42e5ce5 100644 --- a/www/api-reference/components/Tags/Operation/CodeSection/index.tsx +++ b/www/api-reference/components/Tags/Operation/CodeSection/index.tsx @@ -28,13 +28,15 @@ const TagOperationCodeSection = ({
- {endpointPath} + + {endpointPath} +
diff --git a/www/api-reference/hooks/scroll-utils.tsx b/www/api-reference/hooks/scroll-utils.tsx new file mode 100644 index 0000000000..8eb0b28055 --- /dev/null +++ b/www/api-reference/hooks/scroll-utils.tsx @@ -0,0 +1,332 @@ +"use client" + +/* Copied from Docusaurus to maintain scroll position on re-renders. Useful when content of the page is updated dynamically */ + +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, + type ReactNode, +} from "react" + +type EventFunc = (...args: never[]) => unknown + +export function useEvent(callback: T): T { + const ref = useRef(callback) + + useLayoutEffect(() => { + ref.current = callback + }, [callback]) + + // @ts-expect-error: TS is right that this callback may be a supertype of T, + // but good enough for our use + return useCallback((...args) => ref.current(...args), []) +} + +/** + * Gets `value` from the last render. + */ +export function usePrevious(value: T): T | undefined { + const ref = useRef() + + useLayoutEffect(() => { + ref.current = value + }) + + return ref.current +} + +type ScrollController = { + /** A boolean ref tracking whether scroll events are enabled. */ + scrollEventsEnabledRef: React.MutableRefObject + /** Enable scroll events in `useScrollPosition`. */ + enableScrollEvents: () => void + /** Disable scroll events in `useScrollPosition`. */ + disableScrollEvents: () => void +} + +function useScrollControllerContextValue(): ScrollController { + const scrollEventsEnabledRef = useRef(true) + + return useMemo( + () => ({ + scrollEventsEnabledRef, + enableScrollEvents: () => { + scrollEventsEnabledRef.current = true + }, + disableScrollEvents: () => { + scrollEventsEnabledRef.current = false + }, + }), + [] + ) +} + +const ScrollMonitorContext = React.createContext( + undefined +) + +export function ScrollControllerProvider({ + children, +}: { + children: ReactNode +}): JSX.Element { + const value = useScrollControllerContextValue() + return ( + + {children} + + ) +} + +/** + * We need a way to update the scroll position while ignoring scroll events + * so as not to toggle Navbar/BackToTop visibility. + * + * This API permits to temporarily disable/ignore scroll events. Motivated by + * https://github.com/facebook/docusaurus/pull/5618 + */ +export function useScrollController(): ScrollController { + const context = useContext(ScrollMonitorContext) + if (context == null) { + throw new Error( + `useScrollController must be used by elements in ScrollControllerProvider` + ) + } + return context +} + +type ScrollPosition = { scrollX: number; scrollY: number } + +const getScrollPosition = (): ScrollPosition | null => ({ + scrollX: window.pageXOffset, + scrollY: window.pageYOffset, +}) + +/** + * This hook fires an effect when the scroll position changes. The effect will + * be provided with the before/after scroll positions. Note that the effect may + * not be always run: if scrolling is disabled through `useScrollController`, it + * will be a no-op. + * + * @see {@link useScrollController} + */ +export function useScrollPosition( + effect: ( + position: ScrollPosition, + lastPosition: ScrollPosition | null + ) => void, + deps: unknown[] = [] +): void { + const { scrollEventsEnabledRef } = useScrollController() + const lastPositionRef = useRef(getScrollPosition()) + + const dynamicEffect = useEvent(effect) + + useEffect(() => { + const handleScroll = () => { + if (!scrollEventsEnabledRef.current) { + return + } + const currentPosition = getScrollPosition()! + dynamicEffect(currentPosition, lastPositionRef.current) + lastPositionRef.current = currentPosition + } + + const opts: AddEventListenerOptions & EventListenerOptions = { + passive: true, + } + + handleScroll() + window.addEventListener("scroll", handleScroll, opts) + + return () => window.removeEventListener("scroll", handleScroll, opts) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dynamicEffect, scrollEventsEnabledRef, ...deps]) +} + +type UseScrollPositionSaver = { + /** Measure the top of an element, and store the details. */ + save: (elem: HTMLElement) => void + /** + * Restore the page position to keep the stored element's position from + * the top of the viewport, and remove the stored details. + */ + restore: () => { restored: boolean } +} + +function useScrollPositionSaver(): UseScrollPositionSaver { + const lastElementRef = useRef<{ elem: HTMLElement | null; top: number }>({ + elem: null, + top: 0, + }) + + const save = useCallback((elem: HTMLElement) => { + lastElementRef.current = { + elem, + top: elem.getBoundingClientRect().top, + } + }, []) + + const restore = useCallback(() => { + const { + current: { elem, top }, + } = lastElementRef + if (!elem) { + return { restored: false } + } + const newTop = elem.getBoundingClientRect().top + const heightDiff = newTop - top + if (heightDiff) { + window.scrollBy({ left: 0, top: heightDiff }) + } + lastElementRef.current = { elem: null, top: 0 } + + return { restored: heightDiff !== 0 } + }, []) + + return useMemo(() => ({ save, restore }), [restore, save]) +} + +/** + * This hook permits to "block" the scroll position of a DOM element. + * The idea is that we should be able to update DOM content above this element + * but the screen position of this element should not change. + * + * Feature motivated by the Tabs groups: clicking on a tab may affect tabs of + * the same group upper in the tree, yet to avoid a bad UX, the clicked tab must + * remain under the user mouse. + * + * @see https://github.com/facebook/docusaurus/pull/5618 + */ +export function useScrollPositionBlocker(): { + /** + * Takes an element, and keeps its screen position no matter what's getting + * rendered above it, until the next render. + */ + blockElementScrollPositionUntilNextRender: (el: HTMLElement) => void +} { + const scrollController = useScrollController() + const scrollPositionSaver = useScrollPositionSaver() + + const nextLayoutEffectCallbackRef = useRef<(() => void) | undefined>( + undefined + ) + + const blockElementScrollPositionUntilNextRender = useCallback( + (el: HTMLElement) => { + scrollPositionSaver.save(el) + scrollController.disableScrollEvents() + nextLayoutEffectCallbackRef.current = () => { + const { restored } = scrollPositionSaver.restore() + nextLayoutEffectCallbackRef.current = undefined + + // Restoring the former scroll position will trigger a scroll event. We + // need to wait for next scroll event to happen before enabling the + // scrollController events again. + if (restored) { + const handleScrollRestoreEvent = () => { + scrollController.enableScrollEvents() + window.removeEventListener("scroll", handleScrollRestoreEvent) + } + window.addEventListener("scroll", handleScrollRestoreEvent) + } else { + scrollController.enableScrollEvents() + } + } + }, + [scrollController, scrollPositionSaver] + ) + + useLayoutEffect(() => { + // Queuing permits to restore scroll position after all useLayoutEffect + // have run, and yet preserve the sync nature of the scroll restoration + // See https://github.com/facebook/docusaurus/issues/8625 + queueMicrotask(() => nextLayoutEffectCallbackRef.current?.()) + }) + + return { + blockElementScrollPositionUntilNextRender, + } +} + +type CancelScrollTop = () => void + +function smoothScrollNative(top: number): CancelScrollTop { + window.scrollTo({ top, behavior: "smooth" }) + return () => { + // Nothing to cancel, it's natively cancelled if user tries to scroll down + } +} + +function smoothScrollPolyfill(top: number): CancelScrollTop { + let raf: number | null = null + const isUpScroll = document.documentElement.scrollTop > top + function rafRecursion() { + const currentScroll = document.documentElement.scrollTop + if ( + (isUpScroll && currentScroll > top) || + (!isUpScroll && currentScroll < top) + ) { + raf = requestAnimationFrame(rafRecursion) + window.scrollTo(0, Math.floor((currentScroll - top) * 0.85) + top) + } + } + rafRecursion() + + // Break the recursion. Prevents the user from "fighting" against that + // recursion producing a weird UX + return () => raf && cancelAnimationFrame(raf) +} + +/** + * A "smart polyfill" of `window.scrollTo({ top, behavior: "smooth" })`. + * This currently always uses a polyfilled implementation unless + * `scroll-behavior: smooth` has been set in CSS, because native support + * detection for scroll behavior seems unreliable. + * + * This hook does not do anything by itself: it returns a start and a stop + * handle. You can execute either handle at any time. + */ +export function useSmoothScrollTo(): { + /** + * Start the scroll. + * + * @param top The final scroll top position. + */ + startScroll: (top: number) => void + /** + * A cancel function, because the non-native smooth scroll-top + * implementation must be interrupted if user scrolls down. If there's no + * existing animation or the scroll is using native behavior, this is a no-op. + */ + cancelScroll: CancelScrollTop +} { + const cancelRef = useRef(null) + // Not all have support for smooth scrolling (particularly Safari mobile iOS) + // TODO proper detection is currently unreliable! + // see https://github.com/wessberg/scroll-behavior-polyfill/issues/16 + // For now, we only use native scroll behavior if smooth is already set, + // because otherwise the polyfill produces a weird UX when both CSS and JS try + // to scroll a page, and they cancel each other. + const supportsNativeSmoothScrolling = + getComputedStyle(document.documentElement).scrollBehavior === "smooth" + return { + startScroll: (top: number) => { + cancelRef.current = supportsNativeSmoothScrolling + ? smoothScrollNative(top) + : smoothScrollPolyfill(top) + }, + cancelScroll: () => cancelRef.current?.(), + } +} diff --git a/www/api-reference/hooks/use-client.ts b/www/api-reference/hooks/use-client.ts deleted file mode 100644 index 40924fb41a..0000000000 --- a/www/api-reference/hooks/use-client.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useEffect, useState } from "react" - -const useClient = () => { - const [isClient, setIsClient] = useState(false) - - useEffect(() => { - setIsClient(true) - }, []) - - return isClient -} - -export default useClient diff --git a/www/api-reference/hooks/use-height-observer.ts b/www/api-reference/hooks/use-height-observer.ts deleted file mode 100644 index df0ff3ddfa..0000000000 --- a/www/api-reference/hooks/use-height-observer.ts +++ /dev/null @@ -1,23 +0,0 @@ -"use client" - -import { useEffect } from "react" - -const useHeightObserver = () => { - useEffect(() => { - const storedHeight = window.localStorage.getItem("height") - if (storedHeight) { - document.body.style.height = storedHeight - } - - const resizeObserver = new ResizeObserver((entries) => { - window.localStorage.setItem( - "height", - `${entries[0].target.clientHeight}px` - ) - }) - - resizeObserver.observe(document.body) - }, []) -} - -export default useHeightObserver diff --git a/www/api-reference/hooks/use-tabs.ts b/www/api-reference/hooks/use-tabs.ts new file mode 100644 index 0000000000..dce0316628 --- /dev/null +++ b/www/api-reference/hooks/use-tabs.ts @@ -0,0 +1,92 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react" + +export type BaseTabType = { + label: string + value: string +} + +type EventData = { + storageValue: string +} + +type TabProps = { + tabs: T[] + group?: string +} + +function useTabs({ tabs, group }: TabProps) { + const [selectedTab, setSelectedTab] = useState(null) + const storageKey = useMemo(() => `tab_${group}`, [group]) + const eventKey = useMemo(() => `tab_${group}_changed`, [group]) + const scrollPosition = useRef(0) + + const changeSelectedTab = (tab: T) => { + scrollPosition.current = window.scrollY + setSelectedTab(tab) + localStorage.setItem(storageKey, tab.value) + window.dispatchEvent( + new CustomEvent(eventKey, { + detail: { + storageValue: tab.value, + }, + }) + ) + } + + const findTabItem = useCallback( + (val: string) => { + const lowerVal = val.toLowerCase() + return tabs.find((t) => t.value.toLowerCase() === lowerVal) + }, + [tabs] + ) + + const handleStorageChange = useCallback( + (e: CustomEvent) => { + if (e.detail.storageValue !== selectedTab?.value) { + // check if tab exists + const tab = findTabItem(e.detail.storageValue) + if (tab) { + setSelectedTab(tab) + } + } + }, + [selectedTab, findTabItem] + ) as EventListener + + useEffect(() => { + if (!selectedTab) { + const storedSelectedTabValue = localStorage.getItem(storageKey) + setSelectedTab( + storedSelectedTabValue + ? findTabItem(storedSelectedTabValue) || tabs[0] + : tabs[0] + ) + } + }, [selectedTab, storageKey, tabs, findTabItem]) + + useEffect(() => { + window.addEventListener(eventKey, handleStorageChange) + + return () => { + window.removeEventListener(eventKey, handleStorageChange) + } + }, [handleStorageChange, eventKey]) + + useLayoutEffect(() => { + if (scrollPosition.current && window.scrollY !== scrollPosition.current) { + window.scrollTo(0, scrollPosition.current) + } + }, [selectedTab]) + + return { selectedTab, changeSelectedTab } +} + +export default useTabs diff --git a/www/api-reference/tsconfig.json b/www/api-reference/tsconfig.json index 4afb83b9b4..58fb6302a8 100644 --- a/www/api-reference/tsconfig.json +++ b/www/api-reference/tsconfig.json @@ -38,8 +38,8 @@ "**/*.ts", "**/*.js", ".next/types/**/*.ts", - "next.config.mjs" -, "app/api/algolia/route.mjs" ], + "**/*.mjs" + ], "exclude": [ "node_modules", "public", diff --git a/www/tailwind.config.js b/www/tailwind.config.js index ed88b9c005..68f2f7cf85 100644 --- a/www/tailwind.config.js +++ b/www/tailwind.config.js @@ -447,7 +447,7 @@ module.exports = { bg: { base: { DEFAULT: "#111827", - dark: "#1B1B1B" + dark: "#1E1E1E" }, header: { DEFAULT: "#1F2937", @@ -547,7 +547,7 @@ module.exports = { "button-danger-pressed": "linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.16) 100%)", "button-danger-pressed-dark": "linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, rgba(255, 255, 255, 0.14) 100%)", "code-fade": "linear-gradient(90deg, #11182700, #111827 24px)", - "code-fade-dark": "linear-gradient(90deg, #1B1B1B00, #1B1B1B 24px)", + "code-fade-dark": "linear-gradient(90deg, #1E1E1E00, #1E1E1E 24px)", // TODO remove if not used "docs-button-neutral": "linear-gradient(180deg, #FFF 30.10%, #F8F9FA 100%)", "docs-button-neutral-dark": "linear-gradient(180deg, #2E2E32 0%, #28282C 32.67%)",