'use client' import classNames from 'classnames' import { type ChangeEvent, type FocusEvent, ForwardedRef, forwardRef, type KeyboardEvent, type MutableRefObject, type RefObject, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { useElementWidth } from '../../hooks/useElementWidth' import { useScrollLock } from '../../hooks/useScrollLock' import { PktButton } from '../button/Button' import { PktHeaderUserMenu } from '../headerUserMenu/UserMenu' import { PktIcon } from '../icon/Icon' import { PktLink } from '../link/Link' import { PktTextinput } from '../textinput/Textinput' import { IPktHeader, Representing, THeaderMenu, User, UserMenuFooterItem, UserMenuItem } from './types' export type { Representing, THeaderMenu, User, UserMenuFooterItem, UserMenuItem } export type IPktHeaderService = IPktHeader export const PktHeaderService = forwardRef( ( { mobileBreakpoint = 768, tabletBreakpoint = 1280, children, className, compact = false, 'data-mode': dataMode, logOutButtonPlacement = 'none', logOut, openedMenu, showSearch = false, onSearch, onSearchChange, searchValue, searchPlaceholder = 'Søk', representing, serviceLink, serviceClick, serviceName, user, userMenu, userMenuFooter, userOptions, canChangeRepresentation = false, changeRepresentation, hideLogo = false, logoLink, logoClick, position = 'fixed', scrollBehavior = 'hide', slotMenuVariant = 'icon-only', slotMenuText = 'Meny', }: IPktHeaderService, ref: ForwardedRef, ) => { const isFixed = position === 'fixed' const shouldHideOnScroll = scrollBehavior === 'hide' // Deprecation warning for userMenuFooter useEffect(() => { if (userMenuFooter) { // eslint-disable-next-line no-console console.warn( 'PktHeaderService: The "userMenuFooter" prop is deprecated and will be removed in a future version. Please use "userMenu" instead.', ) } }, [userMenuFooter]) // Warning for userOptions - no longer available useEffect(() => { if (userOptions) { // eslint-disable-next-line no-console console.warn('PktHeaderService: The "userOptions" prop is no longer available. Please use "userMenu" instead.') } }, [userOptions]) // Deprecation warning for shortname useEffect(() => { if (user?.shortname) { // eslint-disable-next-line no-console console.warn( 'PktHeaderService: The "shortname" property on the user object is deprecated and will be removed in a future version.', ) } if (representing?.shortname) { // eslint-disable-next-line no-console console.warn( 'PktHeaderService: The "shortname" property on the representing object is deprecated and will be removed in a future version.', ) } }, [user?.shortname, representing?.shortname]) const formattedLastLoggedIn = useMemo(() => { if (!user?.lastLoggedIn) return undefined if (typeof user.lastLoggedIn === 'string') { return user.lastLoggedIn } return new Date(user.lastLoggedIn).toLocaleString('nb-NO', { year: 'numeric', month: 'long', day: 'numeric', }) }, [user?.lastLoggedIn]) const [openMenu, setOpenMenu] = useState<'none' | 'slot' | 'search' | 'user'>(openedMenu || 'none') useEffect(() => { if (openedMenu !== undefined) { setOpenMenu(openedMenu) } }, [openedMenu]) const [hidden, setHidden] = useState(false) const [lastScrollPosition, setLastScrollPosition] = useState(0) const [alignSlotRight, setAlignSlotRight] = useState(false) const [alignSearchRight, setAlignSearchRight] = useState(false) const internalRef = useRef(null) const userContainerRef = useRef(null) const slotContainerRef = useRef(null) const searchContainerRef = useRef(null) const slotContentRef = useRef(null) const searchMenuRef = useRef(null) const lastFocusedElementRef = useRef(null) const shouldRestoreFocusRef = useRef(false) const headerWidth = useElementWidth(internalRef) const isMobile: boolean = headerWidth < mobileBreakpoint const isTablet: boolean = headerWidth < tabletBreakpoint const setRefs = useCallback( (element: HTMLDivElement | null) => { ;(internalRef as MutableRefObject).current = element if (typeof ref === 'function') { ref(element) } else if (ref) { ;(ref as MutableRefObject).current = element } }, [ref], ) useScrollLock(isFixed && isMobile && openMenu !== 'none') const handleFocusOut = useCallback((event: FocusEvent, menuType: THeaderMenu) => { const relatedTarget = event.relatedTarget as HTMLElement | null let containerRef: RefObject switch (menuType) { case 'user': containerRef = userContainerRef break case 'slot': containerRef = slotContainerRef break case 'search': containerRef = searchContainerRef break default: return } const container = containerRef.current if (!container) return if (!relatedTarget || !container.contains(relatedTarget)) { setOpenMenu('none') } }, []) const restoreFocus = useCallback(() => { if ( shouldRestoreFocusRef.current && lastFocusedElementRef.current && document.contains(lastFocusedElementRef.current) ) { lastFocusedElementRef.current.focus() } lastFocusedElementRef.current = null shouldRestoreFocusRef.current = false }, []) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (user && openMenu === 'user' && !(event.target as Element).closest('.pkt-header-service__user-container')) { setOpenMenu('none') } if (openMenu === 'slot' && !(event.target as Element).closest('.pkt-header-service__slot-container')) { setOpenMenu('none') } if ( openMenu === 'search' && !(event.target as Element).closest('.pkt-header-service__search-container') && !(event.target as Element).closest('.pkt-header-service__search-input') ) { setOpenMenu('none') } } const handleEscapeKey = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape' && openMenu !== 'none') { event.preventDefault() shouldRestoreFocusRef.current = true setOpenMenu('none') } } if (openMenu !== 'none') { document.addEventListener('mousedown', handleClickOutside) document.addEventListener('keydown', handleEscapeKey) return () => { document.removeEventListener('mousedown', handleClickOutside) document.removeEventListener('keydown', handleEscapeKey) } } else { restoreFocus() } }, [openMenu, user, restoreFocus]) useEffect(() => { const onScroll = () => { if (shouldHideOnScroll) { const currentScrollPosition = window.pageYOffset || document.documentElement.scrollTop if (currentScrollPosition < 0) { return } if (Math.abs(currentScrollPosition - lastScrollPosition) < 60) { return } setHidden(currentScrollPosition > lastScrollPosition) setLastScrollPosition(currentScrollPosition) } } if (document) { window.addEventListener('scroll', onScroll) } return () => { if (document) { window.removeEventListener('scroll', onScroll) } } }, [shouldHideOnScroll, lastScrollPosition]) const checkDropdownAlignment = useCallback( (mode: 'slot' | 'search') => { const containerRef = mode === 'slot' ? slotContainerRef : searchContainerRef const dropdownRef = mode === 'slot' ? slotContentRef : searchMenuRef if (!containerRef.current || !dropdownRef.current || !isTablet || isMobile) return const buttonRect = containerRef.current.getBoundingClientRect() const dropdownWidth = dropdownRef.current.offsetWidth const wouldOverflow = buttonRect.left + dropdownWidth > window.innerWidth if (mode === 'slot') { setAlignSlotRight(wouldOverflow) } else { setAlignSearchRight(wouldOverflow) } }, [isTablet, isMobile], ) const handleMenuToggle = useCallback( (mode: THeaderMenu) => { if (openMenu !== 'none') { setOpenMenu('none') } else { lastFocusedElementRef.current = document.activeElement as HTMLElement setOpenMenu(mode) } }, [openMenu], ) useEffect(() => { if (openMenu === 'slot' || openMenu === 'search') { requestAnimationFrame(() => { checkDropdownAlignment(openMenu) }) } }, [openMenu, checkDropdownAlignment]) const headerElement = ( ) if (isFixed) { return (
{headerElement}
) } return headerElement }, ) PktHeaderService.displayName = 'PktHeaderService'