import ActionMenu from './ActionMenu'; import DeConsumerMessage from './DeConsumerMessage'; import Logo from '../Logo/Logo'; import Menu from './Menu'; import { useLayoutEffect, useRef, useState } from 'react'; import type * as React from 'react'; import { SkipNav, UsaBanner } from '@cmsgov/design-system'; import { t } from '../i18n'; import classnames from 'classnames'; import defaultMenuLinks from './defaultMenuLinks'; export interface Link { href: string; label: React.ReactNode; ariaLabel?: string; className?: string; onClick?: (...args: any[]) => any; } export interface HeaderProps { /** * Additional classes to be added to the root `
` element. */ className?: string; /** * For applications that handle their own locale switching. Overrides the * default locale link. This takes precedence over the `subpath` prop. */ switchLocaleLink?: string; /** * Indicate that a user is logged-in. */ loggedIn?: boolean; /** * When set to true, do not display the Login text in the upper right of the * header */ hideLoginLink?: boolean; /** * When set to true, even if logged in the Logout link will not render */ hideLogoutLink?: boolean; /** * When set to true, do not display the the switch locale link */ hideLanguageSwitch?: boolean; /** * For logged-in users, pass in their first name to display in the header */ firstName?: string; /** * For applications hosted at paths other than the root `healthcare.gov`/ * `cuidadodesalud.gov`. This string will be appended to the end of the * language links so as to keep the user within the same part of the site * when switching languages. */ subpath?: string; /** * The primary, or root domain where the majority of header links should be * hosted. By default, links render with relative paths, but providing this * prop will force all links to render with absolute paths based on the * provided string. The string should include the protocol (`http://` or * `https://`) and the domain only, with no trailing slash. For example, if * the provided string is `https://test.healthcare.gov`, the login link will * render as `https://test.healthcare.gov/login` instead of just `/login`. * Note that this is only really necessary if your application is hosted on a * subdomain, such as `https://localhelp.healthcare.gov`, where relative links * would direct the user to the wrong location, e.g. the link to `/login` * would incorrectly direct the user to * `https://localhelp.healthcare.gov/login` when it should direct the user to * `https://healthcare.gov/login`. */ primaryDomain?: string; /** * A URL hash used for the "Skip to main content" link. This is * typically the `id` of your "main" content area (Defaults to `'#main'`). */ skipNavHref?: string; /** * An onClick handler used for the "Skip to main content" link. This can * be used in cases where one would need to manually set the focus on the * content area (e.g. where hash routing is being used). */ onSkipNavClick?: (...args: any[]) => any; /** * Indicates when a consumer is coming from a Direct Enrollment flow. * This will include additional messaging and modify some of the links. */ deConsumer?: boolean; /** * Used in conjunction with `deConsumer`, the Direct Enrollment broker's * name is used in some of the messaging displayed to the consumer. */ deBrokerName?: string; /** * Optionally pass in an array of link objects to override the default * set of menu links. This may be useful if you need to customize the * links on a page-by-page basis. To reference the default set of menu * links, you can import the `defaultMenuLinks` method. */ links?: Link[]; /** * Optionally pass a React component to render within the menu. Useful for * when you need more control over what appears in the menu than what's * provided by the `links` prop, e.g. a search input. Will appear *above* any * links provided by the `defaultMenuLinks` method or the links provided by * the `links` prop. */ submenuTop?: React.ReactNode; /** * Same as `submenuTop`, except it will appear *below* any links provided by * the `defaultMenuLinks` method or the links provided by the `links` prop. */ submenuBottom?: React.ReactNode; /** * Element added to display content on Header bottom section */ headerBottom?: React.ReactNode; /** * Open and handler function for fully controlled menu behavior */ isMenuOpen?: boolean; onMenuToggle?: () => void; logoClassName?: string; loginLinkClassName?: string; languageLinkClassName?: string; } export const VARIATION_NAMES = { LOGGED_IN: 'logged-in', LOGGED_OUT: 'logged-out', } as const; /** * For information about how and when to use this component, * [refer to its full documentation page](https://design.cms.gov/components/header/healthcare-header/?theme=healthcare). */ export const Header = (props: HeaderProps) => { const { skipNavHref = '#main' } = props; const isControlledMenu = props.isMenuOpen !== undefined && props.onMenuToggle !== undefined; const [internalIsMenuOpenState, setInternalIsMenuOpenState] = useState(false); const isMenuOpen = isControlledMenu ? props.isMenuOpen : internalIsMenuOpenState; const menuToggleRef = useRef(null); const actionsRef = useRef(null); const isMenuOpenRef = useRef(isMenuOpen); useLayoutEffect(() => { isMenuOpenRef.current = isMenuOpen; }); /** * Opens or closes the menu. Called when the "Menu" or "Close" button * within ActionMenu is clicked, and to close the menu by other means. */ function toggleMenu() { if (!isControlledMenu) { setInternalIsMenuOpenState(!isMenuOpen); } props.onMenuToggle?.(); } /** * Closes the menu when the user presses Escape, moves focus away, or clicks * outside. One action can trigger more than one of these, e.g. clicking a * link outside the menu is both an outside click and a focus change, so * the ref ensures the menu is closed only once until the next render. */ function closeMenu() { if (!isMenuOpenRef.current) return; isMenuOpenRef.current = false; toggleMenu(); } /** * Closes the open menu when Escape is pressed within the header actions, * and returns focus to the "Menu" button. */ function handleActionsKeyDown(event: React.KeyboardEvent) { if (event.key === 'Escape' && isMenuOpen) { closeMenu(); menuToggleRef.current?.focus(); } } /** * Closes the open menu when focus moves to an element outside the header * actions. Focus leaving the page entirely, which has no related target, * leaves the menu open. */ function handleActionsBlur(event: React.FocusEvent) { const nextFocus = event.relatedTarget; if (isMenuOpen && nextFocus instanceof Element && !event.currentTarget.contains(nextFocus)) { closeMenu(); } } /** * While the menu is open, closes it when the user clicks or touches * anywhere outside the header actions. A layout effect attaches the * listener as soon as the menu opens; Preact runs plain effects after * paint, which would leave a gap where an outside click does nothing. */ useLayoutEffect(() => { if (!isMenuOpen) return; function handleClickOutside(event: MouseEvent | TouchEvent) { if (!actionsRef.current?.contains(event.target as Node)) { closeMenu(); } } document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleClickOutside); }; }); const variation = props.loggedIn ? VARIATION_NAMES.LOGGED_IN : VARIATION_NAMES.LOGGED_OUT; const classes = classnames(`hc-c-header hc-c-header--${variation}`, props.className); const hasCustomLinks = !!props.links; const defaultLinksForVariation = defaultMenuLinks({ deConsumer: props.deConsumer, subpath: props.subpath, primaryDomain: props.primaryDomain, switchLocaleLink: props.switchLocaleLink, hideLoginLink: props.hideLoginLink, hideLogoutLink: props.hideLogoutLink, hideLanguageSwitch: props.hideLanguageSwitch, customLinksPassedIn: hasCustomLinks, loginLinkClassName: props.loginLinkClassName, languageLinkClassName: props.languageLinkClassName, })[variation]; const links = hasCustomLinks ? props.links.concat(defaultLinksForVariation) : defaultLinksForVariation; const beforeMenuLinks = props.loggedIn && props.firstName ? (
{props.firstName}
) : undefined; return ( <> {t('header.skipNav')}
{props.deConsumer && } {props.headerBottom}
); }; export default Header;