import { ReactElement, ReactNode, useId, useState } from 'react'; import classnames from 'classnames'; import { useDeviceType } from './contexts/DeviceTypeProvider'; import { NestingProvider } from './contexts/NestingProvider'; import { useSideNavigation } from './contexts/SideNavigationProvider'; import icons from './icons/index'; import styles from './SideNavigation.css'; import SideNavigationGroupContent from './SideNavigation/GroupContent'; import SideNavigationGroupItemTapControl from './SideNavigation/GroupItemTapControl'; import SideNavigationGroupMobile from './SideNavigation/GroupMobile'; import { getChildrenActiveProp, validateChildren } from './SideNavigation/navigationChildrenUtils'; import { flattenChildrenWithKeys } from './utils/flattenChildren'; import { Indexable } from './zIndex'; type IconType = keyof typeof icons | { __path: string }; type Display = 'expandable' | 'static'; type BadgeType = { text: string; type?: 'info' | 'error' | 'warning' | 'success' | 'neutral'; }; type Counter = { number: string; accessibilityLabel: string; }; export type Props = { /** * When set to 'page' or 'section', it displays the item in "active" state. Available only when `href` prop is present. See the [Accessibility](https://gestalt.pinterest.systems/web/sidenavigation#Accessibility) guidelines to learn more. */ active?: 'page' | 'section'; /** * When supplied, will display a [Badge](https://gestalt.pinterest.systems/web/badge) next to the item's label. See the [Badges](https://gestalt.pinterest.systems/web/sidenavigation#Badge) variant to learn more. */ badge?: BadgeType; /** * Content of the group. See [nested directory](https://gestalt.pinterest.systems/web/sidenavigation#Nested-directory) variant for more information. */ children: ReactNode; /** * When supplied, will display a counter. See the [Counter](https://gestalt.pinterest.systems/web/sidenavigation#Counter) variant to learn more. */ counter?: Counter; /** * Nested directories can be static or expandable. See the [group display variant](https://gestalt.pinterest.systems/web/sidenavigation#Group-display) to learn more. */ display?: Display; /** * When passed SideNavigation.Group becomes a controlled component. If not passed, it stays uncontrolled. See the [controlled group display variant](https://gestalt.pinterest.systems/web/sidenavigation#Group-display) to learn more. This functionality is not supported in mobile. */ expanded?: boolean; /** * Directs users to the url when item is selected. Available only for Desktop. */ href?: string; /** * When supplied, will display Icon. See the [Icon](https://gestalt.pinterest.systems/web/sidenavigation#Icon) variant to learn more. */ icon?: IconType; /** * Label for the group. See [nested directory](https://gestalt.pinterest.systems/web/sidenavigation#Nested-directory) variant for more information. */ label: string; /** * When supplied, will display a notification dot. See the [Notification](https://gestalt.pinterest.systems/web/sidenavigation#Notification) variant to learn more. */ notificationAccessibilityLabel?: string; /** * Callback when the user selects an item using the mouse or keyboard. Available only when `href` prop is present. */ onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; dangerouslyDisableOnNavigation: () => void; }) => void; /** * Callback fired when the expand button component is clicked and the component is controlled.This functionality is not supported in mobile. */ onExpand?: (arg1: { expanded: boolean }) => void; /** * The primary action for each group. See the [primary action variant](https://gestalt.pinterest.systems/web/sidenavigation#Primary-action) to learn more. */ primaryAction?: { icon?: 'ellipsis' | 'edit' | 'trash-can'; onClick?: (arg1: { event: React.MouseEvent | React.KeyboardEvent; }) => void; tooltip: { accessibilityLabel?: string; text: string; zIndex?: Indexable; }; dropdownItems?: ReadonlyArray; }; }; /** * Use [SideNavigation.Group](https://gestalt.pinterest.systems/web/sidenavigation#SideNavigation.Group) to hold SideNavigation.NestedItem and SideNavigation.NestedGroup at the top level of SideNavigation. It supports badges, icons, counters, and notifications. */ export default function SideNavigationGroup({ active, badge, children, counter, display = 'expandable', expanded: expandedProp, icon, notificationAccessibilityLabel, label, onClick, onExpand, primaryAction, href, }: Props) { // Manages adaptiveness const deviceType = useDeviceType(); const isMobile = deviceType === 'mobile'; // Manages PrimaryAction const [compression, setCompression] = useState<'compress' | 'none'>('compress'); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); // Manages children const itemId = useId(); const { collapsed: sideNavigationCollapsed, overlayPreview, setOverlayPreview, selectedItemId, setSelectedItemId, } = useSideNavigation(); const navigationChildren = flattenChildrenWithKeys(children); const hasAnyActiveChild = !!getChildrenActiveProp(navigationChildren); validateChildren({ children: navigationChildren, filterLevel: 'nested' }); const isExpandable = display === 'expandable'; const isUncontrolled = expandedProp === undefined; const isLink = !!href; const [expanded, setExpanded] = useState(hasAnyActiveChild || !isExpandable); const isExpanded = isUncontrolled ? expanded : expandedProp; const collapsed = sideNavigationCollapsed && !overlayPreview; const handleExpand = () => { if (isUncontrolled) { setExpanded((value) => !value); } else { onExpand?.({ expanded: !isExpanded }); } }; const handleTap = () => { // Always `true` when item is not a link. // When item is a link and not expanded, allows expanding in the first click. const shoudlToggleExpand = !isLink || (isLink && !isExpanded); if (shoudlToggleExpand) { handleExpand(); } if (!isExpanded && selectedItemId !== itemId) setSelectedItemId(itemId); if (collapsed) { setOverlayPreview(true); } }; if (isMobile) { return ( {navigationChildren} ); } return (
  • setFocused(false)} onFocus={() => setFocused(true)} onLinkClick={onClick} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onTap={handleTap} tapStyle={compression} > {!collapsed && isExpanded ? (
      {navigationChildren}
    ) : null}
  • ); } SideNavigationGroup.displayName = 'SideNavigation.Group';