import React, { forwardRef, useCallback, useEffect, useRef, useState, } from "react"; import { BodyLong, BodyShort, Heading } from "../typography"; import { useId } from "../utils-external"; import { cl, createStrictContext } from "../utils/helpers"; import { useMergeRefs } from "../utils/hooks"; import { useI18n } from "../utils/i18n/i18n.hooks"; interface ProcessProps extends React.HTMLAttributes { /** * `` elements. */ children: React.ReactNode; /** * Hides the "aktiv"-text when the event is active. * @default false */ hideStatusText?: boolean; /** * Indicates that the process is truncated and that there are more Events * not shown either before, after or on both sides of the current list. */ isTruncated?: "start" | "end" | "both"; } type ProcessContextProps = Pick & { rootId: string; syncAriaControls: () => void; }; const { Provider: ProcessContextProvider, useContext: useProcessContext } = createStrictContext({ name: "ProcessContext", errorMessage: "`` must be used within a `` component.", }); interface ProcessComponent extends React.ForwardRefExoticComponent< ProcessProps & React.RefAttributes > { /** * @see 🏷️ {@link ProcessEventProps} */ Event: typeof ProcessEvent; } /** * A component that presents a Process as a vertical line of events. * Each event can contain information, actions, links or status indicators. * * @see [📝 Documentation](https://aksel.nav.no/komponenter/core/process) * @see 🏷️ {@link ProcessProps} * * @example * ```jsx * <> * * Søknadssteg * * * * } * > * Saksopplysninger er sendt inn * * *

Vedlegg er lastet opp

*

* Dokumentasjon av saksopplysninger er lastet opp og tilgjengelig for * saksbehandler. *

*
* * Det er gjort endelig vedtak i saken * *
* * ``` */ export const Process: ProcessComponent = forwardRef< HTMLOListElement, ProcessProps >( ( { children, className, hideStatusText = false, id, isTruncated, ...restProps }: ProcessProps, forwardedRef, ) => { const rootId = useId(id); const rootRef = useRef(null); const mergedRef = useMergeRefs(forwardedRef, rootRef); const [activeChildId, setActiveChildId] = useState( undefined, ); const syncAriaControls = useCallback(() => { const activeChildren = rootRef.current?.querySelectorAll( '[data-process-event][aria-current="true"]', ); if (!activeChildren) { setActiveChildId(undefined); return; } if (activeChildren.length > 1) { if (process.env.NODE_ENV !== "production") { console.warn( "Aksel: Found multiple `` elements with `status='active'`. Only one event should be active at a time.", rootRef.current, ); } setActiveChildId(undefined); return; } if (activeChildren.length === 1) { const lastActiveChild = activeChildren[activeChildren.length - 1]; setActiveChildId(lastActiveChild.id); } else { setActiveChildId(undefined); } }, []); return ( // `
    ` elements with `list-style: none;` tends to be ignored by voiceover on Safari. // To resolve this, we add `role="list"` to the `
      ` element. // eslint-disable-next-line jsx-a11y/no-redundant-roles
        {children}
      ); }, ) as ProcessComponent; /* ------------------------------ Process Event ------------------------------ */ interface ProcessEventProps extends React.HTMLAttributes { /** * Rich content to display under the title and timestamp if provided. */ children?: React.ReactNode; /** * Hide the content section of the event. */ hideContent?: boolean; /** * Step title. */ title?: string; /** * Timestamp or date to display for event. */ timestamp?: string; /** * Icon or number to display inside the bullet. */ bullet?: React.ReactNode; /** * Current event status. * @default "uncompleted" */ status?: "active" | "completed" | "uncompleted"; } export const ProcessEvent = forwardRef( ( { title, timestamp, children, bullet, hideContent, className, id, status = "uncompleted", ...restProps }: ProcessEventProps, forwardedRef, ) => { const translate = useI18n("Process"); const eventId = useId(); const { syncAriaControls, hideStatusText, rootId } = useProcessContext(); // syncAriaControls is already memoized with useCallback // biome-ignore lint/correctness/useExhaustiveDependencies: We want to run this only when status changes useEffect(syncAriaControls, [status]); // eslint-disable-line react-hooks/exhaustive-deps const isActive = status === "active"; return (
    1. {bullet}
      {title && {title}} {isActive && !hideStatusText && ( {translate("active")} )} {timestamp && {timestamp}} {!hideContent && !!children && ( {children} )}
    2. ); }, ); /* ------------------------------ Process Title ----------------------------- */ interface ProcessTitleProps { /** * Title content. */ children: React.ReactNode; } const ProcessTitle = ({ children }: ProcessTitleProps) => { return ( {children} ); }; /* ---------------------------- Process timestamp --------------------------- */ interface ProcessTimestampProps { /** * Timestamp content. */ children: React.ReactNode; } const ProcessTimestamp = ({ children }: ProcessTimestampProps) => { return ( {children} ); }; /* ----------------------------- Process Content ---------------------------- */ interface ProcessContentProps { /** * Content content. */ children: React.ReactNode; } const ProcessContent = ({ children }: ProcessContentProps) => { return ( {children} ); }; /* ----------------------------- Process Bullet ----------------------------- */ interface ProcessBulletProps { /** * Bullet content. */ children: React.ReactNode; } const ProcessBullet = ({ children }: ProcessBulletProps) => { return ( {children} ); }; /* ------------------------------ Process Line ------------------------------ */ type ProcessLineProps = { position?: "start" | "end"; }; const ProcessLine = ({ position }: ProcessLineProps) => { return ; }; /* -------------------------- Process exports ------------------------- */ Process.Event = ProcessEvent; export type { ProcessEventProps, ProcessProps };